diff --git a/AGENTS.md b/AGENTS.md index 6c174db60..d3568a014 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,6 +28,7 @@ dapr/ # Core SDK package (single PyPI dist: `pip install ├── flask/ # Flask integration ← see dapr/ext/flask/AGENTS.md (`pip install dapr[flask]`) ├── grpc/ # gRPC App extension ← see dapr/ext/grpc/AGENTS.md (`pip install dapr[grpc]`) ├── langgraph/ # LangGraph checkpointer ← see dapr/ext/langgraph/AGENTS.md (`pip install dapr[langgraph]`) + ├── rag/ # Durable RAG ingestion ← see dapr/ext/rag/AGENTS.md (`pip install dapr[rag,...]`) ├── strands/ # Strands agent sessions ← see dapr/ext/strands/AGENTS.md (`pip install dapr[strands]`) └── workflow/ # Workflow authoring ← see dapr/ext/workflow/AGENTS.md (`pip install dapr[workflow]`) @@ -62,6 +63,7 @@ Extensions are bundled into the core `dapr` wheel and exposed as installable ext | `dapr[flask]` | `dapr.ext.flask` | Flask integration for pub/sub and actors (legacy `flask_dapr` import path is a deprecated shim) | Low | | `dapr[langgraph]` | `dapr.ext.langgraph` | LangGraph checkpoint persistence to Dapr state store | Moderate | | `dapr[strands]` | `dapr.ext.strands` | Strands agent session management via Dapr state store | New | +| `dapr[rag]` | `dapr.ext.rag` | Durable RAG ingestion pipeline on Dapr Workflow (S3/Azure sources, pgvector/Pinecone stores) | New | The previously-separate distributions (`dapr-ext-*`, `flask-dapr`) are no longer published. `dapr/__init__.py` emits a `FutureWarning` if it detects a legacy install at import time; see `RELEASE.md` for the migration recipe. @@ -109,6 +111,7 @@ uv run python -m unittest discover -v ./tests/ext/grpc uv run python -m unittest discover -v ./tests/ext/fastapi uv run python -m unittest discover -v ./tests/ext/langgraph uv run python -m unittest discover -v ./tests/ext/strands +uv run python -m unittest discover -v ./tests/ext/rag # pytest-style suites: uv run pytest -m "not e2e" ./tests/ext/workflow/durabletask/ diff --git a/dapr/ext/rag/AGENTS.md b/dapr/ext/rag/AGENTS.md new file mode 100644 index 000000000..e79cc023c --- /dev/null +++ b/dapr/ext/rag/AGENTS.md @@ -0,0 +1,438 @@ +# AGENTS.md — dapr.ext.rag + +The RAG extension provides `DurableRAGPipeline`: a Dapr Workflow orchestration that discovers +documents from a cloud object store, parses and chunks them, generates embeddings, and writes +them into a versioned vector index -- surviving throttling, process crashes, and pod restarts by +resuming from completed work rather than reprocessing everything, and never exposing a +partially-built index to queries. + +## Source layout + +``` +dapr/ext/rag/ +├── __init__.py # Public API exports (see below) +├── py.typed +├── models.py # Every typed dataclass shared across this extension +├── errors.py # RagError hierarchy; RetryableError vs NonRetryableError +├── fingerprints.py # Deterministic SHA-256 hashing (content, config, chunk IDs) +├── _wire.py # dataclass <-> dict conversion at the activity boundary +├── splitting.py # DocumentSplitter ABC + TextSplitter +├── state.py # PipelineStateStore: all Dapr state access, incl. the +│ ETag-guarded ActivationRecord CAS (see "Version activation") +├── retrieval.py # ActiveVersionResolver: resolve + query, for reader processes +├── generation.py # AzureOpenAIChatClient: query-time cited-answer generation +├── pipeline.py # DurableRAGPipeline: orchestrator, activities, public API +├── triggers.py # S3/Azure event-notification parsing + SourceChangeEvent +│ normalization + EventDeduplicator (for pub/sub triggers) +├── testing.py # FailureInjector (demo/test-only crash simulation) +├── sources/ +│ ├── base.py # DocumentSource ABC +│ ├── s3.py # S3Source (boto3, optional) +│ └── azure_blob.py # AzureBlobSource (azure-storage-blob/-identity, optional) +├── parsing/ +│ ├── base.py # DocumentParser ABC +│ ├── unstructured.py # UnstructuredParser (unstructured, optional) +│ └── langchain.py # to/from_langchain_documents (langchain-core, optional) +├── embedding/ +│ ├── base.py # Embedder ABC +│ ├── _openai_common.py # Shared OpenAI/Azure OpenAI response parsing + error +│ │ classification + Azure client construction +│ ├── openai.py # OpenAIEmbedder (openai, optional) +│ └── azure_openai.py # AzureOpenAIEmbedder (openai + azure-identity, optional) +└── vector_stores/ + ├── base.py # VectorIndex ABC (+ activate_version, see below) + ├── pgvector.py # PgVectorStore (psycopg, optional) + ├── pinecone.py # PineconeVectorStore (pinecone, optional) + └── azure_ai_search.py # AzureAISearchVectorStore (azure-search-documents + + # httpx, optional), incl. register_foundry_iq_ + # knowledge_source -- see "Azure AI Search: index + # aliases are REST-only" and "Foundry IQ" below + # before touching this file + +tests/ext/rag/ # Unit tests, one file per module above (unittest.TestCase, mocked I/O), + # plus a real, opt-in `pytest.mark.e2e` profile -- see "Testing" below +examples/rag/ # Runnable worker + CLI + failure demo + pub/sub trigger example +``` + +Installed via the `rag` extra plus whichever adapters you need, e.g. +`pip install "dapr[rag,rag-s3,rag-pgvector]"` or `pip install "dapr[rag,rag-azure,rag-pinecone]"`. + +## Architecture + +``` +┌────────────────────────────────────────────────────────────────────┐ +│ DurableRAGPipeline (pipeline.py) │ +│ .start()/.activate_version() → DaprWorkflowClient.schedule_* │ +│ .get_status()/.resolve_active_version() → PipelineStateStore reads│ +└───────────────────────────────┬──────────────────────────────────────┘ + │ schedules +┌───────────────────────────────▼──────────────────────────────────────┐ +│ rag_ingest orchestrator (deterministic generator; _orchestrate_ingestion) +│ 1. discover_and_manifest (once) 5. validate_version │ +│ 2. get_manifest_batch (per page) 6. activate_version │ +│ 3. process_document x N (bounded) 7. publish_activation_event │ +│ 4. update_status 8. continue_as_new per batch │ +└───────────────────────────────┬──────────────────────────────────────┘ + │ call_activity (all I/O lives here) + ┌───────────────────────┼────────────────────────┬─────────────────┐ + ▼ ▼ ▼ ▼ + DocumentSource DocumentParser + Embedder VectorIndex + (S3 / Azure Blob) DocumentSplitter (OpenAI) (pgvector / Pinecone) + │ │ + └──────────────────────────► PipelineStateStore (state.py) ◄────────┘ + manifest pages, completion records, embed progress, + pipeline status, ETag-guarded activation pointer +``` + +Every arrow crossing into an adapter or `PipelineStateStore` happens **inside a workflow +activity** (`pipeline.py`'s `_activity_*` methods) -- never in the orchestrator generator, which +must stay deterministic (see `dapr/ext/workflow/AGENTS.md`'s determinism rules). The orchestrator +never reads `datetime.now()` (it uses `ctx.current_utc_datetime`), never calls an adapter +directly, and never reads or writes Dapr state directly. + +## Why these adapter interfaces, and why `query()` was added + +`sources/base.py`, `parsing/base.py`, `vector_stores/base.py` follow the shapes sketched in this +extension's design brief (`list_documents`/`get_document`/`get_metadata`, +`parse(content, ...)`, `upsert`/`delete_document`/`validate_version`), adapted in two ways: + +- `DocumentParser.parse` takes the richer `SourceDocument` rather than just `SourceMetadata`, + since a parser needs the document's *name* (for extension-based format detection), which lives + on `SourceDocument`, not `SourceMetadata`. +- `VectorIndex` gained a `query()` method beyond the brief's write-only sketch: retrieval (the + sample CLI's `query` command, and `retrieval.ActiveVersionResolver`) needs *some* similarity + search, and adding it to the ABC is what keeps that search provider-agnostic rather than + branching on `pgvector` vs. `pinecone` in a reader. +- `VectorIndex` also gained `activate_version(version, *, previous_version)`, a no-op by default. + For pgvector/Pinecone, the Dapr-state `ActivationRecord` alone *is* activation (see "Version + activation" below). `AzureAISearchVectorStore` overrides it because, there, a version is a whole + separate physical index rather than a column/namespace -- see "Azure AI Search: index aliases are + REST-only, not SDK-only" below for what that override actually does and why it exists at all. + +No `dapr.ext.rag` component existed before this extension, and there is no separate +"Dapr Agents" package in this repository to reuse from -- these ABCs, `OpenAIEmbedder`, etc. were +written fresh, following this repo's conventions (dataclasses, `logging.getLogger(__name__)`, +per-adapter optional-import guards). + +## Public API + +```python +from dapr.ext.rag import ( + DurableRAGPipeline, PipelineConfig, + S3Source, AzureBlobSource, + UnstructuredParser, TextSplitter, OpenAIEmbedder, + PgVectorStore, PineconeVectorStore, + ActiveVersionResolver, +) + +pipeline = DurableRAGPipeline( + source=S3Source(bucket='company-docs', prefix='policies/'), + parser=UnstructuredParser(), + splitter=TextSplitter(chunk_size=1000, chunk_overlap=150), + embedder=OpenAIEmbedder(model='text-embedding-3-small'), + vector_store=PgVectorStore(connection_string='...', collection='company-knowledge'), + state_store_name='rag-pipeline-state', +) +pipeline.run_worker() # in the worker process; a client-only process skips this +instance_id = pipeline.start(version='2026-09', activate_when_complete=True) +status = pipeline.get_status('2026-09') +active = pipeline.resolve_active_version() +``` + +See `examples/rag/README.md` for the full worker/CLI/failure-demo walkthrough and +`docs/rag/README.md` for architecture, configuration, and operational documentation. + +## Optional-dependency guard pattern (deliberately *not* the langgraph/strands pattern) + +`dapr.ext.langgraph`/`dapr.ext.strands` each wrap exactly one third-party SDK, so they guard the +import once, in `__init__.py` (try/except around the whole implementation import, re-raising with +an install hint if the *specific* missing module matches). `dapr.ext.rag` bundles **several +independent** optional adapters (boto3, azure-storage-blob/-identity, openai, psycopg, pinecone, +unstructured, langchain-core) -- guarding only in `__init__.py` would mean the first missing +package breaks `import dapr.ext.rag` entirely, even for a user who only needs pgvector. + +So each adapter module guards its own import instead, e.g. `sources/s3.py`: +```python +try: + import boto3 +except ImportError: + boto3 = None +``` +and raises `OptionalDependencyError` lazily, from the class's `__init__`, only when neither the +package nor an injected `client=`/`connection_factory=` is available. `dapr/ext/rag/__init__.py` +therefore imports every adapter module unconditionally, and always succeeds regardless of which +optional packages are installed. + +## Idempotency and recovery (the core value proposition) + +State store key schema (see `state.py` for the exact key builders): + +| Key pattern | Contents | +|---|---| +| `rag:manifest:{pipeline_id}:{version}:meta` | `ManifestSummary` + page/document counts | +| `rag:manifest:{pipeline_id}:{version}:page:{n}` | One page of `DocumentWorkItem` (also the fan-out batch source) | +| `rag:completion:{pipeline_id}:{version}:{document_id}` | `CompletionRecord` -- the idempotency check | +| `rag:embed-progress:{pipeline_id}:{version}:{document_id}` | `EmbedProgressRecord` -- which embedding batches are durably upserted | +| `rag:attempts:{pipeline_id}:{version}:{document_id}` | Attempt counter, for the retry-count metric | +| `rag:status:{pipeline_id}:{version}` | `PipelineStatus` -- what `get_status()` reads | +| `rag:activation:{pipeline_id}` | `ActivationRecord`, ETag-guarded | + +Recovery relies on two independent mechanisms: + +1. **durabletask's own crash recovery.** Orchestration state lives in the Dapr-managed backend, + not the worker process. If the worker process crashes mid-run, restarting it (same instance ID) + reconnects and durabletask redelivers any work item that didn't record a result -- no + application code needed. This is what `examples/rag/failure_demo.py` demonstrates: kill the + worker, restart it, watch the same instance resume. +2. **Per-document idempotency in Dapr state**, for the case durabletask's own recovery doesn't + cover: a brand-new workflow instance re-processing the same `version` (e.g. after a prior + instance reached a terminal FAILED/TERMINATED state). `process_document` checks + `CompletionRecord` (keyed by `source_content_hash` + `pipeline_fingerprint`) before doing any + real work, and checks `EmbedProgressRecord` per embedding batch before re-embedding. A + completion record is written **only after** its vectors are durably upserted -- see + `pipeline.py::_process_one_document`'s comment on write ordering -- so "crashed after embedding, + before recording completion" always re-embeds at most one in-flight batch, never silently loses + or duplicates a whole document's work. Vector upserts are idempotent through the deterministic + chunk IDs computed by `fingerprints.compute_chunk_id`. + +**Retryable vs. non-retryable classification is hand-rolled, deliberately.** `dapr.ext.workflow`'s +public `RetryPolicy` has no supported way to mark an exception type non-retryable (the vendored +durabletask engine has one, `NonRetryableError`/`non_retryable_error_types`, but it isn't +re-exported and importing `_durabletask` from outside the extension is unsupported -- see +`dapr/ext/workflow/AGENTS.md`). So `errors.py` defines its own `RetryableError`/`NonRetryableError` +split, and `pipeline.py::_activity_process_document` catches accordingly: a `RetryableError` +re-raises (engaging the activity's `RetryPolicy`); a `NonRetryableError` is caught and turned into +a `DocumentOutcome(status='failed', retryable=False)` so the activity returns normally instead of +burning retry budget on a failure retrying can't fix. + +**Bounded fan-out without `when_all`.** The orchestrator schedules an entire manifest page's +worth of `process_document` calls up front (the fan-out, bounded to `max_concurrent_documents`), +then `yield`s each task *individually* in a loop rather than via `wf.when_all(...)`. This is +deliberate: `process_document` never raises for an *expected* failure (see above), so a raised +exception at that point only ever means retries were exhausted or something truly unexpected +happened -- and because every task was already scheduled together, every other document in the +batch has already run to completion (including recording its own state) by the time we get there. +Catching per-task lets `fail_fast=False` (the default) collect that batch's results and move on +without one document's failure discarding its siblings' outcomes. + +**History size**: after each batch, the orchestrator calls `ctx.continue_as_new(...)` with a small, +flat cursor state (`_IngestionState`) rather than accumulating the manifest or every batch's +results in workflow history -- the same pattern `examples/workflow/monitor.py` uses for an eternal +polling workflow. History size per generation stays bounded regardless of total corpus size. + +**Dataclasses, not pydantic, across the activity boundary.** `dapr.ext.workflow`'s automatic +input/output coercion (`_model_protocol.py`) only recognizes pydantic-v2-shaped classes +(`model_dump`/`model_validate`). This extension uses plain `@dataclass` types throughout, matching +the rest of the SDK, so `_wire.py`'s `to_wire`/`from_wire` do that conversion explicitly at each +`call_activity`/`continue_as_new` boundary. Every dataclass that crosses that boundary must stay +*flat* (JSON-primitive fields only) -- see `_wire.py`'s module comment for why a nested dataclass +field wouldn't survive the round trip. + +## Version activation + +`ActivationRecord` (in `models.py`) is a single JSON value at `rag:activation:{pipeline_id}`, +updated via a plain ETag-conditional `save_state` (not a multi-key transaction) -- deliberately, +so activation only requires the widely-supported per-key ETag/concurrency state-store capability, +not the less commonly supported multi-key transaction one. A conflicting concurrent write raises +Dapr's `ABORTED` status, which `state.py::write_activation` turns into `ActivationConflictError` +(a `RetryableError` -- re-running the activity re-reads the current ETag, so the workflow's own +`RetryPolicy` resolves the race without a bespoke inner loop). Repeated activation of the same +`(version, manifest_hash)` is a no-op (`_activity_activate_version` checks before writing). +`retrieval.ActiveVersionResolver` is the read-side counterpart: it reads the pointer with +`consistency: strong` and queries only the resolved version, so a reader never sees a +partially-built one. + +## Azure AI Search: index aliases are REST-only, not SDK-only + +`AzureAISearchVectorStore.activate_version` needs to atomically repoint an alias +(`{index_base_name}-active`) from one physical index to another -- but `azure-search-documents`' +`SearchIndexClient` has **no alias method at all**, in any installed or currently-released version. + +This was not a deliberate design choice; it was discovered mid-implementation. Index aliases were +briefly in the SDK as a beta feature (`11.4.0b1`) but were removed before the stable `11.4.0` +release and have not been restored in any version since, up to and including `12.1.0b2` (verified +against the SDK's own `CHANGELOG.md` on 2026-09-10). The symptom, if you hit this again after an +SDK upgrade: importing `SearchAlias` from `azure.search.documents.indexes.models` raises +`ImportError` -- and because that import used to sit inside the same guarded `try/except ImportError` +block as every other schema/client class this module needs, *that one missing name silently made +the entire block fall back to its `except` branch*, setting `SimpleField` and friends to `None` even +though `azure-search-documents` genuinely was installed. That, in turn, made every +`AzureAISearchVectorStore` test using the real SDK skip via `_HAS_REAL_SDK`, for a reason that had +nothing to do with what those tests actually exercise. If a "why is everything skipping" mystery +like that shows up again, check for exactly this pattern before assuming the package isn't +installed: `python -c "import azure.search.documents.indexes.models"` and read `ImportError.name` +to find which single missing name is poisoning the whole block, rather than trusting the `except` +branch's own claim about why it was taken. + +The fix: `activate_version`, `_alias_indexes`, and `_put_alias` talk to the Search REST API's +`/aliases('{name}')` operations directly via an injectable `httpx`-compatible transport +(`alias_transport=`), instead of going through `SearchIndexClient` at all -- verified against +Microsoft's REST API reference for `searchservice.aliases.createorupdate` on 2026-09-10. +Everything else this class does (index CRUD, document upsert/query) still goes through the real +SDK, since that surface *is* fully supported -- only alias management is REST-only. Auth for the +REST calls reuses whatever credential the constructor already resolved (`api-key` header for an +`AzureKeyCredential`, a bearer token via `credential.get_token(...)` otherwise), so callers never +configure Search auth twice. This is also why `endpoint` became a required constructor parameter +even when `index_client`/`search_client_factory` are fully injected: the alias REST calls need it +regardless of whether the SDK client was constructed at all. + +Since there's no local Azure AI Search emulator, `AzureAISearchVectorStoreActivateVersionTest` +exercises this purely against `_FakeAliasTransport` (a `dict`-backed stand-in for the REST +endpoint) in `tests/ext/rag/test_vector_stores_azure_ai_search.py` -- it needs no real SDK and is +not `_HAS_REAL_SDK`-gated, unlike the schema-creation and query tests in the same file. + +## Foundry IQ + +`register_foundry_iq_knowledge_source` (a method on `AzureAISearchVectorStore`, wired into +`DurableRAGPipeline` as an opt-in constructor param, `foundry_iq_knowledge_source=`) -- +`docs/rag/foundry-iq.md`. A workflow activity (`_activity_register_foundry_iq_knowledge_source`) +chained strictly after `activate_version` succeeds, in both `_orchestrate_ingestion` and +`_orchestrate_activation`. Best-effort like `_activity_publish_activation_event` -- a failure is +logged, never raised. `DurableRAGPipeline.__init__` rejects `foundry_iq_knowledge_source=` at +construction time (a `ValueError`, not a later activity failure) when `vector_store` doesn't +have this method, via `hasattr` -- checked once at startup rather than the activity discovering +it at run time. Like the alias calls in the section above, this talks to the Search REST API +directly (reusing the exact same `httpx` transport/credential/headers): Microsoft's own docs +illustrate knowledge-source creation as an `azure-search-documents` SDK call, but +`SearchIndexKnowledgeSource`/`SearchIndexKnowledgeSourceParameters` do not exist in +`azure-search-documents` 11.6.0 (confirmed by introspecting the installed package on +2026-09-10) -- the same SDK-lags-the-REST-API situation as aliases, discovered while +implementing this method. + +**No `AzureSearchIntegratedVectorizationPipeline`.** Azure AI Search's own "integrated +vectorization" (indexers + skillsets that chunk/embed as part of indexing) was briefly +implemented as a class here and deliberately removed: it has no Dapr Workflow, Dapr client, or +Dapr sidecar involvement whatsoever (it is plain `azure-search-documents` SDK orchestration), and +shipping it as part of this Dapr extension misrepresented it as inheriting `DurableRAGPipeline`'s +durability guarantees, which it does not and cannot -- an indexer run's own internal +retry/recovery behavior is opaque to the caller, nothing like the per-document/per-batch +`CompletionRecord`/`EmbedProgressRecord` tracking below. `docs/rag/integrated-vectorization +-alternative.md` still documents it as a legitimate alternative *architecture* for teams who +decide they don't want Dapr Workflow's guarantees at all -- but as a design note to build from +scratch outside this package if you want it, not as shipped code here. + +## Testing + +```bash +uv run python -m unittest discover -v ./tests/ext/rag +``` + +Every adapter accepts an injectable client/connection (`client=`, `connection_factory=`, +`partition_fn=`, `index_client=`/`search_client_factory=`/`alias_transport=`) specifically so tests +exercise the real adapter logic against a mock/fake without needing live network access to the +corresponding third-party service -- mirroring `tests/ext/strands/test_session_manager.py`'s +`@mock.patch(...DaprClient)` pattern, applied per adapter instead of per extension. Every optional +package (`boto3`, `azure-*`, `openai`, `psycopg`, `pinecone`, `unstructured`, `langchain_core`) is +installed in the `dev` dependency group (via the `rag`/`rag-*` extras folded into `dapr[all]`), so +the full test run does exercise each adapter's real SDK types/classes -- what none of it does is +real network I/O: `S3Source` uses `botocore.stub.Stubber` against real `boto3` calls, and everything +else drives injected fakes end to end (see `_FakeAliasTransport` in +`test_vector_stores_azure_ai_search.py` for the least trivial example). A machine without any of +these packages installed still passes the full suite too -- each adapter's `OptionalDependencyError` +path is tested by patching its guarded import to `None`, not by uninstalling anything. + +**`unstructured` is the one exception, and only on Windows**: `all` (`pyproject.toml`) excludes +`rag-unstructured` there via `sys_platform != 'win32'`. `unstructured` pulls in `python-magic`, +which needs a real libmagic to sniff file types -- Windows has none, and `python-magic`'s own +compat shim (`magic/compat.py`) crashes the whole interpreter with a native access violation +instead of raising a catchable `ImportError` when it can't find one. That crash happens at *import* +time, so it took down every test in the process, not just `UnstructuredParser`'s own. The standard +pip-installable Windows workaround, `python-magic-bin`, isn't a real fix: it installs its own +`magic/__init__.py` at the same path as `python-magic`'s, predates and lacks `compat.py` entirely, +and hasn't been released since 2017 -- pairing the two risks an install-order-dependent file +collision, not a working combination. So on Windows, `UnstructuredParser`'s tests exercise the +already-tested `OptionalDependencyError` fallback path (same mechanism as the "not installed" case +above), not real parsing -- `unstructured` genuinely isn't installed there, not simulated. + +**A separate, real, opt-in integration profile also exists**, marked `pytest.mark.e2e` (excluded +from `-m "not e2e"`, the flag every documented full-suite command in this file and the root +`AGENTS.md` already passes -- and silently uncollected by `unittest discover` too, since both +files use plain pytest functions with fixtures rather than `TestCase` subclasses, matching the +existing `tests/ext/flask`/`tests/ext/workflow/durabletask` caveat): + +- `test_pipeline_integration.py` -- a full `DurableRAGPipeline` run through a real `S3Source` + (LocalStack), a real `PgVectorStore` (a `pgvector/pgvector` Postgres image), and a real Dapr + Workflow sidecar, then a real query through `ActiveVersionResolver`. Three scenarios, each + proving something distinct real infrastructure can catch that fakes can't: + - `test_ingests_activates_and_is_queryable_end_to_end` -- the basic happy path. + - `test_a_second_run_of_unchanged_content_skips_every_document` -- a full *second* run of an + *already-completed* version skips re-embedding (`CompletionRecord`'s idempotency holding + across independent runs). + - `test_a_real_worker_process_crash_mid_run_resumes_from_where_it_left_off` -- the literal + headline scenario: a real worker *process* (`_crash_resume_worker.py`, launched via + `subprocess.Popen`, not simulated in-process) is hard-exited by `FailureInjector` + (`os._exit(70)`) partway through a 10-document run, and a second, fresh worker process + resumes the *same* Dapr Workflow instance and finishes it, with `embedding_requests` + proving none of the pre-crash documents were re-embedded. Distinct from the "second run" + test above: that one proves idempotency *across* independent runs; this one proves resuming + *the same in-flight run*, which is what durabletask's own crash recovery actually is. + - Fakes only the embedder and parser (a hash-based `DeterministicEmbedder`, a `.txt`-only + `PlainTextParser`, both in `_rag_integration_fixtures.py`, shared with the subprocess + worker), since OpenAI/`unstructured` wire compatibility is already covered elsewhere against + mocks -- what this file proves is durability, not those two adapters' own logic. +- `test_sources_azure_blob_integration.py` -- `AzureBlobSource` against a real Azurite container. + +Each file's module comment has the exact `docker run` commands and `uv run pytest ... -m e2e` +invocation. Building and running these surfaced two genuine bugs: + +1. (`_activity_update_status`'s comment in `pipeline.py`): re-running a version's stable-instance-ID + under a *different* explicit `instance_id` (an intentionally-supported `DurableRAGPipeline.start()` + capability, not just the default resume-with-the-same-ID path) left `PipelineStatus` permanently + attributed to whichever instance first wrote it, silently accumulating counts across independent + runs instead of resetting for the new one -- invisible to every existing unit test because none of + them exercised two full runs of one version under two different instance IDs against a real, + persistent-across-calls state store. +2. A test-harness-only bug in `_spawn_crash_test_worker` (`test_pipeline_integration.py`), not in + `dapr.ext.rag` itself: its original readiness check assumed a spawned worker's *first* stdout + line was always its own `CRASH_TEST_WORKER_READY` marker, and on a mismatch called a plain + blocking `.read()` to capture "the rest" for an error message. A worker process that starts + successfully never closes stdout (it keeps running to serve work items), so that `.read()` + call -- waiting for EOF that will never come -- hung forever the one time something else + legitimately got printed first, with no timeout to save it. Fixed by reading output on a + background thread into a queue the caller polls against a deadline, which cannot block past + that deadline regardless of what the child process does. A `sample`/stack-trace of the stuck + process (blocked in `_io_FileIO_readall_impl` -> `read`) is what found this, after the failure + mode itself -- reproducible only when all three tests in the file ran together, not any pair + of them -- ruled out both a one-off fluke and a `dapr.ext.rag` bug. + +## Key details + +- **`pipeline_id` defaults to `vector_store.target_index_name`** (the collection/index name) if + not given explicitly, and namespaces every workflow/activity registration name + (`rag_ingest__{pipeline_id}`, etc.) so multiple `DurableRAGPipeline` instances can share one + process without name collisions. +- **Activities are plain closures, not bound methods, at registration time.** `WorkflowRuntime` + registration (and `call_activity` given a function rather than a string) can stamp a + `_dapr_alternate_name` attribute onto the registered callable; bound methods don't support + arbitrary attribute assignment. `pipeline.py::_register_workflow_and_activities` registers plain + nested-function closures that delegate to `self._activity_*`/`self._orchestrate_*`, and every + `call_activity`/`schedule_new_workflow` call passes the activity/workflow's name as a string + (which `DaprWorkflowContext` explicitly supports) rather than a function reference. +- **Provenance lives inside each vector's own metadata**, not a separate store: every + `VectorRecord` written carries a full `ProvenanceRecord` (pipeline/workflow IDs, source + identity, content/config hashes, parser/splitter/embedder identity, target index/version, + timestamp, attempt number) merged into its metadata dict. This avoids a second per-chunk + storage system and keeps provenance queryable alongside search results. No credentials, signed + URLs, or connection strings are ever included (`Embedder.config()`/`DocumentParser.config()` + implementations only return non-secret, behavior-affecting settings). +- **An empty manifest (zero discovered documents) fails validation on purpose** -- see + `pipeline.py::_activity_validate_version`'s comment -- rather than trivially activating an empty + index, which is far more likely to mean a misconfigured prefix/source than an intentional + empty version. +- **`FailureInjector` (`testing.py`) is opt-in and off by default** (`None`/no-op unless a + `DurableRAGPipeline` is explicitly constructed with `failure_injector=FailureInjector(...)`); it + calls `os._exit()` (not `sys.exit()`) to simulate a real crash, deliberately skipping + cleanup/atexit handling. Never wire this into a normal production code path. +- **`AzureOpenAIChatClient.generate_answer` (`generation.py`) is query-time only**, called from a + retrieval-side process (e.g. `examples/rag/query_api.py`), never from the workflow -- generation + needs the *question*, asked at query time, which doesn't exist during ingestion. It refuses to + answer (a fixed insufficient-evidence response, no citations) when `matches` is empty, rather than + letting the model guess without grounding. +- **`triggers.EventDeduplicator` and `SourceChangeEvent.event_id`** give event-driven ingestion + (Event Grid/Service Bus for Azure, S3 event notifications for AWS) an idempotency key independent + of the underlying pub/sub system's own delivery guarantees -- both systems document at-least-once + delivery, so a duplicate/redelivered notification must not start a second workflow instance or + re-ingest a document. `pubsub_trigger_servicebus.py`/`pubsub_trigger.py` check + `already_seen(event_id)` before scheduling a workflow and `mark_seen(event_id)` after, backed by a + Dapr state key with a TTL (not workflow state -- this check happens *before* a workflow instance + exists). diff --git a/dapr/ext/rag/README.md b/dapr/ext/rag/README.md new file mode 100644 index 000000000..66bf44416 --- /dev/null +++ b/dapr/ext/rag/README.md @@ -0,0 +1,29 @@ +# dapr.ext.rag + +Durable RAG (retrieval-augmented generation) document ingestion on Dapr Workflow. +`DurableRAGPipeline` discovers documents from S3 or Azure Blob Storage, parses and chunks them, +generates embeddings, and writes them into a versioned pgvector or Pinecone index -- surviving +throttling, process crashes, and pod restarts by resuming from completed work. + +```sh +pip install "dapr[rag,rag-s3,rag-pgvector]" # S3 + pgvector +pip install "dapr[rag,rag-azure,rag-pinecone]" # Azure Blob + Pinecone +``` + +```python +from dapr.ext.rag import DurableRAGPipeline, S3Source, UnstructuredParser, TextSplitter, OpenAIEmbedder, PgVectorStore + +pipeline = DurableRAGPipeline( + source=S3Source(bucket='company-docs', prefix='policies/'), + parser=UnstructuredParser(), + splitter=TextSplitter(chunk_size=1000, chunk_overlap=150), + embedder=OpenAIEmbedder(model='text-embedding-3-small'), + vector_store=PgVectorStore(connection_string='...', collection='company-knowledge'), + state_store_name='rag-pipeline-state', +) +instance_id = pipeline.start(version='2026-09', activate_when_complete=True) +``` + +See [`AGENTS.md`](AGENTS.md) for architecture and internals, [`examples/rag/`](../../../examples/rag) +for a runnable worker/CLI/failure-demo, and [`docs/rag/README.md`](../../../docs/rag/README.md) +for full configuration, authentication, and operational documentation. diff --git a/dapr/ext/rag/__init__.py b/dapr/ext/rag/__init__.py new file mode 100644 index 000000000..321b44619 --- /dev/null +++ b/dapr/ext/rag/__init__.py @@ -0,0 +1,201 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +# Unlike dapr.ext.langgraph/strands (each wrapping exactly one all-or-nothing +# third-party SDK, guarded by a single try/except here in __init__.py), this +# extension bundles several *independent* optional adapters (boto3, +# azure-storage-blob, azure-identity, azure-search-documents, openai, psycopg, +# pinecone, unstructured, langchain-core). Guarding the import here would mean +# the first missing package breaks `import dapr.ext.rag` entirely, even for +# users who only need e.g. pgvector. So each adapter module guards its own +# third-party import instead (see AGENTS.md), and raises +# OptionalDependencyError lazily, from the adapter class's constructor rather +# than at import time -- every import below is therefore unconditional and +# always succeeds. + +from dapr.ext.rag.embedding import Embedder, OpenAIEmbedder +from dapr.ext.rag.embedding.azure_openai import AzureOpenAIEmbedder +from dapr.ext.rag.errors import ( + ActivationConflictError, + DocumentChangedError, + DocumentParseError, + InvalidEmbeddingRequestError, + InvalidGenerationRequestError, + NonRetryableError, + OptionalDependencyError, + RagError, + RetryableError, + SourceAccessDeniedError, + SourceNotFoundError, + TransientEmbeddingError, + TransientGenerationError, + TransientSourceError, + TransientVectorStoreError, + UnsupportedDocumentError, + VectorStoreError, + VersionValidationError, +) +from dapr.ext.rag.fingerprints import ( + compute_chunk_id, + compute_config_hash, + compute_content_hash, + compute_manifest_hash, + compute_pipeline_fingerprint, +) +from dapr.ext.rag.generation import AzureOpenAIChatClient +from dapr.ext.rag.models import ( + ActivationRecord, + AnswerResult, + Chunk, + Citation, + CompletionRecord, + Document, + DocumentFailure, + DocumentOutcome, + DocumentOutcomeStatus, + DocumentWorkItem, + EmbeddingBatchResult, + EmbedProgressRecord, + FoundryIQKnowledgeSourceConfig, + ManifestSummary, + PipelineConfig, + PipelineStage, + PipelineStatus, + ProvenanceRecord, + QueryMatch, + SourceChangeEvent, + SourceDocument, + SourceMetadata, + SourceProvider, + UpsertResult, + ValidationResult, + VectorRecord, +) +from dapr.ext.rag.parsing import ( + DocumentParser, + UnstructuredParser, + from_langchain_documents, + to_langchain_documents, +) +from dapr.ext.rag.pipeline import DurableRAGPipeline +from dapr.ext.rag.retrieval import ActiveVersionResolver +from dapr.ext.rag.sources import AzureBlobSource, DocumentSource, S3Source +from dapr.ext.rag.splitting import DocumentSplitter, TextSplitter +from dapr.ext.rag.state import PipelineStateStore +from dapr.ext.rag.testing import FailureInjector +from dapr.ext.rag.triggers import ( + EventDeduplicator, + StorageEventNotification, + parse_azure_blob_event, + parse_s3_event_notifications, + to_source_change_event, +) +from dapr.ext.rag.vector_stores import ( + AzureAISearchVectorStore, + PgVectorStore, + PineconeVectorStore, + VectorIndex, +) + +__all__ = [ + # Pipeline + 'DurableRAGPipeline', + 'PipelineConfig', + 'PipelineStatus', + 'PipelineStage', + 'PipelineStateStore', + 'FoundryIQKnowledgeSourceConfig', + # Retrieval + 'ActiveVersionResolver', + # Generation (query-time; not part of the durable ingestion path) + 'AzureOpenAIChatClient', + 'AnswerResult', + 'Citation', + # Sources + 'DocumentSource', + 'S3Source', + 'AzureBlobSource', + # Parsing + 'DocumentParser', + 'UnstructuredParser', + 'to_langchain_documents', + 'from_langchain_documents', + # Splitting + 'DocumentSplitter', + 'TextSplitter', + # Embedding + 'Embedder', + 'OpenAIEmbedder', + 'AzureOpenAIEmbedder', + 'EmbeddingBatchResult', + # Vector stores + 'VectorIndex', + 'PgVectorStore', + 'PineconeVectorStore', + 'AzureAISearchVectorStore', + 'QueryMatch', + # Models + 'SourceDocument', + 'SourceMetadata', + 'SourceProvider', + 'DocumentWorkItem', + 'Document', + 'Chunk', + 'VectorRecord', + 'UpsertResult', + 'ValidationResult', + 'ActivationRecord', + 'ProvenanceRecord', + 'DocumentOutcome', + 'DocumentOutcomeStatus', + 'DocumentFailure', + 'ManifestSummary', + 'CompletionRecord', + 'EmbedProgressRecord', + # Fingerprints + 'compute_chunk_id', + 'compute_config_hash', + 'compute_content_hash', + 'compute_manifest_hash', + 'compute_pipeline_fingerprint', + # Triggers + 'SourceChangeEvent', + 'StorageEventNotification', + 'parse_s3_event_notifications', + 'parse_azure_blob_event', + 'to_source_change_event', + 'EventDeduplicator', + # Testing + 'FailureInjector', + # Errors + 'RagError', + 'OptionalDependencyError', + 'RetryableError', + 'NonRetryableError', + 'TransientSourceError', + 'SourceNotFoundError', + 'SourceAccessDeniedError', + 'DocumentChangedError', + 'UnsupportedDocumentError', + 'DocumentParseError', + 'TransientEmbeddingError', + 'InvalidEmbeddingRequestError', + 'TransientGenerationError', + 'InvalidGenerationRequestError', + 'TransientVectorStoreError', + 'VectorStoreError', + 'VersionValidationError', + 'ActivationConflictError', +] diff --git a/dapr/ext/rag/_wire.py b/dapr/ext/rag/_wire.py new file mode 100644 index 000000000..6ffca95e7 --- /dev/null +++ b/dapr/ext/rag/_wire.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +# `dapr.ext.workflow`'s automatic activity input/output coercion +# (`dapr.ext.workflow._model_protocol`) only recognizes Pydantic-v2-shaped +# classes (objects exposing `model_dump` / `model_validate`). This package +# uses plain `@dataclass` types everywhere instead, matching the dataclass +# style used across the rest of the SDK (see `dapr/clients/grpc/_jobs.py`, +# `dapr/ext/workflow/propagation.py`) rather than adding a new hard runtime +# dependency on pydantic. A plain dataclass passed as `call_activity(..., +# input=...)` therefore arrives on the other side undecoded, as a `dict` or +# `SimpleNamespace` -- these two helpers perform that conversion explicitly at +# each activity/workflow boundary. +# +# Every dataclass that crosses an activity boundary must be *flat* (fields are +# JSON primitives, or lists/dicts of them): `dataclasses.asdict` recurses into +# nested dataclass fields on the way out, but `from_wire` does not reconstruct +# them on the way back, so a nested dataclass field would come back as a plain +# dict instead of an instance. Richer nested models (e.g. `SourceDocument`) +# are used only within a single activity's body, never as its input/output. + +from __future__ import annotations + +import dataclasses +from types import SimpleNamespace +from typing import Any, Mapping, TypeVar + +T = TypeVar('T') + + +def to_wire(value: Any) -> Any: + """Serializes a flat dataclass instance to a JSON-safe dict. + + Args: + value: A dataclass instance, or any already-JSON-safe value. + + Returns: + `dataclasses.asdict(value)` if `value` is a dataclass instance, + otherwise `value` unchanged. + """ + if dataclasses.is_dataclass(value) and not isinstance(value, type): + return dataclasses.asdict(value) + return value + + +def from_wire(raw: Any, cls: type[T]) -> T: + """Reconstructs a flat dataclass instance from a decoded activity payload. + + Args: + raw: The value durabletask handed to the activity/workflow: typically + a `dict`, occasionally a `SimpleNamespace`, or already an + instance of `cls` (e.g. when called directly from a unit test). + cls: The flat dataclass type to reconstruct. + + Returns: + An instance of `cls`. + + Raises: + TypeError: `raw` is not a dict, SimpleNamespace, or `cls` instance. + """ + if isinstance(raw, cls): + return raw + if isinstance(raw, SimpleNamespace): + raw = vars(raw) + if isinstance(raw, Mapping): + return cls(**raw) + raise TypeError( + f'Cannot interpret {type(raw).__name__!r} as {cls.__name__}; expected a dict, ' + 'SimpleNamespace, or existing instance.' + ) diff --git a/dapr/ext/rag/embedding/__init__.py b/dapr/ext/rag/embedding/__init__.py new file mode 100644 index 000000000..d22bae15b --- /dev/null +++ b/dapr/ext/rag/embedding/__init__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from dapr.ext.rag.embedding.base import Embedder +from dapr.ext.rag.embedding.openai import OpenAIEmbedder + +__all__ = [ + 'Embedder', + 'OpenAIEmbedder', +] diff --git a/dapr/ext/rag/embedding/_openai_common.py b/dapr/ext/rag/embedding/_openai_common.py new file mode 100644 index 000000000..7fe3ac761 --- /dev/null +++ b/dapr/ext/rag/embedding/_openai_common.py @@ -0,0 +1,205 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +# Shared between OpenAIEmbedder and AzureOpenAIEmbedder: Azure OpenAI's +# embeddings API is wire-compatible with OpenAI's (same response shape, same +# `openai` Python package, same exception types), so response parsing and +# error classification live here once instead of being duplicated across the +# two client configurations. + +from __future__ import annotations + +import logging +from typing import Any, FrozenSet, Optional, Sequence, Type + +from dapr.ext.rag.errors import ( + InvalidEmbeddingRequestError, + NonRetryableError, + OptionalDependencyError, + RagError, + RetryableError, + TransientEmbeddingError, +) +from dapr.ext.rag.models import EmbeddingBatchResult + +# See dapr/ext/rag/AGENTS.md for why the optional-dependency guard lives here, +# per adapter module, rather than once in dapr/ext/rag/__init__.py. Both +# AzureOpenAIEmbedder and generation.AzureOpenAIChatClient build their client +# through `build_azure_client` below, so the guard lives here once for both. +try: + import openai +except ImportError: # pragma: no cover - exercised only without openai installed + openai = None # type: ignore[assignment] + +try: + from azure.identity import DefaultAzureCredential, get_bearer_token_provider +except ImportError: # pragma: no cover - exercised only without azure-identity installed + DefaultAzureCredential = None # type: ignore[assignment,misc] + get_bearer_token_provider = None # type: ignore[assignment] + +logger = logging.getLogger(__name__) + +# Azure OpenAI (Cognitive Services) requires this exact resource scope for +# Entra ID token acquisition -- see Microsoft's Azure OpenAI auth docs. +AAD_SCOPE = 'https://cognitiveservices.azure.com/.default' +DEFAULT_AZURE_API_VERSION = '2024-10-21' + +# Classified by exception *name* rather than `isinstance` against the real +# `openai` exception classes, so this still works when a test injects a fake +# client that raises a look-alike exception without the `openai` package +# installed at all -- and so classification doesn't require importing every +# exception type individually under the same guard as the client itself. +TRANSIENT_EXCEPTION_NAMES = frozenset( + {'RateLimitError', 'APITimeoutError', 'APIConnectionError', 'InternalServerError'} +) +INVALID_REQUEST_EXCEPTION_NAMES = frozenset( + { + 'BadRequestError', + 'AuthenticationError', + 'PermissionDeniedError', + 'NotFoundError', + 'UnprocessableEntityError', + 'ConflictError', + } +) + + +def parse_embeddings_response(response: Any) -> EmbeddingBatchResult: + """Parses an OpenAI/Azure OpenAI `embeddings.create()` response, ordered by input index.""" + by_index = sorted(response.data, key=lambda item: item.index) + embeddings: list[Sequence[float]] = [list(item.embedding) for item in by_index] + usage = getattr(response, 'usage', None) + total_tokens = getattr(usage, 'total_tokens', None) + return EmbeddingBatchResult(embeddings=embeddings, total_tokens=total_tokens) + + +def classify_error( + exc: Exception, + *, + extra_transient_status_codes: FrozenSet[int] = frozenset(), + transient_cls: Type[RetryableError] = TransientEmbeddingError, + invalid_request_cls: Type[NonRetryableError] = InvalidEmbeddingRequestError, +) -> RagError: + """Classifies an OpenAI/Azure OpenAI SDK exception as retryable or not. + + Args: + exc: The exception raised by the `openai` client. + extra_transient_status_codes: HTTP statuses, beyond the universally + transient 429/5xx, to treat as transient for this provider (e.g. + Azure OpenAI's 408 request-timeout). + transient_cls: The exception type to raise for a transient failure -- + embeddings and chat-completion callers use different types (see + `errors.py`) so callers can distinguish which concern failed. + invalid_request_cls: The exception type to raise for an invalid, + non-retryable request. + + Returns: + An instance of `invalid_request_cls` (non-retryable) or + `transient_cls` (retryable), the latter carrying a + `retry_after_seconds` attribute when the provider sent one. + """ + name = type(exc).__name__ + retry_after = _extract_retry_after(exc) + + if name in INVALID_REQUEST_EXCEPTION_NAMES: + return invalid_request_cls(str(exc)) + if name in TRANSIENT_EXCEPTION_NAMES: + return _transient_error(exc, retry_after, transient_cls) + + status_code = getattr(exc, 'status_code', None) + if isinstance(status_code, int): + if status_code == 429 or status_code in extra_transient_status_codes or status_code >= 500: + return _transient_error(exc, retry_after, transient_cls) + if 400 <= status_code < 500: + return invalid_request_cls(str(exc)) + # Unrecognized failure (e.g. a raw connection error): assume transient so a + # genuine blip gets retried rather than abandoning the document. + return _transient_error(exc, retry_after, transient_cls) + + +def _transient_error( + exc: Exception, retry_after: Optional[float], transient_cls: Type[RetryableError] +) -> RetryableError: + error = transient_cls(str(exc)) + if retry_after is not None: + error.retry_after_seconds = retry_after # type: ignore[attr-defined] + logger.info( + 'Request throttled; provider requested a %.1fs retry delay ' + '(the workflow RetryPolicy governs the actual backoff).', + retry_after, + ) + return error + + +def build_azure_client( + *, + endpoint: str, + api_version: str, + credential: Optional[Any], + api_key: Optional[str], + timeout: float, + feature: str, +) -> Any: + """Builds an `openai.AzureOpenAI` client, preferring Entra ID over an API key. + + Shared by `AzureOpenAIEmbedder` and `generation.AzureOpenAIChatClient` so + the two authentication paths (and their dependency checks) stay in sync. + + Raises: + OptionalDependencyError: `openai` is not installed, or a credential + must be built and `azure-identity` is not installed. + """ + if openai is None: + raise OptionalDependencyError(package='openai', extra='rag', feature=feature) + + client_kwargs: dict[str, Any] = { + 'azure_endpoint': endpoint, + 'api_version': api_version, + 'timeout': timeout, + } + if api_key is not None: + client_kwargs['api_key'] = api_key + else: + resolved_credential = credential + if resolved_credential is None: + if DefaultAzureCredential is None: + raise OptionalDependencyError( + package='azure-identity', extra='rag-azure', feature=feature + ) + resolved_credential = DefaultAzureCredential() + if get_bearer_token_provider is None: + raise OptionalDependencyError( + package='azure-identity', extra='rag-azure', feature=feature + ) + client_kwargs['azure_ad_token_provider'] = get_bearer_token_provider( + resolved_credential, AAD_SCOPE + ) + return openai.AzureOpenAI(**client_kwargs) + + +def _extract_retry_after(exc: Exception) -> Optional[float]: + """Reads a `Retry-After` response header, in seconds, if the SDK exposes one.""" + response = getattr(exc, 'response', None) + headers = getattr(response, 'headers', None) + get_header = getattr( + headers, 'get', None + ) # dynamic: avoids a static .get() on a possibly-None headers + value = get_header('retry-after') if callable(get_header) else None + if value is None: + return None + try: + return float(value) + except (TypeError, ValueError): + return None diff --git a/dapr/ext/rag/embedding/azure_openai.py b/dapr/ext/rag/embedding/azure_openai.py new file mode 100644 index 000000000..06c14cba3 --- /dev/null +++ b/dapr/ext/rag/embedding/azure_openai.py @@ -0,0 +1,130 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from __future__ import annotations + +from typing import Any, Optional, Sequence + +from dapr.ext.rag.embedding import _openai_common +from dapr.ext.rag.embedding.base import Embedder +from dapr.ext.rag.models import EmbeddingBatchResult + +# Azure OpenAI additionally treats 408 (request timeout) as transient, on top +# of the 429/5xx every OpenAI-compatible endpoint shares. +_AZURE_EXTRA_TRANSIENT_STATUS_CODES = frozenset({408}) + + +class AzureOpenAIEmbedder(Embedder): + """Generates embeddings via an Azure OpenAI embeddings deployment. + + `deployment` (the customer-chosen name a request is actually routed by) + and `model` (the underlying model the deployment runs, e.g. + `text-embedding-3-small`) are tracked separately: Azure OpenAI requests + are addressed by deployment name, but provenance and the pipeline + fingerprint care about the model identity, and an operator renaming or + repointing a deployment is exactly the kind of thing that should stay + visible in provenance. + """ + + def __init__( + self, + *, + endpoint: str, + deployment: str, + model: Optional[str] = None, + api_version: str = _openai_common.DEFAULT_AZURE_API_VERSION, + credential: Optional[Any] = None, + api_key: Optional[str] = None, + dimensions: Optional[int] = None, + timeout: float = 60.0, + client: Optional[Any] = None, + ) -> None: + """Initializes an AzureOpenAIEmbedder. + + Args: + endpoint: The Azure OpenAI resource endpoint, e.g. + `https://my-resource.openai.azure.com`. + deployment: The embeddings deployment name to call. + model: The underlying model name, for provenance and the + pipeline fingerprint. Defaults to `deployment` when the + deployment is named after its model (the common case); set + this explicitly when it isn't. + api_version: The Azure OpenAI REST API version. + credential: An `azure-identity` credential for Microsoft Entra ID + authentication. Defaults to `DefaultAzureCredential()` + (managed identity, workload identity, `az login`, ...) when + neither this nor `api_key` is given. + api_key: Optional API key, for development only -- prefer + `credential`/`DefaultAzureCredential` in production. Never + logged or included in provenance/config fingerprints. + dimensions: Optional reduced embedding dimensionality, for models + that support it. + timeout: Per-request timeout, in seconds. + client: A pre-built `openai.AzureOpenAI` client (or any object + exposing `.embeddings.create(...)`) to use instead of + constructing one -- bypasses both the `openai` and + `azure-identity` dependency checks, which is how tests + exercise this class without either installed. + + Raises: + OptionalDependencyError: A required package (`openai`, or + `azure-identity` when using the default credential) is not + installed and no `client` was given. + """ + self._deployment = deployment + self._model = model or deployment + self._dimensions = dimensions + self._client = client or _openai_common.build_azure_client( + endpoint=endpoint, + api_version=api_version, + credential=credential, + api_key=api_key, + timeout=timeout, + feature='AzureOpenAIEmbedder', + ) + + @property + def embedding_model(self) -> str: + return self._model + + @property + def deployment(self) -> str: + """The Azure OpenAI deployment name requests are routed to.""" + return self._deployment + + def config(self) -> dict[str, Any]: + return { + 'deployment': self._deployment, + 'model': self._model, + 'dimensions': self._dimensions, + } + + def embed_batch(self, texts: Sequence[str]) -> EmbeddingBatchResult: + if not texts: + return EmbeddingBatchResult(embeddings=[], total_tokens=0) + + # Azure OpenAI addresses a request by deployment name, in the `model` field. + create_kwargs: dict[str, Any] = {'model': self._deployment, 'input': list(texts)} + if self._dimensions is not None: + create_kwargs['dimensions'] = self._dimensions + + try: + response = self._client.embeddings.create(**create_kwargs) + except Exception as exc: + raise _openai_common.classify_error( + exc, extra_transient_status_codes=_AZURE_EXTRA_TRANSIENT_STATUS_CODES + ) from exc + + return _openai_common.parse_embeddings_response(response) diff --git a/dapr/ext/rag/embedding/base.py b/dapr/ext/rag/embedding/base.py new file mode 100644 index 000000000..d20d4340c --- /dev/null +++ b/dapr/ext/rag/embedding/base.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import Any, Sequence + +from dapr.ext.rag.fingerprints import compute_config_hash +from dapr.ext.rag.models import EmbeddingBatchResult + + +class Embedder(ABC): + """Generates embeddings for chunk text, in caller-controlled batches. + + Implementations perform network I/O and must only ever be called from + within a workflow activity, never from the orchestrator. + """ + + @property + @abstractmethod + def embedding_model(self) -> str: + """The specific model name used, for provenance (e.g. 'text-embedding-3-small').""" + + @abstractmethod + def embed_batch(self, texts: Sequence[str]) -> EmbeddingBatchResult: + """Embeds a batch of texts, preserving input order in the result. + + Args: + texts: Chunk texts to embed. Callers are responsible for keeping + batches within the provider's request limits (see + `PipelineConfig.embedding_batch_size`). + + Returns: + An `EmbeddingBatchResult` with one embedding per input text, in + the same order, plus token usage when the provider reports it. + + Raises: + TransientEmbeddingError: The request failed transiently + (throttling, timeout, provider-side 5xx). + InvalidEmbeddingRequestError: The provider rejected the request + as invalid; retrying it unchanged will not help. + """ + + def config(self) -> dict[str, Any]: + """Behavior-affecting configuration to fold into this embedder's fingerprint. + + Must be JSON-serializable and must not include secrets (API keys). + """ + return {'model': self.embedding_model} + + def config_fingerprint(self) -> str: + """A stable hash of `config()`, used to build the pipeline fingerprint.""" + return compute_config_hash(self.config()) diff --git a/dapr/ext/rag/embedding/openai.py b/dapr/ext/rag/embedding/openai.py new file mode 100644 index 000000000..53f63b452 --- /dev/null +++ b/dapr/ext/rag/embedding/openai.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from __future__ import annotations + +from typing import Any, Optional, Sequence + +from dapr.ext.rag.embedding import _openai_common +from dapr.ext.rag.embedding.base import Embedder +from dapr.ext.rag.errors import OptionalDependencyError +from dapr.ext.rag.models import EmbeddingBatchResult + +# See dapr/ext/rag/AGENTS.md for why the optional-dependency guard lives here, +# per adapter module, rather than once in dapr/ext/rag/__init__.py. +try: + import openai +except ImportError: # pragma: no cover - exercised only without openai installed + openai = None # type: ignore[assignment] + + +class OpenAIEmbedder(Embedder): + """Generates embeddings via the OpenAI embeddings API.""" + + def __init__( + self, + *, + model: str = 'text-embedding-3-small', + api_key: Optional[str] = None, + base_url: Optional[str] = None, + dimensions: Optional[int] = None, + timeout: float = 60.0, + client: Optional[Any] = None, + ) -> None: + """Initializes an OpenAIEmbedder. + + Args: + model: The embedding model to use. + api_key: Optional explicit API key; otherwise resolved by the + `openai` client from the `OPENAI_API_KEY` environment + variable. Never logged or included in provenance/config + fingerprints. + base_url: Optional API base URL override (e.g. for an + OpenAI-compatible gateway). + dimensions: Optional reduced embedding dimensionality, for models + that support it. + timeout: Per-request timeout, in seconds. + client: A pre-built `openai.OpenAI` client (or any object + exposing `.embeddings.create(...)`) to use instead of + constructing one -- bypasses the `openai` dependency check + entirely, which is how tests exercise this class without it + installed. + + Raises: + OptionalDependencyError: `openai` is not installed and no + `client` was given. + """ + self._model = model + self._dimensions = dimensions + + if client is not None: + self._client = client + else: + if openai is None: + raise OptionalDependencyError( + package='openai', extra='rag', feature='OpenAIEmbedder' + ) + client_kwargs: dict[str, Any] = {'timeout': timeout} + if api_key is not None: + client_kwargs['api_key'] = api_key + if base_url is not None: + client_kwargs['base_url'] = base_url + self._client = openai.OpenAI(**client_kwargs) + + @property + def embedding_model(self) -> str: + return self._model + + def config(self) -> dict[str, Any]: + return {'model': self._model, 'dimensions': self._dimensions} + + def embed_batch(self, texts: Sequence[str]) -> EmbeddingBatchResult: + if not texts: + return EmbeddingBatchResult(embeddings=[], total_tokens=0) + + create_kwargs: dict[str, Any] = {'model': self._model, 'input': list(texts)} + if self._dimensions is not None: + create_kwargs['dimensions'] = self._dimensions + + try: + response = self._client.embeddings.create(**create_kwargs) + except Exception as exc: + raise _openai_common.classify_error(exc) from exc + + return _openai_common.parse_embeddings_response(response) diff --git a/dapr/ext/rag/errors.py b/dapr/ext/rag/errors.py new file mode 100644 index 000000000..6b2306f3a --- /dev/null +++ b/dapr/ext/rag/errors.py @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +# Activities distinguish *retryable* failures (transient I/O, provider +# throttling) from *non-retryable* ones (invalid input, corrupt or unsupported +# documents) by exception type, via the RetryableError / NonRetryableError +# split below, rather than through a Dapr Workflow RetryPolicy feature: the +# public `dapr.ext.workflow.RetryPolicy` has no supported way to mark +# individual exception types as non-retryable. The vendored durabletask engine +# does have one (`NonRetryableError` / `RetryPolicy.non_retryable_error_types`), +# but it is not re-exported from `dapr.ext.workflow`, and importing +# `_durabletask` from outside the extension is unsupported (see +# dapr/ext/workflow/AGENTS.md). So instead, pipeline activities catch these +# types directly: a RetryableError is re-raised so the activity's RetryPolicy +# backs off and retries the whole activity; a NonRetryableError is caught +# internally and converted into a failed DocumentOutcome, so the activity +# still returns normally and no retry budget is wasted on a failure that +# retrying cannot fix. + +from __future__ import annotations + + +class RagError(Exception): + """Base class for all errors raised by dapr.ext.rag.""" + + +class OptionalDependencyError(RagError, ImportError): + """An adapter needs a third-party package that is not installed. + + Raised at construction time (not import time) so that `import dapr.ext.rag` + always succeeds regardless of which optional adapters are usable, and so + that passing an already-constructed client bypasses the check entirely. + """ + + def __init__(self, *, package: str, extra: str, feature: str) -> None: + self.package = package + self.extra = extra + self.feature = feature + super().__init__( + f"{feature} requires the optional dependency '{package}', which is not " + f'installed. Install it with: pip install "dapr[{extra}]" -- or pass an ' + f'already-constructed client/connection to the constructor to bypass this.' + ) + + +class RetryableError(RagError): + """Base class for failures that a backoff-and-retry may resolve.""" + + +class NonRetryableError(RagError): + """Base class for failures that retrying will not fix.""" + + +class TransientSourceError(RetryableError): + """A document source operation failed transiently (throttling, timeout, network).""" + + +class SourceNotFoundError(NonRetryableError): + """The requested document no longer exists at the source.""" + + +class SourceAccessDeniedError(NonRetryableError): + """The source rejected the request as unauthorized or forbidden.""" + + +class DocumentChangedError(RetryableError): + """The document's ETag/version changed between discovery and download. + + Retryable because the correct recovery is to pick up the new version on a + later run, not to index content that no longer matches the manifest's + recorded ETag/version under stale metadata. + """ + + +class UnsupportedDocumentError(NonRetryableError): + """The parser does not support this document's format.""" + + +class DocumentParseError(NonRetryableError): + """The parser rejected this document's content (corrupt or unreadable).""" + + +class TransientEmbeddingError(RetryableError): + """The embedding provider failed transiently (HTTP 429, timeout, 5xx).""" + + +class InvalidEmbeddingRequestError(NonRetryableError): + """The embedding provider rejected the request as invalid.""" + + +class TransientGenerationError(RetryableError): + """The answer-generation (chat completion) provider failed transiently.""" + + +class InvalidGenerationRequestError(NonRetryableError): + """The answer-generation provider rejected the request as invalid.""" + + +class TransientVectorStoreError(RetryableError): + """The vector store failed transiently (connection reset, deadline exceeded).""" + + +class VectorStoreError(RagError): + """A non-transient vector store failure (bad configuration, unsupported operation).""" + + +class VersionValidationError(RagError): + """A built index version failed validation against its manifest.""" + + +class ActivationConflictError(RetryableError): + """A concurrent writer updated the active-version pointer first (ETag conflict). + + Retryable: re-running the activity re-reads the pointer's current ETag, so + the workflow's own `RetryPolicy` is sufficient to resolve the race without + a bespoke inner retry loop. + """ diff --git a/dapr/ext/rag/fingerprints.py b/dapr/ext/rag/fingerprints.py new file mode 100644 index 000000000..4b277c160 --- /dev/null +++ b/dapr/ext/rag/fingerprints.py @@ -0,0 +1,137 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +# Deterministic hashing for stable IDs and change detection. +# +# Every function here is a pure function of its inputs: no randomness, no +# clock reads, and no dependence on Python's per-process-randomized `hash()`. +# That makes them safe to call from a workflow orchestrator directly, and +# guarantees a replay or a retry recomputes the same IDs. SHA-256 is used +# throughout (rather than e.g. `uuid.uuid5`) so the exact fields folded into +# an ID are visible and debuggable from the source below, matching how the +# spec describes chunk-ID derivation as a list of named fields. + +from __future__ import annotations + +import hashlib +import json +from typing import Any, Iterable, Mapping + +# ASCII Unit Separator: joins hash inputs without the field-boundary ambiguity +# a plain string like '|' or ':' would have if a field's own value contained it. +_FIELD_SEPARATOR = '\x1f' + + +def _stable_digest(*parts: str) -> str: + return hashlib.sha256(_FIELD_SEPARATOR.join(parts).encode('utf-8')).hexdigest() + + +def compute_content_hash(data: bytes) -> str: + """Hashes raw document bytes for change detection and provenance. + + Args: + data: The document's raw bytes, as downloaded from its source. + + Returns: + A hex SHA-256 digest of `data`. + """ + return hashlib.sha256(data).hexdigest() + + +def compute_config_hash(config: Mapping[str, Any]) -> str: + """Hashes an adapter's configuration (e.g. a parser's or splitter's). + + Args: + config: A JSON-serializable mapping describing the adapter's + behavior-affecting settings (e.g. `{'chunk_size': 1000}`). Must + not include secrets (API keys, connection strings) -- this hash + is stored in provenance records and pipeline state. + + Returns: + A hex SHA-256 digest, stable regardless of key order. + """ + canonical = json.dumps(config, sort_keys=True, separators=(',', ':'), default=str) + return hashlib.sha256(canonical.encode('utf-8')).hexdigest() + + +def compute_pipeline_fingerprint( + *, + parser_config_hash: str, + splitter_config_hash: str, + embedding_model: str, + embedding_config_hash: str, +) -> str: + """Fingerprints the processing recipe applied to every document. + + Used alongside a document's own content hash to decide whether prior + work for that document is still valid: changing the parser, splitter, or + embedding configuration changes this fingerprint, which invalidates prior + completion records even though the source content hasn't changed. + + Returns: + A hex SHA-256 digest of the four inputs. + """ + return _stable_digest( + parser_config_hash, splitter_config_hash, embedding_model, embedding_config_hash + ) + + +def compute_chunk_id( + *, + source_document_id: str, + source_content_hash: str, + parser_config_hash: str, + splitter_config_hash: str, + chunk_ordinal: int, + chunk_content_hash: str, + embedding_model: str, +) -> str: + """Derives a stable chunk ID, per the spec's field list. + + A replay or a retry of the same document, under the same configuration, + produces the same chunk IDs -- which is what makes vector upserts + idempotent. Changing any input (new content, new parser/splitter config, + a different embedding model) produces different IDs, so the new content + is written under new IDs rather than silently overwriting stale ones with + a different provenance. + + Returns: + A hex SHA-256 digest of the seven inputs, suitable as a vector ID. + """ + return _stable_digest( + source_document_id, + source_content_hash, + parser_config_hash, + splitter_config_hash, + str(chunk_ordinal), + chunk_content_hash, + embedding_model, + ) + + +def compute_manifest_hash(document_ids: Iterable[str]) -> str: + """Hashes the (sorted) set of document IDs discovered for a version. + + Sorting first makes the hash independent of listing/pagination order, so + two discovery activities that see the same documents in a different + order still agree on the manifest hash. + + Args: + document_ids: The `document_id` of every document in the manifest. + + Returns: + A hex SHA-256 digest of the sorted document IDs. + """ + return _stable_digest(*sorted(document_ids)) diff --git a/dapr/ext/rag/generation.py b/dapr/ext/rag/generation.py new file mode 100644 index 000000000..27545c3ba --- /dev/null +++ b/dapr/ext/rag/generation.py @@ -0,0 +1,177 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +# Query-time answer generation. Deliberately outside the durable ingestion +# path: nothing here runs inside a workflow activity or affects the +# pipeline's determinism/idempotency guarantees -- it's a plain synchronous +# call a retrieval-time reader (e.g. examples/rag/query_api.py) makes after +# `retrieval.ActiveVersionResolver.query(...)` returns matches. Not part of +# the spec's original interface sketch; added because the Azure-native +# sample's flagship path explicitly ends in a grounded, cited answer. + +from __future__ import annotations + +from typing import Any, Optional, Sequence + +from dapr.ext.rag.embedding import _openai_common +from dapr.ext.rag.errors import InvalidGenerationRequestError, TransientGenerationError +from dapr.ext.rag.models import AnswerResult, Citation, QueryMatch + +_AZURE_EXTRA_TRANSIENT_STATUS_CODES = frozenset({408}) + +_SYSTEM_PROMPT = ( + 'You are a precise assistant that answers questions using only the numbered context ' + 'passages provided. Cite passages by their bracketed number inline, e.g. [1]. If the ' + 'context does not contain enough information to answer confidently, say so plainly ' + 'rather than guessing.' +) + + +class AzureOpenAIChatClient: + """Generates a grounded, cited answer from retrieved chunks via Azure OpenAI chat.""" + + def __init__( + self, + *, + endpoint: str, + deployment: str, + api_version: str = _openai_common.DEFAULT_AZURE_API_VERSION, + credential: Optional[Any] = None, + api_key: Optional[str] = None, + timeout: float = 60.0, + min_context_matches: int = 1, + min_score: float = 0.0, + client: Optional[Any] = None, + ) -> None: + """Initializes an AzureOpenAIChatClient. + + Args: + endpoint: The Azure OpenAI resource endpoint. + deployment: The chat-completion deployment name (may be a + different deployment than the one `AzureOpenAIEmbedder` uses). + api_version: The Azure OpenAI REST API version. + credential: An `azure-identity` credential; defaults to + `DefaultAzureCredential()` when neither this nor `api_key` + is given. + api_key: Optional API key, for development only. + timeout: Per-request timeout, in seconds. + min_context_matches: Minimum number of retrieved matches (after + `min_score` filtering) required before attempting an answer; + below this, `generate_answer` returns an insufficient-evidence + result without calling the model at all. + min_score: Matches scoring below this are treated as irrelevant + context and excluded before both the `min_context_matches` + check and the prompt itself. + client: A pre-built `openai.AzureOpenAI` client (or any object + exposing `.chat.completions.create(...)`) to use instead of + constructing one -- bypasses the `openai`/`azure-identity` + dependency checks, which is how tests exercise this class + without either installed. + """ + self._deployment = deployment + self._min_context_matches = min_context_matches + self._min_score = min_score + self._client = client or _openai_common.build_azure_client( + endpoint=endpoint, + api_version=api_version, + credential=credential, + api_key=api_key, + timeout=timeout, + feature='AzureOpenAIChatClient', + ) + + def generate_answer( + self, + question: str, + matches: Sequence[QueryMatch], + *, + index_version: str, + workflow_instance_id: Optional[str] = None, + ) -> AnswerResult: + """Generates a grounded answer, or an insufficient-evidence result. + + Args: + question: The user's question. + matches: Retrieved chunks, e.g. from + `retrieval.ActiveVersionResolver.query(...)`. + index_version: The version `matches` was retrieved from, echoed + back on the result for traceability. + workflow_instance_id: Optional ingestion workflow instance ID to + echo back, if the caller wants to correlate an answer with + the run that produced its index. + + Returns: + An `AnswerResult`. When there isn't enough relevant context, + `sufficient_evidence` is `False` and `answer` is a plain refusal + rather than a best-effort guess -- the model is not called at all + in that case. + + Raises: + TransientGenerationError: The chat-completion request failed + transiently (throttling, timeout, provider-side 5xx). + InvalidGenerationRequestError: The provider rejected the request + as invalid. + """ + relevant = [m for m in matches if m.score >= self._min_score] + if len(relevant) < self._min_context_matches: + return AnswerResult( + answer=("I don't have enough indexed information to answer that confidently."), + citations=(), + index_version=index_version, + sufficient_evidence=False, + workflow_instance_id=workflow_instance_id, + ) + + context_block = '\n\n'.join( + f'[{ordinal}] {match.content}' for ordinal, match in enumerate(relevant, start=1) + ) + messages = [ + {'role': 'system', 'content': _SYSTEM_PROMPT}, + {'role': 'user', 'content': f'Context:\n{context_block}\n\nQuestion: {question}'}, + ] + + try: + response = self._client.chat.completions.create( + model=self._deployment, + messages=messages, + temperature=0.0, + ) + except Exception as exc: + raise _openai_common.classify_error( + exc, + extra_transient_status_codes=_AZURE_EXTRA_TRANSIENT_STATUS_CODES, + transient_cls=TransientGenerationError, + invalid_request_cls=InvalidGenerationRequestError, + ) from exc + + answer_text = response.choices[0].message.content or '' + citations = tuple( + Citation( + title=str(match.metadata.get('source_name', match.document_id)), + source_uri=str( + match.metadata.get('source_uri', match.metadata.get('source_document_id', '')) + ), + chunk_id=match.chunk_id, + score=match.score, + ) + for match in relevant + ) + return AnswerResult( + answer=answer_text, + citations=citations, + index_version=index_version, + sufficient_evidence=True, + workflow_instance_id=workflow_instance_id, + ) diff --git a/dapr/ext/rag/models.py b/dapr/ext/rag/models.py new file mode 100644 index 000000000..ef66600a2 --- /dev/null +++ b/dapr/ext/rag/models.py @@ -0,0 +1,535 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +# Typed data models shared across dapr.ext.rag adapters and the pipeline. +# +# This module has no dependency beyond the standard library so every adapter +# (including ones guarding an optional third-party import) and every test can +# import it unconditionally. +# +# Two shapes of model live here: +# - Flat models (all fields are JSON primitives, or lists/dicts of them): +# these cross the workflow activity boundary via `_wire.to_wire` / +# `_wire.from_wire`, e.g. `DocumentWorkItem`, `DocumentOutcome`. +# - Richer, possibly-nested models used only within a single activity's +# body or by adapters directly, e.g. `SourceDocument`, `Chunk`. + +from __future__ import annotations + +import dataclasses +import enum +from dataclasses import dataclass, field +from typing import Any, Mapping, Optional, Sequence + + +class SourceProvider(str, enum.Enum): + """Identifies which `DocumentSource` implementation produced a document.""" + + S3 = 's3' + AZURE_BLOB = 'azure-blob' + + +@dataclass(frozen=True, slots=True) +class SourceMetadata: + """Point-in-time metadata describing a document at its source. + + All fields are best-effort: not every source or object may expose all of + them (e.g. a source that doesn't version objects has no `version_id`). + """ + + etag: Optional[str] = None + version_id: Optional[str] = None + last_modified: Optional[str] = None # ISO-8601, set inside an activity + content_length: Optional[int] = None + content_type: Optional[str] = None + + +@dataclass(frozen=True, slots=True) +class SourceDocument: + """A document discovered at a `DocumentSource`, identified stably across runs.""" + + document_id: str + provider: SourceProvider + uri: str + name: str + metadata: SourceMetadata = field(default_factory=SourceMetadata) + + +@dataclass(frozen=True, slots=True) +class DocumentWorkItem: + """The flat, wire-safe projection of a `SourceDocument`. + + This is what actually crosses the workflow activity boundary (as manifest + batch elements, and as `process_document`'s input) -- see `_wire.py` for + why nested dataclass fields like `SourceDocument.metadata` don't survive + that trip. + """ + + document_id: str + provider: str + uri: str + name: str + source_etag: Optional[str] = None + source_version_id: Optional[str] = None + source_content_length: Optional[int] = None + + @classmethod + def from_source_document(cls, doc: SourceDocument) -> 'DocumentWorkItem': + """Flattens a `SourceDocument` for the manifest and activity input.""" + return cls( + document_id=doc.document_id, + provider=doc.provider.value, + uri=doc.uri, + name=doc.name, + source_etag=doc.metadata.etag, + source_version_id=doc.metadata.version_id, + source_content_length=doc.metadata.content_length, + ) + + +@dataclass(frozen=True, slots=True) +class SourceChangeEvent: + """A provider-neutral "something changed at this document" notification. + + Normalizes an S3 Event Notification or an Azure Event Grid blob event + (see `triggers.py`) into one shape a trigger handler can act on without + branching on provider. `event_id` is the provider's own event/message ID + (e.g. Event Grid's `id`, or an S3 notification has none, so callers + derive a stable one -- see `triggers.py`) and is the idempotency key an + `EventDeduplicator` checks before starting or re-triggering ingestion. + + Deliberately a flat dataclass rather than the pydantic `BaseModel` a + provider-neutral event sketch might suggest: this repo's own typed-model + convention is dataclasses (see `_wire.py`'s module comment), and + `provider` reuses this package's existing `SourceProvider` values + ('s3' / 'azure-blob') rather than introducing a second, differently + spelled vocabulary. + """ + + provider: str # SourceProvider value + event_type: str # 'created' | 'updated' | 'deleted' + source_document_id: str + uri: str + etag: Optional[str] + version_id: Optional[str] + occurred_at: Optional[str] # ISO-8601 when the provider reports one + event_id: str + + def to_dict(self) -> dict[str, Any]: + return dataclasses.asdict(self) + + @classmethod + def from_dict(cls, data: Mapping[str, Any]) -> 'SourceChangeEvent': + return cls(**dict(data)) + + +@dataclass(frozen=True, slots=True) +class Document: + """Parsed content plus metadata for one logical unit (e.g. a PDF page). + + Mirrors LangChain's `Document` shape without requiring LangChain to be + installed -- see `parsing/langchain.py` for lossless conversion. + """ + + page_content: str + metadata: dict[str, Any] = field(default_factory=dict) + + +@dataclass(frozen=True, slots=True) +class Chunk: + """One splitter-produced unit of text, ordered within its parsed document.""" + + chunk_ordinal: int + content: str + metadata: dict[str, Any] = field(default_factory=dict) + + +@dataclass(frozen=True, slots=True) +class VectorRecord: + """A single chunk ready to be written to a `VectorIndex`.""" + + chunk_id: str + document_id: str + content: str + embedding: Sequence[float] + metadata: dict[str, Any] = field(default_factory=dict) + + +@dataclass(frozen=True, slots=True) +class EmbeddingBatchResult: + """The result of embedding one batch of chunk texts.""" + + embeddings: list[Sequence[float]] + total_tokens: Optional[int] = None + + +@dataclass(frozen=True, slots=True) +class QueryMatch: + """One similarity-search result from `VectorIndex.query()`.""" + + chunk_id: str + document_id: str + content: str + score: float + metadata: dict[str, Any] = field(default_factory=dict) + + +@dataclass(frozen=True, slots=True) +class UpsertResult: + """Outcome of writing a batch of `VectorRecord`s to a `VectorIndex`.""" + + upserted_count: int + version: str + + +@dataclass(frozen=True, slots=True) +class ValidationResult: + """Outcome of comparing a built index version against its manifest. + + A `VectorIndex.validate_version()` implementation only knows what's + actually stored, and not every store can cheaply report a distinct + document count (e.g. Pinecone's stats API reports vector counts per + namespace but not distinct metadata-field counts) -- so only + `actual_chunk_count` is guaranteed. `pipeline.py`'s validation activity + fills in the `expected_*` fields from the manifest/completion records + (via `dataclasses.replace`) and recomputes `valid` before using the + result to gate activation. + """ + + valid: bool + version: str + actual_chunk_count: int + expected_document_count: Optional[int] = None + expected_chunk_count: Optional[int] = None + actual_document_count: Optional[int] = None + details: str = '' + + +@dataclass(frozen=True, slots=True) +class ActivationRecord: + """The active-version pointer for one pipeline, persisted in Dapr state.""" + + pipeline_id: str + active_version: str + previous_version: Optional[str] + manifest_hash: str + activated_at: str # ISO-8601, set inside an activity + workflow_instance_id: str + + def to_dict(self) -> dict[str, Any]: + return dataclasses.asdict(self) + + @classmethod + def from_dict(cls, data: Mapping[str, Any]) -> 'ActivationRecord': + return cls(**dict(data)) + + +@dataclass(frozen=True, slots=True) +class ProvenanceRecord: + """Everything needed to answer "where did this chunk's vector come from?" + + Deliberately flat and JSON-primitive-only so it can be signed or + externally attested later without redesigning the stored shape. + """ + + chunk_id: str + pipeline_id: str + workflow_instance_id: str + source_provider: str + source_document_id: str + source_uri: str + source_name: str + source_content_hash: str + source_etag: Optional[str] + source_version_id: Optional[str] + source_content_type: Optional[str] + document_ordinal: int + chunk_ordinal: int + chunk_content_hash: str + parser_type: str + parser_config_hash: str + splitter_type: str + splitter_config_hash: str + embedding_provider: str + embedding_model: str + target_index: str + target_version: str + ingested_at: str # ISO-8601, set inside an activity + activity_attempt: Optional[int] = None + # The deployment name is distinct from the underlying model (Azure OpenAI: + # a customer-chosen deployment alias fronting a specific model version). + # None for embedders with no separate deployment concept (e.g. OpenAIEmbedder). + embedding_deployment: Optional[str] = None + + def to_dict(self) -> dict[str, Any]: + return dataclasses.asdict(self) + + @classmethod + def from_dict(cls, data: Mapping[str, Any]) -> 'ProvenanceRecord': + return cls(**dict(data)) + + +class DocumentOutcomeStatus(str, enum.Enum): + """The result of processing one document within an ingestion run.""" + + COMPLETED = 'completed' + SKIPPED = 'skipped' + FAILED = 'failed' + + +@dataclass(frozen=True, slots=True) +class DocumentOutcome: + """The flat result of the `process_document` activity for one document.""" + + document_id: str + status: str # DocumentOutcomeStatus value + chunk_count: int = 0 + embedded_chunk_count: int = 0 + reused_chunk_count: int = 0 + bytes_processed: int = 0 + attempts: int = 1 + error_type: Optional[str] = None + error_message: Optional[str] = None + retryable: Optional[bool] = None + + +@dataclass(frozen=True, slots=True) +class DocumentFailure: + """A compact, status-report-friendly view of a failed `DocumentOutcome`.""" + + document_id: str + error_type: str + error_message: str + retryable: bool + + +@dataclass(frozen=True, slots=True) +class ManifestSummary: + """The flat result of the `discover_and_manifest` activity. + + The full list of `DocumentWorkItem`s is persisted to Dapr state in pages + (see `state.py`) rather than returned here, so it never enters workflow + history. + """ + + version: str + total_documents: int + manifest_hash: str + page_size: int + created_at: str # ISO-8601, set inside an activity + + +@dataclass(frozen=True, slots=True) +class CompletionRecord: + """Persisted proof that a document was fully indexed under a given fingerprint. + + Read at the top of `process_document` to decide whether to skip a + document entirely; written only after its vectors are durably upserted. + """ + + document_id: str + source_content_hash: str + pipeline_fingerprint: str + chunk_count: int + embedded_chunk_count: int + completed_at: str # ISO-8601, set inside an activity + status: str = DocumentOutcomeStatus.COMPLETED.value + + def to_dict(self) -> dict[str, Any]: + return dataclasses.asdict(self) + + @classmethod + def from_dict(cls, data: Mapping[str, Any]) -> 'CompletionRecord': + return cls(**dict(data)) + + +@dataclass(frozen=True, slots=True) +class EmbedProgressRecord: + """Tracks which embedding batches of one document have been durably upserted. + + Lets `process_document` resume mid-document after a crash without + re-embedding batches that already made it to the vector store -- see + `dapr/ext/rag/AGENTS.md` for the checkpoint-after-durable-write ordering + this depends on. + """ + + document_id: str + source_content_hash: str + pipeline_fingerprint: str + total_batches: int + completed_batch_indices: list[int] = field(default_factory=list) + + def to_dict(self) -> dict[str, Any]: + return dataclasses.asdict(self) + + @classmethod + def from_dict(cls, data: Mapping[str, Any]) -> 'EmbedProgressRecord': + payload = dict(data) + payload['completed_batch_indices'] = list(payload.get('completed_batch_indices', [])) + return cls(**payload) + + +class PipelineStage(str, enum.Enum): + """The ingestion workflow's current logical stage, for status reporting.""" + + DISCOVERING = 'discovering' + PROCESSING_DOCUMENTS = 'processing_documents' + VALIDATING = 'validating' + ACTIVATING = 'activating' + COMPLETED = 'completed' + FAILED = 'failed' + + +@dataclass(frozen=True, slots=True) +class PipelineConfig: + """User-tunable knobs for a `DurableRAGPipeline` run. + + All fields are plain primitives (rather than e.g. `timedelta`) so this + config can be embedded verbatim in workflow input and hashed as part of + the pipeline fingerprint (see `fingerprints.py`). + """ + + max_concurrent_documents: int = 10 + embedding_batch_size: int = 64 + max_activity_attempts: int = 5 + fail_fast: bool = False + first_retry_interval_seconds: float = 5.0 + backoff_coefficient: float = 2.0 + max_retry_interval_seconds: float = 300.0 + manifest_page_size: Optional[int] = None + + def __post_init__(self) -> None: + if self.max_concurrent_documents < 1: + raise ValueError('max_concurrent_documents must be >= 1') + if self.embedding_batch_size < 1: + raise ValueError('embedding_batch_size must be >= 1') + if self.max_activity_attempts < 1: + raise ValueError('max_activity_attempts must be >= 1') + + @property + def effective_manifest_page_size(self) -> int: + """The manifest page size, defaulting to `max_concurrent_documents`. + + Manifest pages double as fan-out batches, so sizing them the same as + `max_concurrent_documents` means one page read yields exactly one + bounded batch of concurrent `process_document` activity calls. + """ + return self.manifest_page_size or self.max_concurrent_documents + + def to_dict(self) -> dict[str, Any]: + return dataclasses.asdict(self) + + @classmethod + def from_dict(cls, data: Mapping[str, Any]) -> 'PipelineConfig': + return cls(**dict(data)) + + +@dataclass(frozen=True, slots=True) +class FoundryIQKnowledgeSourceConfig: + """Opt-in: registers this pipeline's activated Azure AI Search index as a Foundry IQ + search-index knowledge source. See `docs/rag/foundry-iq.md` for the full rationale -- + this is off by default, and only meaningful with `AzureAISearchVectorStore`. + + A constructor-level `DurableRAGPipeline` setting (like `pubsub_name`/`pubsub_topic`), + not a per-run one: every worker process must construct the pipeline with the same + value for the registration activity to behave consistently across restarts. + """ + + name: str + """The Foundry IQ knowledge source's name.""" + + description: Optional[str] = None + source_data_fields: tuple[str, ...] = () + """Index field names to return as source data for a matched chunk.""" + search_fields: tuple[str, ...] = () + """Index field names Foundry IQ's own retrieval searches over. Defaults (empty) to + Azure AI Search's own default of all searchable fields.""" + + +@dataclass(frozen=True, slots=True) +class PipelineStatus: + """A point-in-time snapshot of an ingestion run, persisted in Dapr state. + + This is both the record `pipeline.get_status(...)` reads and the shape of + the ingestion workflow's own final return value. + """ + + pipeline_id: str + requested_version: str + workflow_instance_id: str + stage: str = PipelineStage.DISCOVERING.value + active_version: Optional[str] = None + total_documents: int = 0 + pending_documents: int = 0 + running_documents: int = 0 + completed_documents: int = 0 + skipped_documents: int = 0 + failed_documents: int = 0 + total_chunks: int = 0 + embedded_chunks: int = 0 + reused_chunks: int = 0 + embedding_requests: int = 0 + avoided_embedding_units: int = 0 + retry_count: int = 0 + retry_count_by_activity: dict[str, int] = field(default_factory=dict) + bytes_processed: int = 0 + validation_succeeded: Optional[bool] = None + activation_succeeded: Optional[bool] = None + started_at: Optional[str] = None + updated_at: Optional[str] = None + completed_at: Optional[str] = None + duration_seconds: Optional[float] = None + failures: tuple[DocumentFailure, ...] = () + + def to_dict(self) -> dict[str, Any]: + return dataclasses.asdict(self) + + @classmethod + def from_dict(cls, data: Mapping[str, Any]) -> 'PipelineStatus': + payload = dict(data) + raw_failures = payload.pop('failures', None) or () + failures = tuple( + f if isinstance(f, DocumentFailure) else DocumentFailure(**f) for f in raw_failures + ) + return cls(**payload, failures=failures) + + +@dataclass(frozen=True, slots=True) +class Citation: + """One retrieved chunk cited in a generated answer.""" + + title: str + source_uri: str + chunk_id: str + score: float + + +@dataclass(frozen=True, slots=True) +class AnswerResult: + """A grounded answer plus the chunks it was generated from. + + `sufficient_evidence=False` means retrieval didn't surface enough + relevant context; `answer` is then a refusal/insufficient-evidence + message rather than a best-effort guess. + """ + + answer: str + citations: tuple[Citation, ...] + index_version: str + sufficient_evidence: bool = True + workflow_instance_id: Optional[str] = None + + def to_dict(self) -> dict[str, Any]: + return dataclasses.asdict(self) diff --git a/dapr/ext/rag/parsing/__init__.py b/dapr/ext/rag/parsing/__init__.py new file mode 100644 index 000000000..4f8d7d772 --- /dev/null +++ b/dapr/ext/rag/parsing/__init__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from dapr.ext.rag.parsing.base import DocumentParser +from dapr.ext.rag.parsing.langchain import from_langchain_documents, to_langchain_documents +from dapr.ext.rag.parsing.unstructured import UnstructuredParser + +__all__ = [ + 'DocumentParser', + 'UnstructuredParser', + 'to_langchain_documents', + 'from_langchain_documents', +] diff --git a/dapr/ext/rag/parsing/base.py b/dapr/ext/rag/parsing/base.py new file mode 100644 index 000000000..0d81ea5b3 --- /dev/null +++ b/dapr/ext/rag/parsing/base.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import Any + +from dapr.ext.rag.fingerprints import compute_config_hash +from dapr.ext.rag.models import Document, SourceDocument + + +class DocumentParser(ABC): + """Parses a downloaded document's raw bytes into `Document` units. + + The spec this package implements sketches `parse(content, metadata: + SourceMetadata)`; this takes the richer `SourceDocument` instead (it + carries both identity -- `name`, used for file-type detection -- and the + point-in-time `SourceMetadata`, e.g. `content_type`), so a parser has + everything it needs without a second lookup. + """ + + @property + @abstractmethod + def parser_type(self) -> str: + """A short, stable name identifying this parser for provenance (e.g. 'unstructured').""" + + @abstractmethod + def parse(self, content: bytes, document: SourceDocument) -> list[Document]: + """Parses raw document bytes into one or more `Document` units. + + Args: + content: The document's raw bytes. + document: The `SourceDocument` this content was downloaded for + (for filename/content-type-based format detection). + + Returns: + One or more parsed `Document`s (e.g. one per page or section). + + Raises: + UnsupportedDocumentError: This format isn't supported. + DocumentParseError: The content could not be parsed (corrupt). + """ + + def config(self) -> dict[str, Any]: + """Behavior-affecting configuration to fold into the parser's fingerprint. + + Must be JSON-serializable and must not include secrets. The default + implementation returns an empty config; override when a parser has + settings that change its output (e.g. a strategy or language list). + """ + return {} + + def config_fingerprint(self) -> str: + """A stable hash of `config()`, used to build the pipeline fingerprint.""" + return compute_config_hash(self.config()) diff --git a/dapr/ext/rag/parsing/langchain.py b/dapr/ext/rag/parsing/langchain.py new file mode 100644 index 000000000..44936c5aa --- /dev/null +++ b/dapr/ext/rag/parsing/langchain.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from __future__ import annotations + +from typing import Any, Iterable + +from dapr.ext.rag.errors import OptionalDependencyError +from dapr.ext.rag.models import Document + +try: + from langchain_core.documents import Document as _LangchainDocument +except ImportError: # pragma: no cover - exercised only without langchain-core installed + _LangchainDocument = None + + +def to_langchain_documents(documents: Iterable[Document]) -> list[Any]: + """Converts this package's `Document`s into LangChain `Document`s. + + Args: + documents: `Document`s to convert. + + Returns: + A list of `langchain_core.documents.Document`, one per input. + + Raises: + OptionalDependencyError: `langchain-core` is not installed. + """ + if _LangchainDocument is None: + raise OptionalDependencyError( + package='langchain-core', extra='rag-langchain', feature='to_langchain_documents' + ) + return [ + _LangchainDocument(page_content=doc.page_content, metadata=dict(doc.metadata)) + for doc in documents + ] + + +def from_langchain_documents(documents: Iterable[Any]) -> list[Document]: + """Converts LangChain `Document`s (or any `page_content`/`metadata` object) back. + + Duck-typed on `.page_content` / `.metadata` rather than importing + `langchain_core`, so callers who already have LangChain `Document` + instances can convert them without this package requiring LangChain to be + installed for this direction. + + Args: + documents: Objects exposing `.page_content: str` and `.metadata: dict`. + + Returns: + A list of this package's `Document`, one per input, with page content + and metadata preserved. + """ + return [ + Document(page_content=doc.page_content, metadata=dict(doc.metadata)) for doc in documents + ] diff --git a/dapr/ext/rag/parsing/unstructured.py b/dapr/ext/rag/parsing/unstructured.py new file mode 100644 index 000000000..bcd8add4c --- /dev/null +++ b/dapr/ext/rag/parsing/unstructured.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from __future__ import annotations + +import io +from pathlib import Path +from typing import Any, Callable, Optional, Sequence + +from dapr.ext.rag.errors import ( + DocumentParseError, + OptionalDependencyError, + UnsupportedDocumentError, +) +from dapr.ext.rag.models import Document, SourceDocument +from dapr.ext.rag.parsing.base import DocumentParser + +# See dapr/ext/rag/AGENTS.md for why the optional-dependency guard lives here, +# per adapter module, rather than once in dapr/ext/rag/__init__.py. +try: + from unstructured.partition.auto import partition as _default_partition +except ImportError: # pragma: no cover - exercised only without unstructured installed + _default_partition = None + +_SUPPORTED_SUFFIXES = frozenset( + {'.txt', '.text', '.md', '.markdown', '.pdf', '.html', '.htm', '.docx'} +) + + +class UnstructuredParser(DocumentParser): + """Parses documents via the `unstructured` library's auto-partitioner. + + Supports, at minimum, plain text, Markdown, PDF, HTML, and DOCX (DOCX + requires the installed Unstructured configuration to include its `docx` + extra). Elements are grouped by page number into one `Document` per page + when the underlying format has pages (PDF, DOCX); formats without a page + concept (txt, md, html) produce a single `Document`. + """ + + def __init__( + self, + *, + strategy: str = 'fast', + languages: Optional[Sequence[str]] = None, + partition_fn: Optional[Callable[..., Any]] = None, + ) -> None: + """Initializes an UnstructuredParser. + + Args: + strategy: Unstructured's partitioning strategy (e.g. `'fast'`, + `'hi_res'`). Affects output, so it is part of this parser's + config fingerprint. + languages: Optional language hints passed through to Unstructured. + partition_fn: A drop-in replacement for + `unstructured.partition.auto.partition` -- bypasses the + `unstructured` dependency check entirely, which is how tests + exercise this class without it installed. + + Raises: + OptionalDependencyError: Raised lazily, from `parse()`, if + `unstructured` is not installed and no `partition_fn` was given + (construction itself never requires the dependency). + """ + self._partition = partition_fn or _default_partition + self._strategy = strategy + self._languages = tuple(languages) if languages else None + + @property + def parser_type(self) -> str: + return 'unstructured' + + def config(self) -> dict[str, Any]: + return {'strategy': self._strategy, 'languages': self._languages} + + def parse(self, content: bytes, document: SourceDocument) -> list[Document]: + if self._partition is None: + raise OptionalDependencyError( + package='unstructured', extra='rag-unstructured', feature='UnstructuredParser' + ) + + suffix = Path(document.name).suffix.lower() + if suffix and suffix not in _SUPPORTED_SUFFIXES: + raise UnsupportedDocumentError( + f"UnstructuredParser does not support '{suffix}' files ({document.name!r})." + ) + + partition_kwargs: dict[str, Any] = {'strategy': self._strategy} + if self._languages: + partition_kwargs['languages'] = list(self._languages) + if document.metadata.content_type: + partition_kwargs['content_type'] = document.metadata.content_type + + try: + elements = self._partition( + file=io.BytesIO(content), metadata_filename=document.name, **partition_kwargs + ) + except ImportError as exc: + # A format-specific optional dependency (e.g. python-docx) is missing from + # this installation -- distinct from a genuinely unsupported extension. + raise UnsupportedDocumentError( + f'{document.name!r} needs an Unstructured extra that is not installed: {exc}' + ) from exc + except Exception as exc: + raise DocumentParseError(f'Failed to parse {document.name!r}: {exc}') from exc + + return _group_by_page(elements, document) + + +def _group_by_page(elements: Sequence[Any], document: SourceDocument) -> list[Document]: + """Groups Unstructured elements sharing a page number into one Document each. + + Formats without pages (txt, md, html) leave every element's page_number + unset, so they collapse into a single Document -- consistent with + LangChain's own "paged" Unstructured loader mode. + """ + pages: dict[Any, list[str]] = {} + for element in elements: + text = getattr(element, 'text', None) or str(element) + if not text.strip(): + continue + metadata = getattr(element, 'metadata', None) + page_number = getattr(metadata, 'page_number', None) + pages.setdefault(page_number, []).append(text) + + if not pages: + return [] + + return [ + Document( + page_content='\n\n'.join(texts), + metadata={ + 'source_document_id': document.document_id, + 'filename': document.name, + 'page_number': page_number, + }, + ) + for page_number, texts in pages.items() + ] diff --git a/dapr/ext/rag/pipeline.py b/dapr/ext/rag/pipeline.py new file mode 100644 index 000000000..aad07f79e --- /dev/null +++ b/dapr/ext/rag/pipeline.py @@ -0,0 +1,1181 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +# DurableRAGPipeline and its workflow orchestrator/activities. +# +# Every activity below is a thin `self._activity_*` method wrapped by a plain +# nested-function closure at registration time (see +# `_register_workflow_and_activities`), rather than registered directly as a +# bound method: `WorkflowRuntime` registration (and `DaprWorkflowContext. +# call_activity` when given a function rather than a string) can stamp a +# `_dapr_alternate_name` attribute onto the registered callable, and bound +# methods do not support arbitrary attribute assignment. Plain closures avoid +# the question entirely, matching how every example in this repo defines +# activities/workflows as plain functions. Every `call_activity`/ +# `schedule_new_workflow` call below also passes the activity/workflow's +# *name* (a string) rather than a function object, which `DaprWorkflowContext` +# explicitly supports and sidesteps the same concern on the calling side. +# +# The orchestrator (`_orchestrate_ingestion`) is deterministic: it never +# performs I/O, reads the wall clock (it uses `ctx.current_utc_datetime` +# instead), or generates random values -- every one of those happens inside +# an activity. Documents are processed in bounded batches sized by +# `PipelineConfig.max_concurrent_documents`; after each batch the +# orchestrator calls `ctx.continue_as_new(...)` with a small, flat "cursor" +# state (`_IngestionState`) rather than accumulating the manifest or every +# batch's results in workflow history, so history size stays bounded +# regardless of corpus size (see `examples/workflow/monitor.py` for the same +# continue_as_new pattern applied to an eternal polling workflow). + +from __future__ import annotations + +import dataclasses +import json +import logging +import math +from datetime import datetime, timedelta, timezone +from typing import Any, Optional + +from dapr.clients import DaprClient +from dapr.ext.rag._wire import from_wire, to_wire +from dapr.ext.rag.embedding.base import Embedder +from dapr.ext.rag.errors import ( + DocumentChangedError, + NonRetryableError, + RetryableError, + VersionValidationError, +) +from dapr.ext.rag.fingerprints import ( + compute_chunk_id, + compute_content_hash, + compute_manifest_hash, + compute_pipeline_fingerprint, +) +from dapr.ext.rag.models import ( + ActivationRecord, + CompletionRecord, + DocumentFailure, + DocumentOutcome, + DocumentOutcomeStatus, + DocumentWorkItem, + EmbedProgressRecord, + FoundryIQKnowledgeSourceConfig, + ManifestSummary, + PipelineConfig, + PipelineStage, + PipelineStatus, + ProvenanceRecord, + SourceDocument, + SourceMetadata, + SourceProvider, + ValidationResult, + VectorRecord, +) +from dapr.ext.rag.parsing.base import DocumentParser +from dapr.ext.rag.sources.base import DocumentSource +from dapr.ext.rag.splitting import DocumentSplitter +from dapr.ext.rag.state import PipelineStateStore +from dapr.ext.rag.testing import FailureInjector +from dapr.ext.rag.vector_stores.base import VectorIndex +from dapr.ext.workflow import ( + DaprWorkflowClient, + DaprWorkflowContext, + RetryPolicy, + WorkflowActivityContext, + WorkflowRuntime, + WorkflowState, + WorkflowStatus, +) + +logger = logging.getLogger(__name__) + +_NON_TERMINAL_STATUSES = frozenset( + {WorkflowStatus.RUNNING, WorkflowStatus.PENDING, WorkflowStatus.SUSPENDED} +) + + +@dataclasses.dataclass(frozen=True, slots=True) +class _IngestionState: + """The ingestion orchestrator's entire input, carried across `continue_as_new`. + + Flat by necessity (see `_wire.py`) and deliberately small: the actual + manifest and per-document results live in Dapr state, not here, so this + never grows with corpus size no matter how many `continue_as_new` + generations a large run goes through. + """ + + pipeline_id: str + version: str + activate_when_complete: bool + fail_fast: bool + page_size: int + embedding_batch_size: int + max_activity_attempts: int + first_retry_interval_seconds: float + backoff_coefficient: float + max_retry_interval_seconds: float + prefix: Optional[str] = None + manifest_ready: bool = False + total_documents: int = 0 + manifest_hash: str = '' + cursor: int = 0 + + +@dataclasses.dataclass(frozen=True, slots=True) +class _ActivationState: + """Input for the standalone activation workflow (`DurableRAGPipeline.activate_version`).""" + + pipeline_id: str + version: str + + +def _retry_policy_from(config: Any) -> RetryPolicy: + """Builds a `RetryPolicy` from anything with the five retry-shaped fields. + + Works for both `PipelineConfig` and `_IngestionState`, which intentionally + share these field names so this helper can serve both. + """ + return RetryPolicy( + first_retry_interval=timedelta(seconds=config.first_retry_interval_seconds), + max_number_of_attempts=config.max_activity_attempts, + backoff_coefficient=config.backoff_coefficient, + max_retry_interval=timedelta(seconds=config.max_retry_interval_seconds), + ) + + +def _utcnow_iso() -> str: + """Wall-clock timestamp for use inside activities. Never call from the orchestrator.""" + return datetime.now(timezone.utc).isoformat() + + +def _parse_iso(value: str) -> datetime: + """Parses an ISO-8601 timestamp, treating a naive one as UTC. + + Activity-side timestamps (`_utcnow_iso`) are always timezone-aware; + orchestrator-side ones (`ctx.current_utc_datetime.isoformat()`) may not + be, depending on the durabletask engine's own convention. Normalizing + here avoids a `TypeError` when subtracting one from the other. + """ + parsed = datetime.fromisoformat(value) + return parsed if parsed.tzinfo is not None else parsed.replace(tzinfo=timezone.utc) + + +class DurableRAGPipeline: + """Durably ingests documents from a `DocumentSource` into a versioned `VectorIndex`. + + See `dapr/ext/rag/AGENTS.md` for the full architecture. In short: a Dapr + Workflow orchestrator drives discovery, per-document processing (in + bounded batches), validation, and activation; every step that performs + I/O runs in a workflow activity, and per-document idempotency is tracked + in Dapr state so a crash or a re-run resumes without re-embedding + completed work. + """ + + def __init__( + self, + *, + source: DocumentSource, + parser: DocumentParser, + splitter: DocumentSplitter, + embedder: Embedder, + vector_store: VectorIndex, + state_store_name: str, + pipeline_id: Optional[str] = None, + config: Optional[PipelineConfig] = None, + pubsub_name: Optional[str] = None, + pubsub_topic: str = 'index.version.activated', + foundry_iq_knowledge_source: Optional[FoundryIQKnowledgeSourceConfig] = None, + workflow_runtime: Optional[WorkflowRuntime] = None, + workflow_client: Optional[DaprWorkflowClient] = None, + dapr_client: Optional[DaprClient] = None, + failure_injector: Optional[FailureInjector] = None, + ) -> None: + """Initializes a DurableRAGPipeline. + + Args: + source: Where to discover and download documents from. + parser: Parses downloaded bytes into `Document`s. + splitter: Splits `Document`s into `Chunk`s. + embedder: Generates embeddings for chunk text. + vector_store: The versioned vector index to write into. + state_store_name: The Dapr state store component backing + manifests, idempotency records, status, and the + active-version pointer. + pipeline_id: A stable identifier for this logical pipeline (used + to namespace state keys and workflow/activity names). + Defaults to `vector_store.target_index_name`. + config: Tuning knobs; see `PipelineConfig`. + pubsub_name: Optional Dapr pub/sub component to publish an + `index.version.activated` event to after a successful + activation. Publishing is best-effort: a failure is logged, + not raised, so it never fails an otherwise-successful run. + pubsub_topic: Topic to publish activation events to. + foundry_iq_knowledge_source: Opt-in, off by default. When set, + registers/updates a Foundry IQ search-index knowledge source + for this pipeline's activated version, as a workflow activity + chained strictly after activation succeeds. Requires + `vector_store` to support it (currently only + `AzureAISearchVectorStore`) -- see `docs/rag/foundry-iq.md` + and `AzureAISearchVectorStore.register_foundry_iq_knowledge_ + source` for the full rationale and idempotency behavior. + Registration is best-effort like `pubsub_name`: a failure is + logged, not raised, so it never fails an otherwise-successful + activation. + workflow_runtime: A `WorkflowRuntime` to register the pipeline's + orchestrator/activities on; a new one is created when + omitted. Only `run_worker()` actually starts it, so + constructing a pipeline is safe in a short-lived + CLI/client process too. + workflow_client: A `DaprWorkflowClient` to reuse; a new one is + created (and owned/closed by this instance) when omitted. + dapr_client: A `DaprClient` to reuse for state access; a new one + is created (and owned/closed by this instance) when omitted. + failure_injector: Test/demo-only hook to deliberately crash the + process at a chosen point (see `testing.py`). Never set this + outside a controlled demo or test. + + Raises: + ValueError: `foundry_iq_knowledge_source` was given but + `vector_store` has no `register_foundry_iq_knowledge_source` + method. + """ + if foundry_iq_knowledge_source is not None and not hasattr( + vector_store, 'register_foundry_iq_knowledge_source' + ): + raise ValueError( + 'foundry_iq_knowledge_source requires a vector_store that supports it ' + f'(currently only AzureAISearchVectorStore); got {type(vector_store).__name__}.' + ) + self._source = source + self._parser = parser + self._splitter = splitter + self._embedder = embedder + self._vector_store = vector_store + self._pipeline_id = pipeline_id or vector_store.target_index_name + self._config = config or PipelineConfig() + self._pubsub_name = pubsub_name + self._pubsub_topic = pubsub_topic + self._foundry_iq_knowledge_source = foundry_iq_knowledge_source + self._failure_injector = failure_injector or FailureInjector() + + self._owns_workflow_client = workflow_client is None + self._workflow_client = workflow_client or DaprWorkflowClient() + self._owns_dapr_client = dapr_client is None + self._dapr_client = dapr_client or DaprClient() + self._state = PipelineStateStore( + state_store_name=state_store_name, dapr_client=self._dapr_client + ) + self._workflow_runtime = workflow_runtime or WorkflowRuntime() + + self._pipeline_fingerprint = compute_pipeline_fingerprint( + parser_config_hash=self._parser.config_fingerprint(), + splitter_config_hash=self._splitter.config_fingerprint(), + embedding_model=self._embedder.embedding_model, + embedding_config_hash=self._embedder.config_fingerprint(), + ) + + self._orchestrator_name = f'rag_ingest__{self._pipeline_id}' + self._activate_workflow_name = f'rag_activate__{self._pipeline_id}' + self._activity_names = { + 'discover_and_manifest': f'rag_discover__{self._pipeline_id}', + 'get_manifest_batch': f'rag_get_batch__{self._pipeline_id}', + 'process_document': f'rag_process_document__{self._pipeline_id}', + 'validate_version': f'rag_validate__{self._pipeline_id}', + 'activate_version': f'rag_activate_version__{self._pipeline_id}', + 'publish_activation_event': f'rag_publish_activation__{self._pipeline_id}', + 'register_foundry_iq_knowledge_source': f'rag_foundry_iq__{self._pipeline_id}', + 'update_status': f'rag_update_status__{self._pipeline_id}', + } + self._register_workflow_and_activities() + + # -- process lifecycle -------------------------------------------------- + + def run_worker(self, *, wait_for_ready: bool = True, timeout: float = 30.0) -> None: + """Starts this pipeline's `WorkflowRuntime` worker. + + Call this in whichever process should execute the ingestion workflow + and its activities (see `examples/rag/worker.py`); a short-lived + client process that only calls `start()`/`get_status()`/etc. never + needs to call this. + """ + self._workflow_runtime.start() + if wait_for_ready: + self._workflow_runtime.wait_for_worker_ready(timeout=timeout) + + def shutdown_worker(self) -> None: + """Stops this pipeline's `WorkflowRuntime` worker.""" + self._workflow_runtime.shutdown() + + def close(self) -> None: + """Releases owned clients and adapter resources.""" + if self._owns_workflow_client: + self._workflow_client.close() + if self._owns_dapr_client: + self._dapr_client.close() + self._source.close() + self._vector_store.close() + + # -- public API ----------------------------------------------------- + + def start( + self, + *, + version: str, + activate_when_complete: bool = True, + prefix: Optional[str] = None, + instance_id: Optional[str] = None, + ) -> str: + """Starts (or resumes) an ingestion run for `version`. + + Uses a stable, deterministic instance ID by default + (`rag-ingest-{pipeline_id}-{version}`), so calling `start()` again for + a version whose run is still in flight returns that same instance + rather than starting a second, conflicting one. Per + `DaprWorkflowClient`, an instance ID can always be reused once the + prior run reached a terminal state, which is how a fresh run after a + prior failure begins -- idempotency then comes from the per-document + completion records in Dapr state, not from the instance ID. + + Args: + version: The logical index version to build (never the currently + active one -- see `AGENTS.md` for why). + activate_when_complete: Activate `version` automatically once it + validates successfully. + prefix: Overrides the source's configured prefix for this run. + instance_id: Overrides the default stable instance ID. + + Returns: + The workflow instance ID (whether newly scheduled, or already running). + """ + resolved_instance_id = instance_id or self._stable_instance_id(version) + existing = self._workflow_client.get_workflow_state(resolved_instance_id) + if existing is not None and existing.runtime_status in _NON_TERMINAL_STATUSES: + logger.info( + 'Ingestion for pipeline=%s version=%s is already running as %s; not starting ' + 'a second run.', + self._pipeline_id, + version, + resolved_instance_id, + ) + return resolved_instance_id + + initial_state = _IngestionState( + pipeline_id=self._pipeline_id, + version=version, + activate_when_complete=activate_when_complete, + fail_fast=self._config.fail_fast, + page_size=self._config.effective_manifest_page_size, + embedding_batch_size=self._config.embedding_batch_size, + max_activity_attempts=self._config.max_activity_attempts, + first_retry_interval_seconds=self._config.first_retry_interval_seconds, + backoff_coefficient=self._config.backoff_coefficient, + max_retry_interval_seconds=self._config.max_retry_interval_seconds, + prefix=prefix, + ) + return self._workflow_client.schedule_new_workflow( + self._orchestrator_name, + input=to_wire(initial_state), + instance_id=resolved_instance_id, + ) + + def activate_version(self, version: str, *, instance_id: Optional[str] = None) -> str: + """Validates and activates an already-built version on its own. + + Runs the same validate-then-activate steps the main ingestion + workflow runs at the end of a successful build, as a standalone + workflow -- for activating a version built by an earlier run without + rebuilding it. + """ + resolved_instance_id = instance_id or f'rag-activate-{self._pipeline_id}-{version}' + existing = self._workflow_client.get_workflow_state(resolved_instance_id) + if existing is not None and existing.runtime_status in _NON_TERMINAL_STATUSES: + return resolved_instance_id + payload = _ActivationState(pipeline_id=self._pipeline_id, version=version) + return self._workflow_client.schedule_new_workflow( + self._activate_workflow_name, + input=to_wire(payload), + instance_id=resolved_instance_id, + ) + + def get_status(self, version: str) -> Optional[PipelineStatus]: + """Reads the current status of an ingestion run for `version`, if any.""" + return self._state.read_status(pipeline_id=self._pipeline_id, version=version) + + def get_workflow_state(self, instance_id: str) -> Optional[WorkflowState]: + """Reads the underlying workflow instance's raw state (status, timestamps, ...).""" + return self._workflow_client.get_workflow_state(instance_id) + + def resolve_active_version(self) -> Optional[str]: + """Returns this pipeline's currently-active version, or `None` if never activated.""" + record, _etag = self._state.read_activation(self._pipeline_id) + return record.active_version if record is not None else None + + def _stable_instance_id(self, version: str) -> str: + return f'rag-ingest-{self._pipeline_id}-{version}' + + # -- registration ------------------------------------------------------ + + def _register_workflow_and_activities(self) -> None: + names = self._activity_names + + def rag_ingest_orchestrator(ctx: DaprWorkflowContext, wf_input: dict): + return (yield from self._orchestrate_ingestion(ctx, wf_input)) + + def rag_activate_orchestrator(ctx: DaprWorkflowContext, wf_input: dict): + return (yield from self._orchestrate_activation(ctx, wf_input)) + + def discover_and_manifest(ctx: WorkflowActivityContext, raw: dict) -> dict: + return self._activity_discover_and_manifest(ctx, raw) + + def get_manifest_batch(ctx: WorkflowActivityContext, raw: dict) -> dict: + return self._activity_get_manifest_batch(ctx, raw) + + def process_document(ctx: WorkflowActivityContext, raw: dict) -> dict: + return self._activity_process_document(ctx, raw) + + def validate_version(ctx: WorkflowActivityContext, raw: dict) -> dict: + return self._activity_validate_version(ctx, raw) + + def activate_version(ctx: WorkflowActivityContext, raw: dict) -> dict: + return self._activity_activate_version(ctx, raw) + + def publish_activation_event(ctx: WorkflowActivityContext, raw: dict) -> dict: + return self._activity_publish_activation_event(ctx, raw) + + def register_foundry_iq_knowledge_source(ctx: WorkflowActivityContext, raw: dict) -> dict: + return self._activity_register_foundry_iq_knowledge_source(ctx, raw) + + def update_status(ctx: WorkflowActivityContext, raw: dict) -> dict: + return self._activity_update_status(ctx, raw) + + self._workflow_runtime.register_workflow( + rag_ingest_orchestrator, name=self._orchestrator_name + ) + self._workflow_runtime.register_workflow( + rag_activate_orchestrator, name=self._activate_workflow_name + ) + self._workflow_runtime.register_activity( + discover_and_manifest, name=names['discover_and_manifest'] + ) + self._workflow_runtime.register_activity( + get_manifest_batch, name=names['get_manifest_batch'] + ) + self._workflow_runtime.register_activity(process_document, name=names['process_document']) + self._workflow_runtime.register_activity(validate_version, name=names['validate_version']) + self._workflow_runtime.register_activity(activate_version, name=names['activate_version']) + self._workflow_runtime.register_activity( + publish_activation_event, name=names['publish_activation_event'] + ) + self._workflow_runtime.register_activity( + register_foundry_iq_knowledge_source, name=names['register_foundry_iq_knowledge_source'] + ) + self._workflow_runtime.register_activity(update_status, name=names['update_status']) + + # -- orchestrators (deterministic: no I/O, no clock, no randomness) ----- + + def _orchestrate_ingestion(self, ctx: DaprWorkflowContext, wf_input: dict): + state = from_wire(wf_input, _IngestionState) + retry_policy = _retry_policy_from(state) + names = self._activity_names + + if not state.manifest_ready: + summary_raw = yield ctx.call_activity( + names['discover_and_manifest'], + input={ + 'pipeline_id': state.pipeline_id, + 'version': state.version, + 'page_size': state.page_size, + 'prefix': state.prefix, + }, + retry_policy=retry_policy, + ) + summary = from_wire(summary_raw, ManifestSummary) + state = dataclasses.replace( + state, + manifest_ready=True, + total_documents=summary.total_documents, + manifest_hash=summary.manifest_hash, + ) + yield ctx.call_activity( + names['update_status'], + input={ + 'pipeline_id': state.pipeline_id, + 'version': state.version, + 'workflow_instance_id': ctx.instance_id, + 'total_documents': summary.total_documents, + 'stage': PipelineStage.PROCESSING_DOCUMENTS.value, + }, + retry_policy=retry_policy, + ) + + if state.cursor < state.total_documents: + page_index = state.cursor // state.page_size + batch_raw = yield ctx.call_activity( + names['get_manifest_batch'], + input={ + 'pipeline_id': state.pipeline_id, + 'version': state.version, + 'page_index': page_index, + }, + retry_policy=retry_policy, + ) + batch_items: list[dict] = batch_raw['items'] + + # Every item is scheduled up front -- this *is* the fan-out, bounded to + # at most `page_size` (== max_concurrent_documents) in-flight activities + # at once. Each is then yielded individually (fan-in) rather than via + # `when_all`, so one document's exhausted-retry failure doesn't prevent + # collecting the others' results: `process_document` never raises for an + # *expected* failure (see errors.py's module docstring), so an exception + # here only ever means retries were exhausted or something truly + # unexpected happened -- and every other task in the batch has already + # run to completion (including recording its own state) by this point. + tasks: dict[str, Any] = {} + for ordinal, item in enumerate(batch_items): + tasks[item['document_id']] = ctx.call_activity( + names['process_document'], + input={ + 'work_item': item, + 'pipeline_id': state.pipeline_id, + 'version': state.version, + 'pipeline_fingerprint': self._pipeline_fingerprint, + 'document_ordinal': state.cursor + ordinal, + }, + retry_policy=retry_policy, + ) + + outcomes: list[DocumentOutcome] = [] + for document_id, task in tasks.items(): + try: + outcome_raw = yield task + outcomes.append(from_wire(outcome_raw, DocumentOutcome)) + except Exception as exc: + outcomes.append( + DocumentOutcome( + document_id=document_id, + status=DocumentOutcomeStatus.FAILED.value, + error_type=type(exc).__name__, + error_message=str(exc)[:500], + retryable=None, + ) + ) + + if not ctx.is_replaying: + logger.info( + 'pipeline=%s version=%s processed a batch of %d document(s) (cursor %d/%d)', + state.pipeline_id, + state.version, + len(batch_items), + state.cursor, + state.total_documents, + ) + + yield ctx.call_activity( + names['update_status'], + input={ + 'pipeline_id': state.pipeline_id, + 'version': state.version, + 'outcomes': [to_wire(o) for o in outcomes], + }, + retry_policy=retry_policy, + ) + + any_failed = any(o.status == DocumentOutcomeStatus.FAILED.value for o in outcomes) + if any_failed and state.fail_fast: + final_raw = yield ctx.call_activity( + names['update_status'], + input={ + 'pipeline_id': state.pipeline_id, + 'version': state.version, + 'stage': PipelineStage.FAILED.value, + 'completed_at': ctx.current_utc_datetime.isoformat(), + }, + retry_policy=retry_policy, + ) + return final_raw + + state = dataclasses.replace(state, cursor=state.cursor + len(batch_items)) + ctx.continue_as_new(to_wire(state)) + return None + + # Every document has been processed (or none existed) -- validate, then + # activate only if validation passed. + validation_raw = yield ctx.call_activity( + names['validate_version'], + input={ + 'pipeline_id': state.pipeline_id, + 'version': state.version, + 'expected_documents': state.total_documents, + }, + retry_policy=retry_policy, + ) + validation = from_wire(validation_raw, ValidationResult) + + active_version: Optional[str] = None + activation_succeeded = False + if validation.valid and state.activate_when_complete: + activation_raw = yield ctx.call_activity( + names['activate_version'], + input={ + 'pipeline_id': state.pipeline_id, + 'version': state.version, + 'manifest_hash': state.manifest_hash, + 'workflow_instance_id': ctx.instance_id, + }, + retry_policy=retry_policy, + ) + activation_succeeded = True + active_version = activation_raw['active_version'] + yield ctx.call_activity( + names['register_foundry_iq_knowledge_source'], + input={'pipeline_id': state.pipeline_id, 'version': state.version}, + retry_policy=retry_policy, + ) + yield ctx.call_activity( + names['publish_activation_event'], + input={'activation_record': activation_raw}, + retry_policy=retry_policy, + ) + + final_raw = yield ctx.call_activity( + names['update_status'], + input={ + 'pipeline_id': state.pipeline_id, + 'version': state.version, + 'stage': ( + PipelineStage.COMPLETED.value + if validation.valid + else PipelineStage.FAILED.value + ), + 'validation_succeeded': validation.valid, + 'activation_succeeded': activation_succeeded, + 'active_version': active_version, + 'completed_at': ctx.current_utc_datetime.isoformat(), + }, + retry_policy=retry_policy, + ) + return final_raw + + def _orchestrate_activation(self, ctx: DaprWorkflowContext, wf_input: dict): + payload = from_wire(wf_input, _ActivationState) + retry_policy = _retry_policy_from(self._config) + names = self._activity_names + + validation_raw = yield ctx.call_activity( + names['validate_version'], + input={'pipeline_id': payload.pipeline_id, 'version': payload.version}, + retry_policy=retry_policy, + ) + validation = from_wire(validation_raw, ValidationResult) + if not validation.valid: + raise VersionValidationError( + f'Version {payload.version!r} failed validation: {validation.details}' + ) + + activation_raw = yield ctx.call_activity( + names['activate_version'], + input={'pipeline_id': payload.pipeline_id, 'version': payload.version}, + retry_policy=retry_policy, + ) + yield ctx.call_activity( + names['register_foundry_iq_knowledge_source'], + input={'pipeline_id': payload.pipeline_id, 'version': payload.version}, + retry_policy=retry_policy, + ) + yield ctx.call_activity( + names['publish_activation_event'], + input={'activation_record': activation_raw}, + retry_policy=retry_policy, + ) + return activation_raw + + # -- activities (all I/O; no determinism constraints) ------------------- + + def _activity_discover_and_manifest(self, ctx: WorkflowActivityContext, raw: dict) -> dict: + pipeline_id, version = raw['pipeline_id'], raw['version'] + documents = [ + DocumentWorkItem.from_source_document(doc) + for doc in self._source.list_documents(raw.get('prefix')) + ] + summary = self._state.write_manifest( + pipeline_id=pipeline_id, + version=version, + documents=documents, + page_size=raw['page_size'], + manifest_hash=compute_manifest_hash(d.document_id for d in documents), + created_at=_utcnow_iso(), + ) + return to_wire(summary) + + def _activity_get_manifest_batch(self, ctx: WorkflowActivityContext, raw: dict) -> dict: + items = self._state.read_manifest_page( + pipeline_id=raw['pipeline_id'], version=raw['version'], page_index=raw['page_index'] + ) + return {'items': [dataclasses.asdict(item) for item in items]} + + def _activity_process_document(self, ctx: WorkflowActivityContext, raw: dict) -> dict: + work_item = DocumentWorkItem(**raw['work_item']) + pipeline_id, version = raw['pipeline_id'], raw['version'] + attempts = self._state.increment_attempt_count( + pipeline_id=pipeline_id, version=version, document_id=work_item.document_id + ) + self._failure_injector.maybe_fail_before_start(work_item.document_id, attempts) + + try: + outcome = self._process_one_document( + work_item=work_item, + pipeline_id=pipeline_id, + version=version, + pipeline_fingerprint=raw['pipeline_fingerprint'], + document_ordinal=raw['document_ordinal'], + workflow_instance_id=ctx.workflow_id, + attempts=attempts, + ) + except RetryableError: + raise # let the outer RetryPolicy back off and retry the whole activity + except NonRetryableError as exc: + outcome = DocumentOutcome( + document_id=work_item.document_id, + status=DocumentOutcomeStatus.FAILED.value, + attempts=attempts, + error_type=type(exc).__name__, + error_message=str(exc)[:500], + retryable=False, + ) + return to_wire(outcome) + + def _process_one_document( + self, + *, + work_item: DocumentWorkItem, + pipeline_id: str, + version: str, + pipeline_fingerprint: str, + document_ordinal: int, + workflow_instance_id: str, + attempts: int, + ) -> DocumentOutcome: + prior = self._state.read_completion( + pipeline_id=pipeline_id, version=version, document_id=work_item.document_id + ) + + current_metadata: SourceMetadata = self._source.get_metadata(work_item.document_id) + if ( + work_item.source_etag + and current_metadata.etag + and current_metadata.etag != work_item.source_etag + ): + raise DocumentChangedError( + f'{work_item.document_id} changed since discovery (etag ' + f'{work_item.source_etag!r} -> {current_metadata.etag!r}); it will be picked up ' + 'on a later run rather than indexed under stale manifest metadata.' + ) + + content = self._source.get_document(work_item.document_id) + content_hash = compute_content_hash(content) + + if ( + prior is not None + and prior.source_content_hash == content_hash + and prior.pipeline_fingerprint == pipeline_fingerprint + and prior.status == DocumentOutcomeStatus.COMPLETED.value + ): + return DocumentOutcome( + document_id=work_item.document_id, + status=DocumentOutcomeStatus.SKIPPED.value, + chunk_count=prior.chunk_count, + reused_chunk_count=prior.chunk_count, + bytes_processed=0, + attempts=attempts, + ) + + source_document = SourceDocument( + document_id=work_item.document_id, + provider=SourceProvider(work_item.provider), + uri=work_item.uri, + name=work_item.name, + metadata=current_metadata, + ) + parsed_documents = self._parser.parse(content, source_document) + chunks = [chunk for doc in parsed_documents for chunk in self._splitter.split(doc)] + + parser_config_hash = self._parser.config_fingerprint() + splitter_config_hash = self._splitter.config_fingerprint() + embedding_model = self._embedder.embedding_model + chunk_content_hashes = [compute_content_hash(c.content.encode('utf-8')) for c in chunks] + chunk_ids = [ + compute_chunk_id( + source_document_id=work_item.document_id, + source_content_hash=content_hash, + parser_config_hash=parser_config_hash, + splitter_config_hash=splitter_config_hash, + chunk_ordinal=chunk.chunk_ordinal, + chunk_content_hash=chunk_content_hashes[i], + embedding_model=embedding_model, + ) + for i, chunk in enumerate(chunks) + ] + + progress = self._state.read_embed_progress( + pipeline_id=pipeline_id, version=version, document_id=work_item.document_id + ) + if ( + progress is None + or progress.source_content_hash != content_hash + or progress.pipeline_fingerprint != pipeline_fingerprint + ): + progress = EmbedProgressRecord( + document_id=work_item.document_id, + source_content_hash=content_hash, + pipeline_fingerprint=pipeline_fingerprint, + total_batches=( + math.ceil(len(chunks) / self._config.embedding_batch_size) if chunks else 0 + ), + ) + + embedded_count = 0 + reused_count = 0 + batch_size = self._config.embedding_batch_size + for batch_index, batch_start in enumerate(range(0, len(chunks), batch_size)): + batch_chunks = chunks[batch_start : batch_start + batch_size] + + if batch_index in progress.completed_batch_indices: + reused_count += len(batch_chunks) + continue + + self._failure_injector.maybe_fail_during_embedding(work_item.document_id, batch_index) + + batch_chunk_ids = chunk_ids[batch_start : batch_start + batch_size] + batch_hashes = chunk_content_hashes[batch_start : batch_start + batch_size] + embedding_result = self._embedder.embed_batch([c.content for c in batch_chunks]) + embedded_count += len(batch_chunks) + + ingested_at = _utcnow_iso() + records = [] + for local_index, chunk in enumerate(batch_chunks): + provenance = ProvenanceRecord( + chunk_id=batch_chunk_ids[local_index], + pipeline_id=pipeline_id, + workflow_instance_id=workflow_instance_id, + source_provider=work_item.provider, + source_document_id=work_item.document_id, + source_uri=work_item.uri, + source_name=work_item.name, + source_content_hash=content_hash, + source_etag=current_metadata.etag, + source_version_id=current_metadata.version_id, + source_content_type=current_metadata.content_type, + document_ordinal=document_ordinal, + chunk_ordinal=chunk.chunk_ordinal, + chunk_content_hash=batch_hashes[local_index], + parser_type=self._parser.parser_type, + parser_config_hash=parser_config_hash, + splitter_type=self._splitter.splitter_type, + splitter_config_hash=splitter_config_hash, + embedding_provider=type(self._embedder).__name__, + embedding_model=embedding_model, + target_index=self._vector_store.target_index_name, + target_version=version, + ingested_at=ingested_at, + activity_attempt=attempts, + # Duck-typed: only embedders with a separate deployment concept + # (e.g. AzureOpenAIEmbedder) expose `.deployment`; None otherwise. + embedding_deployment=getattr(self._embedder, 'deployment', None), + ) + records.append( + VectorRecord( + chunk_id=batch_chunk_ids[local_index], + document_id=work_item.document_id, + content=chunk.content, + embedding=embedding_result.embeddings[local_index], + metadata={**chunk.metadata, **provenance.to_dict()}, + ) + ) + self._vector_store.upsert(records, version) + + self._failure_injector.maybe_fail_after_embedding_before_completion( + work_item.document_id, batch_index + ) + + # Progress is only recorded *after* the upsert above durably lands -- + # a crash between them simply re-embeds and re-upserts this one batch + # on the next attempt, which is safe because upserts are idempotent. + progress = dataclasses.replace( + progress, completed_batch_indices=[*progress.completed_batch_indices, batch_index] + ) + self._state.write_embed_progress( + pipeline_id=pipeline_id, version=version, record=progress + ) + + self._state.write_completion( + pipeline_id=pipeline_id, + version=version, + record=CompletionRecord( + document_id=work_item.document_id, + source_content_hash=content_hash, + pipeline_fingerprint=pipeline_fingerprint, + chunk_count=len(chunks), + embedded_chunk_count=embedded_count, + completed_at=_utcnow_iso(), + ), + ) + return DocumentOutcome( + document_id=work_item.document_id, + status=DocumentOutcomeStatus.COMPLETED.value, + chunk_count=len(chunks), + embedded_chunk_count=embedded_count, + reused_chunk_count=reused_count, + bytes_processed=len(content), + attempts=attempts, + ) + + def _activity_validate_version(self, ctx: WorkflowActivityContext, raw: dict) -> dict: + pipeline_id, version = raw['pipeline_id'], raw['version'] + expected_documents = raw.get('expected_documents') + manifest_meta = ( + self._state.read_manifest_meta(pipeline_id=pipeline_id, version=version) or {} + ) + if expected_documents is None: + expected_documents = manifest_meta.get('total_documents', 0) + + store_result = self._vector_store.validate_version(version) + + page_count = manifest_meta.get('page_count', 0) + completed_documents = 0 + expected_chunk_count = 0 + for page_index in range(page_count): + page = self._state.read_manifest_page( + pipeline_id=pipeline_id, version=version, page_index=page_index + ) + for item in page: + record = self._state.read_completion( + pipeline_id=pipeline_id, version=version, document_id=item.document_id + ) + if record is not None and record.status == DocumentOutcomeStatus.COMPLETED.value: + completed_documents += 1 + expected_chunk_count += record.chunk_count + + # A version with zero expected documents is treated as invalid rather than + # trivially valid: it's far more likely to be a misconfigured prefix/source + # than an intentional empty index, and activating one would silently + # blank out query results for that pipeline. + valid = ( + expected_documents > 0 + and completed_documents == expected_documents + and store_result.actual_chunk_count >= expected_chunk_count + ) + result = dataclasses.replace( + store_result, + valid=valid, + expected_document_count=expected_documents, + expected_chunk_count=expected_chunk_count, + details=( + f'{completed_documents}/{expected_documents} document(s) completed; ' + f'{store_result.actual_chunk_count} chunk(s) in store (expected ' + f'{expected_chunk_count}).' + ), + ) + return to_wire(result) + + def _activity_activate_version(self, ctx: WorkflowActivityContext, raw: dict) -> dict: + pipeline_id, version = raw['pipeline_id'], raw['version'] + manifest_hash = raw.get('manifest_hash') + if manifest_hash is None: + meta = self._state.read_manifest_meta(pipeline_id=pipeline_id, version=version) or {} + manifest_hash = meta.get('manifest_hash', '') + workflow_instance_id = raw.get('workflow_instance_id') or ctx.workflow_id + + current, etag = self._state.read_activation(pipeline_id) + if ( + current is not None + and current.active_version == version + and current.manifest_hash == manifest_hash + ): + return to_wire(current) # already active under this exact manifest: idempotent no-op + + previous_version = current.active_version if current is not None else None + # Store-native activation (e.g. AzureAISearchVectorStore's alias switch) runs + # before the Dapr-state write and is a no-op for stores without one (see + # VectorIndex.activate_version's default). This ordering makes a retry after a + # partial failure safe: re-running finds the store's own state already correct + # and just completes the Dapr-state write, rather than switching twice. + self._vector_store.activate_version(version, previous_version=previous_version) + + record = ActivationRecord( + pipeline_id=pipeline_id, + active_version=version, + previous_version=previous_version, + manifest_hash=manifest_hash, + activated_at=_utcnow_iso(), + workflow_instance_id=workflow_instance_id, + ) + self._state.write_activation(record, etag=etag) + return to_wire(record) + + def _activity_publish_activation_event(self, ctx: WorkflowActivityContext, raw: dict) -> dict: + if not self._pubsub_name: + return {'published': False} + try: + self._dapr_client.publish_event( + pubsub_name=self._pubsub_name, + topic_name=self._pubsub_topic, + data=json.dumps(raw['activation_record']), + data_content_type='application/json', + ) + return {'published': True} + except Exception as exc: + # Best-effort: a notification failure must not fail an otherwise + # successful activation. + logger.warning('Failed to publish %s event: %s', self._pubsub_topic, exc) + return {'published': False} + + def _activity_register_foundry_iq_knowledge_source( + self, ctx: WorkflowActivityContext, raw: dict + ) -> dict: + """Opt-in: see `foundry_iq_knowledge_source` on `__init__` and `docs/rag/foundry-iq.md`. + + Only ever called after `activate_version` has already succeeded for + `raw['version']` (see `_orchestrate_ingestion`/`_orchestrate_activation`), so this + never registers a knowledge source for a version that hasn't passed this + pipeline's own validation gate. Best-effort, like `_activity_publish_activation_ + event`: a registration failure is logged, not raised, so it never fails an + otherwise-successful activation -- Foundry IQ registration is a convenience on + top of activation, not a precondition for it. + """ + config = self._foundry_iq_knowledge_source + if config is None: + return {'registered': False} + try: + # Not every VectorIndex supports this (only AzureAISearchVectorStore currently + # does) -- __init__ already validated vector_store has the method whenever + # foundry_iq_knowledge_source is set, so this dynamic call is safe at runtime; + # getattr sidesteps a static attribute check against the base VectorIndex ABC. + register = getattr(self._vector_store, 'register_foundry_iq_knowledge_source') + register( + raw['version'], + name=config.name, + description=config.description, + source_data_fields=list(config.source_data_fields), + search_fields=list(config.search_fields), + ) + return {'registered': True} + except Exception as exc: + logger.warning( + 'Failed to register Foundry IQ knowledge source %s: %s', config.name, exc + ) + return {'registered': False} + + def _activity_update_status(self, ctx: WorkflowActivityContext, raw: dict) -> dict: + pipeline_id, version = raw['pipeline_id'], raw['version'] + now = _utcnow_iso() + this_run_instance_id = raw.get('workflow_instance_id') or ctx.workflow_id + + current = self._state.read_status(pipeline_id=pipeline_id, version=version) + if current is None or current.workflow_instance_id != this_run_instance_id: + # Either the very first status write for this (pipeline_id, version), or a + # *different* workflow instance re-running a version whose last attempt already + # reached a terminal status (DurableRAGPipeline.start() explicitly supports this -- + # see its docstring). Either way, counters must restart from zero here: carrying + # forward a prior instance's counts would silently double-count documents that both + # runs discover independently. When the same stable instance ID is reused (the + # common resume-after-failure case), this is a no-op: it already matches `current`. + current = PipelineStatus( + pipeline_id=pipeline_id, + requested_version=version, + workflow_instance_id=this_run_instance_id, + started_at=now, + ) + + total_documents = raw.get('total_documents', current.total_documents) + outcomes = [DocumentOutcome(**item) for item in raw.get('outcomes', [])] + + completed, skipped, failed = ( + current.completed_documents, + current.skipped_documents, + current.failed_documents, + ) + total_chunks, embedded_chunks, reused_chunks = ( + current.total_chunks, + current.embedded_chunks, + current.reused_chunks, + ) + embedding_requests = current.embedding_requests + bytes_processed = current.bytes_processed + retry_count = current.retry_count + retry_by_activity = dict(current.retry_count_by_activity) + failures = list(current.failures) + + for outcome in outcomes: + if outcome.status == DocumentOutcomeStatus.COMPLETED.value: + completed += 1 + elif outcome.status == DocumentOutcomeStatus.SKIPPED.value: + skipped += 1 + else: + failed += 1 + failures.append( + DocumentFailure( + document_id=outcome.document_id, + error_type=outcome.error_type or 'Unknown', + error_message=outcome.error_message or '', + retryable=bool(outcome.retryable), + ) + ) + total_chunks += outcome.chunk_count + embedded_chunks += outcome.embedded_chunk_count + reused_chunks += outcome.reused_chunk_count + bytes_processed += outcome.bytes_processed + if outcome.embedded_chunk_count: + embedding_requests += 1 # one embed_batch call per non-empty embedded batch + if outcome.attempts > 1: + retry_count += outcome.attempts - 1 + retry_by_activity['process_document'] = ( + retry_by_activity.get('process_document', 0) + outcome.attempts - 1 + ) + + pending = max(total_documents - (completed + skipped + failed), 0) + completed_at = raw.get('completed_at', current.completed_at) + duration_seconds = current.duration_seconds + if completed_at and current.started_at: + duration_seconds = ( + _parse_iso(completed_at) - _parse_iso(current.started_at) + ).total_seconds() + + new_status = dataclasses.replace( + current, + stage=raw.get('stage', current.stage), + total_documents=total_documents, + pending_documents=pending, + running_documents=0, + completed_documents=completed, + skipped_documents=skipped, + failed_documents=failed, + total_chunks=total_chunks, + embedded_chunks=embedded_chunks, + reused_chunks=reused_chunks, + embedding_requests=embedding_requests, + avoided_embedding_units=reused_chunks, + retry_count=retry_count, + retry_count_by_activity=retry_by_activity, + bytes_processed=bytes_processed, + active_version=raw.get('active_version', current.active_version), + validation_succeeded=raw.get('validation_succeeded', current.validation_succeeded), + activation_succeeded=raw.get('activation_succeeded', current.activation_succeeded), + updated_at=now, + completed_at=completed_at, + duration_seconds=duration_seconds, + failures=tuple(failures), + ) + self._state.write_status(new_status) + return new_status.to_dict() diff --git a/dapr/ext/rag/py.typed b/dapr/ext/rag/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/dapr/ext/rag/retrieval.py b/dapr/ext/rag/retrieval.py new file mode 100644 index 000000000..550cc347c --- /dev/null +++ b/dapr/ext/rag/retrieval.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from __future__ import annotations + +from typing import Any, Optional + +from dapr.clients import DaprClient +from dapr.ext.rag.embedding.base import Embedder +from dapr.ext.rag.errors import VersionValidationError +from dapr.ext.rag.models import QueryMatch +from dapr.ext.rag.state import PipelineStateStore +from dapr.ext.rag.vector_stores.base import VectorIndex + + +class ActiveVersionResolver: + """Resolves a pipeline's active version and queries only that version. + + This is deliberately independent of `DurableRAGPipeline` and the + workflow runtime: any reader process (a retrieval service, a notebook, a + CLI) can construct one directly to query the currently-active index while + a new version is being built in the background, without ever seeing a + partially-built version. Not a workflow activity -- ordinary client-side + code, exactly like `DaprWorkflowClient.get_workflow_state()`. + """ + + def __init__( + self, + *, + pipeline_id: str, + state_store_name: str, + vector_store: VectorIndex, + embedder: Embedder, + dapr_client: Optional[DaprClient] = None, + ) -> None: + """Initializes an ActiveVersionResolver. + + Args: + pipeline_id: The pipeline whose active version to resolve. + state_store_name: The Dapr state store holding the activation record. + vector_store: The `VectorIndex` to query once a version is resolved. + embedder: Used to embed query text with the same model used at + ingestion time. + dapr_client: A `DaprClient` to reuse; a new one is created (and + owned/closed by this instance) when omitted. + """ + self._vector_store = vector_store + self._embedder = embedder + self._state = PipelineStateStore(state_store_name=state_store_name, dapr_client=dapr_client) + self._pipeline_id = pipeline_id + + def resolve_active_version(self) -> Optional[str]: + """Returns the pipeline's currently-active version, or `None` if never activated.""" + record, _etag = self._state.read_activation(self._pipeline_id) + return record.active_version if record is not None else None + + def query( + self, + text: str, + *, + top_k: int = 5, + metadata_filter: Optional[dict[str, Any]] = None, + ) -> list[QueryMatch]: + """Embeds `text` and searches only the currently-active version. + + Raises: + VersionValidationError: No version has ever been activated for + this pipeline. + """ + version = self.resolve_active_version() + if version is None: + raise VersionValidationError( + f'Pipeline {self._pipeline_id!r} has no active version yet.' + ) + result = self._embedder.embed_batch([text]) + return self._vector_store.query( + result.embeddings[0], + version, + top_k=top_k, + metadata_filter=metadata_filter, + query_text=text, + ) + + def close(self) -> None: + """Releases the underlying `DaprClient`, if this instance created it.""" + self._state.close() diff --git a/dapr/ext/rag/sources/__init__.py b/dapr/ext/rag/sources/__init__.py new file mode 100644 index 000000000..c9c8b4a4f --- /dev/null +++ b/dapr/ext/rag/sources/__init__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from dapr.ext.rag.sources.azure_blob import AzureBlobSource +from dapr.ext.rag.sources.base import DocumentSource +from dapr.ext.rag.sources.s3 import S3Source + +__all__ = [ + 'DocumentSource', + 'S3Source', + 'AzureBlobSource', +] diff --git a/dapr/ext/rag/sources/azure_blob.py b/dapr/ext/rag/sources/azure_blob.py new file mode 100644 index 000000000..77e5a18d3 --- /dev/null +++ b/dapr/ext/rag/sources/azure_blob.py @@ -0,0 +1,245 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from __future__ import annotations + +from typing import Any, Iterator, Optional + +from dapr.ext.rag.errors import ( + OptionalDependencyError, + RagError, + SourceAccessDeniedError, + SourceNotFoundError, + TransientSourceError, +) +from dapr.ext.rag.models import SourceDocument, SourceMetadata, SourceProvider +from dapr.ext.rag.sources.base import DocumentSource + +# See dapr/ext/rag/AGENTS.md for why the optional-dependency guard lives here, +# per adapter module, rather than once in dapr/ext/rag/__init__.py. +try: + from azure.storage.blob import BlobServiceClient +except ImportError: # pragma: no cover - exercised only without azure-storage-blob installed + BlobServiceClient = None # type: ignore[assignment,misc] + +try: + from azure.identity import DefaultAzureCredential +except ImportError: # pragma: no cover - exercised only without azure-identity installed + DefaultAzureCredential = None # type: ignore[assignment,misc] + +# Classified (and caught, in list_documents/get_document/get_metadata below) +# by exception *name* rather than `isinstance` against the real azure-core +# classes: that works identically whether azure-core is installed at all, and +# whether the caller injected a real client or a test's look-alike fake that +# isn't actually a subclass of those classes. +_NOT_FOUND_EXCEPTION_NAMES = frozenset({'ResourceNotFoundError'}) +_ACCESS_DENIED_EXCEPTION_NAMES = frozenset({'ClientAuthenticationError'}) +_TRANSIENT_EXCEPTION_NAMES = frozenset({'ServiceRequestError', 'ServiceResponseError'}) + + +class AzureBlobSource(DocumentSource): + """Reads documents from an Azure Blob Storage container. + + `DefaultAzureCredential` (managed identity, workload identity, Azure CLI + login, ...) is the preferred authentication mechanism and requires no + secrets in configuration. A `connection_string` is accepted for local + development and testing (e.g. against Azurite), and an explicit + `credential` may be injected for any other `azure-identity` credential + type. + """ + + def __init__( + self, + *, + account_url: Optional[str] = None, + container: str, + prefix: Optional[str] = None, + credential: Optional[Any] = None, + connection_string: Optional[str] = None, + page_size: int = 1000, + client: Optional[Any] = None, + ) -> None: + """Initializes an AzureBlobSource. + + Args: + account_url: The storage account's blob endpoint, e.g. + `https://example.blob.core.windows.net`. Required unless + `connection_string` or `client` is given. + container: The container to read from. + prefix: Restricts `list_documents()` to blobs under this prefix + when the call doesn't override it. + credential: An `azure-identity` credential. Defaults to + `DefaultAzureCredential()` when neither this nor + `connection_string` is given. + connection_string: A full connection string (e.g. Azurite's + well-known development string, or an account connection + string) -- an alternative to `account_url` + `credential` + for local development and testing. + page_size: Blobs requested per listing page. + client: A pre-built `BlobServiceClient` or `ContainerClient` to + use instead of constructing one -- bypasses the + `azure-storage-blob`/`azure-identity` dependency check + entirely, which is how tests exercise this class without + those packages installed. + + Raises: + OptionalDependencyError: `azure-storage-blob` (or + `azure-identity`, when a credential must be constructed) is + not installed and no `client` was given. + ValueError: Neither `client`, `connection_string`, nor + `account_url` was given. + """ + self._container_name = container + self._prefix = prefix + self._page_size = page_size + + if client is not None: + self._container_client = self._as_container_client(client, container) + else: + self._container_client = self._build_container_client( + account_url=account_url, + container=container, + credential=credential, + connection_string=connection_string, + ) + self._account_name = getattr(self._container_client, 'account_name', None) or 'unknown' + + @staticmethod + def _as_container_client(client: Any, container: str) -> Any: + get_container_client = getattr(client, 'get_container_client', None) + if callable(get_container_client): + return get_container_client(container) # a BlobServiceClient was injected + return client # a ContainerClient was injected directly + + @staticmethod + def _build_container_client( + *, + account_url: Optional[str], + container: str, + credential: Optional[Any], + connection_string: Optional[str], + ) -> Any: + if BlobServiceClient is None: + raise OptionalDependencyError( + package='azure-storage-blob', extra='rag-azure', feature='AzureBlobSource' + ) + if connection_string is not None: + service_client = BlobServiceClient.from_connection_string(connection_string) + return service_client.get_container_client(container) + if account_url is None: + raise ValueError('AzureBlobSource requires account_url, connection_string, or client.') + if credential is None: + if DefaultAzureCredential is None: + raise OptionalDependencyError( + package='azure-identity', extra='rag-azure', feature='AzureBlobSource' + ) + credential = DefaultAzureCredential() + service_client = BlobServiceClient(account_url=account_url, credential=credential) + return service_client.get_container_client(container) + + @property + def provider(self) -> SourceProvider: + return SourceProvider.AZURE_BLOB + + def list_documents(self, prefix: Optional[str] = None) -> Iterator[SourceDocument]: + effective_prefix = prefix if prefix is not None else self._prefix + try: + blobs = self._container_client.list_blobs( + name_starts_with=effective_prefix or None, + results_per_page=self._page_size, + ) + for blob in blobs: + if getattr(blob, 'deleted', False): + continue + yield self._to_source_document(blob) + except Exception as exc: + raise self._classify(exc) from exc + + def get_document(self, document_id: str) -> bytes: + blob_name = self._blob_name_from_document_id(document_id) + try: + downloader = self._container_client.download_blob(blob_name) + return downloader.readall() + except Exception as exc: + raise self._classify(exc) from exc + + def get_metadata(self, document_id: str) -> SourceMetadata: + blob_name = self._blob_name_from_document_id(document_id) + try: + properties = self._container_client.get_blob_client(blob_name).get_blob_properties() + except Exception as exc: + raise self._classify(exc) from exc + return self._to_metadata(properties) + + def close(self) -> None: + close = getattr(self._container_client, 'close', None) + if callable(close): + close() + + def _to_source_document(self, blob: Any) -> SourceDocument: + document_id = self._document_id(blob.name) + return SourceDocument( + document_id=document_id, + provider=SourceProvider.AZURE_BLOB, + uri=document_id, + name=blob.name, + metadata=self._to_metadata(blob), + ) + + @staticmethod + def _to_metadata(blob_like: Any) -> SourceMetadata: + content_settings = getattr(blob_like, 'content_settings', None) + last_modified = getattr(blob_like, 'last_modified', None) + return SourceMetadata( + etag=_normalize_etag(getattr(blob_like, 'etag', None)), + version_id=getattr(blob_like, 'version_id', None), + last_modified=last_modified.isoformat() if last_modified is not None else None, + content_length=getattr(blob_like, 'size', None), + content_type=getattr(content_settings, 'content_type', None), + ) + + def _document_id(self, blob_name: str) -> str: + return f'azure-blob://{self._account_name}/{self._container_name}/{blob_name}' + + def _blob_name_from_document_id(self, document_id: str) -> str: + prefix = f'azure-blob://{self._account_name}/{self._container_name}/' + if not document_id.startswith(prefix): + raise SourceNotFoundError( + f'{document_id!r} does not belong to container {self._container_name!r}' + ) + return document_id[len(prefix) :] + + @staticmethod + def _classify(exc: Exception) -> RagError: + name = type(exc).__name__ + if name in _NOT_FOUND_EXCEPTION_NAMES: + return SourceNotFoundError(str(exc)) + if name in _ACCESS_DENIED_EXCEPTION_NAMES: + return SourceAccessDeniedError(str(exc)) + if name in _TRANSIENT_EXCEPTION_NAMES: + return TransientSourceError(str(exc)) # network-level failure, always transient + + status = getattr(exc, 'status_code', None) + if status == 403: + return SourceAccessDeniedError(str(exc)) + if status == 429 or (isinstance(status, int) and status >= 500): + return TransientSourceError(str(exc)) + if isinstance(status, int) and 400 <= status < 500: + return SourceAccessDeniedError(str(exc)) + return TransientSourceError(str(exc)) + + +def _normalize_etag(raw_etag: Optional[str]) -> Optional[str]: + return raw_etag.strip('"') if raw_etag else None diff --git a/dapr/ext/rag/sources/base.py b/dapr/ext/rag/sources/base.py new file mode 100644 index 000000000..0bc5ec5b4 --- /dev/null +++ b/dapr/ext/rag/sources/base.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import Iterator, Optional + +from dapr.ext.rag.models import SourceDocument, SourceMetadata, SourceProvider + + +class DocumentSource(ABC): + """A pluggable source of documents to ingest (S3, Azure Blob, ...). + + Implementations must produce `SourceDocument`/`SourceMetadata` values that + are normalized the same way regardless of provider, so `pipeline.py` + contains no provider-specific branching. All methods perform I/O and must + only ever be called from within a workflow activity, never from the + orchestrator. + """ + + @property + @abstractmethod + def provider(self) -> SourceProvider: + """Which provider this source implements.""" + + @abstractmethod + def list_documents(self, prefix: Optional[str] = None) -> Iterator[SourceDocument]: + """Lists documents under `prefix` (or this source's configured prefix). + + Implementations must page through the underlying API internally and + yield one `SourceDocument` at a time, so a caller can persist results + incrementally instead of holding an entire large corpus in memory. + + Args: + prefix: Overrides the source's configured prefix for this call. + + Yields: + One `SourceDocument` per discovered object, in provider-listing + order (typically lexicographic by key/name, but callers should + not depend on a specific order). + + Raises: + TransientSourceError: The listing call failed transiently. + """ + + @abstractmethod + def get_document(self, document_id: str) -> bytes: + """Downloads a document's full content. + + Args: + document_id: A `document_id` previously returned by + `list_documents`. + + Returns: + The document's raw bytes. + + Raises: + SourceNotFoundError: The document no longer exists. + SourceAccessDeniedError: The credentials lack access. + TransientSourceError: The download failed transiently. + """ + + @abstractmethod + def get_metadata(self, document_id: str) -> SourceMetadata: + """Fetches a document's current metadata without downloading its content. + + Used to detect whether a document changed between discovery and + download (a different ETag/version than what was recorded in the + manifest). + + Args: + document_id: A `document_id` previously returned by + `list_documents`. + + Returns: + The document's current `SourceMetadata`. + + Raises: + SourceNotFoundError: The document no longer exists. + SourceAccessDeniedError: The credentials lack access. + TransientSourceError: The metadata call failed transiently. + """ + + def close(self) -> None: + """Releases any held resources (connections, sessions). Optional to override.""" diff --git a/dapr/ext/rag/sources/s3.py b/dapr/ext/rag/sources/s3.py new file mode 100644 index 000000000..8e2cf6a51 --- /dev/null +++ b/dapr/ext/rag/sources/s3.py @@ -0,0 +1,228 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from __future__ import annotations + +from typing import Any, Iterator, Optional + +from dapr.ext.rag.errors import ( + OptionalDependencyError, + RagError, + SourceAccessDeniedError, + SourceNotFoundError, + TransientSourceError, +) +from dapr.ext.rag.models import SourceDocument, SourceMetadata, SourceProvider +from dapr.ext.rag.sources.base import DocumentSource + +# Guarded like every other optional adapter in this package (see +# dapr/ext/rag/AGENTS.md): the import is attempted once at module load, and a +# missing package only becomes an error when S3Source is actually +# instantiated without an injected client -- never at `import dapr.ext.rag`. +try: + import boto3 + from botocore.exceptions import BotoCoreError, ClientError +except ImportError: # pragma: no cover - exercised only without boto3 installed + boto3 = None # type: ignore[assignment] + BotoCoreError = ClientError = Exception # type: ignore[assignment,misc] + +_THROTTLE_CODES = frozenset( + { + 'SlowDown', + 'RequestTimeout', + 'RequestTimeTooSkewed', + 'ThrottlingException', + 'ServiceUnavailable', + 'InternalError', + 'Throttling', + } +) +_NOT_FOUND_CODES = frozenset({'NoSuchKey', 'NoSuchBucket', '404'}) +_ACCESS_DENIED_CODES = frozenset({'AccessDenied', 'InvalidAccessKeyId', 'SignatureDoesNotMatch'}) + + +class S3Source(DocumentSource): + """Reads documents from an Amazon S3 (or S3-compatible) bucket. + + Credentials are resolved via boto3's standard credential-provider chain + (environment, shared config/credentials files, IAM instance/task role, + SSO, ...) by default -- nothing here requires static credentials in + configuration. Explicit `aws_access_key_id`/`aws_secret_access_key` are + accepted only for cases like local testing against LocalStack. + """ + + def __init__( + self, + *, + bucket: str, + prefix: Optional[str] = None, + region_name: Optional[str] = None, + endpoint_url: Optional[str] = None, + aws_access_key_id: Optional[str] = None, + aws_secret_access_key: Optional[str] = None, + aws_session_token: Optional[str] = None, + page_size: int = 1000, + client: Optional[Any] = None, + ) -> None: + """Initializes an S3Source. + + Args: + bucket: The S3 bucket to read from. + prefix: Restricts `list_documents()` to keys under this prefix + when the call doesn't override it. + region_name: Optional AWS region; otherwise resolved by boto3. + endpoint_url: Optional endpoint override, e.g. + `http://localhost:4566` for LocalStack or any S3-compatible + store. Ignored if `client` is given. + aws_access_key_id: Optional static credential. Prefer the default + credential chain; this exists for local/test setups (e.g. + LocalStack's fixed test credentials). + aws_secret_access_key: Paired with `aws_access_key_id`. + aws_session_token: Optional session token for temporary credentials. + page_size: Objects requested per `ListObjectsV2` page. + client: A pre-built boto3 S3 client to use instead of + constructing one -- bypasses the `boto3` dependency check + entirely, which is how tests exercise this class without + boto3 installed. + + Raises: + OptionalDependencyError: `boto3` is not installed and no `client` + was given. + """ + self._bucket = bucket + self._prefix = prefix + self._page_size = page_size + + if client is not None: + self._client = client + else: + if boto3 is None: + raise OptionalDependencyError(package='boto3', extra='rag-s3', feature='S3Source') + session_kwargs: dict[str, Any] = {} + if region_name is not None: + session_kwargs['region_name'] = region_name + if aws_access_key_id is not None: + session_kwargs['aws_access_key_id'] = aws_access_key_id + if aws_secret_access_key is not None: + session_kwargs['aws_secret_access_key'] = aws_secret_access_key + if aws_session_token is not None: + session_kwargs['aws_session_token'] = aws_session_token + self._client = boto3.client('s3', endpoint_url=endpoint_url, **session_kwargs) + + @property + def provider(self) -> SourceProvider: + return SourceProvider.S3 + + def list_documents(self, prefix: Optional[str] = None) -> Iterator[SourceDocument]: + effective_prefix = prefix if prefix is not None else self._prefix + list_kwargs: dict[str, Any] = {'Bucket': self._bucket} + if effective_prefix: + list_kwargs['Prefix'] = effective_prefix + + try: + paginator = self._client.get_paginator('list_objects_v2') + for page in paginator.paginate( + PaginationConfig={'PageSize': self._page_size}, **list_kwargs + ): + for obj in page.get('Contents', []): + key = obj['Key'] + if key.endswith('/') and obj.get('Size', 0) == 0: + continue # "directory marker" placeholder object, not a document + yield self._to_source_document(key, obj) + except (ClientError, BotoCoreError) as exc: + raise self._classify(exc) from exc + + def get_document(self, document_id: str) -> bytes: + key = self._key_from_document_id(document_id) + try: + response = self._client.get_object(Bucket=self._bucket, Key=key) + return response['Body'].read() + except (ClientError, BotoCoreError) as exc: + raise self._classify(exc) from exc + + def get_metadata(self, document_id: str) -> SourceMetadata: + key = self._key_from_document_id(document_id) + try: + response = self._client.head_object(Bucket=self._bucket, Key=key) + except (ClientError, BotoCoreError) as exc: + raise self._classify(exc) from exc + return SourceMetadata( + etag=_strip_etag(response.get('ETag')), + version_id=response.get('VersionId'), + last_modified=_isoformat(response.get('LastModified')), + content_length=response.get('ContentLength'), + content_type=response.get('ContentType'), + ) + + def close(self) -> None: + close = getattr(self._client, 'close', None) + if callable(close): + close() + + def _to_source_document(self, key: str, listing_entry: dict[str, Any]) -> SourceDocument: + document_id = self._document_id(key) + return SourceDocument( + document_id=document_id, + provider=SourceProvider.S3, + uri=document_id, + name=key, + metadata=SourceMetadata( + etag=_strip_etag(listing_entry.get('ETag')), + version_id=None, # ListObjectsV2 doesn't return VersionId; head_object does. + last_modified=_isoformat(listing_entry.get('LastModified')), + content_length=listing_entry.get('Size'), + content_type=None, + ), + ) + + def _document_id(self, key: str) -> str: + return f's3://{self._bucket}/{key}' + + def _key_from_document_id(self, document_id: str) -> str: + prefix = f's3://{self._bucket}/' + if not document_id.startswith(prefix): + raise SourceNotFoundError(f'{document_id!r} does not belong to bucket {self._bucket!r}') + return document_id[len(prefix) :] + + @staticmethod + def _classify(exc: Exception) -> RagError: + if not isinstance(exc, ClientError): + # A BotoCoreError that isn't a ClientError (e.g. EndpointConnectionError, + # ConnectTimeoutError) is a network-level failure -- always transient. + return TransientSourceError(str(exc)) + + error = exc.response.get('Error', {}) if hasattr(exc, 'response') else {} + code = error.get('Code', '') + status = exc.response.get('ResponseMetadata', {}).get('HTTPStatusCode') + + if code in _NOT_FOUND_CODES: + return SourceNotFoundError(str(exc)) + if code in _ACCESS_DENIED_CODES or status == 403: + return SourceAccessDeniedError(str(exc)) + if code in _THROTTLE_CODES or (isinstance(status, int) and status >= 500): + return TransientSourceError(str(exc)) + # Unrecognized 4xx: most likely a request/config problem retrying won't fix. + if isinstance(status, int) and 400 <= status < 500: + return SourceAccessDeniedError(str(exc)) + return TransientSourceError(str(exc)) + + +def _strip_etag(raw_etag: Optional[str]) -> Optional[str]: + return raw_etag.strip('"') if raw_etag else None + + +def _isoformat(value: Any) -> Optional[str]: + isoformat = getattr(value, 'isoformat', None) + return isoformat() if callable(isoformat) else None diff --git a/dapr/ext/rag/splitting.py b/dapr/ext/rag/splitting.py new file mode 100644 index 000000000..10156a5c8 --- /dev/null +++ b/dapr/ext/rag/splitting.py @@ -0,0 +1,159 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import Any, Sequence + +from dapr.ext.rag.fingerprints import compute_config_hash +from dapr.ext.rag.models import Chunk, Document + +_DEFAULT_SEPARATORS: tuple[str, ...] = ('\n\n', '\n', ' ', '') + + +class DocumentSplitter(ABC): + """Splits a parsed `Document` into ordered `Chunk`s.""" + + @property + @abstractmethod + def splitter_type(self) -> str: + """A short, stable name identifying this splitter for provenance.""" + + @abstractmethod + def split(self, document: Document) -> list[Chunk]: + """Splits `document` into ordered chunks. + + Implementations must be deterministic: the same document must always + produce the same chunk content in the same order, since chunk IDs are + derived in part from `chunk_ordinal` and `chunk_content_hash`. + """ + + def config(self) -> dict[str, Any]: + """Behavior-affecting configuration to fold into the splitter's fingerprint.""" + return {} + + def config_fingerprint(self) -> str: + """A stable hash of `config()`, used to build the pipeline fingerprint.""" + return compute_config_hash(self.config()) + + +class TextSplitter(DocumentSplitter): + """A recursive character splitter with overlap (no third-party dependency). + + Recursively tries each separator in `separators` (paragraph, then line, + then space, then a hard character cut) to break text into pieces no + larger than `chunk_size`, then greedily merges adjacent pieces back up to + `chunk_size`, carrying the trailing `chunk_overlap` characters of each + chunk into the start of the next so context isn't lost at a chunk + boundary. + """ + + def __init__( + self, + *, + chunk_size: int = 1000, + chunk_overlap: int = 150, + separators: Sequence[str] = _DEFAULT_SEPARATORS, + ) -> None: + """Initializes a TextSplitter. + + Args: + chunk_size: Maximum characters per chunk. + chunk_overlap: Characters of context carried from the end of one + chunk into the start of the next. + separators: Tried in order to find split points; must end with + `''` (or another separator guaranteed to appear) so recursion + always terminates. + + Raises: + ValueError: `chunk_size` isn't positive, or `chunk_overlap` isn't + smaller than `chunk_size`. + """ + if chunk_size < 1: + raise ValueError('chunk_size must be >= 1') + if chunk_overlap < 0: + raise ValueError('chunk_overlap must be >= 0') + if chunk_overlap >= chunk_size: + raise ValueError('chunk_overlap must be smaller than chunk_size') + self._chunk_size = chunk_size + self._chunk_overlap = chunk_overlap + self._separators = tuple(separators) + + @property + def splitter_type(self) -> str: + return 'text_splitter' + + def config(self) -> dict[str, Any]: + return { + 'chunk_size': self._chunk_size, + 'chunk_overlap': self._chunk_overlap, + 'separators': list(self._separators), + } + + def split(self, document: Document) -> list[Chunk]: + pieces = _split_recursive(document.page_content, self._chunk_size, self._separators) + merged = _merge_with_overlap(pieces, self._chunk_size, self._chunk_overlap) + return [ + Chunk(chunk_ordinal=ordinal, content=text, metadata=dict(document.metadata)) + for ordinal, text in enumerate(merged) + if text.strip() + ] + + +def _split_recursive(text: str, chunk_size: int, separators: Sequence[str]) -> list[str]: + if len(text) <= chunk_size or not separators: + return [text] if text else [] + + separator, *remaining = separators + if separator == '': + return [text[i : i + chunk_size] for i in range(0, len(text), chunk_size)] + if separator not in text: + return _split_recursive(text, chunk_size, remaining) + + pieces: list[str] = [] + parts = text.split(separator) + for index, part in enumerate(parts): + # Re-attach the separator to every part but the last, so re-joining + # `pieces` losslessly reproduces the original text. + piece = part + separator if index < len(parts) - 1 else part + if not piece: + continue + if len(piece) > chunk_size: + pieces.extend(_split_recursive(piece, chunk_size, remaining)) + else: + pieces.append(piece) + return pieces + + +def _merge_with_overlap(pieces: Sequence[str], chunk_size: int, chunk_overlap: int) -> list[str]: + # A chunk's hard maximum length is chunk_size + chunk_overlap, not chunk_size: + # after closing a chunk, the overlap tail (<= chunk_overlap chars) is + # unconditionally joined with the next piece (<= chunk_size chars) before the + # overflow check runs again, so that one join can exceed chunk_size by up to + # chunk_overlap chars. It can't compound further: the very next piece added to + # an already-oversized `current` immediately re-triggers the close, so no chunk + # ever grows past chunk_size + chunk_overlap. + chunks: list[str] = [] + current = '' + for piece in pieces: + if current and len(current) + len(piece) > chunk_size: + chunks.append(current) + # `current[-0:]` would be the *whole* string, not '' -- guard chunk_overlap == 0. + current = current[-chunk_overlap:] if chunk_overlap else '' + current += piece + if current: + chunks.append(current) + return chunks diff --git a/dapr/ext/rag/state.py b/dapr/ext/rag/state.py new file mode 100644 index 000000000..87b0c5160 --- /dev/null +++ b/dapr/ext/rag/state.py @@ -0,0 +1,275 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +# All Dapr state access for the pipeline goes through this one class, called +# both from within workflow activities (during ingestion) and directly by +# client-side code such as `pipeline.get_status()` (a plain query, exactly +# like `DaprWorkflowClient.get_workflow_state()` -- not part of any +# orchestrator replay, so it's fine for it to talk to Dapr state directly). +# +# Key schema (all under the `rag:` prefix): +# rag:manifest:{pipeline_id}:{version}:meta -- ManifestSummary + page_count +# rag:manifest:{pipeline_id}:{version}:page:{n} -- one page of DocumentWorkItem +# rag:completion:{pipeline_id}:{version}:{document_id} -- CompletionRecord (idempotency) +# rag:embed-progress:{pipeline_id}:{version}:{document_id} -- EmbedProgressRecord +# rag:attempts:{pipeline_id}:{version}:{document_id} -- {"attempts": N} +# rag:status:{pipeline_id}:{version} -- PipelineStatus +# rag:activation:{pipeline_id} -- ActivationRecord (ETag-guarded) + +from __future__ import annotations + +import dataclasses +import json +import logging +from typing import Any, Iterator, Optional, Sequence, TypeVar + +import grpc + +from dapr.clients import DaprClient +from dapr.ext.rag.errors import ActivationConflictError +from dapr.ext.rag.models import ( + ActivationRecord, + CompletionRecord, + DocumentWorkItem, + EmbedProgressRecord, + ManifestSummary, + PipelineStatus, +) + +logger = logging.getLogger(__name__) + +T = TypeVar('T') + + +class PipelineStateStore: + """Reads and writes all `dapr.ext.rag` pipeline state for one Dapr state store.""" + + def __init__(self, *, state_store_name: str, dapr_client: Optional[DaprClient] = None) -> None: + """Initializes a PipelineStateStore. + + Args: + state_store_name: The Dapr state store component name. + dapr_client: A `DaprClient` to reuse; a new one is created (and + owned/closed by this instance) when omitted. + """ + self._state_store_name = state_store_name + self._owns_client = dapr_client is None + self._client = dapr_client or DaprClient() + + def close(self) -> None: + """Closes the underlying `DaprClient`, if this instance created it.""" + if self._owns_client: + self._client.close() + + # -- manifest --------------------------------------------------------- + + def write_manifest( + self, + *, + pipeline_id: str, + version: str, + documents: Sequence[DocumentWorkItem], + page_size: int, + manifest_hash: str, + created_at: str, + ) -> ManifestSummary: + """Persists a manifest in fixed-size pages and returns its summary. + + Paging keeps any single state value small regardless of corpus size, + and lets `read_manifest_page` double as the fan-out batch source (one + page read == one bounded batch of concurrent `process_document` + calls). + """ + pages = list(_chunked(documents, page_size)) + for page_index, page in enumerate(pages): + self._save_json( + self._manifest_page_key(pipeline_id, version, page_index), + [dataclasses.asdict(item) for item in page], + ) + summary = ManifestSummary( + version=version, + total_documents=len(documents), + manifest_hash=manifest_hash, + page_size=page_size, + created_at=created_at, + ) + meta = {**dataclasses.asdict(summary), 'page_count': len(pages)} + self._save_json(self._manifest_meta_key(pipeline_id, version), meta) + return summary + + def read_manifest_meta(self, *, pipeline_id: str, version: str) -> Optional[dict[str, Any]]: + return self._get_json(self._manifest_meta_key(pipeline_id, version)) + + def read_manifest_page( + self, *, pipeline_id: str, version: str, page_index: int + ) -> list[DocumentWorkItem]: + raw = self._get_json(self._manifest_page_key(pipeline_id, version, page_index)) or [] + return [DocumentWorkItem(**item) for item in raw] + + # -- per-document completion (idempotency) ----------------------------- + + def read_completion( + self, *, pipeline_id: str, version: str, document_id: str + ) -> Optional[CompletionRecord]: + raw = self._get_json(self._completion_key(pipeline_id, version, document_id)) + return CompletionRecord.from_dict(raw) if raw is not None else None + + def write_completion(self, *, pipeline_id: str, version: str, record: CompletionRecord) -> None: + self._save_json( + self._completion_key(pipeline_id, version, record.document_id), record.to_dict() + ) + + # -- per-document embedding progress (batch-level resumability) -------- + + def read_embed_progress( + self, *, pipeline_id: str, version: str, document_id: str + ) -> Optional[EmbedProgressRecord]: + raw = self._get_json(self._embed_progress_key(pipeline_id, version, document_id)) + return EmbedProgressRecord.from_dict(raw) if raw is not None else None + + def write_embed_progress( + self, *, pipeline_id: str, version: str, record: EmbedProgressRecord + ) -> None: + self._save_json( + self._embed_progress_key(pipeline_id, version, record.document_id), record.to_dict() + ) + + # -- per-document attempt counts (retry metric) ------------------------- + + def increment_attempt_count(self, *, pipeline_id: str, version: str, document_id: str) -> int: + """Increments and returns the number of times this document has been attempted. + + Not etag-guarded: Dapr Workflow retries a given activity task + sequentially, never concurrently, so there is exactly one writer at a + time for a given document's counter. + """ + key = self._attempts_key(pipeline_id, version, document_id) + current = self._get_json(key) or {'attempts': 0} + attempts = int(current.get('attempts', 0)) + 1 + self._save_json(key, {'attempts': attempts}) + return attempts + + # -- pipeline status ----------------------------------------------------- + + def read_status(self, *, pipeline_id: str, version: str) -> Optional[PipelineStatus]: + raw = self._get_json(self._status_key(pipeline_id, version)) + return PipelineStatus.from_dict(raw) if raw is not None else None + + def write_status(self, status: PipelineStatus) -> None: + self._save_json( + self._status_key(status.pipeline_id, status.requested_version), status.to_dict() + ) + + # -- activation (ETag-guarded active-version pointer) -------------------- + + def read_activation(self, pipeline_id: str) -> tuple[Optional[ActivationRecord], Optional[str]]: + """Reads the active-version pointer and its current ETag. + + Returns: + `(None, etag_or_none)` if no version has ever been activated for + this pipeline, else `(record, etag)`. The etag (which may be an + empty string for some state stores' "key doesn't exist yet" + case) must be threaded back into `write_activation` to detect a + concurrent activation. + """ + response = self._client.get_state( + store_name=self._state_store_name, + key=self._activation_key(pipeline_id), + state_metadata={'consistency': 'strong'}, + ) + if not response.data: + return None, response.etag + return ActivationRecord.from_dict(_decode_json(response.data)), response.etag + + def write_activation(self, record: ActivationRecord, *, etag: Optional[str]) -> None: + """Writes the active-version pointer, conditioned on `etag`. + + Args: + record: The new activation record to write. + etag: The etag last read via `read_activation` for this + pipeline_id (or `None`/empty if none was ever activated). + + Raises: + ActivationConflictError: Another writer updated the pointer + after `etag` was read (Dapr returns `ABORTED`). + """ + try: + self._client.save_state( + store_name=self._state_store_name, + key=self._activation_key(record.pipeline_id), + value=json.dumps(record.to_dict()), + etag=etag or None, + ) + except grpc.RpcError as exc: + if exc.code() == grpc.StatusCode.ABORTED: + raise ActivationConflictError( + f'Active-version pointer for pipeline {record.pipeline_id!r} was updated ' + 'concurrently; retry will re-read the current pointer.' + ) from exc + raise + + # -- key schema ------------------------------------------------------ + + @staticmethod + def _manifest_meta_key(pipeline_id: str, version: str) -> str: + return f'rag:manifest:{pipeline_id}:{version}:meta' + + @staticmethod + def _manifest_page_key(pipeline_id: str, version: str, page_index: int) -> str: + return f'rag:manifest:{pipeline_id}:{version}:page:{page_index}' + + @staticmethod + def _completion_key(pipeline_id: str, version: str, document_id: str) -> str: + return f'rag:completion:{pipeline_id}:{version}:{document_id}' + + @staticmethod + def _embed_progress_key(pipeline_id: str, version: str, document_id: str) -> str: + return f'rag:embed-progress:{pipeline_id}:{version}:{document_id}' + + @staticmethod + def _attempts_key(pipeline_id: str, version: str, document_id: str) -> str: + return f'rag:attempts:{pipeline_id}:{version}:{document_id}' + + @staticmethod + def _status_key(pipeline_id: str, version: str) -> str: + return f'rag:status:{pipeline_id}:{version}' + + @staticmethod + def _activation_key(pipeline_id: str) -> str: + return f'rag:activation:{pipeline_id}' + + # -- JSON helpers ------------------------------------------------------ + # DaprClient.save_state/get_state move only bytes/str -- see + # dapr/clients/grpc/_state.py -- so JSON (de)serialization happens here, + # once, rather than at every call site. + + def _get_json(self, key: str) -> Optional[Any]: + response = self._client.get_state(store_name=self._state_store_name, key=key) + if not response.data: + return None + return _decode_json(response.data) + + def _save_json(self, key: str, value: Any) -> None: + self._client.save_state(store_name=self._state_store_name, key=key, value=json.dumps(value)) + + +def _decode_json(data: Any) -> Any: + text = data.decode('utf-8') if isinstance(data, bytes) else data + return json.loads(text) + + +def _chunked(items: Sequence[T], size: int) -> Iterator[list[T]]: + for start in range(0, len(items), size): + yield list(items[start : start + size]) diff --git a/dapr/ext/rag/testing.py b/dapr/ext/rag/testing.py new file mode 100644 index 000000000..f7b07225b --- /dev/null +++ b/dapr/ext/rag/testing.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +# A demo/test-only hook for proving crash recovery. Every trigger is `None` +# (disabled) by default, so a `DurableRAGPipeline` constructed without an +# explicit `failure_injector=` behaves exactly as it would without this +# module existing at all -- this must never be wired into a normal +# production code path. See `examples/rag/failure_demo.py` for the intended +# usage: run a worker with one trigger configured, watch it hard-exit +# mid-run, then restart the *same* worker process (with the trigger +# disabled) and observe the still-active workflow instance resume and skip +# already-completed embedding work. + +from __future__ import annotations + +import logging +import os +from dataclasses import dataclass, field +from typing import NoReturn, Optional + +logger = logging.getLogger(__name__) + + +@dataclass +class FailureInjector: + """Deliberately crashes the current process at a chosen point in ingestion. + + Every field is `None` (disabled) by default. At most one trigger should + be set at a time for a given demo run. + """ + + fail_after_documents: Optional[int] = None + """Hard-exit once more than this many `process_document` attempts have started.""" + + fail_after_embedding_before_completion_for_document: Optional[str] = None + """Hard-exit right after this document's vectors are upserted, before its + completion record is written -- proves a redelivered activity resumes + from state rather than re-embedding.""" + + fail_during_batch_index: Optional[int] = None + """Hard-exit right before embedding the batch at this index, for any document.""" + + _processed_count: int = field(default=0, init=False, repr=False) + + def maybe_fail_before_start(self, document_id: str, attempt: int) -> None: + """Call once per `process_document` attempt, before any real work starts.""" + if self.fail_after_documents is None: + return + self._processed_count += 1 + if self._processed_count > self.fail_after_documents: + _simulate_crash( + f'FailureInjector: simulating a crash after {self.fail_after_documents} ' + f'document(s) (triggered while starting {document_id!r}, attempt {attempt}).' + ) + + def maybe_fail_during_embedding(self, document_id: str, batch_index: int) -> None: + """Call immediately before embedding one batch of a document's chunks.""" + if self.fail_during_batch_index is not None and batch_index == self.fail_during_batch_index: + _simulate_crash( + f'FailureInjector: simulating a crash during embedding batch {batch_index} ' + f'of {document_id!r}.' + ) + + def maybe_fail_after_embedding_before_completion( + self, document_id: str, batch_index: int + ) -> None: + """Call immediately after a batch's vectors are durably upserted.""" + if self.fail_after_embedding_before_completion_for_document == document_id: + _simulate_crash( + f'FailureInjector: simulating a crash after embedding batch {batch_index} of ' + f'{document_id!r} but before its completion record is written.' + ) + + +def _simulate_crash(message: str) -> NoReturn: + logger.warning(message) + # os._exit (not sys.exit) skips atexit handlers and finally blocks, which is + # the point: a real process crash or `kill -9` gets neither, and the demo + # is only meaningful if recovery doesn't depend on graceful-shutdown code. + os._exit(70) diff --git a/dapr/ext/rag/triggers.py b/dapr/ext/rag/triggers.py new file mode 100644 index 000000000..22c856892 --- /dev/null +++ b/dapr/ext/rag/triggers.py @@ -0,0 +1,234 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +# Normalizes cloud storage event notifications into a small, provider-agnostic +# shape (`SourceChangeEvent`). Deliberately separate from `pipeline.py`: +# parsing an S3 Event Notification or an Azure Event Grid envelope (delivered +# through Service Bus, or directly through a Dapr pub/sub component) is +# provider-specific plumbing that has nothing to do with durable ingestion, +# and keeping it here means `DurableRAGPipeline` never needs to know these +# payload shapes exist. See `examples/rag/pubsub_trigger.py` (S3/EventBridge) +# and `examples/rag/pubsub_trigger_servicebus.py` (Azure) for runnable Dapr +# pub/sub subscribers built on these functions, and `EventDeduplicator` below +# for handling at-least-once/duplicate delivery. + +from __future__ import annotations + +import re +from dataclasses import dataclass +from typing import Any, Optional +from urllib.parse import unquote_plus + +from dapr.clients import DaprClient +from dapr.ext.rag.fingerprints import compute_content_hash +from dapr.ext.rag.models import SourceChangeEvent, SourceProvider + + +@dataclass(frozen=True, slots=True) +class StorageEventNotification: + """A normalized "something changed at this object/blob" event. + + An intermediate shape: still close to the provider's own vocabulary + (`event_type` is the raw event name), before `to_source_change_event` + maps it onto the fully provider-neutral `SourceChangeEvent`. + """ + + provider: str # SourceProvider value + event_type: str # provider-native event name, e.g. 'ObjectCreated:Put' + bucket_or_container: str + key_or_blob_name: str + event_id: str + etag: Optional[str] + version_id: Optional[str] + occurred_at: Optional[str] + raw: dict[str, Any] + + +def parse_s3_event_notifications(payload: dict[str, Any]) -> list[StorageEventNotification]: + """Parses an S3 Event Notification payload into `StorageEventNotification`s. + + Handles the standard S3 Event Notification JSON shape (`{"Records": [...]}`) + as delivered via SNS, SQS, or EventBridge and forwarded to a Dapr + subscriber through a pub/sub component -- one payload commonly batches + multiple records. + + Args: + payload: The decoded event body. + + Returns: + One `StorageEventNotification` per record (empty if `payload` has no + `Records`, e.g. an unrelated or malformed message). + """ + notifications = [] + for record in payload.get('Records', []): + bucket = record.get('s3', {}).get('bucket', {}).get('name', '') + s3_object = record.get('s3', {}).get('object', {}) + raw_key = s3_object.get('key', '') + event_name = record.get('eventName', '') + # S3 notifications have no top-level event ID; `sequencer` is a + # real per-object-version identifier when present, so it's a stable + # idempotency key -- fall back to hashing the identifying fields. + sequencer = s3_object.get('sequencer') + event_id = sequencer or compute_content_hash( + f'{bucket}:{raw_key}:{event_name}:{s3_object.get("eTag", "")}'.encode() + ) + notifications.append( + StorageEventNotification( + provider=SourceProvider.S3.value, + event_type=event_name, + bucket_or_container=bucket, + key_or_blob_name=unquote_plus(raw_key), + event_id=event_id, + etag=_strip_quotes(s3_object.get('eTag')), + version_id=s3_object.get('versionId'), + occurred_at=record.get('eventTime'), + raw=record, + ) + ) + return notifications + + +_AZURE_BLOB_SUBJECT = re.compile(r'/containers/(?P[^/]+)/blobs/(?P.+)$') + + +def parse_azure_blob_event(payload: dict[str, Any]) -> Optional[StorageEventNotification]: + """Parses one Azure Event Grid blob-storage event into a `StorageEventNotification`. + + Handles `Microsoft.Storage.BlobCreated` / `BlobDeleted` events (and any + other Storage event carrying the same `subject` shape), in either Event + Grid schema (`eventType`/`eventTime`/`id`) or CloudEvents schema + (`type`/`time`/`id`) -- both are delivered with the same field meanings + under different names. Works whether the event arrives directly through + a Dapr pub/sub component or was relayed through Azure Service Bus first + (unwrap the Service Bus message body before calling this). Event Grid + may deliver a batch as a JSON array; call this once per element. + + Args: + payload: One decoded Event Grid/CloudEvents event. + + Returns: + A `StorageEventNotification`, or `None` if `payload["subject"]` + doesn't match the expected `.../containers/{c}/blobs/{b}` shape. + """ + match = _AZURE_BLOB_SUBJECT.search(payload.get('subject', '')) + if not match: + return None + data = payload.get('data', {}) + return StorageEventNotification( + provider=SourceProvider.AZURE_BLOB.value, + event_type=payload.get('eventType') or payload.get('type', ''), + bucket_or_container=match.group('container'), + key_or_blob_name=match.group('blob_name'), + event_id=payload.get('id', ''), + etag=_strip_quotes(data.get('etag')), + version_id=data.get('versionId') or data.get('snapshot'), + occurred_at=payload.get('eventTime') or payload.get('time'), + raw=payload, + ) + + +_DELETED_EVENT_NAMES = frozenset({'Microsoft.Storage.BlobDeleted'}) + + +def to_source_change_event(notification: StorageEventNotification) -> SourceChangeEvent: + """Maps a provider-flavored `StorageEventNotification` onto a `SourceChangeEvent`. + + `event_type` is normalized to `'created'` / `'deleted'` (neither S3 nor + Azure Blob notifications distinguish a fresh upload from an overwrite at + the event-name level -- both are `'created'`; the pipeline's own + ETag-based document-changed detection, not the event stream, is the + authority on whether content actually changed). + """ + if notification.provider == SourceProvider.S3.value: + event_type = 'deleted' if notification.event_type.startswith('ObjectRemoved') else 'created' + source_document_id = ( + f's3://{notification.bucket_or_container}/{notification.key_or_blob_name}' + ) + else: + event_type = 'deleted' if notification.event_type in _DELETED_EVENT_NAMES else 'created' + source_document_id = ( + f'azure-blob://{notification.bucket_or_container}/{notification.key_or_blob_name}' + ) + + return SourceChangeEvent( + provider=notification.provider, + event_type=event_type, + source_document_id=source_document_id, + uri=source_document_id, + etag=notification.etag, + version_id=notification.version_id, + occurred_at=notification.occurred_at, + event_id=notification.event_id, + ) + + +class EventDeduplicator: + """Tracks handled event IDs in Dapr state to absorb duplicate deliveries. + + Service Bus, SQS, and Event Grid all offer only at-least-once delivery, + and a consumer restart can also cause redelivery. Checking (and + recording) `event.event_id` here before acting on an event keeps a + duplicate from starting a second, redundant ingestion run. + """ + + def __init__( + self, + *, + state_store_name: str, + dapr_client: Optional[DaprClient] = None, + ttl_seconds: int = 86400, + ) -> None: + """Initializes an EventDeduplicator. + + Args: + state_store_name: The Dapr state store to record seen event IDs in. + dapr_client: A `DaprClient` to reuse; a new one is created (and + owned/closed by this instance) when omitted. + ttl_seconds: How long a seen-event marker is kept. Only needs to + cover the provider's own redelivery/retry window, not forever. + """ + self._state_store_name = state_store_name + self._owns_client = dapr_client is None + self._client = dapr_client or DaprClient() + self._ttl_seconds = ttl_seconds + + def already_seen(self, event_id: str) -> bool: + """Returns whether `event_id` was already marked seen.""" + response = self._client.get_state( + store_name=self._state_store_name, key=self._key(event_id) + ) + return bool(response.data) + + def mark_seen(self, event_id: str) -> None: + """Records `event_id` as handled, for `ttl_seconds`.""" + self._client.save_state( + store_name=self._state_store_name, + key=self._key(event_id), + value='1', + state_metadata={'ttlInSeconds': str(self._ttl_seconds)}, + ) + + def close(self) -> None: + """Releases the underlying `DaprClient`, if this instance created it.""" + if self._owns_client: + self._client.close() + + @staticmethod + def _key(event_id: str) -> str: + return f'rag:seen-event:{event_id}' + + +def _strip_quotes(value: Optional[str]) -> Optional[str]: + return value.strip('"') if value else value diff --git a/dapr/ext/rag/vector_stores/__init__.py b/dapr/ext/rag/vector_stores/__init__.py new file mode 100644 index 000000000..c48953697 --- /dev/null +++ b/dapr/ext/rag/vector_stores/__init__.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from dapr.ext.rag.vector_stores.azure_ai_search import AzureAISearchVectorStore +from dapr.ext.rag.vector_stores.base import VectorIndex +from dapr.ext.rag.vector_stores.pgvector import PgVectorStore +from dapr.ext.rag.vector_stores.pinecone import PineconeVectorStore + +__all__ = [ + 'VectorIndex', + 'PgVectorStore', + 'PineconeVectorStore', + 'AzureAISearchVectorStore', +] diff --git a/dapr/ext/rag/vector_stores/azure_ai_search.py b/dapr/ext/rag/vector_stores/azure_ai_search.py new file mode 100644 index 000000000..3fa1f3e0b --- /dev/null +++ b/dapr/ext/rag/vector_stores/azure_ai_search.py @@ -0,0 +1,879 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +# One physical Azure AI Search index per pipeline version (`{index_base_name} +# -{version}`), routed through a stable alias (`{index_base_name}-active` by +# default) that `activate_version` atomically repoints -- see that method's +# docstring for the read-check-write-poll sequence the spec requires. Unlike +# pgvector/Pinecone, "version" here is *not* a column/namespace inside one +# always-queryable index; it names a whole separate physical index, so the +# Dapr-state `ActivationRecord` (state.py) alone is not enough to route +# Azure AI Search traffic -- the alias is the actual routing mechanism, and +# this class participates in the two-step activation sequence documented on +# `VectorIndex.activate_version`. +# +# No local/offline emulator exists for Azure AI Search (unlike LocalStack/ +# Azurite for S3/Blob), so this class is tested only via dependency injection +# (`index_client=`/`search_client_factory=`) against fakes -- see +# dapr/ext/rag/AGENTS.md and the completion report for what that does and +# does not verify. + +from __future__ import annotations + +import time +from typing import Any, Callable, Iterable, Iterator, Optional, TypeVar + +from dapr.ext.rag.errors import ( + OptionalDependencyError, + RagError, + TransientVectorStoreError, + VectorStoreError, +) +from dapr.ext.rag.models import QueryMatch, UpsertResult, ValidationResult, VectorRecord +from dapr.ext.rag.vector_stores.base import VectorIndex + +# See dapr/ext/rag/AGENTS.md for why the optional-dependency guard lives here, +# per adapter module, rather than once in dapr/ext/rag/__init__.py. +try: + from azure.core.credentials import AzureKeyCredential + from azure.search.documents import SearchClient + from azure.search.documents.indexes import SearchIndexClient + from azure.search.documents.indexes.models import ( + HnswAlgorithmConfiguration, + HnswParameters, + SearchableField, + SearchField, + SearchFieldDataType, + SearchIndex, + SemanticConfiguration, + SemanticField, + SemanticPrioritizedFields, + SemanticSearch, + SimpleField, + VectorSearch, + VectorSearchProfile, + ) + from azure.search.documents.models import VectorizedQuery +except ImportError: # pragma: no cover - exercised only without azure-search-documents installed + AzureKeyCredential = None # type: ignore[assignment,misc] + SearchClient = SearchIndexClient = None # type: ignore[assignment,misc] + HnswAlgorithmConfiguration = HnswParameters = SearchableField = None # type: ignore[assignment,misc] + SearchField = SearchFieldDataType = SearchIndex = None # type: ignore[assignment,misc] + SemanticConfiguration = SemanticField = SemanticPrioritizedFields = None # type: ignore[assignment,misc] + SemanticSearch = SimpleField = VectorSearch = VectorSearchProfile = None # type: ignore[assignment,misc] + VectorizedQuery = None # type: ignore[assignment,misc] + +try: + from azure.identity import DefaultAzureCredential +except ImportError: # pragma: no cover - exercised only without azure-identity installed + DefaultAzureCredential = None # type: ignore[assignment,misc] + +try: + import httpx +except ImportError: # pragma: no cover - exercised only without httpx installed + httpx = None # type: ignore[assignment] + +_VECTOR_FIELD_NAME = 'content_vector' +_VECTOR_PROFILE_NAME = 'rag-vector-profile' +_HNSW_ALGORITHM_NAME = 'rag-hnsw' + +# Index aliases were briefly a beta SDK feature (11.4.0b1) but were removed +# before the stable 11.4.0 release and have not been restored in any +# azure-search-documents version since (verified against the SDK's own +# CHANGELOG.md on 2026-09-10) -- SearchIndexClient has no alias method at all. +# The REST API itself does support them, so activate_version talks to it +# directly instead. Verified against Microsoft's REST API reference +# (searchservice.aliases.createorupdate) on 2026-09-10. +# +# Foundry IQ knowledge sources are the same story: `SearchIndexKnowledgeSource`/ +# `SearchIndexKnowledgeSourceParameters` do not exist in azure-search-documents +# 11.6.0 (confirmed by introspecting the installed package on 2026-09-10) even +# though Microsoft's own docs illustrate them as SDK calls -- the feature is +# GA at the REST layer (2026-04-01) but not yet wrapped by this SDK version. +# register_foundry_iq_knowledge_source below talks to the REST API directly, +# reusing the same transport/credential/headers as the alias calls (same +# management-plane surface, same auth) -- verified against Microsoft's REST +# API reference (searchservice.knowledge-sources.create-or-update) on +# 2026-09-10. Named _MANAGEMENT_* (not _ALIAS_*) since both features share it. +_MANAGEMENT_API_VERSION = '2026-04-01' +_MANAGEMENT_REQUEST_TIMEOUT_SECONDS = 30.0 + +# Classified by exception *name*, not `isinstance` against the azure-core +# classes above: when azure-core isn't installed, every name in that +# try/except aliases to the same `Exception`, which would make an isinstance +# check match (and thus misclassify) anything. +_NOT_FOUND_EXCEPTION_NAMES = frozenset({'ResourceNotFoundError'}) +_TRANSIENT_EXCEPTION_NAMES = frozenset( + { + 'ServiceRequestError', + 'ServiceResponseError', + # httpx's own transient exceptions, from the alias REST calls below. + 'ConnectError', + 'ConnectTimeout', + 'ReadTimeout', + 'WriteTimeout', + 'PoolTimeout', + 'TimeoutException', + } +) + + +def _is_not_found(exc: Exception) -> bool: + return type(exc).__name__ in _NOT_FOUND_EXCEPTION_NAMES + + +# Every schema field except the vector -- the default `select` for query(), +# so the (large) vector is never returned unless a caller explicitly asks +# for it via `include_vector=True`. +_DEFAULT_SELECT_FIELDS = [ + 'id', + 'content', + 'title', + 'source_uri', + 'source_provider', + 'source_document_id', + 'source_etag', + 'source_version', + 'source_content_hash', + 'document_ordinal', + 'chunk_ordinal', + 'chunk_content_hash', + 'content_type', + 'pipeline_id', + 'pipeline_version', + 'workflow_instance_id', + 'parser_version', + 'splitter_version', + 'embedding_provider', + 'embedding_model', + 'ingested_at', + 'tenant_id', + 'authorization_groups', +] + +T = TypeVar('T') + + +class AzureAISearchVectorStore(VectorIndex): + """An Azure AI Search-backed `VectorIndex`, one physical index per version. + + Supports pure-vector, pure-keyword, and hybrid (default) retrieval, with + optional semantic ranking when `semantic_configuration_name` is set and + the Search service's tier/configuration supports it. + """ + + def __init__( + self, + *, + endpoint: str, + index_base_name: str, + alias_name: Optional[str] = None, + credential: Optional[Any] = None, + api_key: Optional[str] = None, + vector_dimensions: Optional[int] = None, + semantic_configuration_name: Optional[str] = None, + batch_size: int = 100, + max_batch_retries: int = 3, + alias_poll_attempts: int = 5, + alias_poll_interval_seconds: float = 1.0, + index_client: Optional[Any] = None, + search_client_factory: Optional[Callable[[str], Any]] = None, + alias_transport: Optional[Any] = None, + ) -> None: + """Initializes an AzureAISearchVectorStore. + + Args: + endpoint: The Azure AI Search service endpoint, e.g. + `https://my-search.search.windows.net`. Always required, even + when `index_client`/`search_client_factory` are injected: the + alias REST calls below (see the module comment on why they + aren't SDK calls) need it regardless. + index_base_name: The logical index name; each version gets its + own physical index `{index_base_name}-{version}`. + alias_name: The stable alias query traffic should read through. + Defaults to `{index_base_name}-active`. + credential: An `azure-identity` credential for Microsoft Entra ID + authentication. Defaults to `DefaultAzureCredential()` when + neither this nor `api_key` is given. + api_key: Optional API key, for development only. Never logged. + vector_dimensions: The embedding vector width, used to create a + new physical index's vector field. Inferred from the first + batch passed to `upsert` when omitted. + semantic_configuration_name: Enables semantic ranking with this + configuration name when the Search service tier supports it; + `None` disables it. + batch_size: Records per `merge_or_upload_documents` call. + max_batch_retries: How many times to retry just the batch + members Azure AI Search reported as failed before raising. + alias_poll_attempts: How many times `activate_version` re-checks + the alias mapping after switching it, waiting for the + propagation Azure AI Search documents for alias updates. + alias_poll_interval_seconds: Delay between alias-mapping checks. + index_client: A pre-built `SearchIndexClient` (index administration) + to use instead of constructing one -- bypasses the + `azure-search-documents` dependency check, which is how tests + exercise index/document operations without it installed. + Requires `search_client_factory` too. + search_client_factory: A callable `(index_name) -> SearchClient` + (document operations), used instead of constructing one per + physical index -- required when `index_client` is given. + alias_transport: An object exposing `.get(url, headers=...)` / + `.put(url, json=..., headers=...)` returning a + `.status_code`/`.text`/`.json()`-shaped response (i.e. an + `httpx.Client`-compatible interface), used for alias REST + calls instead of constructing a real `httpx.Client` -- + bypasses the `httpx` dependency check, which is how tests + exercise `activate_version` without it installed. + + Raises: + OptionalDependencyError: `azure-search-documents` is not + installed and no `index_client` was given; `httpx` is not + installed and no `alias_transport` was given; or + `azure-identity` is needed to build the default credential + and is not installed. + ValueError: `index_client` was given without `search_client_factory`. + """ + self._endpoint = endpoint.rstrip('/') + self._index_base_name = index_base_name + self._alias_name = alias_name or f'{index_base_name}-active' + self._vector_dimensions = vector_dimensions + self._semantic_configuration_name = semantic_configuration_name + self._batch_size = batch_size + self._max_batch_retries = max_batch_retries + self._alias_poll_attempts = alias_poll_attempts + self._alias_poll_interval_seconds = alias_poll_interval_seconds + self._ensured_indexes: set[str] = set() + self._search_clients: dict[str, Any] = {} + + if index_client is not None and search_client_factory is None: + raise ValueError('search_client_factory is required when index_client is injected.') + if index_client is None and (SearchIndexClient is None or SearchClient is None): + raise OptionalDependencyError( + package='azure-search-documents', + extra='rag-azure-search', + feature='AzureAISearchVectorStore', + ) + + # A credential is only actually needed if *something* below isn't fully + # dependency-injected; when both index_client and alias_transport are + # given (the unit-test path), no azure-identity dependency is touched at all. + resolved_credential = None + if index_client is None or alias_transport is None: + resolved_credential = _resolve_credential(credential, api_key) + + if index_client is not None: + # The guard clause above already rejected index_client without + # search_client_factory; this repeats that fact for mypy. + assert search_client_factory is not None + self._index_client = index_client + self._search_client_factory = search_client_factory + else: + # index_client is None => the `if` above always resolved a credential. + assert resolved_credential is not None + self._index_client = SearchIndexClient( + endpoint=endpoint, credential=resolved_credential + ) + self._search_client_factory = lambda index_name: SearchClient( + endpoint=endpoint, index_name=index_name, credential=resolved_credential + ) + + self._alias_credential = resolved_credential + if alias_transport is not None: + self._alias_transport = alias_transport + else: + if httpx is None: + raise OptionalDependencyError( + package='httpx', extra='rag-azure-search', feature='AzureAISearchVectorStore' + ) + self._alias_transport = httpx.Client(timeout=_MANAGEMENT_REQUEST_TIMEOUT_SECONDS) + + @property + def target_index_name(self) -> str: + return self._index_base_name + + @property + def alias_name(self) -> str: + """The stable alias query traffic reads through.""" + return self._alias_name + + @property + def store_type(self) -> str: + return 'azure-ai-search' + + def upsert(self, records: Iterable[VectorRecord], version: str) -> UpsertResult: + materialized = list(records) + if not materialized: + return UpsertResult(upserted_count=0, version=version) + + index_name = self._index_name_for(version) + dimensions = self._vector_dimensions or len(materialized[0].embedding) + self._ensure_index(index_name, dimensions) + search_client = self._search_client(index_name) + + upserted_count = 0 + for batch in _batched(materialized, self._batch_size): + documents = [_to_search_document(record) for record in batch] + upserted_count += self._upload_with_retry(search_client, documents) + return UpsertResult(upserted_count=upserted_count, version=version) + + def delete_document(self, document_id: str, version: str) -> None: + index_name = self._index_name_for(version) + search_client = self._search_client(index_name) + try: + matches = search_client.search( + search_text='*', + filter=f"source_document_id eq '{_escape_odata_string(document_id)}'", + select=['id'], + top=1000, + ) + keys = [match['id'] for match in matches] + if keys: + search_client.delete_documents(documents=[{'id': key} for key in keys]) + except Exception as exc: + raise self._classify(exc) from exc + + def validate_version(self, version: str) -> ValidationResult: + index_name = self._index_name_for(version) + try: + self._index_client.get_index(index_name) + except Exception as exc: + if _is_not_found(exc): + return ValidationResult( + valid=False, + version=version, + actual_chunk_count=0, + details=f'Index {index_name!r} does not exist.', + ) + raise self._classify(exc) from exc + + try: + count = self._search_client(index_name).get_document_count() + except Exception as exc: + raise self._classify(exc) from exc + + return ValidationResult( + valid=count > 0, + version=version, + actual_chunk_count=count, + details=f'{count} chunk(s) in index {index_name!r}.', + ) + + def activate_version(self, version: str, *, previous_version: Optional[str]) -> None: + """Atomically repoints the alias to this version's physical index. + + Retry-safe: reads the current alias mapping first and returns + immediately if it already points to the target index (an activity + retry after a partial failure, or a repeated activation, is then a + no-op). Otherwise switches the alias and polls until the new mapping + is observable, matching Azure AI Search's documented alias + propagation delay -- never deletes or repurposes `previous_version`'s + index. + + Raises: + TransientVectorStoreError: The alias switch failed transiently, + or didn't become observable within `alias_poll_attempts`. + VectorStoreError: The alias switch failed non-transiently. + """ + new_index_name = self._index_name_for(version) + if self._alias_indexes() == [new_index_name]: + return # already correct: idempotent no-op + + self._put_alias(new_index_name) + + for _attempt in range(self._alias_poll_attempts): + if self._alias_indexes() == [new_index_name]: + return + time.sleep(self._alias_poll_interval_seconds) + + raise TransientVectorStoreError( + f'Alias {self._alias_name!r} did not observably switch to {new_index_name!r} after ' + f'{self._alias_poll_attempts} check(s); a retry will re-check rather than switch again.' + ) + + def register_foundry_iq_knowledge_source( + self, + version: str, + *, + name: str, + description: Optional[str] = None, + source_data_fields: Optional[list[str]] = None, + search_fields: Optional[list[str]] = None, + ) -> None: + """Registers (or updates) a Foundry IQ search-index knowledge source for `version`. + + Opt-in and separate from this store's own retrieval path -- see + `docs/rag/foundry-iq.md` for the full rationale and the rule this + follows if a caller ever wires this up. Not called from anywhere in + `dapr.ext.rag` by default; a caller invokes this explicitly (e.g. from + a workflow activity chained strictly after `activate_version` + succeeds for `version`, as the doc specifies) when they want this + version's index registered with Foundry IQ. + + Deliberately targets `{index_base_name}-{version}` (the concrete + physical index), never the alias: whether a knowledge source resolves + an alias dynamically or binds to whatever index it pointed at when + created is undocumented, so this method assumes the more dangerous + case (it binds once) and always re-points the knowledge source + explicitly -- the same reasoning `activate_version` applies to the + alias itself. + + Idempotent: a no-op if the knowledge source already targets this + exact index and semantic configuration. + + Raises: + VectorStoreError: This store has no `semantic_configuration_name` + configured -- required on every search index knowledge source + by the `2026-04-01` REST API this method targets -- or the + registration request failed non-transiently. + TransientVectorStoreError: The registration request failed + transiently. + """ + if not self._semantic_configuration_name: + raise VectorStoreError( + 'register_foundry_iq_knowledge_source requires semantic_configuration_name to ' + 'be set on this AzureAISearchVectorStore -- required by every search index ' + 'knowledge source on the 2026-04-01 REST API this method targets.' + ) + target_index_name = self._index_name_for(version) + existing = self._get_knowledge_source(name) + existing_params = (existing or {}).get('searchIndexParameters') or {} + already_correct = ( + existing is not None + and existing_params.get('searchIndexName') == target_index_name + and existing_params.get('semanticConfigurationName') + == self._semantic_configuration_name + ) + if already_correct: + return # idempotent no-op + + self._put_knowledge_source( + name, + { + 'name': name, + 'kind': 'searchIndex', + 'description': description, + 'searchIndexParameters': { + 'searchIndexName': target_index_name, + 'semanticConfigurationName': self._semantic_configuration_name, + 'sourceDataFields': [{'name': field} for field in (source_data_fields or [])], + 'searchFields': [{'name': field} for field in (search_fields or [])], + }, + }, + ) + + def query( + self, + embedding: Iterable[float], + version: str, + *, + top_k: int = 5, + metadata_filter: Optional[dict[str, Any]] = None, + query_text: Optional[str] = None, + mode: str = 'hybrid', + include_vector: bool = False, + ) -> list[QueryMatch]: + """Runs a vector, keyword, or hybrid (default) search. + + Args: + mode: `'vector'`, `'keyword'`, or `'hybrid'` (vector + keyword, + merged by Azure AI Search -- the default, per this store's + design: hybrid retrieval is what the flagship sample uses). + include_vector: When `True`, includes the (large) embedding + vector field in results. Excluded by default. + """ + index_name = self._index_name_for(version) + search_client = self._search_client(index_name) + + search_kwargs: dict[str, Any] = {'top': top_k} + if not include_vector: + search_kwargs['select'] = _DEFAULT_SELECT_FIELDS + if metadata_filter: + search_kwargs['filter'] = _build_odata_filter(metadata_filter) + if mode in ('vector', 'hybrid'): + search_kwargs['vector_queries'] = [ + VectorizedQuery( + vector=list(embedding), k_nearest_neighbors=top_k, fields=_VECTOR_FIELD_NAME + ) + ] + if mode in ('keyword', 'hybrid'): + search_kwargs['search_text'] = query_text or '*' + if self._semantic_configuration_name and mode != 'vector': + search_kwargs['query_type'] = 'semantic' + search_kwargs['semantic_configuration_name'] = self._semantic_configuration_name + + try: + results = search_client.search(**search_kwargs) + except Exception as exc: + raise self._classify(exc) from exc + + return [ + _to_query_match(result, semantic=bool(self._semantic_configuration_name)) + for result in results + ] + + def close(self) -> None: + for client in (self._index_client, self._alias_transport, *self._search_clients.values()): + close = getattr(client, 'close', None) + if callable(close): + close() + + # -- internal helpers --------------------------------------------------- + + def _index_name_for(self, version: str) -> str: + return f'{self._index_base_name}-{version}' + + def _search_client(self, index_name: str) -> Any: + client = self._search_clients.get(index_name) + if client is None: + client = self._search_client_factory(index_name) + self._search_clients[index_name] = client + return client + + def _alias_indexes(self) -> list[str]: + """Returns the alias's current target index names, or `[]` if it doesn't exist yet. + + A direct REST call, not an SDK method -- see the module comment on + why `SearchIndexClient` has no alias support to call instead. + """ + try: + response = self._alias_transport.get(self._alias_url(), headers=self._alias_headers()) + except Exception as exc: + raise self._classify(exc) from exc + if response.status_code == 404: + return [] + _raise_for_alias_response(response) + return list(response.json().get('indexes', [])) + + def _put_alias(self, index_name: str) -> None: + try: + response = self._alias_transport.put( + self._alias_url(), + headers={**self._alias_headers(), 'Content-Type': 'application/json'}, + json={'name': self._alias_name, 'indexes': [index_name]}, + ) + except Exception as exc: + raise self._classify(exc) from exc + _raise_for_alias_response(response) + + def _alias_url(self) -> str: + return ( + f"{self._endpoint}/aliases('{self._alias_name}')?api-version={_MANAGEMENT_API_VERSION}" + ) + + def _get_knowledge_source(self, name: str) -> Optional[dict[str, Any]]: + try: + response = self._alias_transport.get( + self._knowledge_source_url(name), headers=self._alias_headers() + ) + except Exception as exc: + raise self._classify(exc) from exc + if response.status_code == 404: + return None + _raise_for_alias_response(response) + return dict(response.json()) + + def _put_knowledge_source(self, name: str, body: dict[str, Any]) -> None: + try: + response = self._alias_transport.put( + self._knowledge_source_url(name), + headers={**self._alias_headers(), 'Content-Type': 'application/json'}, + json=body, + ) + except Exception as exc: + raise self._classify(exc) from exc + _raise_for_alias_response(response) + + def _knowledge_source_url(self, name: str) -> str: + return f"{self._endpoint}/knowledgesources('{name}')?api-version={_MANAGEMENT_API_VERSION}" + + def _alias_headers(self) -> dict[str, str]: + """Auth headers for this store's REST management calls -- aliases and Foundry IQ + knowledge sources alike (both are otherwise-unsupported-by-the-SDK REST calls + against the same management plane, using the same credential).""" + api_key = getattr(self._alias_credential, 'key', None) + if api_key is not None: + return {'api-key': api_key} + if self._alias_credential is not None: + token = self._alias_credential.get_token('https://search.azure.com/.default') + return {'Authorization': f'Bearer {token.token}'} + return {} + + def _ensure_index(self, index_name: str, dimensions: int) -> None: + if index_name in self._ensured_indexes: + return + try: + existing = self._index_client.get_index(index_name) + except Exception as exc: + if not _is_not_found(exc): + raise self._classify(exc) from exc + existing = None + + try: + if existing is None: + schema = _build_index_schema( + index_name, dimensions, self._semantic_configuration_name + ) + self._index_client.create_index(schema) + else: + _validate_existing_schema(existing, dimensions) + except Exception as exc: + raise self._classify(exc) from exc + self._ensured_indexes.add(index_name) + + def _upload_with_retry(self, search_client: Any, documents: list[dict[str, Any]]) -> int: + pending = documents + succeeded_count = 0 + for attempt in range(self._max_batch_retries + 1): + try: + results = search_client.merge_or_upload_documents(documents=pending) + except Exception as exc: + raise self._classify(exc) from exc + + failed_keys = {result.key for result in results if not result.succeeded} + succeeded_count += len(pending) - len(failed_keys) + if not failed_keys: + return succeeded_count + if attempt == self._max_batch_retries: + raise TransientVectorStoreError( + f'{len(failed_keys)} document(s) failed to index after ' + f'{self._max_batch_retries} retry attempt(s): {sorted(failed_keys)}' + ) + pending = [ + doc for doc in pending if doc['id'] in failed_keys + ] # retry only failed members + return succeeded_count # unreachable: the loop above always returns or raises + + @staticmethod + def _classify(exc: Exception) -> RagError: + if type(exc).__name__ in _TRANSIENT_EXCEPTION_NAMES: + return TransientVectorStoreError(str(exc)) + status_code = getattr(exc, 'status_code', None) + if isinstance(status_code, int) and (status_code == 429 or status_code >= 500): + return TransientVectorStoreError(str(exc)) + return VectorStoreError(str(exc)) + + +def _raise_for_alias_response(response: Any) -> None: + """Raises a classified error for a non-2xx alias REST response. + + A separate check from `AzureAISearchVectorStore._classify` (which + classifies *exceptions*): httpx doesn't raise for a non-2xx status by + itself, so an HTTP-level failure surfaces as a normal response object, + not an exception, unless explicitly checked here. + """ + if response.status_code < 400: + return + message = f'Alias request failed with status {response.status_code}: {response.text}' + if response.status_code == 429 or response.status_code >= 500: + raise TransientVectorStoreError(message) + raise VectorStoreError(message) + + +def _resolve_credential(credential: Optional[Any], api_key: Optional[str]) -> Any: + if api_key is not None: + if AzureKeyCredential is None: + raise OptionalDependencyError( + package='azure-search-documents', + extra='rag-azure-search', + feature='AzureAISearchVectorStore', + ) + return AzureKeyCredential(api_key) + if credential is not None: + return credential + if DefaultAzureCredential is None: + raise OptionalDependencyError( + package='azure-identity', extra='rag-azure', feature='AzureAISearchVectorStore' + ) + return DefaultAzureCredential() + + +def _build_index_schema( + index_name: str, dimensions: int, semantic_configuration_name: Optional[str] +) -> Any: + if SimpleField is None: + # Reachable even with index_client/search_client_factory injected: DI bypasses + # constructing the *clients*, not the schema *model* classes used here. + raise OptionalDependencyError( + package='azure-search-documents', + extra='rag-azure-search', + feature='AzureAISearchVectorStore', + ) + fields = [ + SimpleField(name='id', type=SearchFieldDataType.String, key=True, filterable=True), + SearchableField(name='content', type=SearchFieldDataType.String), + SearchField( + name=_VECTOR_FIELD_NAME, + type=SearchFieldDataType.Collection(SearchFieldDataType.Single), + searchable=True, + vector_search_dimensions=dimensions, + vector_search_profile_name=_VECTOR_PROFILE_NAME, + ), + SearchableField( + name='title', type=SearchFieldDataType.String, filterable=True, sortable=True + ), + SimpleField(name='source_uri', type=SearchFieldDataType.String), + SimpleField(name='source_provider', type=SearchFieldDataType.String, filterable=True), + SimpleField(name='source_document_id', type=SearchFieldDataType.String, filterable=True), + SimpleField(name='source_etag', type=SearchFieldDataType.String), + SimpleField(name='source_version', type=SearchFieldDataType.String), + SimpleField(name='source_content_hash', type=SearchFieldDataType.String), + SimpleField( + name='document_ordinal', type=SearchFieldDataType.Int32, filterable=True, sortable=True + ), + SimpleField( + name='chunk_ordinal', type=SearchFieldDataType.Int32, filterable=True, sortable=True + ), + SimpleField(name='chunk_content_hash', type=SearchFieldDataType.String), + SimpleField(name='content_type', type=SearchFieldDataType.String, filterable=True), + SimpleField(name='pipeline_id', type=SearchFieldDataType.String, filterable=True), + SimpleField(name='pipeline_version', type=SearchFieldDataType.String, filterable=True), + SimpleField(name='workflow_instance_id', type=SearchFieldDataType.String, filterable=True), + # "*_version" (matching the spec's schema field names) holds a config + # *hash*, not a semantic version number -- see fingerprints.py. + SimpleField(name='parser_version', type=SearchFieldDataType.String), + SimpleField(name='splitter_version', type=SearchFieldDataType.String), + SimpleField(name='embedding_provider', type=SearchFieldDataType.String, filterable=True), + SimpleField(name='embedding_model', type=SearchFieldDataType.String, filterable=True), + SimpleField( + name='ingested_at', + type=SearchFieldDataType.DateTimeOffset, + filterable=True, + sortable=True, + ), + # Multi-tenancy pass-through: unset unless a caller's own Document/Chunk + # metadata populates them -- see dapr/ext/rag/AGENTS.md's limitations. + SimpleField(name='tenant_id', type=SearchFieldDataType.String, filterable=True), + SimpleField( + name='authorization_groups', + type=SearchFieldDataType.Collection(SearchFieldDataType.String), + filterable=True, + ), + ] + vector_search = VectorSearch( + algorithms=[ + HnswAlgorithmConfiguration( + name=_HNSW_ALGORITHM_NAME, + parameters=HnswParameters(m=4, ef_construction=400, ef_search=500, metric='cosine'), + ) + ], + profiles=[ + VectorSearchProfile( + name=_VECTOR_PROFILE_NAME, algorithm_configuration_name=_HNSW_ALGORITHM_NAME + ) + ], + ) + semantic_search = None + if semantic_configuration_name: + semantic_search = SemanticSearch( + configurations=[ + SemanticConfiguration( + name=semantic_configuration_name, + prioritized_fields=SemanticPrioritizedFields( + title_field=SemanticField(field_name='title'), + content_fields=[SemanticField(field_name='content')], + ), + ) + ] + ) + return SearchIndex( + name=index_name, fields=fields, vector_search=vector_search, semantic_search=semantic_search + ) + + +def _validate_existing_schema(existing_index: Any, expected_dimensions: int) -> None: + vector_field = next((f for f in existing_index.fields if f.name == _VECTOR_FIELD_NAME), None) + actual_dimensions = getattr(vector_field, 'vector_search_dimensions', None) + if actual_dimensions is not None and actual_dimensions != expected_dimensions: + raise VectorStoreError( + f'Index {existing_index.name!r} already exists with vector dimensions ' + f'{actual_dimensions}, but this pipeline is configured for {expected_dimensions}. ' + 'Use a different index_base_name or version rather than reusing a mismatched index.' + ) + + +def _to_search_document(record: VectorRecord) -> dict[str, Any]: + metadata = record.metadata + document = { + 'id': record.chunk_id, + 'content': record.content, + _VECTOR_FIELD_NAME: list(record.embedding), + 'title': metadata.get('source_name'), + 'source_uri': metadata.get('source_uri'), + 'source_provider': metadata.get('source_provider'), + 'source_document_id': record.document_id, + 'source_etag': metadata.get('source_etag'), + 'source_version': metadata.get('source_version_id'), + 'source_content_hash': metadata.get('source_content_hash'), + 'document_ordinal': metadata.get('document_ordinal'), + 'chunk_ordinal': metadata.get('chunk_ordinal'), + 'chunk_content_hash': metadata.get('chunk_content_hash'), + 'content_type': metadata.get('source_content_type'), + 'pipeline_id': metadata.get('pipeline_id'), + 'pipeline_version': metadata.get('target_version'), + 'workflow_instance_id': metadata.get('workflow_instance_id'), + 'parser_version': metadata.get('parser_config_hash'), + 'splitter_version': metadata.get('splitter_config_hash'), + 'embedding_provider': metadata.get('embedding_provider'), + 'embedding_model': metadata.get('embedding_model'), + 'ingested_at': metadata.get('ingested_at'), + 'tenant_id': metadata.get('tenant_id'), + 'authorization_groups': metadata.get('authorization_groups') or [], + } + return {key: value for key, value in document.items() if value is not None} + + +def _to_query_match(result: Any, *, semantic: bool) -> QueryMatch: + fields = dict(result) + score = fields.pop('@search.score', 0.0) + if semantic: + score = fields.pop('@search.reranker_score', None) or score + for key in list(fields): + if key.startswith('@search.'): + fields.pop(key) + chunk_id = fields.pop('id', '') + content = fields.pop('content', '') or '' + document_id = fields.get('source_document_id', '') or '' + return QueryMatch( + chunk_id=chunk_id, + document_id=document_id, + content=content, + score=float(score), + metadata=fields, + ) + + +def _build_odata_filter(metadata_filter: dict[str, Any]) -> str: + clauses = [] + for field, value in metadata_filter.items(): + if isinstance(value, str): + clauses.append(f"{field} eq '{_escape_odata_string(value)}'") + elif isinstance(value, bool): + clauses.append(f'{field} eq {"true" if value else "false"}') + elif isinstance(value, (int, float)): + clauses.append(f'{field} eq {value}') + else: + raise ValueError( + f'Unsupported metadata_filter value type for {field!r}: {type(value).__name__}' + ) + return ' and '.join(clauses) + + +def _escape_odata_string(value: str) -> str: + return value.replace("'", "''") + + +def _batched(items: list[T], size: int) -> Iterator[list[T]]: + for start in range(0, len(items), size): + yield items[start : start + size] diff --git a/dapr/ext/rag/vector_stores/base.py b/dapr/ext/rag/vector_stores/base.py new file mode 100644 index 000000000..5c4700d35 --- /dev/null +++ b/dapr/ext/rag/vector_stores/base.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import Any, Iterable, Optional, Sequence + +from dapr.ext.rag.models import QueryMatch, UpsertResult, ValidationResult, VectorRecord + + +class VectorIndex(ABC): + """A versioned vector store: every write and read is scoped to a version. + + Versions let a new ingestion run build a complete index alongside the + currently-active one without ever writing into it -- see `state.py`'s + `PipelineStateStore.write_activation` for how a version is promoted to + active only after `validate_version` passes. Implementations perform I/O + and must only ever be called from within a workflow activity, never from + the orchestrator. + """ + + @property + @abstractmethod + def target_index_name(self) -> str: + """The collection/index name this store writes to, for provenance.""" + + @property + @abstractmethod + def store_type(self) -> str: + """A short, stable name identifying this store (e.g. 'pgvector').""" + + @abstractmethod + def upsert(self, records: Iterable[VectorRecord], version: str) -> UpsertResult: + """Writes records into `version`, keyed by each record's `chunk_id`. + + Must be idempotent: upserting the same `chunk_id` twice (e.g. after + an activity retry) overwrites rather than duplicates. + + Raises: + TransientVectorStoreError: The write failed transiently. + VectorStoreError: The write failed non-transiently. + """ + + @abstractmethod + def delete_document(self, document_id: str, version: str) -> None: + """Removes every chunk belonging to `document_id` within `version`.""" + + @abstractmethod + def validate_version(self, version: str) -> ValidationResult: + """Reports what this store actually holds for `version`. + + Implementations can only observe their own contents, so + `expected_document_count` / `expected_chunk_count` are left `None` + here; `pipeline.py`'s validation activity fills those in from the + manifest before using the result to gate activation. + """ + + @abstractmethod + def query( + self, + embedding: Sequence[float], + version: str, + *, + top_k: int = 5, + metadata_filter: Optional[dict[str, Any]] = None, + query_text: Optional[str] = None, + ) -> list[QueryMatch]: + """Runs a similarity search scoped to one version. + + Not part of the spec's original `VectorIndex` sketch (which covers + only writes and validation), but added here rather than in + provider-specific code: retrieval is an explicit requirement (the + sample CLI's "query" command, and `retrieval.py`'s active-version + helper), and this is the one place that can serve it without + provider-specific branching leaking into either of those callers. + + Args: + embedding: The query embedding, from the same model used to + embed the indexed chunks. + version: The version to search within. + top_k: Maximum number of matches to return. + metadata_filter: An optional, store-specific metadata filter + (e.g. `{'document_id': {'$eq': '...'}}` for Pinecone). + query_text: The original query text, for stores that combine + keyword/full-text search with vector search (e.g. + `AzureAISearchVectorStore`'s hybrid mode). Purely + vector-based stores accept and ignore it. + + Returns: + Up to `top_k` `QueryMatch`es, ordered by descending score. + """ + + def activate_version(self, version: str, *, previous_version: Optional[str]) -> None: + """Performs any store-native activation step, in addition to the Dapr-state pointer. + + The default implementation is a no-op: most stores (pgvector, + Pinecone) have no separate "physical" activation step -- the Dapr + state `ActivationRecord` (see `state.py`) is the only routing + mechanism, since `version` there is just a column/namespace value + within one always-queryable physical index/table. + + A store built around one physical index *per version* (e.g. + `AzureAISearchVectorStore`, which creates `{base}-{version}`) + overrides this to atomically point a stable alias at the new + version's index -- see that class for the retry-safe read-check- + write-poll sequence this method is expected to perform. Called from + `pipeline.py`'s `_activity_activate_version`, inside a workflow + activity, before the Dapr-state write -- so a retry after a partial + failure re-checks this store's own state first and finds it already + correct (no-op) rather than double-switching. + + Must be idempotent and safe to call repeatedly with the same + `version` (e.g. on activity retry). + + Raises: + TransientVectorStoreError: The activation step failed transiently. + VectorStoreError: The activation step failed non-transiently. + """ + + def close(self) -> None: + """Releases any held resources (connections, sessions). Optional to override.""" diff --git a/dapr/ext/rag/vector_stores/pgvector.py b/dapr/ext/rag/vector_stores/pgvector.py new file mode 100644 index 000000000..ef16423c1 --- /dev/null +++ b/dapr/ext/rag/vector_stores/pgvector.py @@ -0,0 +1,277 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from __future__ import annotations + +import json +import re +from typing import Any, Callable, Iterable, Optional + +from dapr.ext.rag.errors import ( + OptionalDependencyError, + RagError, + TransientVectorStoreError, + VectorStoreError, +) +from dapr.ext.rag.models import QueryMatch, UpsertResult, ValidationResult, VectorRecord +from dapr.ext.rag.vector_stores.base import VectorIndex + +# See dapr/ext/rag/AGENTS.md for why the optional-dependency guard lives here, +# per adapter module, rather than once in dapr/ext/rag/__init__.py. +try: + import psycopg +except ImportError: # pragma: no cover - exercised only without psycopg installed + psycopg = None # type: ignore[assignment] + +_SAFE_IDENTIFIER = re.compile(r'^[A-Za-z_][A-Za-z0-9_]*$') +_TRANSIENT_EXCEPTION_NAMES = frozenset( + {'OperationalError', 'InterfaceError', 'AdminShutdown', 'ConnectionTimeout'} +) + + +class PgVectorStore(VectorIndex): + """A pgvector-backed `VectorIndex`. + + All versions of one `collection` share a single physical table, scoped by + a `version` column with a `(version, chunk_id)` primary key -- so writes + are idempotent per version, queries can be scoped to exactly one version, + and no separate physical database or table is needed per version. Schema + (the table, its `vector` extension, and a supporting index) is created on + first use if missing, and never drops or alters an existing table. + """ + + def __init__( + self, + *, + connection_string: Optional[str] = None, + collection: str, + embedding_dimensions: Optional[int] = None, + connection_factory: Optional[Callable[[], Any]] = None, + ) -> None: + """Initializes a PgVectorStore. + + Args: + connection_string: A libpq connection string/URI. Required + unless `connection_factory` is given. + collection: A logical name for this index; mapped to the table + `rag_chunks_{collection}` (collection must match + `^[A-Za-z_][A-Za-z0-9_]*$`, since SQL identifiers can't be + parameterized). + embedding_dimensions: The embedding vector width, used to create + the table's `vector(n)` column. If omitted, inferred from the + first batch passed to `upsert`. + connection_factory: A zero-argument callable returning a + psycopg-connection-like context manager, used instead of + `psycopg.connect(connection_string)` -- bypasses the + `psycopg` dependency check entirely, which is how tests + exercise this class without it installed. A new connection is + requested per call, which keeps this class safe to share + across concurrently-running activities without pooling. + + Raises: + OptionalDependencyError: `psycopg` is not installed and no + `connection_factory` was given. + ValueError: `collection` isn't a safe SQL identifier, or neither + `connection_string` nor `connection_factory` was given. + """ + if not _SAFE_IDENTIFIER.match(collection): + raise ValueError( + f'collection={collection!r} must match {_SAFE_IDENTIFIER.pattern!r} to be used ' + 'as part of a SQL table name.' + ) + self._collection = collection + self._table = f'rag_chunks_{collection}' + self._embedding_dimensions = embedding_dimensions + self._schema_ready = False + + if connection_factory is not None: + self._connection_factory = connection_factory + else: + if psycopg is None: + raise OptionalDependencyError( + package='psycopg', extra='rag-pgvector', feature='PgVectorStore' + ) + if not connection_string: + raise ValueError('PgVectorStore requires connection_string or connection_factory.') + self._connection_factory = lambda: psycopg.connect(connection_string) + + @property + def target_index_name(self) -> str: + return self._collection + + @property + def store_type(self) -> str: + return 'pgvector' + + def upsert(self, records: Iterable[VectorRecord], version: str) -> UpsertResult: + materialized = list(records) + if not materialized: + return UpsertResult(upserted_count=0, version=version) + + dimensions = self._embedding_dimensions or len(materialized[0].embedding) + try: + with self._connection_factory() as conn: + self._ensure_schema(conn, dimensions) + with conn.cursor() as cur: + for record in materialized: + cur.execute( + f'INSERT INTO {self._table} ' # noqa: S608 - table name is identifier-validated above + '(chunk_id, version, document_id, content, embedding, metadata) ' + 'VALUES (%s, %s, %s, %s, %s::vector, %s) ' + 'ON CONFLICT (version, chunk_id) DO UPDATE SET ' + 'content = EXCLUDED.content, embedding = EXCLUDED.embedding, ' + 'metadata = EXCLUDED.metadata', + ( + record.chunk_id, + version, + record.document_id, + record.content, + _to_vector_literal(record.embedding), + json.dumps(record.metadata), + ), + ) + conn.commit() + except Exception as exc: + raise self._classify(exc) from exc + return UpsertResult(upserted_count=len(materialized), version=version) + + def delete_document(self, document_id: str, version: str) -> None: + try: + with self._connection_factory() as conn: + with conn.cursor() as cur: + cur.execute( + f'DELETE FROM {self._table} WHERE version = %s AND document_id = %s', # noqa: S608 + (version, document_id), + ) + conn.commit() + except Exception as exc: + raise self._classify(exc) from exc + + def validate_version(self, version: str) -> ValidationResult: + try: + with self._connection_factory() as conn: + with conn.cursor() as cur: + cur.execute( + f'SELECT COUNT(*), COUNT(DISTINCT document_id) FROM {self._table} ' # noqa: S608 + 'WHERE version = %s', + (version,), + ) + row = cur.fetchone() + except Exception as exc: + raise self._classify(exc) from exc + + chunk_count, document_count = (row[0], row[1]) if row else (0, 0) + return ValidationResult( + valid=chunk_count > 0, + version=version, + actual_document_count=document_count, + actual_chunk_count=chunk_count, + details=f'{chunk_count} chunk(s) across {document_count} document(s) in version {version!r}', + ) + + def query( + self, + embedding: Iterable[float], + version: str, + *, + top_k: int = 5, + metadata_filter: Optional[dict[str, Any]] = None, + query_text: Optional[str] = None, # unused: pgvector search here is vector-only + ) -> list[QueryMatch]: + vector_literal = _to_vector_literal(embedding) + where_clauses = ['version = %s'] + params: list[Any] = [version] + if metadata_filter: + where_clauses.append('metadata @> %s::jsonb') + params.append(json.dumps(metadata_filter)) + + query = ( + 'SELECT chunk_id, document_id, content, metadata, 1 - (embedding <=> %s::vector) AS score ' + f'FROM {self._table} WHERE {" AND ".join(where_clauses)} ' # noqa: S608 + 'ORDER BY embedding <=> %s::vector LIMIT %s' + ) + try: + with self._connection_factory() as conn: + with conn.cursor() as cur: + cur.execute(query, [vector_literal, *params, vector_literal, top_k]) + rows = cur.fetchall() + except Exception as exc: + raise self._classify(exc) from exc + + return [ + QueryMatch( + chunk_id=row[0], + document_id=row[1], + content=row[2] or '', + score=float(row[4]), + metadata=_load_metadata(row[3]), + ) + for row in rows + ] + + def close(self) -> None: + pass # a new connection is opened and closed per call; nothing to hold open + + def _ensure_schema(self, conn: Any, dimensions: int) -> None: + if self._schema_ready: + return + with conn.cursor() as cur: + try: + cur.execute('CREATE EXTENSION IF NOT EXISTS vector') + except Exception: + # Best-effort: the extension may already exist, or this role may lack + # CREATE EXTENSION privilege while an admin already installed it. + conn.rollback() + cur.execute( + f'CREATE TABLE IF NOT EXISTS {self._table} (' # noqa: S608 + 'chunk_id TEXT NOT NULL, ' + 'version TEXT NOT NULL, ' + 'document_id TEXT NOT NULL, ' + 'content TEXT, ' + f'embedding VECTOR({dimensions}), ' + 'metadata JSONB, ' + 'created_at TIMESTAMPTZ NOT NULL DEFAULT now(), ' + 'PRIMARY KEY (version, chunk_id))' + ) + cur.execute( + f'CREATE INDEX IF NOT EXISTS {self._table}_document_idx ' # noqa: S608 + f'ON {self._table} (version, document_id)' + ) + conn.commit() + self._schema_ready = True + + @staticmethod + def _classify(exc: Exception) -> RagError: + if type(exc).__name__ in _TRANSIENT_EXCEPTION_NAMES: + return TransientVectorStoreError(str(exc)) + return VectorStoreError(str(exc)) + + +def _to_vector_literal(embedding: Iterable[float]) -> str: + """Formats an embedding as a pgvector input literal, e.g. '[0.1,0.2]'. + + Avoids a hard dependency on the separate `pgvector` Python package (which + exists mainly to register numpy-array adapters); a plain literal string + cast with `::vector` is all pgvector's wire format needs. + """ + return '[' + ','.join(repr(float(value)) for value in embedding) + ']' + + +def _load_metadata(raw: Any) -> dict[str, Any]: + if isinstance(raw, dict): + return raw + if isinstance(raw, str) and raw: + return json.loads(raw) + return {} diff --git a/dapr/ext/rag/vector_stores/pinecone.py b/dapr/ext/rag/vector_stores/pinecone.py new file mode 100644 index 000000000..59d984c33 --- /dev/null +++ b/dapr/ext/rag/vector_stores/pinecone.py @@ -0,0 +1,236 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from __future__ import annotations + +from typing import Any, Iterable, Iterator, Optional, TypeVar + +from dapr.ext.rag.errors import ( + OptionalDependencyError, + RagError, + TransientVectorStoreError, + VectorStoreError, +) +from dapr.ext.rag.models import QueryMatch, UpsertResult, ValidationResult, VectorRecord +from dapr.ext.rag.vector_stores.base import VectorIndex + +# See dapr/ext/rag/AGENTS.md for why the optional-dependency guard lives here, +# per adapter module, rather than once in dapr/ext/rag/__init__.py. +try: + from pinecone import Pinecone +except ImportError: # pragma: no cover - exercised only without pinecone installed + Pinecone = None # type: ignore[assignment] + +_TRANSIENT_EXCEPTION_NAMES = frozenset( + { + 'ServiceException', + 'UnauthorizedException', + 'PineconeApiException', + 'MaxRetryError', + 'TimeoutError', + } +) +T = TypeVar('T') + + +class PineconeVectorStore(VectorIndex): + """A Pinecone-backed `VectorIndex`, using one namespace per version. + + Namespaces give version isolation without a separate physical index per + version: writes to an inactive version's namespace never affect queries + scoped to the active version's namespace. + """ + + def __init__( + self, + *, + index_name: str, + api_key: Optional[str] = None, + batch_size: int = 100, + client: Optional[Any] = None, + ) -> None: + """Initializes a PineconeVectorStore. + + Args: + index_name: The name of an existing Pinecone index (this class + does not create indexes -- their vector dimension/metric must + already match the configured embedder). + api_key: Optional explicit API key; otherwise resolved by the + `pinecone` client from the `PINECONE_API_KEY` environment + variable. Never logged or included in provenance. + batch_size: Records per `upsert` request. + client: A pre-built Pinecone `Index` handle (or any object + exposing `.upsert`/`.delete`/`.describe_index_stats`) to use + instead of constructing one -- bypasses the `pinecone` + dependency check entirely, which is how tests exercise this + class without it installed. + + Raises: + OptionalDependencyError: `pinecone` is not installed and no + `client` was given. + """ + self._index_name = index_name + self._batch_size = batch_size + + if client is not None: + self._index = client + else: + if Pinecone is None: + raise OptionalDependencyError( + package='pinecone', extra='rag-pinecone', feature='PineconeVectorStore' + ) + self._index = Pinecone(api_key=api_key).Index(index_name) + + @property + def target_index_name(self) -> str: + return self._index_name + + @property + def store_type(self) -> str: + return 'pinecone' + + def upsert(self, records: Iterable[VectorRecord], version: str) -> UpsertResult: + materialized = list(records) + if not materialized: + return UpsertResult(upserted_count=0, version=version) + + try: + for batch in _batched(materialized, self._batch_size): + vectors = [_to_pinecone_vector(record) for record in batch] + self._index.upsert(vectors=vectors, namespace=version) + except Exception as exc: + raise self._classify(exc) from exc + return UpsertResult(upserted_count=len(materialized), version=version) + + def delete_document(self, document_id: str, version: str) -> None: + try: + self._index.delete(filter={'document_id': {'$eq': document_id}}, namespace=version) + except Exception as exc: + # Serverless Pinecone indexes don't support metadata-filtered delete + # (only pod-based ones do); surface that as a clear, non-transient error. + if 'serverless' in str(exc).lower() or 'not supported' in str(exc).lower(): + raise VectorStoreError( + f'delete_document by metadata filter is not supported on this Pinecone ' + f'index (serverless indexes require pod-based indexes for filtered ' + f'delete): {exc}' + ) from exc + raise self._classify(exc) from exc + + def validate_version(self, version: str) -> ValidationResult: + try: + stats = self._index.describe_index_stats() + except Exception as exc: + raise self._classify(exc) from exc + + chunk_count = _namespace_vector_count(stats, version) + return ValidationResult( + valid=chunk_count > 0, + version=version, + actual_chunk_count=chunk_count, + details=f'{chunk_count} vector(s) in namespace {version!r} (per describe_index_stats)', + ) + + def query( + self, + embedding: Iterable[float], + version: str, + *, + top_k: int = 5, + metadata_filter: Optional[dict[str, Any]] = None, + query_text: Optional[str] = None, # unused: this Pinecone adapter is dense-vector-only + ) -> list[QueryMatch]: + try: + response = self._index.query( + vector=list(embedding), + top_k=top_k, + namespace=version, + include_metadata=True, + filter=metadata_filter, + ) + except Exception as exc: + raise self._classify(exc) from exc + return [_to_query_match(match) for match in _response_matches(response)] + + @staticmethod + def _classify(exc: Exception) -> RagError: + if type(exc).__name__ in _TRANSIENT_EXCEPTION_NAMES: + return TransientVectorStoreError(str(exc)) + status_code = getattr(exc, 'status', None) or getattr(exc, 'status_code', None) + if isinstance(status_code, int) and (status_code == 429 or status_code >= 500): + return TransientVectorStoreError(str(exc)) + return VectorStoreError(str(exc)) + + +def _namespace_vector_count(stats: Any, version: str) -> int: + namespaces = getattr(stats, 'namespaces', None) + if namespaces is None and isinstance(stats, dict): + namespaces = stats.get('namespaces') + namespaces = namespaces or {} + + namespace_stats = namespaces.get(version) + if namespace_stats is None: + return 0 + vector_count = getattr(namespace_stats, 'vector_count', None) + if vector_count is None and isinstance(namespace_stats, dict): + vector_count = namespace_stats.get('vector_count') + return int(vector_count) if vector_count is not None else 0 + + +def _batched(items: list[T], size: int) -> Iterator[list[T]]: + for start in range(0, len(items), size): + yield items[start : start + size] + + +def _to_pinecone_vector(record: VectorRecord) -> dict[str, Any]: + # Built via an explicit dict() + assignment rather than a `{**record.metadata, + # 'document_id': ..., 'content': ...}` literal: mypy infers a dict literal's + # value type from *all* its keys, including the two str-typed ones, and then + # rejects the unpacked dict[str, Any] as incompatible with that narrowed type. + metadata: dict[str, Any] = dict(record.metadata) + metadata['document_id'] = record.document_id + metadata['content'] = record.content + return {'id': record.chunk_id, 'values': list(record.embedding), 'metadata': metadata} + + +def _response_matches(response: Any) -> list[Any]: + matches = getattr(response, 'matches', None) + if matches is None and isinstance(response, dict): + matches = response.get('matches') + return list(matches or []) + + +def _to_query_match(match: Any) -> QueryMatch: + metadata = getattr(match, 'metadata', None) + if metadata is None and isinstance(match, dict): + metadata = match.get('metadata') + metadata = dict(metadata or {}) + content = metadata.pop('content', '') + document_id = metadata.pop('document_id', '') + + match_id = getattr(match, 'id', None) + if match_id is None and isinstance(match, dict): + match_id = match.get('id') + + score = getattr(match, 'score', None) + if score is None and isinstance(match, dict): + score = match.get('score') + + return QueryMatch( + chunk_id=match_id or '', + document_id=document_id, + content=content, + score=float(score or 0.0), + metadata=metadata, + ) diff --git a/docs/rag/README.md b/docs/rag/README.md new file mode 100644 index 000000000..bafb20b4d --- /dev/null +++ b/docs/rag/README.md @@ -0,0 +1,376 @@ +# Durable RAG ingestion (`dapr.ext.rag`) + +`DurableRAGPipeline` durably ingests documents from a cloud object store into a versioned vector +index, using Dapr Workflow for orchestration. See [`dapr/ext/rag/AGENTS.md`](../../dapr/ext/rag/AGENTS.md) +for the internal architecture reference (module layout, why each design choice was made); this +document is the user-facing guide: the problem, configuration for every supported provider, +authentication, operations, and the Azure-native flagship path. + +## The problem this solves + +Indexing a large document corpus for retrieval-augmented generation means downloading, parsing, +chunking, and embedding potentially thousands of documents, then writing the result into a vector +index -- a process that can run for hours and is exposed to exactly the failures long-running jobs +always are: the embedding provider throttles you, a network call times out, the worker process +gets OOM-killed or the pod is rescheduled mid-run. A naive batch script either loses all progress on +a crash (reprocessing, and re-paying for, every document) or -- worse -- partially updates a *live* +index that queries are already reading from, serving incomplete results while ingestion is still +running. + +`DurableRAGPipeline` addresses both problems structurally: Dapr Workflow durably checkpoints +progress at the activity level, so a crash resumes from the last completed step rather than the +beginning; and every ingestion run builds a separate, inactive index version, which is only ever +switched to "active" after it validates as complete -- so a query never sees a partially-built +index. + +## Architecture and durability boundaries + +```mermaid +flowchart TD + subgraph Orchestrator["rag_ingest orchestrator (deterministic)"] + A[discover_and_manifest] --> B[get_manifest_batch] + B --> C["process_document x N (bounded batch)"] + C --> D[update_status] + D -->|more batches| B + D -->|done| E[validate_version] + E -->|valid| F[activate_version] + F --> G[publish_activation_event] + end + C -->|I/O| Source[(DocumentSource\nS3 / Azure Blob)] + C -->|I/O| Parser[DocumentParser + DocumentSplitter] + C -->|I/O| Embedder[(Embedder\nOpenAI / Azure OpenAI)] + C -->|I/O| Store[(VectorIndex\npgvector / Pinecone / Azure AI Search)] + A -->|I/O| Source + E -->|I/O| Store + F -->|I/O| Store + C -->|I/O| State[(Dapr state:\nmanifest, completion,\nembed progress)] + F -->|I/O| Activation[(Dapr state:\nActivationRecord, ETag-guarded)] + Reader[Reader process\nActiveVersionResolver] -->|resolves + queries| Activation + Reader --> Store +``` + +**Why every external call is a workflow activity, never orchestrator code.** Dapr Workflow replays +an orchestrator function from its recorded history to resume after a crash -- that only produces +the same result on replay if the orchestrator is a pure function of its inputs and history (see +[`dapr/ext/workflow/AGENTS.md`](../../dapr/ext/workflow/AGENTS.md)'s determinism rules). Listing a +bucket, downloading a blob, calling an embedding API, writing a vector, reading or writing Dapr +state, and generating a timestamp are all either I/O or non-deterministic -- every one of them runs +inside a `pipeline.py` `_activity_*` method, never in `_orchestrate_ingestion` itself, which only +ever calls `ctx.call_activity(...)`, reads `ctx.current_utc_datetime`, and does plain +in-memory bookkeeping on its own (flat, replay-safe) input state. + +## Supported combinations + +| Concern | Options | +|---|---| +| Source | `S3Source`, `AzureBlobSource` | +| Parser | `UnstructuredParser` (txt, md, PDF, HTML, DOCX) | +| Splitter | `TextSplitter` | +| Embedder | `OpenAIEmbedder`, `AzureOpenAIEmbedder` | +| Vector store | `PgVectorStore`, `PineconeVectorStore`, `AzureAISearchVectorStore` | + +Any source can pair with any embedder and any vector store -- see `examples/rag/config.py` for a +single set of scripts that switches between all of them via environment variables. + +## Configuration + +### S3 (`S3Source`) + +```python +from dapr.ext.rag import S3Source + +source = S3Source(bucket="company-docs", prefix="policies/") +``` + +Credentials come from **boto3's standard credential-provider chain** (environment variables, +shared config/credentials files, an EC2/ECS/EKS instance or task role, SSO) -- nothing in this +class requires a static access key. Pass `endpoint_url="http://localhost:4566"` to target +LocalStack for local development (see below), and `region_name`/explicit +`aws_access_key_id`/`aws_secret_access_key` only where you genuinely need to override the chain +(LocalStack's fixed test credentials, for instance). Requires the `rag-s3` extra +(`pip install "dapr[rag,rag-s3]"`). + +### Azure Blob Storage (`AzureBlobSource`) + +```python +from dapr.ext.rag import AzureBlobSource + +source = AzureBlobSource(account_url="https://example.blob.core.windows.net", container="company-docs", prefix="policies/") +``` + +Authenticates with **`DefaultAzureCredential`** by default (managed identity, workload identity, +`az login`, environment credentials, in that standard order) -- again, no static secret required. +Pass `connection_string=...` instead for Azurite or another local/dev connection string, or inject +`credential=` for any other `azure-identity` credential type. Requires the `rag-azure` extra +(`pip install "dapr[rag,rag-azure]"`). + +### OpenAI embeddings (`OpenAIEmbedder`) + +```python +from dapr.ext.rag import OpenAIEmbedder + +embedder = OpenAIEmbedder(model="text-embedding-3-small") +``` + +Resolves `OPENAI_API_KEY` from the environment by default. Requires the `rag` extra +(`pip install "dapr[rag]"`). + +### Azure OpenAI embeddings (`AzureOpenAIEmbedder`) + +```python +from azure.identity import DefaultAzureCredential +from dapr.ext.rag import AzureOpenAIEmbedder + +embedder = AzureOpenAIEmbedder( + endpoint="https://my-resource.openai.azure.com", + deployment="text-embedding-3-small", + credential=DefaultAzureCredential(), # this is also the default when omitted +) +``` + +`deployment` (what a request is actually routed by) and `model` (the underlying model, defaulted +from `deployment` when not given separately) are tracked independently and both recorded in +provenance -- an operator repointing a deployment to a different model version is exactly the kind +of change provenance should make visible. Authenticates via Microsoft Entra ID +(`DefaultAzureCredential` by default) using a bearer-token provider; `api_key=` is accepted for +local development only. Classifies HTTP 429/408/5xx as retryable and surfaces a `Retry-After` +header (as `retry_after_seconds` on the raised `TransientEmbeddingError`) when the service sends +one. Requires the `rag` and `rag-azure` extras. + +### PostgreSQL + pgvector (`PgVectorStore`) + +```python +from dapr.ext.rag import PgVectorStore + +vector_store = PgVectorStore(connection_string="postgresql://user:password@host/db", collection="company_knowledge") +``` + +All versions of one `collection` share a single physical table (`rag_chunks_{collection}`), scoped +by a `version` column with a `(version, chunk_id)` primary key -- no separate database or table per +version, and no destructive changes to an existing table (schema is created with `CREATE TABLE IF +NOT EXISTS` on first use). `collection` must be a safe SQL identifier (letters, digits, +underscores). Requires the `rag-pgvector` extra (`psycopg[binary]`). + +### Pinecone (`PineconeVectorStore`) + +```python +from dapr.ext.rag import PineconeVectorStore + +vector_store = PineconeVectorStore(index_name="company-knowledge") +``` + +Uses one **namespace per version** within a single Pinecone index (create the index itself, with a +matching vector dimension/metric, ahead of time -- this class does not create indexes). Resolves +`PINECONE_API_KEY` from the environment by default. Requires the `rag-pinecone` extra. + +### Azure AI Search (`AzureAISearchVectorStore`) -- the Azure-native flagship path + +```python +from dapr.ext.rag import AzureAISearchVectorStore + +vector_store = AzureAISearchVectorStore( + endpoint="https://my-search.search.windows.net", + index_base_name="company-knowledge", # creates company-knowledge-{version} indexes + semantic_configuration_name="company-knowledge-semantic", # optional +) +``` + +Unlike pgvector/Pinecone, a version here is a **whole separate physical index** +(`{index_base_name}-{version}`), not a column/namespace inside one always-queryable index -- see +[Version activation](#version-activation) below for why that changes how activation works. +Authenticates via `DefaultAzureCredential` by default (`api_key=` for local development). Supports +vector-only, keyword-only, and hybrid (default) retrieval, with optional semantic ranking; the +`content_vector` field is never returned from a query unless you explicitly ask for it +(`include_vector=True`). Batch upserts inspect Azure AI Search's per-document indexing result and +retry only the documents that failed, not the whole batch. Requires the `rag-azure-search` extra +(`azure-search-documents`). + +## Authentication summary + +| Provider | Default | Local/dev override | +|---|---|---| +| AWS (S3) | Default credential provider chain (env, shared config, instance/task role, SSO) | Explicit `aws_access_key_id`/`aws_secret_access_key`, or `endpoint_url` for LocalStack | +| Azure (Blob, OpenAI, AI Search) | `DefaultAzureCredential` (managed/workload identity, `az login`, ...) | `connection_string` (Blob/Azurite) or `api_key` (OpenAI/AI Search) | + +No adapter in this package requires a static credential in code or configuration for its +recommended path. See [`docs/rag/azure-rbac.md`](azure-rbac.md) for the exact Azure role +assignments the Azure-native deployment needs, split between an ingestion identity (index/document +write, alias switch) and a query identity (read-only) -- the query identity must never be able to +write documents, create indexes, or switch the alias. + +## Starting, observing, and querying a pipeline + +```python +pipeline = DurableRAGPipeline( + source=..., parser=UnstructuredParser(), splitter=TextSplitter(chunk_size=1000, chunk_overlap=150), + embedder=..., vector_store=..., state_store_name="rag-pipeline-state", +) +pipeline.run_worker() # only in the process that should execute the workflow -- see examples/rag/worker.py + +instance_id = pipeline.start(version="2026-09", activate_when_complete=True) +status = pipeline.get_status("2026-09") # PipelineStatus: counts, chunks, retries, bytes, stage, ... +active = pipeline.resolve_active_version() # None until activation succeeds +``` + +A short-lived client process (a CLI, a script) never needs to call `run_worker()` -- constructing a +`DurableRAGPipeline` only registers activities locally and is safe without a live sidecar; +`start()`/`get_status()`/`resolve_active_version()` all just talk to Dapr through the sidecar. See +`examples/rag/cli.py` and `examples/rag/worker.py` for the worker/client split in practice, and +`examples/rag/README.md` for exact commands. + +For query-time retrieval, use `ActiveVersionResolver` directly rather than constructing a full +pipeline (it needs only the vector store, embedder, and state store -- not source credentials): + +```python +from dapr.ext.rag import ActiveVersionResolver + +resolver = ActiveVersionResolver(pipeline_id="company-knowledge", state_store_name="rag-pipeline-state", + vector_store=..., embedder=...) +matches = resolver.query("What is the remote work policy?", top_k=5) +``` + +`resolver.query(...)` always resolves the active version fresh (with strong read consistency) and +searches only that version -- a reader never needs its own logic to avoid an in-progress build. + +## Version activation + +An `ActivationRecord` (`pipeline_id`, `active_version`, `previous_version`, `manifest_hash`, +`activated_at`, `workflow_instance_id`) lives at a single Dapr state key per pipeline, updated only +via an ETag-conditional write -- a concurrent activation attempt gets Dapr's `ABORTED` status, +which surfaces as a retryable `ActivationConflictError` (the workflow's own retry re-reads the +current ETag, so no bespoke retry loop is needed). Activation only ever follows successful +`validate_version`, and repeated activation of the same `(version, manifest_hash)` is a no-op. + +For pgvector and Pinecone, this record *is* the whole activation mechanism -- `version` is a +column/namespace inside one always-queryable index, so flipping the pointer is enough. +**Azure AI Search is different**: each version is a separate physical index, so an +`ActivationRecord` alone doesn't change what a query actually reads. `AzureAISearchVectorStore` +participates in a second step (`VectorIndex.activate_version`, called from the same activity, +before the Dapr-state write): it reads the current alias mapping, does nothing if it already points +at the target index (idempotent), otherwise atomically repoints the alias +(`{index_base_name}-active` by default) and polls until the change is observable -- never deleting +or repurposing the previous version's index. The query path should read through this alias, not a +version-specific index name; `examples/rag/query_api.py` does exactly that. + +The alias switch itself is implemented as a direct call to the Search REST API, not the +`azure-search-documents` SDK: the SDK has no alias support at all (removed before its stable 11.4.0 +release and never restored). See +[`dapr/ext/rag/AGENTS.md`](../../dapr/ext/rag/AGENTS.md#azure-ai-search-index-aliases-are-rest-only-not-sdk-only) +for the full finding -- it doesn't change any of the behavior described above, only how it talks to +Azure AI Search under the hood. + +## Recovery and retry behavior + +Two independent mechanisms make a crash recoverable, described in full (with the exact state-store +key schema) in [`dapr/ext/rag/AGENTS.md`](../../dapr/ext/rag/AGENTS.md#idempotency-and-recovery-the-core-value-proposition): + +1. **Dapr Workflow's own crash recovery.** Orchestration state lives in the Dapr-managed backend, + not the worker process's memory. Restarting the *same* worker process reconnects and resumes the + same instance automatically -- see `examples/rag/README.md`'s failure demo for how to see this + directly. +2. **Per-document idempotency in Dapr state**, for a brand-new workflow instance re-processing the + same version (e.g. after a prior instance reached a terminal failed state). A completion record + is written only *after* a document's vectors are durably upserted, and embedding progress is + tracked per batch, so "crashed mid-embedding" never loses or duplicates more than one in-flight + batch's worth of work. + +Retryable failures (throttling, timeouts, transient 5xx) are retried by the workflow's own +`RetryPolicy` with exponential backoff (`PipelineConfig.first_retry_interval_seconds`/ +`backoff_coefficient`/`max_retry_interval_seconds`/`max_activity_attempts`); non-retryable failures +(corrupt/unsupported documents, invalid embedding requests) are recorded as a failed document and +never retried, so no retry budget is wasted on something retrying can't fix. + +## Provenance + +Every indexed chunk's vector-store metadata carries a full `ProvenanceRecord`: pipeline and +workflow-instance IDs, source provider/document ID/URI/ETag/version/content type/content hash, +document and chunk ordinals, chunk content hash, parser and splitter type + config hash, embedding +provider/model/deployment, target index and version, ingestion timestamp, and activity attempt +number -- see `dapr/ext/rag/models.py`'s `ProvenanceRecord` for the exact fields. No credential, +signed URL, or connection string is ever included: adapter `config()` methods (which feed the +pipeline fingerprint and are safe to log) are hand-written to return only non-secret, +behavior-affecting settings. + +## Local development with LocalStack and Azurite + +```python +# S3 against LocalStack +S3Source(bucket="company-docs", endpoint_url="http://localhost:4566", aws_access_key_id="test", aws_secret_access_key="test") + +# Azure Blob against Azurite +AzureBlobSource(container="company-docs", connection_string="UseDevelopmentStorage=true") +``` + +Both accept an injected `client=`, so the default unit test run exercises the same adapter code +against a fake without either service installed -- see `tests/ext/rag/test_sources_s3.py` and +`test_sources_azure_blob.py`. There is also a real, opt-in integration profile that runs against +live LocalStack, Azurite, and pgvector containers instead of fakes -- `pytest.mark.e2e`, excluded +from the default suite. See `tests/ext/rag/test_pipeline_integration.py` (a full ingest -> validate +-> activate -> query run through real S3-compatible storage and real pgvector; a same-version +re-run proving avoided recomputation; and -- the literal headline crash/resume scenario -- a real +worker *process* hard-killed mid-run via `FailureInjector`, with a second, fresh worker process +resuming the same in-flight Dapr Workflow instance) and +`tests/ext/rag/test_sources_azure_blob_integration.py` (list/get/metadata against real Azurite) +for the exact `docker run` commands and `uv run pytest ... -m e2e` invocations -- each file's +module comment has both. + +## Event-driven ingestion + +Two triggering paths, one per provider (see `dapr/ext/rag/triggers.py` for the normalization +functions and `examples/rag/pubsub_trigger.py` / `pubsub_trigger_servicebus.py` for runnable Dapr +pub/sub subscribers): + +- **S3**: Blob change -> SNS/SQS or EventBridge -> a Dapr pub/sub component -> `parse_s3_event_notifications`. +- **Azure**: Blob change -> Event Grid -> Azure Service Bus -> the Dapr Service Bus Topics pub/sub + component -> `parse_azure_blob_event` (handles both Event Grid and CloudEvents schema). + +Both paths normalize into the same `SourceChangeEvent`. Because delivery is at-least-once and can +arrive out of order, `EventDeduplicator` records each event's ID in Dapr state before acting on it, +and `reconciliation_workflow.ReconciliationTrigger` debounces a burst of events into a single, +prefix-level reconciliation run shortly after the burst goes quiet -- discovery against the real +source (not the event stream) remains the source of truth for what actually needs indexing. See +that module's docstring for the single-process limitation of its `threading.Timer`-based debounce +and the Dapr-Workflow-based alternative for a multi-replica subscriber deployment. + +## Running the failure/recovery demo + +See `examples/rag/README.md`'s "Failure and resume demo" section for the exact commands: set one +`RAG_DEMO_FAIL_*` environment variable, start the worker, start an ingestion run, watch the worker +hard-exit partway through, restart the *same* worker (with the demo variable unset), and watch +`python3 cli.py status` show the run complete without re-embedding the documents processed before +the crash. + +## Azure-native flagship path + +For the complete Azure Blob -> Azure OpenAI -> Azure AI Search -> hybrid retrieval -> Azure OpenAI +grounded answer path, see: + +- [`examples/rag/README.md`](../../examples/rag/README.md) -- the runnable end-to-end sample and its 10 demonstrated steps. +- [`examples/rag/infra/`](../../examples/rag/infra) -- Bicep to provision the Azure resources (not validated against a real subscription -- see its README). +- [`docs/rag/azure-rbac.md`](azure-rbac.md) -- exact role assignments for the ingestion vs. query identities. +- [`docs/rag/observability.md`](observability.md) -- OpenTelemetry spans, correlation IDs, and Azure Monitor export. +- [`docs/rag/integrated-vectorization-alternative.md`](integrated-vectorization-alternative.md) -- why `DurableRAGPipeline` keeps ingestion in Dapr Workflow instead of an Azure AI Search indexer/skillset (a design note, not shipped code). +- [`docs/rag/foundry-iq.md`](foundry-iq.md) -- optional, feature-flagged Foundry IQ knowledge-source registration (`DurableRAGPipeline(foundry_iq_knowledge_source=...)`). + +## Current MVP limitations + +- **No local/offline emulator for Azure AI Search or (Azure) OpenAI.** Unlike S3/Blob, which have a + real, opt-in `pytest.mark.e2e` integration profile against LocalStack/Azurite/pgvector (see + "Local development with LocalStack and Azurite" above), these two adapters are verified only via + dependency-injected fakes in the unit test suite, not against a real service -- see + `examples/rag/README.md`'s "What this example does not automate" section for exactly what has + and hasn't been run against real services. +- **Multi-tenancy fields are pass-through, not enforced.** `AzureAISearchVectorStore`'s schema + includes `tenant_id`/`authorization_groups`, but the pipeline itself has no tenancy concept; + populating them (e.g. via a custom parser/splitter tagging `Document.metadata`) and enforcing + them at query time is left to the caller. +- **The event-driven reconciliation trigger is single-process.** See + `reconciliation_workflow.py`'s docstring for the multi-replica alternative. +- **No automatic deletion of old index versions** (by design -- out of scope for this MVP; nothing + stops you from deleting an old pgvector version's rows, a Pinecone namespace, or an old Azure AI + Search index yourself once you're confident it's no longer needed). +- **No cross-vector-store migration, generic Dapr vector-store building block, or LLM-based + reranking** -- all explicitly out of scope; see `dapr/ext/rag/AGENTS.md`. +- **OpenTelemetry instrumentation is a specification, not shipped code.** `docs/rag/observability.md` + documents the span names/attributes an integrator should add; `dapr.ext.rag` does not create any + of them itself today. diff --git a/docs/rag/azure-rbac.md b/docs/rag/azure-rbac.md new file mode 100644 index 000000000..6dc2f3a32 --- /dev/null +++ b/docs/rag/azure-rbac.md @@ -0,0 +1,167 @@ +# Azure RBAC for the RAG pipeline + +This document lists the exact Azure role assignments the Azure-native deployment of +`DurableRAGPipeline` needs, organized around the two identities +[`examples/rag/infra`](../../examples/rag/infra) provisions: an **ingestion** identity (the Dapr +Workflow worker) and a **query** identity (the RAG query API). It is the reference the Bicep's +[`modules/rbac.bicep`](../../examples/rag/infra/modules/rbac.bicep) was written against, and should +be reviewed by whoever owns your subscription's security posture before you grant either identity +access to real data. + +**Verification note.** Every built-in role name below was checked against Microsoft Learn's +built-in-roles reference and each service's own RBAC documentation on **2026-09-10**, rather than +recalled from memory -- role names and casing change, and this doc is meant to be read by someone +doing a real deployment. The specific pages are cited under each role. If you are reading this much +later than that date, re-check the citations: role names occasionally change or gain +successors. + +## The two identities + +| Identity | Runs | Needs to | +|---|---|---| +| **Ingestion** | Dapr Workflow worker (the pipeline's activities: discover, download, parse, chunk, embed, write index, validate, activate) | Read source blobs, receive the Service Bus trigger, create/manage Search indexes and switch the alias, write Search documents, call the embeddings deployment, read+write the Redis-backed workflow state store | +| **Query** | RAG query API | Query Search documents (read-only), call the chat deployment, read the Redis-backed active-version pointer (read-only) | + +The query identity is deliberately a strict subset of the ingestion identity's access. It must +**not** be able to: + +- Create, delete, or reconfigure any Azure AI Search index. +- Switch the Azure AI Search alias. +- Write, update, or delete any Azure AI Search document. +- Read source blobs, or do anything with Service Bus. +- Write to the Redis-backed state store. + +The ingestion identity is also, in effect, "the Service Bus consumer identity" (it is the one +workload that reads pipeline-trigger messages). It must not be able to do anything on Service Bus +beyond receiving messages from its one subscription -- no `Send`, no namespace management, no +access to other topics/subscriptions in the namespace. + +**Scope discipline.** Every role assignment below is scoped to the one resource (or, for Service +Bus, the one subscription entity) that needs it. Avoid subscription-level or resource-group-level +role assignments (especially `Owner`/`Contributor` at subscription scope) for either identity -- +they would grant far more than either workload needs, and would make the table below meaningless +as a security boundary. + +## Role assignments + +| # | Permission | Identity | Role | Scope | +|---|---|---|---|---| +| 1 | Read blobs from the source container | Ingestion | `Storage Blob Data Reader` | Storage account (or the container, for tighter scoping) | +| 2 | Receive messages from the Service Bus subscription | Ingestion | `Azure Service Bus Data Receiver` | The specific topic subscription | +| 3 | Create/manage Search indexes and switch aliases | Ingestion | `Search Service Contributor` | Search service | +| 4 | Read and write Search documents | Ingestion | `Search Index Data Contributor` | Search service (or a single index, see below) | +| 5 | Query Search documents only | Query | `Search Index Data Reader` | Search service (or a single index) | +| 6 | Call the embeddings deployment | Ingestion | `Cognitive Services OpenAI User` | Azure OpenAI account | +| 7 | Call the chat deployment | Query | `Cognitive Services OpenAI User` | Azure OpenAI account | +| 8 | Read/write the Redis-backed workflow state store | Ingestion | Redis access policy `Data Owner` | Azure Cache for Redis instance | +| 9 | Read the Redis-backed active-version pointer | Query | Redis access policy `Data Reader` | Azure Cache for Redis instance | +| 10 | Read secrets from Key Vault (only if the optional secret store is deployed) | Ingestion and/or Query | `Key Vault Secrets User` | Key Vault | + +Rows 8-9 are not Azure RBAC role assignments (`Microsoft.Authorization/roleAssignments`) -- Azure +Cache for Redis's Microsoft Entra ID integration uses its own **access-policy assignment** +concept layered over Redis's ACL system. They are listed here for completeness since they are the +functional equivalent for this pipeline's state store, and because they still follow the same +least-privilege pattern (`Data Owner` vs. `Data Reader`). + +### Why the two OpenAI role assignments look identical + +Row 6 and row 7 are the *same* role (`Cognitive Services OpenAI User`) on the *same* Azure OpenAI +account, once for each identity. Azure OpenAI's RBAC roles are account-scoped, not +deployment-scoped -- there is currently no built-in way to grant "inference on the embeddings +deployment only" separately from "inference on the chat deployment only" within one account. If +you need that level of isolation, deploy the embeddings and chat models to separate Azure OpenAI +accounts and assign each identity only to the account it needs. + +### Per-index scoping (optional, tighter than the default) + +Azure AI Search supports scoping `Search Index Data Contributor`/`Search Index Data Reader` to a +single index instead of the whole service: + +```sh +az role assignment create \ + --assignee \ + --role "Search Index Data Reader" \ + --scope "/subscriptions//resourceGroups//providers/Microsoft.Search/searchServices//indexes/" +``` + +This sample does not do this by default because `DurableRAGPipeline` creates a *new* index per +version (`{index_base_name}-{version}`) -- a static per-index scope would need to be re-granted on +every version activation. `Search Service Contributor` (row 3) already limits the ingestion +identity's *index-management* surface to this one search service; per-index data-plane scoping is +a further tightening worth considering once you have a fixed, small set of versions in flight. + +### Detailed citations + +**Row 1 -- Storage Blob Data Reader.** Confirmed via [Azure built-in roles for +Storage](https://learn.microsoft.com/azure/role-based-access-control/built-in-roles/storage) and +corroborated by [Azure Storage's own blob-access-authorization +docs](https://learn.microsoft.com/azure/storage/blobs/authorize-data-operations-portal). Role ID +`2a2b9908-6ea1-4ae2-8e65-a410df84e7d1`. Description: "Allows for read access to Azure Storage blob +containers and data." + +**Rows 2 -- Azure Service Bus Data Receiver.** Confirmed via [Azure built-in roles for +Integration](https://learn.microsoft.com/azure/role-based-access-control/built-in-roles/integration). +Role ID `4f6d3b9b-027b-4f4c-9142-0e5a2a2247e0`. Description: "Allows for receive access to Azure +Service Bus resources." (The companion `Azure Service Bus Data Sender`, ID +`69a216fc-b8fb-44d8-bc22-1f3c2cd27a39`, is used in the Bicep for the Event Grid system topic's own +delivery identity -- not by either workload identity.) + +**Rows 3-5 -- Search Service Contributor / Search Index Data Contributor / Search Index Data +Reader.** Confirmed via Azure AI Search's own RBAC doc, [Connect using Azure +roles](https://learn.microsoft.com/azure/search/search-security-rbac), which is more precise than +the general-purpose built-in-roles page for this product. That page's permissions table explicitly +lists **aliases** as one of the object types covered by `Search Service Contributor`'s "create, +run, and manage search objects" permission (footnote 1: "Includes indexes, indexers, data sources, +skillsets, aliases, synonym maps, debug sessions, knowledge bases, and knowledge sources") -- +confirming that **switching the alias is a control-plane operation** requiring +`Search Service Contributor`, not a data-plane one. Role IDs: `Search Service Contributor` +`7ca78c08-252a-4471-8644-bb5ff32d4ba0`, `Search Index Data Contributor` +`8ebe5a00-799e-43f5-93ac-243d3dce84a7`, `Search Index Data Reader` +`1407120a-92aa-4202-b7e9-c0e197c71c8f`. `Search Index Data Contributor` is indeed a separate +data-plane role from `Search Service Contributor`, as the prompt for this doc anticipated needing +to check -- one manages index *definitions* (control plane), the other manages index *documents* +(data plane), and the ingestion identity needs both. + +**Rows 6-7 -- Cognitive Services OpenAI User.** Confirmed via [Azure built-in roles for AI + +machine learning](https://learn.microsoft.com/azure/role-based-access-control/built-in-roles/ai-machine-learning) +for the exact role name, and via [Role-based access control for Azure +OpenAI](https://learn.microsoft.com/azure/ai-services/openai/how-to/role-based-access-control) for +its exact permitted/denied task list. Role ID `5e0bd9bd-7b93-4f28-af87-19fc36ad61ae`. Confirmed +capability relevant here: "Make inference API calls with Microsoft Entra ID" (covers both chat +completions and embeddings against already-deployed models). Confirmed it explicitly **cannot**: +create/edit model deployments, fine-tune models, view/copy/regenerate keys, or access quota -- so +neither identity can reconfigure the Azure OpenAI account, only call its already-deployed models. + +**Rows 8-9 -- Redis access policies Data Owner / Data Reader.** Confirmed via [Use Microsoft Entra +for cache +authentication](https://learn.microsoft.com/azure/azure-cache-for-redis/cache-azure-active-directory-for-authentication) +(Azure Cache for Redis's own docs), which names the built-in access policies exactly as `Data +Owner`, `Data Contributor`, and `Data Reader`. That page also notes Microsoft Entra ID +authentication is **not supported on the Enterprise/Enterprise Flash tiers** of Azure Cache for +Redis (Basic/Standard/Premium only) -- relevant if you scale up to Enterprise for throughput or +clustering, since you would lose this managed-identity path and need to fall back to access keys +(see the commented-out alternative in +[`workflow-statestore.yaml`](../../examples/rag/components/azure/workflow-statestore.yaml)). That +same page also carries a retirement notice for Azure Cache for Redis across all SKUs in favor of +"Azure Managed Redis" -- see the flagged callout in +[`examples/rag/infra/README.md`](../../examples/rag/infra/README.md). + +**Row 10 -- Key Vault Secrets User.** Confirmed via [Azure RBAC for Key +Vault](https://learn.microsoft.com/azure/key-vault/general/rbac-guide), which requires the vault to +use the "Azure role-based access control" permission model (this sample's Bicep sets +`enableRbacAuthorization: true`). Role ID `4633458b-17de-408a-b874-0445c86b69e6`. Description: +"Read secret contents including secret portion of a certificate with private key." This is +read-only for secret *values*; it cannot create, rotate, or delete secrets (that is `Key Vault +Secrets Officer`, a separate, more privileged role this pipeline's identities do not need). + +## What this pipeline never needs + +No identity in this design needs `Owner`, `Contributor`, or `User Access Administrator` at any +scope, a Storage/Search/OpenAI/Key Vault **management-plane write** role (e.g. `Search Service +Contributor`'s sibling for *creating the Search service itself* is a deployment-time concern +handled by whoever runs the Bicep, not a runtime identity), or any role at subscription or +resource-group scope. If a future change to this pipeline seems to need one of those, treat that as +a signal to re-scope the *specific* permission needed (via a resource-scoped built-in role, a +per-index/per-secret scope, or a custom role -- Azure AI Search's own docs show how to clone +`Search Index Data Reader` into a narrower custom role, for example) rather than reaching for a +broader built-in role. diff --git a/docs/rag/foundry-iq.md b/docs/rag/foundry-iq.md new file mode 100644 index 000000000..75d536c35 --- /dev/null +++ b/docs/rag/foundry-iq.md @@ -0,0 +1,119 @@ +# Foundry IQ integration + +`dapr.ext.rag` can register a validated Azure AI Search index produced by `DurableRAGPipeline` +as a Foundry IQ knowledge source. This is **opt-in and off by default**, and it does not +duplicate Foundry IQ's own retrieval engine -- see "What this integration does and does not do" +below before enabling it. + +**Preview-dependent, verify before relying on this.** Foundry IQ and Azure AI Search's agentic +retrieval feature are moving quickly. The terminology, API surface, and GA/preview split below +were checked against Microsoft Learn on **2026-09-10** (cited inline); by the time you read this, +names, capabilities, or availability may have changed. Re-verify against current docs before +depending on this in production. + +## What Foundry IQ and knowledge sources are + +[Foundry IQ](https://learn.microsoft.com/azure/foundry/agents/concepts/what-is-foundry-iq) is +Microsoft's managed knowledge layer for AI agents, built on Azure AI Search. A **knowledge base** +is the top-level resource an agent queries; it references one or more **knowledge sources** +(connections to indexed or remote content -- Blob Storage, SharePoint, OneLake, an existing Azure +AI Search index, the web, or MCP in private preview) and holds retrieval parameters. At query +time, **agentic retrieval** decomposes a question into subqueries, runs them in parallel, +semantically reranks the results, and returns a grounded, cited answer -- optionally using an LLM +for the query-planning step. + +Microsoft Learn's own note as of this writing: "Some Foundry IQ features are now generally +available, while others remain in preview. Availability depends on the Search Service REST API +version you use." Concretely: creating a **search index knowledge source** (the kind this +integration uses -- see below) is GA as of the `2026-04-01` REST API; `semanticConfigurationName` +is required on that API version and optional starting with `2026-05-01-preview`; and the portal +experience for all of this remains preview regardless of REST API version. Source: +[Create a Search Index Knowledge Source](https://learn.microsoft.com/azure/search/agentic-knowledge-source-how-to-search-index). + +## How to use it + +```python +from dapr.ext.rag import DurableRAGPipeline, FoundryIQKnowledgeSourceConfig + +pipeline = DurableRAGPipeline( + source=..., parser=..., splitter=..., embedder=..., + vector_store=..., # must be AzureAISearchVectorStore + state_store_name="rag-pipeline-state", + foundry_iq_knowledge_source=FoundryIQKnowledgeSourceConfig( + name="company-knowledge-ks", + source_data_fields=("title", "source_uri"), + search_fields=("content",), + ), +) +``` + +With this set, every successful activation -- from `pipeline.start(activate_when_complete=True)` +or `pipeline.activate_version(...)` -- runs one additional, durable workflow activity right after +`activate_version` succeeds: `AzureAISearchVectorStore.register_foundry_iq_knowledge_source` +creates or updates the named knowledge source to point at that version's concrete physical index. +Like `pubsub_name`'s activation-event publish, registration is **best-effort**: a failure is +logged, never raised, so it never fails an otherwise-successful activation (Foundry IQ +registration is a convenience on top of a validated, active index, not a precondition for one). + +`AzureAISearchVectorStore.register_foundry_iq_knowledge_source` requires +`semantic_configuration_name` to be set on that store (the `2026-04-01` REST API this method +targets requires it on every search index knowledge source), and is itself idempotent -- calling +it again with the same version is a no-op, matching `activate_version`'s own idempotency. + +## Why this talks to a REST endpoint, not the SDK + +Microsoft's own docs illustrate knowledge-source creation as an `azure-search-documents` SDK +call (`SearchIndexClient.create_or_update_knowledge_source(...)`). As of `azure-search-documents` +11.6.0 (confirmed by introspecting the installed package on 2026-09-10), neither +`SearchIndexKnowledgeSource`/`SearchIndexKnowledgeSourceParameters` nor that client method exist +-- the feature is GA at the REST layer but not yet wrapped by this SDK version. This is the same +situation as index aliases (see `dapr/ext/rag/AGENTS.md`'s "Azure AI Search: index aliases are +REST-only, not SDK-only"), and the fix is the same: `register_foundry_iq_knowledge_source` talks +to the Search REST API directly (`PUT {endpoint}/knowledgesources('{name}')?api-version=2026-04-01`), +reusing the exact same `httpx` transport, credential, and headers the alias calls already use. +Verified against Microsoft's REST API reference +([Knowledge Sources - Create or Update](https://learn.microsoft.com/rest/api/searchservice/knowledge-sources/create-or-update)) +on 2026-09-10. If a future `azure-search-documents` release adds real SDK support, only this one +method's implementation needs to change -- its signature and behavior would not. + +## Why this targets the concrete physical index, not the alias + +**This is a real operational hazard, not just a naming detail.** A search index knowledge +source's `searchIndexParameters.searchIndexName` takes a specific index name; every example in +Microsoft's own docs uses a concrete name like `"my-search-index"`, never an alias. Whether the +underlying knowledge-source resource *resolves* an alias dynamically (re-checking on every +retrieval) or *binds* to whatever physical index the alias pointed to at creation time is not +documented anywhere this note's research could confirm -- `register_foundry_iq_knowledge_source` +therefore assumes the more dangerous case (that it binds once) and always re-points the knowledge +source explicitly to `{index_base_name}-{version}` on every activation, the same way +`activate_version` re-points the alias itself. Registering a knowledge source against the alias +name instead would risk leaving it silently pointing at a stale, superseded index after this +pipeline's next version activation flips the alias -- the exact class of staleness bug this +pipeline's whole alias-based design exists to prevent for direct queries. + +## What this integration does and does not do + +- It **does** durably coordinate creating/updating the knowledge source as a workflow activity, + chained strictly after `activate_version` succeeds -- never for a version that has not passed + this pipeline's own validation gate. +- It does **not** duplicate Foundry IQ's own query-planning or retrieval engine. This pipeline's + `ActiveVersionResolver`/`AzureAISearchVectorStore.query()` (hybrid Azure AI Search retrieval) + and `AzureOpenAIChatClient` (direct Azure OpenAI chat completion) are one query path; Foundry + IQ's agentic retrieval against the registered knowledge source is a separate, alternative one + against the same underlying index. A deployment picks one per use case; this integration's job + stops at handing over a validated index and keeping a knowledge source pointed at the current + one -- never at re-implementing multi-query planning or reranking Foundry IQ already provides. +- It does **not** create a knowledge source of any kind other than `searchIndex` (wrapping an + already-built index). A knowledge source that owns its own indexer/skillset against a raw + source is a *different*, uncoordinated ingestion path into the same search service -- exactly + what this pipeline's durable-workflow design is meant to be the single source of truth against + for its own indexes. (Compare `docs/rag/integrated-vectorization-alternative.md`, which + discusses Azure AI Search's indexer/skillset-based ingestion as a deliberate, wholesale + *alternative* to `DurableRAGPipeline` -- not something this package ships or composes with it.) + +## Sources + +- [What is Foundry IQ?](https://learn.microsoft.com/azure/foundry/agents/concepts/what-is-foundry-iq) +- [Create a Search Index Knowledge Source](https://learn.microsoft.com/azure/search/agentic-knowledge-source-how-to-search-index) +- [Knowledge Sources - Create or Update (REST)](https://learn.microsoft.com/rest/api/searchservice/knowledge-sources/create-or-update) +- [Connect an Azure AI Search index to Foundry agents](https://learn.microsoft.com/azure/foundry/agents/how-to/tools/ai-search) diff --git a/docs/rag/integrated-vectorization-alternative.md b/docs/rag/integrated-vectorization-alternative.md new file mode 100644 index 000000000..cb01b2c91 --- /dev/null +++ b/docs/rag/integrated-vectorization-alternative.md @@ -0,0 +1,70 @@ +# Alternative architecture: Azure AI Search integrated vectorization + +This is a short note on a legitimate alternative to this pipeline's architecture, for anyone +evaluating whether they need `DurableRAGPipeline` at all: Azure AI Search **indexers with +integrated vectorization** (skillsets that chunk and embed documents as part of indexing), where +Search itself owns ingestion end to end. This is a design note, not shipped code: `dapr.ext.rag` +does not include a class for this, and deliberately so -- see "Why `DurableRAGPipeline` +deliberately does not use it" below. An earlier version of this package briefly shipped one +(`AzureSearchIntegratedVectorizationPipeline`) and removed it: that class had no Dapr Workflow, +Dapr client, or Dapr sidecar involvement whatsoever, and packaging plain `azure-search-documents` +SDK orchestration inside a Dapr extension risked implying it inherited `DurableRAGPipeline`'s +durability guarantees, which it fundamentally cannot (an indexer run's own retry/recovery +behavior is opaque to the caller -- see reason 1 below). If you want this architecture, build it +directly against `azure-search-documents` in your own application code, outside this package. + +## What integrated vectorization is + +An Azure AI Search **indexer** can read documents directly from a source (including Blob Storage) +and run a **skillset** against each one as part of indexing -- built-in or custom skills that +split text into chunks and call an embedding model (including an Azure OpenAI embeddings +deployment) inline, writing the resulting text and vector fields straight into the index. Search +manages the crawl/re-crawl schedule, change detection, and the chunk-and-embed step itself. For a +single Azure-AI-Search-only deployment with a straightforward document set, this is a +legitimate, lower-code way to get from "documents in Blob Storage" to "a queryable vector index" -- +no separate compute layer, orchestrator, or workflow engine required. + +## Why this pipeline deliberately does not use it + +`DurableRAGPipeline` keeps ingestion, retries, and checkpointing inside Dapr Workflow instead, for +four reasons specific to what this sample is trying to guarantee: + +1. **Durability across worker crashes and pod restarts.** An indexer run is a black box from the + caller's perspective: if it fails partway through a large corpus, the recovery story is + "re-run the indexer" (or rely on its own internal, less transparent retry/resume behavior), not + "resume exactly the documents that were not yet durably processed." Dapr Workflow's durable + execution means killing and restarting the worker process mid-run resumes from the last + completed activity, not from scratch -- with no application code needed for the crash-recovery + half of that guarantee (see `dapr/ext/rag/AGENTS.md`'s "Idempotency and recovery" section). +2. **Avoided recomputation via idempotent per-document/per-batch state.** Because this pipeline + tracks completion at the document and embedding-batch level (content-hash-keyed + `CompletionRecord`/`EmbedProgressRecord`s in the state store), re-processing a version after a + prior failed attempt skips everything already durably completed -- including embeddings, which + are the expensive, rate-limited, billed part of ingestion. An indexer re-run's unit of + "already done" is coarser and less exposed to the caller. +3. **Provenance recorded per chunk.** Every vector this pipeline writes carries a full + `ProvenanceRecord` in its metadata (pipeline/workflow IDs, source identity, content/config + hashes, parser/splitter/embedder identity, target index/version, timestamp, attempt number) -- + queryable alongside search results, and exactly what makes the correlation narrative in + `docs/rag/observability.md` possible. An indexer-driven pipeline can populate custom metadata + fields too, but that provenance has to be designed and maintained as part of the skillset + rather than coming from a workflow engine that already tracks this state for its own recovery + purposes. +4. **One ingestion model across deployment targets.** Integrated vectorization is Azure AI + Search-specific -- it has no equivalent for this same pipeline's S3 + pgvector/Pinecone + deployment target, which has no "indexer" concept at all. Because `DurableRAGPipeline`'s + ingestion logic (discover, download, parse, chunk, embed, upsert, validate, activate) lives in + Dapr Workflow activities calling pluggable `DocumentSource`/`DocumentParser`/`Embedder`/ + `VectorIndex` adapters, the *same* orchestration code and the *same* durability/idempotency + guarantees apply whether the target is Azure Blob Storage + Azure AI Search or S3 + pgvector/ + Pinecone. Integrated vectorization would only ever cover the former, meaning a team supporting + both targets would need two entirely different ingestion architectures. + +## When integrated vectorization is the better choice + +If your deployment is Azure-AI-Search-only, your corpus is modest, you do not need cross-restart +durability guarantees stronger than "re-run the indexer," and you would rather not run a workflow +worker process at all, integrated vectorization is a reasonable, simpler starting point. The two +approaches are not mutually exclusive within Azure AI Search itself -- but mixing them against the +*same* index (part indexer-managed, part Dapr-Workflow-managed) is not a configuration this sample +supports or recommends: pick one system as the owner of a given index's ingestion. diff --git a/docs/rag/observability.md b/docs/rag/observability.md new file mode 100644 index 000000000..7654925d8 --- /dev/null +++ b/docs/rag/observability.md @@ -0,0 +1,206 @@ +# Observability + +This document covers the OpenTelemetry spans `DurableRAGPipeline` and its query path should emit, +how to export them (plus logs/metrics) to Azure Monitor / Application Insights, and how to trace +one blob change through the entire pipeline using the resulting telemetry. + +It assumes a Python process already instrumented with OpenTelemetry (Dapr's own gRPC/HTTP client +calls and the workflow runtime produce their own spans independently of this document) and +describes the *pipeline-specific* spans layered on top -- the ones that make it possible to answer +"what happened to this one document/chunk/query" rather than just "what did the SDK call." + +**Status: specification, not shipped code.** `dapr.ext.rag` does not currently create any of the +spans below itself -- there is no `opentelemetry` import anywhere in the extension. Everything past +this point is a naming/attribute reference for an integrator to instrument their own worker/query +process against (e.g. by wrapping the `_activity_*` calls or the retrieval/generation calls with the +spans described here), not a description of built-in behavior. Treat the span names and attribute +keys as the recommended contract to converge on, not as something you will already see in a trace +if you export telemetry from an unmodified pipeline today. + +## Span reference + +| Span | Emitted by | When | +|---|---|---| +| `rag.discover_and_manifest` | Ingestion (orchestrator's first activity) | Once per workflow run: lists the source, builds and persists the manifest | +| `rag.process_document` | Ingestion (per-document activity) | Once per document, fanned out (bounded) per manifest page | +| `rag.embed_batch` | Ingestion (nested within `rag.process_document`) | Once per embedding-batch call to Azure OpenAI | +| `rag.vector_upsert` | Ingestion (nested within `rag.process_document`) | Once per upsert call to Azure AI Search | +| `rag.validate_version` | Ingestion (orchestrator activity) | Once per version, before activation | +| `rag.activate_version` | Ingestion (orchestrator activity) | Once per version, after validation passes | +| `rag.query.retrieve` | Query API | Once per query request: the Azure AI Search leg | +| `rag.query.generate_answer` | Query API | Once per query request: the chat-completion leg | + +### Attributes + +Shared attributes (used across several spans): + +| Attribute | Meaning | +|---|---| +| `dapr.workflow.instance_id` | The Dapr Workflow orchestration instance ID -- the durable correlation key for one ingestion run (see the correlation narrative below) | +| `rag.pipeline_id` | Pipeline identity (defaults to the vector store's index base name) | +| `rag.version` | The version string being ingested, validated, activated, or queried | +| `rag.document.id` | Source document identity (source-specific: blob name or S3 key) | +| `rag.batch.id` | Pipeline-generated logical identifier for one embedding/upsert batch | +| `azure.openai.deployment` | Azure OpenAI deployment name called | +| `azure.openai.request_id` | Request ID from the Azure OpenAI response. Azure OpenAI commonly surfaces both `x-request-id` and (when fronted by API Management) `apim-request-id`; capture whichever your HTTP client exposes -- for the official SDK's non-streaming responses this is available as a property on the top-level response object, otherwise attach an HTTP client hook to read response headers directly | +| `azure.search.index_name` | Physical index name (`{index_base_name}-{version}`) being written to or read from | +| `azure.search.alias_name` | The stable alias (`{index_base_name}-active` by default) | +| `azure.search.request_id` | The `x-ms-request-id` Azure AI Search returns for the specific HTTP call | + +Per-span attributes: + +- **`rag.discover_and_manifest`**: `rag.eventgrid.event_id`, `messaging.message.id` (both absent for + a manually-started run -- see the correlation narrative), `rag.source.type` (`s3` | `azure_blob`), + `rag.manifest.document_count`, `rag.manifest.page_count`. +- **`rag.process_document`**: `rag.document.content_hash` (the hash used for the idempotency check + -- never the content itself), `rag.attempt`, `rag.document.chunk_count`, `rag.document.outcome` + (`completed` | `failed` | `skipped_already_complete`). +- **`rag.embed_batch`**: `rag.batch.chunk_count`. +- **`rag.vector_upsert`**: `rag.batch.chunk_count`, and the batch's first/last `rag.chunk.id` rather + than the full list (see the cardinality note below). +- **`rag.validate_version`**: `rag.validation.document_count`, `rag.validation.passed` (bool), + `rag.validation.failure_reason` (a short code such as `"empty_manifest"`, never a message that + embeds document content). +- **`rag.activate_version`**: `rag.activation.was_noop` (bool -- true when this `(version, + manifest_hash)` was already active, i.e. the activity's no-op path). +- **`rag.query.retrieve`**: `rag.query.request_id` (an app-generated correlation ID for one query + request, independent of any ingestion run's workflow instance ID), `rag.query.top_k`, + `rag.retrieval.mode` (`hybrid` | `vector` | `keyword`), `rag.retrieval.result_count`. +- **`rag.query.generate_answer`**: `rag.query.request_id`, `rag.answer.citation_count`. + +**Cardinality note.** A `rag.vector_upsert` span can cover many chunks in one batch. Attach a +count and the first/last chunk ID rather than the full ID list as a single attribute -- if you need +true per-chunk granularity, add a [span event](https://opentelemetry.io/docs/concepts/signals/traces/#span-events) +per chunk ID instead of growing one attribute unboundedly. + +### What must never be a span attribute + +**Never attach document content, chunk text, prompts, or generated answers to a span**, as an +attribute, an event, or a span name. This includes: the text passed to the embeddings API, the +chat prompt (including any retrieved-context stanza built for it), the generated answer, and raw +document bytes/text at any pipeline stage. Reasons this is a hard rule, not a style preference: + +- Traces are typically retained, indexed, and access-controlled far more loosely than the + documents the pipeline ingests -- attaching content to a span silently widens that content's + blast radius to everyone with trace-read access (which, in Application Insights, is often a + broader group than everyone with source-data access). +- Span attribute values have practical size limits and are billed as ingested telemetry volume in + Azure Monitor; document/chunk text can be arbitrarily large. +- IDs and hashes (`rag.document.id`, `rag.document.content_hash`, `rag.chunk.id`) already give you + everything you need to correlate a trace with the actual content in its source system of record + (the blob, the search index) without duplicating that content into telemetry. + +If you need content-level debugging, use a separate, explicitly opt-in, access-controlled debug +log sink (and make sure whoever operates it understands it now holds a copy of potentially +sensitive document content) -- never the tracing pipeline. + +## Correlation-ID propagation: tracing one blob through the whole pipeline + +One blob change accumulates several different identifiers on its way through the system. They are +**not the same value** at each hop, and treating them as interchangeable is the most common way to +lose the thread when debugging in Application Insights. In order: + +1. **Event Grid event ID.** When a blob is created/updated, Event Grid emits an event with its own + `id` field (`rag.eventgrid.event_id`). This ID is scoped to Event Grid -- it shows up in Event + Grid's own delivery diagnostics. +2. **Service Bus message ID.** Event Grid delivers that event as the *body* of a message it + publishes to the Service Bus topic. That message gets its own transport-level `MessageId` + (`messaging.message.id`, using the OpenTelemetry messaging semantic convention name) -- + independent of the Event Grid event ID, and the one visible in Service Bus's own diagnostic + logs and metrics. +3. **Dapr's pub/sub envelope ID.** Dapr's Service Bus pub/sub component delivers the message to + the app wrapped in a CloudEvents envelope. Dapr's envelope carries its own `id`, which can differ + from both of the above (Dapr assigns a fresh one unless the inbound message already parses as a + valid CloudEvent). Capture all three IDs -- Event Grid event ID, Service Bus message ID, and + Dapr envelope ID -- at the trigger boundary rather than assuming any two of them match. +4. **Dapr Workflow instance ID.** The trigger handler starts (or signals) a workflow instance. + From here on, `dapr.workflow.instance_id` is the **durable** correlation key for the rest of the + run: it survives worker crashes and pod restarts (durabletask's own recovery reconnects to the + same instance ID on restart), unlike the three upstream message IDs, which only matter for the + initial hop. `rag.discover_and_manifest`'s span should record all four IDs from steps 1-4 + together, once, so later spans only need to carry the workflow instance ID. +5. **Per-document and per-chunk IDs.** `rag.process_document` carries `rag.document.id`; + `rag.embed_batch` and `rag.vector_upsert` carry `rag.batch.id` and chunk IDs, all nested under + the same `dapr.workflow.instance_id` trace. +6. **Azure OpenAI and Azure AI Search request IDs.** `rag.embed_batch` carries the + `azure.openai.request_id` for that embedding call; `rag.vector_upsert` carries the + `azure.search.request_id` for that upsert call. These are what you hand to Azure support (or use + in the respective service's own diagnostic logs) if you suspect a service-side issue rather than + a pipeline bug. +7. **Activation.** `rag.activate_version` ties the whole run to the alias switch, recording which + physical index (`azure.search.index_name`) the alias (`azure.search.alias_name`) now points to. + +**Query-time traces are independent.** A query request starts a *new* trace +(`rag.query.request_id`) with no causal link back to any specific ingestion run -- it queries +whatever the alias currently points to, and the documents a single query touches may have been +written by several different ingestion runs over time. To find out which ingestion run produced a +document a query retrieved, join on that document's own provenance metadata (the +`ProvenanceRecord` fields -- pipeline ID, workflow instance ID, source content hash -- stored +alongside every vector; see `dapr/ext/rag/AGENTS.md`'s provenance note) returned with the search +result, not on trace context. + +## Exporting to Azure Monitor / Application Insights + +Use the [`azure-monitor-opentelemetry`](https://pypi.org/project/azure-monitor-opentelemetry/) +distro package, which wires up the OpenTelemetry SDK's trace/log/metric exporters for Application +Insights in one call. The connection string is the Bicep output +`appInsightsConnectionString` from [`examples/rag/infra`](../../examples/rag/infra). + +```sh +pip install azure-monitor-opentelemetry +``` + +```python +# Illustrative minimal setup -- call this once, at process startup, before +# creating any tracer or starting the Dapr Workflow runtime/query API. +import os + +from azure.monitor.opentelemetry import configure_azure_monitor + +configure_azure_monitor( + # Falls back to the APPLICATIONINSIGHTS_CONNECTION_STRING environment + # variable automatically if this kwarg is omitted. + connection_string=os.environ["APPLICATIONINSIGHTS_CONNECTION_STRING"], +) +``` + +`configure_azure_monitor()` also auto-instruments common libraries (`requests`, `httpx`, etc.), so +the underlying HTTP calls Dapr's client SDK, `azure-search-documents`, and the Azure OpenAI SDK +make are exported alongside the pipeline's own spans without additional setup. From there, create +this pipeline's own spans with the standard OpenTelemetry API and set the attributes from the +reference above: + +```python +from opentelemetry import trace + +tracer = trace.get_tracer("dapr.ext.rag") + +with tracer.start_as_current_span("rag.process_document") as span: + span.set_attribute("dapr.workflow.instance_id", instance_id) + span.set_attribute("rag.pipeline_id", pipeline_id) + span.set_attribute("rag.version", version) + span.set_attribute("rag.document.id", document_id) + ... +``` + +For production traffic volumes, pair this with a +[sampler](https://opentelemetry.io/docs/languages/python/sdk/#sampling) appropriate to your ingest +budget -- Application Insights bills on ingested telemetry volume, and a busy ingestion run can +generate a `rag.embed_batch`/`rag.vector_upsert` span per batch across a large corpus. + +## Finding one blob's trace in Application Insights + +With the above in place, a typical investigation ("what happened to `policies/handbook.pdf`?") +looks like: + +1. **Transaction search** for the known `rag.document.id` (or the Event Grid event ID, if that is + all you have) as a custom-dimension filter -- this surfaces the specific `rag.process_document` + span (and, through trace context, its parent `dapr.workflow.instance_id`). +2. **Application Map / end-to-end transaction view** on that trace to see every nested + `rag.embed_batch` and `rag.vector_upsert` span, each with its own + `azure.openai.request_id`/`azure.search.request_id` for cross-referencing with the respective + service's own diagnostic logs if needed. +3. A **Log Analytics (KQL) query** joining on `dapr.workflow.instance_id` across + `customEvents`/`dependencies`/`traces` tables gives the full run, including + `rag.validate_version` and `rag.activate_version`, to confirm whether (and when) this document's + version actually went live behind the alias. diff --git a/examples/invoke-custom-data/README.md b/examples/invoke-custom-data/README.md index f0622aa82..fa427a03b 100644 --- a/examples/invoke-custom-data/README.md +++ b/examples/invoke-custom-data/README.md @@ -4,6 +4,12 @@ This example utilizes a receiver and a caller for the OnInvoke / Invoke function > **Note:** Make sure to use the latest proto bindings and have them available under `dapr_pb2` and `daprclient_pb2` +> **Why `custom_data_proto/` and not `proto/`:** the SDK's dev environment installs `google-cloud-vision` +> (a transitive dependency of the `rag` extension's optional `unstructured[pdf]` parser), which brings in +> a PyPI package literally named `proto`. A local `proto/` directory here would be shadowed by that +> installed package when this script runs in the same environment as the rest of the test suite, +> breaking `import proto.response_pb2` with a confusing `ModuleNotFoundError`. + ## Pre-requisites - [Dapr CLI and initialized environment](https://docs.dapr.io/getting-started) @@ -25,7 +31,8 @@ To run this example, the following steps should be followed: 1. Compile Protobuf for Custom Response ```bash - python3 -m grpc_tools.protoc --proto_path=./proto/ --python_out=./proto/ --grpc_python_out=./proto/ ./proto/response.proto + python3 -m grpc_tools.protoc --proto_path=./custom_data_proto/ --python_out=./custom_data_proto/ \ + --grpc_python_out=./custom_data_proto/ ./custom_data_proto/response.proto ``` 2. Start Receiver (expose gRPC server receiver on port 13551) diff --git a/examples/invoke-custom-data/proto/response.proto b/examples/invoke-custom-data/custom_data_proto/response.proto similarity index 100% rename from examples/invoke-custom-data/proto/response.proto rename to examples/invoke-custom-data/custom_data_proto/response.proto diff --git a/examples/invoke-custom-data/proto/response_pb2.py b/examples/invoke-custom-data/custom_data_proto/response_pb2.py similarity index 100% rename from examples/invoke-custom-data/proto/response_pb2.py rename to examples/invoke-custom-data/custom_data_proto/response_pb2.py diff --git a/examples/invoke-custom-data/proto/response_pb2_grpc.py b/examples/invoke-custom-data/custom_data_proto/response_pb2_grpc.py similarity index 100% rename from examples/invoke-custom-data/proto/response_pb2_grpc.py rename to examples/invoke-custom-data/custom_data_proto/response_pb2_grpc.py diff --git a/examples/invoke-custom-data/invoke-caller.py b/examples/invoke-custom-data/invoke-caller.py index caeb84313..16fc107ba 100644 --- a/examples/invoke-custom-data/invoke-caller.py +++ b/examples/invoke-custom-data/invoke-caller.py @@ -1,4 +1,4 @@ -import proto.response_pb2 as response_messages +import custom_data_proto.response_pb2 as response_messages from dapr.clients import DaprClient diff --git a/examples/invoke-custom-data/invoke-receiver.py b/examples/invoke-custom-data/invoke-receiver.py index 75882ef69..bb9ef5de4 100644 --- a/examples/invoke-custom-data/invoke-receiver.py +++ b/examples/invoke-custom-data/invoke-receiver.py @@ -1,4 +1,4 @@ -import proto.response_pb2 as response_messages +import custom_data_proto.response_pb2 as response_messages from dapr.ext.grpc import App, InvokeMethodRequest diff --git a/examples/rag/.env.example b/examples/rag/.env.example new file mode 100644 index 000000000..84aab35dc --- /dev/null +++ b/examples/rag/.env.example @@ -0,0 +1,110 @@ +# Copy to `.env` and fill in real values, or export these directly -- never commit +# a filled-in `.env`. `dapr run` does not load `.env` files automatically; either +# `export $(grep -v '^#' .env | xargs)` first, or use `dapr run ... -- env $(cat +# .env | xargs) python3 worker.py`, or your shell/IDE's own .env support. +# +# No variable here is a secret you must set: every credential-shaped one is +# optional, because the default authentication path for both clouds is the +# ambient credential chain (AWS's default provider chain; Azure's +# DefaultAzureCredential) -- set the corresponding *_ACCESS_KEY/*_API_KEY only +# for local development against services that don't support that (Azurite), +# or if you've deliberately chosen key-based auth. + +# --------------------------------------------------------------------------- +# Pipeline identity and Dapr components +# --------------------------------------------------------------------------- +RAG_PIPELINE_ID=company-knowledge +RAG_COLLECTION=company-knowledge +RAG_STATE_STORE=rag-pipeline-state +# Set to a configured Dapr pub/sub component name to publish an +# `index.version.activated` event after each successful activation. Leave +# unset to skip publishing (activation still happens either way). +RAG_ACTIVATION_PUBSUB= + +# --------------------------------------------------------------------------- +# Source: RAG_SOURCE=s3 | azure-blob +# --------------------------------------------------------------------------- +RAG_SOURCE=s3 + +# --- s3 --- +RAG_S3_BUCKET=company-docs +RAG_S3_PREFIX=policies/ +AWS_REGION=us-east-1 +# Point at LocalStack for local development, e.g. http://localhost:4566. +# Leave unset for real AWS. Credentials come from AWS's default provider +# chain (env vars, shared config, IAM role, SSO, ...) -- LocalStack accepts +# any non-empty AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY, conventionally "test". +RAG_S3_ENDPOINT_URL= + +# --- azure-blob --- +RAG_AZURE_ACCOUNT_URL=https://example.blob.core.windows.net +RAG_AZURE_CONTAINER=company-docs +RAG_AZURE_PREFIX=policies/ +# Set for Azurite or another dev connection string; omit to use +# DefaultAzureCredential (managed identity, workload identity, `az login`, ...). +RAG_AZURE_CONNECTION_STRING= + +# --------------------------------------------------------------------------- +# Embedder: RAG_EMBEDDER=openai | azure-openai +# --------------------------------------------------------------------------- +RAG_EMBEDDER=openai + +# --- openai --- +RAG_OPENAI_EMBEDDING_MODEL=text-embedding-3-small +OPENAI_API_KEY= + +# --- azure-openai --- +AZURE_OPENAI_ENDPOINT=https://my-resource.openai.azure.com +AZURE_OPENAI_EMBEDDING_DEPLOYMENT=text-embedding-3-small +AZURE_OPENAI_EMBEDDING_MODEL= +# Dev-only fallback; omit to use DefaultAzureCredential (recommended). +AZURE_OPENAI_API_KEY= +# Only needed by query_api.py (answer generation) -- may be a different +# deployment than the embeddings one, e.g. a GPT-4o-class chat model. +AZURE_OPENAI_CHAT_DEPLOYMENT= + +# --------------------------------------------------------------------------- +# Vector store: RAG_VECTOR_STORE=pgvector | pinecone | azure-ai-search +# --------------------------------------------------------------------------- +RAG_VECTOR_STORE=pgvector + +# --- pgvector --- +RAG_PGVECTOR_CONNECTION_STRING=postgresql://user:password@localhost:5432/ragdb + +# --- pinecone --- +RAG_PINECONE_INDEX=company-knowledge +PINECONE_API_KEY= + +# --- azure-ai-search --- +AZURE_SEARCH_ENDPOINT=https://my-search.search.windows.net +# Dev-only fallback; omit to use DefaultAzureCredential (recommended). +AZURE_SEARCH_API_KEY= +# Enables semantic ranking with this configuration name, if the Search +# service tier/configuration supports it. Leave unset to disable. +AZURE_SEARCH_SEMANTIC_CONFIG= + +# --- Foundry IQ knowledge-source registration (optional, off by default) --- +# See docs/rag/foundry-iq.md. Only meaningful with RAG_VECTOR_STORE=azure-ai-search, and +# requires AZURE_SEARCH_SEMANTIC_CONFIG to be set too (required by the REST API this targets). +# Leave RAG_FOUNDRY_IQ_KNOWLEDGE_SOURCE unset to disable registration entirely. +RAG_FOUNDRY_IQ_KNOWLEDGE_SOURCE= +RAG_FOUNDRY_IQ_SOURCE_DATA_FIELDS=title,source_uri +RAG_FOUNDRY_IQ_SEARCH_FIELDS=content + +# --------------------------------------------------------------------------- +# Pipeline tuning (all optional; defaults shown) +# --------------------------------------------------------------------------- +RAG_CHUNK_SIZE=1000 +RAG_CHUNK_OVERLAP=150 +RAG_MAX_CONCURRENT_DOCUMENTS=10 +RAG_EMBEDDING_BATCH_SIZE=64 +RAG_FAIL_FAST=false +RAG_QUERY_TOP_K=5 + +# --------------------------------------------------------------------------- +# Failure/resume demo -- set at most ONE of these, and never in a normal run. +# See README.md's "Failure and resume demo" section. +# --------------------------------------------------------------------------- +RAG_DEMO_FAIL_AFTER_DOCUMENTS= +RAG_DEMO_FAIL_AFTER_EMBEDDING_DOCUMENT= +RAG_DEMO_FAIL_DURING_BATCH= diff --git a/examples/rag/README.md b/examples/rag/README.md new file mode 100644 index 000000000..7811e5e8b --- /dev/null +++ b/examples/rag/README.md @@ -0,0 +1,313 @@ +# Durable RAG ingestion example + +A runnable sample for `DurableRAGPipeline`: discover documents from S3 or Azure Blob Storage, +parse and chunk them, embed them with OpenAI or Azure OpenAI, and write them into a versioned +pgvector, Pinecone, or Azure AI Search index -- surviving a worker crash mid-run without +re-embedding completed work, and never exposing a partially-built index to queries. See +[`docs/rag/README.md`](../../docs/rag/README.md) for the full architecture and configuration +reference this example is built on, and [`dapr/ext/rag/AGENTS.md`](../../dapr/ext/rag/AGENTS.md) +for internals. + +All scripts here read their configuration from environment variables (`config.py`), so the same +files work across every supported combination -- see [`.env.example`](.env.example) for every +variable. + +## Quickstart (fully local, except embeddings) + +The fastest path to a working end-to-end run: LocalStack standing in for S3, a local Docker +Postgres for pgvector, and a real OpenAI API key for embeddings (the one piece with no local +emulator). Every command below is copy-pasteable, in order, from this directory. + +1. **Initialize Dapr** (skip if you've already run this once): + ```sh + dapr init + ``` + +2. **Start LocalStack (S3) and a local pgvector Postgres.** Port 5432 is Postgres's own default + and is often already taken by a locally-installed Postgres -- 5544 avoids that: + ```sh + docker run -d --rm --name rag-localstack -p 4566:4566 -e SERVICES=s3 localstack/localstack:3 + docker run -d --rm --name rag-pgvector -p 5544:5432 \ + -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=ragdb pgvector/pgvector:pg16 + ``` + +3. **Install this example's dependencies**: + ```sh + pip3 install -r requirements.txt + ``` + +4. **Configure**: + ```sh + cp .env.example .env + ``` + Edit `.env` and set: + - `OPENAI_API_KEY=` -- the only real credential this quickstart needs. + - `RAG_S3_ENDPOINT_URL=http://localhost:4566` + - `RAG_PGVECTOR_CONNECTION_STRING=postgresql://postgres:postgres@localhost:5544/ragdb` + + Then load it, plus the two AWS variables LocalStack requires (any non-empty value works -- + `.env.example` notes this but doesn't template the lines, since real AWS deployments should + never set them): + ```sh + export $(grep -v '^#' .env | xargs) + export AWS_ACCESS_KEY_ID=test + export AWS_SECRET_ACCESS_KEY=test + ``` + +5. **Create the bucket and upload a sample document** (`boto3` is already installed from step 3): + ```sh + python3 -c " + import boto3 + s3 = boto3.client('s3', endpoint_url='http://localhost:4566', region_name='us-east-1') + s3.create_bucket(Bucket='company-docs') + s3.put_object(Bucket='company-docs', Key='policies/remote-work.txt', + Body=b'Employees may work remotely up to three days per week.') + " + ``` + +6. **Start the worker** and leave it running in this terminal (watch for `Worker ready.`): + ```sh + dapr run --app-id rag-worker --resources-path components/ -- python3 worker.py + ``` + +7. **In a second terminal**, first export the same variables again -- this is a new shell, so step + 4's exports aren't there: + ```sh + export $(grep -v '^#' .env | xargs) + export AWS_ACCESS_KEY_ID=test + export AWS_SECRET_ACCESS_KEY=test + ``` + Skipping this fails fast, either with a clear `Missing required environment variable: ...` + message or (for `OPENAI_API_KEY` specifically, which has no such check) a raw + `openai.OpenAIError: Missing credentials` traceback -- neither is dangerous, just restart the + command after exporting. + + Then start ingestion and poll status until it completes. Use the **same** `--app-id` as the + worker (`rag-worker`), not a new one -- see "Running the worker and CLI" below for why that + matters: + ```sh + dapr run --app-id rag-worker --resources-path components/ -- python3 cli.py start --version v1 + dapr run --app-id rag-worker --resources-path components/ -- python3 cli.py status --version v1 + ``` + Re-run `status` (a few seconds apart) until it shows `"stage": "completed"` and + `"activation_succeeded": true`. + +8. **Query it**: + ```sh + dapr run --app-id rag-worker --resources-path components/ -- python3 cli.py query "What is the remote work policy?" + ``` + You should see `remote-work.txt` as the top-scoring match. + +When you're done, stop the worker (`Ctrl+C` in its terminal) and the containers: +```sh +docker stop rag-localstack rag-pgvector +``` + +**Re-running this later?** Use a new `--version` (e.g. `v2`), not `v1` again. `cli.py start +--version v1` against a version that's already been used reattaches to that same, deterministic +workflow instance instead of starting a fresh one -- Dapr Workflow's state (kept in `dapr init`'s +Redis) isn't cleared by stopping the LocalStack/pgvector containers above, so you'd silently get +back the old run's status instead of processing anything new. This matches real usage anyway: a +version is meant to be unique per batch of content, not reused across attempts. + +From here: "Failure and resume demo" below shows the crash/resume behavior on this exact setup +(just add the `RAG_DEMO_FAIL_AFTER_DOCUMENTS` trigger before restarting the worker); "Prerequisites" +and the sections after it are the general reference for the other source/embedder/vector-store +combinations (Azure Blob, Pinecone, Azure AI Search) and the Azure-native flagship path. + +## Prerequisites + +- [Dapr CLI and initialized environment](https://docs.dapr.io/getting-started) +- [Install Python 3.10+](https://www.python.org/downloads/) +- [Docker](https://docs.docker.com/get-docker/) -- only for the Quickstart's local LocalStack/ + pgvector containers; skip it if you're pointing at real cloud services directly. +- One source (an S3 bucket or Azure Blob container with a few documents), one embedder (an OpenAI + or Azure OpenAI API key/deployment), and one vector store (a Postgres instance with the + `pgvector` extension available, a Pinecone index, or an Azure AI Search service) -- see + `docs/rag/README.md`'s per-provider configuration section for exactly what each needs. + `docs/rag/README.md`'s "Local development with LocalStack and Azurite" section covers the source + side for local testing without real cloud credentials. + +### Install requirements + +```sh +pip3 install -r requirements.txt +``` + +### Configure + +```sh +cp .env.example .env +# edit .env with real values, then: +export $(grep -v '^#' .env | xargs) +``` + +**This export is per-shell, not global.** Every terminal you run `worker.py`, `cli.py`, or +`failure_demo.py` from needs it run again -- a fresh terminal that skips it fails fast, either with +a clear `Missing required environment variable: ...` message or (for `OPENAI_API_KEY` +specifically, which has no such check) a raw `openai.OpenAIError: Missing credentials` traceback. + +## Running the worker and CLI + +The worker and CLI are separate processes on purpose: the worker (`worker.py`) is the thing that +must keep running (or be restarted) for ingestion to make progress; the CLI (`cli.py`) is a +short-lived process that only schedules/queries runs through the Dapr sidecar. They must run with +the **same** `--app-id`, though -- not "each with its own" as you might expect from other +multi-process Dapr examples: + +```sh +dapr run --app-id rag-worker --resources-path components/ -- python3 worker.py +``` + +In another terminal (export your `.env` variables there too -- see "Configure" above; it's a new +shell): + +```sh +dapr run --app-id rag-worker --resources-path components/ -- python3 cli.py start --version 2026-09 +dapr run --app-id rag-worker --resources-path components/ -- python3 cli.py status --version 2026-09 +dapr run --app-id rag-worker --resources-path components/ -- python3 cli.py resolve +dapr run --app-id rag-worker --resources-path components/ -- python3 cli.py query "What is the remote work policy?" +``` + +**Why the same app-id:** Dapr Workflow is backed by Dapr Actors internally, and the actor type +that hosts a given app's workflows/activities is namespaced by that app's `--app-id` +(`dapr.internal...workflow`). `DaprWorkflowClient` -- what `cli.py`'s +`pipeline.start()`/`get_status()` and `failure_demo.py`'s polling loop use under the hood -- always +targets the actor type namespaced under its *own local* sidecar's app-id; it has no parameter to +target a different one (that only exists on `ctx.call_activity(..., app_id=...)`, for cross-app +calls made from inside an already-running workflow). Give the CLI a different app-id than the +worker and it doesn't error -- it **hangs forever**, because its sidecar waits on a placement-table +entry for an actor type that nothing will ever register. The CLI can still be a separate OS +process, restarted per invocation, even on a different machine -- it just needs that one string to +match the worker's. + +`components/statestore.yaml` (a local Redis state store, with the `actorStateStore: "true"` +metadata Dapr Workflow requires) and `components/pubsub.yaml` (a local Redis pub/sub, standing in +for the real SNS/SQS/EventBridge or Azure Service Bus component -- see "Event-driven ingestion" +below) are provided for local development against `dapr init`'s default Redis. Point +`RAG_PGVECTOR_CONNECTION_STRING`/`RAG_PINECONE_INDEX`+`PINECONE_API_KEY`/`AZURE_SEARCH_ENDPOINT` at +a real service for the vector store side -- there's no local emulator for any of them. + +## Failure and resume demo + +Proves the core value proposition: a worker crash mid-run resumes without repeating completed +embedding work, and without exposing an incomplete index. + +1. In one terminal, set a failure trigger and start the worker: + ```sh + export RAG_DEMO_FAIL_AFTER_DOCUMENTS=2 + dapr run --app-id rag-worker --resources-path components/ -- python3 worker.py + ``` +2. In another terminal (export your `.env` variables there too, plus the AWS test vars -- it's a + new shell), start ingestion and watch status live. Both use the worker's **same** `--app-id` + (see "Running the worker and CLI" above for why): + ```sh + dapr run --app-id rag-worker --resources-path components/ -- python3 cli.py start --version demo + dapr run --app-id rag-worker --resources-path components/ -- python3 failure_demo.py --version demo + ``` +3. After the second document, the worker process hard-exits (`os._exit`, skipping cleanup -- + simulating a real crash or pod restart) -- you'll see the `dapr run` for `rag-worker` exit too. +4. Unset the trigger and restart the **same** worker: + ```sh + unset RAG_DEMO_FAIL_AFTER_DOCUMENTS + dapr run --app-id rag-worker --resources-path components/ -- python3 worker.py + ``` + Do **not** re-run `cli.py start` -- Dapr Workflow's own durability resumes the same orchestration + instance automatically once the worker reconnects. +5. Watch `failure_demo.py`'s output (or run `cli.py status --version demo` again): the run reaches + `stage=completed`, and `embedding_requests`/`avoided_embedding_units` show the documents + processed before the crash were not re-embedded. + +`RAG_DEMO_FAIL_AFTER_EMBEDDING_DOCUMENT=` and `RAG_DEMO_FAIL_DURING_BATCH=` are +the other two trigger points (see `.env.example`) -- set at most one at a time, and never in a +normal run; `FailureInjector` (`dapr/ext/rag/testing.py`) is off by default and only exists for +this kind of deliberate demonstration. + +## Event-driven ingestion + +One example subscriber per provider, entering through Dapr pub/sub rather than a direct +`cli.py start` call: + +```sh +# S3: Blob change -> SNS/SQS or EventBridge -> Dapr pub/sub -> here +dapr run --app-id rag-s3-trigger --resources-path components/ --app-port 6001 -- python3 pubsub_trigger.py + +# Azure: Blob change -> Event Grid -> Service Bus -> Dapr pub/sub -> here +dapr run --app-id rag-azure-trigger --resources-path components/azure/ --app-port 6002 -- python3 pubsub_trigger_servicebus.py +``` + +Both deduplicate by event ID (`EventDeduplicator`) and debounce a burst of changes into one +prefix-level reconciliation run (`reconciliation_workflow.py`) rather than starting ingestion per +individual event -- see that module's docstring and `docs/rag/README.md`'s "Event-driven ingestion" +section for why, and its documented limitation for a multi-replica subscriber deployment. + +## The Azure-native flagship path + +The complete path from the design brief: + +``` +Azure Blob Storage -> Event Grid -> Service Bus -> Dapr pub/sub -> Dapr Workflow + -> Azure OpenAI embeddings -> versioned Azure AI Search index -> alias activation + -> RAG query API -> Azure AI Search hybrid retrieval -> Azure OpenAI grounded answer + citations +``` + +Set `RAG_SOURCE=azure-blob`, `RAG_EMBEDDER=azure-openai`, `RAG_VECTOR_STORE=azure-ai-search`, plus +the corresponding `AZURE_*` variables (see `.env.example`), provision the Azure resources (see +[`infra/README.md`](infra/README.md) -- **not validated against a real subscription**, review +before use), and load `components/azure/*.yaml` instead of the local Redis components. Then: + +1. Upload a few documents to the configured Blob container. +2. `cli.py start --version 2026-09` -- starts the indexing workflow. +3. `failure_demo.py --version 2026-09` -- observe document/chunk progress live. +4. Set `RAG_DEMO_FAIL_AFTER_EMBEDDING_DOCUMENT=` and restart + the worker mid-run to intentionally terminate it during embedding (see "Failure and resume demo" + above for the full pattern). +5. Restart the worker (trigger unset) -- completed batches are not embedded again. +6. Once `stage=completed` and `validation_succeeded=true`, activation (with + `activate_when_complete=True`, the default) switches the Azure AI Search alias automatically -- + `cli.py resolve` shows the newly active version. +7. `python3 query_api.py "What is the remote work policy?"` -- submits a question. +8. Internally: retrieves via Azure AI Search hybrid search through the stable alias... +9. ...then calls Azure OpenAI to generate a grounded answer... +10. ...and prints the answer with citations back to the source blobs, plus `index_version` and + `workflow_instance_id` for provenance: + ```json + { + "answer": "...", + "citations": [{"title": "employee-handbook.pdf", "source_uri": "...", "chunk_id": "...", "score": 0.91}], + "index_version": "2026-09", + "workflow_instance_id": null, + "sufficient_evidence": true + } + ``` + +See [`docs/rag/observability.md`](../../docs/rag/observability.md) for the spans/correlation IDs +that let you trace one blob change through this entire path in Application Insights, and +[`docs/rag/azure-rbac.md`](../../docs/rag/azure-rbac.md) for exactly which identity needs which +permission at each step. + +## Foundry IQ knowledge-source registration (optional) + +Set `RAG_FOUNDRY_IQ_KNOWLEDGE_SOURCE` (plus `AZURE_SEARCH_SEMANTIC_CONFIG`, which it requires) +before starting `worker.py`, and every successful activation also registers/updates that Foundry +IQ knowledge source, pointed at the version's concrete physical index -- see +[`docs/rag/foundry-iq.md`](../../docs/rag/foundry-iq.md). Off by default; leave the variable +unset for every other flow above. + +## What this example does not automate + +There is no `tests/examples/test_rag.py`: every combination above needs a real (or LocalStack/ +Azurite-backed) cloud source, a real or local vector store, and -- for the embedder and Azure AI +Search/OpenAI paths -- a real service with no local emulator, which the default test suite +deliberately avoids depending on (see `dapr/ext/rag/AGENTS.md`'s Testing section). + +**What's actually been verified, manually, against real infrastructure:** the Quickstart above -- +S3 via LocalStack, a real pgvector Postgres, and a real OpenAI key -- end to end, including +`start`/`status`/`resolve`/`query` all returning correct results, and the crash/resume behavior in +"Failure and resume demo" with a real worker process killed mid-run. + +**What has not been run against anything real:** the Azure-native flagship path (Azure Blob, Azure +OpenAI, Azure AI Search, Foundry IQ -- `infra/README.md` says outright it's "not validated against +a real subscription"), the Pinecone vector store, and event-driven ingestion (both pub/sub +triggers). Those are exercised only by the mocked-I/O unit tests. Treat them as unverified until +you've run them yourself with real or local-emulated credentials. diff --git a/examples/rag/cli.py b/examples/rag/cli.py new file mode 100644 index 000000000..7c2909585 --- /dev/null +++ b/examples/rag/cli.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 The Dapr Authors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Command-line client for the DurableRAGPipeline example. + +A short-lived process: it only schedules/queries workflow runs through Dapr's +sidecar, so it needs `worker.py` running (separately, possibly on a different +machine/pod) to actually make progress. Run it with the **same** `--app-id` as +the worker -- Dapr Workflow resolves purely by that string, not by anything +else, so giving the CLI a different one silently hangs instead of erroring +(see README.md's "Running the worker and CLI" section for why): + + dapr run --app-id rag-worker --resources-path components/ -- python3 cli.py start --version 2026-09 + dapr run --app-id rag-worker --resources-path components/ -- python3 cli.py status --version 2026-09 + dapr run --app-id rag-worker --resources-path components/ -- python3 cli.py resolve + dapr run --app-id rag-worker --resources-path components/ -- python3 cli.py activate --version 2026-09 + dapr run --app-id rag-worker --resources-path components/ -- python3 cli.py query "What is the remote work policy?" + +See README.md for full setup. +""" + +from __future__ import annotations + +import argparse +import json + +from config import build_pipeline, build_retrieval_resolver + + +def _cmd_start(args: argparse.Namespace) -> None: + pipeline = build_pipeline() + try: + instance_id = pipeline.start( + version=args.version, + activate_when_complete=not args.no_activate, + prefix=args.prefix, + ) + print(f'Started ingestion. instance_id={instance_id}') + print(f'Check progress with: python3 cli.py status --version {args.version}') + finally: + pipeline.close() + + +def _cmd_status(args: argparse.Namespace) -> None: + pipeline = build_pipeline() + try: + status = pipeline.get_status(args.version) + finally: + pipeline.close() + if status is None: + print(f'No status recorded yet for version={args.version!r}.') + return + print(json.dumps(status.to_dict(), indent=2, default=str)) + + +def _cmd_resolve(_args: argparse.Namespace) -> None: + pipeline = build_pipeline() + try: + active_version = pipeline.resolve_active_version() + finally: + pipeline.close() + print(active_version or '(no version activated yet)') + + +def _cmd_activate(args: argparse.Namespace) -> None: + pipeline = build_pipeline() + try: + instance_id = pipeline.activate_version(args.version) + finally: + pipeline.close() + print(f'Activation started. instance_id={instance_id}') + + +def _cmd_query(args: argparse.Namespace) -> None: + resolver = build_retrieval_resolver() + try: + matches = resolver.query(args.question, top_k=args.top_k) + finally: + resolver.close() + if not matches: + print('No matches found (has a version been activated yet?).') + return + for rank, match in enumerate(matches, start=1): + source_name = match.metadata.get('source_name', match.document_id) + print(f'[{rank}] score={match.score:.3f} source={source_name} chunk_id={match.chunk_id}') + print(f' {match.content[:200]}{"..." if len(match.content) > 200 else ""}') + + +def _build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter + ) + subparsers = parser.add_subparsers(dest='command', required=True) + + start = subparsers.add_parser('start', help='Start (or resume) an ingestion run.') + start.add_argument( + '--version', required=True, help='Logical index version to build, e.g. 2026-09.' + ) + start.add_argument( + '--prefix', default=None, help='Overrides the source-configured prefix for this run.' + ) + start.add_argument( + '--no-activate', action='store_true', help='Build the version without activating it.' + ) + start.set_defaults(func=_cmd_start) + + status = subparsers.add_parser('status', help='Show ingestion status/metrics for a version.') + status.add_argument('--version', required=True) + status.set_defaults(func=_cmd_status) + + resolve = subparsers.add_parser('resolve', help='Print the currently active version.') + resolve.set_defaults(func=_cmd_resolve) + + activate = subparsers.add_parser( + 'activate', help='Validate and activate an already-built version.' + ) + activate.add_argument('--version', required=True) + activate.set_defaults(func=_cmd_activate) + + query = subparsers.add_parser( + 'query', help='Run a sample retrieval against the active version.' + ) + query.add_argument('question') + query.add_argument('--top-k', type=int, default=5) + query.set_defaults(func=_cmd_query) + + return parser + + +def main() -> None: + args = _build_parser().parse_args() + args.func(args) + + +if __name__ == '__main__': + main() diff --git a/examples/rag/components/azure/azure-keyvault-secretstore.yaml b/examples/rag/components/azure/azure-keyvault-secretstore.yaml new file mode 100644 index 000000000..612c4e15f --- /dev/null +++ b/examples/rag/components/azure/azure-keyvault-secretstore.yaml @@ -0,0 +1,50 @@ +# Dapr secret store component for Azure Key Vault. +# +# OPTIONAL. Only needed if a deployment chooses to keep some secret (e.g. a +# dev-only API key) outside of managed/workload identity -- the recommended +# deployment of this pipeline needs no secrets at all, and this component +# should simply not be deployed in that case +# (examples/rag/infra/main.bicep's `deploySecretStore` defaults to false and +# provisions no Key Vault when left at that default). +# +# Placeholders (replace before use, e.g. from the Bicep outputs): +# -> Bicep output keyVaultName +# -> Bicep output ingestionIdentityClientId +# +# Metadata field reference verified against +# https://docs.dapr.io/reference/components-reference/supported-secret-stores/azure-keyvault/ +# on 2026-09-10. +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: azure-keyvault +spec: + type: secretstores.azure.keyvault + version: v1 + metadata: + # Required. + - name: vaultName + value: "" + + # Optional. Client ID of the *user-assigned* managed identity that should + # read from this vault. Omit for a system-assigned identity, in which + # case vaultName is the only field this component needs. + # + # Deliberately not set here: azureTenantId / azureClientSecret / + # azureCertificateFile(Password). Those are only required for + # service-principal auth, which this sample avoids. + - name: azureClientId + value: "" + +# Which identity's client ID belongs above depends on which workload reads +# secrets through this component. If both the ingestion and query workloads +# need the same kind of dev-only fallback secret (e.g. an Azure OpenAI API +# key used instead of Microsoft Entra ID auth), deploy one rendered copy of +# this file per workload -- each with that workload's own azureClientId -- +# and use `scopes` below so each copy is only loaded by its own app-id. +# Both identities are granted the "Key Vault Secrets User" role in +# examples/rag/infra/modules/rbac.bicep when deploySecretStore is true; see +# docs/rag/azure-rbac.md. +# +# scopes: +# - rag-ingestion-worker diff --git a/examples/rag/components/azure/servicebus-pubsub.yaml b/examples/rag/components/azure/servicebus-pubsub.yaml new file mode 100644 index 000000000..83ba1aa9d --- /dev/null +++ b/examples/rag/components/azure/servicebus-pubsub.yaml @@ -0,0 +1,75 @@ +# Dapr pub/sub component for Azure Service Bus Topics. +# +# Wiring: Azure Blob Storage change events -> Event Grid system topic -> +# this Service Bus topic -> Dapr pub/sub subscription -> the ingestion +# workload's Dapr Workflow trigger. See examples/rag/infra/ for the Bicep +# that provisions the namespace/topic/subscription and the Event Grid +# relay, and docs/rag/azure-rbac.md for the exact role +# ("Azure Service Bus Data Receiver") the ingestion identity needs here. +# +# This component belongs to the INGESTION workload only -- the query +# workload never touches Service Bus. +# +# Placeholders (replace before use, e.g. from the Bicep outputs): +# -> Bicep output serviceBusNamespaceHostName +# -> Bicep output ingestionIdentityClientId +# +# Metadata field reference verified against +# https://docs.dapr.io/reference/components-reference/supported-pubsub/setup-azure-servicebus-topics/ +# on 2026-09-10. +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + # Matches the local Redis pub/sub component's name (components/pubsub.yaml) + # and what pubsub_trigger_servicebus.py subscribes through, so switching + # from local dev to Azure is a file swap, not a code change. + name: rag-events-pubsub +spec: + type: pubsub.azure.servicebus.topics + version: v1 + metadata: + # Required for Microsoft Entra ID (managed identity) auth in place of a + # connection string. Fully-qualified Service Bus namespace domain name. + - name: namespaceName + value: "" + + # Optional. Client ID of the ingestion workload's *user-assigned* managed + # identity. Omit this field entirely if the workload instead runs under a + # *system-assigned* managed identity -- Dapr's Azure AD auth then falls + # back to the platform-provided identity automatically, and no azure* + # auth field is needed at all. + # + # Deliberately not set here: azureTenantId / azureClientSecret / + # azureCertificateFile. Those are only required for service-principal + # (client-secret or certificate) auth, which this sample avoids in favor + # of managed identity -- the recommended path needs no secrets at all. + - name: azureClientId + value: "" + + # Optional. Identifies this consumer on the underlying AMQP link; shows up + # in Service Bus diagnostics and is useful when correlating connections + # across worker replicas. + - name: consumerID + value: "rag-ingestion-worker" + + # Optional. How long a message may stay unacknowledged before Service Bus + # redelivers it. Keep this comfortably above your slowest single-document + # processing time (download + parse + chunk + embed) to avoid duplicate + # deliveries racing the pipeline's own idempotency checks -- redelivery + # is still handled safely (see dapr/ext/rag/AGENTS.md's idempotency + # section), but a shorter lock duration means more redundant work. + - name: lockDurationInSec + value: "300" + + # Optional. Should match (or be <=) the subscription's own max delivery + # count (see examples/rag/infra/modules/servicebus.bicep) so Dapr and + # Service Bus agree on when a message is dead-lettered rather than + # retried forever. + - name: maxDeliveryCount + value: "10" + +# Optional hardening: restrict this component to the ingestion app-id only, +# once you know it (Dapr loads every component in its resources path by +# default; `scopes` limits which app-ids may use a given component). +# scopes: +# - rag-ingestion-worker diff --git a/examples/rag/components/azure/workflow-statestore.yaml b/examples/rag/components/azure/workflow-statestore.yaml new file mode 100644 index 000000000..01c097eac --- /dev/null +++ b/examples/rag/components/azure/workflow-statestore.yaml @@ -0,0 +1,102 @@ +# Dapr state store component for Azure Cache for Redis, used as the Dapr +# Workflow actor state store. This backs both Dapr Workflow's own +# orchestration state *and* DurableRAGPipeline's own idempotency/completion/ +# activation records (see dapr/ext/rag/AGENTS.md's "Idempotency and +# recovery" section) -- one store, two consumers. +# +# FLAGGED FOR REVIEW: Microsoft has announced a retirement timeline for +# Azure Cache for Redis across all SKUs, directing new workloads to "Azure +# Managed Redis" instead (see the retirement notice on +# https://learn.microsoft.com/azure/azure-cache-for-redis/cache-azure-active-directory-for-authentication, +# fetched 2026-09-10). This component still targets Azure Cache for Redis +# because that is the Dapr-documented, RBAC-capable path today; re-evaluate +# before a long-lived deployment. Azure Cosmos DB (Core/SQL API, with the +# right indexing policy) is the other commonly-used Dapr actor state store +# on Azure if you would rather avoid Redis entirely. +# +# BOTH workloads load this component: the ingestion workload needs +# read-write (Dapr Workflow's actor-state transactions plus its own +# idempotency writes), the query workload needs read-only (resolving the +# active-version pointer via ActiveVersionResolver). Because +# `azureClientId` below is a single static value, deploy one rendered copy +# of this file per workload, each with that workload's own managed +# identity's client ID, and use `scopes` (see the bottom of this file) to +# keep each copy loaded only by its own app-id. The placeholders below show +# the ingestion workload's copy. +# +# Placeholders (replace before use, e.g. from the Bicep outputs): +# -> Bicep output redisHostName +# -> Bicep output ingestionIdentityClientId +# (use queryIdentityClientId in the +# query workload's copy of this file) +# +# Metadata field reference verified against +# https://docs.dapr.io/reference/components-reference/supported-state-stores/setup-redis/ +# on 2026-09-10. +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + # Matches the local Redis state store component's name + # (components/statestore.yaml) and RAG_STATE_STORE's default in + # .env.example, so switching from local dev to Azure is a file swap. + name: rag-pipeline-state +spec: + type: state.redis + version: v1 + metadata: + # Required. "host:port" form -- Azure Cache for Redis's TLS port is 6380, + # not the plaintext default 6379. + - name: redisHost + value: ":6380" + + # Required alongside useEntraID: Microsoft Entra ID auth over Redis's ACL + # integration only works over a TLS connection. + - name: enableTLS + value: "true" + + # Enables Microsoft Entra ID (managed identity) authentication instead of + # an access key -- no secret is stored anywhere. Assumes the connecting + # identity has been granted a Redis access-policy assignment ("Data + # Owner" for this, the ingestion copy; "Data Reader" for the query + # workload's copy) -- see examples/rag/infra/modules/rbac.bicep and + # docs/rag/azure-rbac.md. Note this is a Redis-specific access-policy + # concept, not a generic Azure RBAC role. + - name: useEntraID + value: "true" + + # Client ID of this workload's *user-assigned* managed identity. Omit for + # a system-assigned identity. + - name: azureClientId + value: "" + + # Required by Dapr Workflow: the durabletask backend needs the actor + # state store's transactional multi-key write contract. + - name: actorStateStore + value: "true" + + # --- Alternative: access-key authentication ----------------------------- + # Use this instead of useEntraID/azureClientId above only if Microsoft + # Entra ID auth is unavailable or undesired for your cache -- notably, + # Entra ID authentication is NOT supported on Azure Cache for Redis + # Enterprise/Enterprise Flash tiers (Basic/Standard/Premium only). This + # keeps the access key itself out of this file: it is stored as a secret + # in the optional Key Vault secret store + # (azure-keyvault-secretstore.yaml) and resolved by name at + # component-load time via Dapr's secretKeyRef, using the secretStore + # named in this component's `auth` block below. This is a real + # trade-off, not a strict improvement: it reintroduces a long-lived + # credential (rotated through Key Vault) in place of the no-secrets + # managed-identity design this sample otherwise uses everywhere. + # + # - name: redisPassword + # secretKeyRef: + # name: workflow-redis-access-key + # key: workflow-redis-access-key + # auth: + # secretStore: azure-keyvault + +# Optional hardening: restrict each rendered copy of this component to its +# own workload's app-id (see the note above about deploying one copy per +# identity). +# scopes: +# - rag-ingestion-worker diff --git a/examples/rag/components/pubsub.yaml b/examples/rag/components/pubsub.yaml new file mode 100644 index 000000000..37c3bda57 --- /dev/null +++ b/examples/rag/components/pubsub.yaml @@ -0,0 +1,20 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: rag-events-pubsub +spec: + type: pubsub.redis + version: v1 + metadata: + - name: redisHost + value: localhost:6379 + - name: redisPassword + value: "" +# Local-development stand-in for the real pub/sub used in production: +# an SNS/SQS or EventBridge-backed component for the S3 path +# (pubsub_trigger.py), or the Azure Service Bus Topics component for the +# Azure path (pubsub_trigger_servicebus.py; see +# examples/rag/components/azure/servicebus-pubsub.yaml). Only the component +# name (`rag-events-pubsub`) and topic names need to match what the +# subscriber scripts declare -- swap this file for your cloud provider's +# pub/sub component to go from local testing to a real deployment. diff --git a/examples/rag/components/statestore.yaml b/examples/rag/components/statestore.yaml new file mode 100644 index 000000000..51c5ad604 --- /dev/null +++ b/examples/rag/components/statestore.yaml @@ -0,0 +1,19 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: rag-pipeline-state +spec: + type: state.redis + version: v1 + metadata: + - name: redisHost + value: localhost:6379 + - name: redisPassword + value: "" + # Required: Dapr Workflow uses the actor runtime internally, which needs an + # actor-capable state store -- see dapr/ext/workflow/AGENTS.md. This same + # store also holds this pipeline's own manifest/completion/activation + # records (small JSON documents; never vectors -- see + # dapr/ext/rag/AGENTS.md's state store key schema). + - name: actorStateStore + value: "true" diff --git a/examples/rag/config.py b/examples/rag/config.py new file mode 100644 index 000000000..647faea4c --- /dev/null +++ b/examples/rag/config.py @@ -0,0 +1,215 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 The Dapr Authors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Shared, environment-variable-driven configuration for the RAG example scripts. + +Every script in this directory (worker.py, cli.py, failure_demo.py, ...) +builds its `DurableRAGPipeline` through this module, so the same scripts work +across every supported source/embedder/vector-store combination -- see +`.env.example` for every variable, and `README.md` for the two worked +combinations from the SDK documentation (S3 + pgvector, Azure Blob + +Pinecone), plus the Azure-native flagship path (Azure Blob + Azure OpenAI + +Azure AI Search). +""" + +from __future__ import annotations + +import os +from typing import Optional + +from dapr.ext.rag import ( + ActiveVersionResolver, + AzureAISearchVectorStore, + AzureBlobSource, + AzureOpenAIEmbedder, + DocumentSource, + DurableRAGPipeline, + Embedder, + FoundryIQKnowledgeSourceConfig, + OpenAIEmbedder, + PgVectorStore, + PineconeVectorStore, + PipelineConfig, + S3Source, + TextSplitter, + UnstructuredParser, + VectorIndex, +) +from dapr.ext.rag.testing import FailureInjector + + +def require_env(name: str) -> str: + value = os.environ.get(name) + if not value: + raise SystemExit(f'Missing required environment variable: {name} (see .env.example)') + return value + + +def optional_env(name: str) -> Optional[str]: + return os.environ.get(name) or None + + +def build_source() -> DocumentSource: + """Builds the configured `DocumentSource` from `RAG_SOURCE` ('s3' or 'azure-blob').""" + provider = os.environ.get('RAG_SOURCE', 's3').lower() + if provider == 's3': + return S3Source( + bucket=require_env('RAG_S3_BUCKET'), + prefix=optional_env('RAG_S3_PREFIX'), + region_name=optional_env('AWS_REGION'), + # Set for LocalStack, e.g. http://localhost:4566 -- see README.md. + endpoint_url=optional_env('RAG_S3_ENDPOINT_URL'), + ) + if provider == 'azure-blob': + return AzureBlobSource( + account_url=optional_env('RAG_AZURE_ACCOUNT_URL'), + container=require_env('RAG_AZURE_CONTAINER'), + prefix=optional_env('RAG_AZURE_PREFIX'), + # Set for Azurite or local dev; omit to use DefaultAzureCredential. + connection_string=optional_env('RAG_AZURE_CONNECTION_STRING'), + ) + raise SystemExit(f"Unknown RAG_SOURCE={provider!r}; expected 's3' or 'azure-blob'.") + + +def build_embedder() -> Embedder: + """Builds the configured `Embedder` from `RAG_EMBEDDER` ('openai' or 'azure-openai').""" + provider = os.environ.get('RAG_EMBEDDER', 'openai').lower() + if provider == 'openai': + return OpenAIEmbedder( + model=os.environ.get('RAG_OPENAI_EMBEDDING_MODEL', 'text-embedding-3-small') + ) + if provider == 'azure-openai': + return AzureOpenAIEmbedder( + endpoint=require_env('AZURE_OPENAI_ENDPOINT'), + deployment=require_env('AZURE_OPENAI_EMBEDDING_DEPLOYMENT'), + model=optional_env('AZURE_OPENAI_EMBEDDING_MODEL'), + # Dev-only fallback; omit to use DefaultAzureCredential (recommended). + api_key=optional_env('AZURE_OPENAI_API_KEY'), + ) + raise SystemExit(f"Unknown RAG_EMBEDDER={provider!r}; expected 'openai' or 'azure-openai'.") + + +def build_vector_store() -> VectorIndex: + """Builds the configured `VectorIndex` from `RAG_VECTOR_STORE`.""" + provider = os.environ.get('RAG_VECTOR_STORE', 'pgvector').lower() + collection = os.environ.get('RAG_COLLECTION', 'company-knowledge') + if provider == 'pgvector': + return PgVectorStore( + connection_string=require_env('RAG_PGVECTOR_CONNECTION_STRING'), + collection=collection.replace('-', '_'), # PgVectorStore requires a SQL-safe identifier + ) + if provider == 'pinecone': + return PineconeVectorStore( + index_name=require_env('RAG_PINECONE_INDEX'), api_key=optional_env('PINECONE_API_KEY') + ) + if provider == 'azure-ai-search': + return AzureAISearchVectorStore( + endpoint=require_env('AZURE_SEARCH_ENDPOINT'), + index_base_name=collection, + api_key=optional_env('AZURE_SEARCH_API_KEY'), + semantic_configuration_name=optional_env('AZURE_SEARCH_SEMANTIC_CONFIG'), + ) + raise SystemExit( + f"Unknown RAG_VECTOR_STORE={provider!r}; expected 'pgvector', 'pinecone', or 'azure-ai-search'." + ) + + +def build_pipeline_id() -> str: + """The pipeline ID every script agrees on, so the CLI/worker/query-time reader + all address the same pipeline state without needing to construct a full + `DurableRAGPipeline` (which requires source credentials) just to query it.""" + return os.environ.get('RAG_PIPELINE_ID') or os.environ.get( + 'RAG_COLLECTION', 'company-knowledge' + ) + + +def build_state_store_name() -> str: + return os.environ.get('RAG_STATE_STORE', 'rag-pipeline-state') + + +def build_demo_failure_injector() -> Optional[FailureInjector]: + """Builds a `FailureInjector` from `RAG_DEMO_FAIL_*` env vars, or `None`. + + See failure_demo.py and README.md's "Failure and resume demo" section -- + at most one of these should be set at a time, and never in a normal run. + """ + if os.environ.get('RAG_DEMO_FAIL_AFTER_DOCUMENTS'): + return FailureInjector( + fail_after_documents=int(os.environ['RAG_DEMO_FAIL_AFTER_DOCUMENTS']) + ) + if os.environ.get('RAG_DEMO_FAIL_AFTER_EMBEDDING_DOCUMENT'): + return FailureInjector( + fail_after_embedding_before_completion_for_document=os.environ[ + 'RAG_DEMO_FAIL_AFTER_EMBEDDING_DOCUMENT' + ] + ) + if os.environ.get('RAG_DEMO_FAIL_DURING_BATCH'): + return FailureInjector( + fail_during_batch_index=int(os.environ['RAG_DEMO_FAIL_DURING_BATCH']) + ) + return None + + +def build_foundry_iq_knowledge_source() -> Optional[FoundryIQKnowledgeSourceConfig]: + """Builds a `FoundryIQKnowledgeSourceConfig` from `RAG_FOUNDRY_IQ_KNOWLEDGE_SOURCE`, or `None`. + + Opt-in and off by default -- see docs/rag/foundry-iq.md. Only meaningful with + RAG_VECTOR_STORE=azure-ai-search; DurableRAGPipeline itself raises a clear ValueError at + construction time if this is set alongside any other vector store. + """ + name = optional_env('RAG_FOUNDRY_IQ_KNOWLEDGE_SOURCE') + if not name: + return None + return FoundryIQKnowledgeSourceConfig( + name=name, + source_data_fields=tuple(_split_csv_env('RAG_FOUNDRY_IQ_SOURCE_DATA_FIELDS')), + search_fields=tuple(_split_csv_env('RAG_FOUNDRY_IQ_SEARCH_FIELDS')), + ) + + +def _split_csv_env(name: str) -> list[str]: + raw = optional_env(name) + return [field.strip() for field in raw.split(',') if field.strip()] if raw else [] + + +def build_pipeline(*, failure_injector: Optional[FailureInjector] = None) -> DurableRAGPipeline: + """Builds the fully-configured `DurableRAGPipeline` for this environment.""" + return DurableRAGPipeline( + source=build_source(), + parser=UnstructuredParser(), + splitter=TextSplitter( + chunk_size=int(os.environ.get('RAG_CHUNK_SIZE', '1000')), + chunk_overlap=int(os.environ.get('RAG_CHUNK_OVERLAP', '150')), + ), + embedder=build_embedder(), + vector_store=build_vector_store(), + state_store_name=build_state_store_name(), + pipeline_id=build_pipeline_id(), + config=PipelineConfig( + max_concurrent_documents=int(os.environ.get('RAG_MAX_CONCURRENT_DOCUMENTS', '10')), + embedding_batch_size=int(os.environ.get('RAG_EMBEDDING_BATCH_SIZE', '64')), + fail_fast=os.environ.get('RAG_FAIL_FAST', 'false').lower() == 'true', + ), + pubsub_name=optional_env('RAG_ACTIVATION_PUBSUB'), + foundry_iq_knowledge_source=build_foundry_iq_knowledge_source(), + failure_injector=failure_injector or build_demo_failure_injector(), + ) + + +def build_retrieval_resolver() -> ActiveVersionResolver: + """Builds a resolver for query-time use, without needing source credentials.""" + return ActiveVersionResolver( + pipeline_id=build_pipeline_id(), + state_store_name=build_state_store_name(), + vector_store=build_vector_store(), + embedder=build_embedder(), + ) diff --git a/examples/rag/failure_demo.py b/examples/rag/failure_demo.py new file mode 100644 index 000000000..31a7262c0 --- /dev/null +++ b/examples/rag/failure_demo.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 The Dapr Authors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Watches ingestion status live, for the failure-and-resume demo. + +This script doesn't itself crash anything -- `worker.py` does that, via +`FailureInjector`, when one of the `RAG_DEMO_FAIL_*` environment variables is +set (see config.py's `build_demo_failure_injector` and README.md's "Failure +and resume demo" section for the full step-by-step). Run this alongside the +worker and `cli.py start`, with the **same** `--app-id` as the worker (see +README.md's "Running the worker and CLI" section for why), to watch +`embedding_requests` and `avoided_embedding_units` live across a crash and +restart: + + dapr run --app-id rag-worker --resources-path components/ -- python3 failure_demo.py --version demo + +Ctrl+C to stop watching (this never touches the pipeline itself). +""" + +from __future__ import annotations + +import argparse +import time + +from config import build_pipeline + + +def watch(version: str, *, interval_seconds: float) -> None: + pipeline = build_pipeline() + try: + print(f'Watching pipeline status for version={version!r} (Ctrl+C to stop)...\n') + last_line = None + while True: + status = pipeline.get_status(version) + if status is None: + line = f'[{_now()}] no status recorded yet' + else: + line = ( + f'[{_now()}] stage={status.stage} ' + f'documents(completed/skipped/failed/total)=' + f'{status.completed_documents}/{status.skipped_documents}/' + f'{status.failed_documents}/{status.total_documents} ' + f'embedding_requests={status.embedding_requests} ' + f'avoided_embedding_units={status.avoided_embedding_units} ' + f'retry_count={status.retry_count} ' + f'active_version={pipeline.resolve_active_version()}' + ) + if line != last_line: + print(line, flush=True) + last_line = line + time.sleep(interval_seconds) + except KeyboardInterrupt: + print('\nStopped watching.') + finally: + pipeline.close() + + +def _now() -> str: + return time.strftime('%H:%M:%S') + + +def main() -> None: + parser = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter + ) + parser.add_argument('--version', required=True, help='The version being ingested, e.g. demo.') + parser.add_argument('--interval-seconds', type=float, default=2.0) + args = parser.parse_args() + watch(args.version, interval_seconds=args.interval_seconds) + + +if __name__ == '__main__': + main() diff --git a/examples/rag/infra/README.md b/examples/rag/infra/README.md new file mode 100644 index 000000000..f6d1e4ef7 --- /dev/null +++ b/examples/rag/infra/README.md @@ -0,0 +1,189 @@ +# RAG pipeline infrastructure (Bicep) + +Bicep templates that provision the Azure resources for the flagship, Azure-native deployment of +`DurableRAGPipeline`: + +``` +Azure Blob Storage + -> Event Grid -> Azure Service Bus -> Dapr pub/sub -> Dapr Workflow + -> download, parse, chunk (workflow activities) + -> Azure OpenAI embeddings (workflow activities) + -> write to a version-specific Azure AI Search index (workflow activities) + -> validate the new index, then atomically switch the Azure AI Search alias + -> RAG query API + -> Azure AI Search hybrid retrieval, queried via the alias + -> Azure OpenAI chat completion for a grounded, cited answer +``` + +## Status: starting point, not a validated template + +**This Bicep was authored, and reviewed with the standalone Bicep CLI (`bicep build` / `bicep +lint`, both clean), in an environment with no Azure subscription available.** It has not been +deployed against a real subscription, and no `az deployment group what-if`/`create` has been run +against it. Treat it as a reviewed starting point, not production-ready infrastructure: + +- Re-check every SKU, capacity, and default value against your own quota, region availability, + and cost constraints before deploying. +- Re-check the two flagged items below before relying on this in anything long-lived. +- Have someone who owns your subscription's security posture review the RBAC design in + [`../../../docs/rag/azure-rbac.md`](../../../docs/rag/azure-rbac.md) before granting it access to + real data. + +### Flagged for review + +1. **Azure Cache for Redis retirement.** Microsoft has announced a retirement timeline for Azure + Cache for Redis across all SKUs, directing new workloads to "Azure Managed Redis" instead (see + the retirement notice on [Microsoft Learn's Entra ID authentication + page](https://learn.microsoft.com/azure/azure-cache-for-redis/cache-azure-active-directory-for-authentication), + fetched 2026-09-10). `modules/redis.bicep` still provisions `Microsoft.Cache/redis` because + that is what the Dapr Redis state store component and this sample's component YAML + (`../components/azure/workflow-statestore.yaml`) are documented against. Re-evaluate against + Azure Managed Redis, or against the Cosmos DB alternative mentioned in the RBAC doc, before + committing to this for anything long-lived. +2. **Redis Microsoft Entra ID wiring is the least independently-verified part of this template.** + The `redisConfiguration['aad-enabled']` property and the `Microsoft.Cache/redis/accessPolicyAssignments` + sub-resource (with built-in access policy names `Data Owner` / `Data Reader`) reflect the + product behavior described in Microsoft's own Redis Entra ID documentation, but the exact ARM + property/resource shape was not independently confirmed against the ARM template reference in + this session. It compiles cleanly with the Bicep CLI, which validates property names against + the resource type's schema, but a schema-valid template can still behave differently than + intended at runtime. Confirm end-to-end (enable Entra auth, assign an access policy, connect + with `useEntraID: "true"`) against a real cache before depending on it. + +## Prerequisites + +- An Azure subscription and a resource group to deploy into. +- [Azure CLI](https://learn.microsoft.com/cli/azure/install-azure-cli), signed in: + ```sh + az login + az account set --subscription + ``` +- Bicep tooling. The Azure CLI can install/update it for you: + ```sh + az bicep install + az bicep upgrade + ``` +- Quota for an Azure OpenAI resource with the embeddings and chat models/versions you intend to + deploy, in your target region. Azure OpenAI model availability varies by region and + subscription -- check with: + ```sh + az cognitiveservices account list-models \ + --location \ + --query "[].{model:name, version:version}" \ + -o table + ``` + (This requires an existing Cognitive Services account in that region to query against, or use + the [Azure AI Foundry model catalog](https://ai.azure.com) to check regional availability before + deploying.) + +## Deploy + +```sh +az group create --name --location + +az deployment group create \ + --resource-group \ + --template-file main.bicep \ + --parameters namePrefix=rag \ + --parameters embeddingModelVersion= \ + --parameters chatModelVersion= +``` + +Every parameter has a default (see `main.bicep`'s `@description` decorators); override only what +you need to. Common overrides: + +```sh +az deployment group create \ + --resource-group \ + --template-file main.bicep \ + --parameters \ + namePrefix=myrag \ + location=eastus2 \ + searchSkuName=standard \ + embeddingModelName=text-embedding-3-small \ + embeddingModelVersion=1 \ + chatModelName=gpt-4o \ + chatModelVersion=2024-11-20 \ + deploySecretStore=false +``` + +After a successful deployment, read the outputs back out to fill in the Python config surface and +the Dapr component YAMLs: + +```sh +az deployment group show \ + --resource-group \ + --name main \ + --query properties.outputs +``` + +## What gets created + +| Resource | Purpose | +|---|---| +| Storage account + blob container | Source documents the pipeline ingests | +| Service Bus namespace + topic + subscription | Relays Blob Storage change events to Dapr pub/sub | +| Event Grid system topic + event subscription (optional, `deployEventGridSubscription`) | Wires Blob Storage `BlobCreated`/`BlobDeleted` events to the Service Bus topic, using the system topic's own managed identity to deliver | +| Azure AI Search service | Vector store; `DurableRAGPipeline` creates the versioned indexes and alias at runtime, not this template | +| Azure OpenAI account + 2 deployments | One embeddings deployment, one chat deployment | +| Azure Cache for Redis | Dapr Workflow actor state store + this pipeline's idempotency records (see the retirement note above) | +| 2 user-assigned managed identities | Ingestion workload and query workload, least-privilege (see `docs/rag/azure-rbac.md`) | +| Role/access-policy assignments | Wires each identity to exactly the resources it needs, scoped per-resource (never subscription- or resource-group-scoped) | +| Azure Key Vault (optional, `deploySecretStore`) | Only if a deployment intentionally keeps a dev-only secret outside of managed identity | +| Log Analytics workspace + Application Insights | OpenTelemetry destination (see `docs/rag/observability.md`) | + +## Manual step: Event Grid, if `deployEventGridSubscription=false` + +If you disable the Event Grid module (or it fails to deploy and you want to unblock the rest of +the stack), wire the same thing manually once the storage account and Service Bus topic exist: + +```sh +az eventgrid system-topic create \ + --name -events \ + --resource-group \ + --location \ + --topic-type Microsoft.Storage.StorageAccounts \ + --source /subscriptions//resourceGroups//providers/Microsoft.Storage/storageAccounts/ \ + --mi-system-assigned + +az eventgrid system-topic event-subscription create \ + --name blob-to-servicebus \ + --system-topic-name -events \ + --resource-group \ + --endpoint-type servicebustopic \ + --endpoint /subscriptions//resourceGroups//providers/Microsoft.ServiceBus/namespaces//topics/ \ + --included-event-types Microsoft.Storage.BlobCreated Microsoft.Storage.BlobDeleted \ + --delivery-identity SystemAssigned + +# The system topic's identity needs to send to the destination topic: +az role assignment create \ + --assignee-object-id \ + --assignee-principal-type ServicePrincipal \ + --role "Azure Service Bus Data Sender" \ + --scope /subscriptions//resourceGroups//providers/Microsoft.ServiceBus/namespaces//topics/ +``` + +## File layout + +``` +infra/ +├── main.bicep # Parameters, module wiring, outputs +├── modules/ +│ ├── identity.bicep # Ingestion + query user-assigned managed identities +│ ├── storage.bicep # Storage account + blob container +│ ├── servicebus.bicep # Namespace + topic + subscription +│ ├── eventgrid.bicep # System topic + event subscription (Blob -> Service Bus) +│ ├── search.bicep # Azure AI Search service +│ ├── openai.bicep # Azure OpenAI account + embeddings/chat deployments +│ ├── redis.bicep # Azure Cache for Redis (Dapr actor state store) +│ ├── keyvault.bicep # Optional secret store +│ ├── monitoring.bicep # Log Analytics + Application Insights +│ └── rbac.bicep # Every role/access-policy assignment, centralized for review +└── README.md +``` + +## Cleaning up + +```sh +az group delete --name --yes --no-wait +``` diff --git a/examples/rag/infra/main.bicep b/examples/rag/infra/main.bicep new file mode 100644 index 000000000..21d3b4654 --- /dev/null +++ b/examples/rag/infra/main.bicep @@ -0,0 +1,325 @@ +// Azure infrastructure for the dapr.ext.rag example: a durable RAG ingestion +// pipeline (DurableRAGPipeline) running on Dapr Workflow, with an Azure AI +// Search-backed vector store, Azure OpenAI for embeddings/chat, Azure Cache +// for Redis as the Dapr Workflow actor state store, and an Event Grid -> +// Service Bus -> Dapr pub/sub trigger path from Blob Storage. +// +// STARTING POINT, NOT A VALIDATED TEMPLATE. This file was authored without +// access to an Azure subscription to deploy or test against -- there is no +// substitute for reviewing every resource, SKU, and role assignment below +// before using it against a real subscription. See infra/README.md for the +// full disclaimer, prerequisites, and deployment command. +// +// Naming: every globally-unique resource name is derived from `namePrefix` +// and `uniqueString(resourceGroup().id)` -- nothing here hard-codes a name, +// region, subscription ID, or tenant ID. +// +// See docs/rag/azure-rbac.md for the two identities this template provisions +// (ingestion vs. query) and exactly which role each role assignment below +// corresponds to. + +targetScope = 'resourceGroup' + +@description('Short prefix used to build resource names. Keep it short and lowercase-alphanumeric: it feeds into length-constrained names (storage account, Key Vault).') +@maxLength(12) +param namePrefix string = 'rag' + +@description('Azure region for all resources. Defaults to the location of the target resource group.') +param location string = resourceGroup().location + +@description('Tags applied to every resource this template creates.') +param tags object = { + sample: 'dapr-rag-pipeline' +} + +@description('Name of the blob container that holds source documents.') +param blobContainerName string = 'source-documents' + +@description('Service Bus topic that receives Blob Storage change notifications relayed by Event Grid.') +param serviceBusTopicName string = 'blob-events' + +@description('Service Bus subscription the ingestion workload consumes from via Dapr pub/sub.') +param serviceBusSubscriptionName string = 'rag-ingestion' + +@description('Service Bus namespace SKU. Topics require Standard or Premium -- Basic tier does not support topics/subscriptions at all.') +@allowed([ + 'Standard' + 'Premium' +]) +param serviceBusSkuName string = 'Standard' + +@description('Attempt to provision the Event Grid system topic + event subscription that relays Blob Storage change events to the Service Bus topic. When false, wire this manually -- see infra/README.md for the equivalent az cli steps.') +param deployEventGridSubscription bool = true + +@description('Azure AI Search SKU. Free supports hybrid/vector search but has tight per-service limits (3 indexes, 50 MB storage) and no semantic ranking. Basic is the practical minimum for this sample: it supports semantic ranking (used to re-rank hybrid results) and comfortably holds the base + in-progress-version indexes this pipeline creates during a rebuild.') +@allowed([ + 'basic' + 'standard' + 'standard2' + 'standard3' +]) +param searchSkuName string = 'basic' + +@description('Base name for the versioned physical indexes and the stable alias this pipeline manages, e.g. "company-knowledge" -> indexes "company-knowledge-2026-09", alias "company-knowledge-active". Passed straight through to AzureAISearchVectorStore(index_base_name=...).') +param searchIndexBaseName string = 'company-knowledge' + +@description('Azure OpenAI account SKU. S0 is the standard (only) SKU for Azure OpenAI accounts today.') +param openAiSkuName string = 'S0' + +@description('Embeddings model to deploy. Model/version availability varies by region and subscription -- verify with `az cognitiveservices account list-models` before relying on this default.') +param embeddingModelName string = 'text-embedding-3-small' + +@description('Embeddings model version.') +param embeddingModelVersion string = '1' + +@description('Provisioned throughput for the embeddings deployment (capacity units; 1 unit = 1K TPM for Standard/GlobalStandard SKUs).') +param embeddingDeploymentCapacity int = 30 + +@description('Chat/completions model deployed for query-time, grounded answer generation. Model/version availability varies by region and subscription.') +param chatModelName string = 'gpt-4o' + +@description('Chat model version.') +param chatModelVersion string = '2024-11-20' + +@description('Provisioned throughput for the chat deployment.') +param chatDeploymentCapacity int = 10 + +@description('Deployment SKU shared by both model deployments. GlobalStandard has the broadest quota availability; fall back to Standard if GlobalStandard is not offered for a model/region combination in your subscription.') +param openAiDeploymentSkuName string = 'GlobalStandard' + +@description('Azure Cache for Redis SKU name backing the Dapr Workflow actor state store.') +@allowed([ + 'Basic' + 'Standard' + 'Premium' +]) +param redisSkuName string = 'Basic' + +@description('Redis SKU family: C for Basic/Standard, P for Premium.') +param redisSkuFamily string = 'C' + +@description('Redis SKU capacity/size within the family (0-6 for Basic/Standard family C).') +param redisSkuCapacity int = 1 + +@description('Deploy an optional Azure Key Vault secret store. The recommended path needs no secrets at all (pure managed/workload identity) -- only enable this if a deployment intentionally keeps a dev-only credential (e.g. a fallback API key) outside of identity-based auth.') +param deploySecretStore bool = false + +var uniqueSuffix = uniqueString(resourceGroup().id, namePrefix) +var storageAccountName = take(toLower('${namePrefix}st${uniqueSuffix}'), 24) +var serviceBusNamespaceName = '${namePrefix}-sb-${uniqueSuffix}' +var searchServiceName = '${namePrefix}-search-${uniqueSuffix}' +var openAiAccountName = '${namePrefix}-aoai-${uniqueSuffix}' +var redisName = '${namePrefix}-redis-${uniqueSuffix}' +var keyVaultName = take('${namePrefix}-kv-${uniqueSuffix}', 24) +var logAnalyticsName = '${namePrefix}-logs-${uniqueSuffix}' +var appInsightsName = '${namePrefix}-appi-${uniqueSuffix}' +var ingestionIdentityName = '${namePrefix}-ingestion-id' +var queryIdentityName = '${namePrefix}-query-id' +var embeddingDeploymentName = 'embedding' +var chatDeploymentName = 'chat' +var searchAliasName = '${searchIndexBaseName}-active' +// Free tier cannot run the semantic ranker at all; every paid tier can. +var semanticSearchSetting = searchSkuName == 'free' ? 'disabled' : 'standard' + +module identities 'modules/identity.bicep' = { + name: 'identities' + params: { + location: location + ingestionIdentityName: ingestionIdentityName + queryIdentityName: queryIdentityName + tags: tags + } +} + +module storage 'modules/storage.bicep' = { + name: 'storage' + params: { + location: location + storageAccountName: storageAccountName + blobContainerName: blobContainerName + tags: tags + } +} + +module serviceBus 'modules/servicebus.bicep' = { + name: 'serviceBus' + params: { + location: location + serviceBusNamespaceName: serviceBusNamespaceName + skuName: serviceBusSkuName + topicName: serviceBusTopicName + subscriptionName: serviceBusSubscriptionName + tags: tags + } +} + +// Blob Storage -> Event Grid -> this Service Bus topic. See the module for +// the identity-based delivery wiring and infra/README.md for the manual +// az cli fallback if you set deployEventGridSubscription to false. +module eventGrid 'modules/eventgrid.bicep' = if (deployEventGridSubscription) { + name: 'eventGrid' + params: { + location: location + storageAccountId: storage.outputs.storageAccountId + storageAccountName: storage.outputs.storageAccountName + serviceBusNamespaceName: serviceBus.outputs.namespaceName + serviceBusTopicName: serviceBus.outputs.topicName + serviceBusTopicId: serviceBus.outputs.topicId + tags: tags + } +} + +module search 'modules/search.bicep' = { + name: 'search' + params: { + location: location + searchServiceName: searchServiceName + skuName: searchSkuName + semanticSearchTier: semanticSearchSetting + tags: tags + } +} + +module openAi 'modules/openai.bicep' = { + name: 'openAi' + params: { + location: location + openAiAccountName: openAiAccountName + skuName: openAiSkuName + embeddingModelName: embeddingModelName + embeddingModelVersion: embeddingModelVersion + embeddingDeploymentName: embeddingDeploymentName + embeddingCapacity: embeddingDeploymentCapacity + chatModelName: chatModelName + chatModelVersion: chatModelVersion + chatDeploymentName: chatDeploymentName + chatCapacity: chatDeploymentCapacity + deploymentSkuName: openAiDeploymentSkuName + tags: tags + } +} + +// NOTE (flagged for review, see infra/README.md): Microsoft has announced a +// retirement timeline for Azure Cache for Redis across all SKUs in favor of +// "Azure Managed Redis". This module still targets Microsoft.Cache/redis +// because that is what the Dapr Redis state store component documents and +// what this sample's Dapr component YAML (examples/rag/components/azure/ +// workflow-statestore.yaml) is written against. Re-evaluate against Azure +// Managed Redis before using this in a long-lived deployment. +module redis 'modules/redis.bicep' = { + name: 'redis' + params: { + location: location + redisName: redisName + skuName: redisSkuName + skuFamily: redisSkuFamily + skuCapacity: redisSkuCapacity + tags: tags + } +} + +module monitoring 'modules/monitoring.bicep' = { + name: 'monitoring' + params: { + location: location + logAnalyticsName: logAnalyticsName + appInsightsName: appInsightsName + tags: tags + } +} + +module keyVault 'modules/keyvault.bicep' = if (deploySecretStore) { + name: 'keyVault' + params: { + deploy: deploySecretStore + location: location + keyVaultName: keyVaultName + tags: tags + } +} + +// Centralizes every role/access-policy assignment for both workload +// identities in one auditable place -- cross-reference against the table in +// docs/rag/azure-rbac.md. +module rbac 'modules/rbac.bicep' = { + name: 'rbac' + params: { + ingestionPrincipalId: identities.outputs.ingestionPrincipalId + queryPrincipalId: identities.outputs.queryPrincipalId + storageAccountName: storage.outputs.storageAccountName + serviceBusNamespaceName: serviceBus.outputs.namespaceName + serviceBusTopicName: serviceBus.outputs.topicName + serviceBusSubscriptionName: serviceBus.outputs.subscriptionName + searchServiceName: search.outputs.searchServiceName + openAiAccountName: openAi.outputs.openAiAccountName + redisName: redis.outputs.redisName + deploySecretStore: deploySecretStore + keyVaultName: keyVault.?outputs.?keyVaultName ?? '' + } +} + +@description('Storage account name backing the source-documents container.') +output storageAccountName string = storage.outputs.storageAccountName + +@description('Blob endpoint for the storage account.') +output blobEndpoint string = storage.outputs.blobEndpoint + +@description('Source-documents blob container name.') +output sourceContainerName string = storage.outputs.containerName + +@description('Service Bus namespace fully-qualified domain name -- fills the namespaceName field of the pubsub component.') +output serviceBusNamespaceHostName string = '${serviceBus.outputs.namespaceName}.servicebus.windows.net' + +@description('Service Bus topic that receives Blob Storage change events.') +output serviceBusTopicName string = serviceBus.outputs.topicName + +@description('Service Bus subscription the ingestion workload consumes from.') +output serviceBusSubscriptionName string = serviceBus.outputs.subscriptionName + +@description('Azure AI Search endpoint -- AzureAISearchVectorStore(endpoint=...).') +output searchEndpoint string = search.outputs.searchEndpoint + +@description('Base name for versioned physical indexes -- AzureAISearchVectorStore(index_base_name=...).') +output searchIndexBaseName string = searchIndexBaseName + +@description('Stable alias name the query path reads through -- AzureAISearchVectorStore(alias_name=...).') +output searchAliasName string = searchAliasName + +@description('Azure OpenAI endpoint -- AzureOpenAIEmbedder(endpoint=...) and AzureOpenAIChatClient(endpoint=...).') +output openAiEndpoint string = openAi.outputs.openAiEndpoint + +@description('Embeddings deployment name -- AzureOpenAIEmbedder(deployment=...).') +output openAiEmbeddingDeploymentName string = openAi.outputs.embeddingDeploymentName + +@description('Chat deployment name -- AzureOpenAIChatClient(deployment=...).') +output openAiChatDeploymentName string = openAi.outputs.chatDeploymentName + +@description('Redis host name for the redisHost field of the Dapr state store component (pair with redisSslPort for the "host:port" form).') +output redisHostName string = redis.outputs.redisHostName + +@description('Redis TLS port, required alongside redisHostName for useEntraID / enableTLS.') +output redisSslPort int = redis.outputs.redisSslPort + +@description('Ingestion workload managed identity: client ID (Dapr component YAML azureClientId).') +output ingestionIdentityClientId string = identities.outputs.ingestionClientId + +@description('Ingestion workload managed identity: full resource ID (for workload-identity federation or VM/container host identity association).') +output ingestionIdentityResourceId string = identities.outputs.ingestionIdentityId + +@description('Query workload managed identity: client ID.') +output queryIdentityClientId string = identities.outputs.queryClientId + +@description('Query workload managed identity: full resource ID.') +output queryIdentityResourceId string = identities.outputs.queryIdentityId + +@description('Application Insights connection string for OpenTelemetry export -- see docs/rag/observability.md.') +output appInsightsConnectionString string = monitoring.outputs.connectionString + +@description('Log Analytics workspace resource ID backing Application Insights.') +output logAnalyticsWorkspaceId string = monitoring.outputs.logAnalyticsWorkspaceId + +@description('Key Vault name. Empty string unless deploySecretStore is true.') +output keyVaultName string = keyVault.?outputs.?keyVaultName ?? '' + +@description('Key Vault URI. Empty string unless deploySecretStore is true.') +output keyVaultUri string = keyVault.?outputs.?keyVaultUri ?? '' diff --git a/examples/rag/infra/modules/eventgrid.bicep b/examples/rag/infra/modules/eventgrid.bicep new file mode 100644 index 000000000..2b931a123 --- /dev/null +++ b/examples/rag/infra/modules/eventgrid.bicep @@ -0,0 +1,99 @@ +// Relays Blob Storage change events (BlobCreated / BlobDeleted) to the +// Service Bus topic via an Event Grid system topic + event subscription. +// +// UNVALIDATED, TRICKIEST PART OF THIS TEMPLATE -- see infra/README.md. If +// this module fails to deploy or behaves unexpectedly, the equivalent +// `az eventgrid` CLI steps documented there are the fallback: disable this +// module with `deployEventGridSubscription = false` and wire the +// subscription manually. +// +// Delivery uses the system topic's own system-assigned managed identity +// (deliveryWithResourceIdentity) rather than a connection string, so Event +// Grid itself needs "Azure Service Bus Data Sender" on the destination +// topic -- granted below, scoped to that one topic. + +@description('Azure region. Kept the same as the storage account for simplicity.') +param location string + +@description('Resource ID of the source storage account.') +param storageAccountId string + +@description('Name of the source storage account (used to derive the system topic name).') +param storageAccountName string + +@description('Name of the Service Bus namespace that owns the destination topic.') +param serviceBusNamespaceName string + +@description('Name of the destination Service Bus topic.') +param serviceBusTopicName string + +@description('Resource ID of the destination Service Bus topic.') +param serviceBusTopicId string + +@description('Event subscription name.') +param eventSubscriptionName string = 'blob-to-servicebus' + +@description('Tags applied to the system topic.') +param tags object = {} + +var roleIdServiceBusDataSender = '69a216fc-b8fb-44d8-bc22-1f3c2cd27a39' + +resource systemTopic 'Microsoft.EventGrid/systemTopics@2022-06-15' = { + name: '${storageAccountName}-events' + location: location + tags: tags + identity: { + type: 'SystemAssigned' + } + properties: { + source: storageAccountId + topicType: 'Microsoft.Storage.StorageAccounts' + } +} + +resource eventSubscription 'Microsoft.EventGrid/systemTopics/eventSubscriptions@2022-06-15' = { + parent: systemTopic + name: eventSubscriptionName + properties: { + deliveryWithResourceIdentity: { + identity: { + type: 'SystemAssigned' + } + destination: { + endpointType: 'ServiceBusTopic' + properties: { + resourceId: serviceBusTopicId + } + } + } + filter: { + includedEventTypes: [ + 'Microsoft.Storage.BlobCreated' + 'Microsoft.Storage.BlobDeleted' + ] + } + eventDeliverySchema: 'EventGridSchema' + } +} + +resource existingSbNamespace 'Microsoft.ServiceBus/namespaces@2021-11-01' existing = { + name: serviceBusNamespaceName +} + +resource existingSbTopic 'Microsoft.ServiceBus/namespaces/topics@2021-11-01' existing = { + parent: existingSbNamespace + name: serviceBusTopicName +} + +resource eventGridServiceBusSender 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(existingSbTopic.id, systemTopic.id, roleIdServiceBusDataSender) + scope: existingSbTopic + properties: { + principalId: systemTopic.identity.principalId + principalType: 'ServicePrincipal' + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleIdServiceBusDataSender) + } +} + +output systemTopicName string = systemTopic.name +output eventSubscriptionName string = eventSubscription.name diff --git a/examples/rag/infra/modules/identity.bicep b/examples/rag/infra/modules/identity.bicep new file mode 100644 index 000000000..38b82135b --- /dev/null +++ b/examples/rag/infra/modules/identity.bicep @@ -0,0 +1,38 @@ +// Two user-assigned managed identities: one for the ingestion workload +// (Dapr Workflow worker: reads blobs, consumes Service Bus, writes/activates +// Search indexes, calls the embeddings deployment) and one for the query +// workload (RAG query API: reads Search only, calls the chat deployment). +// Splitting these in two -- rather than one shared identity -- is what makes +// the least-privilege role assignments in modules/rbac.bicep possible; see +// docs/rag/azure-rbac.md for exactly what each may and may not do. + +@description('Azure region for both identities.') +param location string + +@description('Name of the managed identity for the ingestion workload.') +param ingestionIdentityName string + +@description('Name of the managed identity for the query workload.') +param queryIdentityName string + +@description('Tags applied to both identities.') +param tags object = {} + +resource ingestionIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { + name: ingestionIdentityName + location: location + tags: tags +} + +resource queryIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { + name: queryIdentityName + location: location + tags: tags +} + +output ingestionIdentityId string = ingestionIdentity.id +output ingestionPrincipalId string = ingestionIdentity.properties.principalId +output ingestionClientId string = ingestionIdentity.properties.clientId +output queryIdentityId string = queryIdentity.id +output queryPrincipalId string = queryIdentity.properties.principalId +output queryClientId string = queryIdentity.properties.clientId diff --git a/examples/rag/infra/modules/keyvault.bicep b/examples/rag/infra/modules/keyvault.bicep new file mode 100644 index 000000000..2cd7b74ab --- /dev/null +++ b/examples/rag/infra/modules/keyvault.bicep @@ -0,0 +1,42 @@ +// Optional Azure Key Vault, only created when the caller opts into the +// non-default secrets path (deploySecretStore = true). The recommended +// deployment of this sample needs no secrets at all: every component +// authenticates via the ingestion/query managed identities. +// +// Uses Azure RBAC for data-plane authorization (enableRbacAuthorization: +// true), not the legacy vault access-policy model, so access is granted via +// modules/rbac.bicep's "Key Vault Secrets User" role assignments. + +@description('Set to true to create the vault; false deploys nothing from this module.') +param deploy bool = false + +@description('Azure region.') +param location string + +@description('Globally-unique Key Vault name (<=24 chars).') +param keyVaultName string + +@description('Microsoft Entra tenant ID for the vault. Defaults to the tenant of the current deployment -- never hard-code a tenant ID.') +param tenantId string = subscription().tenantId + +@description('Tags applied to the vault.') +param tags object = {} + +resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' = if (deploy) { + name: keyVaultName + location: location + tags: tags + properties: { + sku: { + family: 'A' + name: 'standard' + } + tenantId: tenantId + enableRbacAuthorization: true + enableSoftDelete: true + softDeleteRetentionInDays: 90 + } +} + +output keyVaultName string = keyVault.?name ?? '' +output keyVaultUri string = keyVault.?properties.?vaultUri ?? '' diff --git a/examples/rag/infra/modules/monitoring.bicep b/examples/rag/infra/modules/monitoring.bicep new file mode 100644 index 000000000..e1b55f702 --- /dev/null +++ b/examples/rag/infra/modules/monitoring.bicep @@ -0,0 +1,43 @@ +// Log Analytics workspace + Application Insights, used as the OpenTelemetry +// destination for both workloads. See docs/rag/observability.md for the +// exporter setup that consumes `connectionString`. + +@description('Azure region.') +param location string + +@description('Log Analytics workspace name.') +param logAnalyticsName string + +@description('Application Insights resource name.') +param appInsightsName string + +@description('Tags applied to both resources.') +param tags object = {} + +resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = { + name: logAnalyticsName + location: location + tags: tags + properties: { + sku: { + name: 'PerGB2018' + } + retentionInDays: 30 + } +} + +resource appInsights 'Microsoft.Insights/components@2020-02-02' = { + name: appInsightsName + location: location + tags: tags + kind: 'web' + properties: { + Application_Type: 'web' + WorkspaceResourceId: logAnalytics.id + IngestionMode: 'LogAnalytics' + } +} + +output connectionString string = appInsights.properties.ConnectionString +output instrumentationKey string = appInsights.properties.InstrumentationKey +output logAnalyticsWorkspaceId string = logAnalytics.id diff --git a/examples/rag/infra/modules/openai.bicep b/examples/rag/infra/modules/openai.bicep new file mode 100644 index 000000000..9c83a7bc9 --- /dev/null +++ b/examples/rag/infra/modules/openai.bicep @@ -0,0 +1,105 @@ +// Azure OpenAI (Cognitive Services) account with two model deployments: an +// embeddings deployment (ingestion side) and a chat/completions deployment +// (query side). Both live on one account since Azure OpenAI RBAC roles are +// account-scoped, not deployment-scoped -- see docs/rag/azure-rbac.md for +// the consequence of that (both workload identities end up with the same +// "Cognitive Services OpenAI User" role on this account). + +@description('Azure region. Model/version availability varies by region -- verify before deploying.') +param location string + +@description('Globally-unique Azure OpenAI account name.') +param openAiAccountName string + +@description('Azure OpenAI account SKU.') +param skuName string = 'S0' + +@description('Embeddings model name, e.g. text-embedding-3-small.') +param embeddingModelName string + +@description('Embeddings model version.') +param embeddingModelVersion string + +@description('Embeddings deployment name.') +param embeddingDeploymentName string + +@description('Embeddings deployment capacity (capacity units).') +param embeddingCapacity int + +@description('Chat model name, e.g. gpt-4o.') +param chatModelName string + +@description('Chat model version.') +param chatModelVersion string + +@description('Chat deployment name.') +param chatDeploymentName string + +@description('Chat deployment capacity (capacity units).') +param chatCapacity int + +@description('Deployment SKU shared by both deployments (e.g. GlobalStandard, Standard).') +param deploymentSkuName string = 'GlobalStandard' + +@description('Tags applied to the account.') +param tags object = {} + +resource openAiAccount 'Microsoft.CognitiveServices/accounts@2024-10-01' = { + name: openAiAccountName + location: location + tags: tags + sku: { + name: skuName + } + kind: 'OpenAI' + properties: { + customSubDomainName: openAiAccountName + publicNetworkAccess: 'Enabled' + disableLocalAuth: false + } +} + +resource embeddingDeployment 'Microsoft.CognitiveServices/accounts/deployments@2024-10-01' = { + parent: openAiAccount + name: embeddingDeploymentName + sku: { + name: deploymentSkuName + capacity: embeddingCapacity + } + properties: { + model: { + format: 'OpenAI' + name: embeddingModelName + version: embeddingModelVersion + } + } +} + +resource chatDeployment 'Microsoft.CognitiveServices/accounts/deployments@2024-10-01' = { + parent: openAiAccount + name: chatDeploymentName + sku: { + name: deploymentSkuName + capacity: chatCapacity + } + properties: { + model: { + format: 'OpenAI' + name: chatModelName + version: chatModelVersion + } + } + // Azure OpenAI serializes deployment operations against one account -- + // creating both deployments "in parallel" (Bicep's default behavior for + // sibling resources with no data dependency) can 409. This dependsOn + // forces them to run one after the other. + dependsOn: [ + embeddingDeployment + ] +} + +output openAiAccountId string = openAiAccount.id +output openAiAccountName string = openAiAccount.name +output openAiEndpoint string = openAiAccount.properties.endpoint +output embeddingDeploymentName string = embeddingDeployment.name +output chatDeploymentName string = chatDeployment.name diff --git a/examples/rag/infra/modules/rbac.bicep b/examples/rag/infra/modules/rbac.bicep new file mode 100644 index 000000000..04c1c8582 --- /dev/null +++ b/examples/rag/infra/modules/rbac.bicep @@ -0,0 +1,211 @@ +// Every role and access-policy assignment for the two workload identities, +// centralized here so it can be reviewed as a single unit against +// docs/rag/azure-rbac.md's role table. Nothing in this file grants a +// subscription- or resource-group-scoped role (e.g. Owner/Contributor) -- +// every assignment is scoped to the one resource that needs it. +// +// Built-in role names and IDs below were verified against Microsoft Learn's +// built-in-roles reference and each service's own RBAC documentation on +// 2026-09-10 (see docs/rag/azure-rbac.md for the exact pages and citations). + +@description('Principal (object) ID of the ingestion workload managed identity.') +param ingestionPrincipalId string + +@description('Principal (object) ID of the query workload managed identity.') +param queryPrincipalId string + +@description('Storage account name (source documents).') +param storageAccountName string + +@description('Service Bus namespace name.') +param serviceBusNamespaceName string + +@description('Service Bus topic name.') +param serviceBusTopicName string + +@description('Service Bus subscription name the ingestion workload consumes from.') +param serviceBusSubscriptionName string + +@description('Azure AI Search service name.') +param searchServiceName string + +@description('Azure OpenAI account name.') +param openAiAccountName string + +@description('Azure Cache for Redis name.') +param redisName string + +@description('Whether the optional Key Vault was deployed; gates the Key Vault Secrets User assignments below.') +param deploySecretStore bool = false + +@description('Key Vault name. Required only when deploySecretStore is true.') +param keyVaultName string = '' + +// --- Built-in role definition IDs -- see docs/rag/azure-rbac.md for the verified name/description behind each. --- +var roleIdStorageBlobDataReader = '2a2b9908-6ea1-4ae2-8e65-a410df84e7d1' +var roleIdServiceBusDataReceiver = '4f6d3b9b-027b-4f4c-9142-0e5a2a2247e0' +var roleIdSearchServiceContributor = '7ca78c08-252a-4471-8644-bb5ff32d4ba0' +var roleIdSearchIndexDataContributor = '8ebe5a00-799e-43f5-93ac-243d3dce84a7' +var roleIdSearchIndexDataReader = '1407120a-92aa-4202-b7e9-c0e197c71c8f' +var roleIdCognitiveServicesOpenAiUser = '5e0bd9bd-7b93-4f28-af87-19fc36ad61ae' +var roleIdKeyVaultSecretsUser = '4633458b-17de-408a-b874-0445c86b69e6' + +resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' existing = { + name: storageAccountName +} + +resource sbNamespace 'Microsoft.ServiceBus/namespaces@2021-11-01' existing = { + name: serviceBusNamespaceName +} + +resource sbTopic 'Microsoft.ServiceBus/namespaces/topics@2021-11-01' existing = { + parent: sbNamespace + name: serviceBusTopicName +} + +resource sbSubscription 'Microsoft.ServiceBus/namespaces/topics/subscriptions@2021-11-01' existing = { + parent: sbTopic + name: serviceBusSubscriptionName +} + +resource searchService 'Microsoft.Search/searchServices@2023-11-01' existing = { + name: searchServiceName +} + +resource openAiAccount 'Microsoft.CognitiveServices/accounts@2024-10-01' existing = { + name: openAiAccountName +} + +resource redisCache 'Microsoft.Cache/redis@2024-11-01' existing = { + name: redisName +} + +resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = if (deploySecretStore) { + name: keyVaultName +} + +// ============ Ingestion identity ============ +// Reads source blobs, receives the Service Bus trigger, creates/manages +// Search indexes and the alias, writes Search documents, and calls the +// embeddings deployment. This is also "the Service Bus consumer identity": +// it must not be able to do anything beyond receiving messages (no Send, +// no Manage) on the namespace. + +resource ingestionBlobRead 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(storageAccount.id, ingestionPrincipalId, roleIdStorageBlobDataReader) + scope: storageAccount + properties: { + principalId: ingestionPrincipalId + principalType: 'ServicePrincipal' + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleIdStorageBlobDataReader) + } +} + +resource ingestionServiceBusReceive 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(sbSubscription.id, ingestionPrincipalId, roleIdServiceBusDataReceiver) + scope: sbSubscription + properties: { + principalId: ingestionPrincipalId + principalType: 'ServicePrincipal' + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleIdServiceBusDataReceiver) + } +} + +resource ingestionSearchServiceContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(searchService.id, ingestionPrincipalId, roleIdSearchServiceContributor) + scope: searchService + properties: { + principalId: ingestionPrincipalId + principalType: 'ServicePrincipal' + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleIdSearchServiceContributor) + } +} + +resource ingestionSearchIndexDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(searchService.id, ingestionPrincipalId, roleIdSearchIndexDataContributor) + scope: searchService + properties: { + principalId: ingestionPrincipalId + principalType: 'ServicePrincipal' + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleIdSearchIndexDataContributor) + } +} + +resource ingestionOpenAiUser 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(openAiAccount.id, ingestionPrincipalId, roleIdCognitiveServicesOpenAiUser) + scope: openAiAccount + properties: { + principalId: ingestionPrincipalId + principalType: 'ServicePrincipal' + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleIdCognitiveServicesOpenAiUser) + } +} + +// Redis data-plane access uses its own access-policy-assignment concept +// (not Microsoft.Authorization/roleAssignments). "Data Owner" gives +// ingestion the read-write access Dapr Workflow's actor state store needs. +resource ingestionRedisAccess 'Microsoft.Cache/redis/accessPolicyAssignments@2024-11-01' = { + parent: redisCache + name: 'ingestion-identity' + properties: { + accessPolicyName: 'Data Owner' + objectId: ingestionPrincipalId + objectIdAlias: 'ingestionWorkloadIdentity' + } +} + +resource ingestionKeyVaultSecretsUser 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (deploySecretStore) { + name: guid(keyVaultName, ingestionPrincipalId, roleIdKeyVaultSecretsUser) + scope: keyVault + properties: { + principalId: ingestionPrincipalId + principalType: 'ServicePrincipal' + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleIdKeyVaultSecretsUser) + } +} + +// ============ Query identity ============ +// Strictly narrower than ingestion: read-only Search access, inference-only +// OpenAI access, read-only Redis access to resolve the active version +// pointer. Must NOT be able to create/delete indexes, switch the alias, +// write documents, touch Storage, or touch Service Bus. + +resource querySearchIndexDataReader 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(searchService.id, queryPrincipalId, roleIdSearchIndexDataReader) + scope: searchService + properties: { + principalId: queryPrincipalId + principalType: 'ServicePrincipal' + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleIdSearchIndexDataReader) + } +} + +resource queryOpenAiUser 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(openAiAccount.id, queryPrincipalId, roleIdCognitiveServicesOpenAiUser) + scope: openAiAccount + properties: { + principalId: queryPrincipalId + principalType: 'ServicePrincipal' + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleIdCognitiveServicesOpenAiUser) + } +} + +resource queryRedisAccess 'Microsoft.Cache/redis/accessPolicyAssignments@2024-11-01' = { + parent: redisCache + name: 'query-identity' + properties: { + accessPolicyName: 'Data Reader' + objectId: queryPrincipalId + objectIdAlias: 'queryWorkloadIdentity' + } +} + +resource queryKeyVaultSecretsUser 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (deploySecretStore) { + name: guid(keyVaultName, queryPrincipalId, roleIdKeyVaultSecretsUser) + scope: keyVault + properties: { + principalId: queryPrincipalId + principalType: 'ServicePrincipal' + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleIdKeyVaultSecretsUser) + } +} diff --git a/examples/rag/infra/modules/redis.bicep b/examples/rag/infra/modules/redis.bicep new file mode 100644 index 000000000..110540fa5 --- /dev/null +++ b/examples/rag/infra/modules/redis.bicep @@ -0,0 +1,62 @@ +// Azure Cache for Redis, used as the Dapr actor state store backing both +// Dapr Workflow's own orchestration state and this pipeline's idempotency +// records (see examples/rag/components/azure/workflow-statestore.yaml). +// +// FLAGGED FOR REVIEW: Microsoft has announced a retirement timeline for +// Azure Cache for Redis across all SKUs (Basic/Standard/Premium), directing +// new workloads to "Azure Managed Redis" instead +// (https://learn.microsoft.com/azure/azure-cache-for-redis/cache-azure-active-directory-for-authentication, +// fetched 2026-09-10). This module still targets Microsoft.Cache/redis +// because that is the resource type Dapr's Redis state store component and +// this sample's component YAML are documented against, and because this +// template cannot be deployed or tested in this environment -- re-evaluate +// against Azure Managed Redis (or Cosmos DB, see docs/rag/azure-rbac.md and +// the pipeline's own state-store docs) before committing to this for a +// long-lived deployment. +// +// `redisConfiguration['aad-enabled']` turns on Microsoft Entra ID +// authentication; modules/rbac.bicep grants each workload identity a Redis +// access-policy assignment ("Data Owner" for ingestion, "Data Reader" for +// query) rather than a generic Azure role, since Redis data-plane access +// uses its own access-policy concept, not Microsoft.Authorization +// roleAssignments. + +@description('Azure region.') +param location string + +@description('Globally-unique cache name.') +param redisName string + +@description('Redis SKU name.') +param skuName string = 'Basic' + +@description('Redis SKU family: C for Basic/Standard, P for Premium.') +param skuFamily string = 'C' + +@description('Redis SKU capacity within the family.') +param skuCapacity int = 1 + +@description('Tags applied to the cache.') +param tags object = {} + +resource redisCache 'Microsoft.Cache/redis@2024-11-01' = { + name: redisName + location: location + tags: tags + properties: { + sku: { + name: skuName + family: skuFamily + capacity: skuCapacity + } + minimumTlsVersion: '1.2' + redisConfiguration: { + 'aad-enabled': 'true' + } + } +} + +output redisId string = redisCache.id +output redisName string = redisCache.name +output redisHostName string = redisCache.properties.hostName +output redisSslPort int = redisCache.properties.sslPort diff --git a/examples/rag/infra/modules/search.bicep b/examples/rag/infra/modules/search.bicep new file mode 100644 index 000000000..97ade10fb --- /dev/null +++ b/examples/rag/infra/modules/search.bicep @@ -0,0 +1,50 @@ +// Azure AI Search service. This module only provisions the *service* -- +// DurableRAGPipeline itself creates one physical index per pipeline version +// (via AzureAISearchVectorStore) and manages the stable alias at runtime, so +// no index or alias resource is declared here. + +@description('Azure region.') +param location string + +@description('Globally-unique search service name.') +param searchServiceName string + +@description('Search service SKU.') +param skuName string = 'basic' + +@description('Semantic ranker tier: "standard" on paid tiers, "disabled" on Free (which cannot run the semantic ranker at all).') +param semanticSearchTier string = 'standard' + +@description('Tags applied to the search service.') +param tags object = {} + +resource searchService 'Microsoft.Search/searchServices@2023-11-01' = { + name: searchServiceName + location: location + tags: tags + sku: { + name: skuName + } + properties: { + replicaCount: 1 + partitionCount: 1 + hostingMode: 'default' + semanticSearch: semanticSearchTier + // "Both" mode: keeps key-based auth available (e.g. for the portal's + // Search Explorer while validating this deployment) alongside Microsoft + // Entra ID / RBAC, which is what the ingestion and query identities + // actually use (see modules/rbac.bicep). Tighten to disableLocalAuth: + // true once RBAC access is confirmed working, for a fully keyless + // service -- see infra/README.md. + authOptions: { + aadOrApiKey: { + aadAuthFailureMode: 'http401WithBearerChallenge' + } + } + disableLocalAuth: false + } +} + +output searchServiceId string = searchService.id +output searchServiceName string = searchService.name +output searchEndpoint string = 'https://${searchService.name}.search.windows.net' diff --git a/examples/rag/infra/modules/servicebus.bicep b/examples/rag/infra/modules/servicebus.bicep new file mode 100644 index 000000000..e83df1446 --- /dev/null +++ b/examples/rag/infra/modules/servicebus.bicep @@ -0,0 +1,58 @@ +// Service Bus namespace + topic + subscription for the +// Event Grid -> Service Bus -> Dapr pub/sub trigger path. The topic receives +// Blob Storage change notifications (relayed by the Event Grid system topic +// in modules/eventgrid.bicep); the ingestion workload's Dapr sidecar +// consumes from `subscriptionName` via the servicebus-pubsub component +// (examples/rag/components/azure/servicebus-pubsub.yaml). + +@description('Azure region.') +param location string + +@description('Globally-unique Service Bus namespace name.') +param serviceBusNamespaceName string + +@description('Namespace SKU. Standard or Premium -- Basic does not support topics.') +param skuName string = 'Standard' + +@description('Topic name for Blob Storage change events.') +param topicName string + +@description('Subscription name the ingestion workload consumes from.') +param subscriptionName string + +@description('Tags applied to the namespace.') +param tags object = {} + +resource sbNamespace 'Microsoft.ServiceBus/namespaces@2021-11-01' = { + name: serviceBusNamespaceName + location: location + tags: tags + sku: { + name: skuName + tier: skuName + } +} + +resource sbTopic 'Microsoft.ServiceBus/namespaces/topics@2021-11-01' = { + parent: sbNamespace + name: topicName + properties: { + defaultMessageTimeToLive: 'P14D' + } +} + +resource sbSubscription 'Microsoft.ServiceBus/namespaces/topics/subscriptions@2021-11-01' = { + parent: sbTopic + name: subscriptionName + properties: { + maxDeliveryCount: 10 + lockDuration: 'PT5M' + defaultMessageTimeToLive: 'P14D' + } +} + +output namespaceId string = sbNamespace.id +output namespaceName string = sbNamespace.name +output topicId string = sbTopic.id +output topicName string = sbTopic.name +output subscriptionName string = sbSubscription.name diff --git a/examples/rag/infra/modules/storage.bicep b/examples/rag/infra/modules/storage.bicep new file mode 100644 index 000000000..401681c62 --- /dev/null +++ b/examples/rag/infra/modules/storage.bicep @@ -0,0 +1,49 @@ +// Storage account + blob container holding the source documents this +// pipeline ingests. Public blob access is disabled -- the ingestion identity +// reads through Microsoft Entra ID (Storage Blob Data Reader; see +// docs/rag/azure-rbac.md), never a connection string or SAS token. + +@description('Azure region.') +param location string + +@description('Globally-unique storage account name (lowercase alphanumeric, <=24 chars).') +param storageAccountName string + +@description('Blob container name for source documents.') +param blobContainerName string + +@description('Tags applied to the storage account.') +param tags object = {} + +resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = { + name: storageAccountName + location: location + tags: tags + kind: 'StorageV2' + sku: { + name: 'Standard_LRS' + } + properties: { + minimumTlsVersion: 'TLS1_2' + allowBlobPublicAccess: false + supportsHttpsTrafficOnly: true + } +} + +resource blobService 'Microsoft.Storage/storageAccounts/blobServices@2023-01-01' = { + parent: storageAccount + name: 'default' +} + +resource sourceContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-01-01' = { + parent: blobService + name: blobContainerName + properties: { + publicAccess: 'None' + } +} + +output storageAccountId string = storageAccount.id +output storageAccountName string = storageAccount.name +output blobEndpoint string = storageAccount.properties.primaryEndpoints.blob +output containerName string = sourceContainer.name diff --git a/examples/rag/pubsub_trigger.py b/examples/rag/pubsub_trigger.py new file mode 100644 index 000000000..58c9f125f --- /dev/null +++ b/examples/rag/pubsub_trigger.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 The Dapr Authors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""One example of entering ingestion through Dapr pub/sub: S3 Event Notifications. + +Wires S3 -> (SNS/SQS or EventBridge) -> a Dapr pub/sub component -> this +subscriber, which normalizes the event (triggers.py) and reconciles the +affected prefix. See `reconciliation_workflow.py` for why this debounces +rather than starting a new ingestion run per event, and +`pubsub_trigger_servicebus.py` for the Azure Blob equivalent. + +This is deliberately the *one* transport shown for S3, per the MVP's scope +("one clear example per provider is sufficient") -- adapt the pub/sub +component (SNS->SQS, or EventBridge->SQS) to your own AWS setup; only the +Dapr-side subscriber and payload shape need to match. + + dapr run --app-id rag-s3-trigger --resources-path components/ --app-port 6001 -- python3 pubsub_trigger.py +""" + +from __future__ import annotations + +import logging + +from config import build_pipeline_id, build_state_store_name +from reconciliation_workflow import ReconciliationTrigger + +from dapr.ext.grpc import App, SubscriptionMessage, TopicEventResponse +from dapr.ext.rag.triggers import ( + EventDeduplicator, + parse_s3_event_notifications, + to_source_change_event, +) + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger('rag-s3-trigger') + +app = App() +deduplicator = EventDeduplicator(state_store_name=build_state_store_name()) +reconciliation = ReconciliationTrigger(pipeline_id=build_pipeline_id()) + + +@app.subscribe(pubsub_name='rag-events-pubsub', topic='s3-object-events') +def on_s3_event(message: SubscriptionMessage) -> TopicEventResponse: + payload = message.data() + notifications = parse_s3_event_notifications(payload) + if not notifications: + logger.warning('Received a message with no recognizable S3 event Records; ignoring.') + return TopicEventResponse('success') + + for notification in notifications: + event = to_source_change_event(notification) + if deduplicator.already_seen(event.event_id): + logger.info('Duplicate delivery of event_id=%s; skipping.', event.event_id) + continue + deduplicator.mark_seen(event.event_id) + logger.info( + 'source_document_id=%s event_type=%s -- scheduling a debounced reconciliation', + event.source_document_id, + event.event_type, + ) + reconciliation.notify_change(event) + + return TopicEventResponse('success') + + +if __name__ == '__main__': + app.run(6001) diff --git a/examples/rag/pubsub_trigger_servicebus.py b/examples/rag/pubsub_trigger_servicebus.py new file mode 100644 index 000000000..d2770102d --- /dev/null +++ b/examples/rag/pubsub_trigger_servicebus.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 The Dapr Authors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""The Azure-native event path: Blob change -> Event Grid -> Service Bus -> Dapr pub/sub -> here. + + Azure Blob Storage (BlobCreated/BlobDeleted) + -> Event Grid system topic + -> Event Grid subscription targeting a Service Bus topic + -> Dapr Azure Service Bus Topics pub/sub component (examples/rag/components/azure/servicebus-pubsub.yaml) + -> this subscriber + +See `examples/rag/infra/main.bicep` for provisioning the Event Grid system +topic + subscription and the Service Bus namespace/topic, and +`docs/rag/azure-rbac.md` for the identity this process needs (Service Bus +data-receive only -- see that doc for why it must not have any Search/OpenAI/ +Storage-write permissions). + +Event Grid delivers in either its own schema or CloudEvents schema; both are +handled by `triggers.parse_azure_blob_event`. Duplicate and out-of-order +delivery are handled the same way as the S3 path (see pubsub_trigger.py) -- +by `EventDeduplicator` plus debounced reconciliation, since delivery order +and exactly-once are never guaranteed. + + dapr run --app-id rag-azure-trigger --resources-path components/ --app-port 6002 -- python3 pubsub_trigger_servicebus.py +""" + +from __future__ import annotations + +import logging + +from config import build_pipeline_id, build_state_store_name +from reconciliation_workflow import ReconciliationTrigger + +from dapr.ext.grpc import App, SubscriptionMessage, TopicEventResponse +from dapr.ext.rag.triggers import EventDeduplicator, parse_azure_blob_event, to_source_change_event + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger('rag-azure-trigger') + +app = App() +deduplicator = EventDeduplicator(state_store_name=build_state_store_name()) +reconciliation = ReconciliationTrigger(pipeline_id=build_pipeline_id()) + + +@app.subscribe( + pubsub_name='rag-events-pubsub', topic='blob-events' +) # matches infra/main.bicep's serviceBusTopicName +def on_blob_event(message: SubscriptionMessage) -> TopicEventResponse: + payload = message.data() + notification = parse_azure_blob_event(payload) + if notification is None: + logger.warning('Received a message that is not a recognizable blob event; ignoring.') + return TopicEventResponse('success') + + event = to_source_change_event(notification) + if deduplicator.already_seen(event.event_id): + logger.info('Duplicate delivery of event_id=%s; skipping.', event.event_id) + return TopicEventResponse('success') + + deduplicator.mark_seen(event.event_id) + logger.info( + 'source_document_id=%s event_type=%s -- scheduling a debounced reconciliation', + event.source_document_id, + event.event_type, + ) + reconciliation.notify_change(event) + return TopicEventResponse('success') + + +if __name__ == '__main__': + app.run(6002) diff --git a/examples/rag/query_api.py b/examples/rag/query_api.py new file mode 100644 index 000000000..910201de0 --- /dev/null +++ b/examples/rag/query_api.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 The Dapr Authors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""The Azure-native flagship query path: hybrid retrieval + a grounded, cited answer. + + Azure AI Search hybrid (vector + keyword) retrieval, through the stable alias + -> Azure OpenAI chat completion, grounded in the retrieved chunks + -> an answer with citations back to the source blobs, or a plain + "not enough evidence" result when retrieval didn't surface enough. + +Requires RAG_VECTOR_STORE=azure-ai-search and RAG_EMBEDDER=azure-openai (see +.env.example), plus AZURE_OPENAI_CHAT_DEPLOYMENT for the answer-generation +deployment (which may differ from the embeddings deployment). + + dapr run --app-id rag-query --resources-path components/ -- python3 query_api.py "What is the remote work policy?" +""" + +from __future__ import annotations + +import argparse +import json +import os + +from config import build_retrieval_resolver, optional_env, require_env + +from dapr.ext.rag import AzureOpenAIChatClient + + +def build_chat_client() -> AzureOpenAIChatClient: + return AzureOpenAIChatClient( + endpoint=require_env('AZURE_OPENAI_ENDPOINT'), + deployment=require_env('AZURE_OPENAI_CHAT_DEPLOYMENT'), + api_key=optional_env('AZURE_OPENAI_API_KEY'), + ) + + +def answer(question: str, *, top_k: int = 5) -> dict: + resolver = build_retrieval_resolver() + try: + index_version = resolver.resolve_active_version() + if index_version is None: + return { + 'answer': 'No index has been activated yet for this pipeline.', + 'citations': [], + 'index_version': None, + 'workflow_instance_id': None, + } + matches = resolver.query(question, top_k=top_k) + finally: + resolver.close() + + chat = build_chat_client() + result = chat.generate_answer(question, matches, index_version=index_version) + return result.to_dict() + + +def main() -> None: + parser = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter + ) + parser.add_argument('question') + parser.add_argument('--top-k', type=int, default=int(os.environ.get('RAG_QUERY_TOP_K', '5'))) + args = parser.parse_args() + + result = answer(args.question, top_k=args.top_k) + print(json.dumps(result, indent=2, default=str)) + + +if __name__ == '__main__': + main() diff --git a/examples/rag/reconciliation_workflow.py b/examples/rag/reconciliation_workflow.py new file mode 100644 index 000000000..cc6ebdb70 --- /dev/null +++ b/examples/rag/reconciliation_workflow.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 The Dapr Authors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Debounces a burst of change events into one reconciliation run per quiet window. + +Event delivery alone never proves a container's final contents (events can be +dropped, duplicated, or arrive out of order), and starting a full ingestion +run per individual blob event would be wasteful and could overlap. This +module implements the second MVP option the design calls for: a short +debounce window followed by a prefix-level reconciliation -- rather than a +per-document incremental workflow. `DurableRAGPipeline.start()`'s own +discovery step (against the real source, not the event stream) remains the +source of truth for what actually needs (re)indexing; this module's only job +is deciding *when* to call it. + +`ReconciliationTrigger` below is deliberately **not** a Dapr Workflow: it +debounces with a plain `threading.Timer` inside one subscriber process, which +is correct for a single-instance subscriber (this example's deployment +shape) but does not coordinate across multiple concurrent subscriber +replicas -- each replica would debounce independently, and a production +deployment that scales the subscriber out would want the equivalent +`ctx.create_timer(...)` / `ctx.wait_for_external_event(...)` pattern inside an +actual Dapr Workflow instead (one durable "quiet window" orchestration per +prefix, restarting its timer on every new external event, matching +`examples/workflow/human_approval.py`'s wait-with-timeout shape) so exactly +one reconciliation fires regardless of replica count. Noted here rather than +implemented, to keep this example's scope matched to its single-process +demo. +""" + +from __future__ import annotations + +import logging +import threading +from datetime import datetime, timezone +from typing import Callable, Optional + +from config import build_pipeline + +from dapr.ext.rag.models import SourceChangeEvent + +logger = logging.getLogger('rag-reconciliation') + + +def _default_version_for_now() -> str: + """Buckets changes into a daily version -- replace with your own policy.""" + return datetime.now(timezone.utc).strftime('%Y-%m-%d') + + +class ReconciliationTrigger: + """Coalesces `notify_change` calls into a single, debounced ingestion run.""" + + def __init__( + self, + *, + pipeline_id: str, + debounce_seconds: float = 30.0, + version_fn: Callable[[], str] = _default_version_for_now, + ) -> None: + self._pipeline_id = pipeline_id + self._debounce_seconds = debounce_seconds + self._version_fn = version_fn + self._lock = threading.Lock() + self._pending_timer: Optional[threading.Timer] = None + self._pending_prefix: Optional[str] = None + + def notify_change(self, event: SourceChangeEvent) -> None: + """Records a change and (re)starts the debounce window. + + Any change arriving before the window elapses cancels and restarts + the timer, so a burst of events collapses into one reconciliation + shortly after the burst goes quiet. + """ + prefix = _prefix_of(event.source_document_id) + with self._lock: + if self._pending_timer is not None: + self._pending_timer.cancel() + self._pending_prefix = prefix if prefix == self._pending_prefix else None + timer = threading.Timer(self._debounce_seconds, self._reconcile) + timer.daemon = True + self._pending_timer = timer + timer.start() + + def _reconcile(self) -> None: + version = self._version_fn() + with self._lock: + prefix = self._pending_prefix + self._pending_timer = None + logger.info( + 'Debounce window elapsed for pipeline_id=%s; starting reconciliation version=%s prefix=%s', + self._pipeline_id, + version, + prefix, + ) + pipeline = build_pipeline() + try: + instance_id = pipeline.start(version=version, prefix=prefix) + logger.info('Reconciliation ingestion instance_id=%s', instance_id) + finally: + pipeline.close() + + +def _prefix_of(source_document_id: str) -> Optional[str]: + """A conservative common-prefix guess, for status/logging only -- discovery + always re-lists the real source, so an imprecise prefix here never causes + incorrect indexing, only a possibly-wider-than-necessary re-scan.""" + _scheme, _, rest = source_document_id.partition('://') + _bucket, _, key = rest.partition('/') + return key.rsplit('/', 1)[0] + '/' if '/' in key else None diff --git a/examples/rag/requirements.txt b/examples/rag/requirements.txt new file mode 100644 index 000000000..e4326c81e --- /dev/null +++ b/examples/rag/requirements.txt @@ -0,0 +1,5 @@ +# Install every optional adapter this example's scripts can select between at +# runtime via environment variables (see .env.example). A deployment that +# only ever uses one source/vector-store combination needs a smaller subset -- +# see dapr/ext/rag/README.md for exactly which extra each adapter needs. +dapr[rag,rag-s3,rag-azure,rag-azure-search,rag-pgvector,rag-pinecone,rag-unstructured,rag-langchain] >= 1.19.0.dev diff --git a/examples/rag/worker.py b/examples/rag/worker.py new file mode 100644 index 000000000..8091d110e --- /dev/null +++ b/examples/rag/worker.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 The Dapr Authors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Runs the DurableRAGPipeline's Dapr Workflow worker. + +This process hosts the orchestrator and activities -- it's the thing that +must keep running (or be restarted) for ingestion to make progress. `cli.py` +is a separate, short-lived process that only *schedules*/*queries* runs; it +never needs this worker running in the same process. + + dapr run --app-id rag-worker --resources-path components/ -- python3 worker.py + +See README.md for full setup (env vars, component YAMLs) and +failure_demo.py for how to kill and restart this process mid-run. +""" + +from __future__ import annotations + +import logging +import signal +import threading +from types import FrameType +from typing import Optional + +from config import build_pipeline + +logging.basicConfig(level=logging.INFO, format='%(asctime)s %(name)s %(levelname)s: %(message)s') +logger = logging.getLogger('rag-worker') + + +def main() -> None: + pipeline = build_pipeline() + logger.info('Starting RAG ingestion worker (Ctrl+C to stop)...') + pipeline.run_worker() + logger.info('Worker ready.') + + stop_event = threading.Event() + + def _handle_shutdown_signal(signum: int, frame: Optional[FrameType]) -> None: + logger.info('Received signal %s; shutting down...', signum) + stop_event.set() + + signal.signal(signal.SIGINT, _handle_shutdown_signal) + try: + signal.signal(signal.SIGTERM, _handle_shutdown_signal) + except (ValueError, AttributeError): + pass # SIGTERM isn't meaningfully available on every platform (e.g. Windows) + + stop_event.wait() + pipeline.shutdown_worker() + pipeline.close() + logger.info('Worker stopped.') + + +if __name__ == '__main__': + main() diff --git a/pyproject.toml b/pyproject.toml index 68264315d..878d09b82 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,73 @@ strands = [ # exists so users get a consistent `pip install dapr[]` shape and so the # extra is reserved if workflow ever grows external deps. workflow = [] -all = ["dapr[fastapi,flask,grpc,langgraph,strands,workflow]"] +# rag bundles only the embedder (the one adapter every rag pipeline needs); +# every cloud source, vector store, and parser is its own extra below so +# installing `dapr[rag]` never pulls in boto3/azure/pinecone/psycopg/ +# unstructured a given user doesn't need. `pip install "dapr[rag,rag-s3, +# rag-pgvector]"` mirrors the S3 + pgvector example in dapr/ext/rag/AGENTS.md. +rag = [ + "openai>=1.50.0,<2.0.0", +] +rag-langchain = [ + "langchain-core>=0.3.0,<2.0.0", +] +rag-unstructured = [ + "unstructured[md,pdf,docx]>=0.15.0,<1.0.0", +] +rag-s3 = [ + "boto3>=1.34.0,<2.0.0", +] +rag-azure = [ + "azure-storage-blob>=12.19.0,<13.0.0", + "azure-identity>=1.15.0,<2.0.0", +] +rag-pgvector = [ + "psycopg[binary]>=3.1.0,<4.0.0", +] +rag-pinecone = [ + "pinecone>=5.0.0,<8.0.0", +] +# Separate from rag-azure (azure-storage-blob/-identity): a different SDK +# package (azure-search-documents) for a different Azure service, needed only +# by AzureAISearchVectorStore. AzureOpenAIEmbedder/AzureOpenAIChatClient reuse +# `openai` (already in `rag`) and `azure-identity` (already in `rag-azure`), +# so they need no extra of their own. +# httpx is needed here specifically: Azure AI Search index aliases were +# briefly in the SDK as a beta feature (11.4.0b1) but removed before the +# stable 11.4.0 release and have not been restored in any version since +# (verified against the SDK's own CHANGELOG.md) -- alias management is +# implemented as direct REST calls (see vector_stores/azure_ai_search.py) +# since the typed client has no equivalent method. +rag-azure-search = [ + "azure-search-documents>=11.5.0,<12.0.0", + "httpx>=0.27.0,<1.0.0", +] +all = [ + "dapr[fastapi,flask,grpc,langgraph,strands,workflow]", + "dapr[rag,rag-langchain,rag-s3,rag-azure,rag-azure-search,rag-pgvector,rag-pinecone]", + # rag-unstructured pulls in unstructured -> python-magic, which needs a + # real libmagic for file-type sniffing. Windows has no system libmagic, + # and python-magic's own compat shim (magic/compat.py) crashes the whole + # process with a native access violation -- not a catchable ImportError + # -- instead of failing cleanly when it can't find one. The standard + # Windows workaround, python-magic-bin, isn't a fix here: it installs its + # own `magic/__init__.py` at the same path as python-magic's, predates + # (and lacks) compat.py entirely, and hasn't been released since 2017 -- + # pairing it with python-magic risks an install-order-dependent file + # collision rather than a working combination. So `all` (this SDK's own + # dev/test bundle) simply excludes rag-unstructured on Windows; + # dapr/ext/rag/parsing/unstructured.py already handles unstructured + # being absent via UnstructuredParser raising OptionalDependencyError + # (see its own ImportError guard). Windows users who've separately sorted + # out libmagic (e.g. via conda; see unstructured's own install docs) can + # still `pip install dapr[rag-unstructured]` directly -- this exclusion + # is scoped to `all` only. (Inlined rather than `dapr[rag-unstructured]; + # sys_platform != 'win32'`: uv silently drops a marker attached to a + # self-referential extra like that -- verified against this exact case, + # not assumed -- so it must go on the concrete requirement instead.) + "unstructured[md,pdf,docx]>=0.15.0,<1.0.0; sys_platform != 'win32'", +] [project.urls] Documentation = "https://github.com/dapr/docs" @@ -90,6 +156,17 @@ exclude = [ [tool.uv.workspace] members = [] +[tool.uv] +constraint-dependencies = [ + # onnxruntime (pulled in transitively via rag-unstructured's unstructured[pdf] + # -> unstructured-inference, for PDF parsing) dropped cp310 wheels starting at + # 1.24.1 -- verified against PyPI's actual file listing, not just metadata. + # Pin to the last cp310-compatible release so `uv lock`/`uv sync` doesn't pick + # a version with no installable wheel for this project's Python 3.10 floor. + # python_version >= 3.11 is unaffected and resolves onnxruntime normally. + "onnxruntime<1.24; python_version < '3.11'", +] + [dependency-groups] dev = [ # Pull in every extension's third-party deps so the full test suite runs @@ -189,6 +266,47 @@ ignore_missing_imports = true module = ["langgraph.*", "langchain.*", "strands.*", "strands_agents.*"] ignore_missing_imports = true +# dapr.ext.rag's optional adapters: some of these (boto3, langchain_core) are +# installed in CI/dev without inline type stubs or only via a transitive +# extra; others (azure-*, psycopg, pinecone, unstructured) may not be +# installed at all, since every dapr.ext.rag adapter guards its own optional +# import (see dapr/ext/rag/AGENTS.md) rather than requiring the full `rag-*` +# extra set. Unlike the fastapi/langgraph/strands/workflow modules below, +# dapr.ext.rag is new code and is NOT in the ignore_errors bucket -- it is +# expected to pass mypy cleanly. +[[tool.mypy.overrides]] +module = [ + "boto3", + "boto3.*", + "botocore.*", + "azure.*", + "psycopg.*", +] +ignore_missing_imports = true + +# unstructured (pdf/docx extras), openai, pinecone, and langchain-core all +# transitively pull in numpy -- openai, pinecone, and langchain-core each +# reference numpy array/vector types directly from their own inline stubs, +# and unstructured's pdf/docx extras drag in a large ML stack (pandas, spacy, +# transformers, onnx, ...) that does the same. dapr.ext.rag never imports +# numpy itself. Recent numpy versions bundle stubs/source using PEP 695 +# generic syntax that mypy cannot parse under this project's python_version = +# "3.10" target, and unlike a normal type error, a syntax error aborts the +# *entire* mypy run, not just dapr.ext.rag. follow_imports = "skip" keeps +# mypy from following past the one place dapr.ext.rag actually imports each +# of these (parsing/unstructured.py, embedding/_openai_common.py, +# generation.py, vector_stores/pinecone.py, parsing/langchain.py) into that +# graph, so numpy's own files are never opened; ignore_missing_imports covers +# environments where a given package isn't installed at all (each is an +# optional dependency here). Verified (see the completion report) that a +# plain `import numpy` reached this way still crashes mypy even when numpy +# itself is separately skipped or stub-shadowed -- skipping has to happen at +# the direct import site, not at the transitively-reached destination. +[[tool.mypy.overrides]] +module = ["unstructured.*", "openai.*", "pinecone.*", "langchain_core.*"] +follow_imports = "skip" +ignore_missing_imports = true + # Bundling the extensions into the core wheel brings their source under mypy's # namespace walk for the first time. These modules carry pre-existing type # errors that were previously hidden by the separate-workspace-package layout. diff --git a/tests/ext/rag/__init__.py b/tests/ext/rag/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/ext/rag/_crash_resume_worker.py b/tests/ext/rag/_crash_resume_worker.py new file mode 100644 index 000000000..f7c64f5f5 --- /dev/null +++ b/tests/ext/rag/_crash_resume_worker.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +# A standalone worker *process* for test_pipeline_integration.py's real crash/resume test. +# Launched via `python -m tests.ext.rag._crash_resume_worker` (not imported) so that +# FailureInjector's real os._exit() kills only this process, never the pytest process driving +# the test -- proving an actual separate-process crash-and-restart, not a simulated one. No +# test_ prefix (not collected by pytest as a test module itself). +# +# Configuration comes entirely from environment variables, mirroring examples/rag/worker.py's +# own env-var-driven pattern (this is that same idea, scoped to exactly what the test needs). + +from __future__ import annotations + +import os +import signal +import sys +import threading + +from dapr.clients import DaprClient +from dapr.conf import settings +from dapr.ext.rag.models import PipelineConfig +from dapr.ext.rag.pipeline import DurableRAGPipeline +from dapr.ext.rag.sources.s3 import S3Source +from dapr.ext.rag.splitting import TextSplitter +from dapr.ext.rag.testing import FailureInjector +from dapr.ext.rag.vector_stores.pgvector import PgVectorStore +from dapr.ext.workflow import DaprWorkflowClient, WorkflowRuntime +from tests.ext.rag._rag_integration_fixtures import DeterministicEmbedder, PlainTextParser + +HOST = '127.0.0.1' + + +def _env(name: str) -> str: + value = os.environ.get(name) + if not value: + raise SystemExit(f'{name} must be set (see test_pipeline_integration.py)') + return value + + +def main() -> None: + grpc_port = _env('RAG_CRASH_TEST_GRPC_PORT') + fail_after_documents = os.environ.get('RAG_CRASH_TEST_FAIL_AFTER_DOCUMENTS') + collection = _env('RAG_CRASH_TEST_COLLECTION') + + # DaprClient's constructor blocks on an HTTP health check against + # settings.DAPR_HTTP_PORT, which defaults to 3500 and is process-local -- this process + # doesn't inherit the parent test process's own mutation of it (see + # tests/integration/conftest.py's DaprTestEnvironment.start_sidecar for the same fix + # applied there), so it must be set here too, before constructing DaprClient below. + settings.DAPR_HTTP_PORT = int(_env('RAG_CRASH_TEST_HTTP_PORT')) + + pipeline = DurableRAGPipeline( + source=S3Source( + bucket=_env('RAG_CRASH_TEST_BUCKET'), + region_name='us-east-1', + endpoint_url=_env('RAG_CRASH_TEST_S3_ENDPOINT'), + aws_access_key_id='test', + aws_secret_access_key='test', + ), + parser=PlainTextParser(), + splitter=TextSplitter(chunk_size=200, chunk_overlap=20), + embedder=DeterministicEmbedder(), + vector_store=PgVectorStore( + connection_string=_env('RAG_CRASH_TEST_PG_DSN'), collection=collection + ), + state_store_name='statestore', + pipeline_id=collection, + # One document per batch: makes crash timing deterministic (fail_after_documents=N + # crashes only once N documents have each individually completed their own + # continue_as_new generation, never mid-way through a bigger concurrent batch). + config=PipelineConfig(max_concurrent_documents=1), + workflow_runtime=WorkflowRuntime(host=HOST, port=grpc_port), + workflow_client=DaprWorkflowClient(host=HOST, port=grpc_port), + dapr_client=DaprClient(address=f'{HOST}:{grpc_port}'), + failure_injector=( + FailureInjector(fail_after_documents=int(fail_after_documents)) + if fail_after_documents + else None + ), + ) + pipeline.run_worker() + print('CRASH_TEST_WORKER_READY', flush=True) + + stop = threading.Event() + signal.signal(signal.SIGTERM, lambda *_args: stop.set()) + stop.wait() + pipeline.shutdown_worker() + pipeline.close() + + +if __name__ == '__main__': + sys.exit(main() or 0) diff --git a/tests/ext/rag/_rag_integration_fixtures.py b/tests/ext/rag/_rag_integration_fixtures.py new file mode 100644 index 000000000..15a2e5ea5 --- /dev/null +++ b/tests/ext/rag/_rag_integration_fixtures.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +# Shared between test_pipeline_integration.py (in-process) and _crash_resume_worker.py (a +# separate subprocess) -- both need the *exact same* Embedder/DocumentParser classes so a +# document embedded by one process and re-checked by the other agree on content hashes and +# embeddings. No test_ prefix (not collected by pytest itself); no leading underscore would +# make this look like a test module too, hence the _ prefix instead. + +from __future__ import annotations + +import hashlib +from typing import Sequence + +from dapr.ext.rag.embedding.base import Embedder +from dapr.ext.rag.errors import UnsupportedDocumentError +from dapr.ext.rag.models import Document, EmbeddingBatchResult, SourceDocument +from dapr.ext.rag.parsing.base import DocumentParser + + +class DeterministicEmbedder(Embedder): + """A hash-based, fully offline stand-in for a real embedding provider. + + Deterministic (same text -> same vector always, including across separate process runs) so + a re-run of unchanged content is a genuine test of the pipeline's own idempotency, not an + artifact of embedding randomness. Uses `hashlib.sha256`, not the builtin `hash()`, which is + salted per-process (see `fingerprints.py` for the same rule applied to production code). + """ + + _DIMENSIONS = 8 + + @property + def embedding_model(self) -> str: + return 'deterministic-test-embedder-v1' + + def embed_batch(self, texts: Sequence[str]) -> EmbeddingBatchResult: + embeddings = [self._embed_one(text) for text in texts] + return EmbeddingBatchResult(embeddings=embeddings, total_tokens=sum(len(t) for t in texts)) + + def _embed_one(self, text: str) -> list[float]: + digest = hashlib.sha256(text.encode('utf-8')).digest() + return [byte / 255.0 for byte in digest[: self._DIMENSIONS]] + + +class PlainTextParser(DocumentParser): + """Decodes bytes as UTF-8 text, one `Document` per file -- no `unstructured` dependency. + + Real format detection (PDF/DOCX/HTML/...) is already covered by + `test_parsing_unstructured.py` against a mocked `partition()`; these integration tests' + job is proving the *workflow* is durable, not proving parsing correctness a second time. + """ + + @property + def parser_type(self) -> str: + return 'plain-text-test-parser' + + def parse(self, content: bytes, document: SourceDocument) -> list[Document]: + if not document.name.endswith('.txt'): + raise UnsupportedDocumentError( + f'Only .txt is supported by this test parser: {document.name}' + ) + return [Document(page_content=content.decode('utf-8'), metadata={})] diff --git a/tests/ext/rag/integration_resources/statestore.yaml b/tests/ext/rag/integration_resources/statestore.yaml new file mode 100644 index 000000000..2f676bff8 --- /dev/null +++ b/tests/ext/rag/integration_resources/statestore.yaml @@ -0,0 +1,14 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: statestore +spec: + type: state.redis + version: v1 + metadata: + - name: redisHost + value: localhost:6379 + - name: redisPassword + value: "" + - name: actorStateStore + value: "true" diff --git a/tests/ext/rag/test_embedding_azure_openai.py b/tests/ext/rag/test_embedding_azure_openai.py new file mode 100644 index 000000000..8ac60df7a --- /dev/null +++ b/tests/ext/rag/test_embedding_azure_openai.py @@ -0,0 +1,153 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import unittest +from types import SimpleNamespace +from unittest import mock + +from dapr.ext.rag.embedding.azure_openai import AzureOpenAIEmbedder +from dapr.ext.rag.errors import OptionalDependencyError, TransientEmbeddingError + + +class _FakeEmbeddings: + def __init__(self, response=None, exception=None): + self._response = response + self._exception = exception + self.calls = [] + + def create(self, **kwargs): + self.calls.append(kwargs) + if self._exception is not None: + raise self._exception + return self._response + + +class _FakeAzureOpenAIClient: + def __init__(self, response=None, exception=None): + self.embeddings = _FakeEmbeddings(response=response, exception=exception) + + +def _response(items): + data = [SimpleNamespace(index=index, embedding=embedding) for index, embedding in items] + return SimpleNamespace(data=data, usage=SimpleNamespace(total_tokens=len(items))) + + +def _fake_error(name, status_code=None, retry_after=None): + error_cls = type(name, (Exception,), {}) + error = error_cls('boom') + if status_code is not None: + error.status_code = status_code + if retry_after is not None: + error.response = SimpleNamespace(headers={'retry-after': str(retry_after)}) + return error + + +class AzureOpenAIEmbedderConstructionTest(unittest.TestCase): + def test_raises_optional_dependency_error_without_openai_or_client(self): + with mock.patch('dapr.ext.rag.embedding._openai_common.openai', None): + with self.assertRaises(OptionalDependencyError) as ctx: + AzureOpenAIEmbedder( + endpoint='https://x.openai.azure.com', deployment='text-embedding-3-small' + ) + self.assertEqual(ctx.exception.package, 'openai') + + def test_raises_optional_dependency_error_for_missing_azure_identity(self): + fake_openai_module = mock.Mock() + with mock.patch('dapr.ext.rag.embedding._openai_common.openai', fake_openai_module): + with mock.patch('dapr.ext.rag.embedding._openai_common.DefaultAzureCredential', None): + with self.assertRaises(OptionalDependencyError) as ctx: + AzureOpenAIEmbedder( + endpoint='https://x.openai.azure.com', deployment='text-embedding-3-small' + ) + self.assertEqual(ctx.exception.package, 'azure-identity') + + def test_client_injection_bypasses_dependency_checks(self): + with mock.patch('dapr.ext.rag.embedding._openai_common.openai', None): + embedder = AzureOpenAIEmbedder( + endpoint='https://x.openai.azure.com', + deployment='embed-deploy', + client=_FakeAzureOpenAIClient(response=_response([])), + ) + self.assertEqual(embedder.deployment, 'embed-deploy') + + def test_model_defaults_to_deployment_name(self): + embedder = AzureOpenAIEmbedder( + endpoint='https://x.openai.azure.com', + deployment='text-embedding-3-small', + client=_FakeAzureOpenAIClient(), + ) + self.assertEqual(embedder.embedding_model, 'text-embedding-3-small') + + def test_model_can_be_set_separately_from_deployment(self): + embedder = AzureOpenAIEmbedder( + endpoint='https://x.openai.azure.com', + deployment='my-custom-deployment', + model='text-embedding-3-small', + client=_FakeAzureOpenAIClient(), + ) + self.assertEqual(embedder.deployment, 'my-custom-deployment') + self.assertEqual(embedder.embedding_model, 'text-embedding-3-small') + + def test_config_never_includes_the_api_key(self): + embedder = AzureOpenAIEmbedder( + endpoint='https://x.openai.azure.com', + deployment='d', + api_key='super-secret-key', + client=_FakeAzureOpenAIClient(), + ) + self.assertNotIn('super-secret-key', str(embedder.config())) + + +class AzureOpenAIEmbedderEmbedBatchTest(unittest.TestCase): + def test_addresses_the_request_by_deployment_name(self): + client = _FakeAzureOpenAIClient(response=_response([(0, [0.1])])) + embedder = AzureOpenAIEmbedder( + endpoint='https://x.openai.azure.com', deployment='my-deployment', client=client + ) + embedder.embed_batch(['hello']) + self.assertEqual(client.embeddings.calls[0]['model'], 'my-deployment') + + def test_returns_embeddings_in_order(self): + client = _FakeAzureOpenAIClient(response=_response([(1, [0.2]), (0, [0.1])])) + embedder = AzureOpenAIEmbedder( + endpoint='https://x.openai.azure.com', deployment='d', client=client + ) + result = embedder.embed_batch(['a', 'b']) + self.assertEqual(result.embeddings, [[0.1], [0.2]]) + + +class AzureOpenAIEmbedderErrorClassificationTest(unittest.TestCase): + def test_408_is_treated_as_transient(self): + client = _FakeAzureOpenAIClient( + exception=_fake_error('SomeTimeoutStatusError', status_code=408) + ) + embedder = AzureOpenAIEmbedder( + endpoint='https://x.openai.azure.com', deployment='d', client=client + ) + with self.assertRaises(TransientEmbeddingError): + embedder.embed_batch(['text']) + + def test_retry_after_header_is_surfaced_on_the_exception(self): + client = _FakeAzureOpenAIClient(exception=_fake_error('RateLimitError', retry_after=2.5)) + embedder = AzureOpenAIEmbedder( + endpoint='https://x.openai.azure.com', deployment='d', client=client + ) + with self.assertRaises(TransientEmbeddingError) as ctx: + embedder.embed_batch(['text']) + self.assertEqual(ctx.exception.retry_after_seconds, 2.5) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/ext/rag/test_embedding_openai.py b/tests/ext/rag/test_embedding_openai.py new file mode 100644 index 000000000..1858c0ee6 --- /dev/null +++ b/tests/ext/rag/test_embedding_openai.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import unittest +from types import SimpleNamespace +from unittest import mock + +from dapr.ext.rag.embedding.openai import OpenAIEmbedder +from dapr.ext.rag.errors import ( + InvalidEmbeddingRequestError, + OptionalDependencyError, + TransientEmbeddingError, +) + + +class _FakeEmbeddings: + def __init__(self, response=None, exception=None): + self._response = response + self._exception = exception + self.calls = [] + + def create(self, **kwargs): + self.calls.append(kwargs) + if self._exception is not None: + raise self._exception + return self._response + + +class _FakeOpenAIClient: + def __init__(self, response=None, exception=None): + self.embeddings = _FakeEmbeddings(response=response, exception=exception) + + +def _response(items, total_tokens=None): + data = [SimpleNamespace(index=index, embedding=embedding) for index, embedding in items] + usage = SimpleNamespace(total_tokens=total_tokens) if total_tokens is not None else None + return SimpleNamespace(data=data, usage=usage) + + +def _fake_error(name, status_code=None): + error_cls = type(name, (Exception,), {}) + error = error_cls('boom') + if status_code is not None: + error.status_code = status_code + return error + + +class OpenAIEmbedderConstructionTest(unittest.TestCase): + def test_raises_optional_dependency_error_without_openai_or_client(self): + with mock.patch('dapr.ext.rag.embedding.openai.openai', None): + with self.assertRaises(OptionalDependencyError): + OpenAIEmbedder() + + def test_client_injection_bypasses_the_dependency_check(self): + with mock.patch('dapr.ext.rag.embedding.openai.openai', None): + embedder = OpenAIEmbedder(client=_FakeOpenAIClient(response=_response([]))) + self.assertEqual(embedder.embedding_model, 'text-embedding-3-small') + + def test_config_fingerprint_changes_with_model(self): + small = OpenAIEmbedder(client=_FakeOpenAIClient()).config_fingerprint() + large = OpenAIEmbedder( + model='text-embedding-3-large', client=_FakeOpenAIClient() + ).config_fingerprint() + self.assertNotEqual(small, large) + + +class OpenAIEmbedderEmbedBatchTest(unittest.TestCase): + def test_returns_embeddings_in_input_order_even_if_the_response_is_out_of_order(self): + response = _response([(1, [0.2, 0.2]), (0, [0.1, 0.1])], total_tokens=42) + embedder = OpenAIEmbedder(client=_FakeOpenAIClient(response=response)) + result = embedder.embed_batch(['first', 'second']) + self.assertEqual(result.embeddings, [[0.1, 0.1], [0.2, 0.2]]) + self.assertEqual(result.total_tokens, 42) + + def test_empty_batch_returns_immediately_without_calling_the_client(self): + client = _FakeOpenAIClient(response=_response([])) + embedder = OpenAIEmbedder(client=client) + result = embedder.embed_batch([]) + self.assertEqual(result.embeddings, []) + self.assertEqual(client.embeddings.calls, []) + + def test_dimensions_are_forwarded_when_configured(self): + client = _FakeOpenAIClient(response=_response([(0, [0.1])])) + embedder = OpenAIEmbedder(dimensions=256, client=client) + embedder.embed_batch(['text']) + self.assertEqual(client.embeddings.calls[0]['dimensions'], 256) + + def test_dimensions_are_omitted_by_default(self): + client = _FakeOpenAIClient(response=_response([(0, [0.1])])) + embedder = OpenAIEmbedder(client=client) + embedder.embed_batch(['text']) + self.assertNotIn('dimensions', client.embeddings.calls[0]) + + +class OpenAIEmbedderErrorClassificationTest(unittest.TestCase): + def test_rate_limit_error_is_transient(self): + client = _FakeOpenAIClient(exception=_fake_error('RateLimitError')) + embedder = OpenAIEmbedder(client=client) + with self.assertRaises(TransientEmbeddingError): + embedder.embed_batch(['text']) + + def test_bad_request_error_is_non_retryable(self): + client = _FakeOpenAIClient(exception=_fake_error('BadRequestError')) + embedder = OpenAIEmbedder(client=client) + with self.assertRaises(InvalidEmbeddingRequestError): + embedder.embed_batch(['text']) + + def test_unrecognized_5xx_status_is_transient(self): + client = _FakeOpenAIClient(exception=_fake_error('SomeFutureServerError', status_code=503)) + embedder = OpenAIEmbedder(client=client) + with self.assertRaises(TransientEmbeddingError): + embedder.embed_batch(['text']) + + def test_unrecognized_4xx_status_is_non_retryable(self): + client = _FakeOpenAIClient(exception=_fake_error('SomeFutureClientError', status_code=422)) + embedder = OpenAIEmbedder(client=client) + with self.assertRaises(InvalidEmbeddingRequestError): + embedder.embed_batch(['text']) + + def test_totally_unrecognized_failure_defaults_to_transient(self): + client = _FakeOpenAIClient(exception=_fake_error('WeirdNetworkGlitch')) + embedder = OpenAIEmbedder(client=client) + with self.assertRaises(TransientEmbeddingError): + embedder.embed_batch(['text']) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/ext/rag/test_fingerprints.py b/tests/ext/rag/test_fingerprints.py new file mode 100644 index 000000000..66dc6beef --- /dev/null +++ b/tests/ext/rag/test_fingerprints.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import unittest + +from dapr.ext.rag.fingerprints import ( + compute_chunk_id, + compute_config_hash, + compute_content_hash, + compute_manifest_hash, + compute_pipeline_fingerprint, +) + + +def _base_chunk_id_kwargs() -> dict: + return dict( + source_document_id='s3://bucket/doc.txt', + source_content_hash='abc123', + parser_config_hash='parser-hash', + splitter_config_hash='splitter-hash', + chunk_ordinal=0, + chunk_content_hash='chunk-hash', + embedding_model='text-embedding-3-small', + ) + + +class ComputeContentHashTest(unittest.TestCase): + def test_deterministic(self): + self.assertEqual(compute_content_hash(b'hello'), compute_content_hash(b'hello')) + + def test_sensitive_to_content(self): + self.assertNotEqual(compute_content_hash(b'hello'), compute_content_hash(b'hellp')) + + def test_no_python_hash_randomization_dependency(self): + # A regression guard: compute_content_hash must never delegate to Python's + # process-randomized `hash()` for a persistent identity. + digest = compute_content_hash(b'hello') + self.assertEqual(len(digest), 64) # hex sha256 + int(digest, 16) # raises ValueError if it isn't hex + + +class ComputeConfigHashTest(unittest.TestCase): + def test_stable_regardless_of_key_order(self): + first = compute_config_hash({'a': 1, 'b': 2}) + second = compute_config_hash({'b': 2, 'a': 1}) + self.assertEqual(first, second) + + def test_sensitive_to_values(self): + first = compute_config_hash({'chunk_size': 1000}) + second = compute_config_hash({'chunk_size': 1001}) + self.assertNotEqual(first, second) + + def test_sensitive_to_added_keys(self): + first = compute_config_hash({'a': 1}) + second = compute_config_hash({'a': 1, 'b': None}) + self.assertNotEqual(first, second) + + +class ComputeChunkIdTest(unittest.TestCase): + def test_deterministic_replay_produces_the_same_id(self): + kwargs = _base_chunk_id_kwargs() + self.assertEqual(compute_chunk_id(**kwargs), compute_chunk_id(**kwargs)) + + def test_changing_any_single_field_changes_the_id(self): + baseline = compute_chunk_id(**_base_chunk_id_kwargs()) + overrides = { + 'source_document_id': 's3://bucket/other.txt', + 'source_content_hash': 'different-content-hash', + 'parser_config_hash': 'different-parser-hash', + 'splitter_config_hash': 'different-splitter-hash', + 'chunk_ordinal': 1, + 'chunk_content_hash': 'different-chunk-hash', + 'embedding_model': 'text-embedding-3-large', + } + for field, new_value in overrides.items(): + with self.subTest(field=field): + kwargs = _base_chunk_id_kwargs() + kwargs[field] = new_value + self.assertNotEqual(baseline, compute_chunk_id(**kwargs)) + + def test_id_is_a_hex_sha256_digest(self): + chunk_id = compute_chunk_id(**_base_chunk_id_kwargs()) + self.assertEqual(len(chunk_id), 64) + int(chunk_id, 16) + + +class ComputeManifestHashTest(unittest.TestCase): + def test_stable_regardless_of_listing_order(self): + ids = ['doc-a', 'doc-b', 'doc-c'] + self.assertEqual(compute_manifest_hash(ids), compute_manifest_hash(reversed(ids))) + + def test_sensitive_to_membership(self): + first = compute_manifest_hash(['doc-a', 'doc-b']) + second = compute_manifest_hash(['doc-a', 'doc-c']) + self.assertNotEqual(first, second) + + def test_empty_manifest_is_stable(self): + self.assertEqual(compute_manifest_hash([]), compute_manifest_hash([])) + + +class ComputePipelineFingerprintTest(unittest.TestCase): + def _base_kwargs(self) -> dict: + return dict( + parser_config_hash='parser-hash', + splitter_config_hash='splitter-hash', + embedding_model='text-embedding-3-small', + embedding_config_hash='embed-hash', + ) + + def test_deterministic(self): + kwargs = self._base_kwargs() + self.assertEqual( + compute_pipeline_fingerprint(**kwargs), compute_pipeline_fingerprint(**kwargs) + ) + + def test_changing_parser_config_changes_fingerprint(self): + baseline = compute_pipeline_fingerprint(**self._base_kwargs()) + kwargs = self._base_kwargs() + kwargs['parser_config_hash'] = 'different' + self.assertNotEqual(baseline, compute_pipeline_fingerprint(**kwargs)) + + def test_changing_splitter_config_changes_fingerprint(self): + baseline = compute_pipeline_fingerprint(**self._base_kwargs()) + kwargs = self._base_kwargs() + kwargs['splitter_config_hash'] = 'different' + self.assertNotEqual(baseline, compute_pipeline_fingerprint(**kwargs)) + + def test_changing_embedding_model_changes_fingerprint(self): + baseline = compute_pipeline_fingerprint(**self._base_kwargs()) + kwargs = self._base_kwargs() + kwargs['embedding_model'] = 'text-embedding-3-large' + self.assertNotEqual(baseline, compute_pipeline_fingerprint(**kwargs)) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/ext/rag/test_generation.py b/tests/ext/rag/test_generation.py new file mode 100644 index 000000000..8feec23af --- /dev/null +++ b/tests/ext/rag/test_generation.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import unittest +from types import SimpleNamespace + +from dapr.ext.rag.errors import InvalidGenerationRequestError, TransientGenerationError +from dapr.ext.rag.generation import AzureOpenAIChatClient +from dapr.ext.rag.models import QueryMatch + + +class _FakeChatCompletions: + def __init__(self, answer_text='the answer', exception=None): + self._answer_text = answer_text + self._exception = exception + self.calls = [] + + def create(self, **kwargs): + self.calls.append(kwargs) + if self._exception is not None: + raise self._exception + message = SimpleNamespace(content=self._answer_text) + return SimpleNamespace(choices=[SimpleNamespace(message=message)]) + + +class _FakeChatClient: + def __init__(self, answer_text='the answer', exception=None): + self.chat = SimpleNamespace(completions=_FakeChatCompletions(answer_text, exception)) + + +def _match(chunk_id='c1', document_id='doc-1', content='relevant text', score=0.9, **metadata): + return QueryMatch( + chunk_id=chunk_id, document_id=document_id, content=content, score=score, metadata=metadata + ) + + +def _fake_error(name, status_code=None): + error_cls = type(name, (Exception,), {}) + error = error_cls('boom') + if status_code is not None: + error.status_code = status_code + return error + + +class AzureOpenAIChatClientTest(unittest.TestCase): + def test_generates_an_answer_with_citations(self): + client = _FakeChatClient(answer_text='The policy allows remote work. [1]') + chat = AzureOpenAIChatClient( + endpoint='https://x.openai.azure.com', deployment='chat-deploy', client=client + ) + matches = [ + _match( + chunk_id='c1', + document_id='doc-1', + source_name='handbook.pdf', + source_uri='blob://handbook.pdf', + ) + ] + + result = chat.generate_answer('Can I work remotely?', matches, index_version='2026-09') + + self.assertEqual(result.answer, 'The policy allows remote work. [1]') + self.assertTrue(result.sufficient_evidence) + self.assertEqual(len(result.citations), 1) + self.assertEqual(result.citations[0].chunk_id, 'c1') + self.assertEqual(result.citations[0].title, 'handbook.pdf') + self.assertEqual(result.citations[0].source_uri, 'blob://handbook.pdf') + self.assertEqual(result.index_version, '2026-09') + + def test_addresses_the_request_by_deployment_name(self): + client = _FakeChatClient() + chat = AzureOpenAIChatClient( + endpoint='https://x.openai.azure.com', deployment='my-chat-deploy', client=client + ) + chat.generate_answer('question', [_match()], index_version='v1') + self.assertEqual(client.chat.completions.calls[0]['model'], 'my-chat-deploy') + + def test_insufficient_evidence_skips_the_model_call_entirely(self): + client = _FakeChatClient() + chat = AzureOpenAIChatClient( + endpoint='https://x.openai.azure.com', + deployment='d', + client=client, + min_context_matches=1, + ) + result = chat.generate_answer('question', [], index_version='v1') + self.assertFalse(result.sufficient_evidence) + self.assertEqual(result.citations, ()) + self.assertEqual(client.chat.completions.calls, []) + + def test_low_score_matches_are_excluded_from_context_and_the_sufficiency_check(self): + client = _FakeChatClient() + chat = AzureOpenAIChatClient( + endpoint='https://x.openai.azure.com', + deployment='d', + client=client, + min_context_matches=1, + min_score=0.5, + ) + result = chat.generate_answer('question', [_match(score=0.1)], index_version='v1') + self.assertFalse(result.sufficient_evidence) + + def test_workflow_instance_id_is_echoed_back(self): + client = _FakeChatClient() + chat = AzureOpenAIChatClient( + endpoint='https://x.openai.azure.com', deployment='d', client=client + ) + result = chat.generate_answer( + 'q', [_match()], index_version='v1', workflow_instance_id='wf-1' + ) + self.assertEqual(result.workflow_instance_id, 'wf-1') + + def test_transient_failure_raises_transient_generation_error(self): + client = _FakeChatClient(exception=_fake_error('RateLimitError')) + chat = AzureOpenAIChatClient( + endpoint='https://x.openai.azure.com', deployment='d', client=client + ) + with self.assertRaises(TransientGenerationError): + chat.generate_answer('q', [_match()], index_version='v1') + + def test_invalid_request_raises_invalid_generation_request_error(self): + client = _FakeChatClient(exception=_fake_error('BadRequestError')) + chat = AzureOpenAIChatClient( + endpoint='https://x.openai.azure.com', deployment='d', client=client + ) + with self.assertRaises(InvalidGenerationRequestError): + chat.generate_answer('q', [_match()], index_version='v1') + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/ext/rag/test_models.py b/tests/ext/rag/test_models.py new file mode 100644 index 000000000..d50cb47e8 --- /dev/null +++ b/tests/ext/rag/test_models.py @@ -0,0 +1,158 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import unittest + +from dapr.ext.rag.models import ( + ActivationRecord, + CompletionRecord, + DocumentFailure, + DocumentWorkItem, + EmbedProgressRecord, + PipelineConfig, + PipelineStatus, + SourceDocument, + SourceMetadata, + SourceProvider, +) + + +class PipelineConfigTest(unittest.TestCase): + def test_defaults_match_the_documented_values(self): + config = PipelineConfig() + self.assertEqual(config.max_concurrent_documents, 10) + self.assertEqual(config.embedding_batch_size, 64) + self.assertEqual(config.max_activity_attempts, 5) + self.assertFalse(config.fail_fast) + + def test_effective_manifest_page_size_defaults_to_max_concurrent_documents(self): + config = PipelineConfig(max_concurrent_documents=7) + self.assertEqual(config.effective_manifest_page_size, 7) + + def test_effective_manifest_page_size_honors_explicit_override(self): + config = PipelineConfig(max_concurrent_documents=7, manifest_page_size=25) + self.assertEqual(config.effective_manifest_page_size, 25) + + def test_rejects_non_positive_max_concurrent_documents(self): + with self.assertRaises(ValueError): + PipelineConfig(max_concurrent_documents=0) + + def test_rejects_non_positive_embedding_batch_size(self): + with self.assertRaises(ValueError): + PipelineConfig(embedding_batch_size=0) + + def test_rejects_non_positive_max_activity_attempts(self): + with self.assertRaises(ValueError): + PipelineConfig(max_activity_attempts=0) + + def test_round_trips_through_to_dict_and_from_dict(self): + config = PipelineConfig(max_concurrent_documents=3, fail_fast=True) + self.assertEqual(PipelineConfig.from_dict(config.to_dict()), config) + + +class DocumentWorkItemTest(unittest.TestCase): + def test_from_source_document_flattens_nested_metadata(self): + doc = SourceDocument( + document_id='s3://bucket/a.txt', + provider=SourceProvider.S3, + uri='s3://bucket/a.txt', + name='a.txt', + metadata=SourceMetadata(etag='etag-1', version_id='v1', content_length=42), + ) + item = DocumentWorkItem.from_source_document(doc) + self.assertEqual( + item, + DocumentWorkItem( + document_id='s3://bucket/a.txt', + provider='s3', + uri='s3://bucket/a.txt', + name='a.txt', + source_etag='etag-1', + source_version_id='v1', + source_content_length=42, + ), + ) + + +class ActivationRecordTest(unittest.TestCase): + def test_round_trips_through_to_dict_and_from_dict(self): + record = ActivationRecord( + pipeline_id='company-knowledge', + active_version='2026-09', + previous_version='2026-08', + manifest_hash='abc', + activated_at='2026-09-10T00:00:00+00:00', + workflow_instance_id='wf-1', + ) + self.assertEqual(ActivationRecord.from_dict(record.to_dict()), record) + + +class CompletionRecordTest(unittest.TestCase): + def test_round_trips_through_to_dict_and_from_dict(self): + record = CompletionRecord( + document_id='doc-1', + source_content_hash='hash-1', + pipeline_fingerprint='fp-1', + chunk_count=3, + embedded_chunk_count=3, + completed_at='2026-09-10T00:00:00+00:00', + ) + self.assertEqual(CompletionRecord.from_dict(record.to_dict()), record) + + +class EmbedProgressRecordTest(unittest.TestCase): + def test_round_trips_including_completed_batch_indices(self): + record = EmbedProgressRecord( + document_id='doc-1', + source_content_hash='hash-1', + pipeline_fingerprint='fp-1', + total_batches=3, + completed_batch_indices=[0, 1], + ) + restored = EmbedProgressRecord.from_dict(record.to_dict()) + self.assertEqual(restored, record) + self.assertEqual(restored.completed_batch_indices, [0, 1]) + + +class PipelineStatusTest(unittest.TestCase): + def test_round_trips_including_nested_failures(self): + status = PipelineStatus( + pipeline_id='company-knowledge', + requested_version='2026-09', + workflow_instance_id='wf-1', + failed_documents=1, + failures=( + DocumentFailure( + document_id='doc-1', + error_type='DocumentParseError', + error_message='corrupt', + retryable=False, + ), + ), + ) + restored = PipelineStatus.from_dict(status.to_dict()) + self.assertEqual(restored, status) + self.assertIsInstance(restored.failures[0], DocumentFailure) + + def test_from_dict_defaults_missing_failures_to_empty(self): + status = PipelineStatus(pipeline_id='p', requested_version='v', workflow_instance_id='wf-1') + payload = status.to_dict() + del payload['failures'] + restored = PipelineStatus.from_dict(payload) + self.assertEqual(restored.failures, ()) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/ext/rag/test_parsing_langchain.py b/tests/ext/rag/test_parsing_langchain.py new file mode 100644 index 000000000..5bccbbca0 --- /dev/null +++ b/tests/ext/rag/test_parsing_langchain.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import unittest +from unittest import mock + +from dapr.ext.rag.errors import OptionalDependencyError +from dapr.ext.rag.models import Document +from dapr.ext.rag.parsing.langchain import from_langchain_documents, to_langchain_documents + + +class _DuckTypedDocument: + """A minimal page_content/metadata object -- not a real LangChain Document.""" + + def __init__(self, page_content, metadata): + self.page_content = page_content + self.metadata = metadata + + +class ToLangchainDocumentsTest(unittest.TestCase): + def test_converts_preserving_content_and_metadata(self): + documents = [Document(page_content='hello', metadata={'page_number': 1})] + converted = to_langchain_documents(documents) + self.assertEqual(len(converted), 1) + self.assertEqual(converted[0].page_content, 'hello') + self.assertEqual(converted[0].metadata, {'page_number': 1}) + + def test_converts_multiple_documents_preserving_order(self): + documents = [Document(page_content=f'doc-{i}') for i in range(3)] + converted = to_langchain_documents(documents) + self.assertEqual([d.page_content for d in converted], ['doc-0', 'doc-1', 'doc-2']) + + def test_raises_optional_dependency_error_when_langchain_core_is_missing(self): + with mock.patch('dapr.ext.rag.parsing.langchain._LangchainDocument', None): + with self.assertRaises(OptionalDependencyError): + to_langchain_documents([Document(page_content='hello')]) + + +class FromLangchainDocumentsTest(unittest.TestCase): + def test_converts_real_langchain_documents(self): + from langchain_core.documents import Document as LangchainDocument + + lc_docs = [LangchainDocument(page_content='hello', metadata={'source': 'a.txt'})] + converted = from_langchain_documents(lc_docs) + self.assertEqual(converted, [Document(page_content='hello', metadata={'source': 'a.txt'})]) + + def test_converts_duck_typed_documents_without_requiring_langchain(self): + # No `langchain_core` import happens on this path -- see from_langchain_documents' + # docstring -- so this must work even if langchain-core were uninstalled. + duck_docs = [_DuckTypedDocument(page_content='hello', metadata={'a': 1})] + converted = from_langchain_documents(duck_docs) + self.assertEqual(converted, [Document(page_content='hello', metadata={'a': 1})]) + + def test_round_trips_through_to_and_from(self): + original = [Document(page_content='hello', metadata={'k': 'v'})] + round_tripped = from_langchain_documents(to_langchain_documents(original)) + self.assertEqual(round_tripped, original) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/ext/rag/test_parsing_unstructured.py b/tests/ext/rag/test_parsing_unstructured.py new file mode 100644 index 000000000..d0cbc8670 --- /dev/null +++ b/tests/ext/rag/test_parsing_unstructured.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import unittest +from types import SimpleNamespace +from unittest import mock + +from dapr.ext.rag.errors import ( + DocumentParseError, + OptionalDependencyError, + UnsupportedDocumentError, +) +from dapr.ext.rag.models import SourceDocument, SourceMetadata, SourceProvider +from dapr.ext.rag.parsing.unstructured import UnstructuredParser + + +def _element(text, page_number=None): + return SimpleNamespace(text=text, metadata=SimpleNamespace(page_number=page_number)) + + +def _doc(name='a.txt', content_type=None): + return SourceDocument( + document_id=f's3://bucket/{name}', + provider=SourceProvider.S3, + uri=f's3://bucket/{name}', + name=name, + metadata=SourceMetadata(content_type=content_type), + ) + + +class UnstructuredParserConstructionTest(unittest.TestCase): + def test_parser_type_is_stable(self): + self.assertEqual( + UnstructuredParser(partition_fn=lambda **_: []).parser_type, 'unstructured' + ) + + def test_config_reflects_strategy_and_languages(self): + parser = UnstructuredParser( + strategy='hi_res', languages=['eng'], partition_fn=lambda **_: [] + ) + self.assertEqual(parser.config(), {'strategy': 'hi_res', 'languages': ('eng',)}) + + def test_raises_optional_dependency_error_without_unstructured_or_partition_fn(self): + with mock.patch('dapr.ext.rag.parsing.unstructured._default_partition', None): + parser = UnstructuredParser() + with self.assertRaises(OptionalDependencyError): + parser.parse(b'hello', _doc()) + + +class UnstructuredParserParseTest(unittest.TestCase): + def test_unsupported_extension_raises_without_calling_partition(self): + partition_fn = mock.Mock() + parser = UnstructuredParser(partition_fn=partition_fn) + with self.assertRaises(UnsupportedDocumentError): + parser.parse(b'binary', _doc(name='a.exe')) + partition_fn.assert_not_called() + + def test_groups_elements_by_page_number(self): + elements = [ + _element('Title', page_number=1), + _element('Body text on page 1', page_number=1), + _element('Body text on page 2', page_number=2), + ] + parser = UnstructuredParser(partition_fn=lambda **_: elements) + documents = parser.parse(b'%PDF-1.4...', _doc(name='a.pdf')) + self.assertEqual(len(documents), 2) + self.assertEqual(documents[0].page_content, 'Title\n\nBody text on page 1') + self.assertEqual(documents[0].metadata['page_number'], 1) + self.assertEqual(documents[1].page_content, 'Body text on page 2') + self.assertEqual(documents[1].metadata['page_number'], 2) + + def test_documents_without_pages_collapse_to_a_single_document(self): + elements = [_element('Paragraph one.'), _element('Paragraph two.')] + parser = UnstructuredParser(partition_fn=lambda **_: elements) + documents = parser.parse(b'plain text', _doc(name='a.txt')) + self.assertEqual(len(documents), 1) + self.assertEqual(documents[0].page_content, 'Paragraph one.\n\nParagraph two.') + + def test_blank_elements_are_skipped(self): + elements = [_element(' '), _element('Real content')] + parser = UnstructuredParser(partition_fn=lambda **_: elements) + documents = parser.parse(b'text', _doc(name='a.txt')) + self.assertEqual(len(documents), 1) + self.assertEqual(documents[0].page_content, 'Real content') + + def test_no_elements_produces_no_documents(self): + parser = UnstructuredParser(partition_fn=lambda **_: []) + self.assertEqual(parser.parse(b'', _doc()), []) + + def test_document_metadata_includes_source_document_id_and_filename(self): + parser = UnstructuredParser(partition_fn=lambda **_: [_element('hello')]) + documents = parser.parse(b'hello', _doc(name='policies/a.txt')) + self.assertEqual(documents[0].metadata['filename'], 'policies/a.txt') + self.assertEqual(documents[0].metadata['source_document_id'], 's3://bucket/policies/a.txt') + + def test_generic_partition_failure_becomes_document_parse_error(self): + def failing_partition(**_): + raise ValueError('corrupt PDF') + + parser = UnstructuredParser(partition_fn=failing_partition) + with self.assertRaises(DocumentParseError): + parser.parse(b'bad', _doc(name='a.pdf')) + + def test_missing_format_extra_becomes_unsupported_document_error(self): + def failing_partition(**_): + raise ImportError('python-docx is not installed') + + parser = UnstructuredParser(partition_fn=failing_partition) + with self.assertRaises(UnsupportedDocumentError): + parser.parse(b'bad', _doc(name='a.docx')) + + def test_partition_is_called_with_the_filename_hint(self): + partition_fn = mock.Mock(return_value=[]) + parser = UnstructuredParser(partition_fn=partition_fn) + parser.parse(b'hello', _doc(name='a.md')) + _, kwargs = partition_fn.call_args + self.assertEqual(kwargs['metadata_filename'], 'a.md') + + def test_content_type_hint_is_forwarded_when_present(self): + partition_fn = mock.Mock(return_value=[]) + parser = UnstructuredParser(partition_fn=partition_fn) + parser.parse(b'hello', _doc(name='a.html', content_type='text/html')) + _, kwargs = partition_fn.call_args + self.assertEqual(kwargs['content_type'], 'text/html') + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/ext/rag/test_pipeline.py b/tests/ext/rag/test_pipeline.py new file mode 100644 index 000000000..bf9b7bc7e --- /dev/null +++ b/tests/ext/rag/test_pipeline.py @@ -0,0 +1,1270 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import dataclasses +import unittest +from datetime import datetime, timezone +from types import SimpleNamespace +from typing import Optional +from unittest import mock + +import grpc + +from dapr.ext.rag._wire import to_wire +from dapr.ext.rag.embedding.base import Embedder +from dapr.ext.rag.errors import DocumentParseError, TransientEmbeddingError +from dapr.ext.rag.models import ( + Chunk, + Document, + DocumentOutcomeStatus, + DocumentWorkItem, + EmbeddingBatchResult, + FoundryIQKnowledgeSourceConfig, + PipelineConfig, + PipelineStage, + SourceDocument, + SourceMetadata, + SourceProvider, + UpsertResult, + ValidationResult, +) +from dapr.ext.rag.parsing.base import DocumentParser +from dapr.ext.rag.pipeline import DurableRAGPipeline, _ActivationState, _IngestionState +from dapr.ext.rag.sources.base import DocumentSource +from dapr.ext.rag.splitting import DocumentSplitter +from dapr.ext.rag.vector_stores.base import VectorIndex + +_SECRET_CONNECTION_STRING = 'postgresql://user:sk-should-not-leak@host/db' +_SECRET_API_KEY = 'sk-should-not-leak-either' + + +# --------------------------------------------------------------------------- +# Fakes: real (if minimal) ABC implementations with controllable behavior, +# rather than loose Mocks -- exercising this package's actual glue code +# (config()/config_fingerprint(), provenance construction) for real. +# --------------------------------------------------------------------------- + + +class _FakeSource(DocumentSource): + def __init__(self, documents=(), content_by_id=None, metadata_sequence_by_id=None): + self._documents = list(documents) + self._content_by_id = content_by_id or {} + self._metadata_sequence_by_id = metadata_sequence_by_id or {} + self._metadata_call_count: dict[str, int] = {} + self.closed = False + + @property + def provider(self): + return SourceProvider.S3 + + def list_documents(self, prefix=None): + return iter(self._documents) + + def get_document(self, document_id): + return self._content_by_id[document_id] + + def get_metadata(self, document_id): + sequence = self._metadata_sequence_by_id.get(document_id) + if sequence is None: + return SourceMetadata() + call_index = self._metadata_call_count.get(document_id, 0) + self._metadata_call_count[document_id] = call_index + 1 + return sequence[min(call_index, len(sequence) - 1)] + + def close(self): + self.closed = True + + +class _FakeParser(DocumentParser): + """Splits raw bytes on '||' into one Document per resulting piece.""" + + def __init__(self, fail_for_document_ids=()): + self._fail_for_document_ids = set(fail_for_document_ids) + + @property + def parser_type(self): + return 'fake-parser' + + def parse(self, content, document): + if document.document_id in self._fail_for_document_ids: + raise DocumentParseError(f'simulated parse failure for {document.document_id}') + return [Document(page_content=piece) for piece in content.decode('utf-8').split('||')] + + +class _FakeSplitter(DocumentSplitter): + """Splits a Document's page_content on '|' into one Chunk per piece.""" + + @property + def splitter_type(self): + return 'fake-splitter' + + def split(self, document): + pieces = [p for p in document.page_content.split('|') if p] + return [ + Chunk(chunk_ordinal=i, content=p, metadata=dict(document.metadata)) + for i, p in enumerate(pieces) + ] + + +class _FakeEmbedder(Embedder): + def __init__(self, dimension=2, fail_on_texts=()): + self._dimension = dimension + self._fail_on_texts = set(fail_on_texts) + self.calls: list[list[str]] = [] + + @property + def embedding_model(self): + return 'fake-embedding-model' + + def config(self): + return { + 'model': self.embedding_model, + 'api_key': _SECRET_API_KEY, + } # must never leak -- see config() + + def embed_batch(self, texts): + self.calls.append(list(texts)) + for text in texts: + if text in self._fail_on_texts: + raise TransientEmbeddingError(f'simulated embedding failure for {text!r}') + embeddings = [[float(i)] * self._dimension for i in range(len(texts))] + return EmbeddingBatchResult(embeddings=embeddings, total_tokens=len(texts)) + + +class _FakeVectorStore(VectorIndex): + def __init__(self, index_name='company-knowledge'): + self._index_name = index_name + self.upserted: list[tuple] = [] # (version, VectorRecord) + self.deleted: list[tuple] = [] + self.activate_version_calls: list[tuple] = [] # (version, previous_version) + self.foundry_iq_registration_calls: list[dict] = [] + self.foundry_iq_registration_error: Optional[Exception] = None + + def activate_version(self, version, *, previous_version): + self.activate_version_calls.append((version, previous_version)) + + def register_foundry_iq_knowledge_source( + self, version, *, name, description=None, source_data_fields=None, search_fields=None + ): + self.foundry_iq_registration_calls.append( + { + 'version': version, + 'name': name, + 'description': description, + 'source_data_fields': source_data_fields, + 'search_fields': search_fields, + } + ) + if self.foundry_iq_registration_error is not None: + raise self.foundry_iq_registration_error + + @property + def target_index_name(self): + return self._index_name + + @property + def store_type(self): + return 'fake-store' + + def upsert(self, records, version): + records = list(records) + self.upserted.extend((version, r) for r in records) + return UpsertResult(upserted_count=len(records), version=version) + + def delete_document(self, document_id, version): + self.deleted.append((version, document_id)) + + def validate_version(self, version): + chunks = [r for v, r in self.upserted if v == version] + return ValidationResult( + valid=len(chunks) > 0, + version=version, + actual_chunk_count=len(chunks), + actual_document_count=len({c.document_id for c in chunks}), + ) + + def query(self, embedding, version, *, top_k=5, metadata_filter=None, query_text=None): + return [] + + +class _FakeSimpleVectorStore(VectorIndex): + """A minimal `VectorIndex` with no `register_foundry_iq_knowledge_source` method at + all -- unlike `_FakeVectorStore`, which every other fake in this file uses and which + always has one. Used only to prove `DurableRAGPipeline`'s constructor validation + actually rejects a vector_store that lacks the capability (a class attribute set to + `None` would still satisfy `hasattr`, so this has to be a genuinely different class, + not `_FakeVectorStore` with the method overridden away).""" + + @property + def target_index_name(self): + return 'company-knowledge' + + @property + def store_type(self): + return 'fake-simple-store' + + def upsert(self, records, version): + return UpsertResult(upserted_count=len(list(records)), version=version) + + def delete_document(self, document_id, version): + pass + + def validate_version(self, version): + return ValidationResult(valid=True, version=version, actual_chunk_count=0) + + def query(self, embedding, version, *, top_k=5, metadata_filter=None, query_text=None): + return [] + + +class _FakeAbortedError(grpc.RpcError): + def code(self): + return grpc.StatusCode.ABORTED + + def details(self): + return 'etag mismatch' + + +class _FakeDaprClient: + """A minimal but *real* key-value store behind the DaprClient state API, + including etag-conditional writes -- see dapr/clients/grpc/_state.py for + the contract this mimics. + """ + + def __init__(self): + self._store: dict[str, tuple[bytes, str]] = {} + self._etag_counter = 0 + self.published_events = [] + + def get_state(self, store_name, key, state_metadata=None, metadata=None): + data, etag = self._store.get(key, (b'', '')) + return SimpleNamespace(data=data, etag=etag) + + def save_state( + self, store_name, key, value, etag=None, options=None, state_metadata=None, metadata=None + ): + _current_data, current_etag = self._store.get(key, (b'', '')) + if etag and etag != current_etag: + raise _FakeAbortedError() + self._etag_counter += 1 + data = value.encode('utf-8') if isinstance(value, str) else value + self._store[key] = (data, str(self._etag_counter)) + + def publish_event(self, pubsub_name, topic_name, data, data_content_type=None): + self.published_events.append( + {'pubsub_name': pubsub_name, 'topic_name': topic_name, 'data': data} + ) + + def close(self): + pass + + +class _PendingCall: + def __init__(self, activity_name, input): + self.activity_name = activity_name + self.input = input + + +class _FakeWorkflowContext: + def __init__(self, instance_id='wf-instance-1'): + self.instance_id = instance_id + self.is_replaying = False + self.current_utc_datetime = datetime(2026, 9, 10, tzinfo=timezone.utc) + self.calls: list[_PendingCall] = [] + self.continue_as_new_input = None + + def call_activity( + self, activity, *, input=None, retry_policy=None, app_id=None, propagation=None + ): + pending = _PendingCall(activity_name=activity, input=input) + self.calls.append(pending) + return pending + + def continue_as_new(self, new_input, *, save_events=False): + self.continue_as_new_input = new_input + + +def _activity_dispatch(pipeline): + return { + pipeline._activity_names['discover_and_manifest']: pipeline._activity_discover_and_manifest, + pipeline._activity_names['get_manifest_batch']: pipeline._activity_get_manifest_batch, + pipeline._activity_names['process_document']: pipeline._activity_process_document, + pipeline._activity_names['validate_version']: pipeline._activity_validate_version, + pipeline._activity_names['activate_version']: pipeline._activity_activate_version, + pipeline._activity_names[ + 'publish_activation_event' + ]: pipeline._activity_publish_activation_event, + pipeline._activity_names[ + 'register_foundry_iq_knowledge_source' + ]: pipeline._activity_register_foundry_iq_knowledge_source, + pipeline._activity_names['update_status']: pipeline._activity_update_status, + } + + +def _drive_one_generation(dispatch, gen, activity_ctx): + """Drives a single orchestrator generation to `StopIteration`, executing each + yielded call against `dispatch`. Returns the generation's return value -- + which, for `_orchestrate_ingestion`, is `None` when it ended via + `continue_as_new` rather than by actually finishing. + """ + sent = None + while True: + try: + pending = gen.send(sent) + except StopIteration as stop: + return stop.value + sent = dispatch[pending.activity_name](activity_ctx, pending.input) + + +def _drive_with_real_activities(pipeline, orchestrator_fn, ctx, wf_input, activity_ctx): + """Drives an orchestrator to completion, executing each yielded call + against the pipeline's real `_activity_*` methods -- exercising real + state reads/writes and idempotency checks without a live durabletask + engine. Transparently follows `continue_as_new` the way a real + durabletask worker would (ending one generation and starting the next + with its recorded input), accumulating every generation's calls onto the + same `ctx.calls` so a caller sees the full cross-generation history. + """ + dispatch = _activity_dispatch(pipeline) + current_input = wf_input + while True: + ctx.continue_as_new_input = None + final = _drive_one_generation(dispatch, orchestrator_fn(ctx, current_input), activity_ctx) + if ctx.continue_as_new_input is None: + return final + current_input = ctx.continue_as_new_input + + +def _recording_dispatch(pipeline, results_sink): + """Like `_activity_dispatch`, but appends each activity's result to `results_sink` + in call order -- for capturing a real run's results to replay later.""" + wrapped = {} + for name, fn in _activity_dispatch(pipeline).items(): + + def make_wrapper(fn=fn): + def wrapper(activity_ctx, input): + result = fn(activity_ctx, input) + results_sink.append(result) + return result + + return wrapper + + wrapped[name] = make_wrapper() + return wrapped + + +def _drive_with_canned_results(gen, canned_results): + """Drives a generator using pre-recorded results rather than executing + activities -- a true replay: durabletask never re-invokes an activity + whose result is already in history, it just feeds the cached result back. + """ + sent = None + results = iter(canned_results) + while True: + try: + gen.send(sent) + except StopIteration as stop: + return stop.value + sent = next(results) + + +def _pipeline(config=None, **overrides): + kwargs = dict( + source=_FakeSource(), + parser=_FakeParser(), + splitter=_FakeSplitter(), + embedder=_FakeEmbedder(), + vector_store=_FakeVectorStore(), + state_store_name='rag-pipeline-state', + pipeline_id='company-knowledge', + config=config or PipelineConfig(), + workflow_runtime=mock.Mock(), + workflow_client=mock.Mock(), + dapr_client=_FakeDaprClient(), + ) + kwargs.update(overrides) + return DurableRAGPipeline(**kwargs) + + +def _work_item(document_id, name=None, etag=None): + return DocumentWorkItem( + document_id=document_id, + provider='s3', + uri=document_id, + name=name or document_id, + source_etag=etag, + ) + + +def _activity_ctx(workflow_id='wf-instance-1'): + return SimpleNamespace(workflow_id=workflow_id) + + +class DurableRAGPipelineConstructionTest(unittest.TestCase): + def test_pipeline_id_defaults_to_the_vector_store_index_name(self): + pipeline = _pipeline( + pipeline_id=None, vector_store=_FakeVectorStore(index_name='from-store') + ) + self.assertEqual(pipeline._pipeline_id, 'from-store') + + def test_registers_two_workflows_and_eight_activities(self): + runtime = mock.Mock() + _pipeline(workflow_runtime=runtime) + self.assertEqual(runtime.register_workflow.call_count, 2) + self.assertEqual(runtime.register_activity.call_count, 8) + + def test_activity_names_are_namespaced_by_pipeline_id(self): + pipeline = _pipeline(pipeline_id='my-pipeline') + for name in pipeline._activity_names.values(): + self.assertIn('my-pipeline', name) + + def test_foundry_iq_knowledge_source_requires_a_vector_store_that_supports_it(self): + with self.assertRaises(ValueError): + _pipeline( + vector_store=_FakeSimpleVectorStore(), + foundry_iq_knowledge_source=FoundryIQKnowledgeSourceConfig(name='ks'), + ) + + def test_foundry_iq_knowledge_source_is_accepted_when_the_vector_store_supports_it(self): + # _FakeVectorStore implements register_foundry_iq_knowledge_source -- must not raise. + _pipeline( + vector_store=_FakeVectorStore(), + foundry_iq_knowledge_source=FoundryIQKnowledgeSourceConfig(name='ks'), + ) + + +class DurableRAGPipelineStartTest(unittest.TestCase): + def test_schedules_a_new_workflow_with_a_stable_instance_id(self): + workflow_client = mock.Mock() + workflow_client.get_workflow_state.return_value = None + workflow_client.schedule_new_workflow.return_value = 'rag-ingest-company-knowledge-2026-09' + pipeline = _pipeline(workflow_client=workflow_client) + + instance_id = pipeline.start(version='2026-09') + + self.assertEqual(instance_id, 'rag-ingest-company-knowledge-2026-09') + workflow_client.schedule_new_workflow.assert_called_once() + _, kwargs = workflow_client.schedule_new_workflow.call_args + self.assertEqual(kwargs['instance_id'], 'rag-ingest-company-knowledge-2026-09') + + def test_does_not_reschedule_a_run_that_is_still_in_flight(self): + from dapr.ext.workflow import WorkflowStatus + + workflow_client = mock.Mock() + workflow_client.get_workflow_state.return_value = SimpleNamespace( + runtime_status=WorkflowStatus.RUNNING + ) + pipeline = _pipeline(workflow_client=workflow_client) + + instance_id = pipeline.start(version='2026-09') + + self.assertEqual(instance_id, 'rag-ingest-company-knowledge-2026-09') + workflow_client.schedule_new_workflow.assert_not_called() + + def test_reschedules_after_a_prior_terminal_run(self): + from dapr.ext.workflow import WorkflowStatus + + workflow_client = mock.Mock() + workflow_client.get_workflow_state.return_value = SimpleNamespace( + runtime_status=WorkflowStatus.FAILED + ) + pipeline = _pipeline(workflow_client=workflow_client) + + pipeline.start(version='2026-09') + + workflow_client.schedule_new_workflow.assert_called_once() + + +class DurableRAGPipelineActivateVersionTest(unittest.TestCase): + def test_schedules_the_standalone_activation_workflow(self): + workflow_client = mock.Mock() + workflow_client.get_workflow_state.return_value = None + workflow_client.schedule_new_workflow.return_value = ( + 'rag-activate-company-knowledge-2026-09' + ) + pipeline = _pipeline(workflow_client=workflow_client) + + instance_id = pipeline.activate_version('2026-09') + + self.assertEqual(instance_id, 'rag-activate-company-knowledge-2026-09') + _, kwargs = workflow_client.schedule_new_workflow.call_args + self.assertEqual( + kwargs['input'], {'pipeline_id': 'company-knowledge', 'version': '2026-09'} + ) + + +class DurableRAGPipelineStatusTest(unittest.TestCase): + def test_get_status_reflects_what_the_update_status_activity_wrote(self): + pipeline = _pipeline() + pipeline._activity_update_status( + _activity_ctx(), + { + 'pipeline_id': 'company-knowledge', + 'version': '2026-09', + 'stage': 'processing_documents', + }, + ) + status = pipeline.get_status('2026-09') + self.assertEqual(status.stage, 'processing_documents') + + def test_resolve_active_version_reflects_activation(self): + pipeline = _pipeline() + self.assertIsNone(pipeline.resolve_active_version()) + pipeline._activity_activate_version( + _activity_ctx(), + {'pipeline_id': 'company-knowledge', 'version': '2026-09', 'manifest_hash': 'h1'}, + ) + self.assertEqual(pipeline.resolve_active_version(), '2026-09') + + def test_a_new_instance_re_running_a_version_resets_counters_instead_of_accumulating(self): + """Regression test: a real integration run (see test_pipeline_integration.py) caught + this -- re-running a version's stable instance ID under a *different* explicit + instance_id (start()'s instance_id= override, not just resume-with-the-same-ID) left + `workflow_instance_id` stuck on whichever instance first wrote status, and kept + incrementing counters on top of the prior run's instead of starting over.""" + pipeline = _pipeline() + pipeline._activity_update_status( + _activity_ctx(workflow_id='wf-instance-1'), + { + 'pipeline_id': 'company-knowledge', + 'version': '2026-09', + 'stage': 'completed', + 'outcomes': [ + { + 'document_id': 'doc-1', + 'status': DocumentOutcomeStatus.COMPLETED.value, + 'chunk_count': 2, + 'embedded_chunk_count': 2, + }, + ], + }, + ) + first = pipeline.get_status('2026-09') + self.assertEqual(first.workflow_instance_id, 'wf-instance-1') + self.assertEqual(first.completed_documents, 1) + + pipeline._activity_update_status( + _activity_ctx(workflow_id='wf-instance-2'), + { + 'pipeline_id': 'company-knowledge', + 'version': '2026-09', + 'stage': 'completed', + 'outcomes': [ + { + 'document_id': 'doc-1', + 'status': DocumentOutcomeStatus.SKIPPED.value, + 'reused_chunk_count': 2, + }, + ], + }, + ) + second = pipeline.get_status('2026-09') + self.assertEqual(second.workflow_instance_id, 'wf-instance-2') + self.assertEqual(second.completed_documents, 0) + self.assertEqual(second.skipped_documents, 1) + self.assertEqual(second.reused_chunks, 2) + + +class ActivityDiscoverAndManifestTest(unittest.TestCase): + def test_writes_a_manifest_and_returns_its_summary(self): + documents = [ + SourceDocument( + document_id=f's3://b/{i}.txt', + provider=SourceProvider.S3, + uri=f's3://b/{i}.txt', + name=f'{i}.txt', + ) + for i in range(3) + ] + pipeline = _pipeline(source=_FakeSource(documents=documents)) + + summary_raw = pipeline._activity_discover_and_manifest( + _activity_ctx(), + { + 'pipeline_id': 'company-knowledge', + 'version': '2026-09', + 'page_size': 2, + 'prefix': None, + }, + ) + + self.assertEqual(summary_raw['total_documents'], 3) + page0 = pipeline._activity_get_manifest_batch( + _activity_ctx(), + {'pipeline_id': 'company-knowledge', 'version': '2026-09', 'page_index': 0}, + ) + self.assertEqual(len(page0['items']), 2) + + +class ActivityProcessDocumentTest(unittest.TestCase): + def _base_raw(self, work_item, pipeline_fingerprint='fp-1', document_ordinal=0): + return { + 'work_item': dataclasses.asdict(work_item), + 'pipeline_id': 'company-knowledge', + 'version': '2026-09', + 'pipeline_fingerprint': pipeline_fingerprint, + 'document_ordinal': document_ordinal, + } + + def test_completes_a_fresh_document(self): + work_item = _work_item('doc-1') + source = _FakeSource(content_by_id={'doc-1': b'sentence one|sentence two'}) + vector_store = _FakeVectorStore() + pipeline = _pipeline(source=source, vector_store=vector_store) + + raw = self._base_raw(work_item, pipeline_fingerprint=pipeline._pipeline_fingerprint) + outcome_raw = pipeline._activity_process_document(_activity_ctx(), raw) + + self.assertEqual(outcome_raw['status'], DocumentOutcomeStatus.COMPLETED.value) + self.assertEqual(outcome_raw['chunk_count'], 2) + self.assertEqual(outcome_raw['embedded_chunk_count'], 2) + self.assertEqual(len(vector_store.upserted), 2) + + def test_skips_an_already_completed_document_without_touching_the_embedder(self): + work_item = _work_item('doc-1') + source = _FakeSource(content_by_id={'doc-1': b'sentence one'}) + embedder = _FakeEmbedder() + pipeline = _pipeline(source=source, embedder=embedder) + raw = self._base_raw(work_item, pipeline_fingerprint=pipeline._pipeline_fingerprint) + + first = pipeline._activity_process_document(_activity_ctx(), raw) + self.assertEqual(first['status'], DocumentOutcomeStatus.COMPLETED.value) + self.assertEqual(len(embedder.calls), 1) + + second = pipeline._activity_process_document(_activity_ctx(), raw) + self.assertEqual(second['status'], DocumentOutcomeStatus.SKIPPED.value) + self.assertEqual(second['reused_chunk_count'], first['chunk_count']) + self.assertEqual(len(embedder.calls), 1) # no new embedding calls + + def test_resumes_after_a_simulated_crash_without_re_embedding_the_completed_batch(self): + work_item = _work_item('doc-1') + # Two chunks -> two embedding batches at batch size 1; the embedder fails + # only on the *second* batch's text, simulating a crash partway through. + source = _FakeSource(content_by_id={'doc-1': b'first sentence|second sentence'}) + embedder = _FakeEmbedder(fail_on_texts={'second sentence'}) + vector_store = _FakeVectorStore() + pipeline = _pipeline( + source=source, + embedder=embedder, + vector_store=vector_store, + config=PipelineConfig(embedding_batch_size=1), + ) + raw = self._base_raw(work_item, pipeline_fingerprint=pipeline._pipeline_fingerprint) + + with self.assertRaises(TransientEmbeddingError): + pipeline._activity_process_document(_activity_ctx(), raw) + + # The first batch's vector is already durably upserted even though the + # activity as a whole "failed" (simulating the crash). _FakeEmbedder records + # a call before checking whether it should fail, so the failed attempt at + # the second batch shows up here too -- only the *vector_store* side proves + # which batch actually completed. + self.assertEqual(len(vector_store.upserted), 1) + self.assertEqual(embedder.calls, [['first sentence'], ['second sentence']]) + + embedder._fail_on_texts.clear() # the retry no longer hits the failure + outcome_raw = pipeline._activity_process_document(_activity_ctx(), raw) + + self.assertEqual(outcome_raw['status'], DocumentOutcomeStatus.COMPLETED.value) + self.assertEqual(len(vector_store.upserted), 2) + # The retry only embeds the second batch again -- a third call, not a + # second attempt at the first batch, which its recorded progress skips. + self.assertEqual( + embedder.calls, [['first sentence'], ['second sentence'], ['second sentence']] + ) + + def test_a_document_changed_since_discovery_raises_a_retryable_error(self): + work_item = _work_item('doc-1', etag='etag-at-discovery') + source = _FakeSource( + content_by_id={'doc-1': b'hello'}, + metadata_sequence_by_id={'doc-1': [SourceMetadata(etag='etag-changed')]}, + ) + pipeline = _pipeline(source=source) + raw = self._base_raw(work_item, pipeline_fingerprint=pipeline._pipeline_fingerprint) + + from dapr.ext.rag.errors import DocumentChangedError + + with self.assertRaises(DocumentChangedError): + pipeline._activity_process_document(_activity_ctx(), raw) + + def test_a_non_retryable_parser_failure_becomes_a_failed_outcome_not_an_exception(self): + work_item = _work_item('doc-1') + source = _FakeSource(content_by_id={'doc-1': b'hello'}) + parser = _FakeParser(fail_for_document_ids={'doc-1'}) + pipeline = _pipeline(source=source, parser=parser) + raw = self._base_raw(work_item, pipeline_fingerprint=pipeline._pipeline_fingerprint) + + outcome_raw = pipeline._activity_process_document(_activity_ctx(), raw) + + self.assertEqual(outcome_raw['status'], DocumentOutcomeStatus.FAILED.value) + self.assertFalse(outcome_raw['retryable']) + self.assertEqual(outcome_raw['error_type'], 'DocumentParseError') + + def test_attempts_counter_increments_across_calls(self): + work_item = _work_item('doc-1') + source = _FakeSource(content_by_id={'doc-1': b'hello'}) + parser = _FakeParser( + fail_for_document_ids={'doc-1'} + ) # always "fails" (non-retryable), no state churn + pipeline = _pipeline(source=source, parser=parser) + raw = self._base_raw(work_item, pipeline_fingerprint=pipeline._pipeline_fingerprint) + + first = pipeline._activity_process_document(_activity_ctx(), raw) + second = pipeline._activity_process_document(_activity_ctx(), raw) + self.assertEqual(first['attempts'], 1) + self.assertEqual(second['attempts'], 2) + + def test_provenance_never_includes_secret_values(self): + work_item = _work_item('doc-1') + source = _FakeSource(content_by_id={'doc-1': b'hello there'}) + vector_store = _FakeVectorStore() + pipeline = _pipeline(source=source, vector_store=vector_store) + raw = self._base_raw(work_item, pipeline_fingerprint=pipeline._pipeline_fingerprint) + + pipeline._activity_process_document(_activity_ctx(), raw) + + for _version, record in vector_store.upserted: + serialized = str(record.metadata) + self.assertNotIn(_SECRET_API_KEY, serialized) + self.assertNotIn(_SECRET_CONNECTION_STRING, serialized) + + +class ActivityValidateVersionTest(unittest.TestCase): + def test_invalid_when_no_documents_were_expected(self): + pipeline = _pipeline() + pipeline._activity_discover_and_manifest( + _activity_ctx(), + { + 'pipeline_id': 'company-knowledge', + 'version': '2026-09', + 'page_size': 10, + 'prefix': None, + }, + ) + result_raw = pipeline._activity_validate_version( + _activity_ctx(), {'pipeline_id': 'company-knowledge', 'version': '2026-09'} + ) + self.assertFalse(result_raw['valid']) + + def test_valid_once_every_document_completed(self): + documents = [ + SourceDocument( + document_id='s3://b/a.txt', + provider=SourceProvider.S3, + uri='s3://b/a.txt', + name='a.txt', + ) + ] + source = _FakeSource(documents=documents, content_by_id={'s3://b/a.txt': b'hello'}) + pipeline = _pipeline(source=source) + pipeline._activity_discover_and_manifest( + _activity_ctx(), + { + 'pipeline_id': 'company-knowledge', + 'version': '2026-09', + 'page_size': 10, + 'prefix': None, + }, + ) + batch = pipeline._activity_get_manifest_batch( + _activity_ctx(), + {'pipeline_id': 'company-knowledge', 'version': '2026-09', 'page_index': 0}, + ) + [item] = batch['items'] + pipeline._activity_process_document( + _activity_ctx(), + { + 'work_item': item, + 'pipeline_id': 'company-knowledge', + 'version': '2026-09', + 'pipeline_fingerprint': pipeline._pipeline_fingerprint, + 'document_ordinal': 0, + }, + ) + + result_raw = pipeline._activity_validate_version( + _activity_ctx(), {'pipeline_id': 'company-knowledge', 'version': '2026-09'} + ) + self.assertTrue(result_raw['valid']) + + +class ActivityActivateVersionTest(unittest.TestCase): + def test_activates_when_nothing_was_active_before(self): + pipeline = _pipeline() + record_raw = pipeline._activity_activate_version( + _activity_ctx(), + {'pipeline_id': 'company-knowledge', 'version': '2026-09', 'manifest_hash': 'h1'}, + ) + self.assertEqual(record_raw['active_version'], '2026-09') + self.assertIsNone(record_raw['previous_version']) + + def test_records_the_previous_version_on_a_later_activation(self): + pipeline = _pipeline() + pipeline._activity_activate_version( + _activity_ctx(), + {'pipeline_id': 'company-knowledge', 'version': '2026-08', 'manifest_hash': 'h1'}, + ) + record_raw = pipeline._activity_activate_version( + _activity_ctx(), + {'pipeline_id': 'company-knowledge', 'version': '2026-09', 'manifest_hash': 'h2'}, + ) + self.assertEqual(record_raw['active_version'], '2026-09') + self.assertEqual(record_raw['previous_version'], '2026-08') + + def test_repeated_activation_of_the_same_version_is_a_no_op(self): + pipeline = _pipeline() + raw = {'pipeline_id': 'company-knowledge', 'version': '2026-09', 'manifest_hash': 'h1'} + first = pipeline._activity_activate_version(_activity_ctx(), raw) + state_size_after_first = len(pipeline._dapr_client._store) + second = pipeline._activity_activate_version(_activity_ctx(), raw) + self.assertEqual(first, second) + self.assertEqual( + len(pipeline._dapr_client._store), state_size_after_first + ) # no extra write + + def test_calls_the_store_native_activation_hook_before_the_dapr_state_write(self): + vector_store = _FakeVectorStore() + pipeline = _pipeline(vector_store=vector_store) + pipeline._activity_activate_version( + _activity_ctx(), + {'pipeline_id': 'company-knowledge', 'version': '2026-09', 'manifest_hash': 'h1'}, + ) + self.assertEqual(vector_store.activate_version_calls, [('2026-09', None)]) + + def test_repeated_activation_does_not_call_the_store_native_hook_again(self): + vector_store = _FakeVectorStore() + pipeline = _pipeline(vector_store=vector_store) + raw = {'pipeline_id': 'company-knowledge', 'version': '2026-09', 'manifest_hash': 'h1'} + pipeline._activity_activate_version(_activity_ctx(), raw) + pipeline._activity_activate_version(_activity_ctx(), raw) + self.assertEqual(len(vector_store.activate_version_calls), 1) + + def test_a_later_activation_passes_the_previous_version_to_the_store(self): + vector_store = _FakeVectorStore() + pipeline = _pipeline(vector_store=vector_store) + pipeline._activity_activate_version( + _activity_ctx(), + {'pipeline_id': 'company-knowledge', 'version': '2026-08', 'manifest_hash': 'h1'}, + ) + pipeline._activity_activate_version( + _activity_ctx(), + {'pipeline_id': 'company-knowledge', 'version': '2026-09', 'manifest_hash': 'h2'}, + ) + self.assertEqual(vector_store.activate_version_calls[-1], ('2026-09', '2026-08')) + + +class ActivityPublishActivationEventTest(unittest.TestCase): + def test_no_op_when_pubsub_is_not_configured(self): + pipeline = _pipeline() + result = pipeline._activity_publish_activation_event( + _activity_ctx(), {'activation_record': {}} + ) + self.assertFalse(result['published']) + + def test_publishes_when_pubsub_is_configured(self): + dapr_client = _FakeDaprClient() + pipeline = _pipeline(dapr_client=dapr_client, pubsub_name='pubsub') + result = pipeline._activity_publish_activation_event( + _activity_ctx(), {'activation_record': {'active_version': '2026-09'}} + ) + self.assertTrue(result['published']) + self.assertEqual(len(dapr_client.published_events), 1) + + def test_a_publish_failure_is_swallowed(self): + dapr_client = mock.Mock() + dapr_client.publish_event.side_effect = RuntimeError('pubsub down') + pipeline = _pipeline(dapr_client=dapr_client, pubsub_name='pubsub') + result = pipeline._activity_publish_activation_event( + _activity_ctx(), {'activation_record': {}} + ) + self.assertFalse(result['published']) + + +class ActivityRegisterFoundryIQKnowledgeSourceTest(unittest.TestCase): + def test_no_op_when_not_configured(self): + vector_store = _FakeVectorStore() + pipeline = _pipeline(vector_store=vector_store) + result = pipeline._activity_register_foundry_iq_knowledge_source( + _activity_ctx(), {'pipeline_id': 'company-knowledge', 'version': '2026-09'} + ) + self.assertFalse(result['registered']) + self.assertEqual(vector_store.foundry_iq_registration_calls, []) + + def test_registers_against_the_vector_store_when_configured(self): + vector_store = _FakeVectorStore() + pipeline = _pipeline( + vector_store=vector_store, + foundry_iq_knowledge_source=FoundryIQKnowledgeSourceConfig( + name='company-knowledge-ks', + description='desc', + source_data_fields=('title',), + search_fields=('content',), + ), + ) + result = pipeline._activity_register_foundry_iq_knowledge_source( + _activity_ctx(), {'pipeline_id': 'company-knowledge', 'version': '2026-09'} + ) + self.assertTrue(result['registered']) + self.assertEqual( + vector_store.foundry_iq_registration_calls, + [ + { + 'version': '2026-09', + 'name': 'company-knowledge-ks', + 'description': 'desc', + 'source_data_fields': ['title'], + 'search_fields': ['content'], + } + ], + ) + + def test_a_registration_failure_is_swallowed(self): + vector_store = _FakeVectorStore() + vector_store.foundry_iq_registration_error = RuntimeError('search down') + pipeline = _pipeline( + vector_store=vector_store, + foundry_iq_knowledge_source=FoundryIQKnowledgeSourceConfig(name='ks'), + ) + result = pipeline._activity_register_foundry_iq_knowledge_source( + _activity_ctx(), {'pipeline_id': 'company-knowledge', 'version': '2026-09'} + ) + self.assertFalse(result['registered']) + + +class OrchestratorIngestionEndToEndTest(unittest.TestCase): + def _documents(self, n): + return [ + SourceDocument( + document_id=f's3://b/{i}.txt', + provider=SourceProvider.S3, + uri=f's3://b/{i}.txt', + name=f'{i}.txt', + ) + for i in range(n) + ] + + def test_completes_validates_and_activates_a_small_run(self): + documents = self._documents(2) + content_by_id = {d.document_id: f'chunk-{d.document_id}'.encode() for d in documents} + vector_store = _FakeVectorStore() + pipeline = _pipeline( + source=_FakeSource(documents=documents, content_by_id=content_by_id), + vector_store=vector_store, + ) + wf_input = to_wire( + _IngestionState( + pipeline_id='company-knowledge', + version='2026-09', + activate_when_complete=True, + fail_fast=False, + page_size=10, + embedding_batch_size=64, + max_activity_attempts=5, + first_retry_interval_seconds=1.0, + backoff_coefficient=2.0, + max_retry_interval_seconds=30.0, + ) + ) + + ctx = _FakeWorkflowContext() + final = _drive_with_real_activities( + pipeline, pipeline._orchestrate_ingestion, ctx, wf_input, _activity_ctx() + ) + + self.assertEqual(final['stage'], PipelineStage.COMPLETED.value) + self.assertTrue(final['validation_succeeded']) + self.assertTrue(final['activation_succeeded']) + self.assertEqual(final['active_version'], '2026-09') + self.assertEqual(pipeline.resolve_active_version(), '2026-09') + self.assertEqual(len(vector_store.upserted), 2) + + def test_registers_the_foundry_iq_knowledge_source_after_activation_when_configured(self): + documents = self._documents(1) + content_by_id = {d.document_id: b'chunk one' for d in documents} + vector_store = _FakeVectorStore() + pipeline = _pipeline( + source=_FakeSource(documents=documents, content_by_id=content_by_id), + vector_store=vector_store, + foundry_iq_knowledge_source=FoundryIQKnowledgeSourceConfig(name='company-knowledge-ks'), + ) + wf_input = to_wire( + _IngestionState( + pipeline_id='company-knowledge', + version='2026-09', + activate_when_complete=True, + fail_fast=False, + page_size=10, + embedding_batch_size=64, + max_activity_attempts=5, + first_retry_interval_seconds=1.0, + backoff_coefficient=2.0, + max_retry_interval_seconds=30.0, + ) + ) + + final = _drive_with_real_activities( + pipeline, + pipeline._orchestrate_ingestion, + _FakeWorkflowContext(), + wf_input, + _activity_ctx(), + ) + + self.assertTrue(final['activation_succeeded']) + self.assertEqual( + vector_store.foundry_iq_registration_calls, + [ + { + 'version': '2026-09', + 'name': 'company-knowledge-ks', + 'description': None, + 'source_data_fields': [], + 'search_fields': [], + } + ], + ) + + def test_does_not_register_a_foundry_iq_knowledge_source_when_not_configured(self): + documents = self._documents(1) + content_by_id = {d.document_id: b'chunk one' for d in documents} + vector_store = _FakeVectorStore() + pipeline = _pipeline( + source=_FakeSource(documents=documents, content_by_id=content_by_id), + vector_store=vector_store, + ) + wf_input = to_wire( + _IngestionState( + pipeline_id='company-knowledge', + version='2026-09', + activate_when_complete=True, + fail_fast=False, + page_size=10, + embedding_batch_size=64, + max_activity_attempts=5, + first_retry_interval_seconds=1.0, + backoff_coefficient=2.0, + max_retry_interval_seconds=30.0, + ) + ) + + _drive_with_real_activities( + pipeline, + pipeline._orchestrate_ingestion, + _FakeWorkflowContext(), + wf_input, + _activity_ctx(), + ) + + self.assertEqual(vector_store.foundry_iq_registration_calls, []) + + def test_fail_fast_false_keeps_processing_after_one_document_fails(self): + documents = self._documents(2) + content_by_id = { + documents[0].document_id: b'ok content', + documents[1].document_id: b'bad content', + } + parser = _FakeParser(fail_for_document_ids={documents[1].document_id}) + pipeline = _pipeline( + source=_FakeSource(documents=documents, content_by_id=content_by_id), + parser=parser, + config=PipelineConfig(fail_fast=False), + ) + wf_input = to_wire( + _IngestionState( + pipeline_id='company-knowledge', + version='2026-09', + activate_when_complete=True, + fail_fast=False, + page_size=10, + embedding_batch_size=64, + max_activity_attempts=5, + first_retry_interval_seconds=1.0, + backoff_coefficient=2.0, + max_retry_interval_seconds=30.0, + ) + ) + + final = _drive_with_real_activities( + pipeline, + pipeline._orchestrate_ingestion, + _FakeWorkflowContext(), + wf_input, + _activity_ctx(), + ) + + # One document failed, so validation (and therefore activation) must not succeed -- + # a partially-built version is never exposed as active. + self.assertFalse(final['validation_succeeded']) + self.assertFalse(final['activation_succeeded']) + self.assertIsNone(pipeline.resolve_active_version()) + self.assertEqual(final['failed_documents'], 1) + self.assertEqual(final['completed_documents'], 1) + + def test_continues_as_new_between_bounded_batches(self): + documents = self._documents(2) + content_by_id = {d.document_id: b'hello' for d in documents} + pipeline = _pipeline( + source=_FakeSource(documents=documents, content_by_id=content_by_id), + config=PipelineConfig(max_concurrent_documents=1), # forces 2 batches for 2 documents + ) + wf_input = to_wire( + _IngestionState( + pipeline_id='company-knowledge', + version='2026-09', + activate_when_complete=False, + fail_fast=False, + page_size=1, + embedding_batch_size=64, + max_activity_attempts=5, + first_retry_interval_seconds=1.0, + backoff_coefficient=2.0, + max_retry_interval_seconds=30.0, + ) + ) + + ctx = _FakeWorkflowContext() + dispatch = _activity_dispatch(pipeline) + final = _drive_one_generation( + dispatch, pipeline._orchestrate_ingestion(ctx, wf_input), _activity_ctx() + ) + + # This first generation processes exactly one bounded batch (one document, + # since max_concurrent_documents=1) and then ends via continue_as_new -- + # it never reaches validation/activation itself. + self.assertIsNone(final) + self.assertIsNotNone(ctx.continue_as_new_input) + self.assertEqual(ctx.continue_as_new_input['cursor'], 1) + self.assertTrue(ctx.continue_as_new_input['manifest_ready']) + + def test_replay_produces_the_identical_activity_call_sequence(self): + """Feeding a fresh generation the exact results a real run produced for it + must yield the exact same (name, input) call sequence -- the replay-safety + guarantee durabletask's history caching depends on: on replay it never + re-invokes an activity, it just feeds the *same* generation the recorded + result for each call in order. + + Deliberately exercises one generation in isolation (not the multi- + generation continue_as_new chain `_drive_with_real_activities` follows): + durabletask history, and therefore replay, belongs to one generation -- + continue_as_new starts a *new* one with fresh history, which is a + different (and separately covered, by + test_continues_as_new_between_bounded_batches) guarantee than replay. + """ + documents = self._documents(2) + content_by_id = {d.document_id: b'hello' for d in documents} + pipeline = _pipeline(source=_FakeSource(documents=documents, content_by_id=content_by_id)) + wf_input = to_wire( + _IngestionState( + pipeline_id='company-knowledge', + version='2026-09', + activate_when_complete=True, + fail_fast=False, + page_size=10, + embedding_batch_size=64, + max_activity_attempts=5, + first_retry_interval_seconds=1.0, + backoff_coefficient=2.0, + max_retry_interval_seconds=30.0, + ) + ) + + # Run for real once, recording this generation's actual results in call order. + real_results: list = [] + first_ctx = _FakeWorkflowContext() + _drive_one_generation( + _recording_dispatch(pipeline, real_results), + pipeline._orchestrate_ingestion(first_ctx, wf_input), + _activity_ctx(), + ) + first_calls = [(c.activity_name, c.input) for c in first_ctx.calls] + + # True replay: a brand new generation, fed only the recorded results above -- + # no activity is actually re-invoked. + second_ctx = _FakeWorkflowContext() + _drive_with_canned_results( + pipeline._orchestrate_ingestion(second_ctx, wf_input), real_results + ) + second_calls = [(c.activity_name, c.input) for c in second_ctx.calls] + + self.assertEqual(first_calls, second_calls) + + +class OrchestratorActivationTest(unittest.TestCase): + def test_raises_when_validation_fails(self): + pipeline = _pipeline() # nothing discovered/processed -> validation will fail + wf_input = to_wire(_ActivationState(pipeline_id='company-knowledge', version='2026-09')) + + from dapr.ext.rag.errors import VersionValidationError + + with self.assertRaises(VersionValidationError): + _drive_with_real_activities( + pipeline, + pipeline._orchestrate_activation, + _FakeWorkflowContext(), + wf_input, + _activity_ctx(), + ) + + def test_activates_a_previously_built_version(self): + documents = [ + SourceDocument( + document_id='s3://b/a.txt', + provider=SourceProvider.S3, + uri='s3://b/a.txt', + name='a.txt', + ) + ] + pipeline = _pipeline( + source=_FakeSource(documents=documents, content_by_id={'s3://b/a.txt': b'hello'}) + ) + # Build the version first, without activating. + ingest_input = to_wire( + _IngestionState( + pipeline_id='company-knowledge', + version='2026-09', + activate_when_complete=False, + fail_fast=False, + page_size=10, + embedding_batch_size=64, + max_activity_attempts=5, + first_retry_interval_seconds=1.0, + backoff_coefficient=2.0, + max_retry_interval_seconds=30.0, + ) + ) + _drive_with_real_activities( + pipeline, + pipeline._orchestrate_ingestion, + _FakeWorkflowContext(), + ingest_input, + _activity_ctx(), + ) + self.assertIsNone(pipeline.resolve_active_version()) + + activate_input = to_wire( + _ActivationState(pipeline_id='company-knowledge', version='2026-09') + ) + _drive_with_real_activities( + pipeline, + pipeline._orchestrate_activation, + _FakeWorkflowContext(), + activate_input, + _activity_ctx(), + ) + + self.assertEqual(pipeline.resolve_active_version(), '2026-09') + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/ext/rag/test_pipeline_integration.py b/tests/ext/rag/test_pipeline_integration.py new file mode 100644 index 000000000..fc338cd58 --- /dev/null +++ b/tests/ext/rag/test_pipeline_integration.py @@ -0,0 +1,549 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +# Real, opt-in end-to-end test of DurableRAGPipeline against live infrastructure. +# +# Unlike every other file in this directory, this one does not mock or fake the source, the +# vector store, or the Dapr sidecar -- it runs a real ingestion through a real S3Source +# (against LocalStack), a real PgVectorStore (against a real PostgreSQL with the `vector` +# extension), and a real Dapr Workflow engine (a real `dapr run` sidecar), then queries the +# result back through ActiveVersionResolver. It only fakes the two adapters that would +# otherwise require a paid third-party API for a unittest-suite-level test: the embedder (a +# tiny deterministic hash-based one) and the parser (bytes decoded as one Document per file, +# skipping unstructured's real format detection). Both of those are already covered thoroughly +# by test_embedding_openai.py/test_embedding_azure_openai.py/test_parsing_unstructured.py +# against mocks -- what isn't covered anywhere else is "does a real Dapr Workflow, backed by a +# real vector store, actually discover, checkpoint, resume, validate, and activate a real run." +# +# Marked e2e (excluded from the default `-m "not e2e"` suite; see root AGENTS.md) and skips +# itself cleanly if its two prerequisites aren't reachable, rather than failing. +# +# Prerequisites (start once, independent of any single test run): +# +# # 1. A local Dapr runtime and Redis (same prerequisite as tests/integration/): +# dapr init +# +# # 2. LocalStack (S3), on its default port: +# docker run -d --rm --name rag-it-localstack -p 4566:4566 -e SERVICES=s3 \ +# localstack/localstack:3 +# +# # 3. PostgreSQL with the pgvector extension pre-installed: +# docker run -d --rm --name rag-it-pgvector -p 5544:5432 \ +# -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=ragtest pgvector/pgvector:pg16 +# +# Then: +# +# uv run pytest tests/ext/rag/test_pipeline_integration.py -m e2e -v +# +# Override RAG_IT_S3_ENDPOINT / RAG_IT_PG_DSN if either service isn't at the default address. +# Each test run uses a fresh, randomly-suffixed bucket/collection name, and tears both down +# afterward -- the containers themselves are not managed by this file (matching how +# tests/integration/ treats dapr_redis as an externally-provisioned prerequisite, not +# something the suite starts itself). +# +# Two different kinds of "crash and resume" are tested here, deliberately not conflated: +# +# - test_a_real_worker_process_crash_mid_run_resumes_from_where_it_left_off spawns the actual +# worker as a *separate OS process* (_crash_resume_worker.py, via subprocess.Popen) so +# FailureInjector's real os._exit() can kill it for real without taking pytest down too, then +# starts a second, fresh worker process and confirms the *same* Dapr Workflow instance +# resumes on its own -- the literal scenario this pipeline exists for. +# - test_a_second_run_of_unchanged_content_skips_every_document instead starts a full *second* +# run of an *already-completed* version and confirms it skips re-embedding -- a different +# (also real, also valuable) proof: that CompletionRecord's idempotency holds across +# independent runs, not only within one resumed instance. + +from __future__ import annotations + +import os +import queue +import subprocess +import sys +import threading +import time +import uuid +from contextlib import contextmanager +from pathlib import Path + +import httpx +import pytest + +from dapr.clients import DaprClient +from dapr.ext.rag.models import PipelineConfig +from dapr.ext.rag.pipeline import DurableRAGPipeline +from dapr.ext.rag.retrieval import ActiveVersionResolver +from dapr.ext.rag.sources.s3 import S3Source +from dapr.ext.rag.splitting import TextSplitter +from dapr.ext.rag.vector_stores.pgvector import PgVectorStore +from tests.ext.rag._rag_integration_fixtures import DeterministicEmbedder, PlainTextParser +from tests.integration.conftest import DaprTestEnvironment +from tests.wait_utils import wait_until + +pytestmark = pytest.mark.e2e + +HOST = '127.0.0.1' +GRPC_PORT = 13701 +HTTP_PORT = 3700 +INTERNAL_GRPC_PORT = 13702 +METRICS_PORT = 9071 + +S3_ENDPOINT = os.environ.get('RAG_IT_S3_ENDPOINT', 'http://127.0.0.1:4566') +PG_DSN = os.environ.get('RAG_IT_PG_DSN', 'postgresql://postgres:postgres@127.0.0.1:5544/ragtest') + +RESOURCES_DIR = Path(__file__).resolve().parent / 'integration_resources' +REPO_ROOT = Path(__file__).resolve().parents[3] +INGEST_TIMEOUT_SECONDS = 60.0 + + +@pytest.fixture(scope='module') +def s3_client(): + """A boto3 S3 client against LocalStack, or a clean `pytest.skip` if unreachable.""" + try: + import boto3 + except ImportError: + pytest.skip('boto3 is not installed (needed for both S3Source and this fixture)') + + try: + httpx.get(S3_ENDPOINT, timeout=2.0) + except httpx.HTTPError as exc: + pytest.skip(f'LocalStack not reachable at {S3_ENDPOINT} ({exc}) -- see module docstring') + + return boto3.client( + 's3', + endpoint_url=S3_ENDPOINT, + region_name='us-east-1', + aws_access_key_id='test', + aws_secret_access_key='test', + ) + + +@pytest.fixture(scope='module') +def pg_dsn(): + """The pgvector Postgres DSN, or a clean `pytest.skip` if unreachable.""" + try: + import psycopg + except ImportError: + pytest.skip('psycopg is not installed (needed for both PgVectorStore and this fixture)') + + try: + with psycopg.connect(PG_DSN, connect_timeout=2): + pass + except Exception as exc: + pytest.skip(f'pgvector Postgres not reachable at {PG_DSN} ({exc}) -- see module docstring') + + return PG_DSN + + +@pytest.fixture(scope='module') +def dapr_env(): + env = DaprTestEnvironment(default_resources=RESOURCES_DIR) + yield env + env.cleanup() + + +@pytest.fixture(scope='module') +def sidecar(dapr_env): + return dapr_env.start_sidecar( + app_id='test-rag-pipeline-integration', + grpc_port=GRPC_PORT, + http_port=HTTP_PORT, + internal_grpc_port=INTERNAL_GRPC_PORT, + metrics_port=METRICS_PORT, + ) + + +@pytest.fixture() +def bucket(s3_client): + name = f'rag-it-{uuid.uuid4().hex[:12]}' + s3_client.create_bucket(Bucket=name) + yield name + objects = s3_client.list_objects_v2(Bucket=name).get('Contents', []) + if objects: + s3_client.delete_objects( + Bucket=name, Delete={'Objects': [{'Key': o['Key']} for o in objects]} + ) + s3_client.delete_bucket(Bucket=name) + + +@pytest.fixture() +def collection(): + return f'rag_it_{uuid.uuid4().hex[:12]}' + + +@pytest.fixture() +def pipeline(sidecar, bucket, collection, pg_dsn): + from dapr.ext.workflow import DaprWorkflowClient, WorkflowRuntime + + # Constructed explicitly (rather than left for DurableRAGPipeline to own) so this fixture + # can point them at `sidecar`'s ports -- which also means `pipeline.close()` won't close + # them (it only closes clients it created itself), so this fixture closes them itself too. + workflow_client = DaprWorkflowClient(host=HOST, port=str(GRPC_PORT)) + dapr_client = DaprClient(address=f'{HOST}:{GRPC_PORT}') + + pipeline = DurableRAGPipeline( + source=S3Source( + bucket=bucket, + region_name='us-east-1', + endpoint_url=S3_ENDPOINT, + aws_access_key_id='test', + aws_secret_access_key='test', + ), + parser=PlainTextParser(), + splitter=TextSplitter(chunk_size=200, chunk_overlap=20), + embedder=DeterministicEmbedder(), + vector_store=PgVectorStore(connection_string=pg_dsn, collection=collection), + state_store_name='statestore', + pipeline_id=collection, + config=PipelineConfig( + max_concurrent_documents=4, embedding_batch_size=8, manifest_page_size=50 + ), + workflow_runtime=WorkflowRuntime(host=HOST, port=str(GRPC_PORT)), + workflow_client=workflow_client, + dapr_client=dapr_client, + ) + pipeline.run_worker() + yield pipeline + pipeline.shutdown_worker() + pipeline.close() + workflow_client.close() + dapr_client.close() + + +@pytest.fixture() +def pipeline_client(sidecar, bucket, collection, pg_dsn): + """A `DurableRAGPipeline` that only ever acts as a *client* (`start`/`get_status`/ + `resolve_active_version`) -- unlike `pipeline` above, `run_worker()` is never called on + this one, so it never executes any activity itself. Used by the real crash/resume test, + where the actual worker(s) run as separate OS processes (`_crash_resume_worker.py`) -- + registering workflows/activities on this instance's own `WorkflowRuntime` is harmless + bookkeeping as long as it's never started, matching how a short-lived CLI process is safe + to construct without a live sidecar per `DurableRAGPipeline.run_worker`'s own docstring. + """ + from dapr.ext.workflow import DaprWorkflowClient, WorkflowRuntime + + workflow_client = DaprWorkflowClient(host=HOST, port=str(GRPC_PORT)) + dapr_client = DaprClient(address=f'{HOST}:{GRPC_PORT}') + + client = DurableRAGPipeline( + source=S3Source( + bucket=bucket, + region_name='us-east-1', + endpoint_url=S3_ENDPOINT, + aws_access_key_id='test', + aws_secret_access_key='test', + ), + parser=PlainTextParser(), + splitter=TextSplitter(chunk_size=200, chunk_overlap=20), + embedder=DeterministicEmbedder(), + vector_store=PgVectorStore(connection_string=pg_dsn, collection=collection), + state_store_name='statestore', + pipeline_id=collection, + config=PipelineConfig(max_concurrent_documents=1), + workflow_runtime=WorkflowRuntime(host=HOST, port=str(GRPC_PORT)), + workflow_client=workflow_client, + dapr_client=dapr_client, + ) + yield client + client.close() + workflow_client.close() + dapr_client.close() + + +@pytest.fixture() +def crash_test_workers(): + """Tracks every worker subprocess a test spawns and force-terminates any still running at + teardown, so a failed assertion mid-test never leaks an orphaned process holding the + sidecar connection or S3/pgvector connections open.""" + procs: list[subprocess.Popen] = [] + yield procs + for proc in procs: + if proc.poll() is None: + proc.terminate() + try: + proc.wait(timeout=10) + except subprocess.TimeoutExpired: + proc.kill() + proc.wait(timeout=10) + + +READY_TIMEOUT_SECONDS = 30.0 + + +def _spawn_crash_test_worker( + *, bucket: str, collection: str, pg_dsn: str, fail_after_documents: int | None = None +) -> subprocess.Popen: + """Starts `_crash_resume_worker.py` as a real, separate OS process and blocks until it + reports readiness -- or raises, with whatever output it produced, if it exits or simply + never reports readiness within `READY_TIMEOUT_SECONDS`. + + Output is read on a background thread into a queue the caller polls with a deadline, + deliberately never a plain blocking `.readline()`/`.read()` on the calling thread: a worker + that starts successfully never closes its stdout (it keeps running to serve work items), so + treating "the first line wasn't the ready marker" as "read the rest of the output to see + why" would block forever once that assumption is wrong -- which it was, once, during this + test's development (real Dapr SDK log output can legitimately precede the ready marker). + Polling a queue against a deadline instead can never block past `READY_TIMEOUT_SECONDS`, no + matter what the child process does. + """ + env = dict(os.environ) + env['RAG_CRASH_TEST_GRPC_PORT'] = str(GRPC_PORT) + env['RAG_CRASH_TEST_HTTP_PORT'] = str(HTTP_PORT) + env['RAG_CRASH_TEST_BUCKET'] = bucket + env['RAG_CRASH_TEST_COLLECTION'] = collection + env['RAG_CRASH_TEST_S3_ENDPOINT'] = S3_ENDPOINT + env['RAG_CRASH_TEST_PG_DSN'] = pg_dsn + if fail_after_documents is not None: + env['RAG_CRASH_TEST_FAIL_AFTER_DOCUMENTS'] = str(fail_after_documents) + else: + env.pop('RAG_CRASH_TEST_FAIL_AFTER_DOCUMENTS', None) + + proc = subprocess.Popen( + [sys.executable, '-m', 'tests.ext.rag._crash_resume_worker'], + cwd=str(REPO_ROOT), + env=env, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) + + output_lines: queue.Queue[str | None] = queue.Queue() + + def _pump_output() -> None: + assert proc.stdout is not None + for line in proc.stdout: + output_lines.put(line) + output_lines.put(None) # sentinel: stdout closed, i.e. the process exited + + threading.Thread(target=_pump_output, daemon=True).start() + + seen: list[str] = [] + deadline = time.monotonic() + READY_TIMEOUT_SECONDS + while time.monotonic() < deadline: + try: + line = output_lines.get(timeout=0.5) + except queue.Empty: + continue + if line is None: + break # stdout closed -- the process exited before ever reporting readiness + seen.append(line) + if 'CRASH_TEST_WORKER_READY' in line: + return proc + + if proc.poll() is None: + proc.kill() + proc.wait(timeout=10) + raise RuntimeError( + f'crash-test worker (pid={proc.pid}) never reported readiness within ' + f'{READY_TIMEOUT_SECONDS}s; output captured so far:\n{"".join(seen)}' + ) + + +def _put_text_object(s3_client, bucket: str, key: str, text: str) -> None: + s3_client.put_object( + Bucket=bucket, Key=key, Body=text.encode('utf-8'), ContentType='text/plain' + ) + + +def _wait_for_terminal_status(pipeline: DurableRAGPipeline, version: str, *, instance_id: str): + """Polls `get_status(version)` for `instance_id`'s own terminal status. + + `PipelineStatus` is keyed by `(pipeline_id, version)`, not by instance ID -- filtering on + `workflow_instance_id` matters whenever a version is run more than once (e.g. the + already-completed status a prior run left behind), or `wait_until` would return + immediately on stale state instead of waiting for *this* run. + """ + + def poll(): + status = pipeline.get_status(version) + is_this_run = status is not None and status.workflow_instance_id == instance_id + return status if is_this_run and status.stage in ('completed', 'failed') else None + + return wait_until(poll, timeout=INGEST_TIMEOUT_SECONDS) + + +@contextmanager +def _resolver_for(collection: str, pg_dsn: str): + # A fresh vector_store/embedder rather than reaching into `pipeline`'s: both are cheap to + # construct and this is exactly how a separate retrieval-side process would do it in + # practice (see docs/rag/README.md's "Starting, observing, and querying a pipeline"). + # A local DaprClient, closed here explicitly: ActiveVersionResolver.close() only closes a + # DaprClient it created itself, not one passed in (same ownership rule as PgVectorStore's + # connection_factory and DurableRAGPipeline's own workflow_client/dapr_client params). + dapr_client = DaprClient(address=f'{HOST}:{GRPC_PORT}') + resolver = ActiveVersionResolver( + pipeline_id=collection, + state_store_name='statestore', + vector_store=PgVectorStore(connection_string=pg_dsn, collection=collection), + embedder=DeterministicEmbedder(), + dapr_client=dapr_client, + ) + try: + yield resolver + finally: + resolver.close() + dapr_client.close() + + +# Plain functions, not a unittest.TestCase: these need pytest fixtures (`bucket`, `pipeline`, +# ...) as parameters, which unittest.TestCase's setUp-based model cannot request. Matches +# `tests/integration/test_workflow_stateful_history.py`'s plain-function + fixture style. + + +def test_ingests_activates_and_is_queryable_end_to_end( + s3_client, bucket, collection, pg_dsn, pipeline +): + _put_text_object( + s3_client, + bucket, + 'policies/remote-work.txt', + 'Employees may work remotely three days per week.', + ) + _put_text_object( + s3_client, + bucket, + 'policies/expenses.txt', + 'Travel expenses must be submitted within 30 days.', + ) + + instance_id = pipeline.start(version='v1') + status = _wait_for_terminal_status(pipeline, 'v1', instance_id=instance_id) + + assert status.stage == 'completed', status + assert status.completed_documents == 2 + assert status.failed_documents == 0 + assert status.activation_succeeded is True + assert pipeline.resolve_active_version() == 'v1' + + with _resolver_for(collection, pg_dsn) as resolver: + matches = resolver.query('remote work policy', top_k=5) + + assert matches, 'expected at least one match from the real pgvector query' + best = matches[0] + assert 'remote' in best.content.lower() + assert best.metadata['pipeline_id'] == collection + assert best.metadata['workflow_instance_id'] == instance_id + assert best.metadata['source_provider'] == 's3' + assert best.metadata['embedding_model'] == 'deterministic-test-embedder-v1' + assert best.metadata['target_version'] == 'v1' + + +def test_a_second_run_of_unchanged_content_skips_every_document( + s3_client, bucket, collection, pipeline +): + """Proves the same `CompletionRecord` short-circuit that makes a real crash-and-restart + avoid re-embedding -- see the module docstring for why this stands in for a literal + process kill here.""" + _put_text_object( + s3_client, bucket, 'policies/pto.txt', 'Unused PTO does not roll over into the next year.' + ) + + first_instance_id = pipeline.start(version='v1') + first_status = _wait_for_terminal_status(pipeline, 'v1', instance_id=first_instance_id) + assert first_status.stage == 'completed' + assert first_status.embedded_chunks > 0 + + second_instance_id = pipeline.start(version='v1', instance_id=f'{collection}-v1-rerun') + assert second_instance_id != first_instance_id + second_status = _wait_for_terminal_status(pipeline, 'v1', instance_id=second_instance_id) + + assert second_status.stage == 'completed' + # A document whose content is unchanged is reported as *skipped*, not completed -- it did no + # new work (see DocumentOutcomeStatus and _activity_update_status's counting). + assert second_status.skipped_documents == 1, second_status + assert second_status.completed_documents == 0, second_status + assert second_status.embedded_chunks == 0, 'unchanged content must not be re-embedded' + assert second_status.reused_chunks > 0, ( + f'a re-run of unchanged content must reuse prior work: {second_status}' + ) + assert second_status.avoided_embedding_units > 0, second_status + + +def test_a_real_worker_process_crash_mid_run_resumes_from_where_it_left_off( + s3_client, bucket, collection, pg_dsn, pipeline_client, crash_test_workers +): + """The literal scenario `DurableRAGPipeline` exists for: a worker process is killed + partway through a run, and a newly started worker process resumes the *same* in-flight + Dapr Workflow instance, finishing only the documents that were not yet durably completed -- + not "a fresh run of already-completed work skips redoing it" (the test above), but "a run + that was interrupted mid-flight continues from exactly where it stopped." + + Ten single-chunk documents; a real worker process configured to hard-exit (os._exit(70), + via FailureInjector) once it starts a 10th document -- i.e. after 9 have already completed. + max_concurrent_documents=1 (see _crash_resume_worker.py) makes this deterministic: documents + are processed strictly one at a time, so "9 completed, crash starting the 10th" is exact, + not a race between concurrently-running activities. + """ + total_documents = 10 + crash_after = 9 + for i in range(total_documents): + _put_text_object( + s3_client, bucket, f'policies/doc-{i}.txt', f'unique content for document number {i}' + ) + + crashing_worker = _spawn_crash_test_worker( + bucket=bucket, collection=collection, pg_dsn=pg_dsn, fail_after_documents=crash_after + ) + crash_test_workers.append(crashing_worker) + + # A separate client connection starts the run -- the worker above executes it. + instance_id = pipeline_client.start(version='v1') + + exit_code = crashing_worker.wait(timeout=INGEST_TIMEOUT_SECONDS) + assert exit_code == 70, f'expected FailureInjector to hard-exit(70); got {exit_code}' + + # Real, partial progress landed durably *before* the crash -- exactly `crash_after` + # documents, no more (deterministic: see the docstring above). + partial_status = pipeline_client.get_status('v1') + assert partial_status is not None + assert partial_status.workflow_instance_id == instance_id + assert partial_status.stage != 'completed', 'the run must still be in-flight, not finished' + assert partial_status.completed_documents == crash_after, partial_status + assert partial_status.embedding_requests == crash_after, partial_status + + # A second, fresh worker process -- no failure injector -- reconnects. Deliberately does + # NOT call pipeline_client.start() again: per DurableRAGPipeline.start()'s own docstring, + # reconnecting a worker to a still-running instance is what resumes it; re-scheduling is + # neither needed nor correct while the instance is still non-terminal. + resumed_worker = _spawn_crash_test_worker(bucket=bucket, collection=collection, pg_dsn=pg_dsn) + crash_test_workers.append(resumed_worker) + + final_status = _wait_for_terminal_status(pipeline_client, 'v1', instance_id=instance_id) + + assert final_status.stage == 'completed', final_status + assert final_status.failed_documents == 0, final_status + assert final_status.completed_documents == total_documents, final_status + # The strongest proof that documents 1-9 were never touched again: exactly one embedding + # batch per document (one chunk each) across *both* worker processes combined -- if any of + # the pre-crash documents had been silently re-embedded after the restart, this would be + # greater than total_documents. + assert final_status.embedding_requests == total_documents, final_status + + # Looked up by exact source_document_id, not text similarity: DeterministicEmbedder hashes + # text into a vector with no semantic relationship to content (unlike a real embedder), so + # asking "which of these 10 near-identical documents is closest to this query" would not + # reliably pick the right one -- an exact metadata match is the correct tool here, and + # confirms each document's own content really did land in pgvector, not just the count. + with _resolver_for(collection, pg_dsn) as resolver: + for i in range(total_documents): + document_id = f's3://{bucket}/policies/doc-{i}.txt' + [match] = resolver.query( + 'irrelevant -- filtered by document id below', + top_k=1, + metadata_filter={'source_document_id': document_id}, + ) + assert match.content == f'unique content for document number {i}' + + resumed_worker.terminate() + resumed_worker.wait(timeout=10) diff --git a/tests/ext/rag/test_retrieval.py b/tests/ext/rag/test_retrieval.py new file mode 100644 index 000000000..985cd0ba6 --- /dev/null +++ b/tests/ext/rag/test_retrieval.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import json +import unittest +from unittest import mock + +from dapr.ext.rag.errors import VersionValidationError +from dapr.ext.rag.models import ActivationRecord, EmbeddingBatchResult, QueryMatch +from dapr.ext.rag.retrieval import ActiveVersionResolver + + +def _state_response(value): + response = mock.Mock() + response.data = None if value is None else json.dumps(value).encode('utf-8') + response.etag = 'etag-1' + return response + + +class ActiveVersionResolverTest(unittest.TestCase): + def setUp(self): + self.dapr_client = mock.Mock() + self.embedder = mock.Mock() + self.vector_store = mock.Mock() + self.resolver = ActiveVersionResolver( + pipeline_id='company-knowledge', + state_store_name='statestore', + vector_store=self.vector_store, + embedder=self.embedder, + dapr_client=self.dapr_client, + ) + + def _activate(self, version, previous_version=None): + record = ActivationRecord( + pipeline_id='company-knowledge', + active_version=version, + previous_version=previous_version, + manifest_hash='hash', + activated_at='2026-09-10T00:00:00+00:00', + workflow_instance_id='wf-1', + ) + self.dapr_client.get_state.return_value = _state_response(record.to_dict()) + + def test_resolve_active_version_returns_none_before_any_activation(self): + self.dapr_client.get_state.return_value = _state_response(None) + self.assertIsNone(self.resolver.resolve_active_version()) + + def test_resolve_active_version_returns_the_active_version(self): + self._activate('2026-09') + self.assertEqual(self.resolver.resolve_active_version(), '2026-09') + + def test_query_raises_when_no_version_has_ever_been_activated(self): + self.dapr_client.get_state.return_value = _state_response(None) + with self.assertRaises(VersionValidationError): + self.resolver.query('what is the policy?') + + def test_query_embeds_the_text_and_searches_only_the_active_version(self): + self._activate('2026-09') + self.embedder.embed_batch.return_value = EmbeddingBatchResult(embeddings=[[0.1, 0.2]]) + expected = [QueryMatch(chunk_id='c1', document_id='doc-1', content='hello', score=0.9)] + self.vector_store.query.return_value = expected + + results = self.resolver.query('what is the policy?', top_k=3) + + self.embedder.embed_batch.assert_called_once_with(['what is the policy?']) + self.vector_store.query.assert_called_once_with( + [0.1, 0.2], '2026-09', top_k=3, metadata_filter=None, query_text='what is the policy?' + ) + self.assertEqual(results, expected) + + def test_query_does_not_search_a_version_still_being_built(self): + # Only ever activated '2026-08'; a '2026-09' build in progress must not be + # visible here even though it may already have vectors written. + self._activate('2026-08') + self.embedder.embed_batch.return_value = EmbeddingBatchResult(embeddings=[[0.1]]) + self.resolver.query('question') + self.assertEqual(self.vector_store.query.call_args.args[1], '2026-08') + + def test_close_closes_the_underlying_state_store(self): + with mock.patch.object(self.resolver, '_state') as mock_state: + self.resolver.close() + mock_state.close.assert_called_once() + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/ext/rag/test_sources_azure_blob.py b/tests/ext/rag/test_sources_azure_blob.py new file mode 100644 index 000000000..2894de8fd --- /dev/null +++ b/tests/ext/rag/test_sources_azure_blob.py @@ -0,0 +1,248 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import datetime +import unittest +from types import SimpleNamespace +from unittest import mock + +from dapr.ext.rag.errors import ( + OptionalDependencyError, + SourceAccessDeniedError, + SourceNotFoundError, + TransientSourceError, +) +from dapr.ext.rag.models import SourceProvider +from dapr.ext.rag.sources.azure_blob import AzureBlobSource + + +class _FakeBlob: + def __init__( + self, name, etag=None, last_modified=None, size=None, content_type=None, deleted=False + ): + self.name = name + self.etag = etag + self.last_modified = last_modified + self.size = size + self.content_settings = SimpleNamespace(content_type=content_type) + self.version_id = None + self.deleted = deleted + + +class _FakeDownloader: + def __init__(self, data): + self._data = data + + def readall(self): + return self._data + + +class _FakeBlobClient: + def __init__(self, properties): + self._properties = properties + + def get_blob_properties(self): + return self._properties + + +class _FakeContainerClient: + def __init__( + self, account_name='myaccount', blobs=(), downloads=None, properties=None, exception=None + ): + self.account_name = account_name + self._blobs = list(blobs) + self._downloads = downloads or {} + self._properties = properties or {} + self._exception = exception + self.list_calls = [] + + def list_blobs(self, name_starts_with=None, results_per_page=None): + self.list_calls.append( + {'name_starts_with': name_starts_with, 'results_per_page': results_per_page} + ) + if self._exception is not None: + raise self._exception + return iter(self._blobs) + + def download_blob(self, blob_name): + if self._exception is not None: + raise self._exception + return _FakeDownloader(self._downloads[blob_name]) + + def get_blob_client(self, blob_name): + if self._exception is not None: + raise self._exception + return _FakeBlobClient(self._properties[blob_name]) + + +def _fake_error(name, status_code=None): + error_cls = type(name, (Exception,), {}) + error = error_cls('boom') + if status_code is not None: + error.status_code = status_code + return error + + +class AzureBlobSourceConstructionTest(unittest.TestCase): + def test_raises_optional_dependency_error_without_azure_sdk_or_client(self): + with mock.patch('dapr.ext.rag.sources.azure_blob.BlobServiceClient', None): + with self.assertRaises(OptionalDependencyError) as ctx: + AzureBlobSource(account_url='https://x.blob.core.windows.net', container='docs') + self.assertEqual(ctx.exception.package, 'azure-storage-blob') + + def test_raises_optional_dependency_error_for_missing_azure_identity(self): + fake_service_client_cls = mock.Mock() + with mock.patch( + 'dapr.ext.rag.sources.azure_blob.BlobServiceClient', fake_service_client_cls + ): + with mock.patch('dapr.ext.rag.sources.azure_blob.DefaultAzureCredential', None): + with self.assertRaises(OptionalDependencyError) as ctx: + AzureBlobSource(account_url='https://x.blob.core.windows.net', container='docs') + self.assertEqual(ctx.exception.package, 'azure-identity') + + def test_requires_account_url_connection_string_or_client(self): + fake_service_client_cls = mock.Mock() + with mock.patch( + 'dapr.ext.rag.sources.azure_blob.BlobServiceClient', fake_service_client_cls + ): + with self.assertRaises(ValueError): + AzureBlobSource(container='docs') + + def test_client_injection_bypasses_the_dependency_check(self): + with mock.patch('dapr.ext.rag.sources.azure_blob.BlobServiceClient', None): + source = AzureBlobSource(container='docs', client=_FakeContainerClient()) + self.assertEqual(source.provider, SourceProvider.AZURE_BLOB) + + +class AzureBlobSourceListDocumentsTest(unittest.TestCase): + def test_normalizes_blobs_into_source_documents(self): + last_modified = datetime.datetime(2026, 9, 1, tzinfo=datetime.timezone.utc) + blobs = [ + _FakeBlob( + 'policies/a.txt', + etag='"etag-a"', + last_modified=last_modified, + size=10, + content_type='text/plain', + ) + ] + source = AzureBlobSource(container='company-docs', client=_FakeContainerClient(blobs=blobs)) + + [document] = list(source.list_documents()) + + self.assertEqual(document.document_id, 'azure-blob://myaccount/company-docs/policies/a.txt') + self.assertEqual(document.uri, document.document_id) + self.assertEqual(document.name, 'policies/a.txt') + self.assertEqual(document.metadata.etag, 'etag-a') + self.assertEqual(document.metadata.content_length, 10) + self.assertEqual(document.metadata.content_type, 'text/plain') + self.assertEqual(document.metadata.last_modified, last_modified.isoformat()) + + def test_skips_soft_deleted_blobs(self): + blobs = [_FakeBlob('a.txt', deleted=True), _FakeBlob('b.txt', deleted=False)] + source = AzureBlobSource(container='docs', client=_FakeContainerClient(blobs=blobs)) + documents = list(source.list_documents()) + self.assertEqual([d.name for d in documents], ['b.txt']) + + def test_prefix_is_forwarded_to_list_blobs(self): + container_client = _FakeContainerClient() + source = AzureBlobSource(container='docs', prefix='configured/', client=container_client) + list(source.list_documents()) + self.assertEqual(container_client.list_calls[0]['name_starts_with'], 'configured/') + + def test_call_level_prefix_overrides_the_configured_one(self): + container_client = _FakeContainerClient() + source = AzureBlobSource(container='docs', prefix='configured/', client=container_client) + list(source.list_documents(prefix='override/')) + self.assertEqual(container_client.list_calls[0]['name_starts_with'], 'override/') + + +class AzureBlobSourceGetDocumentTest(unittest.TestCase): + def test_returns_blob_bytes(self): + source = AzureBlobSource( + container='docs', client=_FakeContainerClient(downloads={'a.txt': b'hello world'}) + ) + content = source.get_document('azure-blob://myaccount/docs/a.txt') + self.assertEqual(content, b'hello world') + + def test_document_id_from_a_different_container_is_not_found(self): + source = AzureBlobSource(container='docs', client=_FakeContainerClient()) + with self.assertRaises(SourceNotFoundError): + source.get_document('azure-blob://myaccount/other-container/a.txt') + + +class AzureBlobSourceGetMetadataTest(unittest.TestCase): + def test_returns_normalized_metadata(self): + last_modified = datetime.datetime(2026, 9, 1, tzinfo=datetime.timezone.utc) + properties = _FakeBlob( + 'a.txt', + etag='"etag-1"', + last_modified=last_modified, + size=42, + content_type='text/plain', + ) + source = AzureBlobSource( + container='docs', client=_FakeContainerClient(properties={'a.txt': properties}) + ) + metadata = source.get_metadata('azure-blob://myaccount/docs/a.txt') + self.assertEqual(metadata.etag, 'etag-1') + self.assertEqual(metadata.content_length, 42) + self.assertEqual(metadata.content_type, 'text/plain') + + +class AzureBlobSourceErrorClassificationTest(unittest.TestCase): + def test_resource_not_found_error_is_source_not_found(self): + source = AzureBlobSource( + container='docs', + client=_FakeContainerClient(exception=_fake_error('ResourceNotFoundError')), + ) + with self.assertRaises(SourceNotFoundError): + source.get_document('azure-blob://myaccount/docs/a.txt') + + def test_client_authentication_error_is_non_retryable(self): + source = AzureBlobSource( + container='docs', + client=_FakeContainerClient(exception=_fake_error('ClientAuthenticationError')), + ) + with self.assertRaises(SourceAccessDeniedError): + source.get_document('azure-blob://myaccount/docs/a.txt') + + def test_service_request_error_is_transient(self): + source = AzureBlobSource( + container='docs', + client=_FakeContainerClient(exception=_fake_error('ServiceRequestError')), + ) + with self.assertRaises(TransientSourceError): + source.get_document('azure-blob://myaccount/docs/a.txt') + + def test_unrecognized_error_with_5xx_status_is_transient(self): + source = AzureBlobSource( + container='docs', + client=_FakeContainerClient(exception=_fake_error('SomeFutureError', status_code=503)), + ) + with self.assertRaises(TransientSourceError): + source.get_document('azure-blob://myaccount/docs/a.txt') + + def test_unrecognized_error_with_403_status_is_non_retryable(self): + source = AzureBlobSource( + container='docs', + client=_FakeContainerClient(exception=_fake_error('SomeFutureError', status_code=403)), + ) + with self.assertRaises(SourceAccessDeniedError): + source.get_document('azure-blob://myaccount/docs/a.txt') + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/ext/rag/test_sources_azure_blob_integration.py b/tests/ext/rag/test_sources_azure_blob_integration.py new file mode 100644 index 000000000..134afaca1 --- /dev/null +++ b/tests/ext/rag/test_sources_azure_blob_integration.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +# Real, opt-in test of AzureBlobSource against a live Azurite container -- not mocks. Unlike +# test_sources_azure_blob.py (which exercises the class's logic against hand-written fakes, so +# it needs neither azure-storage-blob nor a running emulator), this proves the exception-name +# based classification in _classify actually matches what a *real* azure-storage-blob client +# raises against a *real* (if emulated) service -- something a fake, by construction, can't. +# +# Marked e2e (excluded from the default `-m "not e2e"` suite; see root AGENTS.md) and skips +# itself cleanly if Azurite isn't reachable, rather than failing. +# +# Prerequisite (start once, independent of any single test run): +# +# docker run -d --rm --name rag-it-azurite -p 10000:10000 \ +# mcr.microsoft.com/azure-storage/azurite:latest azurite-blob --blobHost 0.0.0.0 --blobPort 10000 +# +# Then: +# +# uv run pytest tests/ext/rag/test_sources_azure_blob_integration.py -m e2e -v +# +# Override RAG_IT_AZURITE_CONNECTION_STRING if Azurite isn't at the default address. Uses +# Azurite's well-known, publicly-documented development account key (not a secret) unless +# overridden. Each test run uses a fresh, randomly-suffixed container name and deletes it +# afterward -- the Azurite container itself is not managed by this file, matching how +# test_pipeline_integration.py treats LocalStack/pgvector. + +from __future__ import annotations + +import os +import uuid + +import pytest + +from dapr.ext.rag.errors import SourceNotFoundError +from dapr.ext.rag.models import SourceProvider +from dapr.ext.rag.sources.azure_blob import AzureBlobSource + +pytestmark = pytest.mark.e2e + +_AZURITE_WELL_KNOWN_CONNECTION_STRING = ( + 'DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlm' + 'EtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:' + '10000/devstoreaccount1;' +) +CONNECTION_STRING = os.environ.get( + 'RAG_IT_AZURITE_CONNECTION_STRING', _AZURITE_WELL_KNOWN_CONNECTION_STRING +) + + +@pytest.fixture(scope='module') +def blob_service_client(): + """A `BlobServiceClient` against Azurite, or a clean `pytest.skip` if unreachable.""" + try: + from azure.storage.blob import BlobServiceClient + except ImportError: + pytest.skip('azure-storage-blob is not installed') + + client = BlobServiceClient.from_connection_string(CONNECTION_STRING) + try: + next(client.list_containers(results_per_page=1).by_page(), None) + except Exception as exc: + pytest.skip( + f'Azurite not reachable via {CONNECTION_STRING!r} ({exc}) -- see module docstring' + ) + + yield client + client.close() + + +@pytest.fixture() +def container(blob_service_client): + name = f'rag-it-{uuid.uuid4().hex[:12]}' + blob_service_client.create_container(name) + yield name + blob_service_client.delete_container(name) + + +@pytest.fixture() +def source(container): + src = AzureBlobSource(connection_string=CONNECTION_STRING, container=container) + yield src + src.close() + + +def test_lists_and_downloads_a_real_blob_with_etag_and_metadata( + blob_service_client, container, source +): + container_client = blob_service_client.get_container_client(container) + container_client.upload_blob( + 'policies/remote-work.txt', + b'Employees may work remotely three days per week.', + content_settings=_content_settings('text/plain'), + ) + + [document] = list(source.list_documents()) + + assert document.provider == SourceProvider.AZURE_BLOB + assert document.name == 'policies/remote-work.txt' + assert document.metadata.etag # Azurite assigns a real ETag; must not be empty/None + assert document.metadata.content_type == 'text/plain' + assert document.metadata.content_length == len( + b'Employees may work remotely three days per week.' + ) + + content = source.get_document(document.document_id) + assert content == b'Employees may work remotely three days per week.' + + metadata = source.get_metadata(document.document_id) + assert metadata.etag == document.metadata.etag + + +def test_get_document_raises_not_found_for_a_real_missing_blob(container, source): + missing_id = f'azure-blob://devstoreaccount1/{container}/does-not-exist.txt' + with pytest.raises(SourceNotFoundError): + source.get_document(missing_id) + + +def test_prefix_restricts_listing_to_matching_blobs(blob_service_client, container, source): + container_client = blob_service_client.get_container_client(container) + container_client.upload_blob('policies/a.txt', b'a') + container_client.upload_blob('other/b.txt', b'b') + + documents = list(source.list_documents(prefix='policies/')) + + assert [d.name for d in documents] == ['policies/a.txt'] + + +def _content_settings(content_type: str): + from azure.storage.blob import ContentSettings + + return ContentSettings(content_type=content_type) diff --git a/tests/ext/rag/test_sources_s3.py b/tests/ext/rag/test_sources_s3.py new file mode 100644 index 000000000..d3579f703 --- /dev/null +++ b/tests/ext/rag/test_sources_s3.py @@ -0,0 +1,245 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import datetime +import unittest +from unittest import mock + +import boto3 +from botocore.stub import Stubber + +from dapr.ext.rag.errors import ( + OptionalDependencyError, + SourceAccessDeniedError, + SourceNotFoundError, + TransientSourceError, +) +from dapr.ext.rag.models import SourceProvider +from dapr.ext.rag.sources.s3 import S3Source + + +def _client_and_stubber(): + client = boto3.client( + 's3', + region_name='us-east-1', + aws_access_key_id='test', + aws_secret_access_key='test', + ) + return client, Stubber(client) + + +class S3SourceConstructionTest(unittest.TestCase): + def test_raises_optional_dependency_error_without_boto3_or_client(self): + with mock.patch('dapr.ext.rag.sources.s3.boto3', None): + with self.assertRaises(OptionalDependencyError): + S3Source(bucket='company-docs') + + def test_client_injection_bypasses_the_dependency_check(self): + client, _stubber = _client_and_stubber() + with mock.patch('dapr.ext.rag.sources.s3.boto3', None): + source = S3Source(bucket='company-docs', client=client) + self.assertEqual(source.provider, SourceProvider.S3) + + +class S3SourceListDocumentsTest(unittest.TestCase): + def test_paginates_across_multiple_pages(self): + client, stubber = _client_and_stubber() + last_modified = datetime.datetime(2026, 9, 1, tzinfo=datetime.timezone.utc) + stubber.add_response( + 'list_objects_v2', + { + 'Contents': [ + { + 'Key': 'policies/a.txt', + 'ETag': '"etag-a"', + 'Size': 10, + 'LastModified': last_modified, + } + ], + 'IsTruncated': True, + 'NextContinuationToken': 'token-1', + }, + {'Bucket': 'company-docs', 'Prefix': 'policies/', 'MaxKeys': 1000}, + ) + stubber.add_response( + 'list_objects_v2', + { + 'Contents': [ + { + 'Key': 'policies/b.txt', + 'ETag': '"etag-b"', + 'Size': 20, + 'LastModified': last_modified, + } + ], + 'IsTruncated': False, + }, + { + 'Bucket': 'company-docs', + 'Prefix': 'policies/', + 'ContinuationToken': 'token-1', + 'MaxKeys': 1000, + }, + ) + with stubber: + source = S3Source(bucket='company-docs', prefix='policies/', client=client) + documents = list(source.list_documents()) + + self.assertEqual([d.name for d in documents], ['policies/a.txt', 'policies/b.txt']) + self.assertEqual(documents[0].document_id, 's3://company-docs/policies/a.txt') + self.assertEqual(documents[0].uri, documents[0].document_id) + self.assertEqual(documents[0].provider, SourceProvider.S3) + + def test_normalizes_etag_and_last_modified(self): + client, stubber = _client_and_stubber() + last_modified = datetime.datetime(2026, 9, 1, 12, 0, tzinfo=datetime.timezone.utc) + stubber.add_response( + 'list_objects_v2', + { + 'Contents': [ + { + 'Key': 'a.txt', + 'ETag': '"quoted-etag"', + 'Size': 5, + 'LastModified': last_modified, + } + ], + 'IsTruncated': False, + }, + {'Bucket': 'bucket', 'MaxKeys': 1000}, + ) + with stubber: + source = S3Source(bucket='bucket', client=client) + [document] = list(source.list_documents()) + + self.assertEqual(document.metadata.etag, 'quoted-etag') # quotes stripped + self.assertEqual(document.metadata.content_length, 5) + self.assertEqual(document.metadata.last_modified, last_modified.isoformat()) + + def test_skips_zero_byte_directory_marker_objects(self): + client, stubber = _client_and_stubber() + stubber.add_response( + 'list_objects_v2', + { + 'Contents': [ + {'Key': 'policies/', 'ETag': '"dir"', 'Size': 0}, + {'Key': 'policies/a.txt', 'ETag': '"a"', 'Size': 5}, + ], + 'IsTruncated': False, + }, + {'Bucket': 'bucket', 'MaxKeys': 1000}, + ) + with stubber: + source = S3Source(bucket='bucket', client=client) + documents = list(source.list_documents()) + + self.assertEqual([d.name for d in documents], ['policies/a.txt']) + + def test_call_level_prefix_overrides_the_configured_one(self): + client, stubber = _client_and_stubber() + stubber.add_response( + 'list_objects_v2', + {'Contents': [], 'IsTruncated': False}, + {'Bucket': 'bucket', 'Prefix': 'override/', 'MaxKeys': 1000}, + ) + with stubber: + source = S3Source(bucket='bucket', prefix='configured/', client=client) + list(source.list_documents(prefix='override/')) + stubber.assert_no_pending_responses() + + +class S3SourceGetDocumentTest(unittest.TestCase): + def test_returns_the_object_body(self): + client, stubber = _client_and_stubber() + body = mock.Mock() + body.read.return_value = b'hello world' + stubber.add_response('get_object', {'Body': body}, {'Bucket': 'bucket', 'Key': 'a.txt'}) + with stubber: + source = S3Source(bucket='bucket', client=client) + content = source.get_document('s3://bucket/a.txt') + self.assertEqual(content, b'hello world') + + def test_document_id_from_a_different_bucket_is_not_found(self): + client, _stubber = _client_and_stubber() + source = S3Source(bucket='bucket', client=client) + with self.assertRaises(SourceNotFoundError): + source.get_document('s3://other-bucket/a.txt') + + +class S3SourceGetMetadataTest(unittest.TestCase): + def test_returns_normalized_metadata(self): + client, stubber = _client_and_stubber() + last_modified = datetime.datetime(2026, 9, 1, tzinfo=datetime.timezone.utc) + stubber.add_response( + 'head_object', + { + 'ETag': '"etag-1"', + 'VersionId': 'v1', + 'ContentLength': 42, + 'ContentType': 'text/plain', + 'LastModified': last_modified, + }, + {'Bucket': 'bucket', 'Key': 'a.txt'}, + ) + with stubber: + source = S3Source(bucket='bucket', client=client) + metadata = source.get_metadata('s3://bucket/a.txt') + + self.assertEqual(metadata.etag, 'etag-1') + self.assertEqual(metadata.version_id, 'v1') + self.assertEqual(metadata.content_length, 42) + self.assertEqual(metadata.content_type, 'text/plain') + + +class S3SourceErrorClassificationTest(unittest.TestCase): + def test_missing_object_is_source_not_found(self): + client, stubber = _client_and_stubber() + stubber.add_client_error('get_object', service_error_code='NoSuchKey', http_status_code=404) + with stubber: + source = S3Source(bucket='bucket', client=client) + with self.assertRaises(SourceNotFoundError): + source.get_document('s3://bucket/a.txt') + + def test_access_denied_is_non_retryable(self): + client, stubber = _client_and_stubber() + stubber.add_client_error( + 'get_object', service_error_code='AccessDenied', http_status_code=403 + ) + with stubber: + source = S3Source(bucket='bucket', client=client) + with self.assertRaises(SourceAccessDeniedError): + source.get_document('s3://bucket/a.txt') + + def test_throttling_is_transient(self): + client, stubber = _client_and_stubber() + stubber.add_client_error('get_object', service_error_code='SlowDown', http_status_code=503) + with stubber: + source = S3Source(bucket='bucket', client=client) + with self.assertRaises(TransientSourceError): + source.get_document('s3://bucket/a.txt') + + def test_unrecognized_5xx_is_transient(self): + client, stubber = _client_and_stubber() + stubber.add_client_error( + 'get_object', service_error_code='SomeNewError', http_status_code=500 + ) + with stubber: + source = S3Source(bucket='bucket', client=client) + with self.assertRaises(TransientSourceError): + source.get_document('s3://bucket/a.txt') + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/ext/rag/test_splitting.py b/tests/ext/rag/test_splitting.py new file mode 100644 index 000000000..c2a0f078f --- /dev/null +++ b/tests/ext/rag/test_splitting.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import string +import unittest + +from dapr.ext.rag.models import Document +from dapr.ext.rag.splitting import TextSplitter + + +class TextSplitterConstructionTest(unittest.TestCase): + def test_rejects_non_positive_chunk_size(self): + with self.assertRaises(ValueError): + TextSplitter(chunk_size=0) + + def test_rejects_negative_overlap(self): + with self.assertRaises(ValueError): + TextSplitter(chunk_size=100, chunk_overlap=-1) + + def test_rejects_overlap_not_smaller_than_chunk_size(self): + with self.assertRaises(ValueError): + TextSplitter(chunk_size=100, chunk_overlap=100) + + +class TextSplitterSplitTest(unittest.TestCase): + def test_short_document_produces_a_single_chunk(self): + splitter = TextSplitter(chunk_size=1000, chunk_overlap=150) + chunks = splitter.split(Document(page_content='hello world')) + self.assertEqual(len(chunks), 1) + self.assertEqual(chunks[0].content, 'hello world') + self.assertEqual(chunks[0].chunk_ordinal, 0) + + def test_long_document_is_split_into_multiple_ordered_chunks(self): + splitter = TextSplitter(chunk_size=50, chunk_overlap=10) + text = '\n\n'.join(f'Paragraph number {i} with some filler text.' for i in range(20)) + chunks = splitter.split(Document(page_content=text)) + self.assertGreater(len(chunks), 1) + self.assertEqual([c.chunk_ordinal for c in chunks], list(range(len(chunks)))) + + def test_no_chunk_exceeds_chunk_size_plus_chunk_overlap(self): + # chunk_size is a target, not always an exact cap once overlap is involved -- + # see splitting.py's _merge_with_overlap comment for the proven hard bound. + splitter = TextSplitter(chunk_size=50, chunk_overlap=10) + text = 'word ' * 500 + chunks = splitter.split(Document(page_content=text)) + for chunk in chunks: + self.assertLessEqual(len(chunk.content), 60) + + def test_consecutive_chunks_share_overlapping_content(self): + # Every character below is unique, so a matching substring can only mean a + # genuine positional overlap -- not a coincidence from a repeated character. + unique_text = string.ascii_lowercase + string.ascii_uppercase + string.digits + splitter = TextSplitter(chunk_size=20, chunk_overlap=5, separators=['']) + chunks = splitter.split(Document(page_content=unique_text)) + self.assertGreaterEqual(len(chunks), 2) + for first, second in zip(chunks, chunks[1:]): + self.assertEqual(first.content[-5:], second.content[:5]) + + def test_splitting_is_deterministic(self): + splitter = TextSplitter(chunk_size=30, chunk_overlap=5) + document = Document(page_content='One two three.\n\nFour five six.\n\nSeven eight nine.') + first = splitter.split(document) + second = splitter.split(document) + self.assertEqual([c.content for c in first], [c.content for c in second]) + + def test_chunk_metadata_carries_forward_document_metadata(self): + splitter = TextSplitter(chunk_size=1000, chunk_overlap=0) + document = Document(page_content='hello', metadata={'page_number': 3}) + chunks = splitter.split(document) + self.assertEqual(chunks[0].metadata, {'page_number': 3}) + + def test_empty_document_produces_no_chunks(self): + splitter = TextSplitter() + self.assertEqual(splitter.split(Document(page_content='')), []) + + def test_config_fingerprint_changes_with_chunk_size(self): + base = TextSplitter(chunk_size=1000, chunk_overlap=150).config_fingerprint() + changed = TextSplitter(chunk_size=500, chunk_overlap=150).config_fingerprint() + self.assertNotEqual(base, changed) + + def test_splitter_type_is_stable(self): + self.assertEqual(TextSplitter().splitter_type, 'text_splitter') + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/ext/rag/test_state.py b/tests/ext/rag/test_state.py new file mode 100644 index 000000000..de5253404 --- /dev/null +++ b/tests/ext/rag/test_state.py @@ -0,0 +1,251 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import json +import unittest +from unittest import mock + +import grpc + +from dapr.ext.rag.errors import ActivationConflictError +from dapr.ext.rag.models import ( + ActivationRecord, + CompletionRecord, + DocumentWorkItem, + EmbedProgressRecord, + PipelineStatus, +) +from dapr.ext.rag.state import PipelineStateStore + + +def _state_response(value, etag=None): + response = mock.Mock() + response.data = None if value is None else json.dumps(value).encode('utf-8') + response.etag = etag + return response + + +class _FakeRpcError(grpc.RpcError): + """A real `grpc.RpcError` instance (Mock can't be `raise`d as one).""" + + def __init__(self, status_code): + super().__init__() + self._status_code = status_code + + def code(self): + return self._status_code + + def details(self): + return 'boom' + + +def _grpc_error(status_code): + return _FakeRpcError(status_code) + + +def _work_items(n): + return [ + DocumentWorkItem( + document_id=f'doc-{i}', provider='s3', uri=f's3://b/doc-{i}', name=f'doc-{i}' + ) + for i in range(n) + ] + + +@mock.patch('dapr.ext.rag.state.DaprClient') +class PipelineStateStoreTest(unittest.TestCase): + def setUp(self): + self.mock_client = mock.Mock() + self.mock_client.get_state.return_value = _state_response(None) + self.store = PipelineStateStore(state_store_name='statestore', dapr_client=self.mock_client) + + # -- manifest --------------------------------------------------------- + + def test_write_manifest_pages_documents_and_writes_a_meta_key(self, _): + documents = _work_items(5) + summary = self.store.write_manifest( + pipeline_id='p1', + version='v1', + documents=documents, + page_size=2, + manifest_hash='hash-1', + created_at='2026-09-10T00:00:00+00:00', + ) + self.assertEqual(summary.total_documents, 5) + self.assertEqual(summary.manifest_hash, 'hash-1') + # 3 pages (2, 2, 1) + 1 meta key + self.assertEqual(self.mock_client.save_state.call_count, 4) + + def test_read_manifest_page_reconstructs_document_work_items(self, _): + documents = _work_items(2) + page_json = [ + { + 'document_id': d.document_id, + 'provider': d.provider, + 'uri': d.uri, + 'name': d.name, + 'source_etag': None, + 'source_version_id': None, + 'source_content_length': None, + } + for d in documents + ] + self.mock_client.get_state.return_value = _state_response(page_json) + result = self.store.read_manifest_page(pipeline_id='p1', version='v1', page_index=0) + self.assertEqual(result, documents) + + def test_read_manifest_meta_returns_none_when_absent(self, _): + self.assertIsNone(self.store.read_manifest_meta(pipeline_id='p1', version='v1')) + + # -- completion --------------------------------------------------------- + + def test_write_then_read_completion_round_trips(self, _): + record = CompletionRecord( + document_id='doc-1', + source_content_hash='h1', + pipeline_fingerprint='fp1', + chunk_count=3, + embedded_chunk_count=3, + completed_at='2026-09-10T00:00:00+00:00', + ) + self.store.write_completion(pipeline_id='p1', version='v1', record=record) + saved_value = self.mock_client.save_state.call_args.kwargs['value'] + self.mock_client.get_state.return_value = _state_response(json.loads(saved_value)) + self.assertEqual( + self.store.read_completion(pipeline_id='p1', version='v1', document_id='doc-1'), record + ) + + def test_read_completion_returns_none_when_absent(self, _): + self.assertIsNone( + self.store.read_completion(pipeline_id='p1', version='v1', document_id='doc-1') + ) + + # -- embed progress ----------------------------------------------------- + + def test_write_then_read_embed_progress_round_trips(self, _): + record = EmbedProgressRecord( + document_id='doc-1', + source_content_hash='h1', + pipeline_fingerprint='fp1', + total_batches=2, + completed_batch_indices=[0], + ) + self.store.write_embed_progress(pipeline_id='p1', version='v1', record=record) + saved_value = self.mock_client.save_state.call_args.kwargs['value'] + self.mock_client.get_state.return_value = _state_response(json.loads(saved_value)) + restored = self.store.read_embed_progress( + pipeline_id='p1', version='v1', document_id='doc-1' + ) + self.assertEqual(restored, record) + + # -- attempt counter ----------------------------------------------------- + + def test_increment_attempt_count_starts_at_one_and_increments(self, _): + first = self.store.increment_attempt_count( + pipeline_id='p1', version='v1', document_id='doc-1' + ) + self.assertEqual(first, 1) + + saved_value = self.mock_client.save_state.call_args.kwargs['value'] + self.mock_client.get_state.return_value = _state_response(json.loads(saved_value)) + second = self.store.increment_attempt_count( + pipeline_id='p1', version='v1', document_id='doc-1' + ) + self.assertEqual(second, 2) + + # -- status --------------------------------------------------------- + + def test_write_then_read_status_round_trips(self, _): + status = PipelineStatus( + pipeline_id='p1', requested_version='v1', workflow_instance_id='wf-1' + ) + self.store.write_status(status) + saved_value = self.mock_client.save_state.call_args.kwargs['value'] + self.mock_client.get_state.return_value = _state_response(json.loads(saved_value)) + self.assertEqual(self.store.read_status(pipeline_id='p1', version='v1'), status) + + # -- activation ----------------------------------------------------- + + def test_read_activation_returns_none_and_the_etag_when_absent(self, _): + self.mock_client.get_state.return_value = _state_response(None, etag='') + record, etag = self.store.read_activation('p1') + self.assertIsNone(record) + self.assertEqual(etag, '') + + def test_read_activation_returns_the_record_and_etag_when_present(self, _): + record = ActivationRecord( + pipeline_id='p1', + active_version='v1', + previous_version=None, + manifest_hash='h1', + activated_at='2026-09-10T00:00:00+00:00', + workflow_instance_id='wf-1', + ) + self.mock_client.get_state.return_value = _state_response(record.to_dict(), etag='etag-1') + restored, etag = self.store.read_activation('p1') + self.assertEqual(restored, record) + self.assertEqual(etag, 'etag-1') + + def test_write_activation_passes_the_etag_through(self, _): + record = ActivationRecord( + pipeline_id='p1', + active_version='v1', + previous_version=None, + manifest_hash='h1', + activated_at='2026-09-10T00:00:00+00:00', + workflow_instance_id='wf-1', + ) + self.store.write_activation(record, etag='etag-1') + self.assertEqual(self.mock_client.save_state.call_args.kwargs['etag'], 'etag-1') + + def test_write_activation_raises_activation_conflict_on_aborted(self, _): + self.mock_client.save_state.side_effect = _grpc_error(grpc.StatusCode.ABORTED) + record = ActivationRecord( + pipeline_id='p1', + active_version='v1', + previous_version=None, + manifest_hash='h1', + activated_at='2026-09-10T00:00:00+00:00', + workflow_instance_id='wf-1', + ) + with self.assertRaises(ActivationConflictError): + self.store.write_activation(record, etag='stale-etag') + + def test_write_activation_reraises_unrelated_grpc_errors(self, _): + self.mock_client.save_state.side_effect = _grpc_error(grpc.StatusCode.UNAVAILABLE) + record = ActivationRecord( + pipeline_id='p1', + active_version='v1', + previous_version=None, + manifest_hash='h1', + activated_at='2026-09-10T00:00:00+00:00', + workflow_instance_id='wf-1', + ) + with self.assertRaises(grpc.RpcError): + self.store.write_activation(record, etag='etag-1') + + # -- lifecycle ----------------------------------------------------- + + def test_close_only_closes_an_owned_client(self, mock_client_cls): + owned_store = PipelineStateStore(state_store_name='statestore') + owned_store.close() + mock_client_cls.return_value.close.assert_called_once() + + self.store.close() + self.mock_client.close.assert_not_called() + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/ext/rag/test_testing.py b/tests/ext/rag/test_testing.py new file mode 100644 index 000000000..804df5122 --- /dev/null +++ b/tests/ext/rag/test_testing.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import unittest +from unittest import mock + +from dapr.ext.rag.testing import FailureInjector + + +class FailureInjectorDefaultsTest(unittest.TestCase): + @mock.patch('dapr.ext.rag.testing.os._exit') + def test_a_default_injector_never_crashes(self, mock_exit): + injector = FailureInjector() + for _ in range(50): + injector.maybe_fail_before_start('doc-1', attempt=1) + injector.maybe_fail_during_embedding('doc-1', batch_index=0) + injector.maybe_fail_after_embedding_before_completion('doc-1', batch_index=0) + mock_exit.assert_not_called() + + +class FailureInjectorFailAfterDocumentsTest(unittest.TestCase): + @mock.patch('dapr.ext.rag.testing.os._exit') + def test_crashes_only_after_the_configured_count_is_exceeded(self, mock_exit): + injector = FailureInjector(fail_after_documents=2) + injector.maybe_fail_before_start('doc-1', attempt=1) + injector.maybe_fail_before_start('doc-2', attempt=1) + mock_exit.assert_not_called() + injector.maybe_fail_before_start('doc-3', attempt=1) + mock_exit.assert_called_once_with(70) + + +class FailureInjectorFailDuringBatchTest(unittest.TestCase): + @mock.patch('dapr.ext.rag.testing.os._exit') + def test_crashes_only_on_the_configured_batch_index(self, mock_exit): + injector = FailureInjector(fail_during_batch_index=1) + injector.maybe_fail_during_embedding('doc-1', batch_index=0) + mock_exit.assert_not_called() + injector.maybe_fail_during_embedding('doc-1', batch_index=1) + mock_exit.assert_called_once() + + +class FailureInjectorFailAfterEmbeddingTest(unittest.TestCase): + @mock.patch('dapr.ext.rag.testing.os._exit') + def test_crashes_only_for_the_configured_document(self, mock_exit): + injector = FailureInjector(fail_after_embedding_before_completion_for_document='doc-2') + injector.maybe_fail_after_embedding_before_completion('doc-1', batch_index=0) + mock_exit.assert_not_called() + injector.maybe_fail_after_embedding_before_completion('doc-2', batch_index=0) + mock_exit.assert_called_once() + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/ext/rag/test_triggers.py b/tests/ext/rag/test_triggers.py new file mode 100644 index 000000000..ab9afc74e --- /dev/null +++ b/tests/ext/rag/test_triggers.py @@ -0,0 +1,283 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import json +import unittest +from unittest import mock + +from dapr.ext.rag.triggers import ( + EventDeduplicator, + parse_azure_blob_event, + parse_s3_event_notifications, + to_source_change_event, +) + + +class ParseS3EventNotificationsTest(unittest.TestCase): + def test_parses_a_single_record(self): + payload = { + 'Records': [ + { + 'eventName': 'ObjectCreated:Put', + 's3': {'bucket': {'name': 'company-docs'}, 'object': {'key': 'policies/a.txt'}}, + } + ] + } + [event] = parse_s3_event_notifications(payload) + self.assertEqual(event.provider, 's3') + self.assertEqual(event.event_type, 'ObjectCreated:Put') + self.assertEqual(event.bucket_or_container, 'company-docs') + self.assertEqual(event.key_or_blob_name, 'policies/a.txt') + + def test_parses_multiple_batched_records(self): + payload = { + 'Records': [ + { + 'eventName': 'ObjectCreated:Put', + 's3': {'bucket': {'name': 'b'}, 'object': {'key': 'a.txt'}}, + }, + { + 'eventName': 'ObjectRemoved:Delete', + 's3': {'bucket': {'name': 'b'}, 'object': {'key': 'b.txt'}}, + }, + ] + } + events = parse_s3_event_notifications(payload) + self.assertEqual( + [e.event_type for e in events], ['ObjectCreated:Put', 'ObjectRemoved:Delete'] + ) + + def test_url_decodes_the_object_key(self): + payload = { + 'Records': [ + { + 'eventName': 'ObjectCreated:Put', + 's3': {'bucket': {'name': 'b'}, 'object': {'key': 'a+b%3D1.txt'}}, + } + ] + } + [event] = parse_s3_event_notifications(payload) + self.assertEqual(event.key_or_blob_name, 'a b=1.txt') + + def test_unrelated_payload_yields_no_events(self): + self.assertEqual(parse_s3_event_notifications({'not': 'an s3 event'}), []) + + def test_derives_a_stable_event_id_from_the_sequencer(self): + payload = { + 'Records': [ + { + 'eventName': 'ObjectCreated:Put', + 's3': { + 'bucket': {'name': 'b'}, + 'object': {'key': 'a.txt', 'sequencer': '0055AED6DCD90281E5'}, + }, + } + ] + } + [event] = parse_s3_event_notifications(payload) + self.assertEqual(event.event_id, '0055AED6DCD90281E5') + + def test_derives_a_deterministic_event_id_without_a_sequencer(self): + payload = { + 'Records': [ + { + 'eventName': 'ObjectCreated:Put', + 's3': {'bucket': {'name': 'b'}, 'object': {'key': 'a.txt'}}, + } + ] + } + first = parse_s3_event_notifications(payload)[0].event_id + second = parse_s3_event_notifications(payload)[0].event_id + self.assertEqual(first, second) + self.assertTrue(first) + + def test_captures_etag_and_version_id(self): + payload = { + 'Records': [ + { + 'eventName': 'ObjectCreated:Put', + 's3': { + 'bucket': {'name': 'b'}, + 'object': {'key': 'a.txt', 'eTag': '"abc123"', 'versionId': 'v1'}, + }, + } + ] + } + [event] = parse_s3_event_notifications(payload) + self.assertEqual(event.etag, 'abc123') + self.assertEqual(event.version_id, 'v1') + + +class ParseAzureBlobEventTest(unittest.TestCase): + def test_parses_a_blob_created_event(self): + payload = { + 'eventType': 'Microsoft.Storage.BlobCreated', + 'subject': '/blobServices/default/containers/company-docs/blobs/policies/a.txt', + } + event = parse_azure_blob_event(payload) + self.assertEqual(event.provider, 'azure-blob') + self.assertEqual(event.event_type, 'Microsoft.Storage.BlobCreated') + self.assertEqual(event.bucket_or_container, 'company-docs') + self.assertEqual(event.key_or_blob_name, 'policies/a.txt') + + def test_returns_none_for_a_subject_without_a_blob_path(self): + payload = {'eventType': 'Microsoft.Storage.BlobCreated', 'subject': '/blobServices/default'} + self.assertIsNone(parse_azure_blob_event(payload)) + + def test_returns_none_for_a_missing_subject(self): + self.assertIsNone(parse_azure_blob_event({'eventType': 'Microsoft.Storage.BlobCreated'})) + + def test_accepts_cloudevents_schema_field_names(self): + payload = { + 'id': 'event-1', + 'type': 'Microsoft.Storage.BlobCreated', + 'time': '2026-09-10T00:00:00Z', + 'subject': '/blobServices/default/containers/company-docs/blobs/a.txt', + 'data': {'etag': '"abc123"'}, + } + event = parse_azure_blob_event(payload) + self.assertEqual(event.event_type, 'Microsoft.Storage.BlobCreated') + self.assertEqual(event.event_id, 'event-1') + self.assertEqual(event.occurred_at, '2026-09-10T00:00:00Z') + self.assertEqual(event.etag, 'abc123') + + def test_captures_event_grid_schema_id_and_etag(self): + payload = { + 'id': 'event-2', + 'eventType': 'Microsoft.Storage.BlobCreated', + 'eventTime': '2026-09-10T00:00:00Z', + 'subject': '/blobServices/default/containers/company-docs/blobs/a.txt', + 'data': {'etag': '"xyz789"'}, + } + event = parse_azure_blob_event(payload) + self.assertEqual(event.event_id, 'event-2') + self.assertEqual(event.etag, 'xyz789') + + +class ToSourceChangeEventTest(unittest.TestCase): + def test_s3_object_created_normalizes_to_created(self): + [notification] = parse_s3_event_notifications( + { + 'Records': [ + { + 'eventName': 'ObjectCreated:Put', + 's3': {'bucket': {'name': 'b'}, 'object': {'key': 'a.txt'}}, + } + ] + } + ) + event = to_source_change_event(notification) + self.assertEqual(event.event_type, 'created') + self.assertEqual(event.provider, 's3') + self.assertEqual(event.source_document_id, 's3://b/a.txt') + self.assertEqual(event.uri, event.source_document_id) + + def test_s3_object_removed_normalizes_to_deleted(self): + [notification] = parse_s3_event_notifications( + { + 'Records': [ + { + 'eventName': 'ObjectRemoved:Delete', + 's3': {'bucket': {'name': 'b'}, 'object': {'key': 'a.txt'}}, + } + ] + } + ) + event = to_source_change_event(notification) + self.assertEqual(event.event_type, 'deleted') + + def test_azure_blob_created_normalizes_to_created(self): + notification = parse_azure_blob_event( + { + 'eventType': 'Microsoft.Storage.BlobCreated', + 'subject': '/blobServices/default/containers/c/blobs/a.txt', + } + ) + event = to_source_change_event(notification) + self.assertEqual(event.event_type, 'created') + self.assertEqual(event.provider, 'azure-blob') + self.assertEqual(event.source_document_id, 'azure-blob://c/a.txt') + + def test_azure_blob_deleted_normalizes_to_deleted(self): + notification = parse_azure_blob_event( + { + 'eventType': 'Microsoft.Storage.BlobDeleted', + 'subject': '/blobServices/default/containers/c/blobs/a.txt', + } + ) + event = to_source_change_event(notification) + self.assertEqual(event.event_type, 'deleted') + + def test_carries_the_event_id_through_for_deduplication(self): + [notification] = parse_s3_event_notifications( + { + 'Records': [ + { + 'eventName': 'ObjectCreated:Put', + 's3': { + 'bucket': {'name': 'b'}, + 'object': {'key': 'a.txt', 'sequencer': 'seq-1'}, + }, + } + ] + } + ) + event = to_source_change_event(notification) + self.assertEqual(event.event_id, 'seq-1') + + +def _state_response(value): + response = mock.Mock() + response.data = None if value is None else json.dumps(value).encode('utf-8') + return response + + +class EventDeduplicatorTest(unittest.TestCase): + def setUp(self): + self.dapr_client = mock.Mock() + self.dapr_client.get_state.return_value = _state_response(None) + self.deduplicator = EventDeduplicator( + state_store_name='statestore', dapr_client=self.dapr_client + ) + + def test_an_unseen_event_is_not_already_seen(self): + self.assertFalse(self.deduplicator.already_seen('event-1')) + + def test_marking_an_event_seen_makes_it_report_as_seen(self): + self.deduplicator.mark_seen('event-1') + self.dapr_client.get_state.return_value = _state_response(1) + self.assertTrue(self.deduplicator.already_seen('event-1')) + + def test_mark_seen_sets_a_ttl(self): + deduplicator = EventDeduplicator( + state_store_name='statestore', dapr_client=self.dapr_client, ttl_seconds=60 + ) + deduplicator.mark_seen('event-1') + self.assertEqual( + self.dapr_client.save_state.call_args.kwargs['state_metadata']['ttlInSeconds'], '60' + ) + + def test_close_only_closes_an_owned_client(self): + self.deduplicator.close() + self.dapr_client.close.assert_not_called() + + with mock.patch('dapr.ext.rag.triggers.DaprClient') as mock_client_cls: + owned = EventDeduplicator(state_store_name='statestore') + owned.close() + mock_client_cls.return_value.close.assert_called_once() + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/ext/rag/test_vector_stores_azure_ai_search.py b/tests/ext/rag/test_vector_stores_azure_ai_search.py new file mode 100644 index 000000000..5a160a85a --- /dev/null +++ b/tests/ext/rag/test_vector_stores_azure_ai_search.py @@ -0,0 +1,486 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import re +import unittest +from types import SimpleNamespace +from typing import Any +from unittest import mock + +from dapr.ext.rag import vector_stores +from dapr.ext.rag.errors import OptionalDependencyError, TransientVectorStoreError, VectorStoreError +from dapr.ext.rag.models import VectorRecord +from dapr.ext.rag.vector_stores.azure_ai_search import AzureAISearchVectorStore + +_HAS_REAL_SDK = vector_stores.azure_ai_search.SimpleField is not None + + +class ResourceNotFoundError(Exception): + """Stands in for `azure.core.exceptions.ResourceNotFoundError` by class *name* + (see azure_ai_search.py's `_is_not_found`) -- no azure-core dependency needed.""" + + +class _FakeIndexingResult: + def __init__(self, key, succeeded): + self.key = key + self.succeeded = succeeded + + +class _FakeSearchClient: + def __init__(self, fail_keys_once=()): + self.documents: dict[str, dict] = {} + self.upload_calls: list[list[dict]] = [] + self._fail_keys_once = set(fail_keys_once) + + def merge_or_upload_documents(self, documents): + self.upload_calls.append(documents) + results = [] + for doc in documents: + key = doc['id'] + if key in self._fail_keys_once: + self._fail_keys_once.discard(key) + results.append(_FakeIndexingResult(key, succeeded=False)) + else: + self.documents[key] = doc + results.append(_FakeIndexingResult(key, succeeded=True)) + return results + + def delete_documents(self, documents): + for doc in documents: + self.documents.pop(doc['id'], None) + + def get_document_count(self): + return len(self.documents) + + def search(self, **kwargs): + rows = [] + filter_expr = kwargs.get('filter') + for doc in self.documents.values(): + if filter_expr and not _matches_filter(doc, filter_expr): + continue + row = dict(doc) + row['@search.score'] = 0.87 + select = kwargs.get('select') + if select: + row = {k: v for k, v in row.items() if k in select or k.startswith('@search.')} + rows.append(row) + top = kwargs.get('top') + return iter(rows[:top] if top else rows) + + def close(self): + pass + + +def _matches_filter(doc, filter_expr): + quoted = re.match(r"(\w+) eq '(.*)'$", filter_expr) + if quoted: + field, value = quoted.groups() + return doc.get(field) == value.replace("''", "'") + return True + + +class _FakeSearchIndexClient: + def __init__(self, existing_index_names=()): + self.indexes: dict[str, Any] = { + name: SimpleNamespace(name=name, fields=[]) for name in existing_index_names + } + self.create_index_calls: list[Any] = [] + + def get_index(self, name): + if name not in self.indexes: + raise ResourceNotFoundError(name) + return self.indexes[name] + + def create_index(self, schema): + self.create_index_calls.append(schema) + self.indexes[schema.name] = schema + + def close(self): + pass + + +class _FakeAliasResponse: + def __init__(self, status_code, body=None): + self.status_code = status_code + self._body = body or {} + self.text = str(self._body) + + def json(self): + return self._body + + +class _FakeAliasTransport: + """Stands in for `httpx.Client`, backed by the same `aliases`/`knowledge_sources` dicts + a real REST-backed Search service would hold -- used for both alias switching and + Foundry IQ knowledge-source registration, matching how one real transport serves both + in `AzureAISearchVectorStore` (see its module comment).""" + + _ALIAS_NAME_PATTERN = re.compile(r"aliases\('([^']+)'\)") + _KNOWLEDGE_SOURCE_NAME_PATTERN = re.compile(r"knowledgesources\('([^']+)'\)") + + def __init__(self, aliases, knowledge_sources=None): + self._aliases = aliases + self._knowledge_sources = knowledge_sources if knowledge_sources is not None else {} + self.get_calls: list[str] = [] + self.put_calls: list[tuple] = [] + + def get(self, url, headers=None): + self.get_calls.append(url) + alias_match = self._ALIAS_NAME_PATTERN.search(url) + if alias_match: + name = alias_match.group(1) + if name not in self._aliases: + return _FakeAliasResponse(404) + return _FakeAliasResponse(200, {'name': name, 'indexes': self._aliases[name]}) + name = self._KNOWLEDGE_SOURCE_NAME_PATTERN.search(url).group(1) + if name not in self._knowledge_sources: + return _FakeAliasResponse(404) + return _FakeAliasResponse(200, self._knowledge_sources[name]) + + def put(self, url, headers=None, json=None): + self.put_calls.append((url, json)) + if self._ALIAS_NAME_PATTERN.search(url): + self._aliases[json['name']] = list(json['indexes']) + return _FakeAliasResponse( + 200, {'name': json['name'], 'indexes': self._aliases[json['name']]} + ) + self._knowledge_sources[json['name']] = json + return _FakeAliasResponse(200, json) + + def close(self): + pass + + +def _store( + index_client=None, search_clients=None, aliases=None, knowledge_sources=None, **overrides +): + index_client = index_client or _FakeSearchIndexClient( + existing_index_names=['company-knowledge-2026-09'] + ) + search_clients = search_clients if search_clients is not None else {} + aliases = aliases if aliases is not None else {} + + def factory(index_name): + return search_clients.setdefault(index_name, _FakeSearchClient()) + + kwargs = dict( + endpoint='https://fake.search.windows.net', + index_base_name='company-knowledge', + index_client=index_client, + search_client_factory=factory, + alias_transport=_FakeAliasTransport(aliases, knowledge_sources), + ) + kwargs.update(overrides) + return AzureAISearchVectorStore(**kwargs), index_client, search_clients + + +def _record(chunk_id='c1', document_id='doc-1', content='hello', **metadata): + return VectorRecord( + chunk_id=chunk_id, + document_id=document_id, + content=content, + embedding=[0.1, 0.2], + metadata=metadata, + ) + + +class AzureAISearchVectorStoreConstructionTest(unittest.TestCase): + def test_raises_optional_dependency_error_without_sdk_or_injection(self): + with mock.patch('dapr.ext.rag.vector_stores.azure_ai_search.SearchIndexClient', None): + with self.assertRaises(OptionalDependencyError): + AzureAISearchVectorStore( + endpoint='https://x.search.windows.net', index_base_name='docs' + ) + + def test_requires_search_client_factory_alongside_index_client(self): + with self.assertRaises(ValueError): + AzureAISearchVectorStore( + endpoint='https://fake.search.windows.net', + index_base_name='docs', + index_client=_FakeSearchIndexClient(), + alias_transport=_FakeAliasTransport({}), + ) + + def test_alias_name_defaults_to_active_suffix(self): + store, _client, _search = _store() + self.assertEqual(store.alias_name, 'company-knowledge-active') + + def test_alias_name_can_be_overridden(self): + store, _client, _search = _store(alias_name='company-knowledge-live') + self.assertEqual(store.alias_name, 'company-knowledge-live') + + def test_store_type_and_target_index_name(self): + store, _client, _search = _store() + self.assertEqual(store.store_type, 'azure-ai-search') + self.assertEqual(store.target_index_name, 'company-knowledge') + + +class AzureAISearchVectorStoreUpsertTest(unittest.TestCase): + def test_upserts_into_the_version_specific_physical_index(self): + store, _client, search_clients = _store() + result = store.upsert([_record('c1')], version='2026-09') + self.assertEqual(result.upserted_count, 1) + self.assertIn('company-knowledge-2026-09', search_clients) + self.assertIn('c1', search_clients['company-knowledge-2026-09'].documents) + + def test_upserting_the_same_chunk_id_twice_is_idempotent(self): + store, _client, search_clients = _store() + store.upsert([_record(content='v1')], version='2026-09') + store.upsert([_record(content='v2')], version='2026-09') + documents = search_clients['company-knowledge-2026-09'].documents + self.assertEqual(len(documents), 1) + self.assertEqual(documents['c1']['content'], 'v2') + + def test_empty_upsert_does_not_touch_the_search_client(self): + store, _client, search_clients = _store() + store.upsert([], version='2026-09') + self.assertEqual(search_clients, {}) + + def test_retries_only_the_batch_members_that_failed(self): + search_client = _FakeSearchClient(fail_keys_once={'c2'}) + store, _client, _search = _store( + search_clients={'company-knowledge-2026-09': search_client} + ) + result = store.upsert([_record('c1'), _record('c2')], version='2026-09') + self.assertEqual(result.upserted_count, 2) + self.assertEqual(len(search_client.upload_calls), 2) + self.assertEqual([doc['id'] for doc in search_client.upload_calls[1]], ['c2']) + + def test_raises_after_exhausting_batch_retries(self): + search_client = _FakeSearchClient() + search_client.merge_or_upload_documents = lambda documents: [ + _FakeIndexingResult(doc['id'], succeeded=False) for doc in documents + ] + store, _client, _search = _store( + search_clients={'company-knowledge-2026-09': search_client}, max_batch_retries=2 + ) + with self.assertRaises(TransientVectorStoreError): + store.upsert([_record('c1')], version='2026-09') + + def test_vector_field_is_not_returned_by_default_and_metadata_is_populated(self): + store, _client, search_clients = _store() + store.upsert( + [ + _record( + 'c1', document_id='doc-1', source_name='a.txt', pipeline_id='company-knowledge' + ) + ], + version='2026-09', + ) + stored = search_clients['company-knowledge-2026-09'].documents['c1'] + self.assertIn('content_vector', stored) # stored in the index... + self.assertEqual(stored['title'], 'a.txt') + self.assertEqual(stored['pipeline_id'], 'company-knowledge') + + +class AzureAISearchVectorStoreDeleteDocumentTest(unittest.TestCase): + def test_deletes_only_matching_chunks(self): + store, _client, search_clients = _store() + store.upsert( + [_record('c1', document_id='doc-1'), _record('c2', document_id='doc-2')], + version='2026-09', + ) + store.delete_document('doc-1', version='2026-09') + documents = search_clients['company-knowledge-2026-09'].documents + self.assertNotIn('c1', documents) + self.assertIn('c2', documents) + + +class AzureAISearchVectorStoreValidateVersionTest(unittest.TestCase): + def test_invalid_when_the_physical_index_does_not_exist(self): + store, _client, _search = _store(index_client=_FakeSearchIndexClient()) + result = store.validate_version('2026-09') + self.assertFalse(result.valid) + self.assertEqual(result.actual_chunk_count, 0) + + def test_valid_once_documents_are_present(self): + store, _client, _search = _store() + store.upsert([_record('c1')], version='2026-09') + result = store.validate_version('2026-09') + self.assertTrue(result.valid) + self.assertEqual(result.actual_chunk_count, 1) + + +class AzureAISearchVectorStoreActivateVersionTest(unittest.TestCase): + """Alias switching goes through `_FakeAliasTransport` (a REST stand-in), never + through `_FakeSearchIndexClient`, so these tests need no real SDK -- see the + module comment in azure_ai_search.py on why aliases are REST-only.""" + + def test_switches_the_alias_to_the_new_physical_index(self): + aliases: dict[str, list[str]] = {} + store, _client, _search = _store(aliases=aliases) + store.activate_version('2026-09', previous_version=None) + self.assertEqual(aliases['company-knowledge-active'], ['company-knowledge-2026-09']) + + def test_repeated_activation_of_the_same_version_does_not_switch_again(self): + aliases: dict[str, list[str]] = {} + index_client = _FakeSearchIndexClient(existing_index_names=['company-knowledge-2026-09']) + alias_transport = _FakeAliasTransport(aliases) + alias_transport.put = mock.Mock(wraps=alias_transport.put) + store, _client, _search = _store(index_client=index_client, alias_transport=alias_transport) + store.activate_version('2026-09', previous_version=None) + store.activate_version('2026-09', previous_version=None) + alias_transport.put.assert_called_once() + + def test_never_deletes_the_previous_index(self): + aliases = {'company-knowledge-active': ['company-knowledge-2026-08']} + index_client = _FakeSearchIndexClient( + existing_index_names=['company-knowledge-2026-08', 'company-knowledge-2026-09'] + ) + store, _client, _search = _store(index_client=index_client, aliases=aliases) + store.activate_version('2026-09', previous_version='2026-08') + self.assertIn('company-knowledge-2026-08', index_client.indexes) + + def test_raises_transient_error_if_the_switch_never_becomes_observable(self): + index_client = _FakeSearchIndexClient(existing_index_names=['company-knowledge-2026-09']) + alias_transport = _FakeAliasTransport({}) + # The PUT itself succeeds, but never becomes visible to the following GETs + # (e.g. eventual consistency) -- so the underlying dict is left untouched. + alias_transport.put = mock.Mock( + return_value=_FakeAliasResponse(200, {'name': 'x', 'indexes': []}) + ) + store, _client, _search = _store( + index_client=index_client, + alias_transport=alias_transport, + alias_poll_attempts=2, + alias_poll_interval_seconds=0.0, + ) + with self.assertRaises(TransientVectorStoreError): + store.activate_version('2026-09', previous_version=None) + + +class AzureAISearchVectorStoreFoundryIQKnowledgeSourceTest(unittest.TestCase): + """Foundry IQ knowledge-source registration is REST-only for the same reason aliases + are -- see azure_ai_search.py's module comment -- so these tests need no real SDK + either, only `_FakeAliasTransport`.""" + + def test_registers_a_new_knowledge_source_against_the_concrete_versioned_index(self): + knowledge_sources: dict = {} + store, _client, _search = _store( + knowledge_sources=knowledge_sources, semantic_configuration_name='company-semantic' + ) + store.register_foundry_iq_knowledge_source('2026-09', name='company-knowledge-ks') + + registered = knowledge_sources['company-knowledge-ks'] + self.assertEqual(registered['kind'], 'searchIndex') + # The concrete physical index, never the alias -- see the method's docstring. + self.assertEqual( + registered['searchIndexParameters']['searchIndexName'], 'company-knowledge-2026-09' + ) + self.assertEqual( + registered['searchIndexParameters']['semanticConfigurationName'], 'company-semantic' + ) + + def test_requires_semantic_configuration_name_to_be_set(self): + store, _client, _search = _store() # no semantic_configuration_name + with self.assertRaises(VectorStoreError): + store.register_foundry_iq_knowledge_source('2026-09', name='company-knowledge-ks') + + def test_re_registering_the_same_version_is_a_no_op(self): + knowledge_sources: dict = {} + alias_transport = _FakeAliasTransport({}, knowledge_sources) + alias_transport.put = mock.Mock(wraps=alias_transport.put) + store, _client, _search = _store( + alias_transport=alias_transport, semantic_configuration_name='company-semantic' + ) + + store.register_foundry_iq_knowledge_source('2026-09', name='company-knowledge-ks') + store.register_foundry_iq_knowledge_source('2026-09', name='company-knowledge-ks') + + alias_transport.put.assert_called_once() + + def test_a_later_version_re_points_the_knowledge_source(self): + knowledge_sources: dict = {} + index_client = _FakeSearchIndexClient( + existing_index_names=['company-knowledge-2026-08', 'company-knowledge-2026-09'] + ) + store, _client, _search = _store( + index_client=index_client, + knowledge_sources=knowledge_sources, + semantic_configuration_name='company-semantic', + ) + + store.register_foundry_iq_knowledge_source('2026-08', name='company-knowledge-ks') + store.register_foundry_iq_knowledge_source('2026-09', name='company-knowledge-ks') + + self.assertEqual( + knowledge_sources['company-knowledge-ks']['searchIndexParameters']['searchIndexName'], + 'company-knowledge-2026-09', + ) + + def test_source_data_fields_and_search_fields_are_passed_through(self): + knowledge_sources: dict = {} + store, _client, _search = _store( + knowledge_sources=knowledge_sources, semantic_configuration_name='company-semantic' + ) + store.register_foundry_iq_knowledge_source( + '2026-09', + name='company-knowledge-ks', + source_data_fields=['title', 'source_uri'], + search_fields=['content'], + ) + + params = knowledge_sources['company-knowledge-ks']['searchIndexParameters'] + self.assertEqual(params['sourceDataFields'], [{'name': 'title'}, {'name': 'source_uri'}]) + self.assertEqual(params['searchFields'], [{'name': 'content'}]) + + +@unittest.skipUnless( + _HAS_REAL_SDK, 'requires azure-search-documents: query() constructs a real VectorizedQuery' +) +class AzureAISearchVectorStoreQueryTest(unittest.TestCase): + def test_hybrid_query_returns_matches_excluding_the_vector_field(self): + store, _client, _search = _store() + store.upsert([_record('c1', document_id='doc-1', content='hello world')], version='2026-09') + [match] = store.query([0.1, 0.2], version='2026-09', query_text='hello', top_k=5) + self.assertEqual(match.chunk_id, 'c1') + self.assertEqual(match.document_id, 'doc-1') + self.assertEqual(match.content, 'hello world') + self.assertNotIn('content_vector', match.metadata) + + def test_metadata_filter_is_applied(self): + store, _client, search_clients = _store() + store.upsert( + [_record('c1', document_id='doc-1'), _record('c2', document_id='doc-2')], + version='2026-09', + ) + matches = store.query( + [0.1], version='2026-09', metadata_filter={'source_document_id': 'doc-1'} + ) + self.assertEqual([m.chunk_id for m in matches], ['c1']) + + +@unittest.skipUnless(_HAS_REAL_SDK, 'requires azure-search-documents for real schema model classes') +class AzureAISearchVectorStoreSchemaCreationTest(unittest.TestCase): + def test_creates_a_new_index_with_a_vector_field_sized_to_the_embedding(self): + index_client = _FakeSearchIndexClient() # no pre-existing indexes + store, _client, _search = _store(index_client=index_client) + store.upsert([_record('c1')], version='2026-09') # embedding is 2-dimensional + [schema] = index_client.create_index_calls + vector_field = next(f for f in schema.fields if f.name == 'content_vector') + self.assertEqual(vector_field.vector_search_dimensions, 2) + + def test_rejects_a_dimension_mismatch_against_an_existing_index(self): + index_client = _FakeSearchIndexClient() + store, _client, _search = _store(index_client=index_client, vector_dimensions=2) + store.upsert([_record('c1')], version='2026-09') # creates with dimensions=2 + + other_store, _client2, _search2 = _store(index_client=index_client, vector_dimensions=1536) + with self.assertRaises(VectorStoreError): + other_store.upsert([_record('c2')], version='2026-09') + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/ext/rag/test_vector_stores_pgvector.py b/tests/ext/rag/test_vector_stores_pgvector.py new file mode 100644 index 000000000..6060fb8fb --- /dev/null +++ b/tests/ext/rag/test_vector_stores_pgvector.py @@ -0,0 +1,246 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import json +import unittest +from unittest import mock + +from dapr.ext.rag.errors import OptionalDependencyError +from dapr.ext.rag.models import VectorRecord +from dapr.ext.rag.vector_stores.pgvector import PgVectorStore + + +class _FakeCursor: + """Interprets just enough of PgVectorStore's own SQL to fake a tiny database. + + This intentionally couples to PgVectorStore's own query shapes (a real + Postgres+pgvector round trip belongs in the optional integration profile, + not here) -- it verifies *this class's* control flow: upsert is + idempotent by (version, chunk_id), schema is created once, and + validate_version/query reflect what was written. + """ + + def __init__(self, tables, executed): + self._tables = tables + self._executed = executed + self._result_one = None + self._result_many = [] + + def execute(self, sql, params=()): + self._executed.append((' '.join(sql.split()), tuple(params))) + normalized = ' '.join(sql.split()) + if normalized.startswith('CREATE TABLE'): + table = normalized.split()[5] + self._tables.setdefault(table, {}) + elif normalized.startswith('INSERT INTO'): + table = normalized.split()[2] + chunk_id, version, document_id, content, _embedding, metadata_json = params + self._tables.setdefault(table, {})[(version, chunk_id)] = { + 'document_id': document_id, + 'content': content, + 'metadata': json.loads(metadata_json), + } + elif normalized.startswith('DELETE FROM'): + table = normalized.split()[2] + version, document_id = params + rows = self._tables.get(table, {}) + for key in [ + k for k, v in rows.items() if k[0] == version and v['document_id'] == document_id + ]: + del rows[key] + elif 'SELECT COUNT(*), COUNT(DISTINCT document_id)' in normalized: + table = normalized.split()[normalized.split().index('FROM') + 1] + (version,) = params + rows = [v for k, v in self._tables.get(table, {}).items() if k[0] == version] + self._result_one = (len(rows), len({r['document_id'] for r in rows})) + elif normalized.startswith('SELECT chunk_id'): + table = normalized.split()[normalized.split().index('FROM') + 1] + # execute()'s params are [vector_literal, version, (metadata_json,) + # vector_literal, top_k] -- see PgVectorStore.query -- so version is + # the second positional parameter, not the first. + version = params[1] + rows = [ + (chunk_id, v['document_id'], v['content'], v['metadata'], 0.99) + for (row_version, chunk_id), v in self._tables.get(table, {}).items() + if row_version == version + ] + self._result_many = rows + + def fetchone(self): + return self._result_one + + def fetchall(self): + return self._result_many + + def __enter__(self): + return self + + def __exit__(self, *exc_info): + return False + + +class _FakeConnection: + def __init__(self, tables, executed): + self._tables = tables + self._executed = executed + self.commits = 0 + self.rollbacks = 0 + + def cursor(self): + return _FakeCursor(self._tables, self._executed) + + def commit(self): + self.commits += 1 + + def rollback(self): + self.rollbacks += 1 + + def __enter__(self): + return self + + def __exit__(self, *exc_info): + return False + + +def _store(tables=None, executed=None): + tables = tables if tables is not None else {} + executed = executed if executed is not None else [] + factory = lambda: _FakeConnection(tables, executed) # noqa: E731 + return ( + PgVectorStore(collection='company_knowledge', connection_factory=factory), + tables, + executed, + ) + + +def _record(chunk_id='chunk-1', document_id='doc-1', content='hello'): + return VectorRecord( + chunk_id=chunk_id, document_id=document_id, content=content, embedding=[0.1, 0.2] + ) + + +class PgVectorStoreConstructionTest(unittest.TestCase): + def test_raises_optional_dependency_error_without_psycopg_or_connection_factory(self): + with mock.patch('dapr.ext.rag.vector_stores.pgvector.psycopg', None): + with self.assertRaises(OptionalDependencyError): + PgVectorStore(connection_string='postgresql://x', collection='docs') + + def test_rejects_an_unsafe_collection_name(self): + with self.assertRaises(ValueError): + PgVectorStore( + collection='not a safe identifier; DROP TABLE x;--', connection_factory=lambda: None + ) + + def test_requires_connection_string_or_factory(self): + with mock.patch('dapr.ext.rag.vector_stores.pgvector.psycopg', mock.Mock()): + with self.assertRaises(ValueError): + PgVectorStore(collection='docs') + + +class PgVectorStoreUpsertTest(unittest.TestCase): + def test_upserting_the_same_chunk_id_twice_is_idempotent(self): + store, tables, _executed = _store() + store.upsert([_record(content='v1')], version='2026-09') + store.upsert([_record(content='v2')], version='2026-09') + + table_rows = next(iter(tables.values())) + self.assertEqual(len(table_rows), 1) + self.assertEqual(table_rows[('2026-09', 'chunk-1')]['content'], 'v2') + + def test_upsert_result_reports_the_record_count(self): + store, _tables, _executed = _store() + result = store.upsert([_record('c1'), _record('c2')], version='2026-09') + self.assertEqual(result.upserted_count, 2) + self.assertEqual(result.version, '2026-09') + + def test_empty_upsert_does_not_touch_the_connection(self): + store, _tables, executed = _store() + store.upsert([], version='2026-09') + self.assertEqual(executed, []) + + def test_schema_is_created_only_once_across_multiple_upserts(self): + store, _tables, executed = _store() + store.upsert([_record('c1')], version='2026-09') + store.upsert([_record('c2')], version='2026-09') + create_table_calls = [sql for sql, _params in executed if sql.startswith('CREATE TABLE')] + self.assertEqual(len(create_table_calls), 1) + + def test_different_versions_are_isolated(self): + store, _tables, _executed = _store() + store.upsert([_record('c1', document_id='doc-1')], version='2026-08') + store.upsert([_record('c1', document_id='doc-1')], version='2026-09') + result_08 = store.validate_version('2026-08') + result_09 = store.validate_version('2026-09') + self.assertEqual(result_08.actual_chunk_count, 1) + self.assertEqual(result_09.actual_chunk_count, 1) + + +class PgVectorStoreDeleteDocumentTest(unittest.TestCase): + def test_deletes_only_chunks_for_the_given_document_and_version(self): + store, _tables, _executed = _store() + store.upsert( + [_record('c1', document_id='doc-1'), _record('c2', document_id='doc-2')], + version='2026-09', + ) + store.delete_document('doc-1', version='2026-09') + result = store.validate_version('2026-09') + self.assertEqual(result.actual_chunk_count, 1) + self.assertEqual(result.actual_document_count, 1) + + +class PgVectorStoreValidateVersionTest(unittest.TestCase): + def test_reports_zero_for_an_empty_version(self): + store, _tables, _executed = _store() + result = store.validate_version('2026-09') + self.assertEqual(result.actual_chunk_count, 0) + self.assertEqual(result.actual_document_count, 0) + self.assertFalse( + result.valid + ) # base VectorIndex-level default; pipeline recomputes `valid` + + def test_reports_accurate_counts(self): + store, _tables, _executed = _store() + store.upsert( + [ + _record('c1', document_id='doc-1'), + _record('c2', document_id='doc-1'), + _record('c3', document_id='doc-2'), + ], + version='2026-09', + ) + result = store.validate_version('2026-09') + self.assertEqual(result.actual_chunk_count, 3) + self.assertEqual(result.actual_document_count, 2) + + +class PgVectorStoreQueryTest(unittest.TestCase): + def test_returns_query_matches_from_the_stored_rows(self): + store, _tables, _executed = _store() + store.upsert([_record('c1', document_id='doc-1', content='hello world')], version='2026-09') + matches = store.query([0.1, 0.2], version='2026-09', top_k=5) + self.assertEqual(len(matches), 1) + self.assertEqual(matches[0].chunk_id, 'c1') + self.assertEqual(matches[0].document_id, 'doc-1') + self.assertEqual(matches[0].content, 'hello world') + + def test_metadata_filter_adds_a_where_clause(self): + store, _tables, executed = _store() + store.query([0.1, 0.2], version='2026-09', metadata_filter={'document_id': 'doc-1'}) + select_calls = [sql for sql, _params in executed if sql.startswith('SELECT chunk_id')] + self.assertIn('metadata @>', select_calls[0]) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/ext/rag/test_vector_stores_pinecone.py b/tests/ext/rag/test_vector_stores_pinecone.py new file mode 100644 index 000000000..ceb1e92f7 --- /dev/null +++ b/tests/ext/rag/test_vector_stores_pinecone.py @@ -0,0 +1,212 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import unittest +from types import SimpleNamespace +from unittest import mock + +from dapr.ext.rag.errors import OptionalDependencyError, TransientVectorStoreError, VectorStoreError +from dapr.ext.rag.models import VectorRecord +from dapr.ext.rag.vector_stores.pinecone import PineconeVectorStore + + +class _FakeIndex: + def __init__(self, describe_exception=None, delete_exception=None): + self.upsert_calls = [] + self.delete_calls = [] + self._namespaces = {} + self._describe_exception = describe_exception + self._delete_exception = delete_exception + + def upsert(self, vectors, namespace): + self.upsert_calls.append({'vectors': vectors, 'namespace': namespace}) + store = self._namespaces.setdefault(namespace, {}) + for vector in vectors: + store[vector['id']] = vector + + def delete(self, namespace, filter=None, ids=None): + self.delete_calls.append({'filter': filter, 'ids': ids, 'namespace': namespace}) + if self._delete_exception is not None: + raise self._delete_exception + store = self._namespaces.get(namespace, {}) + if filter and 'document_id' in filter: + doc_id = filter['document_id']['$eq'] + for vector_id in [ + vid for vid, v in store.items() if v['metadata'].get('document_id') == doc_id + ]: + del store[vector_id] + + def describe_index_stats(self): + if self._describe_exception is not None: + raise self._describe_exception + return SimpleNamespace( + namespaces={ + ns: SimpleNamespace(vector_count=len(vectors)) + for ns, vectors in self._namespaces.items() + } + ) + + def query(self, vector, top_k, namespace, include_metadata, filter=None): + store = self._namespaces.get(namespace, {}) + matches = [ + SimpleNamespace(id=vector_id, score=0.9, metadata=v['metadata']) + for vector_id, v in list(store.items())[:top_k] + ] + return SimpleNamespace(matches=matches) + + +def _record(chunk_id='c1', document_id='doc-1', content='hello'): + return VectorRecord( + chunk_id=chunk_id, document_id=document_id, content=content, embedding=[0.1, 0.2] + ) + + +def _fake_error(name, status=None): + error_cls = type(name, (Exception,), {}) + error = error_cls('boom') + if status is not None: + error.status = status + return error + + +class PineconeVectorStoreConstructionTest(unittest.TestCase): + def test_raises_optional_dependency_error_without_pinecone_or_client(self): + with mock.patch('dapr.ext.rag.vector_stores.pinecone.Pinecone', None): + with self.assertRaises(OptionalDependencyError): + PineconeVectorStore(index_name='company-knowledge') + + def test_client_injection_bypasses_the_dependency_check(self): + with mock.patch('dapr.ext.rag.vector_stores.pinecone.Pinecone', None): + store = PineconeVectorStore(index_name='company-knowledge', client=_FakeIndex()) + self.assertEqual(store.store_type, 'pinecone') + self.assertEqual(store.target_index_name, 'company-knowledge') + + +class PineconeVectorStoreUpsertTest(unittest.TestCase): + def test_batches_upserts_according_to_batch_size(self): + index = _FakeIndex() + store = PineconeVectorStore(index_name='idx', batch_size=2, client=index) + store.upsert([_record('c1'), _record('c2'), _record('c3')], version='2026-09') + self.assertEqual(len(index.upsert_calls), 2) + self.assertEqual(len(index.upsert_calls[0]['vectors']), 2) + self.assertEqual(len(index.upsert_calls[1]['vectors']), 1) + + def test_upserting_the_same_chunk_id_twice_is_idempotent(self): + index = _FakeIndex() + store = PineconeVectorStore(index_name='idx', client=index) + store.upsert([_record(content='v1')], version='2026-09') + store.upsert([_record(content='v2')], version='2026-09') + self.assertEqual(len(index._namespaces['2026-09']), 1) + self.assertEqual(index._namespaces['2026-09']['c1']['metadata']['content'], 'v2') + + def test_vector_metadata_includes_document_id_and_content(self): + index = _FakeIndex() + store = PineconeVectorStore(index_name='idx', client=index) + store.upsert([_record(document_id='doc-1', content='hello')], version='2026-09') + vector = index.upsert_calls[0]['vectors'][0] + self.assertEqual(vector['metadata']['document_id'], 'doc-1') + self.assertEqual(vector['metadata']['content'], 'hello') + + def test_empty_upsert_does_not_call_the_index(self): + index = _FakeIndex() + store = PineconeVectorStore(index_name='idx', client=index) + store.upsert([], version='2026-09') + self.assertEqual(index.upsert_calls, []) + + def test_upsert_uses_version_as_namespace(self): + index = _FakeIndex() + store = PineconeVectorStore(index_name='idx', client=index) + store.upsert([_record()], version='2026-09') + self.assertEqual(index.upsert_calls[0]['namespace'], '2026-09') + + +class PineconeVectorStoreDeleteDocumentTest(unittest.TestCase): + def test_deletes_only_matching_document(self): + index = _FakeIndex() + store = PineconeVectorStore(index_name='idx', client=index) + store.upsert( + [_record('c1', document_id='doc-1'), _record('c2', document_id='doc-2')], version='v1' + ) + store.delete_document('doc-1', version='v1') + self.assertNotIn('c1', index._namespaces['v1']) + self.assertIn('c2', index._namespaces['v1']) + + def test_serverless_limitation_becomes_a_clear_vector_store_error(self): + index = _FakeIndex( + delete_exception=RuntimeError('operation not supported on Serverless index') + ) + store = PineconeVectorStore(index_name='idx', client=index) + with self.assertRaises(VectorStoreError): + store.delete_document('doc-1', version='v1') + + +class PineconeVectorStoreValidateVersionTest(unittest.TestCase): + def test_reports_the_namespace_vector_count(self): + index = _FakeIndex() + store = PineconeVectorStore(index_name='idx', client=index) + store.upsert([_record('c1'), _record('c2')], version='v1') + result = store.validate_version('v1') + self.assertEqual(result.actual_chunk_count, 2) + self.assertTrue(result.valid) + + def test_missing_namespace_reports_zero(self): + store = PineconeVectorStore(index_name='idx', client=_FakeIndex()) + result = store.validate_version('never-built') + self.assertEqual(result.actual_chunk_count, 0) + self.assertFalse(result.valid) + + def test_handles_dict_shaped_stats_response(self): + index = _FakeIndex() + index.describe_index_stats = lambda: {'namespaces': {'v1': {'vector_count': 5}}} + store = PineconeVectorStore(index_name='idx', client=index) + result = store.validate_version('v1') + self.assertEqual(result.actual_chunk_count, 5) + + +class PineconeVectorStoreQueryTest(unittest.TestCase): + def test_extracts_document_id_and_content_from_metadata(self): + index = _FakeIndex() + store = PineconeVectorStore(index_name='idx', client=index) + store.upsert([_record(document_id='doc-1', content='hello world')], version='v1') + [match] = store.query([0.1, 0.2], version='v1', top_k=1) + self.assertEqual(match.chunk_id, 'c1') + self.assertEqual(match.document_id, 'doc-1') + self.assertEqual(match.content, 'hello world') + self.assertNotIn('document_id', match.metadata) # popped out into its own field + self.assertNotIn('content', match.metadata) + + +class PineconeVectorStoreErrorClassificationTest(unittest.TestCase): + def test_named_transient_exception_is_transient(self): + index = _FakeIndex(describe_exception=_fake_error('PineconeApiException')) + store = PineconeVectorStore(index_name='idx', client=index) + with self.assertRaises(TransientVectorStoreError): + store.validate_version('v1') + + def test_5xx_status_is_transient(self): + index = _FakeIndex(describe_exception=_fake_error('SomeError', status=503)) + store = PineconeVectorStore(index_name='idx', client=index) + with self.assertRaises(TransientVectorStoreError): + store.validate_version('v1') + + def test_unrecognized_failure_is_a_plain_vector_store_error(self): + index = _FakeIndex(describe_exception=_fake_error('SomeConfigError')) + store = PineconeVectorStore(index_name='idx', client=index) + with self.assertRaises(VectorStoreError): + store.validate_version('v1') + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/ext/rag/test_wire.py b/tests/ext/rag/test_wire.py new file mode 100644 index 000000000..54c8e8100 --- /dev/null +++ b/tests/ext/rag/test_wire.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- + +""" +Copyright 2026 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import unittest +from dataclasses import dataclass +from types import SimpleNamespace +from typing import Optional + +from dapr.ext.rag._wire import from_wire, to_wire + + +@dataclass(frozen=True, slots=True) +class _FlatExample: + name: str + count: int = 0 + note: Optional[str] = None + + +class ToWireTest(unittest.TestCase): + def test_converts_a_dataclass_instance_to_a_plain_dict(self): + value = _FlatExample(name='a', count=2) + self.assertEqual(to_wire(value), {'name': 'a', 'count': 2, 'note': None}) + + def test_passes_non_dataclass_values_through_unchanged(self): + self.assertEqual(to_wire({'already': 'a dict'}), {'already': 'a dict'}) + self.assertEqual(to_wire('a string'), 'a string') + self.assertIsNone(to_wire(None)) + + def test_does_not_treat_a_dataclass_type_itself_as_an_instance(self): + # dataclasses.is_dataclass(_FlatExample) is True for the *class* too; + # to_wire must only convert instances. + self.assertIs(to_wire(_FlatExample), _FlatExample) + + +class FromWireTest(unittest.TestCase): + def test_reconstructs_from_a_dict(self): + raw = {'name': 'a', 'count': 2, 'note': None} + result = from_wire(raw, _FlatExample) + self.assertEqual(result, _FlatExample(name='a', count=2)) + + def test_reconstructs_from_a_simple_namespace(self): + raw = SimpleNamespace(name='a', count=2, note='hi') + result = from_wire(raw, _FlatExample) + self.assertEqual(result, _FlatExample(name='a', count=2, note='hi')) + + def test_passes_through_an_existing_instance(self): + value = _FlatExample(name='a') + self.assertIs(from_wire(value, _FlatExample), value) + + def test_raises_type_error_for_an_unsupported_shape(self): + with self.assertRaises(TypeError): + from_wire(42, _FlatExample) + + def test_round_trips_through_to_wire(self): + original = _FlatExample(name='round-trip', count=7, note='ok') + self.assertEqual(from_wire(to_wire(original), _FlatExample), original) + + +if __name__ == '__main__': + unittest.main() diff --git a/uv.lock b/uv.lock index 17ae62033..8811c3a92 100644 --- a/uv.lock +++ b/uv.lock @@ -2,11 +2,52 @@ version = 1 revision = 3 requires-python = ">=3.10" resolution-markers = [ - "python_full_version >= '3.15'", + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.14.*' and sys_platform == 'win32'", "python_full_version == '3.14.*' and sys_platform != 'win32'", - "python_full_version < '3.14' and sys_platform == 'win32'", - "python_full_version < '3.14' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version < '3.11' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'win32'", + "python_full_version < '3.11' and sys_platform != 'win32'", +] + +[manifest] +constraints = [{ name = "onnxruntime", marker = "python_full_version < '3.11'", specifier = "<1.24" }] + +[[package]] +name = "accelerate" +version = "1.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub", version = "1.16.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and sys_platform != 'win32'" }, + { name = "huggingface-hub", version = "1.31.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform != 'win32') or (python_full_version < '3.11' and sys_platform == 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*' and sys_platform != 'win32'" }, + { name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and sys_platform != 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, + { name = "packaging", marker = "python_full_version < '3.11' or python_full_version == '3.12.*' or sys_platform != 'win32'" }, + { name = "psutil", marker = "python_full_version < '3.11' or python_full_version == '3.12.*' or sys_platform != 'win32'" }, + { name = "pyyaml", marker = "python_full_version < '3.11' or python_full_version == '3.12.*' or sys_platform != 'win32'" }, + { name = "safetensors", marker = "python_full_version < '3.11' or python_full_version == '3.12.*' or sys_platform != 'win32'" }, + { name = "torch", marker = "python_full_version < '3.11' or python_full_version == '3.12.*' or sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/b5/1d3ed029ac71d3f2961346829a268da923698e9fd63f218f78841f216bfd/accelerate-1.15.0.tar.gz", hash = "sha256:5654f8c5eaa0d4fa68b33e287a97765da6849bf6d51dcac874e73fbbddfb6134", size = 422615, upload-time = "2026-09-09T13:04:49.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/4c/34f0450479d01195027260da68d8a3880683f1640c3ca5adf64acb3185f1/accelerate-1.15.0-py3-none-any.whl", hash = "sha256:97eacca0b73e45cb867dbf8c5d5d4dc32219544300e0c8992c7334dc2ef33cec", size = 394295, upload-time = "2026-09-09T13:04:47.331Z" }, +] + +[[package]] +name = "aiofiles" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/c3/534eac40372d8ee36ef40df62ec129bee4fdb5ad9706e58a29be53b2c970/aiofiles-25.1.0.tar.gz", hash = "sha256:a8d728f0a29de45dc521f18f07297428d56992a742f0cd2701ba86e44d23d5b2", size = 46354, upload-time = "2025-10-09T20:51:04.358Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/8a/340a1555ae33d7354dbca4faa54948d76d89a27ceef032c8c3bc661d003e/aiofiles-25.1.0-py3-none-any.whl", hash = "sha256:abe311e527c862958650f9438e859c1fa7568a141b22abcd015e120e86a85695", size = 14668, upload-time = "2025-10-09T20:51:03.174Z" }, ] [[package]] @@ -186,6 +227,12 @@ 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 = "antlr4-python3-runtime" +version = "4.9.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/38/7859ff46355f76f8d19459005ca000b6e7012f2f1ca597746cbcd1fbfe5e/antlr4-python3-runtime-4.9.3.tar.gz", hash = "sha256:f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b", size = 117034, upload-time = "2021-11-06T17:52:23.524Z" } + [[package]] name = "anyio" version = "4.12.1" @@ -259,6 +306,83 @@ 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 = "azure-common" +version = "1.1.28" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/71/f6f71a276e2e69264a97ad39ef850dca0a04fce67b12570730cb38d0ccac/azure-common-1.1.28.zip", hash = "sha256:4ac0cd3214e36b6a1b6a442686722a5d8cc449603aa833f3f0f40bda836704a3", size = 20914, upload-time = "2022-02-03T19:39:44.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/55/7f118b9c1b23ec15ca05d15a578d8207aa1706bc6f7c87218efffbbf875d/azure_common-1.1.28-py2.py3-none-any.whl", hash = "sha256:5c12d3dcf4ec20599ca6b0d3e09e86e146353d443e7fcc050c9a19c1f9df20ad", size = 14462, upload-time = "2022-02-03T19:39:42.417Z" }, +] + +[[package]] +name = "azure-core" +version = "1.41.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/f3/b416179e408990df5db0d516283022dde0f5d0111d98c1a848e41853e81c/azure_core-1.41.0.tar.gz", hash = "sha256:f46ff5dfcd230f25cf1c19e8a34b8dc08a337b2503e268bb600a16c00db8ad5a", size = 381042, upload-time = "2026-05-07T23:30:54.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/db/325c6d7312d2200251c52323878281045aaffcb5586612296484e4280eaa/azure_core-1.41.0-py3-none-any.whl", hash = "sha256:522b4011e8180b1a3dcd2024396a4e7fe9ac37fb8597db47163d230b5efe892d", size = 220920, upload-time = "2026-05-07T23:30:56.357Z" }, +] + +[[package]] +name = "azure-identity" +version = "1.25.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-core" }, + { name = "cryptography" }, + { name = "msal" }, + { name = "msal-extensions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/0e/3a63efb48aa4a5ae2cfca61ee152fbcb668092134d3eb8bfda472dd5c617/azure_identity-1.25.3.tar.gz", hash = "sha256:ab23c0d63015f50b630ef6c6cf395e7262f439ce06e5d07a64e874c724f8d9e6", size = 286304, upload-time = "2026-03-13T01:12:20.892Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/9a/417b3a533e01953a7c618884df2cb05a71e7b68bdbce4fbdb62349d2a2e8/azure_identity-1.25.3-py3-none-any.whl", hash = "sha256:f4d0b956a8146f30333e071374171f3cfa7bdb8073adb8c3814b65567aa7447c", size = 192138, upload-time = "2026-03-13T01:12:22.951Z" }, +] + +[[package]] +name = "azure-search-documents" +version = "11.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-common" }, + { name = "azure-core" }, + { name = "isodate" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/68/9d59a0bed5fd9581b45444e8abc3ecda97e0466ae0f03affc7cddfb9fa74/azure_search_documents-11.6.0.tar.gz", hash = "sha256:fcc807076ff82024be576ffccb0d0f3261e5c2a112a6666b86ec70bbdb2e1d64", size = 311194, upload-time = "2025-10-09T22:04:03.655Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/4c/d74e5c3ccc0b9ead0e400a2d70ded67554b56a5d799aaa8bf5baaacf4aea/azure_search_documents-11.6.0-py3-none-any.whl", hash = "sha256:c3eb2deaf7926844e99a881830861225ef68e8b3bc067a76019e87fc7f5586dc", size = 307935, upload-time = "2025-10-09T22:04:05.008Z" }, +] + +[[package]] +name = "azure-storage-blob" +version = "12.30.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-core" }, + { name = "cryptography" }, + { name = "isodate" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/7e/834d7bfcf999ab89d1bd3a5d235ece6824686da2f3d315e2162c613fe43d/azure_storage_blob-12.30.1.tar.gz", hash = "sha256:7a24f978c51d56a0375beebffcbe8453e59ae390d2695705848edc75083e4184", size = 624787, upload-time = "2026-08-27T19:12:54.967Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/90/f06915ccf78a6d965901aae093bd7e88e486c52e0773202656fb29c2304a/azure_storage_blob-12.30.1-py3-none-any.whl", hash = "sha256:7dc09c37f4f58508e20532b4b4c178f4763f41b01e0b9063835b994fd9d2a7b3", size = 438131, upload-time = "2026-08-27T19:12:56.796Z" }, +] + +[[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-asyncio-runner" version = "1.2.0" @@ -268,6 +392,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, ] +[[package]] +name = "beautifulsoup4" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/65/318323f98dbee45d42dff61d8f047181bc6f2268a9068cfad035a46be5af/beautifulsoup4-4.15.0.tar.gz", hash = "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7", size = 632571, upload-time = "2026-06-07T16:44:20.453Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl", hash = "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9", size = 109924, upload-time = "2026-06-07T16:44:21.566Z" }, +] + [[package]] name = "blinker" version = "1.9.0" @@ -277,6 +414,53 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, ] +[[package]] +name = "blis" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/d0/d8cc8c9a4488a787e7fa430f6055e5bd1ddb22c340a751d9e901b82e2efe/blis-1.3.3.tar.gz", hash = "sha256:034d4560ff3cc43e8aa37e188451b0440e3261d989bb8a42ceee865607715ecd", size = 2644873, upload-time = "2025-11-17T12:28:30.511Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/db/d80daf6c060618c72acecf026410b806f620cdea62b2e72f3235d7389d05/blis-1.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:650f1d2b28e3c875927c63deebda463a6f9d237dff30e445bfe2127718c1a344", size = 6925724, upload-time = "2025-11-17T12:27:14.23Z" }, + { url = "https://files.pythonhosted.org/packages/06/cd/7ac854c92e33cfccc0eded48e979a9fc26a447952d07a9c7c7da7c1d6eec/blis-1.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9b0d42420ddd543eec51ccb99d38364a0c0833b6895eced37127822de6ecacff", size = 1233606, upload-time = "2025-11-17T12:27:16.107Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ae/ad3165fdbc4ef6afef585686a778c72cd67fb5aa16ab2fd2f4494186705e/blis-1.3.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f0628a030d44aa71cac5973e40c9e95ec767abaaf2fd366a094b9398885f82f2", size = 2769094, upload-time = "2025-11-17T12:27:17.883Z" }, + { url = "https://files.pythonhosted.org/packages/25/d4/7b0820f139b4ea67606d01b59ba6afbee4552ce7b2fd179f2fb7908e294f/blis-1.3.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d0114cf2d8f19e0ed210f9ae92594cd0a12efa1bbbce444028b0fc365bbbb8af", size = 11300520, upload-time = "2025-11-17T12:27:20.058Z" }, + { url = "https://files.pythonhosted.org/packages/85/f3/865a4322bdbeb944744c1908e67fdabecd476613a17204956cff12d568c9/blis-1.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7e88181e9dd8430029ebaf22d41bf79e756e8c95363e9471717102c66beb4a6d", size = 2962083, upload-time = "2025-11-17T12:27:22.098Z" }, + { url = "https://files.pythonhosted.org/packages/65/a2/c2842fa1e2e6bd56eb93e41b34859a9af8b5b63669ee0442bea585d8f607/blis-1.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:62fb8c731347b0f98f5f81d19d339049e61489798738467d156c66cc329b0754", size = 14177001, upload-time = "2025-11-17T12:27:24.345Z" }, + { url = "https://files.pythonhosted.org/packages/b5/9b/3b1532f23db8bdddf3a976e9acf51e8debd94c63be5dafb8ccbab3e62935/blis-1.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:631836d4f335e62c30aa50a1aa0170773265c73654d296361f95180006e88c04", size = 6184429, upload-time = "2025-11-17T12:27:27.054Z" }, + { url = "https://files.pythonhosted.org/packages/a1/0a/a4c8736bc497d386b0ffc76d321f478c03f1a4725e52092f93b38beb3786/blis-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e10c8d3e892b1dbdff365b9d00e08291876fc336915bf1a5e9f188ed087e1a91", size = 6925522, upload-time = "2025-11-17T12:27:29.199Z" }, + { url = "https://files.pythonhosted.org/packages/83/5a/3437009282f23684ecd3963a8b034f9307cdd2bf4484972e5a6b096bf9ac/blis-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66e6249564f1db22e8af1e0513ff64134041fa7e03c8dd73df74db3f4d8415a7", size = 1232787, upload-time = "2025-11-17T12:27:30.996Z" }, + { url = "https://files.pythonhosted.org/packages/d1/0e/82221910d16259ce3017c1442c468a3f206a4143a96fbba9f5b5b81d62e8/blis-1.3.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7260da065958b4e5475f62f44895ef9d673b0f47dcf61b672b22b7dae1a18505", size = 2844596, upload-time = "2025-11-17T12:27:32.601Z" }, + { url = "https://files.pythonhosted.org/packages/6c/93/ab547f1a5c23e20bca16fbcf04021c32aac3f969be737ea4980509a7ca90/blis-1.3.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e9327a6ca67de8ae76fe071e8584cc7f3b2e8bfadece4961d40f2826e1cda2df", size = 11377746, upload-time = "2025-11-17T12:27:35.342Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a6/7733820aa62da32526287a63cd85c103b2b323b186c8ee43b7772ff7017c/blis-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c4ae70629cf302035d268858a10ca4eb6242a01b2dc8d64422f8e6dcb8a8ee74", size = 3041954, upload-time = "2025-11-17T12:27:37.479Z" }, + { url = "https://files.pythonhosted.org/packages/87/53/e39d67fd3296b649772780ca6aab081412838ecb54e0b0c6432d01626a50/blis-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45866a9027d43b93e8b59980a23c5d7358b6536fc04606286e39fdcfce1101c2", size = 14251222, upload-time = "2025-11-17T12:27:39.705Z" }, + { url = "https://files.pythonhosted.org/packages/ea/44/b749f8777b020b420bceaaf60f66432fc30cc904ca5b69640ec9cbef11ed/blis-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:27f82b8633030f8d095d2b412dffa7eb6dbc8ee43813139909a20012e54422ea", size = 6171233, upload-time = "2025-11-17T12:27:41.921Z" }, + { url = "https://files.pythonhosted.org/packages/16/d1/429cf0cf693d4c7dc2efed969bd474e315aab636e4a95f66c4ed7264912d/blis-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2a1c74e100665f8e918ebdbae2794576adf1f691680b5cdb8b29578432f623ef", size = 6929663, upload-time = "2025-11-17T12:27:44.482Z" }, + { url = "https://files.pythonhosted.org/packages/11/69/363c8df8d98b3cc97be19aad6aabb2c9c53f372490d79316bdee92d476e7/blis-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3f6c595185176ce021316263e1a1d636a3425b6c48366c1fd712d08d0b71849a", size = 1230939, upload-time = "2025-11-17T12:27:46.19Z" }, + { url = "https://files.pythonhosted.org/packages/96/2a/fbf65d906d823d839076c5150a6f8eb5ecbc5f9135e0b6510609bda1e6b7/blis-1.3.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d734b19fba0be7944f272dfa7b443b37c61f9476d9ab054a9ac53555ceadd2e0", size = 2818835, upload-time = "2025-11-17T12:27:48.167Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ad/58deaa3ad856dd3cc96493e40ffd2ed043d18d4d304f85a65cde1ccbf644/blis-1.3.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ef6d6e2b599a3a2788eb6d9b443533961265aa4ec49d574ed4bb846e548dcdb", size = 11366550, upload-time = "2025-11-17T12:27:49.958Z" }, + { url = "https://files.pythonhosted.org/packages/78/82/816a7adfe1f7acc8151f01ec86ef64467a3c833932d8f19f8e06613b8a4e/blis-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8c888438ae99c500422d50698e3028b65caa8ebb44e24204d87fda2df64058f7", size = 3023686, upload-time = "2025-11-17T12:27:52.062Z" }, + { url = "https://files.pythonhosted.org/packages/1e/e2/0e93b865f648b5519360846669a35f28ee8f4e1d93d054f6850d8afbabde/blis-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8177879fd3590b5eecdd377f9deafb5dc8af6d684f065bd01553302fb3fcf9a7", size = 14250939, upload-time = "2025-11-17T12:27:53.847Z" }, + { url = "https://files.pythonhosted.org/packages/20/07/fb43edc2ff0a6a367e4a94fc39eb3b85aa1e55e24cc857af2db145ce9f0d/blis-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:f20f7ad69aaffd1ce14fe77de557b6df9b61e0c9e582f75a843715d836b5c8af", size = 6192759, upload-time = "2025-11-17T12:27:56.176Z" }, + { url = "https://files.pythonhosted.org/packages/e6/f7/d26e62d9be3d70473a63e0a5d30bae49c2fe138bebac224adddcdef8a7ce/blis-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1e647341f958421a86b028a2efe16ce19c67dba2a05f79e8f7e80b1ff45328aa", size = 6928322, upload-time = "2025-11-17T12:27:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/4a/78/750d12da388f714958eb2f2fd177652323bbe7ec528365c37129edd6eb84/blis-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d563160f874abb78a57e346f07312c5323f7ad67b6370052b6b17087ef234a8e", size = 1229635, upload-time = "2025-11-17T12:28:00.118Z" }, + { url = "https://files.pythonhosted.org/packages/e8/36/eac4199c5b200a5f3e93cad197da8d26d909f218eb444c4f552647c95240/blis-1.3.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:30b8a5b90cb6cb81d1ada9ae05aa55fb8e70d9a0ae9db40d2401bb9c1c8f14c4", size = 2815650, upload-time = "2025-11-17T12:28:02.544Z" }, + { url = "https://files.pythonhosted.org/packages/bf/51/472e7b36a6bedb5242a9757e7486f702c3619eff76e256735d0c8b1679c6/blis-1.3.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e9f5c53b277f6ac5b3ca30bc12ebab7ea16c8f8c36b14428abb56924213dc127", size = 11359008, upload-time = "2025-11-17T12:28:04.589Z" }, + { url = "https://files.pythonhosted.org/packages/84/da/d0dfb6d6e6321ae44df0321384c32c322bd07b15740d7422727a1a49fc5d/blis-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6297e7616c158b305c9a8a4e47ca5fc9b0785194dd96c903b1a1591a7ca21ddf", size = 3011959, upload-time = "2025-11-17T12:28:06.862Z" }, + { url = "https://files.pythonhosted.org/packages/20/c5/2b0b5e556fa0364ed671051ea078a6d6d7b979b1cfef78d64ad3ca5f0c7f/blis-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3f966ca74f89f8a33e568b9a1d71992fc9a0d29a423e047f0a212643e21b5458", size = 14232456, upload-time = "2025-11-17T12:28:08.779Z" }, + { url = "https://files.pythonhosted.org/packages/31/07/4cdc81a47bf862c0b06d91f1bc6782064e8b69ac9b5d4ff51d97e4ff03da/blis-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:7a0fc4b237a3a453bdc3c7ab48d91439fcd2d013b665c46948d9eaf9c3e45a97", size = 6192624, upload-time = "2025-11-17T12:28:14.197Z" }, + { url = "https://files.pythonhosted.org/packages/5f/8a/80f7c68fbc24a76fc9c18522c46d6d69329c320abb18e26a707a5d874083/blis-1.3.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c3e33cfbf22a418373766816343fcfcd0556012aa3ffdf562c29cddec448a415", size = 6934081, upload-time = "2025-11-17T12:28:16.436Z" }, + { url = "https://files.pythonhosted.org/packages/e5/52/d1aa3a51a7fc299b0c89dcaa971922714f50b1202769eebbdaadd1b5cff7/blis-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6f165930e8d3a85c606d2003211497e28d528c7416fbfeafb6b15600963f7c9b", size = 1231486, upload-time = "2025-11-17T12:28:18.008Z" }, + { url = "https://files.pythonhosted.org/packages/99/4f/badc7bd7f74861b26c10123bba7b9d16f99cd9535ad0128780360713820f/blis-1.3.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:878d4d96d8f2c7a2459024f013f2e4e5f46d708b23437dae970d998e7bff14a0", size = 2814944, upload-time = "2025-11-17T12:28:19.654Z" }, + { url = "https://files.pythonhosted.org/packages/72/a6/f62a3bd814ca19ec7e29ac889fd354adea1217df3183e10217de51e2eb8b/blis-1.3.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f36c0ca84a05ee5d3dbaa38056c4423c1fc29948b17a7923dd2fed8967375d74", size = 11345825, upload-time = "2025-11-17T12:28:21.354Z" }, + { url = "https://files.pythonhosted.org/packages/d4/6c/671af79ee42bc4c968cae35c091ac89e8721c795bfa4639100670dc59139/blis-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e5a662c48cd4aad5dae1a950345df23957524f071315837a4c6feb7d3b288990", size = 3008771, upload-time = "2025-11-17T12:28:23.637Z" }, + { url = "https://files.pythonhosted.org/packages/be/92/7cd7f8490da7c98ee01557f2105885cc597217b0e7fd2eeb9e22cdd4ef23/blis-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9de26fbd72bac900c273b76d46f0b45b77a28eace2e01f6ac6c2239531a413bb", size = 14219213, upload-time = "2025-11-17T12:28:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/0a/de/acae8e9f9a1f4bb393d41c8265898b0f29772e38eac14e9f69d191e2c006/blis-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:9e5fdf4211b1972400f8ff6dafe87cb689c5d84f046b4a76b207c0bd2270faaf", size = 6324695, upload-time = "2025-11-17T12:28:28.401Z" }, +] + [[package]] name = "boto3" version = "1.42.67" @@ -305,6 +489,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fd/0b/cfe18326230476a0b8e3529609190448f2b46453469b07ae95fc57f90fc6/botocore-1.42.67-py3-none-any.whl", hash = "sha256:a94317d2ce83deae230964beb2729639455de65595d0154f285b0ccfd29780cd", size = 14655819, upload-time = "2026-03-12T19:43:21.952Z" }, ] +[[package]] +name = "catalogue" +version = "2.0.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/b4/244d58127e1cdf04cf2dc7d9566f0d24ef01d5ce21811bab088ecc62b5ea/catalogue-2.0.10.tar.gz", hash = "sha256:4f56daa940913d3f09d589c191c74e5a6d51762b3a9e37dd53b7437afd6cda15", size = 19561, upload-time = "2023-09-25T06:29:24.962Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/96/d32b941a501ab566a16358d68b6eb4e4acc373fab3c3c4d7d9e649f7b4bb/catalogue-2.0.10-py3-none-any.whl", hash = "sha256:58c2de0020aa90f4a2da7dfad161bf7b3b054c86a5f09fcedc0b2b740c109a9f", size = 17325, upload-time = "2023-09-25T06:29:23.337Z" }, +] + [[package]] name = "certifi" version = "2026.2.25" @@ -489,14 +682,40 @@ wheels = [ name = "click" version = "8.3.1" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version < '3.11' and sys_platform != 'win32'", +] dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "colorama", marker = "(python_full_version == '3.11.*' and sys_platform == 'win32') or (python_full_version == '3.14.*' and 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 = "click" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version < '3.11' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34", size = 382235, upload-time = "2026-08-26T13:33:14.56Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" }, +] + [[package]] name = "cloudevents" version = "1.13.0" @@ -509,6 +728,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d4/b7/3bc30b00bfdc69f55ff06a2385f379901e5e86116dac7e27cd5e6cbae375/cloudevents-1.13.0-py3-none-any.whl", hash = "sha256:c4b9ea89f1bb6e2090c8de740870d3c96809181ef691843cb672632afcae039b", size = 55772, upload-time = "2026-07-11T18:52:18.742Z" }, ] +[[package]] +name = "cloudpathlib" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/07/9f/1791893f3d51ec36cd9e3f8da0130d278ee909a68bc17c83b3b3de98c91b/cloudpathlib-0.25.0.tar.gz", hash = "sha256:63612e17778c5e3a51b472def8d785d0aaaf347486d6b6786dc7be627556d4c6", size = 56441, upload-time = "2026-08-22T18:41:21.493Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/f9/9084c945d0b3ef8f129b4f0dd21baec759761a337902b70f17a3945015dd/cloudpathlib-0.25.0-py3-none-any.whl", hash = "sha256:8faef3ed3a0dd71d134e8617b4fdc5ce56a12a6b485c080cfe80106e5f1d1f5d", size = 66109, upload-time = "2026-08-22T18:41:20.417Z" }, +] + +[[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" @@ -518,6 +755,190 @@ 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 = "coloredlogs" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "humanfriendly", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/c7/eed8f27100517e8c0e6b923d5f0845d0cb99763da6fdee00478f91db7325/coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0", size = 278520, upload-time = "2021-06-11T10:22:45.202Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018, upload-time = "2021-06-11T10:22:42.561Z" }, +] + +[[package]] +name = "confection" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/65/efd0fe8a936fc8ca2978cb7b82581fb20d901c6039e746a808f746b7647b/confection-1.3.3.tar.gz", hash = "sha256:f0f6810d567ff73993fe74d218ca5e1ffb6a44fb03f391257fc5d033546cbfaa", size = 54895, upload-time = "2026-03-24T18:45:24.331Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/e4/d66708bdf0d92fb4d49b22cdff4b10cec38aca5dcd7e81d909bb55c65cd7/confection-1.3.3-py3-none-any.whl", hash = "sha256:b9fef9ee84b237ef4611ec3eb5797b70e13063e6310ad9f15536373f5e313c82", size = 35902, upload-time = "2026-03-24T18:45:22.664Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'win32'", + "python_full_version < '3.11' and sys_platform != 'win32'", +] +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551, upload-time = "2025-04-15T17:34:46.581Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399, upload-time = "2025-04-15T17:34:51.427Z" }, + { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061, upload-time = "2025-04-15T17:34:55.961Z" }, + { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956, upload-time = "2025-04-15T17:35:00.992Z" }, + { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872, upload-time = "2025-04-15T17:35:06.177Z" }, + { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027, upload-time = "2025-04-15T17:35:11.244Z" }, + { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641, upload-time = "2025-04-15T17:35:26.701Z" }, + { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075, upload-time = "2025-04-15T17:35:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534, upload-time = "2025-04-15T17:35:46.554Z" }, + { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188, upload-time = "2025-04-15T17:35:50.064Z" }, + { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636, upload-time = "2025-04-15T17:35:54.473Z" }, + { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636, upload-time = "2025-04-15T17:35:58.283Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053, upload-time = "2025-04-15T17:36:03.235Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985, upload-time = "2025-04-15T17:36:08.275Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750, upload-time = "2025-04-15T17:36:13.29Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246, upload-time = "2025-04-15T17:36:18.329Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728, upload-time = "2025-04-15T17:36:33.878Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762, upload-time = "2025-04-15T17:36:51.295Z" }, + { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196, upload-time = "2025-04-15T17:36:55.002Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017, upload-time = "2025-04-15T17:36:58.576Z" }, + { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580, upload-time = "2025-04-15T17:37:03.105Z" }, + { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530, upload-time = "2025-04-15T17:37:07.026Z" }, + { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688, upload-time = "2025-04-15T17:37:11.481Z" }, + { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331, upload-time = "2025-04-15T17:37:18.212Z" }, + { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963, upload-time = "2025-04-15T17:37:22.76Z" }, + { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681, upload-time = "2025-04-15T17:37:33.001Z" }, + { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674, upload-time = "2025-04-15T17:37:48.64Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480, upload-time = "2025-04-15T17:38:06.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489, upload-time = "2025-04-15T17:38:10.338Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042, upload-time = "2025-04-15T17:38:14.239Z" }, + { url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630, upload-time = "2025-04-15T17:38:19.142Z" }, + { url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670, upload-time = "2025-04-15T17:38:23.688Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694, upload-time = "2025-04-15T17:38:28.238Z" }, + { url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986, upload-time = "2025-04-15T17:38:33.502Z" }, + { url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060, upload-time = "2025-04-15T17:38:38.672Z" }, + { url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747, upload-time = "2025-04-15T17:38:43.712Z" }, + { url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895, upload-time = "2025-04-15T17:39:00.224Z" }, + { url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098, upload-time = "2025-04-15T17:43:29.649Z" }, + { url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535, upload-time = "2025-04-15T17:44:44.532Z" }, + { url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096, upload-time = "2025-04-15T17:44:48.194Z" }, + { url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090, upload-time = "2025-04-15T17:43:34.084Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643, upload-time = "2025-04-15T17:43:38.626Z" }, + { url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443, upload-time = "2025-04-15T17:43:44.522Z" }, + { url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865, upload-time = "2025-04-15T17:43:49.545Z" }, + { url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162, upload-time = "2025-04-15T17:43:54.203Z" }, + { url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355, upload-time = "2025-04-15T17:44:01.025Z" }, + { url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935, upload-time = "2025-04-15T17:44:17.322Z" }, + { url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168, upload-time = "2025-04-15T17:44:33.43Z" }, + { url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550, upload-time = "2025-04-15T17:44:37.092Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214, upload-time = "2025-04-15T17:44:40.827Z" }, + { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681, upload-time = "2025-04-15T17:44:59.314Z" }, + { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101, upload-time = "2025-04-15T17:45:04.165Z" }, + { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599, upload-time = "2025-04-15T17:45:08.456Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807, upload-time = "2025-04-15T17:45:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729, upload-time = "2025-04-15T17:45:20.166Z" }, + { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791, upload-time = "2025-04-15T17:45:24.794Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'win32'", +] +dependencies = [ + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*' and sys_platform != 'win32'" }, + { name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and sys_platform != 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, + { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, + { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, + { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, + { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, + { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, + { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, + { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, + { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, + { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, + { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, + { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, + { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, + { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, + { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, + { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, + { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, + { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, + { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, + { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, +] + [[package]] name = "coverage" version = "7.15.0" @@ -673,6 +1094,158 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8e/b5/c2c5fce26f0ee40d21bafe7f191d29a34b35a65ac4fe8a1191d1983612e9/cryptography-50.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c99c003e088647b8a5b7c145d6f78c335f6348332b62e142d411c4b63d1460b9", size = 3813796, upload-time = "2026-07-31T14:25:02.298Z" }, ] +[[package]] +name = "cuda-bindings" +version = "13.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-pathfinder", marker = "python_full_version < '3.15' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/9e/684f3ef34af3089a29286817b7deb23f7aa028bad0adbceaa48ad02085af/cuda_bindings-13.4.1-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f29c65d30826c335aa59031e5dfec5ae306a9b259c9cc64d440e9a1ef9e3159", size = 6508095, upload-time = "2026-09-10T01:16:39.465Z" }, + { url = "https://files.pythonhosted.org/packages/20/af/0acdc339cab896dab5da227ec5627be4f2663d4a7f928728ede33b7f4e45/cuda_bindings-13.4.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e4627fedff4fb96b90c19f178ea1c607316eda7f2daa9f8a44973515e2388c8", size = 7155999, upload-time = "2026-09-10T01:16:41.702Z" }, + { url = "https://files.pythonhosted.org/packages/dc/45/57fb37fea9200d854960e36c2181675f92d16bfd7012cc5a95b153e782f0/cuda_bindings-13.4.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56c1552e207b291c321cef9952fbfaf8591c2a14d9c6e5215071020f541a04e5", size = 6484208, upload-time = "2026-09-10T01:16:46.069Z" }, + { url = "https://files.pythonhosted.org/packages/ff/00/15dc1b8f2e5e98975107c366ec5671256d32b8e5c5ed6c1bb3a90a64f035/cuda_bindings-13.4.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e5dc0f13cfd14cd62206fede462f91c497d0c484b8236f4158531d20377066cd", size = 7159324, upload-time = "2026-09-10T01:16:48.109Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d0/76d0e45d98bf4933bf48eac6bbeb17464684540f69edec41fc37c7a422b0/cuda_bindings-13.4.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:84ee88862e2e6ac39a5434c061f7f4389fbefbc418487d0670c86601d517d038", size = 6479976, upload-time = "2026-09-10T01:16:54.622Z" }, + { url = "https://files.pythonhosted.org/packages/43/56/d7b219516980f3333e232c13d727e8f4dc59afc5381cbbcfbc1215014c81/cuda_bindings-13.4.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f444d7e488cbc47e79b7be0d1cfe97201f3e1f186a18ee10f2e4da3265975b16", size = 7170956, upload-time = "2026-09-10T01:16:56.854Z" }, + { url = "https://files.pythonhosted.org/packages/38/cf/165b4d449f94956c2a60930cf5dfeb27132ead60a7e7f2c37819df1cba07/cuda_bindings-13.4.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b601c0cbf0dffb648f68e56a60b320738a20210293f33896a1964a6438cc65f1", size = 6313772, upload-time = "2026-09-10T01:17:03.969Z" }, + { url = "https://files.pythonhosted.org/packages/d9/f9/cf021d1560541caa1f35f3e7e311d2678dbacb4fa6a4573b63470fe1ae00/cuda_bindings-13.4.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e2c357698588b06ebd65811ee2013b6650dd0a10d16d899924aabad0d606d76", size = 6924300, upload-time = "2026-09-10T01:17:06.23Z" }, + { url = "https://files.pythonhosted.org/packages/58/17/74346b49114779920929ec0ea1361f0a0357262d6020cfdd017f670da638/cuda_bindings-13.4.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7df2dddb81feb15787e8c4a13b7aa3f4c23eabeec7746037e94dbbed065fc6c", size = 6406821, upload-time = "2026-09-10T01:17:12.344Z" }, + { url = "https://files.pythonhosted.org/packages/f6/77/2f9a38be7399a34e3703b6ce1be60bb2c56611d324750f8d544ff90b0473/cuda_bindings-13.4.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62df23df11074e9833bf348bbcf0b8eec2fcbded4f305c6fbaa3ed067433e97d", size = 6972525, upload-time = "2026-09-10T01:17:15.098Z" }, + { url = "https://files.pythonhosted.org/packages/7d/de/41197aebf91f6c5f82b35e06e3b4bbe08edddb335c4d6e53c1f1fe542e0f/cuda_bindings-13.4.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0947a6c491a622b076e9afcbc0444eefd9cbe001630558bbcba8d0a90dab7fe7", size = 6243282, upload-time = "2026-09-10T01:17:21.158Z" }, + { url = "https://files.pythonhosted.org/packages/be/d6/1b697092f53cfd4d721fbbe13e7d66dc72a9d508156af9be4c86607e4373/cuda_bindings-13.4.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e730e80d997b6037566033a79aae3995e1981654054536f512e5c091679c70", size = 6803245, upload-time = "2026-09-10T01:17:23.309Z" }, +] + +[[package]] +name = "cuda-pathfinder" +version = "1.8.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/e6/22df83f82f9bc26cb1c42265cf14d34d4908dba2a0f261bd7b28244acb00/cuda_pathfinder-1.8.1-py3-none-any.whl", hash = "sha256:ae0137ff9e56ea97499bcbf54f5f2778ec25f3266715ac86da192a795af982a8", size = 62552, upload-time = "2026-09-02T16:55:28.64Z" }, +] + +[[package]] +name = "cuda-toolkit" +version = "13.0.3.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/c7/a79086a62c98befcdb8349656c6f114e2db3b8b2422f6e25c97a7f2a9a3c/cuda_toolkit-13.0.3.0-py2.py3-none-any.whl", hash = "sha256:d693caaa261214ddd7dbb60d68e71cbed884e68c2be7509778f3051da0b91c3f", size = 2512, upload-time = "2026-04-14T00:50:08.173Z" }, +] + +[package.optional-dependencies] +cublas = [ + { name = "nvidia-cublas", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cuda-nvrtc", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +cudart = [ + { name = "nvidia-cuda-runtime", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +cufft = [ + { name = "nvidia-cufft", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +cufile = [ + { name = "nvidia-cufile", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +cupti = [ + { name = "nvidia-cuda-cupti", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +curand = [ + { name = "nvidia-curand", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +cusolver = [ + { name = "nvidia-cublas", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cusolver", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cusparse", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +cusparse = [ + { name = "nvidia-cusparse", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +nvjitlink = [ + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +nvrtc = [ + { name = "nvidia-cuda-nvrtc", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +nvtx = [ + { name = "nvidia-nvtx", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + +[[package]] +name = "cymem" +version = "2.0.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/2f0fbb32535c3731b7c2974c569fb9325e0a38ed5565a08e1139a3b71e82/cymem-2.0.13.tar.gz", hash = "sha256:1c91a92ae8c7104275ac26bd4d29b08ccd3e7faff5893d3858cb6fadf1bc1588", size = 12320, upload-time = "2025-11-14T14:58:36.902Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/14/462018dd384ee1848ac9c1951534a813a325abbfc161a74e2cbcb38d2469/cymem-2.0.13-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8efc4f308169237aade0e82877a65a563833dec32eb7ab2326120253e0e9e918", size = 43747, upload-time = "2025-11-14T14:57:11.287Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9b/c123ba65dddcd8a2bc0b3c9046766c15abe0e257c315b3040eed22cce1e2/cymem-2.0.13-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e03bb575a96c59bc210d7d59862747f0012696b0dac3427ce8af33c7afb3d4a2", size = 43328, upload-time = "2025-11-14T14:57:12.578Z" }, + { url = "https://files.pythonhosted.org/packages/bd/be/7b7a4cf9cd2d37e674612a86fc90b3d59bff12177f83430e62b25afaf7fc/cymem-2.0.13-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1775d3fd34cf099929b79c3e48469283642463f977af6801231f3c0e5d9c9369", size = 231539, upload-time = "2025-11-14T14:57:14.441Z" }, + { url = "https://files.pythonhosted.org/packages/79/6d/d165c38cd4caaaf60942e2cec9998b667008f2384047ccfe0b4b5f7a1ffe/cymem-2.0.13-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:84e2976e38cd663f758e40b5497fa5cd183d7c5fb0d04ce81a4b42a1ba124ff0", size = 229674, upload-time = "2025-11-14T14:57:15.685Z" }, + { url = "https://files.pythonhosted.org/packages/95/c1/af83c03a93f890ca81149561b18a4a67a9aa36a1109f15e291dd2703ab12/cymem-2.0.13-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed9de1b9b042f76fe5c312e4359eab58bf52ac7dfdf6887368a760410d809440", size = 229805, upload-time = "2025-11-14T14:57:17.289Z" }, + { url = "https://files.pythonhosted.org/packages/03/2d/12900758b80345d9aed5892a9d61e8a5f6abbbe5837e4def373a53cd0da2/cymem-2.0.13-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1366c7437a209230f4b797fae10227a8206d4021d37c9f9c0d31fd97ea4feb35", size = 234018, upload-time = "2025-11-14T14:57:18.512Z" }, + { url = "https://files.pythonhosted.org/packages/a6/8b/5fcf5430fc81098aef58cc20340e51f37b49b9d8c15766e0d5d63e7288a3/cymem-2.0.13-cp310-cp310-win_amd64.whl", hash = "sha256:7700b116524b087e0169f10f267539223b48240ef2734c3a727a9e6b4db9a671", size = 40102, upload-time = "2025-11-14T14:57:19.972Z" }, + { url = "https://files.pythonhosted.org/packages/0d/d3/cb6c83758fe399443b858faafb7096b72535621a7af7dd9a54ff0989fa14/cymem-2.0.13-cp310-cp310-win_arm64.whl", hash = "sha256:c8dbfddfe5c604974e17c6f373cedd4d25cd67f84812ede7dea12128fa0c2015", size = 36282, upload-time = "2025-11-14T14:57:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/10/64/1db41f7576a6b69f70367e3c15e968fd775ba7419e12059c9966ceb826f8/cymem-2.0.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:673183466b0ff2e060d97ec5116711d44200b8f7be524323e080d215ee2d44a5", size = 43587, upload-time = "2025-11-14T14:57:22.39Z" }, + { url = "https://files.pythonhosted.org/packages/81/13/57f936fc08551323aab3f92ff6b7f4d4b89d5b4e495c870a67cb8d279757/cymem-2.0.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bee2791b3f6fc034ce41268851462bf662ff87e8947e35fb6dd0115b4644a61f", size = 43139, upload-time = "2025-11-14T14:57:23.363Z" }, + { url = "https://files.pythonhosted.org/packages/32/a6/9345754be51e0479aa387b7b6cffc289d0fd3201aaeb8dade4623abd1e02/cymem-2.0.13-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f3aee3adf16272bca81c5826eed55ba3c938add6d8c9e273f01c6b829ecfde22", size = 245063, upload-time = "2025-11-14T14:57:24.839Z" }, + { url = "https://files.pythonhosted.org/packages/d6/01/6bc654101526fa86e82bf6b05d99b2cd47c30a333cfe8622c26c0592beb2/cymem-2.0.13-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:30c4e75a3a1d809e89106b0b21803eb78e839881aa1f5b9bd27b454bc73afde3", size = 244496, upload-time = "2025-11-14T14:57:26.42Z" }, + { url = "https://files.pythonhosted.org/packages/c4/fb/853b7b021e701a1f41687f3704d5f469aeb2a4f898c3fbb8076806885955/cymem-2.0.13-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec99efa03cf8ec11c8906aa4d4cc0c47df393bc9095c9dd64b89b9b43e220b04", size = 243287, upload-time = "2025-11-14T14:57:27.542Z" }, + { url = "https://files.pythonhosted.org/packages/d4/2b/0e4664cafc581de2896d75000651fd2ce7094d33263f466185c28ffc96e4/cymem-2.0.13-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c90a6ecba994a15b17a3f45d7ec74d34081df2f73bd1b090e2adc0317e4e01b6", size = 248287, upload-time = "2025-11-14T14:57:29.055Z" }, + { url = "https://files.pythonhosted.org/packages/21/0f/f94c6950edbfc2aafb81194fc40b6cacc8e994e9359d3cb4328c5705b9b5/cymem-2.0.13-cp311-cp311-win_amd64.whl", hash = "sha256:ce821e6ba59148ed17c4567113b8683a6a0be9c9ac86f14e969919121efb61a5", size = 40116, upload-time = "2025-11-14T14:57:30.592Z" }, + { url = "https://files.pythonhosted.org/packages/00/df/2455eff6ac0381ff165db6883b311f7016e222e3dd62185517f8e8187ed0/cymem-2.0.13-cp311-cp311-win_arm64.whl", hash = "sha256:0dca715e708e545fd1d97693542378a00394b20a37779c1ae2c8bdbb43acef79", size = 36349, upload-time = "2025-11-14T14:57:31.573Z" }, + { url = "https://files.pythonhosted.org/packages/c9/52/478a2911ab5028cb710b4900d64aceba6f4f882fcb13fd8d40a456a1b6dc/cymem-2.0.13-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8afbc5162a0fe14b6463e1c4e45248a1b2fe2cbcecc8a5b9e511117080da0eb", size = 43745, upload-time = "2025-11-14T14:57:32.52Z" }, + { url = "https://files.pythonhosted.org/packages/f9/71/f0f8adee945524774b16af326bd314a14a478ed369a728a22834e6785a18/cymem-2.0.13-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9251d889348fe79a75e9b3e4d1b5fa651fca8a64500820685d73a3acc21b6a8", size = 42927, upload-time = "2025-11-14T14:57:33.827Z" }, + { url = "https://files.pythonhosted.org/packages/62/6d/159780fe162ff715d62b809246e5fc20901cef87ca28b67d255a8d741861/cymem-2.0.13-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:742fc19764467a49ed22e56a4d2134c262d73a6c635409584ae3bf9afa092c33", size = 258346, upload-time = "2025-11-14T14:57:34.917Z" }, + { url = "https://files.pythonhosted.org/packages/eb/12/678d16f7aa1996f947bf17b8cfb917ea9c9674ef5e2bd3690c04123d5680/cymem-2.0.13-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f190a92fe46197ee64d32560eb121c2809bb843341733227f51538ce77b3410d", size = 260843, upload-time = "2025-11-14T14:57:36.503Z" }, + { url = "https://files.pythonhosted.org/packages/31/5d/0dd8c167c08cd85e70d274b7235cfe1e31b3cebc99221178eaf4bbb95c6f/cymem-2.0.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d670329ee8dbbbf241b7c08069fe3f1d3a1a3e2d69c7d05ea008a7010d826298", size = 254607, upload-time = "2025-11-14T14:57:38.036Z" }, + { url = "https://files.pythonhosted.org/packages/b7/c9/d6514a412a1160aa65db539836b3d47f9b59f6675f294ec34ae32f867c82/cymem-2.0.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a84ba3178d9128b9ffb52ce81ebab456e9fe959125b51109f5b73ebdfc6b60d6", size = 262421, upload-time = "2025-11-14T14:57:39.265Z" }, + { url = "https://files.pythonhosted.org/packages/dd/fe/3ee37d02ca4040f2fb22d34eb415198f955862b5dd47eee01df4c8f5454c/cymem-2.0.13-cp312-cp312-win_amd64.whl", hash = "sha256:2ff1c41fd59b789579fdace78aa587c5fc091991fa59458c382b116fc36e30dc", size = 40176, upload-time = "2025-11-14T14:57:40.706Z" }, + { url = "https://files.pythonhosted.org/packages/94/fb/1b681635bfd5f2274d0caa8f934b58435db6c091b97f5593738065ddb786/cymem-2.0.13-cp312-cp312-win_arm64.whl", hash = "sha256:6bbd701338df7bf408648191dff52472a9b334f71bcd31a21a41d83821050f67", size = 35959, upload-time = "2025-11-14T14:57:41.682Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0f/95a4d1e3bebfdfa7829252369357cf9a764f67569328cd9221f21e2c952e/cymem-2.0.13-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:891fd9030293a8b652dc7fb9fdc79a910a6c76fc679cd775e6741b819ffea476", size = 43478, upload-time = "2025-11-14T14:57:42.682Z" }, + { url = "https://files.pythonhosted.org/packages/bf/a0/8fc929cc29ae466b7b4efc23ece99cbd3ea34992ccff319089c624d667fd/cymem-2.0.13-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:89c4889bd16513ce1644ccfe1e7c473ba7ca150f0621e66feac3a571bde09e7e", size = 42695, upload-time = "2025-11-14T14:57:43.741Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b3/deeb01354ebaf384438083ffe0310209ef903db3e7ba5a8f584b06d28387/cymem-2.0.13-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:45dcaba0f48bef9cc3d8b0b92058640244a95a9f12542210b51318da97c2cf28", size = 250573, upload-time = "2025-11-14T14:57:44.81Z" }, + { url = "https://files.pythonhosted.org/packages/36/36/bc980b9a14409f3356309c45a8d88d58797d02002a9d794dd6c84e809d3a/cymem-2.0.13-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e96848faaafccc0abd631f1c5fb194eac0caee4f5a8777fdbb3e349d3a21741c", size = 254572, upload-time = "2025-11-14T14:57:46.023Z" }, + { url = "https://files.pythonhosted.org/packages/fd/dd/a12522952624685bd0f8968e26d2ed6d059c967413ce6eb52292f538f1b0/cymem-2.0.13-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e02d3e2c3bfeb21185d5a4a70790d9df40629a87d8d7617dc22b4e864f665fa3", size = 248060, upload-time = "2025-11-14T14:57:47.605Z" }, + { url = "https://files.pythonhosted.org/packages/08/11/5dc933ddfeb2dfea747a0b935cb965b9a7580b324d96fc5f5a1b5ff8df29/cymem-2.0.13-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fece5229fd5ecdcd7a0738affb8c59890e13073ae5626544e13825f26c019d3c", size = 254601, upload-time = "2025-11-14T14:57:48.861Z" }, + { url = "https://files.pythonhosted.org/packages/70/66/d23b06166864fa94e13a98e5922986ce774832936473578febce64448d75/cymem-2.0.13-cp313-cp313-win_amd64.whl", hash = "sha256:38aefeb269597c1a0c2ddf1567dd8605489b661fa0369c6406c1acd433b4c7ba", size = 40103, upload-time = "2025-11-14T14:57:50.396Z" }, + { url = "https://files.pythonhosted.org/packages/2f/9e/c7b21271ab88a21760f3afdec84d2bc09ffa9e6c8d774ad9d4f1afab0416/cymem-2.0.13-cp313-cp313-win_arm64.whl", hash = "sha256:717270dcfd8c8096b479c42708b151002ff98e434a7b6f1f916387a6c791e2ad", size = 36016, upload-time = "2025-11-14T14:57:51.611Z" }, + { url = "https://files.pythonhosted.org/packages/7f/28/d3b03427edc04ae04910edf1c24b993881c3ba93a9729a42bcbb816a1808/cymem-2.0.13-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7e1a863a7f144ffb345397813701509cfc74fc9ed360a4d92799805b4b865dd1", size = 46429, upload-time = "2025-11-14T14:57:52.582Z" }, + { url = "https://files.pythonhosted.org/packages/35/a9/7ed53e481f47ebfb922b0b42e980cec83e98ccb2137dc597ea156642440c/cymem-2.0.13-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c16cb80efc017b054f78998c6b4b013cef509c7b3d802707ce1f85a1d68361bf", size = 46205, upload-time = "2025-11-14T14:57:53.64Z" }, + { url = "https://files.pythonhosted.org/packages/61/39/a3d6ad073cf7f0fbbb8bbf09698c3c8fac11be3f791d710239a4e8dd3438/cymem-2.0.13-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0d78a27c88b26c89bd1ece247d1d5939dba05a1dae6305aad8fd8056b17ddb51", size = 296083, upload-time = "2025-11-14T14:57:55.922Z" }, + { url = "https://files.pythonhosted.org/packages/36/0c/20697c8bc19f624a595833e566f37d7bcb9167b0ce69de896eba7cfc9c2d/cymem-2.0.13-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6d36710760f817194dacb09d9fc45cb6a5062ed75e85f0ef7ad7aeeb13d80cc3", size = 286159, upload-time = "2025-11-14T14:57:57.106Z" }, + { url = "https://files.pythonhosted.org/packages/82/d4/9326e3422d1c2d2b4a8fb859bdcce80138f6ab721ddafa4cba328a505c71/cymem-2.0.13-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c8f30971cadd5dcf73bcfbbc5849b1f1e1f40db8cd846c4aa7d3b5e035c7b583", size = 288186, upload-time = "2025-11-14T14:57:58.334Z" }, + { url = "https://files.pythonhosted.org/packages/ed/bc/68da7dd749b72884dc22e898562f335002d70306069d496376e5ff3b6153/cymem-2.0.13-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9d441d0e45798ec1fd330373bf7ffa6b795f229275f64016b6a193e6e2a51522", size = 290353, upload-time = "2025-11-14T14:58:00.562Z" }, + { url = "https://files.pythonhosted.org/packages/50/23/dbf2ad6ecd19b99b3aab6203b1a06608bbd04a09c522d836b854f2f30f73/cymem-2.0.13-cp313-cp313t-win_amd64.whl", hash = "sha256:d1c950eebb9f0f15e3ef3591313482a5a611d16fc12d545e2018cd607f40f472", size = 44764, upload-time = "2025-11-14T14:58:01.793Z" }, + { url = "https://files.pythonhosted.org/packages/54/3f/35701c13e1fc7b0895198c8b20068c569a841e0daf8e0b14d1dc0816b28f/cymem-2.0.13-cp313-cp313t-win_arm64.whl", hash = "sha256:042e8611ef862c34a97b13241f5d0da86d58aca3cecc45c533496678e75c5a1f", size = 38964, upload-time = "2025-11-14T14:58:02.87Z" }, + { url = "https://files.pythonhosted.org/packages/a7/2e/f0e1596010a9a57fa9ebd124a678c07c5b2092283781ae51e79edcf5cb98/cymem-2.0.13-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d2a4bf67db76c7b6afc33de44fb1c318207c3224a30da02c70901936b5aafdf1", size = 43812, upload-time = "2025-11-14T14:58:04.227Z" }, + { url = "https://files.pythonhosted.org/packages/bc/45/8ccc21df08fcbfa6aa3efeb7efc11a1c81c90e7476e255768bb9c29ba02a/cymem-2.0.13-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:92a2ce50afa5625fb5ce7c9302cee61e23a57ccac52cd0410b4858e572f8614b", size = 42951, upload-time = "2025-11-14T14:58:05.424Z" }, + { url = "https://files.pythonhosted.org/packages/01/8c/fe16531631f051d3d1226fa42e2d76fd2c8d5cfa893ec93baee90c7a9d90/cymem-2.0.13-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bc116a70cc3a5dc3d1684db5268eff9399a0be8603980005e5b889564f1ea42f", size = 249878, upload-time = "2025-11-14T14:58:06.95Z" }, + { url = "https://files.pythonhosted.org/packages/47/4b/39d67b80ffb260457c05fcc545de37d82e9e2dbafc93dd6b64f17e09b933/cymem-2.0.13-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:68489bf0035c4c280614067ab6a82815b01dc9fcd486742a5306fe9f68deb7ef", size = 252571, upload-time = "2025-11-14T14:58:08.232Z" }, + { url = "https://files.pythonhosted.org/packages/53/0e/76f6531f74dfdfe7107899cce93ab063bb7ee086ccd3910522b31f623c08/cymem-2.0.13-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:03cb7bdb55718d5eb6ef0340b1d2430ba1386db30d33e9134d01ba9d6d34d705", size = 248555, upload-time = "2025-11-14T14:58:09.429Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7c/eee56757db81f0aefc2615267677ae145aff74228f529838425057003c0d/cymem-2.0.13-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1710390e7fb2510a8091a1991024d8ae838fd06b02cdfdcd35f006192e3c6b0e", size = 254177, upload-time = "2025-11-14T14:58:10.594Z" }, + { url = "https://files.pythonhosted.org/packages/77/e0/a4b58ec9e53c836dce07ef39837a64a599f4a21a134fc7ca57a3a8f9a4b5/cymem-2.0.13-cp314-cp314-win_amd64.whl", hash = "sha256:ac699c8ec72a3a9de8109bd78821ab22f60b14cf2abccd970b5ff310e14158ed", size = 40853, upload-time = "2025-11-14T14:58:12.116Z" }, + { url = "https://files.pythonhosted.org/packages/61/81/9931d1f83e5aeba175440af0b28f0c2e6f71274a5a7b688bc3e907669388/cymem-2.0.13-cp314-cp314-win_arm64.whl", hash = "sha256:90c2d0c04bcda12cd5cebe9be93ce3af6742ad8da96e1b1907e3f8e00291def1", size = 36970, upload-time = "2025-11-14T14:58:13.114Z" }, + { url = "https://files.pythonhosted.org/packages/b7/ef/af447c2184dec6dec973be14614df8ccb4d16d1c74e0784ab4f02538433c/cymem-2.0.13-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ff036bbc1464993552fd1251b0a83fe102af334b301e3896d7aa05a4999ad042", size = 46804, upload-time = "2025-11-14T14:58:14.113Z" }, + { url = "https://files.pythonhosted.org/packages/8c/95/e10f33a8d4fc17f9b933d451038218437f9326c2abb15a3e7f58ce2a06ec/cymem-2.0.13-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fb8291691ba7ff4e6e000224cc97a744a8d9588418535c9454fd8436911df612", size = 46254, upload-time = "2025-11-14T14:58:15.156Z" }, + { url = "https://files.pythonhosted.org/packages/e7/7a/5efeb2d2ea6ebad2745301ad33a4fa9a8f9a33b66623ee4d9185683007a6/cymem-2.0.13-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d8d06ea59006b1251ad5794bcc00121e148434826090ead0073c7b7fedebe431", size = 296061, upload-time = "2025-11-14T14:58:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/2a3f65842cc8443c2c0650cf23d525be06c8761ab212e0a095a88627be1b/cymem-2.0.13-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c0046a619ecc845ccb4528b37b63426a0cbcb4f14d7940add3391f59f13701e6", size = 285784, upload-time = "2025-11-14T14:58:17.412Z" }, + { url = "https://files.pythonhosted.org/packages/98/73/dd5f9729398f0108c2e71d942253d0d484d299d08b02e474d7cfc43ed0b0/cymem-2.0.13-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:18ad5b116a82fa3674bc8838bd3792891b428971e2123ae8c0fd3ca472157c5e", size = 288062, upload-time = "2025-11-14T14:58:20.225Z" }, + { url = "https://files.pythonhosted.org/packages/5a/01/ffe51729a8f961a437920560659073e47f575d4627445216c1177ecd4a41/cymem-2.0.13-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:666ce6146bc61b9318aa70d91ce33f126b6344a25cf0b925621baed0c161e9cc", size = 290465, upload-time = "2025-11-14T14:58:21.815Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ac/c9e7d68607f71ef978c81e334ab2898b426944c71950212b1467186f69f9/cymem-2.0.13-cp314-cp314t-win_amd64.whl", hash = "sha256:84c1168c563d9d1e04546cb65e3e54fde2bf814f7c7faf11fc06436598e386d1", size = 46665, upload-time = "2025-11-14T14:58:23.512Z" }, + { url = "https://files.pythonhosted.org/packages/66/66/150e406a2db5535533aa3c946de58f0371f2e412e23f050c704588023e6e/cymem-2.0.13-cp314-cp314t-win_arm64.whl", hash = "sha256:e9027764dc5f1999fb4b4cabee1d0322c59e330c0a6485b436a68275f614277f", size = 39715, upload-time = "2025-11-14T14:58:24.773Z" }, +] + [[package]] name = "dapr" source = { editable = "." } @@ -687,14 +1260,25 @@ dependencies = [ [package.optional-dependencies] all = [ + { name = "azure-identity" }, + { name = "azure-search-documents" }, + { name = "azure-storage-blob" }, + { name = "boto3" }, { name = "cloudevents" }, { name = "fastapi" }, { name = "flask" }, + { name = "httpx" }, { name = "langchain" }, + { name = "langchain-core" }, { name = "langgraph" }, { name = "msgpack" }, + { name = "openai" }, + { name = "pinecone" }, + { name = "psycopg", extra = ["binary"] }, { name = "python-ulid" }, { name = "strands-agents" }, + { name = "unstructured", version = "0.18.32", source = { registry = "https://pypi.org/simple" }, extra = ["docx", "md", "pdf"], marker = "python_full_version < '3.11' and sys_platform != 'win32'" }, + { name = "unstructured", version = "0.27.5", source = { registry = "https://pypi.org/simple" }, extra = ["docx", "md", "pdf"], marker = "python_full_version >= '3.11' and sys_platform != 'win32'" }, { name = "uvicorn" }, ] fastapi = [ @@ -713,6 +1297,33 @@ langgraph = [ { name = "msgpack" }, { name = "python-ulid" }, ] +rag = [ + { name = "openai" }, +] +rag-azure = [ + { name = "azure-identity" }, + { name = "azure-storage-blob" }, +] +rag-azure-search = [ + { name = "azure-search-documents" }, + { name = "httpx" }, +] +rag-langchain = [ + { name = "langchain-core" }, +] +rag-pgvector = [ + { name = "psycopg", extra = ["binary"] }, +] +rag-pinecone = [ + { name = "pinecone" }, +] +rag-s3 = [ + { name = "boto3" }, +] +rag-unstructured = [ + { name = "unstructured", version = "0.18.32", source = { registry = "https://pypi.org/simple" }, extra = ["docx", "md", "pdf"], marker = "python_full_version < '3.11'" }, + { name = "unstructured", version = "0.27.5", source = { registry = "https://pypi.org/simple" }, extra = ["docx", "md", "pdf"], marker = "python_full_version >= '3.11'" }, +] strands = [ { name = "msgpack" }, { name = "python-ulid" }, @@ -774,6 +1385,14 @@ tests = [ [package.metadata] requires-dist = [ { name = "aiohttp", specifier = ">=3.9.5,<4.0.0" }, + { name = "azure-identity", marker = "extra == 'all'", specifier = ">=1.15.0,<2.0.0" }, + { name = "azure-identity", marker = "extra == 'rag-azure'", specifier = ">=1.15.0,<2.0.0" }, + { name = "azure-search-documents", marker = "extra == 'all'", specifier = ">=11.5.0,<12.0.0" }, + { name = "azure-search-documents", marker = "extra == 'rag-azure-search'", specifier = ">=11.5.0,<12.0.0" }, + { name = "azure-storage-blob", marker = "extra == 'all'", specifier = ">=12.19.0,<13.0.0" }, + { name = "azure-storage-blob", marker = "extra == 'rag-azure'", specifier = ">=12.19.0,<13.0.0" }, + { name = "boto3", marker = "extra == 'all'", specifier = ">=1.34.0,<2.0.0" }, + { name = "boto3", marker = "extra == 'rag-s3'", specifier = ">=1.34.0,<2.0.0" }, { name = "cloudevents", marker = "extra == 'all'", specifier = ">=1.0.0,<2.0.0" }, { name = "cloudevents", marker = "extra == 'grpc'", specifier = ">=1.0.0,<2.0.0" }, { name = "fastapi", marker = "extra == 'all'", specifier = ">=0.60.1,<1.0.0" }, @@ -782,14 +1401,24 @@ requires-dist = [ { name = "flask", marker = "extra == 'flask'", specifier = ">=1.1.4,<4.0.0" }, { name = "grpcio", specifier = ">=1.81.1,<2.0.0" }, { name = "grpcio-status", specifier = ">=1.81.1,<2.0.0" }, + { name = "httpx", marker = "extra == 'all'", specifier = ">=0.27.0,<1.0.0" }, + { name = "httpx", marker = "extra == 'rag-azure-search'", specifier = ">=0.27.0,<1.0.0" }, { name = "langchain", marker = "extra == 'all'", specifier = ">=0.1.17,<2.0.0" }, { name = "langchain", marker = "extra == 'langgraph'", specifier = ">=0.1.17,<2.0.0" }, + { name = "langchain-core", marker = "extra == 'all'", specifier = ">=0.3.0,<2.0.0" }, + { name = "langchain-core", marker = "extra == 'rag-langchain'", specifier = ">=0.3.0,<2.0.0" }, { name = "langgraph", marker = "extra == 'all'", specifier = ">=0.3.6,<2.0.0" }, { name = "langgraph", marker = "extra == 'langgraph'", specifier = ">=0.3.6,<2.0.0" }, { name = "msgpack", marker = "extra == 'all'", specifier = ">=1.0,<2.0" }, { name = "msgpack", marker = "extra == 'langgraph'", specifier = ">=1.0,<2.0" }, { name = "msgpack", marker = "extra == 'strands'", specifier = ">=1.0,<2.0" }, + { name = "openai", marker = "extra == 'all'", specifier = ">=1.50.0,<2.0.0" }, + { name = "openai", marker = "extra == 'rag'", specifier = ">=1.50.0,<2.0.0" }, + { name = "pinecone", marker = "extra == 'all'", specifier = ">=5.0.0,<8.0.0" }, + { name = "pinecone", marker = "extra == 'rag-pinecone'", specifier = ">=5.0.0,<8.0.0" }, { name = "protobuf", specifier = ">=6.33.5,<8.0.0" }, + { name = "psycopg", extras = ["binary"], marker = "extra == 'all'", specifier = ">=3.1.0,<4.0.0" }, + { name = "psycopg", extras = ["binary"], marker = "extra == 'rag-pgvector'", specifier = ">=3.1.0,<4.0.0" }, { name = "python-dateutil", specifier = ">=2.8.1,<3.0.0" }, { name = "python-ulid", marker = "extra == 'all'", specifier = ">=3.0.0,<5.0.0" }, { name = "python-ulid", marker = "extra == 'langgraph'", specifier = ">=3.0.0,<5.0.0" }, @@ -797,10 +1426,12 @@ requires-dist = [ { name = "strands-agents", marker = "extra == 'all'", specifier = ">=1.30.0,<2.0.0" }, { name = "strands-agents", marker = "extra == 'strands'", specifier = ">=1.30.0,<2.0.0" }, { name = "typing-extensions", specifier = ">=4.4.0,<5.0.0" }, + { name = "unstructured", extras = ["docx", "md", "pdf"], marker = "sys_platform != 'win32' and extra == 'all'", specifier = ">=0.15.0,<1.0.0" }, + { name = "unstructured", extras = ["docx", "md", "pdf"], marker = "extra == 'rag-unstructured'", specifier = ">=0.15.0,<1.0.0" }, { name = "uvicorn", marker = "extra == 'all'", specifier = ">=0.11.6,<1.0.0" }, { name = "uvicorn", marker = "extra == 'fastapi'", specifier = ">=0.11.6,<1.0.0" }, ] -provides-extras = ["all", "fastapi", "flask", "grpc", "langgraph", "strands", "workflow"] +provides-extras = ["all", "fastapi", "flask", "grpc", "langgraph", "rag", "rag-azure", "rag-azure-search", "rag-langchain", "rag-pgvector", "rag-pinecone", "rag-s3", "rag-unstructured", "strands", "workflow"] [package.metadata.requires-dev] dev = [ @@ -854,6 +1485,28 @@ tests = [ { name = "wheel", specifier = "~=0.46.3" }, ] +[[package]] +name = "dataclasses-json" +version = "0.6.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "marshmallow", marker = "python_full_version < '3.11'" }, + { name = "typing-inspect", marker = "python_full_version < '3.11'" }, +] +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 = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, +] + [[package]] name = "deprecation" version = "2.1.0" @@ -866,6 +1519,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a", size = 11178, upload-time = "2020-04-20T14:23:36.581Z" }, ] +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + [[package]] name = "docstring-parser" version = "0.17.0" @@ -875,12 +1537,37 @@ 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 = "effdet" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "omegaconf", marker = "python_full_version < '3.11'" }, + { name = "pycocotools", marker = "python_full_version < '3.11'" }, + { name = "timm", marker = "python_full_version < '3.11'" }, + { name = "torch", marker = "python_full_version < '3.11'" }, + { name = "torchvision", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/c3/12d45167ec36f7f9a5ed80bc2128392b3f6207f760d437287d32a0e43f41/effdet-0.4.1.tar.gz", hash = "sha256:ac5589fd304a5650c201986b2ef5f8e10c111093a71b1c49fa6b8817710812b5", size = 110134, upload-time = "2023-05-21T22:18:01.039Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/13/563119fe0af82aca5a3b89399c435953072c39515c2e818eb82793955c3b/effdet-0.4.1-py3-none-any.whl", hash = "sha256:10889a226228d515c948e3fcf811e64c0d78d7aa94823a300045653b9c284cb7", size = 112513, upload-time = "2023-05-21T22:17:58.47Z" }, +] + +[[package]] +name = "emoji" +version = "2.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/78/0d2db9382c92a163d7095fc08efff7800880f830a152cfced40161e7638d/emoji-2.15.0.tar.gz", hash = "sha256:eae4ab7d86456a70a00a985125a03263a5eac54cd55e51d7e184b1ed3b6757e4", size = 615483, upload-time = "2025-09-21T12:13:02.755Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/5e/4b5aaaabddfacfe36ba7768817bd1f71a7a810a43705e531f3ae4c690767/emoji-2.15.0-py3-none-any.whl", hash = "sha256:205296793d66a89d88af4688fa57fd6496732eb48917a87175a023c8138995eb", size = 608433, upload-time = "2025-09-21T12:13:01.197Z" }, +] + [[package]] name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] 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 = [ @@ -903,13 +1590,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/03/10388a42375ee7e4ac9b94eb2c5c569c8b5795e377e701c9ac3ad63de890/fastapi-0.141.1-py3-none-any.whl", hash = "sha256:bfb91aa2d334c61cb35ba9a116fc123b3d3df31640b801cf57a7a78ec3f603b3", size = 131954, upload-time = "2026-07-29T17:18:04.364Z" }, ] +[[package]] +name = "filelock" +version = "3.32.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/46/126b1831dca12060d4a8296bf9c4fe5c93c4f22197fa239cb0cc82042bba/filelock-3.32.6.tar.gz", hash = "sha256:a3f55a18af3652a94d8f47d6055df434f254ca1d02ef2524850c6d249ca2512c", size = 225172, upload-time = "2026-09-08T22:57:11.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/06/4f138f618dbea66803291274f228f01daf29f306fe8b96bc30dab765df75/filelock-3.32.6-py3-none-any.whl", hash = "sha256:3f16ecd0117feae0dfc147e8c62eb5daeccd8bd800378c3ddf416de9b4feb6b1", size = 100189, upload-time = "2026-09-08T22:57:10.182Z" }, +] + +[[package]] +name = "filetype" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/29/745f7d30d47fe0f251d3ad3dc2978a23141917661998763bebb6da007eb1/filetype-1.2.0.tar.gz", hash = "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb", size = 998020, upload-time = "2022-11-02T17:34:04.141Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/79/1b8fa1bb3568781e84c9200f951c735f3f157429f44be0495da55894d620/filetype-1.2.0-py2.py3-none-any.whl", hash = "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25", size = 19970, upload-time = "2022-11-02T17:34:01.425Z" }, +] + [[package]] name = "flask" version = "3.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "blinker" }, - { name = "click" }, + { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform != 'win32') or (python_full_version == '3.11.*' and sys_platform == 'win32') or (python_full_version == '3.14.*' and sys_platform == 'win32')" }, + { name = "click", version = "8.5.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32') or (python_full_version >= '3.11' and sys_platform != 'win32') or (python_full_version < '3.11' and sys_platform == 'win32') or (python_full_version >= '3.15' and sys_platform == 'win32')" }, { name = "itsdangerous" }, { name = "jinja2" }, { name = "markupsafe" }, @@ -920,6 +1626,87 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" }, ] +[[package]] +name = "flatbuffers" +version = "25.12.19" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661, upload-time = "2025-12-19T23:16:13.622Z" }, +] + +[[package]] +name = "fonttools" +version = "4.65.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/77/51/d63c7e52163ac14393a35bd14bd7c0da95f8f74be5d7cc988092f9965129/fonttools-4.65.0.tar.gz", hash = "sha256:762ba5431358d0dbd4a01982484a1d494fb267e91f974cdcf20b80eab8560f6f", size = 3674467, upload-time = "2026-09-10T15:35:54.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/d9/1caaa015dd207da7ccd3feba87289997bd88334ea3e74a367cdc7b0e50a3/fonttools-4.65.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:93a73af2075036d36d7fbf856779c56a1b3b86ffcdae6abede7596604c42c156", size = 3095449, upload-time = "2026-09-10T15:33:04.814Z" }, + { url = "https://files.pythonhosted.org/packages/20/d6/988cd9b33ae2d92b15a51d73eb77c991f7a0fe90a7bc74526e9669247789/fonttools-4.65.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c130be2232e3caf8d2b476854ea78421ec1642917ff5ab695284bac31bbb072b", size = 2587697, upload-time = "2026-09-10T15:33:07.653Z" }, + { url = "https://files.pythonhosted.org/packages/f8/6a/36f465a1c277131f9569f6a56fe99cf135391b4860b9c4f4b23e5b1cd5df/fonttools-4.65.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e3944e0bdba42effb71959e43d91b599326b02b59c78310d5675e8a75525e7d8", size = 5313308, upload-time = "2026-09-10T15:33:09.975Z" }, + { url = "https://files.pythonhosted.org/packages/ee/56/151b5e81d20c63834f48ad37a0cbbbe2f9b248e38f8d10387f0cf5219244/fonttools-4.65.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fb53892b570f7f1f0055e75fc4de32673e32f749c4c8a606b63d5c436650e634", size = 5253649, upload-time = "2026-09-10T15:33:12.225Z" }, + { url = "https://files.pythonhosted.org/packages/c8/22/6389215da9d4f98623aacdca9479c1030bbd516cb658e893bc54b61098ce/fonttools-4.65.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a6c8d184e523580a7c55d21cde37176a3c91cb539cf06c2aa36ffc634fd75296", size = 5277745, upload-time = "2026-09-10T15:33:14.783Z" }, + { url = "https://files.pythonhosted.org/packages/89/e3/c1037a1dfb7c8efe6f2a7d1951ebdde40cbbf82e9c5d796fcb03077e4790/fonttools-4.65.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e5ceccaf2e57d83b753a2b5db5d94aa0a8071886d4afebd2d520c9683e6bef0e", size = 5413259, upload-time = "2026-09-10T15:33:17.619Z" }, + { url = "https://files.pythonhosted.org/packages/44/b9/7dd72330168d39635c329f23a98279393b1e825e42a6db362e09897aad7b/fonttools-4.65.0-cp310-cp310-win32.whl", hash = "sha256:aff640a4fcb021fa83f9879d5bfa115b6931522dae991a24faa75888bd6aeff6", size = 1577124, upload-time = "2026-09-10T15:33:19.864Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c2/e959385b4626989b25b82b9f4f99e7c3ac68377d6846b376239b6f126966/fonttools-4.65.0-cp310-cp310-win_amd64.whl", hash = "sha256:5c1700a60e4ff23a0425d5a64abf43d092e6b55071354825781faf255904dcb4", size = 1633354, upload-time = "2026-09-10T15:33:21.963Z" }, + { url = "https://files.pythonhosted.org/packages/62/9e/58250cdc54d96fcfacb544e12997a6390fa4e6b71ae2241cfcfe5b341803/fonttools-4.65.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:06273c71e692caf5989c0437ca50875a5e49e216ddf653228fe9bb35bdc82c0f", size = 3089141, upload-time = "2026-09-10T15:33:24.509Z" }, + { url = "https://files.pythonhosted.org/packages/3e/67/0f0416069e38da0a1327a847a2e8dd1edb425d0043d8a3e63eb940070209/fonttools-4.65.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ca2b02d74e9ad7e21a1d11e4701425800a4b0c63cf90486e60258262feccbcbf", size = 2584298, upload-time = "2026-09-10T15:33:26.598Z" }, + { url = "https://files.pythonhosted.org/packages/99/0d/7e40e9957359afc0bab081131c215370a6d2d203361bb6f945ab595e924c/fonttools-4.65.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7830e9fa3bebc44dbc27ff44d8201def30ea5c48a773696d58e69e6bcd9cd5d4", size = 5498163, upload-time = "2026-09-10T15:33:29.071Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e6/e48cf0a272a5d4d17a09d44f92727e67f975ddfa94acc8464763d19a654d/fonttools-4.65.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a3991732c87b3f054a2a8cf86dd0d602833fa8cb37c911503173771646e1013d", size = 5457456, upload-time = "2026-09-10T15:33:31.918Z" }, + { url = "https://files.pythonhosted.org/packages/e4/8a/a5c67ddeda82ee5e4ec3bc52ac1cbb685f0bb7a0516badc55643b454ab0d/fonttools-4.65.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6031e77b3fb8c765055ba2b8bd8dcb17030f3bf2484c448b472fdedf4460ba80", size = 5464334, upload-time = "2026-09-10T15:33:34.605Z" }, + { url = "https://files.pythonhosted.org/packages/d7/16/294e77383b2d39c9f8f25144a7ba23fe1cbbc05227cc72545097785ff07c/fonttools-4.65.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6813cc1e2e883bd6c15b3e04f72c78dc65fdc4ca861063adf5f341fbaec2ca62", size = 5595597, upload-time = "2026-09-10T15:33:37.591Z" }, + { url = "https://files.pythonhosted.org/packages/80/01/8e74ce8626c734959c782f2d89af8e9f14d078fd3d4ddf8b5a51401ae475/fonttools-4.65.0-cp311-cp311-win32.whl", hash = "sha256:4a5db8442453da4b6f43ad325879381b726bf2238a2253efd9584be21a2cefc2", size = 2441114, upload-time = "2026-09-10T15:33:40.592Z" }, + { url = "https://files.pythonhosted.org/packages/37/3e/835dc6c658426e2670b7f38c38295492fcbaeb06080e9dce89ce8105993c/fonttools-4.65.0-cp311-cp311-win_amd64.whl", hash = "sha256:9f201796c8e24e657be77c16fa664e798a46122144217f90838982937a964f0a", size = 2498823, upload-time = "2026-09-10T15:33:43.402Z" }, + { url = "https://files.pythonhosted.org/packages/58/db/242fa4fce7f632c5f7ab15585343393b25792510c0c32bd218ad24d59f1c/fonttools-4.65.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e844a45c9e5ced6536f184cf1a65b5d65e8f7e711993b413e10500a8223622e5", size = 3097120, upload-time = "2026-09-10T15:33:46Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b6/42fa4d373416675f74446421cf0b2badb82a4245c60745f05f424f75c649/fonttools-4.65.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b30e953de049bf43fc0a63c7d0c44d205c923e4bbf24716aae1518c0e65f977c", size = 2584658, upload-time = "2026-09-10T15:33:48.473Z" }, + { url = "https://files.pythonhosted.org/packages/75/6f/d589b9d62280a846c77a2c383d852c6dcb79ae8aa02bf0fa46c8577af145/fonttools-4.65.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09c34bdeed8915bfb53bee0c8ed2254dbd8ec69c0014b7f3702f347c049bf358", size = 5425737, upload-time = "2026-09-10T15:33:51.473Z" }, + { url = "https://files.pythonhosted.org/packages/b5/09/de2c0c20a42c18e565a2617932beb08c06697bbdd0d3f62b108262e11583/fonttools-4.65.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:05595385ae99f4b9626cebb973bf171b8fe38a8f40708e6e42abba0ed7537778", size = 5402463, upload-time = "2026-09-10T15:33:54.907Z" }, + { url = "https://files.pythonhosted.org/packages/79/2e/bc0f5c9dce21821454bb5812d3b23410bca33c8bbd5468386d0327aa0cff/fonttools-4.65.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d95b34dd68fbfc0e4a1740c421597656117f979ed8dc85de66e08f9f9981806e", size = 5362168, upload-time = "2026-09-10T15:33:57.481Z" }, + { url = "https://files.pythonhosted.org/packages/1f/0d/2116763ade7e71e0e5d421babe1785d745be9b3d605bf914792ce1c97f79/fonttools-4.65.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:924d06e6130429168318db71c40174a765ad016fc4b56ca811287e3d7373b3a6", size = 5524117, upload-time = "2026-09-10T15:34:00.021Z" }, + { url = "https://files.pythonhosted.org/packages/e2/5e/f9600553b9f645e3068553831685ff1dab6259536a23b38d2e048de38f17/fonttools-4.65.0-cp312-cp312-win32.whl", hash = "sha256:04f73dd01005752a6e75cf4a8dc6b70dc724d1d4bc34cc89522153f4a2f07680", size = 2432089, upload-time = "2026-09-10T15:34:02.803Z" }, + { url = "https://files.pythonhosted.org/packages/3a/02/e436a6a1863b9862bab9f82d6da33055dd7aa3738017edd902a163525dc2/fonttools-4.65.0-cp312-cp312-win_amd64.whl", hash = "sha256:3b5d9ba89edf778b376e669b879ae33a198bf45cf5a23c3f6514f935cf9d0d9d", size = 2483475, upload-time = "2026-09-10T15:34:05.09Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5c/343a4225e83eb06f82c1d8bf41fc5e5f71eab2f62bc7ca215c764722c0a9/fonttools-4.65.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8b7bb52817a24731d2e4f4df0e71fdde05e6c806c8f8f1517b015d142fdacfa5", size = 3094663, upload-time = "2026-09-10T15:34:07.235Z" }, + { url = "https://files.pythonhosted.org/packages/9b/c9/49b2401be932741d9218181c08948c96db32eab21ecaaf79283b752e13e7/fonttools-4.65.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e2c21772fcf70325189707b19f346812690bb1b0bd7e207e6ac205244806b303", size = 2584521, upload-time = "2026-09-10T15:34:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/51/c9/48b07e6c5cf44fa56f758a04c3772a66c0e9ba82bbe22077e4076746a62b/fonttools-4.65.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64c9b26816415b5e3d899e9077109d327b22140fe3c4066644d8cdbad5bb1569", size = 5395399, upload-time = "2026-09-10T15:34:12.38Z" }, + { url = "https://files.pythonhosted.org/packages/fa/2d/5cc5a10c8ed56079d6c2e9e3e5920622529a2ae045c9f47a6055ccb1a319/fonttools-4.65.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6dd6243f60e2d6160c2966e1e14020dc261ffd741b69a2e4ca8bfd051592e4b7", size = 5376659, upload-time = "2026-09-10T15:34:15.239Z" }, + { url = "https://files.pythonhosted.org/packages/ab/6f/ccf33739d936bb3afa1655a225be7ee5d63d6d3c8b7d0e570bc5a2a7b899/fonttools-4.65.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:834962fd7cf21c58e81ac50a59e6ed2306f9df5e3dd481dad1cd7d2c4c60b773", size = 5336795, upload-time = "2026-09-10T15:34:17.817Z" }, + { url = "https://files.pythonhosted.org/packages/7e/c1/ffd17483f2094f0f4118974295b514b5b82afd4f6e3c80c23f01684e97a6/fonttools-4.65.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:580eb68ff7bd6954a7a76afddd864bfc66eaaf5f5c20dd6ead9186d0055a4ffe", size = 5496530, upload-time = "2026-09-10T15:34:20.241Z" }, + { url = "https://files.pythonhosted.org/packages/b1/19/9aca7712d0676ba5f8d1530ce20478a9bb09cccd0153d56693337379cdcf/fonttools-4.65.0-cp313-cp313-win32.whl", hash = "sha256:7a18b2ffd44249fe84289253197aa65ad4f2de554c0d381f18b1f5939bc6bc60", size = 2430392, upload-time = "2026-09-10T15:34:22.906Z" }, + { url = "https://files.pythonhosted.org/packages/1f/6f/f015dea0f4354e0798751b657cea2dee482914737f88bf1a078349ce92cf/fonttools-4.65.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ae1846b0f192fd485d26a455af19b8f5cf05aff08f9836f533913d8fcea133c", size = 2481620, upload-time = "2026-09-10T15:34:25.816Z" }, + { url = "https://files.pythonhosted.org/packages/64/29/606365ef601668bfebed14cfe3dc72bb7fcd1e23011bbb2833f17fea3065/fonttools-4.65.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:dc87a9f846bec83c3795804f62b4632716d46e3522869a3dd9cd44a5d245b006", size = 3098393, upload-time = "2026-09-10T15:34:28.472Z" }, + { url = "https://files.pythonhosted.org/packages/c7/61/11412939d6b7abf5ac7ce0d61d7f94a0a4fbabc9f1ab0a04fa622e0fc11c/fonttools-4.65.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:aa50dd7b9baf75e2bbd43401fc0d237f7a94a8ad2e0c57ea97160fc631af5eb0", size = 2585766, upload-time = "2026-09-10T15:34:31.207Z" }, + { url = "https://files.pythonhosted.org/packages/db/17/734921d8aee8309801da42590375d32d4d46f771b73373ec9520d5d4220b/fonttools-4.65.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0d2a9892fdb3b7e2d0f4174e3b907d226ff83698249762eeefce08ec5b2de1dd", size = 5380679, upload-time = "2026-09-10T15:34:33.933Z" }, + { url = "https://files.pythonhosted.org/packages/cf/eb/2a4d78d60d978e694cfa04c98e4d8ddbf7f028fd768ddef470bb9da5d69e/fonttools-4.65.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6d815734e7fede0ad1f233f23f0f191cbe8fc64762ff041e589bc0f78e0b2397", size = 5321833, upload-time = "2026-09-10T15:34:36.295Z" }, + { url = "https://files.pythonhosted.org/packages/d9/ca/1cd48b5c11ef9658732787bf2362e1bf3871dad5945d2f6cc8f675ca769c/fonttools-4.65.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:71e4c67b6196a2f447f46476fd2302604721617f5e0a21b0988bdd87b6bb9687", size = 5320938, upload-time = "2026-09-10T15:34:38.992Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0d/90e6051bded926cccabe9dd0bce3b6ca012f4d5779d61167afbf4989ceb6/fonttools-4.65.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b11d8a4a0c3ca74bbd4c105b7ef82501945c939e6096d9934ec7d288cdf5aaa9", size = 5451747, upload-time = "2026-09-10T15:34:41.387Z" }, + { url = "https://files.pythonhosted.org/packages/18/74/23e0268e48029ff0752083f69312c49b738163d5af919add9dc4ac81e907/fonttools-4.65.0-cp314-cp314-win32.whl", hash = "sha256:8e44a34d91b3c793879767eb115867ced74d2eb94974e64e72fe9e2eea71cf1a", size = 2434246, upload-time = "2026-09-10T15:34:44.385Z" }, + { url = "https://files.pythonhosted.org/packages/a1/2d/ee69affecd4bc81cb932a213438d4199fb48bf8ca6d664438ccc7f623c2a/fonttools-4.65.0-cp314-cp314-win_amd64.whl", hash = "sha256:0aa8901db22875c831d6a91796549590d7e747da37438f38b69d771b668be445", size = 2486706, upload-time = "2026-09-10T15:34:46.842Z" }, + { url = "https://files.pythonhosted.org/packages/8e/9c/edee5f785198ce3327e1ddeace91c773122d47f086a67e0a84b800f4a940/fonttools-4.65.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:2e4a380ca40d3a5372e31b340f0da0d53b4583aadbb8e41f6a516afa69c509a4", size = 3172027, upload-time = "2026-09-10T15:34:49.269Z" }, + { url = "https://files.pythonhosted.org/packages/9c/c6/252ec9884381089bc30da75978b072593920249de60219817f16cbc9145f/fonttools-4.65.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:661bd91c4be13721408b2d4b67a9b3fa7736713adc9a6c9780c9c60fc7959f90", size = 2619020, upload-time = "2026-09-10T15:34:51.453Z" }, + { url = "https://files.pythonhosted.org/packages/42/79/f71b0d202b8473bb45b07876c08a474de9fe560ed2c0cde642812a81e22a/fonttools-4.65.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62c5e42c79449def957adf8a9a65a43018efa7e2a6bc6baa3afe955e0d5fb2ab", size = 5545942, upload-time = "2026-09-10T15:34:54.804Z" }, + { url = "https://files.pythonhosted.org/packages/4a/bd/52e1bf33e0aebfe22ecc9a85c634db707c1dd9f6b1b438efeed98c55b959/fonttools-4.65.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:36fca8efc46b5adfca327c666e739fc05b7a7a6ef17840230f81b22f53230f61", size = 5351477, upload-time = "2026-09-10T15:34:57.514Z" }, + { url = "https://files.pythonhosted.org/packages/5c/76/8c6b2ad20beec95cd446f3a8bdc753c7e4a4fd69ef3e66704c0f7c8cb0b5/fonttools-4.65.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8aa1291e4c767abf1b0b79ca2d6895f7c0b661d9d95d03b5791c883a9d1e1f08", size = 5412271, upload-time = "2026-09-10T15:35:00.895Z" }, + { url = "https://files.pythonhosted.org/packages/79/49/fadbf11bbbd2d699d88a5498a0634280206e01e3bd5da9a4e0c504953ce9/fonttools-4.65.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fcf39949f56911348514b466714efa9118bec3d2be249e1c487263f7cda6edab", size = 5446450, upload-time = "2026-09-10T15:35:03.369Z" }, + { url = "https://files.pythonhosted.org/packages/df/77/5fda646d3a6d5ee26465865c00319cc0925cf484a3b42dd8232d5b39f973/fonttools-4.65.0-cp314-cp314t-win32.whl", hash = "sha256:ffc918702661f1d74d2fbb2f5551036b64f6d2d743139e105289b694bcd16f54", size = 2467858, upload-time = "2026-09-10T15:35:05.744Z" }, + { url = "https://files.pythonhosted.org/packages/ae/0f/afa0f3de70ebe02bba46b32cccb30b1de52624472b14ac2e7cd403d08db9/fonttools-4.65.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5a977e3645dbffaee924209828aa702a215f7ff68bc08010740c10c723787e62", size = 2518248, upload-time = "2026-09-10T15:35:07.846Z" }, + { url = "https://files.pythonhosted.org/packages/86/54/b273cf5712b36a381c13284fbf244e811e1e1d7081aed20f200f0a191ffa/fonttools-4.65.0-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:7aa0518b45ff5286ad56f063938db3add3816e899aab58d782b3f9a252523caa", size = 3092770, upload-time = "2026-09-10T15:35:10.868Z" }, + { url = "https://files.pythonhosted.org/packages/36/1d/d3e4511475954d4ec4f3c254e81f0fcc1ade504cb7381eeca48b8c44b8a6/fonttools-4.65.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:673e2b3ac4ac8e4f3607d390ecc5a606e5db5c4e88fb4cb2999593efb65afea2", size = 2584364, upload-time = "2026-09-10T15:35:13.54Z" }, + { url = "https://files.pythonhosted.org/packages/e5/3f/7cfaba467bdd1d3d04be21ba7b5bd75c6ca58ba280e0519707ca96a43c4d/fonttools-4.65.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a03cff943b204a90bf3d1c04c97b9509a8aa0ee99e2e544084ca43ad995975b", size = 5377819, upload-time = "2026-09-10T15:35:15.839Z" }, + { url = "https://files.pythonhosted.org/packages/25/17/a68d9b19a97bb2ee37e8098fea50657073df97c7e5392afbe1b9d7c0c581/fonttools-4.65.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:41f684ee6212e411196ab054f8308faf6605f154950e6f4686fb8f2103d624b0", size = 5341296, upload-time = "2026-09-10T15:35:18.527Z" }, + { url = "https://files.pythonhosted.org/packages/ad/8d/d744653ed607a241339015d4af6743ca3d85a74a2045a02cc06ea0383c71/fonttools-4.65.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:52ea9d2a8385075770db74d5e5718fa80b2222bb4fc62856a377dd2865ca8848", size = 5315310, upload-time = "2026-09-10T15:35:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c8/1ca6dc69cbaa0e394ff70d9e266777124d3ce3c6433026a6b4de50b890ae/fonttools-4.65.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:d0d25027ade65ec46b13c0436e51bcb7c5171a4ea255a5e7a8d0d1d3ab4cffd7", size = 5463840, upload-time = "2026-09-10T15:35:24.022Z" }, + { url = "https://files.pythonhosted.org/packages/07/97/d374df38a14f2ac04ba9ed96bca89d4988e7d63aa5ed85c54fd8f2badd7d/fonttools-4.65.0-cp315-cp315-win32.whl", hash = "sha256:22cb846d35d278235ef3b7e947c6040b2057d72e8305a314f21d5342eca49040", size = 2433149, upload-time = "2026-09-10T15:35:26.59Z" }, + { url = "https://files.pythonhosted.org/packages/62/c5/eb8f7506faf6a70c5a8f2eccbeea3cd826c748a6fd78c7b4b3effb5a3a11/fonttools-4.65.0-cp315-cp315-win_amd64.whl", hash = "sha256:aecc899fdbf9ecbf728f8977977e2e1043ee4d70c257124c8fa4cbcf796fcd83", size = 2485725, upload-time = "2026-09-10T15:35:28.945Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a4/2df0d97514feb8d857d5de854cd8d660d9a7ee1fd081bc98497124f515a8/fonttools-4.65.0-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:6275863dad195ee34b6e0ca3fc61c74096bc37e5d6fb8e049f4d68d65865a2b7", size = 3164065, upload-time = "2026-09-10T15:35:31.197Z" }, + { url = "https://files.pythonhosted.org/packages/15/33/e09661c09e6c8a3b0bd50da0e72918df7576dede31f39c948cc245011434/fonttools-4.65.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:d8ffd2f62b402180b0edae8f86a071f583970e2177143117db5cf4c52da60079", size = 2615195, upload-time = "2026-09-10T15:35:33.569Z" }, + { url = "https://files.pythonhosted.org/packages/64/d7/114b05f4193679d0935272220083ec43de7f918b5a777018c5ac5c1ae39e/fonttools-4.65.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:830f91327ca83bfc1278e7060068a498938f84d05dc4869675486f84f55d4fe1", size = 5521239, upload-time = "2026-09-10T15:35:36.291Z" }, + { url = "https://files.pythonhosted.org/packages/ba/10/67d615939f859ffe75663a67bca3593966febbcd0109ec72f20f73cbb4cc/fonttools-4.65.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9db2cb95847c18eef74a4ef0fe257a893ae3f4b0395f4866e2f426ab07f3d804", size = 5343802, upload-time = "2026-09-10T15:35:38.82Z" }, + { url = "https://files.pythonhosted.org/packages/d1/06/faa793a806da03acce862fbed353f76026b28103d43860d69be9a6a1f0fd/fonttools-4.65.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:be9b9a95ed0af03375e99020e921c4bc6b41fad10e053dea7acad370521a3c46", size = 5388482, upload-time = "2026-09-10T15:35:41.616Z" }, + { url = "https://files.pythonhosted.org/packages/53/d2/eb7258df60e634db60c9a8cd72bc9eaf8dea409d1b1ceec3b9fb49531ad2/fonttools-4.65.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:bbd9faf777a9deb6790df4f2b0be611857c45fe86605e840d7154a028d828af7", size = 5435088, upload-time = "2026-09-10T15:35:44.639Z" }, + { url = "https://files.pythonhosted.org/packages/00/6a/58597f16e1265fe9205de3069e338a339e3eef0b1daf049ee08269458091/fonttools-4.65.0-cp315-cp315t-win32.whl", hash = "sha256:c779d838815b91889c95ed64c9be5950ad5a683279f91aeb23384cb757ddc6a3", size = 2464925, upload-time = "2026-09-10T15:35:47.224Z" }, + { url = "https://files.pythonhosted.org/packages/4b/95/122fc172006db747f4968e08c710f52a94f55eb007173b859b3f80b5b810/fonttools-4.65.0-cp315-cp315t-win_amd64.whl", hash = "sha256:d9484b7ee1b49b6b8a0231c849f3983723dec29e3a7366d9b1b02f4036f71944", size = 2513945, upload-time = "2026-09-10T15:35:49.895Z" }, + { url = "https://files.pythonhosted.org/packages/e6/35/f894ceb867118c0261d0f69a9bd516b045a3754238f76c88a49513ac7a83/fonttools-4.65.0-py3-none-any.whl", hash = "sha256:3060b8c1fc2329fa20265b7c138614143ea7c1624e26c5c180c76aeb74deae6f", size = 1196441, upload-time = "2026-09-10T15:35:52.347Z" }, +] + [[package]] name = "frozenlist" version = "1.8.0" @@ -1042,26 +1829,87 @@ wheels = [ ] [[package]] -name = "googleapis-common-protos" -version = "1.73.0" +name = "fsspec" +version = "2026.7.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/99/96/a0205167fa0154f4a542fd6925bdc63d039d88dab3588b875078107e6f06/googleapis_common_protos-1.73.0.tar.gz", hash = "sha256:778d07cd4fbeff84c6f7c72102f0daf98fa2bfd3fa8bea426edc545588da0b5a", size = 147323, upload-time = "2026-03-06T21:53:09.727Z" } +sdist = { url = "https://files.pythonhosted.org/packages/00/78/f34251dadb8f3921264a1d9b8946f5e542014ee2614b285261b4e40e6775/fsspec-2026.7.0.tar.gz", hash = "sha256:c803c40f4cf860b49dea58ee3e1c33cb9c790520e233537e1340049f89b82a88", size = 317040, upload-time = "2026-07-28T16:34:51.052Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/28/23eea8acd65972bbfe295ce3666b28ac510dfcb115fac089d3edb0feb00a/googleapis_common_protos-1.73.0-py3-none-any.whl", hash = "sha256:dfdaaa2e860f242046be561e6d6cb5c5f1541ae02cfbcb034371aadb2942b4e8", size = 297578, upload-time = "2026-03-06T21:52:33.933Z" }, + { url = "https://files.pythonhosted.org/packages/fd/3c/6a2bf344106328fd04963664a60b9bb6496fc25df8e962fcdc1367285fb9/fsspec-2026.7.0-py3-none-any.whl", hash = "sha256:b57ddbafedfaef7018c1ecab32aa200a9d7ca26b77965f64e48b70061249d279", size = 206583, upload-time = "2026-07-28T16:34:49.538Z" }, ] [[package]] -name = "grpcio" -version = "1.82.1" +name = "google-api-core" +version = "2.36.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/90/bc/656b89387d6f4ed7e0686c7b64c2ae7e554a759aa58122c8e5fb99392c32/grpcio-1.82.1.tar.gz", hash = "sha256:707b24abd90fcb1e45bcc080577da1dbf9971d107490589b9539af8e1e77b4b5", size = 13187300, upload-time = "2026-07-08T12:36:16.588Z" } -wheels = [ + { name = "google-auth" }, + { name = "googleapis-common-protos" }, + { name = "opentelemetry-api" }, + { name = "proto-plus" }, + { name = "protobuf" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/d8/88c2f0e6b0dd46a7796cca64fad99c7adba2417916f5393e82b9b7d2548e/google_api_core-2.36.0.tar.gz", hash = "sha256:32779307b52e64c9a9592a3621de6281676ecaeea299fe8524e4637ab7ac2531", size = 196879, upload-time = "2026-09-03T22:30:51.216Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/56/30c91c61b8f70d4c09285a005b94798729aeaf4ec8b90c1c360da8207728/google_api_core-2.36.0-py3-none-any.whl", hash = "sha256:e4d0b179260727ea5c42222426d9199285214dbef7bf48f8b16600c7f9a78944", size = 184118, upload-time = "2026-09-03T22:30:06.662Z" }, +] + +[package.optional-dependencies] +grpc = [ + { name = "grpcio" }, + { name = "grpcio-status" }, +] + +[[package]] +name = "google-auth" +version = "2.58.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyasn1-modules" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/ca/f398a483ce5aad18ca2f735646e45ccee2439bd94a41a4ad0cfa646bd495/google_auth-2.58.0.tar.gz", hash = "sha256:55e30cf15e737de92c5323d78cda8a83fcd57e7ffbaf900c4600039fd60a80fd", size = 380018, upload-time = "2026-09-09T20:49:38.043Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/13/477d90d09591b3938b45c4e11f4d8a51291682112cb5efcac961e815d562/google_auth-2.58.0-py3-none-any.whl", hash = "sha256:8a9c4645bb4c8e91668fb1934b95ae6a8687084232753639220ba9bf04a1610d", size = 262404, upload-time = "2026-09-09T20:49:33.951Z" }, +] + +[[package]] +name = "google-cloud-vision" +version = "3.15.0" +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/dc/15/7b24e071fa8ee431ea44663ef3c18351cecae45c6433d25fbc64cdb3836b/google_cloud_vision-3.15.0.tar.gz", hash = "sha256:ed2b79ca05a58a929ec112259a9caa698509f6348762a8eced3310e6572a6c70", size = 589231, upload-time = "2026-06-22T23:22:47.231Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c1/29130149389b654c206e50772a845ff3a375686238b5e73c0c0d94e05f99/google_cloud_vision-3.15.0-py3-none-any.whl", hash = "sha256:6293186bb824a986cdf73bcc83ad0e44a1ef26942347b7e01598061619bb66d4", size = 542216, upload-time = "2026-06-22T23:20:49.89Z" }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.73.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/96/a0205167fa0154f4a542fd6925bdc63d039d88dab3588b875078107e6f06/googleapis_common_protos-1.73.0.tar.gz", hash = "sha256:778d07cd4fbeff84c6f7c72102f0daf98fa2bfd3fa8bea426edc545588da0b5a", size = 147323, upload-time = "2026-03-06T21:53:09.727Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/28/23eea8acd65972bbfe295ce3666b28ac510dfcb115fac089d3edb0feb00a/googleapis_common_protos-1.73.0-py3-none-any.whl", hash = "sha256:dfdaaa2e860f242046be561e6d6cb5c5f1541ae02cfbcb034371aadb2942b4e8", size = 297578, upload-time = "2026-03-06T21:52:33.933Z" }, +] + +[[package]] +name = "grpcio" +version = "1.82.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/bc/656b89387d6f4ed7e0686c7b64c2ae7e554a759aa58122c8e5fb99392c32/grpcio-1.82.1.tar.gz", hash = "sha256:707b24abd90fcb1e45bcc080577da1dbf9971d107490589b9539af8e1e77b4b5", size = 13187300, upload-time = "2026-07-08T12:36:16.588Z" } +wheels = [ { url = "https://files.pythonhosted.org/packages/a3/14/5d05bfd85c101cbe44a12d7c1cea9c40698e0438cddf3a70019f735b5a27/grpcio-1.82.1-cp310-cp310-linux_armv7l.whl", hash = "sha256:91859d1cac5f47caec5fc40e9f827500cdb54ce5b36450dc9a65616b5af49c17", size = 6177087, upload-time = "2026-07-08T12:34:06.825Z" }, { url = "https://files.pythonhosted.org/packages/19/2e/c906f8e6d0b54c0137885fff6f7b5883c6bbc381b44a0ba5ea07d7d1579b/grpcio-1.82.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:c80c9741dcef192f669876a81957cf7713b441c2f0c43631350d75fa49321d31", size = 11960907, upload-time = "2026-07-08T12:34:10.583Z" }, { url = "https://files.pythonhosted.org/packages/de/be/ec4aa76cdf25539b9e960cbb9d5739f892ea6cde58078b5293860c1159d3/grpcio-1.82.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b89cff456796d2f0581783726ad017a2c70aff2d27b0f05504c34e2e417f7560", size = 6754802, upload-time = "2026-07-08T12:34:13.082Z" }, @@ -1200,6 +2048,43 @@ 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 = "hf-xet" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/ab/522a2ab67f27971a9d48ca666d4fca85ef7d5282d142e31fd087e27b1bbe/hf_xet-1.6.0.tar.gz", hash = "sha256:2e58454a340b3556dfa4972d5451aff4fba8dd42a236600ba1a1d2b1514f0fef", size = 920527, upload-time = "2026-08-03T22:33:13.243Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/62/3c062f593bd92ef4e77a0ef39541e3d82a0a1d3947c8a777a02a13a27828/hf_xet-1.6.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:70cbb9c896901600128cb9b6f06e132954fbede1db30f31f7c6c63f84cb7c31d", size = 4074584, upload-time = "2026-08-03T22:32:47.364Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1e/c0ad437dd267a8e435bef594acf781bbc3874ff0b6435b4962d03ecf7cc4/hf_xet-1.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:23379c2f9ec8696d952b16414a2bae72cad86a52df869b050698ba60f538c675", size = 3867381, upload-time = "2026-08-03T22:32:49.049Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ee/7c0d7b6ab336167531b1c30af2af003f054af4c749becbd7209ae33a77c3/hf_xet-1.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f2f7278c05c22fd60cb436cda1269649b3e81db65ecdc8496e5e164aa4143e7b", size = 4453982, upload-time = "2026-08-03T22:32:50.568Z" }, + { url = "https://files.pythonhosted.org/packages/63/06/ad8eab1c9525246650cbaa821caa3cdbaca734ab1a5b8c91bea09cbd8d69/hf_xet-1.6.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:948f15d3a9545cfe5932f6bd8b440f6ae630aee108f14b7bd6c561f7c2dcc522", size = 4249445, upload-time = "2026-08-03T22:32:52.391Z" }, + { url = "https://files.pythonhosted.org/packages/d8/26/1eee8aedb0dafc1ab9717dc9ac602cde33361b232dc06803f1f6ed18b58c/hf_xet-1.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5153e6bb103ad49d6ea9f1b2e230db5a2ea32551ad09a706d2f61d7c7c80d80e", size = 4451099, upload-time = "2026-08-03T22:32:54.114Z" }, + { url = "https://files.pythonhosted.org/packages/67/57/0b88af1f194ab6c9c650547d9cc06bfeaab836ae4dcdb331676bfb8be95a/hf_xet-1.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:35cec30d75c6f9eb9c16a77cef68e85a103b72e24d4b473714ec9ff06428bab9", size = 4664712, upload-time = "2026-08-03T22:32:55.547Z" }, + { url = "https://files.pythonhosted.org/packages/53/a0/26b717a9d1840e8abf48dcec64b5ed8fbe472671d38ad28d30e147132b33/hf_xet-1.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5789835d7c6bc9436962853192082374297fb72d7eff7e7762ec25ceb7e25338", size = 4025906, upload-time = "2026-08-03T22:32:57.391Z" }, + { url = "https://files.pythonhosted.org/packages/49/f6/4a9966633c6fef83af997e2cff68ec1963676d412bdfd096df2a93b8e185/hf_xet-1.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:75765820ce4700db3750c94acc8fe27c5fae4c9ec000a0dbac3ca082acf97765", size = 3849221, upload-time = "2026-08-03T22:32:59.123Z" }, + { url = "https://files.pythonhosted.org/packages/a2/50/7afa2c9c787405864fc47a0d1bbc02c62e9101947ed43c1f43899fc7d91d/hf_xet-1.6.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:633dc0cd71d32da58ab8c03ad38e2fac452c15c2b0a2866ebf6ededfe0a5061d", size = 4071729, upload-time = "2026-08-03T22:33:00.721Z" }, + { url = "https://files.pythonhosted.org/packages/4b/69/55b8dcf636142ae660fec1869fcac14c4da2e8412e14d6eee1523be77e9f/hf_xet-1.6.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:f0906082d9932ae0c0057fa194041c22b4e2cdb46b2592ef3b91f020d62a081a", size = 3876287, upload-time = "2026-08-03T22:33:02.251Z" }, + { url = "https://files.pythonhosted.org/packages/67/4e/a28359bf1c1ecf11eba22123168c138698f7cb576ac678f5a2e16cd5da08/hf_xet-1.6.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d62671bb130879cef0ee4c9ebe47a14af6c66ec53e6d84dc15936e5ffdfac82f", size = 4464663, upload-time = "2026-08-03T22:33:03.802Z" }, + { url = "https://files.pythonhosted.org/packages/9a/69/1f0cbc2fb22ae6082d094f743d1b8945a3f36f6089cb95f42b7ee348cda7/hf_xet-1.6.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0e6e21fa3cdfcdcd76748564bf593870a5e013f47d97cf10aed63aa222cff5b7", size = 4262538, upload-time = "2026-08-03T22:33:05.287Z" }, + { url = "https://files.pythonhosted.org/packages/d1/3a/4f4f2301ade26e404462d3336fa11f7958d914cabbabdd6e03c3c5d5658c/hf_xet-1.6.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4fc74352a17015bd0ee90038bc9efe38db894cde45f268b6712b04fce8cd0acb", size = 4460520, upload-time = "2026-08-03T22:33:06.81Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5f/311725e2a905534dfee2dcb5b08414f249147f1f12252bfc2bd24caa075c/hf_xet-1.6.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8fb4f71cba6129110c3374a33f919001ff130488fc23553698e34cc1c2a1198c", size = 4675937, upload-time = "2026-08-03T22:33:08.616Z" }, + { url = "https://files.pythonhosted.org/packages/98/b7/8c59a66d15205024662f1d66968136f13893f96df1ddc5087e2e281fc95f/hf_xet-1.6.0-cp38-abi3-win_amd64.whl", hash = "sha256:fb4fadde1b2b70bf4c0c14a6dccbe7194b1c28947fefd5bbe3fed9d940676c3b", size = 4033128, upload-time = "2026-08-03T22:33:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/73/63/ca511b6f802f28cf3489b280fe77475bcca8de85e81a6299d7916b5b5555/hf_xet-1.6.0-cp38-abi3-win_arm64.whl", hash = "sha256:3dc3e35441ba395006af5aaacc40ef2e603c51ef46c3530b9156185f00935ea3", size = 3859359, upload-time = "2026-08-03T22:33:11.725Z" }, +] + +[[package]] +name = "html5lib" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/b6/b55c3f49042f1df3dcd422b7f224f939892ee94f22abcf503a9b7339eaf2/html5lib-1.1.tar.gz", hash = "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f", size = 272215, upload-time = "2020-06-22T23:32:38.834Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/dd/a834df6482147d48e225a49515aabc28974ad5a4ca3215c18a882565b028/html5lib-1.1-py2.py3-none-any.whl", hash = "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d", size = 112173, upload-time = "2020-06-22T23:32:36.781Z" }, +] + [[package]] name = "httpcore" version = "1.0.9" @@ -1237,6 +2122,71 @@ 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 = "huggingface-hub" +version = "1.16.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform != 'win32'", +] +dependencies = [ + { name = "filelock", marker = "python_full_version < '3.11' and sys_platform != 'win32'" }, + { name = "fsspec", marker = "python_full_version < '3.11' and sys_platform != 'win32'" }, + { name = "hf-xet", marker = "(python_full_version < '3.11' and platform_machine == 'AMD64' and sys_platform != 'win32') or (python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform != 'win32') or (python_full_version < '3.11' and platform_machine == 'amd64' and sys_platform != 'win32') or (python_full_version < '3.11' and platform_machine == 'arm64' and sys_platform != 'win32') or (python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform != 'win32')" }, + { name = "httpx", marker = "python_full_version < '3.11' and sys_platform != 'win32'" }, + { name = "packaging", marker = "python_full_version < '3.11' and sys_platform != 'win32'" }, + { name = "pyyaml", marker = "python_full_version < '3.11' and sys_platform != 'win32'" }, + { name = "tqdm", marker = "python_full_version < '3.11' and sys_platform != 'win32'" }, + { name = "typer", marker = "python_full_version < '3.11' and sys_platform != 'win32'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11' and sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/0f/ed994dbade67a54407c28cab96ef845e0e6d25500be56aca6394f8bfc9dd/huggingface_hub-1.16.1.tar.gz", hash = "sha256:7f1dc4c5ec21aed69be630ad0c3378616be16f3de1a47b141c0e812965d9c832", size = 792534, upload-time = "2026-05-21T18:40:00.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/79/621a7dbb80c70974f73a597275351ebe03ce5bc65cb5f8f4acb5859252bc/huggingface_hub-1.16.1-py3-none-any.whl", hash = "sha256:64340de934b9ce37857ef85a82de72f5629e8a270f9119eabb12bf495eb53c22", size = 668176, upload-time = "2026-05-21T18:39:58.596Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "1.31.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version < '3.11' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'win32'", +] +dependencies = [ + { name = "click", version = "8.5.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform != 'win32') or (python_full_version < '3.11' and sys_platform == 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, + { name = "filelock", marker = "(python_full_version >= '3.11' and sys_platform != 'win32') or (python_full_version < '3.11' and sys_platform == 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, + { name = "fsspec", marker = "(python_full_version >= '3.11' and sys_platform != 'win32') or (python_full_version < '3.11' and sys_platform == 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, + { name = "hf-xet", marker = "(python_full_version >= '3.11' and platform_machine == 'AMD64' and sys_platform != 'win32') or (python_full_version >= '3.11' and platform_machine == 'aarch64' and sys_platform != 'win32') or (python_full_version >= '3.11' and platform_machine == 'amd64' and sys_platform != 'win32') or (python_full_version >= '3.11' and platform_machine == 'arm64' and sys_platform != 'win32') or (python_full_version >= '3.11' and platform_machine == 'x86_64' and sys_platform != 'win32') or (python_full_version < '3.11' and platform_machine == 'AMD64' and sys_platform == 'win32') or (python_full_version == '3.12.*' and platform_machine == 'AMD64' and sys_platform == 'win32') or (python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'win32') or (python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'win32') or (python_full_version < '3.11' and platform_machine == 'amd64' and sys_platform == 'win32') or (python_full_version == '3.12.*' and platform_machine == 'amd64' and sys_platform == 'win32') or (python_full_version < '3.11' and platform_machine == 'arm64' and sys_platform == 'win32') or (python_full_version == '3.12.*' and platform_machine == 'arm64' and sys_platform == 'win32') or (python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'win32') or (python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'win32')" }, + { name = "httpx", marker = "(python_full_version >= '3.11' and sys_platform != 'win32') or (python_full_version < '3.11' and sys_platform == 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, + { name = "packaging", marker = "(python_full_version >= '3.11' and sys_platform != 'win32') or (python_full_version < '3.11' and sys_platform == 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, + { name = "pyyaml", marker = "(python_full_version >= '3.11' and sys_platform != 'win32') or (python_full_version < '3.11' and sys_platform == 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, + { name = "tqdm", marker = "(python_full_version >= '3.11' and sys_platform != 'win32') or (python_full_version < '3.11' and sys_platform == 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, + { name = "typing-extensions", marker = "(python_full_version >= '3.11' and sys_platform != 'win32') or (python_full_version < '3.11' and sys_platform == 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/f0/61159db90b5cd275d55516fe27920828e7d3be4053fdbdb27c3f70e5f1ef/huggingface_hub-1.31.0.tar.gz", hash = "sha256:f8e9e710a210613fa5d0f26bba6da05ef4aef9fba5a0f23f508f5ac4d08b6f90", size = 968039, upload-time = "2026-09-10T10:27:22.724Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/7f/3f886a625043b77312b80da2f2bf00b5ecbf5a73061af1aa0259cd258c9d/huggingface_hub-1.31.0-py3-none-any.whl", hash = "sha256:9dbb6a503cbe2494ea666695207e7262d410659e09134059deb83e5480864667", size = 798313, upload-time = "2026-09-10T10:27:20.798Z" }, +] + +[[package]] +name = "humanfriendly" +version = "10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyreadline3", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702, upload-time = "2021-09-17T21:40:43.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794, upload-time = "2021-09-17T21:40:39.897Z" }, +] + [[package]] name = "idna" version = "3.11" @@ -1255,6 +2205,24 @@ 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 = "installer" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/18/ceeb4e3ab3aa54495775775b38ae42b10a92f42ce42dfa44da684289b8c8/installer-0.7.0.tar.gz", hash = "sha256:a26d3e3116289bb08216e0d0f7d925fcef0b0194eedfa0c944bcaaa106c4b631", size = 474349, upload-time = "2023-03-17T20:39:38.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/ca/1172b6638d52f2d6caa2dd262ec4c811ba59eee96d54a7701930726bce18/installer-0.7.0-py3-none-any.whl", hash = "sha256:05d1933f0a5ba7d8d6296bb6d5018e7c94fa473ceb10cf198a92ccea19c27b53", size = 453838, upload-time = "2023-03-17T20:39:36.219Z" }, +] + +[[package]] +name = "isodate" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, +] + [[package]] name = "itsdangerous" version = "2.2.0" @@ -1276,6 +2244,105 @@ 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 = "jiter" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/1f/10936e16d8860c70698a1aa939a46aa0224813b782bce4e000e637da0b2d/jiter-0.16.0.tar.gz", hash = "sha256:7b24c3492c5f4f84a37946ad9cf504910cf6a782d6a4e0689b6673c5894b4a1c", size = 176431, upload-time = "2026-06-29T13:05:13.657Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/d8/b959609e44012a42b1f3e5ba98ea3b33c7e41e6d4b77cd8f00fd19b1d3ad/jiter-0.16.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c5fc4f8def331036a7b8e981b4347ebe409981edbc8308a5ea842b8c3614fa6c", size = 310082, upload-time = "2026-06-29T13:02:31.356Z" }, + { url = "https://files.pythonhosted.org/packages/c6/3d/4d7f5667ea0e0548534ba880b84bb3d12924fd133aa83ad6c6c80fca3d76/jiter-0.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5a71d0d2014c3275043e1170bf3d4e771493cb0dcf07be54c567155f4d8ee64b", size = 315643, upload-time = "2026-06-29T13:02:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/9b/83/bed2dcb5c9f3e1ccfcbc67dda48265fe7d5ad0c9cadda5fe95f6e3b87f94/jiter-0.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:741eed508c233a76313a1c7b001f8f21b82f14327e9196ae8bd29a2cc164ae84", size = 341363, upload-time = "2026-06-29T13:02:34.853Z" }, + { url = "https://files.pythonhosted.org/packages/f4/2f/6bb3c3dda668ebc0445689c81a2b0f26a82b10843d67ed9c9b2c3edc177f/jiter-0.16.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3fb7bc819187b56dc48aa5c833aaf92257da8e07efdb9306156667bd2eeb491c", size = 365483, upload-time = "2026-06-29T13:02:36.295Z" }, + { url = "https://files.pythonhosted.org/packages/92/35/8a045ccb39164e70dcdae696413b661771f148b68b12b175c3a04d901937/jiter-0.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c9610fd25ebccb43fca584136f5c2fbb26802447eccd430dfdbab95a0fd5126", size = 461219, upload-time = "2026-06-29T13:02:38.116Z" }, + { url = "https://files.pythonhosted.org/packages/e7/99/22292dbbf0ed0c610cfe5ddc7f3bd67237a412f121318f865196e62a07bd/jiter-0.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4a1d68ff7ca1d3b5dee20a97a3decda7d5f15003823bf6d140c81f8561d3bc5c", size = 374905, upload-time = "2026-06-29T13:02:40.357Z" }, + { url = "https://files.pythonhosted.org/packages/29/ac/2f55ccb1f0eeafa6d89d24caf52f6f0944a59290ee199e9ade62177dca42/jiter-0.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb08c276dd02dac3a284acdd02cacc630d2e3cd6572a4b85519f35cbd133c3de", size = 348320, upload-time = "2026-06-29T13:02:41.923Z" }, + { url = "https://files.pythonhosted.org/packages/50/e3/7d88b9174c40064fabc07c84a9b62e6b10f5644562ec0e0a29392edbe978/jiter-0.16.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:8fc4d94713c4697347e38faf7d6ef91547c142219bdcfc7220c4870879974244", size = 356519, upload-time = "2026-06-29T13:02:43.436Z" }, + { url = "https://files.pythonhosted.org/packages/27/57/c4a33aeef513a9d5e26e31534e0bcc752d6ea0e54c94ddb7b68bade669c2/jiter-0.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a0f05e229edb29e68cdd0ccb83cea13b64263416120cf943767a6fd72e6787f", size = 394204, upload-time = "2026-06-29T13:02:44.987Z" }, + { url = "https://files.pythonhosted.org/packages/9d/70/c6c23e76ebb3766b111bc399437bbc9f870a76e2a92e10b2a5f561d57372/jiter-0.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2c842cbf374a8daf50b2c04212995bee34ca2ac2cdc29a901b4cdb072c9c4131", size = 521477, upload-time = "2026-06-29T13:02:46.724Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d3/0001c8c0c5976af2625bb1cfb1895e8ec693b6589fe4574b8e6fc2c85501/jiter-0.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5ed466aee31294d7cdcd4d37dfe5c42c97bc29d9a5f00eacf24504358309cb9b", size = 552187, upload-time = "2026-06-29T13:02:48.144Z" }, + { url = "https://files.pythonhosted.org/packages/f6/76/311b718e07e85740e48619c0632b36f7e0b8d113984499e436452ed13a9a/jiter-0.16.0-cp310-cp310-win32.whl", hash = "sha256:b42e9ff5376819c053da25809a8d4b6fa6e473b4856ebe42e298ac958be3d7f9", size = 206513, upload-time = "2026-06-29T13:02:49.515Z" }, + { url = "https://files.pythonhosted.org/packages/db/7f/ac680eeb0777dc0eb7dc824800ba27880d7f6bc712e362d34ad8ee559f36/jiter-0.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:10438939205546132189c8e74a2d536a707841f3a25cd7c74ee91fe503407a26", size = 199505, upload-time = "2026-06-29T13:02:50.829Z" }, + { url = "https://files.pythonhosted.org/packages/4e/3f/fae6cc967d120ec89e31c5418a51176d8278b3087fbb384a9176754f353c/jiter-0.16.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:67fddeda1688f0cce2d2ae83ccf8a80f79936f2d2997d6cc2261f82fdb54a4d3", size = 309289, upload-time = "2026-06-29T13:02:52.301Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e3/97c6c3562c077f6247d6e6ce5c82562500b6316c0d928e97e106b7a1321a/jiter-0.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c90c0f63df322be920eda6ce622e3083d8906ba267f8220fe7873213b8b4430e", size = 315181, upload-time = "2026-06-29T13:02:53.964Z" }, + { url = "https://files.pythonhosted.org/packages/7b/89/d8d073f8aa2667e46c6c0873f86fe4a512bba4293cc730f626a076211a62/jiter-0.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64c0203212098470032aabcde9356fc168f377aade3e43def61dfe17e92f2037", size = 340939, upload-time = "2026-06-29T13:02:55.412Z" }, + { url = "https://files.pythonhosted.org/packages/87/c9/db4fda3ed73fb864139305e935e5b8b38a5a24692a5a9dd356c22f1b9c8d/jiter-0.16.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12288303c9844e61e1651d02a9a6f6633e47d39f897d6991d1427161ce6b746e", size = 364932, upload-time = "2026-06-29T13:02:57.28Z" }, + { url = "https://files.pythonhosted.org/packages/a2/74/52b5e86241057f52ddd7c9a580f90effb51f9d06239f6fc612279b91a838/jiter-0.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cf109d010b4b05a105afb3d43be36a21322d345ad3111e13d15f680afef0e5b", size = 461132, upload-time = "2026-06-29T13:02:58.994Z" }, + { url = "https://files.pythonhosted.org/packages/a9/87/544a700f7447c1f31c5d7833821a4daa5683165c2d5a094fbf5b5800c3dc/jiter-0.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62c1b7fe1f77925acf5af68b6140b8810fa87dfd4dc0a9c8568ec2fa2a10429c", size = 374857, upload-time = "2026-06-29T13:03:00.455Z" }, + { url = "https://files.pythonhosted.org/packages/40/cd/0fcc3f7d39183674d5bfa9ec640faaeb506c60be7c8f94625dfba366e37c/jiter-0.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8597d23c87f59294f83bcb6229b9ed1fccee13dbba967b46930d2f1759466fee", size = 347053, upload-time = "2026-06-29T13:03:02.045Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ae/c7e64e7932ad597fa395b61440b249ada6366716e25c6e08dd2afbd021e6/jiter-0.16.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:3126a5dbad56401989ac769aca0cb56005bfb3e2366eea0ca99d1a91c3c1ee03", size = 356153, upload-time = "2026-06-29T13:03:03.706Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/1c719044f14da814e1a060191ab19b96f3e99207bc5b4bfc6d6be34b3f80/jiter-0.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c4b4717bdb35ae456f831a6b08d01880fff399887a6bbc526a583a406e484eea", size = 393956, upload-time = "2026-06-29T13:03:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/3b/dc/7b2f303a2847207e265503853a2d964a55354cffd62a5f2936c155486798/jiter-0.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:adff21bc78edfe086c15eb495b900306076de378dc2337c132401fc39bd79c91", size = 521081, upload-time = "2026-06-29T13:03:06.886Z" }, + { url = "https://files.pythonhosted.org/packages/c2/5f/501cf6e1e09caeb420195179ffc6f62aca603f1220ec53fd80d0d70b3e56/jiter-0.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dab907db06fc593645e73109acf4581ba5b548897d28b9348dc41ddc8343b2d3", size = 552085, upload-time = "2026-06-29T13:03:08.339Z" }, + { url = "https://files.pythonhosted.org/packages/79/54/aa5be86520113b79455c3877f3d1f07a348098df4083ba3688e9537e52dd/jiter-0.16.0-cp311-cp311-win32.whl", hash = "sha256:560b2cf3fb03240cd34f27409a238547488708f05b7c3924f571a60422251ec7", size = 206755, upload-time = "2026-06-29T13:03:09.653Z" }, + { url = "https://files.pythonhosted.org/packages/64/ec/2feb893eb330bd69b413866f4d5daada33c3962f1c6f270c91ca2d87fdf9/jiter-0.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:e431cfc9caf44c1d5459ff77d4e64cbf85fddb6a35dad836a15c6a9ec23087c1", size = 199155, upload-time = "2026-06-29T13:03:10.979Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9c/ca040d94415048a3666fc237774df8151c96f8d2b661cbe3b184acc95876/jiter-0.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:2a8e9e39cf083016137aa5cadafe3188adc2ba6ba1fbf1e5d18889ad3e9ad056", size = 194403, upload-time = "2026-06-29T13:03:12.341Z" }, + { url = "https://files.pythonhosted.org/packages/83/2b/52ace16ed031354f0539749a49e4bf33797d82bea5137910835fa4b09793/jiter-0.16.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:67c3bc1760f8c99d805dcab4e644027142a53b1d5d861f18780ebdbd5d40b72a", size = 306943, upload-time = "2026-06-29T13:03:14.035Z" }, + { url = "https://files.pythonhosted.org/packages/94/2e/34957c2c1b661c252ba9bcc60ae0bddc27e0f7202c6073326a13c5390eec/jiter-0.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5af7780e4a26bd7d0d989592bf9ef12ebf806b74ab709223ecca37c749872ea9", size = 307779, upload-time = "2026-06-29T13:03:15.418Z" }, + { url = "https://files.pythonhosted.org/packages/88/6c/59bd309cab4460c54cf1079f3eb7fe7af6a4c895c5c957a53378693bad2b/jiter-0.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5bf78d0e05e45cfdd66558893938d59afe3d1b1a824a202039b20e607d25a72", size = 335826, upload-time = "2026-06-29T13:03:17.11Z" }, + { url = "https://files.pythonhosted.org/packages/3b/8c/f5ef7b65f0df47afa16596969defb281ebb86e96df346d62be6fd853d620/jiter-0.16.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4444a83f946605990c98f625cdd3d2725bfb818158760c5748c653170a20e0e", size = 362573, upload-time = "2026-06-29T13:03:18.781Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0b/ace4354da061ee38844a0c27dc2c21eecd27aea119e8da324bea987522d0/jiter-0.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a23f0e4f957e1be65752d2dfac9a5a06b1917af8dc85deb639c3b9d02e31290", size = 457979, upload-time = "2026-06-29T13:03:20.293Z" }, + { url = "https://files.pythonhosted.org/packages/55/40/c0253d3772eb9dcd8e6606ee9b2d53ec8e5b814589c47f140aa585f21eaa/jiter-0.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c22a488f7b9218e245a0025a9ba6b100e2e54700831cf4cf16833a27fba3ad01", size = 372302, upload-time = "2026-06-29T13:03:21.739Z" }, + { url = "https://files.pythonhosted.org/packages/a8/d2/4839422241aa12860ce597b20068727094ba0bc480723c74924ca5bad483/jiter-0.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46add52f4ad47a08bfb1219f3e673da972191489a33016edefdb5ea55bfa8c48", size = 343805, upload-time = "2026-06-29T13:03:23.384Z" }, + { url = "https://files.pythonhosted.org/packages/e2/59/e196888a05befdda7dbe299b722d56f2f6eec65402bc34c0a3306d595feb/jiter-0.16.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:9c8a956fd72c2cf1e730d01ea080341f13aa0a97a4a33b51abebe725b7ae9ca9", size = 351107, upload-time = "2026-06-29T13:03:24.815Z" }, + { url = "https://files.pythonhosted.org/packages/ec/74/4cd9e0fca65232136400354b630fbfcd2de634e22ccbb96567725981b548/jiter-0.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:561926e0573ffe4a32498420a76d64b16c513e1ab413b9d28158a8764ac701e5", size = 388441, upload-time = "2026-06-29T13:03:26.266Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8c/554691e48bc711299c0a293dd8a6179e24b2d66a54dc295421fcf64569c0/jiter-0.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:44d019fa8cdaf89bf29c71b39e3712143fdd0ac76725c6ef954f9957a5ea8730", size = 516354, upload-time = "2026-06-29T13:03:28.02Z" }, + { url = "https://files.pythonhosted.org/packages/a4/cb/01e9d69dc2cc6759d4f91e230b34489c4fdb2518992650633f9e20bece89/jiter-0.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0df91907609837f33341b8e6fe73b95991fdaa57caf1a0fbd343dffe826f386f", size = 547880, upload-time = "2026-06-29T13:03:29.534Z" }, + { url = "https://files.pythonhosted.org/packages/79/70/2953195f1c6ad00f49fa67e13df7e60acb3dd4f387101bc15abccddd905e/jiter-0.16.0-cp312-cp312-win32.whl", hash = "sha256:51d7b836acb0108d7c77df1742332cac2a1fa04a74d6dacec46e7091f0e91274", size = 203473, upload-time = "2026-06-29T13:03:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/05/2909a8b10699a4d560f8c502b6b2c5f3991b682b1922c1eedda242b225bd/jiter-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:1878349266f8ee36ecb1375cc5ba2f115f35fd9f0a1a4119e725e379126647f7", size = 196905, upload-time = "2026-06-29T13:03:32.472Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a9/6b82bb1c8d7790d602489b967b982a909e5d092875a6c2ade96444c8dfc5/jiter-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:2ed5738ae4af18271a51a528b8811b0cbfa4a1858de9d83359e4169855d6a331", size = 190618, upload-time = "2026-06-29T13:03:34.672Z" }, + { url = "https://files.pythonhosted.org/packages/91/c0/555fc60473d30d66894ba825e63615e3be7524fac23858356afa7a38906c/jiter-0.16.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:41977aa5654023948c2dae2a81cbf9c43343954bef1cd59a154dd15a4d84c195", size = 306203, upload-time = "2026-06-29T13:03:36.243Z" }, + { url = "https://files.pythonhosted.org/packages/d0/2b/c3eaf16f5d7c9bad66ea32f40a95bd169b29a91217fcc7f081375157e99c/jiter-0.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d28bb3c26762358dadf3e5bf0bccd29ae987d65e6988d2e6f49829c76b003c09", size = 306489, upload-time = "2026-06-29T13:03:37.846Z" }, + { url = "https://files.pythonhosted.org/packages/96/3f/02fdfc6705cad96127d883af5c34e4867f554f29ec7705ec1a46156400a9/jiter-0.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0542a7189c26920778658fc8fcf2af8bae05bae9924577f71804acef37996536", size = 335453, upload-time = "2026-06-29T13:03:39.221Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a6/e4bda5920d4b0d7c5dfb7174ce4a6b2e4d3e11c9162c452ef0eab4cdbdbd/jiter-0.16.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8fb8de1e23a0cb2a7f53c335049c7b72b6db41aa6227cdcc0972a1de5cb39450", size = 361625, upload-time = "2026-06-29T13:03:40.597Z" }, + { url = "https://files.pythonhosted.org/packages/b7/97/4e6b59b2c6e55cbb3e183595f81ad65dcfb21c915fee5e19e335df21bc55/jiter-0.16.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b72d0b2990ca754a9102779ac98d8597b7cb31678958562214a007f909eab78e", size = 456958, upload-time = "2026-06-29T13:03:42.074Z" }, + { url = "https://files.pythonhosted.org/packages/15/e0/97e9557686d2f94f4b93786eccb7eed28e9228ad132ea8237f44727314a7/jiter-0.16.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f91b1c27fc22a57993d5a5cb8a627cb8ed4b10502716fac1ffbfe1d19d84e8", size = 372017, upload-time = "2026-06-29T13:03:43.658Z" }, + { url = "https://files.pythonhosted.org/packages/0f/94/db768b6938e0df35c86beeba3dfbbb025c9ee5c19e1aa271f2396e50864d/jiter-0.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c682bea068a90b764577bdb78a60a4c1d1606daf9cd4c893832a37c7cc9d9026", size = 343320, upload-time = "2026-06-29T13:03:45.226Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d6/5a59d938244a30735fe62d9433fd325f9021ea29d89780ea4596ea93bc89/jiter-0.16.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:8d031aabecc4f1b6276adfb42e3aabb77c89d468bf616600e8d3a11328929053", size = 350520, upload-time = "2026-06-29T13:03:46.671Z" }, + { url = "https://files.pythonhosted.org/packages/67/f8/c4a857f49c9af125f6bbcac7e3eee7f7978ed89682833062e2dbf62576b1/jiter-0.16.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eab2cd170150e70153de16896a1774e3a1dca80154c56b54d7a812c479a7165e", size = 387550, upload-time = "2026-06-29T13:03:48.361Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d6/5fbc2f7d6b67b754caa61a993a2e626e815dec47ffc2f9e35f01adfebec7/jiter-0.16.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:6edb63a46e65a82c26800a868e49b2cac30dd5a4218b88d74bc2c848c8ad60bb", size = 515424, upload-time = "2026-06-29T13:03:49.881Z" }, + { url = "https://files.pythonhosted.org/packages/ed/54/284f0164b64a5fed915fea6ba7e9ba9b3d8d37c67d59cf2e3bb99d45cdfe/jiter-0.16.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:659039cc50b5addcc35fcc87ae2c1833b7c0a8e5326ef631a75e4478447bcf84", size = 546981, upload-time = "2026-06-29T13:03:51.363Z" }, + { url = "https://files.pythonhosted.org/packages/13/c5/2a467585a576594384e1d2c43e1224deaafc085f24e243529cf98beef8e1/jiter-0.16.0-cp313-cp313-win32.whl", hash = "sha256:c9c53be232c2e206ef9cdbad81a48bfa74c3d3f08bcf8124630a8a748aad993e", size = 202853, upload-time = "2026-06-29T13:03:53.015Z" }, + { url = "https://files.pythonhosted.org/packages/88/6a/de61d04b9eec69c71719968d2f716532a3bc121170c44a39e14979c6be81/jiter-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:baad945ed47f163ad833314f8e3288c396118934f94e7bbb9e243ce4b341a4fd", size = 196160, upload-time = "2026-06-29T13:03:54.447Z" }, + { url = "https://files.pythonhosted.org/packages/19/4b/b390ed59bafb3f31d008d1218578f10327714484b334439947f7e5b11e7f/jiter-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:3c1fd2dbe1b0af19e987f03fe66c5f5bd105a2229c1aff4ab14890b24f41d21a", size = 189862, upload-time = "2026-06-29T13:03:55.754Z" }, + { url = "https://files.pythonhosted.org/packages/a7/89/bc4f1b57d5da938fd344a466396541e586d161320d70bffd929aaafcd8f4/jiter-0.16.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b2c61484666ad42726029af0c00ef4541f0f3b5cdc550221f56c2343208018ee", size = 308239, upload-time = "2026-06-29T13:03:57.205Z" }, + { url = "https://files.pythonhosted.org/packages/65/7a/c415453e5213001bf3b411ff65dec3d303b0e76a4a2cfea9768cd4960994/jiter-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:63efadc657488f45db1c676d81e704cac2abf3fdb892def1faea61db053127e2", size = 308928, upload-time = "2026-06-29T13:03:58.643Z" }, + { url = "https://files.pythonhosted.org/packages/11/fc/1f4fb7ebf9a724c7741994f4aae18fba1e2f3133df14521a79194952c34a/jiter-0.16.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf0d73f50e7b6935677854f6e8e31d499ca7064dd24734f703e060f5b237d883", size = 336998, upload-time = "2026-06-29T13:04:00.071Z" }, + { url = "https://files.pythonhosted.org/packages/a0/8d/72cadaac05ccfa7cc3a0a2232862e6c72443ca40cf300ba8b57f9f18b69b/jiter-0.16.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3ea07d9bc8e7d03a9fbc051295462e6dbc295b894fd72457c3136e3e43d898", size = 362112, upload-time = "2026-06-29T13:04:01.52Z" }, + { url = "https://files.pythonhosted.org/packages/58/4a/c4b0d5f651fda90a24ffce9f8d56cde462a2e09d31ae3de3c68cef34c04e/jiter-0.16.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26798522707abb47d767db536e4148ceac1b14446bf028ee85e579a2e043cfe5", size = 459807, upload-time = "2026-06-29T13:04:03.214Z" }, + { url = "https://files.pythonhosted.org/packages/80/58/ef77879ea9aa56b50824edc5a445e226422c7a8d211f3fd2a56bcb9493cf/jiter-0.16.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bc837c1b9631be10abfe0191537fe8009838204cec7e44827401ace390ddb567", size = 373181, upload-time = "2026-06-29T13:04:04.629Z" }, + { url = "https://files.pythonhosted.org/packages/49/2e/ffbc3f254e4d8a66da3062c624a7df4b7c2b2cf9e1fe43cf394b3e104041/jiter-0.16.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49060fd70737fad59d33ba9dcc0d83247dc9e77187de26053a19c16c9f32bd69", size = 344927, upload-time = "2026-06-29T13:04:06.067Z" }, + { url = "https://files.pythonhosted.org/packages/9a/f6/0be5dc6d64a89f80aa8fec984f94dedb2973e251edcae55841d60786d578/jiter-0.16.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:adbb8edeadd431bc4477879d5d371ece7cb1334486584e0f252656dd7ffada29", size = 352754, upload-time = "2026-06-29T13:04:07.477Z" }, + { url = "https://files.pythonhosted.org/packages/da/6e/7d31243b3b91cd261dd19e9d3557fc3251a80883d3d8049c86174e7ab7af/jiter-0.16.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:31aaee5b80f672c1dc21272bcfb9cbdcfc1ea04ff50f00ed5af500b80c44fa93", size = 390553, upload-time = "2026-06-29T13:04:08.92Z" }, + { url = "https://files.pythonhosted.org/packages/25/33/51ae371fde3c88897520f62b4d5f8b27ad7103e2bb10812ff52195609853/jiter-0.16.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:6722bcef4ffc86c835574b1b2fac6b33b9fb4a889c781e67950e891591f3c55a", size = 516900, upload-time = "2026-06-29T13:04:10.407Z" }, + { url = "https://files.pythonhosted.org/packages/a0/45/6449b3d123ea439ba79507c657288f461d55049e7bcbdc2cf8eb8210f491/jiter-0.16.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:5ab4f50ff971b611d656554ea10b75f80097392c827bc32923c6eeb6386c8b00", size = 548754, upload-time = "2026-06-29T13:04:12.046Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e7/fd2fb11ae3e2649333da3aa170d04d7b3000bbdc3b270f6513382fdf4e04/jiter-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:710cc51d4ebdcd3c1f70b232c1db1ea1344a075770422bbd4bede5708335acbe", size = 122381, upload-time = "2026-06-29T13:04:13.413Z" }, + { url = "https://files.pythonhosted.org/packages/26/80/f0b147a62c315a164ed2168908286ca302310824c218d3aae52b06c0c9a9/jiter-0.16.0-cp314-cp314-win32.whl", hash = "sha256:57b37fc887a32d44798e4d8ebfa7c9683ff3da1d5bf38f08d1bb3573ccb39106", size = 204578, upload-time = "2026-06-29T13:04:14.813Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e6/4758a14304b4523a6f5adb2419340086aa3593bd4327c2b25b5948a90548/jiter-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:cbd18dd5e2df96b580487b5745adf57ef64ad89ba2d9662fc3c19386acce7db8", size = 198154, upload-time = "2026-06-29T13:04:16.272Z" }, + { url = "https://files.pythonhosted.org/packages/26/be/41fa54a2e7ea41d6c99f1dc5b1f0fd4cb474680304b5d268dd518e81da3a/jiter-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:a32d2027a9fa67f109ff245a3252ece3ccc32cc56703e1deab6cc846a59e0585", size = 191458, upload-time = "2026-06-29T13:04:17.707Z" }, + { url = "https://files.pythonhosted.org/packages/81/6b/59127338b86d9fe4d99418f5a15118bea778103ee0fe9d9dd7e0af174e95/jiter-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2577196f4474ef3fc4779a088a23b0897bbf86f9ea3679c372d45b8383b43207", size = 316739, upload-time = "2026-06-29T13:04:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/2d/95/49461034d5388196d3dabf98748935f017b7785d8f3f5349f834bcc4ed0d/jiter-0.16.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e89e008a93c01104161c75b4988e58716b01d62307ebfe161e52a56d2a818", size = 340911, upload-time = "2026-06-29T13:04:21.257Z" }, + { url = "https://files.pythonhosted.org/packages/cd/97/a4369f2fb82cb3dda13b98622f31249b2e014b223fe64ee534413ad72294/jiter-0.16.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e2e9efbe042210df657bade597f66d6d75723e3d8f45a12ea6d8167ff8bbce3", size = 361747, upload-time = "2026-06-29T13:04:22.677Z" }, + { url = "https://files.pythonhosted.org/packages/28/51/49b6ed456261646e1906016a6760367a28aacd3c24805e4e5fe64116c1db/jiter-0.16.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f4d9e473a5ce7d27fef8b848df4dc16e283893d3f53b4a585e72c9595f3c284", size = 460225, upload-time = "2026-06-29T13:04:24.441Z" }, + { url = "https://files.pythonhosted.org/packages/33/b5/5689aff4f66c5b60be63106e591dbfcba2190df97d2c9c7cf052361ddb98/jiter-0.16.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d30a4a1c87713060c8d1cc59a7b6c8fb6b8ef0a6900368014c76c87922a2929", size = 373169, upload-time = "2026-06-29T13:04:25.884Z" }, + { url = "https://files.pythonhosted.org/packages/a2/96/3ae1b85ee0d6d6cab254fb7f8da018272b932bbf2d69b07e98aa2a96c746/jiter-0.16.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae96332410f866e5900d809298b1ed82735932986c672495f9701daacd80620", size = 350332, upload-time = "2026-06-29T13:04:27.302Z" }, + { url = "https://files.pythonhosted.org/packages/15/32/c99d7bafd78986556c95bf60ce84c6cc98786eac56066c12d7f828bb6747/jiter-0.16.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:da3d7ec75dc83bb18bca888b5edfae0656a26849056c59e05a7728badd17e7af", size = 353377, upload-time = "2026-06-29T13:04:28.731Z" }, + { url = "https://files.pythonhosted.org/packages/0e/4b/f99a8e571287c3dec766bcc18528bbe8e8fb5365522ab5e6d64c93e87066/jiter-0.16.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ee6162b77d49a9939229df666dfa8af3e656b6701b54c4c84966d740e189264e", size = 387746, upload-time = "2026-06-29T13:04:30.319Z" }, + { url = "https://files.pythonhosted.org/packages/75/69/c78a5b3f71040e34eb5917df26fb7ae9a2174cad1ccbf277512507c53a6e/jiter-0.16.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:63ffdbdae7d4499f4cda14eadc12ddcabef0fc0c081191bdc2247489cb698077", size = 517292, upload-time = "2026-06-29T13:04:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f7/095b38eda4c70d03651c403f29a5590f16d12ddc5d544aac9f9cddf72277/jiter-0.16.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a111256a7193bea0759267b10385e5870949c239ed7b6ddbaaf57573edb38734", size = 549259, upload-time = "2026-06-29T13:04:33.721Z" }, + { url = "https://files.pythonhosted.org/packages/2e/c5/6a0207d90e5f656d95af98ebd0934f382d37674416f215aeda2ff8063e51/jiter-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:de5ba8763e56b793561f43bed197c9ea55776daa5e9a6b91eed68a909bc9cdbf", size = 206523, upload-time = "2026-06-29T13:04:35.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/31/c757d5f30a8980fd945ce7b98be10be9e4ff59c7c42f5fd86804c2e87db8/jiter-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b8a3f9a6008048fe9def7bf465180564a6e458047d2ce499149cfbe73c3ae9db", size = 200366, upload-time = "2026-06-29T13:04:36.61Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a2/d88de6d313d734a544a7901353ad5db67cb38dcfcd91713b7979dafc345d/jiter-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0fa25b09b13075c46f5bc174f2690525a925a4fc2f7c82969a2bbabff22386ce", size = 190516, upload-time = "2026-06-29T13:04:38.004Z" }, + { url = "https://files.pythonhosted.org/packages/06/d3/8e278946d43eeca2585b4dd0834a887cd71136329b837f3a16ed86a8b4b0/jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:850ccb1d7eedb4200f4014b1c0e8a577de114fc3cd88faad646dcc9bc4bb12ad", size = 304518, upload-time = "2026-06-29T13:05:00.172Z" }, + { url = "https://files.pythonhosted.org/packages/72/43/28d4ef495028bf0506a413d4db3f4eb3e7288a382e0f065f306a17bbeb5e/jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:e34e97bda77eb63242a410243c071e28ac7e0d8c0948c5ee658498690a4b2f2f", size = 310207, upload-time = "2026-06-29T13:05:02.123Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ca/c366b1012da1d640de975d9683acd44e4d150d9068845d0ca2610435253f/jiter-0.16.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7dc85ea77d4abbae8bad0d3538678aedee75bceec4e2f6c8dfb1c74772e5aa5", size = 342771, upload-time = "2026-06-29T13:05:03.55Z" }, + { url = "https://files.pythonhosted.org/packages/16/52/50cc4056fc1ae02e7154704e7ecc89df0afb8300222cfe8a52d3f67e4730/jiter-0.16.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17ca7fae79f6d99cd9a042b75f917eaada7b895cfc7dd2ee3a16089dcaec7a85", size = 346468, upload-time = "2026-06-29T13:05:05.452Z" }, + { url = "https://files.pythonhosted.org/packages/98/ab/664fd8c4be028b2bedd3d2ff08769c4ede23d0dbc87a77c62384a0515b5d/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:f17d61a28b4b3e0e3e2ba98490c70501403b4d196f78732439160e7fd3678127", size = 303106, upload-time = "2026-06-29T13:05:07.118Z" }, + { url = "https://files.pythonhosted.org/packages/1a/07/421f1d5b65493a76e16027b848aba6a7d28073ae75944fa4289cc914d39f/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:96e38eea538c8ddf853a35727c7be0741c76c13f04148ac5c116222f50ece3b3", size = 304658, upload-time = "2026-06-29T13:05:08.708Z" }, + { url = "https://files.pythonhosted.org/packages/0a/db/bba1155f01a01c3c37a89425d571da751bbedf5c54247b831a04cb971798/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d284fb8d94d5855d60c44fefcab4bf966f1da6fada73992b01f6f0c9bc0c6702", size = 339719, upload-time = "2026-06-29T13:05:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/78/f7/18a1afcd64f35314b68c1f23afcd9994d0bc13e65cc77517afff4e83986d/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d613743df53199b1aa256a7d328340da6d7078aac7705a7db9d7a791e9cfd2", size = 343885, upload-time = "2026-06-29T13:05:12.087Z" }, +] + [[package]] name = "jmespath" version = "1.1.0" @@ -1285,6 +2352,18 @@ 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.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cloudpickle", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d5/1d/537ab090f302b838943a1b56497dd53059b9a9b46a074936470173a2e207/joblib-1.6.0.tar.gz", hash = "sha256:2ccc96785b12046c08fd6d55839c12857831b54a3c1673ffadd2f04bfc4eda03", size = 327903, upload-time = "2026-08-31T09:39:04.122Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/53/84099323c2ec4be98d935f63c033ac4151ee83836ca1050ede3b3aadf155/joblib-1.6.0-py3-none-any.whl", hash = "sha256:3dbbf9f6e4b592a2357b854608e980fe6390d131d7a82f011a377ef2ebef7aba", size = 306115, upload-time = "2026-08-31T09:39:02.298Z" }, +] + [[package]] name = "jsonpatch" version = "1.33" @@ -1333,6 +2412,154 @@ 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 = "kiwisolver" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/07/bd78e6a8fae171ea041ef5bba3ed21a003522fa088834b069b1909981f30/kiwisolver-1.5.1.tar.gz", hash = "sha256:f1303ef2eec81262a4b708c3e858afe58d7c75ad91c1c05266eda7673369859a", size = 104395, upload-time = "2026-08-28T10:28:27.153Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/54/1b8d2bc580414cc75b3a5b3d195981a6facacd8a8d986e589d0a3b51a709/kiwisolver-1.5.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1acc7e5b7ef05e9da8bb70cd6c7c4513090213d2e1ad9720f599f0bf6c52aec5", size = 123362, upload-time = "2026-08-28T10:24:43.402Z" }, + { url = "https://files.pythonhosted.org/packages/57/19/92c30f540dcfff86ff625389427c39b53a9aaea16420ddcc09b2ab8d1073/kiwisolver-1.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bad20d4c69c851c982a1e3606f4c293edfd5a87885786c50082412240c4b1ffd", size = 66551, upload-time = "2026-08-28T10:24:44.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ae/30b94088722e7bce8b821cd5ee935a87f80023e7243125f1accf98e39bf7/kiwisolver-1.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0a4faea5c6db201c6a21391d2ac926ea97acf7dacdbc3c417189e1adb1a00837", size = 64076, upload-time = "2026-08-28T10:24:46.184Z" }, + { url = "https://files.pythonhosted.org/packages/f1/0e/9f394d693be549fa3fab62498c2778294595991047adc3dcf10aa99b91c0/kiwisolver-1.5.1-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e05c2f7925f1d88778e53cb44f14e0223204a3bdd09a41664750363acfb1f2ef", size = 1628782, upload-time = "2026-08-28T10:24:47.91Z" }, + { url = "https://files.pythonhosted.org/packages/64/96/26efc04348c0f332b6e6c471dff132bb8b18b2df494a1b65826601674e62/kiwisolver-1.5.1-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3a4e41e3096bf1f0f1b76e2ffd6d828d6547f574f702d59bdbef7acfa59db9c", size = 1228112, upload-time = "2026-08-28T10:24:49.503Z" }, + { url = "https://files.pythonhosted.org/packages/5a/ac/df9ddc19ec972cd97b60b262dd2c4be28c6192b5a779cad429c6657ee680/kiwisolver-1.5.1-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1d56ec54d257d05e0b50f5780d967540cd07beeaf9e5f645b26d50cce79f4d8", size = 1246756, upload-time = "2026-08-28T10:24:51.225Z" }, + { url = "https://files.pythonhosted.org/packages/cf/de/c38a246b8a4f4b293c7291aa650917b05c5549b0d532749bc523296e90c4/kiwisolver-1.5.1-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8de6f2a4ce7e7bd27d23dd94abf0ccafe0e0e5cc9c764b0577191f2c25f08f26", size = 1295555, upload-time = "2026-08-28T10:24:52.953Z" }, + { url = "https://files.pythonhosted.org/packages/b6/48/3c95a5232d8b5783050d8c8089b58bf1f57018f9135232d72c5aa6b0c01b/kiwisolver-1.5.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:16895f553ee6620a827d2da56b871f835fb70b9216cca5d188e885caf6e3bd23", size = 2179476, upload-time = "2026-08-28T10:24:54.568Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e9/b9e999336afc561b154e93682f597832bdc7446f2ee18b467c5b0e924867/kiwisolver-1.5.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:6b92f60017dda7d877fdc546438b5e28f31c523264f49cf5a48c1d0ce1a0dfbc", size = 2275258, upload-time = "2026-08-28T10:24:56.186Z" }, + { url = "https://files.pythonhosted.org/packages/76/cd/c1c550796ada4a59644b8264b425fe332686d2a086c4729fcc640ececdfe/kiwisolver-1.5.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:7824b5e8bdbf0bccb4ccd37bbb115849a1dc45437fb4de8351385ed07c437ee0", size = 2443424, upload-time = "2026-08-28T10:24:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d4/30f641eb8e10f6ad5bb9af8a15ec8eea386162a6ef491e68995b8680ef4e/kiwisolver-1.5.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:aa7d00b1700966d2917e54d278aba86897890ca9276dd8b76cf6446b6c181b92", size = 2249457, upload-time = "2026-08-28T10:25:00.223Z" }, + { url = "https://files.pythonhosted.org/packages/1c/d9/211e016f8aa5b5bf496dd3bd7ebab4b73b22d91f025f25e20156a44a77fc/kiwisolver-1.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:96c30002424670b5e1e46495c2b8cbffef39cf77c1d79e76462029d50339785b", size = 70643, upload-time = "2026-08-28T10:25:01.888Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8e/ae7007266dea1fd93fcc48541042c1f6583958055de3b424a59ec16def25/kiwisolver-1.5.1-cp310-cp310-win_arm64.whl", hash = "sha256:f0f4a42db92d6ec7677ab9d12830a2a8ec145a9c6d15db2b593466bc875c78d7", size = 68215, upload-time = "2026-08-28T10:25:03.136Z" }, + { url = "https://files.pythonhosted.org/packages/94/7b/2de6908edc668427c149af5f93112e931f87e1fa4cab80bac32c5844dccc/kiwisolver-1.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b3d78f7bb2b9d9a30345be1474b9aaa8685430b54afb51ba3639b5c6c11e9ed6", size = 123364, upload-time = "2026-08-28T10:25:04.359Z" }, + { url = "https://files.pythonhosted.org/packages/8a/24/e70914415c77c97be7e22c80a0740869cb7428768cc380fdcdf6703e7084/kiwisolver-1.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5025e36fb4fb275cef0a4e30dbb11cb4ae61d1c83deb90189cb5d7e4cafd6b55", size = 66558, upload-time = "2026-08-28T10:25:05.506Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2b/8b08b11833db4d475b8ef1f36174f8d8a7abd31bedd7e794be78e8814b48/kiwisolver-1.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc1a26b8e53395a01c2c611e58602fa47461f136fba7cd5542e6db6d64be1839", size = 64071, upload-time = "2026-08-28T10:25:06.7Z" }, + { url = "https://files.pythonhosted.org/packages/89/00/05c2d0369ac322d22d5c05f84b5c4a6856fa6207fbae42869108a28f0383/kiwisolver-1.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:95a02752aa032eef4aed01cda6d9b687c669bd0396bf4519eef8bba22a286720", size = 1438206, upload-time = "2026-08-28T10:25:08.254Z" }, + { url = "https://files.pythonhosted.org/packages/c0/05/c941a139f27438c1910d630fdc3ccfdab7c8407c72052299ead12ece086e/kiwisolver-1.5.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:719a35fa1156db3640555f95ebb94f60a444e64d1c69626b0edef5df78eba225", size = 1248975, upload-time = "2026-08-28T10:25:10.053Z" }, + { url = "https://files.pythonhosted.org/packages/58/a1/2669ee5512e39b9d4de25faacaedf788c957f93730c5f7c63993ec4f5933/kiwisolver-1.5.1-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:febcce10f2bcdbb80b4ea919238a6a4ac13dbc4c7cadbe8d5d75c3682f8b5404", size = 1266301, upload-time = "2026-08-28T10:25:11.754Z" }, + { url = "https://files.pythonhosted.org/packages/28/b8/353f52f2c7f861a9e90cd2e8f90f85b3ad03060835f823e08298d094c463/kiwisolver-1.5.1-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1d852545c4d0e35a72728d072cbaa59e2fa7dd84bdf01e068d670dd0ceb58eb6", size = 1319708, upload-time = "2026-08-28T10:25:13.559Z" }, + { url = "https://files.pythonhosted.org/packages/21/0e/14b83200eadc2c1d63b76bac01c1813bf072aecf567429f303e00b70258e/kiwisolver-1.5.1-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:2e10ae1bba1899188b33557c10d73affcc12033edd18adddb57d209039976a4c", size = 971720, upload-time = "2026-08-28T10:25:14.934Z" }, + { url = "https://files.pythonhosted.org/packages/86/91/9d43d84d23b1cbff72a142d387ead1ea03db0cba8ff86ed5335addad3cc9/kiwisolver-1.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b69602970994a2ed8bbfa78c2f0394a7435226c6040489702d9f0a0ad0c07052", size = 2200119, upload-time = "2026-08-28T10:25:16.636Z" }, + { url = "https://files.pythonhosted.org/packages/ff/7c/f2bd9616f27ffb5e17cecc0baa5d0bbcee7e55aeddc0ccc871d69e2fc3ee/kiwisolver-1.5.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d50de98e8d807dc31822fff96f50293163a62418eb65487a21b42713d72ed0b7", size = 2295005, upload-time = "2026-08-28T10:25:18.374Z" }, + { url = "https://files.pythonhosted.org/packages/ba/17/ee671b72bf8f46a08379d4392c65582541759a542428197562f2898294ad/kiwisolver-1.5.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3221f78211074f561c44ca42eac0619828171bec15a2c4cf6f7747d07df76e8e", size = 1960982, upload-time = "2026-08-28T10:25:19.893Z" }, + { url = "https://files.pythonhosted.org/packages/2e/f7/0e26b4c05bee3bdb0f048dfa305e4fe701999ea17b51e9c616ef91035bbe/kiwisolver-1.5.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0ba9527afc80ae3d7814ed98b6572d02bf85eaf48065678342c5f0c6dab7a8c7", size = 2464918, upload-time = "2026-08-28T10:25:21.65Z" }, + { url = "https://files.pythonhosted.org/packages/ae/62/6eb431133d30ce656ac1e5ff72fac70dd34d54c3984f4011b9ac8bf77d54/kiwisolver-1.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e12dfea7f5fc2a34a9080efbf79c4c44eb380ec5b9c6fea09407e08f0d1e941d", size = 2270967, upload-time = "2026-08-28T10:25:23.643Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9f/6f9e489c188200e6fb3193935501894811e8c97577c8ffe9033589bf3521/kiwisolver-1.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:1a7587dc335f2c0f5bd577fd0540bd16c66006bdb60f759a1059f025e6c4f071", size = 70744, upload-time = "2026-08-28T10:25:25.061Z" }, + { url = "https://files.pythonhosted.org/packages/c6/6d/dfc430d1d43957061599adea3f08ea982bb6f4ab601a8c974bedcf2ba850/kiwisolver-1.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:e4e4523d6f336708d732516e6cfca7796cf3d96c9474eb5aecf6165f2f1fefc3", size = 68404, upload-time = "2026-08-28T10:25:26.186Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9b/65b302742389c6f96f2956bef5decf26011309feb2fc5d79613af18adea4/kiwisolver-1.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:63fb7294b768f444eb4b068965f2662f28c2fd4161e23bd60fcf3ff27b74c046", size = 123876, upload-time = "2026-08-28T10:25:27.44Z" }, + { url = "https://files.pythonhosted.org/packages/71/74/c21f339956f6f691b2ed7e31d5f3ae767304df6c460192739fc830853051/kiwisolver-1.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0ebdef3eae5336568147c39a55be6a2036ffde53faa9ca2d978989ae7c2da12c", size = 66487, upload-time = "2026-08-28T10:25:28.728Z" }, + { url = "https://files.pythonhosted.org/packages/84/e5/bdb34e21523e01dceda064d63713f3bdec91388af24fba1eca7ea5e85864/kiwisolver-1.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1798e83840c3f627246104c4d8a9639c60fa068adf9ce92b61791781fa8a68c1", size = 64660, upload-time = "2026-08-28T10:25:30.071Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f4/dadfec469313c7f428efa7e84b4aba9732f813c13ea7131a24b7b008ef57/kiwisolver-1.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34633ecf50d16187ab8e5528b7a2530f2feb4e23f300db4672538b51cfc5cd38", size = 1477929, upload-time = "2026-08-28T10:25:31.495Z" }, + { url = "https://files.pythonhosted.org/packages/6f/35/09c58daac34e6f6ea5c6dee0094b422118e5a7c265586008a95fd135ac5f/kiwisolver-1.5.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d27c2123977cb9269c30a49ba45f03a4323017ef693e19db4ec9dbe1299a3002", size = 1278499, upload-time = "2026-08-28T10:25:33.375Z" }, + { url = "https://files.pythonhosted.org/packages/19/32/739765e24fbad29d13f83e546ea4abc215a78cea9d677ca09025b027724d/kiwisolver-1.5.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6a797a1cefc8b9c93170db580337e1fe3d011ad18b1299943231279406342048", size = 1296677, upload-time = "2026-08-28T10:25:35.059Z" }, + { url = "https://files.pythonhosted.org/packages/df/32/03304d1010e2cc45e5b3b52cef7e43fed3a2a5cd6c87a89b4a88e1d85b5d/kiwisolver-1.5.1-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2551cf9917af48ee7c4b29cc82320489508cf96fd26a51f6fc124de661cd44c7", size = 1346037, upload-time = "2026-08-28T10:25:36.705Z" }, + { url = "https://files.pythonhosted.org/packages/3e/57/4c49377bfd274450dd72ecaa13eaac32ea804a03363e4d1db0c5aa999ceb/kiwisolver-1.5.1-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:38f6e0deb4d0a4615efe0c4efc5990b06ae450ab50a0b321c0b078b6d238c083", size = 988248, upload-time = "2026-08-28T10:25:38.299Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c3/38df144a08b6c5d75ca4504e5cc3141bb3bfef64c04f4ef48204f42711b6/kiwisolver-1.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bfd1de989b3330420e29de39352f5c049905c9e3ee67233a50d550e3d652c148", size = 2228722, upload-time = "2026-08-28T10:25:40.038Z" }, + { url = "https://files.pythonhosted.org/packages/e7/11/3221838a89cd64d9b386353e000cd8a296069a20fbe3584507fdfd5bebae/kiwisolver-1.5.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1209042a623ddfda5497e4066c7b77651dde8e1d3a9dd97599dc7e97f3b9b78c", size = 2325216, upload-time = "2026-08-28T10:25:41.699Z" }, + { url = "https://files.pythonhosted.org/packages/83/d4/075c219230697bb5db910d37262b9bacf880f92b4811a02ab81ed073a253/kiwisolver-1.5.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:26e8268480be5061d509e29669d59103c067a26377a56491630ece11762e3858", size = 1977689, upload-time = "2026-08-28T10:25:43.559Z" }, + { url = "https://files.pythonhosted.org/packages/bb/08/1d219c3c2dd960983d0d4da623d916e9de6385df2b0bab3d1af0e9b8fccc/kiwisolver-1.5.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d79308fa689fac89cbcfbd4dbfc80b5f95c54c5a7fd4d194be221f9d33d026e6", size = 2491443, upload-time = "2026-08-28T10:25:45.242Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d3/024208ec1079d273f1047468d1bdffbf38bb75b7b268090fd3a0301b9d9a/kiwisolver-1.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b03af77d77e50edba2030fd5f7c352ff209314b09030a3cba7c14edf9a09a444", size = 2295200, upload-time = "2026-08-28T10:25:46.984Z" }, + { url = "https://files.pythonhosted.org/packages/6e/7c/7b210498f9f92e1cd7855f260fa69ef056881087b199ee20c208f0e4189a/kiwisolver-1.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:06a6917674de9e0fe3f66f5430787f59a9f2ddb64af9b714eaec547e29ef5c19", size = 70748, upload-time = "2026-08-28T10:25:48.444Z" }, + { url = "https://files.pythonhosted.org/packages/94/61/ef0daa157c8bb23672f7423e0d14c39db1dc6ef8ed47e6bc54c9c1bef3bf/kiwisolver-1.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:ad8b9671348d7c8716715652ae11f85ed0eb99e265a2df2ca490577d69860b2c", size = 68324, upload-time = "2026-08-28T10:25:49.81Z" }, + { url = "https://files.pythonhosted.org/packages/08/c1/88018321d976f53c421e379c43bc6993e70ce0c8a3ec5edc4bfe102257f6/kiwisolver-1.5.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b6ae6a0328f0bc035741820fdeecdcd67bf4694eee03972e843663107122f450", size = 62272, upload-time = "2026-08-28T10:25:51.02Z" }, + { url = "https://files.pythonhosted.org/packages/85/d2/712bc17ea4f1d216034928069d612defbc6c95a471c55a7203a39faecb1a/kiwisolver-1.5.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:886fc26012f0e8b5f69d1cfe6d711f6b11f194621539bf8e6bb1c25c5dc82724", size = 64481, upload-time = "2026-08-28T10:25:52.22Z" }, + { url = "https://files.pythonhosted.org/packages/1b/e6/6c5380d676f43b6d918033962ea5e72360ca69e5a404154bc496b598ffdb/kiwisolver-1.5.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:aefe930d113798330e9462f7874542977869c0613cba3262e2de3a8d5dee8f3a", size = 66260, upload-time = "2026-08-28T10:25:53.387Z" }, + { url = "https://files.pythonhosted.org/packages/92/6a/7087f5822cc8bb272679641404b1966a42504dac9ee74e2b33840475a0aa/kiwisolver-1.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5ca5aebae78a0bc13c1943af4af615d4966c5b650b05d5aa83b50e427196fee", size = 123876, upload-time = "2026-08-28T10:25:54.644Z" }, + { url = "https://files.pythonhosted.org/packages/5c/4b/9f385087ca09ee5ab9c09c6832561a7d2f7c78d3e5661d511e669f70e439/kiwisolver-1.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1ed0f5e49d0ceff8b72190824d9e59c062fbbc02c231b853112c78474b3f5ec2", size = 66487, upload-time = "2026-08-28T10:25:55.899Z" }, + { url = "https://files.pythonhosted.org/packages/47/8b/40d33ffd2f378094ed462e9a9a0907e59d4de9845e65a59561272da350d4/kiwisolver-1.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:77a4c8187a5948d7f8795adb765a3c7b553d07d86d88e43038fc32fc1fb9a3f3", size = 64673, upload-time = "2026-08-28T10:25:57.048Z" }, + { url = "https://files.pythonhosted.org/packages/ab/43/86aacc027959108b4c66eeae8b73cedb057dfa6eb3a335d05ad65197081c/kiwisolver-1.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:74ad5c3dad54a4641b4c28cd15ded70899d04459c6c7aeacafea716be97cce6d", size = 1477992, upload-time = "2026-08-28T10:25:58.479Z" }, + { url = "https://files.pythonhosted.org/packages/ed/40/b1d0369048c79733a32c8abb0f2718532e6630641368e33a81384246e844/kiwisolver-1.5.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e46b23a2da695c364124817bc01d970effd5483147f8d66a6a7167e3f6b851", size = 1278821, upload-time = "2026-08-28T10:26:00.121Z" }, + { url = "https://files.pythonhosted.org/packages/61/a1/fa71c1792272ff9461432715ae60ffb7e11a4d7ac3bf68961b9cab6c60cf/kiwisolver-1.5.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75d9b1cf8258462dbdc1eeda718c96ea7f079324c09067f6daabfcf37712b7fe", size = 1296805, upload-time = "2026-08-28T10:26:01.868Z" }, + { url = "https://files.pythonhosted.org/packages/a3/49/3f0bd94af8e06ecc47eb834b195a06f05d48711ceb2352c56d6835160f0e/kiwisolver-1.5.1-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fca690b00c4c48f6c2a547b0160ed511357093a4e4c9b47e0fadf3128066d89", size = 1346109, upload-time = "2026-08-28T10:26:03.59Z" }, + { url = "https://files.pythonhosted.org/packages/a4/44/3afe6ef9cf06d61220953a8963e94eca978491be1d9547cb01d82a1efa08/kiwisolver-1.5.1-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:876bbfd276473d3daffe30e8c975df4ed9429967b41a6cb362dbb5155b6f13ad", size = 988252, upload-time = "2026-08-28T10:26:05.306Z" }, + { url = "https://files.pythonhosted.org/packages/e4/30/a12bd7a7285a211e1747c3eec77b8c614dbfcc1dad942f7611a1a6921ae5/kiwisolver-1.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f942903fde7363d1d879057ec5de01310efda2597161784d752fa9953a01a71a", size = 2228846, upload-time = "2026-08-28T10:26:07.312Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6b/233e2958abf0dab7b18d07e52f286e02e519d7651bfbbe97af9347564109/kiwisolver-1.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c90d3022d8a94778939cda8638c6c8da8fa757b8958dad7ec868ce29c87681b8", size = 2325583, upload-time = "2026-08-28T10:26:09.093Z" }, + { url = "https://files.pythonhosted.org/packages/42/8e/7673060a27b01405b580058510adef34687069d229800239f5e44682d4d0/kiwisolver-1.5.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8a34616dc2521cc8dc1d7d081734da63539f021ac0450ce950908340c6e7aa2f", size = 1978221, upload-time = "2026-08-28T10:26:11.127Z" }, + { url = "https://files.pythonhosted.org/packages/3f/7b/1b882fc1a8b4a0bb8084e7d1d85004116d08c92c0705d31f2928dec607f2/kiwisolver-1.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:8bf4df63592c2a66b4f8edc5df2544998c288aa02f96ce0acd880cd1de8c8127", size = 2491819, upload-time = "2026-08-28T10:26:13.348Z" }, + { url = "https://files.pythonhosted.org/packages/39/9c/426deb49e62c5f69464b64bbeca064d3b758a7506b8913d986ef34f4619c/kiwisolver-1.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d09037ca068d784ebc4aec290ef952ca27ac15dd9c0b5801a88c6e1096b83e6b", size = 2295520, upload-time = "2026-08-28T10:26:15.042Z" }, + { url = "https://files.pythonhosted.org/packages/f5/22/deabbb3ad6d918d74b7831b2d8ae7151b09d21c87974e5ee8a456f58c94c/kiwisolver-1.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:dc23390afe9f4ef9ac3bcc72a03a56eebbde03f4c571a32cb38f859cff9a6524", size = 70758, upload-time = "2026-08-28T10:26:16.504Z" }, + { url = "https://files.pythonhosted.org/packages/b9/71/02fa5c2fd92068bb8952847e70ee6c5cb280e7febe11653d17812acc53dd/kiwisolver-1.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:186884a58486651e3c217b6acea0a53eaa9498fdd472057c46f2f0fb5c25aad5", size = 68329, upload-time = "2026-08-28T10:26:17.658Z" }, + { url = "https://files.pythonhosted.org/packages/dd/87/2d5dfad0daf17dcc18d98c48ed2332fc3f051cf599e60be6182a30dd4cf1/kiwisolver-1.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0324cd2567259b7a095f6cf18a52b0ffc6f3de9e69528ff1bc0e7a37bd43ff1a", size = 62337, upload-time = "2026-08-28T10:26:18.778Z" }, + { url = "https://files.pythonhosted.org/packages/08/c8/83e1624f15d6262b470dbcc80b09979fd4d5b2ea3ddfc6b6e3327e235726/kiwisolver-1.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:74ea337e0ec3f6f342a36a4f1b5cd94dd9affddcd28ba9aae2905af932ee8c6b", size = 64513, upload-time = "2026-08-28T10:26:19.909Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2d/827ec30eb07f528c08d8459ffb318ae91a56d793ee8acbea8b491f0ff906/kiwisolver-1.5.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ee9df1f0d77b9c6e94f4ac0fec533fbddd5ea3a327807f18d7b069ae019ded80", size = 66287, upload-time = "2026-08-28T10:26:21.078Z" }, + { url = "https://files.pythonhosted.org/packages/53/11/5c43a562529dad8def4b81e5e1877c612a7e0298105a5939b3b409d2079c/kiwisolver-1.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fc271a6f0a2126958f4090e5507b9da5848927dae331f8f763bd4aa642b3d2cd", size = 123940, upload-time = "2026-08-28T10:26:22.475Z" }, + { url = "https://files.pythonhosted.org/packages/64/db/9bd6c505c95128c258a55236bfbb3a7a3fb6023f863316b6d7d9f3c69052/kiwisolver-1.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9b3092d8992a1d69b7a59c3e39f35e1b9be327a17f68a7c35fc17329e337d6f2", size = 66493, upload-time = "2026-08-28T10:26:23.743Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f4/b3007a3ed5c9be73f81161140684cf7d9bdb9c4b632f5f484d2a1c713fb9/kiwisolver-1.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c2306e8bb53601979fcb3fa09cc65e031876d9ae01eff2fcbcd7a84ef94d5bc1", size = 64720, upload-time = "2026-08-28T10:26:24.95Z" }, + { url = "https://files.pythonhosted.org/packages/a1/13/08188f0cafa3a800403e4ff62b9aad4e7a17f9c4c7e080dc8f18c64794cf/kiwisolver-1.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:18a0cfb124546a4c2e6087c5f3029c7f44b37c85b142e0ced71f73a7599ac208", size = 1475867, upload-time = "2026-08-28T10:26:26.393Z" }, + { url = "https://files.pythonhosted.org/packages/8a/3e/053bdc3c9abdb8f2606225eda398adca25c0c91ab90add8222a69db65ee0/kiwisolver-1.5.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34ec467940442c9943016fb2d4c81d1ba84351eeca2f1a78f8bc87f1ba0d414c", size = 1282865, upload-time = "2026-08-28T10:26:28.118Z" }, + { url = "https://files.pythonhosted.org/packages/5e/64/a44c341b36b610588cc2f1e89b3cae072a3119aa8be578e90987cd640751/kiwisolver-1.5.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a83ee7107df13abe42a54a6654670eef9bb39425cf2e27f65e0007465e1286ab", size = 1300865, upload-time = "2026-08-28T10:26:30.125Z" }, + { url = "https://files.pythonhosted.org/packages/60/5e/7e7d716dca38c714478b741257a5b4a321d9932b8d851551a136dcaf3984/kiwisolver-1.5.1-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bebb89489b279b2f5661bbbb2abcc87bcd4a46607bb4a5c966f04f1db6b8df9a", size = 1348071, upload-time = "2026-08-28T10:26:31.829Z" }, + { url = "https://files.pythonhosted.org/packages/10/1a/2b98fdda8bf45b7be317e48ed12393d44334394d315c74b81f4a14c0e31b/kiwisolver-1.5.1-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:509735237ae0d849e8a843551d423d2500d2e0a9ac1611a145658b29c0fb9f85", size = 992191, upload-time = "2026-08-28T10:26:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/b6/55/d893f5ede0e50f9e3fcf01f6015f42ec7d9cd221e26772701fe4a98745f9/kiwisolver-1.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:824c3d763a05ea9e9003610145186b0e9848c7584a5575c79bac5a8e7cd80bad", size = 2233854, upload-time = "2026-08-28T10:26:35.282Z" }, + { url = "https://files.pythonhosted.org/packages/b1/82/f85f6279555a6ee1639fef7bfe83adb037a03e11a6fc9eaa54b8d0380339/kiwisolver-1.5.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1fff05e239575b1481b6ed1a782f6fad616efbf1f0b1f44e6e85c4dfe426e483", size = 2330621, upload-time = "2026-08-28T10:26:36.9Z" }, + { url = "https://files.pythonhosted.org/packages/56/31/e11aea078f66fc2fffcc179d38ca90d9da97652a241b64519169742ba46a/kiwisolver-1.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0627b9bceb9c3cdcf12b8a18655eedfed2692b038df27423383c120d0b7dc2d6", size = 1982848, upload-time = "2026-08-28T10:26:39.01Z" }, + { url = "https://files.pythonhosted.org/packages/af/ea/2956b63bf5140ca46aa2c2818e6aa03e2d5754dd2fa41db1c6b28922940c/kiwisolver-1.5.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8a708a47ade1fe19e8371d5da076bac0dd4b0a5a7985ad6c637f7f7e361b6baa", size = 2494850, upload-time = "2026-08-28T10:26:40.837Z" }, + { url = "https://files.pythonhosted.org/packages/11/d1/3829542258d8b3fc0898d221e7ef0e2c83eca0d348709bb8dbe54f3d4005/kiwisolver-1.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:007a5553dfc4f4e8d184f588a0200e2cd4b63a59cc8796df3c39909e679dc7a0", size = 2298067, upload-time = "2026-08-28T10:26:42.803Z" }, + { url = "https://files.pythonhosted.org/packages/4e/0e/49522e1ab5788cbaf63a26fbd3b851f9028616828c961b8a31b35cb96df8/kiwisolver-1.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:f4167e87b397f273dc2356fcf1eaf50a6bac51e6105f45103ef7129c8efb0255", size = 72282, upload-time = "2026-08-28T10:26:44.268Z" }, + { url = "https://files.pythonhosted.org/packages/f5/b6/22e7ca5315d363e6f81c9f37c9472e12e7b298731e77c0428e6a911a2c39/kiwisolver-1.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:5c490db2168a508088f59140dd392556a54b8bd1048fc6383c8baff13c359673", size = 69855, upload-time = "2026-08-28T10:26:45.725Z" }, + { url = "https://files.pythonhosted.org/packages/30/8c/03a9cfbe871964c8758a816eb03ac96c806da2795a9a7cd9bf9648bfb594/kiwisolver-1.5.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4d4ca09bf13cff792b1884f64b98ee6c2467930d632233be25c56b442d99f10e", size = 126289, upload-time = "2026-08-28T10:26:47.023Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e3/14ce3041ca79dff9c9d884ca00c7bf32374e76028a865a9ecd99b4f5a517/kiwisolver-1.5.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:44b8faef94f1857e77fa0238f3390ff1ac51d2ea20a487e2e452a59fd2b5f5ca", size = 67709, upload-time = "2026-08-28T10:26:48.268Z" }, + { url = "https://files.pythonhosted.org/packages/af/c4/45030471a66ec8ef042e9f96ffe1d522c9ab12da180186a0898966fc1385/kiwisolver-1.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2ae70bc59790d2af72a3f76f24b272403e135070340281108b447cb77ea70819", size = 65909, upload-time = "2026-08-28T10:26:49.523Z" }, + { url = "https://files.pythonhosted.org/packages/42/73/17dce073a6ae259bb32cf9d686c4079d2e538868bc45967462bf33df914a/kiwisolver-1.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:43844c1a7ad6d723d5b5b4c4fc7f5bd399c40e288120d16257c7c9e8765c6e85", size = 1584907, upload-time = "2026-08-28T10:26:50.933Z" }, + { url = "https://files.pythonhosted.org/packages/8c/84/ae3c75909f507283cbfcc7e916c7e822579ef962020b97e6882b27b4478f/kiwisolver-1.5.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22d5e5aaad6be121f2515765e3b1c444352cb8eb4c86510801db8f2e50757316", size = 1392474, upload-time = "2026-08-28T10:26:52.638Z" }, + { url = "https://files.pythonhosted.org/packages/34/31/8bcc83caad5bce8fa4577152389848bf6bc110e51e573a2b4e7c2aa34c89/kiwisolver-1.5.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3fa5855898f6d3d01b72ccd48a2d65cbdee301251603fefe34e2025bddba219c", size = 1405246, upload-time = "2026-08-28T10:26:54.248Z" }, + { url = "https://files.pythonhosted.org/packages/55/72/220345537d790cf4ae54f8acfff4b5cc2468e0702a384d651cf7a771c63e/kiwisolver-1.5.1-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d66a64dd5dec136040ec2ae94aa026a912ee60fdd45bc28d3db30037fd809e88", size = 1456099, upload-time = "2026-08-28T10:26:56.042Z" }, + { url = "https://files.pythonhosted.org/packages/cd/10/3725fd2398f66d18c34b4e0f81a8d03764cd4f4f089f58a527f0b4428086/kiwisolver-1.5.1-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:9e51c119992ea8820706871c30a4642ec76de20ae82f9b50b9a45517d8e9f810", size = 1073695, upload-time = "2026-08-28T10:26:57.658Z" }, + { url = "https://files.pythonhosted.org/packages/86/cb/28d6e09e66b93e4588b2e6b7d84d020ccefea09e2f4de788510a07efeab7/kiwisolver-1.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:70ed9a45c7484d2b30cdacf60d220f494a1763b9fec1ad03285c6553fa0889f2", size = 2335355, upload-time = "2026-08-28T10:26:59.202Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b3/a0f31d5e4e40af7dc97c36b8a74fdd3a36cf3c8bbd098da9a23466ff6a94/kiwisolver-1.5.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:98b208a7cc42c803445ef551d6753cc42a5ea13e9cab1ee66cd8b9cb70195330", size = 2426524, upload-time = "2026-08-28T10:27:01.181Z" }, + { url = "https://files.pythonhosted.org/packages/2f/c9/728f63bd58c72cafdc79fc306abeeac7391bec03b757a48dadeb30906521/kiwisolver-1.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c6834b92dd2428e2dd85ef3d85f723d3c12f20aaf43a2ddd4f944ca25d833408", size = 2063430, upload-time = "2026-08-28T10:27:03.06Z" }, + { url = "https://files.pythonhosted.org/packages/84/df/ce188b96f92f9a2c958231da140768918cba53c9713dc887b82f85462118/kiwisolver-1.5.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:5d142e352eb13facc7dd047489aebdff6ba78576c239f1ea04931979caaf0567", size = 2597513, upload-time = "2026-08-28T10:27:05.072Z" }, + { url = "https://files.pythonhosted.org/packages/69/d6/76947c8203768968382e5bd74d9cc95654746703a61ea53015f2c74a2e06/kiwisolver-1.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9b1c4900736e489a812c529100de4b8fb617d4db075e931e213c57424b83d9b", size = 2394488, upload-time = "2026-08-28T10:27:07.423Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d4/14b21e4eb203c4d15425e8b6a2c625a320b4a1f2f7557eead63ffc30ffb7/kiwisolver-1.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5978c3340f16a35c30f8ab2fa7bcf559973c55f1a5ef6970e1f621acf3c4db13", size = 75404, upload-time = "2026-08-28T10:27:08.892Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f5/53157899fc7f45f76421b77b99eb1639dd0f83f26ff9d76300c96bb4a3b0/kiwisolver-1.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ca307d6c259e5c98d3cb9ade55342b47a6839762caf2536f3d7b46ee660cc82e", size = 72946, upload-time = "2026-08-28T10:27:10.944Z" }, + { url = "https://files.pythonhosted.org/packages/75/62/f786c3a27f181fa339d851a77e266d208e776b9883cabc40a5b041a31b5a/kiwisolver-1.5.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:bb7c99f0673c03017a3ee01e54a5c2617a05468b11eabe513b0080e063ed95b1", size = 62420, upload-time = "2026-08-28T10:27:12.308Z" }, + { url = "https://files.pythonhosted.org/packages/02/cd/58a91ed25fbad0facdf503297b03768efab04bdf3141e5e3b49a34be7443/kiwisolver-1.5.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:0d8924877ce22e17326a99a418c3c82037da078df3c6a260b13eca677444e6e7", size = 64577, upload-time = "2026-08-28T10:27:13.502Z" }, + { url = "https://files.pythonhosted.org/packages/f6/5c/d501ef5a0958b226eac28306d24d5e5f114be0ace50e19cabae7b6b3b197/kiwisolver-1.5.1-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:534f02c1abb31ed6dbd3515545285c330b2f12d00fdb1fdb71658b9ca5a13a6a", size = 66284, upload-time = "2026-08-28T10:27:14.813Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a6/8fbecaf4fc18c02f31f05e47a84c010a80e3ec391ed2f0bdade1d62b5954/kiwisolver-1.5.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:cea20da04494e662b83c872683bf4ff2345206043d036315ed0e924b652e7294", size = 124031, upload-time = "2026-08-28T10:27:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c6/bf3090d2983b4204347cbdbe952116e7c3b2abf62b4e33e50167a13e75ee/kiwisolver-1.5.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:7fd82debf43c6acd0a94359d232f6bb516ee13f269a7993736a9ac9f988bb5d9", size = 66489, upload-time = "2026-08-28T10:27:17.506Z" }, + { url = "https://files.pythonhosted.org/packages/85/de/562dddef55fdd7c291da8626d6619e72b5fc0870e6ccca0e149a5731e7f3/kiwisolver-1.5.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:18170a77ddfecf40ec60d0928268dc95880c881864e015a8f34094ed18b9b9ad", size = 64806, upload-time = "2026-08-28T10:27:18.673Z" }, + { url = "https://files.pythonhosted.org/packages/89/b1/ba7b9c0164ce1cf62bf2872db63b8483289cf0f3110d6f9390eb09e409ed/kiwisolver-1.5.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ca7f6fe0f37ca978a1e5eb7a3a68e6413f417e78e838324947ffd420202b198b", size = 1482211, upload-time = "2026-08-28T10:27:20.039Z" }, + { url = "https://files.pythonhosted.org/packages/13/dc/34da54efb4976616d45c20aae32d70e89d6e7395ed908029154d1609ef22/kiwisolver-1.5.1-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b973887ff782cfd6b67c9904ad8ca542e0bc5e4961503408b423b5a688b4d38", size = 1283739, upload-time = "2026-08-28T10:27:21.82Z" }, + { url = "https://files.pythonhosted.org/packages/5a/3a/30ffb62bee646e266e98a1b5cd276d9c75b6116fbfcb87c1190838c1b6df/kiwisolver-1.5.1-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f76fc85bd054c806960f917ec0f329e24e436f1712267d90588e4c39890caa63", size = 1301681, upload-time = "2026-08-28T10:27:23.876Z" }, + { url = "https://files.pythonhosted.org/packages/a9/8d/13c70be22a8506880b35fdc38dca36629613bc493405c79f4037f2cd2bb9/kiwisolver-1.5.1-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:828f75af2b0080c8a972e75f649ab46af008e92c6104a57a759157200b835b75", size = 1349159, upload-time = "2026-08-28T10:27:25.899Z" }, + { url = "https://files.pythonhosted.org/packages/82/0e/993972b8ec6767f47cd69818fb3a5ff14510557d29f7d1a839be7574fa1b/kiwisolver-1.5.1-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:431dc224a1a92a5c8f582d96e505196a3b5997a7271076678da2dfde67b77e9a", size = 997613, upload-time = "2026-08-28T10:27:27.507Z" }, + { url = "https://files.pythonhosted.org/packages/36/82/ca26eddd2eda2420dfc56693449c1f821f78b485da9cbde9904c03af3f93/kiwisolver-1.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:61e9a64c7635095a6bfe483e2ff055d437c59bd45f3617a228b37277f0185d62", size = 2235109, upload-time = "2026-08-28T10:27:30.113Z" }, + { url = "https://files.pythonhosted.org/packages/c7/84/97d920881e10840b8d7c7185620298e3e4c88820b05514e3a15a258b08a6/kiwisolver-1.5.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:3c24cd69455e1b00ddf770c13b6e2c33e07d6dc3f2d34add0bf9277c5c6bbd46", size = 2331207, upload-time = "2026-08-28T10:27:32.795Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ce/34d74b8f25acc58800f4c09268371e8d6159cf0f1206f1e4dc7835629b48/kiwisolver-1.5.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:27add358abe374ebaa3b8763ef380bc99051b5a4b18d94878366a9e4f59efef0", size = 1986696, upload-time = "2026-08-28T10:27:34.628Z" }, + { url = "https://files.pythonhosted.org/packages/0e/01/f892644014612527aef7031d3306a2ffc60b3cb044f802c1561f8e5e14f3/kiwisolver-1.5.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:255605693a483db7bd5c79f60437f7bf658f7f520d61aa42722e32257c941951", size = 2496400, upload-time = "2026-08-28T10:27:36.818Z" }, + { url = "https://files.pythonhosted.org/packages/32/6d/d8284e66e697026536e5f418b9cfe56567bffd3c775e3ecfbae373605854/kiwisolver-1.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:7d38b0c279c3032e8c9cc013b405c6df8e1668dbf15465779aa7f15f61201812", size = 2302967, upload-time = "2026-08-28T10:27:38.803Z" }, + { url = "https://files.pythonhosted.org/packages/46/0a/69a355e27f32ba50d5b6369949b6a1702e122f5277c89bc76d452b81c1c4/kiwisolver-1.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:958254518717542d02d0688d0d20cbf771da5e415e6f49543f92481c850a4540", size = 72283, upload-time = "2026-08-28T10:27:40.491Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d8/7a95be90c33dcdd52204d4aa6384d731443225b887283bbd8b61e7931f6c/kiwisolver-1.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:da3275833be0edbaf4830fae08bae3dc7219f40ce0c37eaa6c25825957e06612", size = 69860, upload-time = "2026-08-28T10:27:41.835Z" }, + { url = "https://files.pythonhosted.org/packages/31/f8/9bc493e7f5707788ba7f621902c68f82dc3a7ba03c78fbd337b026cef1ed/kiwisolver-1.5.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:470d420f98d368d6f010633a20659b544c5fdfa5329e6b70219f2ef08fd4a7ef", size = 126336, upload-time = "2026-08-28T10:27:43.379Z" }, + { url = "https://files.pythonhosted.org/packages/d1/82/3aea86b3f99712db825e9ac5631bf99571e818a8b8961ff98cebd798413e/kiwisolver-1.5.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:83f78128fa28705fa85d01c59771c72fe81c11bd0e6155edbb9f818983a7d761", size = 67698, upload-time = "2026-08-28T10:27:44.613Z" }, + { url = "https://files.pythonhosted.org/packages/84/7d/8daafc5d2e7f9c47a4f78f8865d86d2a9cf399c2a85f86c44a993594410c/kiwisolver-1.5.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:9506e892bcc3b409831d363c6f53e5985e1c8d1f6f6b0256d00358684ff85378", size = 65945, upload-time = "2026-08-28T10:27:45.932Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c9/51dc974d9130da70a8c47a96160123443d387ffe1b6b833d6f91d9429339/kiwisolver-1.5.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cea90547bfd93807e0013a004dc76552be44fad3bc1cc2b38610a9e889ed098f", size = 1588030, upload-time = "2026-08-28T10:27:47.678Z" }, + { url = "https://files.pythonhosted.org/packages/e6/86/f3e1a730e7a995149d8d3ff9e313b6d8a17b2cf1d98a8eff139dc30463fb/kiwisolver-1.5.1-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e8e4d953faaded9ec7ede36824e9814082d22d4c7b1eafbfa079ecba8cd0d076", size = 1390760, upload-time = "2026-08-28T10:27:49.676Z" }, + { url = "https://files.pythonhosted.org/packages/4f/3b/8ba25a2b5a0d2375e046f1b72de5179513f0be95aba6e7b094c89303929f/kiwisolver-1.5.1-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e9c01d3dd7ceba4d1d436cc021d40d592466e40b9bc7f5d83dc4e98a5c9cd8c", size = 1403279, upload-time = "2026-08-28T10:27:51.242Z" }, + { url = "https://files.pythonhosted.org/packages/18/d0/278d5cb8be812740027d5ca0a7eda0c375488a88d6dce0fa60fcc2591ad2/kiwisolver-1.5.1-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37f801b5d7cc0e5a548921308e059fd2b057bb42972b591cfa3049f95423c4ed", size = 1454429, upload-time = "2026-08-28T10:27:53.213Z" }, + { url = "https://files.pythonhosted.org/packages/94/37/bcbab41063ec284c1d200efe5087cf087798c2f8916960aa8a20dd303290/kiwisolver-1.5.1-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:e68e151428b5384f766cd25739bf77c7e4a3dc93b5ded7a12118d9fbfdf78ab6", size = 1073225, upload-time = "2026-08-28T10:27:55.089Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c5/ab79dcdf5ae28909a51210ae0a1c579e97ff997b3466414f0d04c0994583/kiwisolver-1.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:8f8fddb8e323bd6eee4e54e69a39243beab22689070f4c66b472c4cc88bb89d8", size = 2334335, upload-time = "2026-08-28T10:27:56.78Z" }, + { url = "https://files.pythonhosted.org/packages/8d/8e/71d047468a189041d9c93f3b76844b924f9793b188c44bd149fa258912da/kiwisolver-1.5.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:3cc210010fd2f438a3ed430b45f1b501fd13a8618bf984dc2c5ce5b69b78752e", size = 2424982, upload-time = "2026-08-28T10:27:58.639Z" }, + { url = "https://files.pythonhosted.org/packages/7d/37/1347461bbea6d0e1f0580b94ef603b18e72c2be5f667fa1653867361a00b/kiwisolver-1.5.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:b5664603a253efd3a75716d793d1d3a6a82723b61dc6db767b2460bbbeec4c0f", size = 2062857, upload-time = "2026-08-28T10:28:00.407Z" }, + { url = "https://files.pythonhosted.org/packages/1f/87/c7f0976c9cd0d127643351bc0c9929e0b8899d7f49d4ec238cd909e39c42/kiwisolver-1.5.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:a7b85b2cc6ea45e5f7e8c9a30bc9fabd47cda09106cbb4b967335c3e6c43b69d", size = 2596022, upload-time = "2026-08-28T10:28:02.183Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9a/59a6f6ae6f938c15076be2c21b6cedea973d71bb1349ec84fa485fab82cf/kiwisolver-1.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:ab620eb663952455271ac37f9aaad86b73c969c02f11f53cea405b38e96a4300", size = 2395634, upload-time = "2026-08-28T10:28:03.977Z" }, + { url = "https://files.pythonhosted.org/packages/e9/2d/8982c1fb7926da5bb7ed60318c3665b5c3f941447271ac982960a11b8637/kiwisolver-1.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:cb6fae641357ed2f6e533c0d3c6504a4a5703621a50c89459e46051d56b61140", size = 75379, upload-time = "2026-08-28T10:28:06.307Z" }, + { url = "https://files.pythonhosted.org/packages/07/78/ba7b6dfa1708b82b373ac056928a30c545d5c1a627df9839dcec3c6c1881/kiwisolver-1.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:b390aec180a7c054919c04898835e1c77bced23ea8383eb2c570213bf25d1a86", size = 73011, upload-time = "2026-08-28T10:28:07.578Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c4/1407df7512a5b36cc79840e01710dc575733c461b13ab866cae77eaf87f3/kiwisolver-1.5.1-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:482676e5bd48d70ac99d9fc78863469845421e01184fa83f1f9366dc49f7e974", size = 134002, upload-time = "2026-08-28T10:28:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/16/45/c37a21ad5c0ab581a93c55ad544721aaa1f0ae94edb29c6a678a23d013e6/kiwisolver-1.5.1-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:072bdb15a3c19a5b5dbc8f8fb1f4e1884bf4f3507eeb4cc6334401274d37a5c0", size = 194292, upload-time = "2026-08-28T10:28:11.06Z" }, + { url = "https://files.pythonhosted.org/packages/a6/c1/69f00d627949580e43d57af0aa465df46868d7c29801c137a55374101294/kiwisolver-1.5.1-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:a5a00665d1a0e26763a7338d7e911d4598fbc1d50dd0d6b7919b7dc6c5d6569f", size = 73362, upload-time = "2026-08-28T10:28:12.449Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f1/8bae9fac0f1837679ed59a5dd7e97e7eb943b738defa7cc0117ea0d107dc/kiwisolver-1.5.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a5716a33bfabb2c6ce27b6cf03253467b3804f83e215f4d202685cf93c6c9874", size = 59543, upload-time = "2026-08-28T10:28:13.794Z" }, + { url = "https://files.pythonhosted.org/packages/c3/3e/42e3639d32ef3ac6ede80c39dc1d1df6224927409960d8f762b6cb504efe/kiwisolver-1.5.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:685929988b208a911f1285e2f8ed54210b0d681a3dc0f03e00d599d291986e7e", size = 57504, upload-time = "2026-08-28T10:28:15.074Z" }, + { url = "https://files.pythonhosted.org/packages/e1/12/f4beaaceb740b96c363a8fa6f34dbdf37a58d9e5f15416427d7bf89c552a/kiwisolver-1.5.1-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e4e49f7e1a4e7191bdf9dc67a974db714501b1fc52c24324103d06a86abd5c08", size = 79885, upload-time = "2026-08-28T10:28:16.345Z" }, + { url = "https://files.pythonhosted.org/packages/02/24/18d8a755acdae79c37ca2f1a925795aab67b9456c30c8569c377ea3abc77/kiwisolver-1.5.1-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0a887b6565bbfe80efde2b7f6e8890d7d9bbdb11bdb17028a3690c32fe0621f", size = 77582, upload-time = "2026-08-28T10:28:17.719Z" }, + { url = "https://files.pythonhosted.org/packages/df/a9/b86dde4f553ef74db0f9c32398614e24b2e8ec1d29a6ca04ae080d7dd29b/kiwisolver-1.5.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1983f0974a750a6f6556f368ba11105d1d8369c735b944747c9f12ae5aea7aae", size = 98321, upload-time = "2026-08-28T10:28:19.087Z" }, + { url = "https://files.pythonhosted.org/packages/b5/1d/59ba570b1774e95e97fde3a0981b2e22118a7a495f73bf74cedc538566a0/kiwisolver-1.5.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:416ba7ff9f233b7036689bb5a3783537e838ad483f63558d2a800f75afe738b1", size = 59450, upload-time = "2026-08-28T10:28:20.383Z" }, + { url = "https://files.pythonhosted.org/packages/22/98/a6849f04dc18b5400e8b98affa2cd8fd86ed583085f036e57b32e571f4fa/kiwisolver-1.5.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8af9b142ad719ae3a911ebf616bc4b78b32bbab84d6a40d3ad2f129670509957", size = 57400, upload-time = "2026-08-28T10:28:21.632Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ce/a7dc71353dd06a4cbe02222773f52d4a28c81e5a452a75797f8ed113dc99/kiwisolver-1.5.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5daa1f19e097050b9c4d9a78fcc9263cb96c9dfae08037ddc1b7c4ad1889f2a2", size = 79891, upload-time = "2026-08-28T10:28:22.936Z" }, + { url = "https://files.pythonhosted.org/packages/10/b1/d61c61a84ff85d1a36a99df2c152b59ffedb1d356c598902aba44abcdb60/kiwisolver-1.5.1-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cdaeeb6c350106df6bf9d873395973e5f066a9713200b72cd64f55d0a3eafab6", size = 77605, upload-time = "2026-08-28T10:28:24.322Z" }, + { url = "https://files.pythonhosted.org/packages/d3/52/5aef56f21a460a6e43ab3cdfc7697d59d7b87deb0ec97a0f7b91aa4a521b/kiwisolver-1.5.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:17851e5dad4484be0cbccbde3b15331deae036de9aebd45eed964487802b172f", size = 98465, upload-time = "2026-08-28T10:28:25.696Z" }, +] + [[package]] name = "langchain" version = "1.3.11" @@ -1392,6 +2619,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/99/2e/d82db9eec13ad0f72e7aaad5c4bc730ab111934fdc83c85523206eb9b0a0/langchain_protocol-0.0.18-py3-none-any.whl", hash = "sha256:70b53a86fbf9cedc863555effe44da192ab02d556ddbf2cf95b8873adcf41b5a", size = 7221, upload-time = "2026-06-18T17:08:25.996Z" }, ] +[[package]] +name = "langdetect" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/72/a3add0e4eec4eb9e2569554f7c70f4a3c27712f40e3284d483e88094cc0e/langdetect-1.0.9.tar.gz", hash = "sha256:cbc1fef89f8d062739774bd51eda3da3274006b3661d199c2655f6b3f6d605a0", size = 981474, upload-time = "2021-05-07T07:54:13.562Z" } + [[package]] name = "langgraph" version = "1.2.6" @@ -1559,6 +2795,214 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5f/5d/3dcec2884ba1b0806d1408612555c38dd5d68e90156b59f75f6e36435c3a/librt-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2f281549a4c52ac7bb97997f14353f8bd0e53a34ca0dad1c905cfd0b4a58ae99", size = 110771, upload-time = "2026-07-08T12:26:12.303Z" }, ] +[[package]] +name = "llvmlite" +version = "0.49.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/27/72ae94ea5c8f7349ec1c229d4cd058feb799cbd0833ad6d1b47c919b37b7/llvmlite-0.49.0.tar.gz", hash = "sha256:00f16db782f4a13c78c5804aedc434e46794a77e89999a168f9401106270e50a", size = 194467, upload-time = "2026-08-11T16:26:00.489Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/0d/daceb212c44cad1115b2d05dd55beafe23ff06627344adb4ded0c661bb1a/llvmlite-0.49.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ee81e96c15a6f870918f1eb60c913551c16aa23defb4f5f1acfa660d6a0aaac2", size = 40479229, upload-time = "2026-08-11T16:22:56.104Z" }, + { url = "https://files.pythonhosted.org/packages/72/2c/eb42378b4f3afc71f9fe172d01f30135dc1d54c7fd95cf76d5445d6f7809/llvmlite-0.49.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:854941c2267fd4fc5b2ce02b8af8ecdffa79fb7784591d3a89370322039ea09f", size = 59890659, upload-time = "2026-08-11T16:23:03.359Z" }, + { url = "https://files.pythonhosted.org/packages/4f/dc/fe880ac1eb93c09b6c9a0539ad18c98778386978a0e20a13a55788044ad2/llvmlite-0.49.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da7b64474ac15ca595efa2644d5c6836638ccf70709fad3aba3fc56a55966928", size = 58344482, upload-time = "2026-08-11T16:23:12.122Z" }, + { url = "https://files.pythonhosted.org/packages/59/f6/5c18be29145cfca1d9e859e55a3c586a8c5a821825017b04c7999cd166c9/llvmlite-0.49.0-cp310-cp310-win_amd64.whl", hash = "sha256:b352c14353330c879e339b8f8d7491d565fe94242697714a24e80bd757202384", size = 41865252, upload-time = "2026-08-11T16:23:20.532Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/ab52de2328e97ca96cdf0331a5f774796bddc420a51768f4501193f80cbb/llvmlite-0.49.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:4b0e710880b7cc910392bd6b9f1bbf468fed99b182e4420d51598f36114b3dce", size = 40479230, upload-time = "2026-08-11T16:23:28.744Z" }, + { url = "https://files.pythonhosted.org/packages/1f/80/0989432d12b7c86a6f5f380eb92eca7de779af9b34dedbd311b694d7da8d/llvmlite-0.49.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a8c0fc9d624bdc30a3d2db11eb2fb98f80fb209d20b37604eda516cd9b699cf4", size = 59890659, upload-time = "2026-08-11T16:23:37.346Z" }, + { url = "https://files.pythonhosted.org/packages/58/e9/76859ca36aaa460b6ae0508e01637f0e9bdb9b59faaa4637ade3b94bbcca/llvmlite-0.49.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20496a5c9fdb8179fb9300e7d19f6782555d98aeeb4a322264aa7fd99f980618", size = 58344482, upload-time = "2026-08-11T16:23:44.199Z" }, + { url = "https://files.pythonhosted.org/packages/7d/49/47cd23e05d52d117b6119871ec299adedc9d8d332a2296964d9b2adc06d9/llvmlite-0.49.0-cp311-cp311-win_amd64.whl", hash = "sha256:6a5b06c1b5fc4ae4c9b169b065f42b719448ef1f873687ef224ef69969b75ec3", size = 41865253, upload-time = "2026-08-11T16:23:50.198Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/3f699ebe3590e15e023a6372dd147526fd8ec398aacf9ceb844e854964a8/llvmlite-0.49.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:b541c8fac3450db7574d1f53cf9dff83f285bfed9d69bf81fe71fc2a7d4f97fe", size = 40479231, upload-time = "2026-08-11T16:23:56.773Z" }, + { url = "https://files.pythonhosted.org/packages/be/3c/e97f69c62a2d972066d9a2612ce1f3de313035ac897a5b9f787cad8b55f7/llvmlite-0.49.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6acba646d88abbc87d5c113a3d62c1fbf8b8fee11c6493f516803e30f21ae870", size = 59890658, upload-time = "2026-08-11T16:24:05.451Z" }, + { url = "https://files.pythonhosted.org/packages/69/e6/e942ee08605fc0526ff3854260c384d8315a5830e16c4c2a5aebc14dc9bf/llvmlite-0.49.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ec8ad805e7515cb8440a690eb3cef4d34acb29eef80b705ec4e1c1ad3c43c68", size = 58344481, upload-time = "2026-08-11T16:24:13.503Z" }, + { url = "https://files.pythonhosted.org/packages/84/49/2a44871cac6b5a2fd4aabd68cfdaf6de9a5c7edb36dee5d47b77bda4b50f/llvmlite-0.49.0-cp312-cp312-win_amd64.whl", hash = "sha256:3a9c9e3af4e214acfefa4f73ebe7bc3fb35854a62b654edb3953f5ae33c08ba3", size = 41865543, upload-time = "2026-08-11T16:24:20.41Z" }, + { url = "https://files.pythonhosted.org/packages/7d/85/0b536a3c59f2636d9dd51dda832b6c1d0ffec37608429dedf128664918f1/llvmlite-0.49.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:039fa4054a06f537fb39248d4472284ca96be311a142ec09e69f95630ab469cc", size = 40479230, upload-time = "2026-08-11T16:24:27.295Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1c/ca8ba47b057b793099784475499771780ec46839f2782f753a7079d23520/llvmlite-0.49.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ddc7aecd4f56397ed6e8f120ec5dcd5a1a8f0e6032ca4af413462792d4dca2e3", size = 59890659, upload-time = "2026-08-11T16:24:35.595Z" }, + { url = "https://files.pythonhosted.org/packages/de/af/9526dfdd33a923f33e29a18b8f9801ee7ee4b7397e88d28192c1024c4a75/llvmlite-0.49.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d3dee64784201b64c13a8df62c48a4f4218858faaa65889866bb29bdc243c038", size = 58344482, upload-time = "2026-08-11T16:24:45.79Z" }, + { url = "https://files.pythonhosted.org/packages/96/7f/9f5afcf6476b228d6b170408f377a0c4f91477fc1fc91f8141088b45bf46/llvmlite-0.49.0-cp313-cp313-win_amd64.whl", hash = "sha256:a1b414dc6b164738ec39dd8987cea73829057b7dd92fc6d91b52838385fc1dd2", size = 41865544, upload-time = "2026-08-11T16:24:53.962Z" }, + { url = "https://files.pythonhosted.org/packages/25/d1/16599b8c9f21802448059482eab48a9e74086dc56b901a677ba355565e64/llvmlite-0.49.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:80a84683d04516bb51da1bbeebddaf2c2f558809c93078a8f91807909ae331f8", size = 40479230, upload-time = "2026-08-11T16:25:01.513Z" }, + { url = "https://files.pythonhosted.org/packages/3a/61/0b23849141a4c4e7091fcd158ebb45195896974bebca3e58fee7cad4b4f4/llvmlite-0.49.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4281a0171d66d2098adce4ba706b8c550b1b10718650f682d64cde16e84e4de5", size = 59890659, upload-time = "2026-08-11T16:25:08.733Z" }, + { url = "https://files.pythonhosted.org/packages/c4/92/628692b74b31e27af9ba7e8ba651941ee4956959d5478123c453f59aad4a/llvmlite-0.49.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b095f15fb12c4d90495df5b1a3772b4732cc408398b204a787dbedd370e09c69", size = 58344479, upload-time = "2026-08-11T16:25:15.731Z" }, + { url = "https://files.pythonhosted.org/packages/96/8a/412fc273521b02cbfe0b5f8ad56cc696385f6eaeecdb9e9ae6a90111d98d/llvmlite-0.49.0-cp314-cp314-win_amd64.whl", hash = "sha256:294e2f0b70aef8f92d0ae7b203e2609f08beb39437eee73de59a21669331aae9", size = 42986588, upload-time = "2026-08-11T16:25:22.534Z" }, + { url = "https://files.pythonhosted.org/packages/fc/15/f47cf45c00c8b165ac3d268502dcb21d900e86f27fd338268a66ce922ab0/llvmlite-0.49.0-cp314-cp314-win_arm64.whl", hash = "sha256:95d1071023ed858b79f6971954fd7cc1f5dbcbab987718a4ccbe1411e47d0b81", size = 37441881, upload-time = "2026-08-11T16:25:28.324Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2e/eafd488766d1c02413cba24f7b22acb9b3ccdfd8407e98d30eb16bac4e2a/llvmlite-0.49.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:f3f2ff0aeb17d34fcce9f79b99baac441cfd3efa41b83e233ca4530a72381f72", size = 40479230, upload-time = "2026-08-11T16:25:35.125Z" }, + { url = "https://files.pythonhosted.org/packages/98/07/a2c4f04e2111ccc274b4d5e3331398a9dcf6d6e5e55d6444b1ad9d6381cf/llvmlite-0.49.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d5555ea1d63928481cbf7fcb1d67452b216c7e5b393a4eb7aa1401e67f2a4fc4", size = 59890658, upload-time = "2026-08-11T16:25:43.294Z" }, + { url = "https://files.pythonhosted.org/packages/80/f9/7b7b50f80b4585bcd78675ff3110c256877b11df32a8cde284f851762f57/llvmlite-0.49.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32adb84fdaae28aeb86fdb6253084ee707ee157289a2e98fe3caf48a62bee82", size = 58344482, upload-time = "2026-08-11T16:25:51.527Z" }, + { url = "https://files.pythonhosted.org/packages/d8/c6/32d68bfbf1d0c36888530ef6fd72864861af23dc546302b41033471a8c3a/llvmlite-0.49.0-cp314-cp314t-win_amd64.whl", hash = "sha256:be637e465010bc9c50f070468f7f1cf5385e92fee364d192dd5e6cea790ecba9", size = 42986602, upload-time = "2026-08-11T16:25:57.69Z" }, +] + +[[package]] +name = "lxml" +version = "6.1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/23/ad/28ecd7cb894d172f3c9c80a075eeeb2017ac62e3632cee05a5f9493547eb/lxml-6.1.3.tar.gz", hash = "sha256:45222d94ddd511536f3b2f7d9deae3b2339b4ce0f075f1ca25703b07cad9dd21", size = 4211198, upload-time = "2026-09-02T14:48:02.287Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/6b/a7d5c08e19a8e69887ed722fffaefdbaffc8959d5ef5c370a65e52c895ac/lxml-6.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:40bcbd9f94166ffe925811e730607385cec959f42fb1bb7dad83748680465221", size = 8575497, upload-time = "2026-09-02T14:46:05.131Z" }, + { url = "https://files.pythonhosted.org/packages/96/dd/c25a32f9f6039a96cfd52296a4630075868aa16e71858b3076699a059201/lxml-6.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:05f5bce9af14fd1506997594bd81cee6d9c6b58ea80a39c058327aa6371ed9e9", size = 4619233, upload-time = "2026-09-02T14:46:08.898Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f0/d49375a47644369d84f90a9fe4ff1924faad58d4f95563831eca84ca29ae/lxml-6.1.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff88a92cafde90888511242d1c54afcc1a8adbb6dc0a88fa7f87e29e92400d4a", size = 5015387, upload-time = "2026-09-02T14:46:10.797Z" }, + { url = "https://files.pythonhosted.org/packages/76/0f/d1b1f52925442f7b4b1abd81a41905987322f6df6a5dd42fab8579415828/lxml-6.1.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c00e26288784460885fe76e4d4b293573e0f791f52e6d60e27b42edf005922eb", size = 5168571, upload-time = "2026-09-02T14:46:12.989Z" }, + { url = "https://files.pythonhosted.org/packages/b2/13/e5d8291a68a27e564e4e1eefba08c3844c6800bcb43f3e72a32b20971132/lxml-6.1.3-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:773062aec2f2e56b2b22d37054123f0de8a22a4688a0c3376c3fe42685f975cf", size = 5068024, upload-time = "2026-09-02T14:46:15.325Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ce/dbea34cd115ae9b8ef53816daa912563615adf4daed42531878a2fb29c77/lxml-6.1.3-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f6449672f9c93316deb5e2839e18931f468670e44d5bd9b1301a5a9655d45c07", size = 5296830, upload-time = "2026-09-02T14:46:17.52Z" }, + { url = "https://files.pythonhosted.org/packages/20/f6/12a2ab6e8c8afecb82a3f0e9a518952b6a1cddf405ad8542883bd71e6096/lxml-6.1.3-cp310-cp310-manylinux_2_28_i686.whl", hash = "sha256:ec295280f4b37769256da025acf5890370355ac589c27e89caae0b5e9eedc702", size = 5424696, upload-time = "2026-09-02T14:46:19.706Z" }, + { url = "https://files.pythonhosted.org/packages/02/3f/5670e198266c764595687a234fdaed33837f487b95a596262b2548e48933/lxml-6.1.3-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:5929d9df5e7e3379183be0e21f7d559618a5b61cb63280df6164019242e337ed", size = 4783635, upload-time = "2026-09-02T14:46:21.63Z" }, + { url = "https://files.pythonhosted.org/packages/70/24/007ce6b7bffb61a6ca88c3a8f21b26f3f0aa3b3f6bb648a56e328c994a14/lxml-6.1.3-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6e1eb8a4cbffd5553680ad96be6680e364710656eced73d1dc90ec489df599a3", size = 5373212, upload-time = "2026-09-02T14:46:23.572Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/cf09f38cf9005bd5cfa4fd452b03b290b1c48c403fe0319a8013f4b3cae0/lxml-6.1.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:16148acd77ed1d8836a56db883af2f5eed720f9723088110b16a0d08582130a6", size = 5116476, upload-time = "2026-09-02T14:46:26.262Z" }, + { url = "https://files.pythonhosted.org/packages/15/83/eb021e5db4336f0bb1438cba6f053ea135aa00b9f4ef0439473d6b986308/lxml-6.1.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:23c366231259cd75ad06495174701afb3fcb36a92917fa47de2d1f1bd9d95739", size = 4814172, upload-time = "2026-09-02T14:46:28.3Z" }, + { url = "https://files.pythonhosted.org/packages/c8/4e/147b6f9088cc191713249ac547b0af2fece489c8cdff1f2801ab47dda8a9/lxml-6.1.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:da85db328e507da922d586c3c7416ec360ec22e9cd9e0700691afacde0c81f53", size = 5361711, upload-time = "2026-09-02T14:46:31.035Z" }, + { url = "https://files.pythonhosted.org/packages/b7/d9/8cfdac0d7d771e25af2c1f4bc874032f025a4b59e0b6917c3c7858070795/lxml-6.1.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0f17d83c48ee9dfd96abae3ac3e2108c76d2fc86ce96355e37b8da9f7f4ecc08", size = 5321598, upload-time = "2026-09-02T14:46:33.165Z" }, + { url = "https://files.pythonhosted.org/packages/f3/5b/d2413c71f312dccdd07ed985be356657fc624d822ba7e2c87e8722646156/lxml-6.1.3-cp310-cp310-win32.whl", hash = "sha256:7dd624c1eaa629ad44b59a1a0145fdf2d67895592dce94c9358b938b3d075e65", size = 3604471, upload-time = "2026-09-02T14:46:35.245Z" }, + { url = "https://files.pythonhosted.org/packages/7a/bf/74b6785beac6488fd395e78796339bc197fbad6fd6103b41b15a4009dc4b/lxml-6.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:18a4db52b5a7b53a3540b0b0f4123319334621ee8083d496de314d0bf06ff59a", size = 4029086, upload-time = "2026-09-02T14:46:37.744Z" }, + { url = "https://files.pythonhosted.org/packages/f9/a5/ddf6e1744cd76fc9f0ce11cb16b117d6eaac46ebaeca01968e9014e8770c/lxml-6.1.3-cp310-cp310-win_arm64.whl", hash = "sha256:0feebef8d0521188d0157f758356072e840173aa61ca45b8b3f87959ac283dd5", size = 3674608, upload-time = "2026-09-02T14:46:39.802Z" }, + { url = "https://files.pythonhosted.org/packages/96/f1/95133bde7af7afb1f5ba6090b674d826b7a518318bba54bbbb633b27865a/lxml-6.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c66f858b82497173f73366795fc6ee8171620e75a338506d6b2e7bc16f5fca11", size = 8563141, upload-time = "2026-09-02T14:46:42.334Z" }, + { url = "https://files.pythonhosted.org/packages/80/54/5a79ee2181ac773ee13e48205411845feec69e1c3d097e985c1343171712/lxml-6.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:032a0a97eed428bd143c75a11118238546424ceb2fa311cca5f073aa44658dc4", size = 4613690, upload-time = "2026-09-02T14:46:45.253Z" }, + { url = "https://files.pythonhosted.org/packages/ab/29/8c24672f56807f119312f073f24204368574bd16b384ede861b5104b3a2b/lxml-6.1.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4a579dfb9c835f8ab47f4b8ed33440cbc75b806b73297208e6ec2a33e903740b", size = 4935630, upload-time = "2026-09-02T14:46:48.071Z" }, + { url = "https://files.pythonhosted.org/packages/71/69/ce2436d854c848c19fc9287143991f3fc76b8b4e9a0dbba8452e51dff264/lxml-6.1.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:49fbc2682a9306135b7ec49e93f97f9c26689b9b7f96ed2742d8d6497e994d13", size = 5079033, upload-time = "2026-09-02T14:46:50.483Z" }, + { url = "https://files.pythonhosted.org/packages/91/ec/b66f66f6499ad800265d57540b51e6632e3232d3526f42f2f8fd4b14e0ea/lxml-6.1.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ea2c01cdb16dc12156e455007c406dfaaece0c89aa4ba0e3b47586779f951d41", size = 5012298, upload-time = "2026-09-02T14:46:52.603Z" }, + { url = "https://files.pythonhosted.org/packages/94/2a/25d128872f4d51753542bfc3feb482c2ea7c8a2d6d81a0bc5c6a00779ed4/lxml-6.1.3-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:527195c188d7d0af748cd48d220ab8cdc5cb99be3d49ac4d9be7324d8abf9bc0", size = 5211431, upload-time = "2026-09-02T14:46:54.722Z" }, + { url = "https://files.pythonhosted.org/packages/75/b2/0a41bbef074a556110f84fafb6d8c2998293c7d3bfbe1ce74515bc65393b/lxml-6.1.3-cp311-cp311-manylinux_2_28_i686.whl", hash = "sha256:20384c2bbcbf87180c8c61eb60869699c1ec0cd09b62cfd13804022d860b0867", size = 5343417, upload-time = "2026-09-02T14:46:57.46Z" }, + { url = "https://files.pythonhosted.org/packages/7b/cd/16116c3f91791aeeeab1cbe6e7eb6e646f127be7b0158b262eb526a21a0c/lxml-6.1.3-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:424aa5657141d306ba9ad1baab4b2c0a0719040075ee6c66aee9bb2dea2b5054", size = 4673219, upload-time = "2026-09-02T14:46:59.604Z" }, + { url = "https://files.pythonhosted.org/packages/dd/bb/4dff849f443ef70221676aec938bc41e8bae6430aa2ca13b041319e14b98/lxml-6.1.3-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4736e6c87e603146d8949d8501da621ad20c31015060d3fcf95ace2859f3e3e6", size = 5281246, upload-time = "2026-09-02T14:47:02.375Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ac/4aa7dd059420bfd35278c7fe819e9d319ee36a0453b7bbde1907a7832d91/lxml-6.1.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6374e9e382e5a98c9c5e66d41b357b470da1c54bce30f17f9dc4bcc58436cc1c", size = 5055451, upload-time = "2026-09-02T14:47:05.883Z" }, + { url = "https://files.pythonhosted.org/packages/de/44/20d90cf6f4234de9cd9eeb4f519419885fdb087fa80d073c7b57be342021/lxml-6.1.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:22eec57e26c418cde02c051ce9914a365e52a7f135a565c6f0480242aeebab48", size = 4722694, upload-time = "2026-09-02T14:47:08.461Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0e/6bee12325e53dd6613fe1e107def07583b6182ade03e94bfef8976622e44/lxml-6.1.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8753b8d51dbc86fd335ee31fcf7f3658e9f5c016d4edfb23f76ad295f4b8c9d0", size = 5269179, upload-time = "2026-09-02T14:47:10.647Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5d/54d269ce5cd0787c0424d9cef449ee794d4097725d13dd2acd6181c44e9c/lxml-6.1.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:207dfc3d47cf0e575e643bbc140dacc8863b39abaa1e5307cd64c7f2365b8a12", size = 5235559, upload-time = "2026-09-02T14:47:13.932Z" }, + { url = "https://files.pythonhosted.org/packages/e4/f7/5a3095f187f1bec293591616a1677781acc265c5b313c009f8a19c471a09/lxml-6.1.3-cp311-cp311-win32.whl", hash = "sha256:18293f8a8d8b6a8e71ef37706b659e3846a4261232158167b1ddf35f6994f633", size = 3600377, upload-time = "2026-09-02T14:47:15.957Z" }, + { url = "https://files.pythonhosted.org/packages/45/5a/15531a0d307c96282fe8b639b3d74e8bd783e4ab4cb2b0781146ac4161b8/lxml-6.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:7ae4949f212a53b007dbc355884fda122545c5764a54256c9217e419a62a6559", size = 4032700, upload-time = "2026-09-02T14:47:18.566Z" }, + { url = "https://files.pythonhosted.org/packages/12/f9/8de76314955545ceaaa7c0305017b8aaa217905dee59c62c0e2c1e44a68f/lxml-6.1.3-cp311-cp311-win_arm64.whl", hash = "sha256:2123e5aa075ac20d23c7af489255efd129cbfe190dbe88fd42598cc9df3199b6", size = 3674431, upload-time = "2026-09-02T14:47:22.186Z" }, + { url = "https://files.pythonhosted.org/packages/dd/1f/a180b57d9eeabaab77f9d5aa30356898ea749c4795596a8f66d1eb6bef2e/lxml-6.1.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c0710ac085a157b593c38fbcacd950f15c4afa8e2057527185875ab302752bc", size = 8602094, upload-time = "2026-09-02T14:47:26.054Z" }, + { url = "https://files.pythonhosted.org/packages/a8/25/070c92013a1c029a602b03560d68772313d918268667fa993da7961759c9/lxml-6.1.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:623c8799c17128753c65699f1c3aa32402657393a9ad6db09ed8b98ddf76611d", size = 4638308, upload-time = "2026-09-02T14:47:29.587Z" }, + { url = "https://files.pythonhosted.org/packages/1e/1c/722e88883173097a1a375153e3c2447eba3060d0231522cf6596e99f4195/lxml-6.1.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f683dc6300317700025e41d89a43e0276692ded16113a3c43eab704d605c58e5", size = 4939696, upload-time = "2026-09-02T14:47:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/db/36/aa413bc214dc4f785ad2b2ddd8cc99aae7062d49ab155e91e6011af00daf/lxml-6.1.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:379f8a75cf6eb7eef0af074b55f49ab73b868388a98de14646abcdfa4564bb11", size = 5105247, upload-time = "2026-09-02T14:47:36.734Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a0/a1f7f1313795bfec67b77f01ef3b1128d49f2d7f66a8413fa55d47f4e25f/lxml-6.1.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b37772102d44bb6628186accca3a121b1fa3a6b3d97518a8c29a5229ca4c0d0a", size = 5011915, upload-time = "2026-09-02T14:47:39.846Z" }, + { url = "https://files.pythonhosted.org/packages/b9/78/840e7e3f1d0cc7a5cfac5d8505b97e25b6427fd774ac4bae672aaebfb4b5/lxml-6.1.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ddcf547bea2aee967d6a77779376a45e77e610e8465147a1f3d7e20d539d6e32", size = 5638175, upload-time = "2026-09-02T14:47:43.644Z" }, + { url = "https://files.pythonhosted.org/packages/0a/20/e022dbc6b4753a9bc9fc5fb28a27163430c1731b9913997f6544c1b2518c/lxml-6.1.3-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:909f4e927bb051f7740d6367285fc60cdcfdaf0258c2dba4ff5ba7eadadc250c", size = 5244675, upload-time = "2026-09-02T14:47:47.635Z" }, + { url = "https://files.pythonhosted.org/packages/99/83/82cde81d2b5eb38d1539fdfdf318abdd014a7e604f4df01c9cd3deb18f2a/lxml-6.1.3-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:a5c18810318303ce9afb3f95e2ddb54834f96fa699a8600433fd5a93dcf44c56", size = 5358205, upload-time = "2026-09-02T14:47:50.306Z" }, + { url = "https://files.pythonhosted.org/packages/d2/a1/f3b057371c8cb29f2a9c9c44ea320592446e40b74a4b0af68c3d8e65bc73/lxml-6.1.3-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:3e42265103fb385d8642a78672edf376c6f7e1d3598a7a4f9cb1278f2f6b5f6f", size = 4704495, upload-time = "2026-09-02T14:47:53.251Z" }, + { url = "https://files.pythonhosted.org/packages/1a/a4/230eb28be5d412152ffc3c679b51fe1aeede5a53f3a8eb6e9748f2f4754f/lxml-6.1.3-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:21402998e4b78e7cce237d2788841aaa21ac9a4d1574d04dc2d12ee41ae807b5", size = 5255117, upload-time = "2026-09-02T14:47:55.963Z" }, + { url = "https://files.pythonhosted.org/packages/a3/18/1969f56763af24ce42ea156007b0b2d73fddea552e283b2010416394f0f4/lxml-6.1.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:38fc4e4e4e084e0bd491949482527d406788045c546d4f8789e93fc527b91385", size = 5054424, upload-time = "2026-09-02T14:47:58.131Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d4/2a90acc1f6fabaa3a8db9340437822bd8d041b205d626a4b3e8621aaa390/lxml-6.1.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5609efdb0d3c95499c00046bc53648b3482ec2175b5503d6e611b3f0555dc71d", size = 4785572, upload-time = "2026-09-02T14:48:01.029Z" }, + { url = "https://files.pythonhosted.org/packages/a5/1e/b90e845b1dcd0f2f3f26b98283d857f25909223aacd265eee032c34ab8b1/lxml-6.1.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:97ce49699d87ebf8aad631b55d65b33219a4f1bfefbbf5bff19dc9af160aeaf9", size = 5656516, upload-time = "2026-09-02T14:48:03.419Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ab/0a1b802c57f3fba5c4efd77d5c6b78adaa8f7b681f0c90456b140fe8bf6c/lxml-6.1.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:48542c9acba9ff9450bd18d871d2c2c8787fdb283572b623d206f1b927cd7d9e", size = 5245982, upload-time = "2026-09-02T14:48:06.109Z" }, + { url = "https://files.pythonhosted.org/packages/da/ee/2c016fbceb3778137459292538d9dfa7e3ad9070fe409c15254ddd90d2cc/lxml-6.1.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c55e71a9b1db1f107efb60da49c093689b74c5c31a708e5379e2fd9439d4fbb5", size = 5267340, upload-time = "2026-09-02T14:48:08.374Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b1/736d18fd6f0835761923b7bac1f0c27d60c1200384e9093f05d8c5100525/lxml-6.1.3-cp312-cp312-win32.whl", hash = "sha256:b3ff39654f0ce6ebd4db154211136dbe7e8157bcc3bed2344c87f32c7c6ecb6c", size = 3602606, upload-time = "2026-09-02T14:48:10.384Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5b/6ed903e4e6278a020c8a6f0dbbe78030d041840a6b4a64ea441a1e414077/lxml-6.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:3e9a00d1c2c30936f7add097c41afc5da6556c580909104aafd382cac92a855c", size = 4005999, upload-time = "2026-09-02T14:48:12.51Z" }, + { url = "https://files.pythonhosted.org/packages/e4/1b/7bcebb7b6332cb3ae85e9c13b139adb6f23f75c71d84041c56a5005d9a29/lxml-6.1.3-cp312-cp312-win_arm64.whl", hash = "sha256:1aeca87830c4fe649dcf93fe2b059525b71c72587f21be4ae4af7103082a79fa", size = 3666631, upload-time = "2026-09-02T14:48:14.567Z" }, + { url = "https://files.pythonhosted.org/packages/52/05/3ef45db776baea068044c799bbba68f3ca00a440c0e930a17c572f3d9639/lxml-6.1.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3a48093cdb058a93af842ede9703520e810b05dcd0fc6d7190a06376c3bfb6bd", size = 8590357, upload-time = "2026-09-02T14:48:17.413Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a5/eee2fc77eee5ea68e4a4334b1def1781a3beaeefd3d98e81b4a38dc447b7/lxml-6.1.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:887c021d9a977cff89cb273047c1352997b772a8908a25c21836861f69b92be1", size = 4632616, upload-time = "2026-09-02T14:48:20.745Z" }, + { url = "https://files.pythonhosted.org/packages/35/42/df27b56848acd29d8a720acc28977911aab36f2a09df4208d5502e887415/lxml-6.1.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:611a51e61c92f62345a50b0035df6fc0d678f9299f33728826d831598862f59d", size = 4936186, upload-time = "2026-09-02T14:48:22.94Z" }, + { url = "https://files.pythonhosted.org/packages/ab/8d/8a7b91df0b54d09d25f5f44885d6b3e0a6d6643a8c070191580318d20c42/lxml-6.1.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b477912f42c5c33405a10c759d22f80cf5af043ae02d95b9d8e5e5bc555739ed", size = 5093324, upload-time = "2026-09-02T14:48:25.132Z" }, + { url = "https://files.pythonhosted.org/packages/c6/7e/8f340ddcd43790332fb0de8a26628d571a492da3300cd191821698407c96/lxml-6.1.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cffe18571ccc51d742cd08cbb3f8b756de9311d18c7ea98f5d92f37b8fb60c2", size = 4998850, upload-time = "2026-09-02T14:48:27.394Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c1/9c5bb572f1f09ec9e4322bd4a4e9f4ad48347fc56ef94cf4df58a5279dc8/lxml-6.1.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75cc6569e86be5785b6188ef1642670c6adbc984e81ec35e224842ecd9eefcc8", size = 5626813, upload-time = "2026-09-02T14:48:29.61Z" }, + { url = "https://files.pythonhosted.org/packages/ac/7d/8bf1fd8bae8247743968bb76d027a1ac5bd2c4b44495fba6a71b30d10706/lxml-6.1.3-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d85dfab42dd672f87a7f76e9de7172962aee69fa12044f0d6e1a23cbd53fb80e", size = 5232385, upload-time = "2026-09-02T14:48:31.969Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2e/6cef69ed81cb7df0d03b0dd09d08e6e2cf5061a743ff6f42f0b741548e9b/lxml-6.1.3-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:42632b4024ab24a6b488f559ac851312509888b6b80ae2aa11cf29a646a0d245", size = 5347088, upload-time = "2026-09-02T14:48:34.13Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e1/8e5fd8ddc8c7d685badb0f2db149e3c9da84eefc2827c01c658df2c4e3cb/lxml-6.1.3-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:febd35ef45f603c2d74b74655efdbf45e14f55fc0aef4ac82b663ca829b283e0", size = 4707227, upload-time = "2026-09-02T14:48:36.62Z" }, + { url = "https://files.pythonhosted.org/packages/7a/7e/00041382a11be40a88bf405ebff11c8efabd3de79f2691e1638b1c47a8a0/lxml-6.1.3-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a43b3bdf11e477dc7770609d3477316f974354dfc8425d596f64f471cc8daf6e", size = 5240208, upload-time = "2026-09-02T14:48:38.893Z" }, + { url = "https://files.pythonhosted.org/packages/fd/fe/316538b5cff0936fa63d45d421c655730fcbb5a28dcac728c175083002bc/lxml-6.1.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d582042c69857c364e8153de6e18e0da9b7b515a6a8113caf69a6ec8e0520f2", size = 5050271, upload-time = "2026-09-02T14:48:41.213Z" }, + { url = "https://files.pythonhosted.org/packages/c9/91/455bcccb3ac725373007344d351151810cd19762d1673b64b811f4359a42/lxml-6.1.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8e49a646acfab83c68974f4aa1d0a2acca9e88d7d627ae0fc13201b14b76d310", size = 4780433, upload-time = "2026-09-02T14:48:43.779Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f6/580440e2f52cf00bba5c5e1080bfa88cdfcde73be71a11d95170ddbb663f/lxml-6.1.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0dee106e9aa97fb00541b1ed7827070564d0549c3d3fba8920e6b20fd980f748", size = 5645928, upload-time = "2026-09-02T14:48:46.187Z" }, + { url = "https://files.pythonhosted.org/packages/f6/dc/d123c1f244306543d545f62443f794959e4f1ea709fe100f8740d514e74a/lxml-6.1.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:dd5e90f34cffcfed97f36cf066325773d2b6021c60c29942e53a18b028501b1d", size = 5231184, upload-time = "2026-09-02T14:48:48.691Z" }, + { url = "https://files.pythonhosted.org/packages/c3/3c/fe55b2bd5c6113c906511cd88f6a470195c5fbff1124f19970ab706c3477/lxml-6.1.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d9b3e7d71bf6acff341233417abbdface29c647e3113892d9aaedc02eb4aa2bc", size = 5255814, upload-time = "2026-09-02T14:48:50.948Z" }, + { url = "https://files.pythonhosted.org/packages/e7/a7/485df55acf55dc35e4ca89d2f48f03889e5a3241826b18b85102b32ce9d8/lxml-6.1.3-cp313-cp313-win32.whl", hash = "sha256:160fcf381f76c3aeac28a756bec44f48942a8f7245a87aa28e3a523b4d90cd87", size = 3602214, upload-time = "2026-09-02T14:48:53.236Z" }, + { url = "https://files.pythonhosted.org/packages/c0/28/e46a7702bd95e9043291f7c3539b6184cba66f96cea9936f20939b284eeb/lxml-6.1.3-cp313-cp313-win_amd64.whl", hash = "sha256:e477aca0bc0d19f3b4ae9e4f2a1cfd687c31bf772d78734910658186b40b2477", size = 4004091, upload-time = "2026-09-02T14:48:55.699Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1d/154c78e20479a43916e63f19cb720d83f44f024b03228be44c92d9a97b24/lxml-6.1.3-cp313-cp313-win_arm64.whl", hash = "sha256:b1cc980905221a5d8b3c476330730b3adb40ff80add71ffbdb6215ba055656f1", size = 3665468, upload-time = "2026-09-02T14:48:57.703Z" }, + { url = "https://files.pythonhosted.org/packages/0c/15/fc75a70b0af6021d0ea16811f1fc71cc42cd06ce90fe10f007a69b2eed84/lxml-6.1.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2bec13085dc8ef48a3fe62f7dfcacfeda2c785cdf19cc8eeda2bb9ed081da165", size = 8609725, upload-time = "2026-09-02T14:49:00.156Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/398fcf9018f881ec9aeaafae1ddd6586dfb13314a35d35e899de373dcae0/lxml-6.1.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4f4db7c7e954d289d71878938348b3d91b904a3e8210a11939359fb758a58e7d", size = 4639629, upload-time = "2026-09-02T14:49:02.81Z" }, + { url = "https://files.pythonhosted.org/packages/a7/2d/49b6a6ad7ce8f64b07b9fe852ff0c6d3fcbb26db61bee4f63d4120180a1c/lxml-6.1.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2cae5d5c90a62d9139c512a0cb1aad1d182b022b5740daea2617eb5bf7fc658e", size = 4965074, upload-time = "2026-09-02T14:49:05.133Z" }, + { url = "https://files.pythonhosted.org/packages/66/bc/6230cf80e4331c33383b0b6b73dc31a393dd76edd4cb73d761de5123034d/lxml-6.1.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c6c0c13128a32eb04a51357e56a094e13aa8e6d3d1884de2e9ae923f6915e1a8", size = 5099355, upload-time = "2026-09-02T14:49:07.343Z" }, + { url = "https://files.pythonhosted.org/packages/ac/cf/d1143d9b7717e07a82f158a1fc9ce6e581fdad1226734950af869e3ffde4/lxml-6.1.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2221e88679d1351e9a40aaee54bc65679b9795bbd0160bc3d5e36b163344eb75", size = 5036795, upload-time = "2026-09-02T14:49:09.65Z" }, + { url = "https://files.pythonhosted.org/packages/31/6f/194bb00ffb89712c30f5a7e1b8e685590e140fad6c8261fec172c09a3dc0/lxml-6.1.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cfb398886a7eb4c719161c3efcff2a1248febc53a4d8e5072d2d8a87fed84ac9", size = 5658740, upload-time = "2026-09-02T14:49:11.9Z" }, + { url = "https://files.pythonhosted.org/packages/e9/44/27e3cee3dcdb3b7bc09727b642bdbfcd098490ea77df04611db9060d7722/lxml-6.1.3-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7eb78ba28b187e1e9203a55c60fcf70df2d22cb205fe6d51b9383d6097419f0", size = 5245991, upload-time = "2026-09-02T14:49:14.154Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e9/8312560579fc980bbd2233a8a673cc46f7d613d3633f2bf08a21e8f4ad13/lxml-6.1.3-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:ea6b1e9105b4b24a34c722432d9fb578f9ed83af21fa1abda639011e0f22bbb6", size = 5354136, upload-time = "2026-09-02T14:49:16.459Z" }, + { url = "https://files.pythonhosted.org/packages/74/d8/eda60f4f73a9c780b5d6e1175484f66e6c81a2c93346e2906a1fec9c7a02/lxml-6.1.3-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:e8b17e23df3e827a69d25af70990ca2420e92668aaffaeeb3cd2351d7916a023", size = 4704379, upload-time = "2026-09-02T14:49:19.032Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c8/c9cc60057be78ac34bd2b842e45e6e88edbfe5e532e82c3b82381b7aab49/lxml-6.1.3-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b7c37339d7e75cab9a123a04248e243cefefb302ad6db566ea0c77cbcde421e", size = 5258676, upload-time = "2026-09-02T14:49:21.306Z" }, + { url = "https://files.pythonhosted.org/packages/41/7b/66894008fee8d1785b8db129747ae963fd427b68f456918df7f2f24a8b98/lxml-6.1.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:83e3a51e7933db700a0da0db31849db3a24022d9970da9bb73001e1d0326fd92", size = 5090069, upload-time = "2026-09-02T14:49:23.562Z" }, + { url = "https://files.pythonhosted.org/packages/8b/31/c1b60404859f4c3cd1f41f29c65a24e25cea78fde822d9574a21f66810be/lxml-6.1.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:9bde9ae026a55b9a192078dfa6e27dd0ca4a050171ab6272e92f97b757dfdf48", size = 4741958, upload-time = "2026-09-02T14:49:26.037Z" }, + { url = "https://files.pythonhosted.org/packages/23/b8/6285f0cf546f14da2554cabdeaf7c2c2ff3190c74807f0de2e8810a786f9/lxml-6.1.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1a635e837b50a1819bebfedaac5916498ea024120969da8790500148fb0a894d", size = 5683245, upload-time = "2026-09-02T14:49:28.438Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f6/2168cab44336dcb15fed0f0b78577225b83297cdf0dee349c95420c3dcb0/lxml-6.1.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d0c5c362bc94f1929dc7e96e715bbe7bd17037f802e6d8f0d1545df9133c0559", size = 5246087, upload-time = "2026-09-02T14:49:30.955Z" }, + { url = "https://files.pythonhosted.org/packages/f5/89/32f5de69a0a31f30e6164981851f87b37ecb2c4ee838e504b88d49d4818e/lxml-6.1.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c59e4265608da6a041f54646ecc0c9ecdbb19aaf14c4c684bb6c2114998cc415", size = 5269352, upload-time = "2026-09-02T14:49:33.502Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a1/741d952ed3a7ef7a50055c6415aec3f067015e97f72f4389ce77b09657ba/lxml-6.1.3-cp314-cp314-win32.whl", hash = "sha256:2e62c569ec7531b679b184cbfe335c501c1d13c4b363560013019962eb630e6d", size = 3662783, upload-time = "2026-09-02T14:50:23.751Z" }, + { url = "https://files.pythonhosted.org/packages/0f/bc/5811cc73cac05e324e05ba9b0924e1a163a317a167ede8a9c748b11db30a/lxml-6.1.3-cp314-cp314-win_amd64.whl", hash = "sha256:66299564c046bc7e0cc5de5106601eae907e9fa5904cd68a323380a8502f7861", size = 4073951, upload-time = "2026-09-02T14:50:26.348Z" }, + { url = "https://files.pythonhosted.org/packages/92/18/3768c8b01ac3a9bed1914715e6011711b00e2a11628ffa6f7fa37f8e0269/lxml-6.1.3-cp314-cp314-win_arm64.whl", hash = "sha256:ebd054ad1737a68fb7c5c073d405cef2b88bb824e294de3b4a4e995b47f0e376", size = 3749279, upload-time = "2026-09-02T14:50:28.749Z" }, + { url = "https://files.pythonhosted.org/packages/72/38/84684784738d9451db2b330de2483f496690c3a5c642071df24135739b37/lxml-6.1.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:5a143e6207579de8baeded4eaac9134413200359f1969d636f0bfb98ee8c3c8f", size = 8860296, upload-time = "2026-09-02T14:49:36.346Z" }, + { url = "https://files.pythonhosted.org/packages/24/b7/fc4c50bb1b38e864010ea396046cabe85129bf9e65b11edcfbc37d356241/lxml-6.1.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a1cec0f99b9b914d39176347a93b7610dc09324491aee1cbc57cd291a41a1d55", size = 4755190, upload-time = "2026-09-02T14:49:39.872Z" }, + { url = "https://files.pythonhosted.org/packages/94/e2/ee9aa6ed2b666b2db1f6f7fd48964ff9da39ebe827ef5eac0ab881f639d9/lxml-6.1.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f6b9d2aad499c769ee8287609ab0e6de99d8bcea99c6e6c2e64945259fd52fb2", size = 4979517, upload-time = "2026-09-02T14:49:42.153Z" }, + { url = "https://files.pythonhosted.org/packages/29/e3/e7763d1661b283ddd4fa36f91b9a497db6b8d2aff55028b16c7f642e0755/lxml-6.1.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a23fefdb345b2d4d0ff2860571b5ff9a89a28b6a120f720e8fb0324d346626", size = 5115270, upload-time = "2026-09-02T14:49:44.493Z" }, + { url = "https://files.pythonhosted.org/packages/2d/cd/22205d5b4d177e3f4156f780412426ee7c7f8107809f119f0dcc40fa51e3/lxml-6.1.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:545ccc14fb05485f48b4439ec35beb16d5b5280eb6c81c658bd4707a2a119414", size = 5032449, upload-time = "2026-09-02T14:49:46.841Z" }, + { url = "https://files.pythonhosted.org/packages/da/43/06a4626c3bb79ef8c501b674afab8100d64e798665bb2a97d1c960636a49/lxml-6.1.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:93476b6514b373fc6ca67d26c442784f7807c86f00635bfe79f935c3eab2af17", size = 5603325, upload-time = "2026-09-02T14:49:49.664Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9c/733682a0c2de9f5779ba207bbb3f3f6be8c6bda863fc01739b186b38783a/lxml-6.1.3-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8db38ff3fb7aee7d6a82ae4da2eef1178656fe1216841fbd24870062a9d60473", size = 5229023, upload-time = "2026-09-02T14:49:52.447Z" }, + { url = "https://files.pythonhosted.org/packages/c6/8a/e69cdaca3fd33a647942925664f01b20908d41a6968c182305be9c38fb11/lxml-6.1.3-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:25f4118c438f96bb466e83108506d03d5c31b1bd2387e83e5b070bda6ded9c37", size = 5317811, upload-time = "2026-09-02T14:49:55.25Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b2/0c397588174403c2ab68fc464abf97e03e7324f9c6cb6a99023104707195/lxml-6.1.3-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:1beb0f9909b26cee938df9ba56b15252a84429b1fc30ce6fca161390b9789a70", size = 4646516, upload-time = "2026-09-02T14:49:57.761Z" }, + { url = "https://files.pythonhosted.org/packages/56/7e/cfea25afafbe49db8b225764f7f74bb37c2a7f5e717d917d3d4a5e098ed4/lxml-6.1.3-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3a27ac6c780c8b8a1cd231b58407634cafc1c4cc28cd6c7141362df0f36351e7", size = 5240626, upload-time = "2026-09-02T14:50:00.279Z" }, + { url = "https://files.pythonhosted.org/packages/a1/75/7a587771bb52ebb0e2c57b6dbe9fd96a70fbb54d72ddd97d54c5f8ec18d5/lxml-6.1.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a1932d7ce78a561367512c594fe66eac2b2ec9b9264cfd9b5f950622f4a116e2", size = 5086619, upload-time = "2026-09-02T14:50:03.245Z" }, + { url = "https://files.pythonhosted.org/packages/1e/01/94c0ebe6d831861542d251e038052e52bf6d33f1d18f1cfffdc82851065a/lxml-6.1.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:7d0f5976aa2701996f759b30172925829867547bb073af0ae67d1307a0f0262c", size = 4758828, upload-time = "2026-09-02T14:50:05.873Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f1/938d67bd0e5b1fdfa52be28aefdffbad57e1f6b8e921c2aab88542c75f40/lxml-6.1.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:c5e7ce578aa8a80910a72a8ca0bbea3baae10100827249001999726a788456d8", size = 5627083, upload-time = "2026-09-02T14:50:08.555Z" }, + { url = "https://files.pythonhosted.org/packages/d8/65/4e51522f6c214650db0abb7b16ccd11b1238b8a05a8d59aa4ebed59c9f67/lxml-6.1.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d97c5227621af74b111882a290b10f371780a38eef9d9e730408fba2259b52fb", size = 5235170, upload-time = "2026-09-02T14:50:11.255Z" }, + { url = "https://files.pythonhosted.org/packages/92/c2/e73d19365665f6b16ef84df21199befc3b06e4c539046ad2d9595f6fb9ea/lxml-6.1.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:da707f14ea3c35ee463d50acd596d6488e4b2b4ae7cf77a5bf93f55c023d63e8", size = 5252273, upload-time = "2026-09-02T14:50:13.782Z" }, + { url = "https://files.pythonhosted.org/packages/48/a9/7f386c84c9fe2854e1ca6e231c285e1c8f392971ac353c6865e6ec49faff/lxml-6.1.3-cp314-cp314t-win32.whl", hash = "sha256:9efe56a68179f3adc4de41861c9358931db03837c48dd5e1c78077b84dd07f3a", size = 3902712, upload-time = "2026-09-02T14:50:16.171Z" }, + { url = "https://files.pythonhosted.org/packages/82/a6/8a3eb793f7900ef01c7f99e6f5fcbcfbdff35251cfaef66b32a4c16352d6/lxml-6.1.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c9389b3784b56c58d933b5e0aecdf28f901b073ff385358d8a7d40907f6e14b2", size = 4400979, upload-time = "2026-09-02T14:50:18.621Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c4/3807bea283b4fe9e9d9f5dde46a73df91178472b335d2778e10b2a37aa22/lxml-6.1.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32a409be3190b088f960ac92bfedfbef2f86c49ff940765e1548177592d20026", size = 3823401, upload-time = "2026-09-02T14:50:21.119Z" }, + { url = "https://files.pythonhosted.org/packages/e1/8e/4614fcd65496054cfb7172662f3576a59200278739506433b8c241ea422a/lxml-6.1.3-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:6ea2f13dce778ca072ccee598bca46a092ce192e8fd907b6c1f0e52c800529a0", size = 8609378, upload-time = "2026-09-02T14:50:31.772Z" }, + { url = "https://files.pythonhosted.org/packages/f2/51/2cdce3c65fa99a6195dd8fbd512d33407c1000ad99f63e0a285b63d7a8eb/lxml-6.1.3-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:c581b1d68b3845fb86c6b2983e755b29bf001461c59fa411d2c26a911b6559a9", size = 4640022, upload-time = "2026-09-02T14:50:34.41Z" }, + { url = "https://files.pythonhosted.org/packages/52/09/0b30084e9eb1c546a4be3d9c56df70058d116b1a320400a59b0f7da87bf0/lxml-6.1.3-cp315-cp315-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e01125896585139453cab8cb235893644d8815d7509520da95ae3ee8d1c1f79", size = 5037928, upload-time = "2026-09-02T14:50:37.007Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0e/5c37275a3e361f6138dc06db748ea565c1fe8a5f4ee5e2ddd80047c81a89/lxml-6.1.3-cp315-cp315-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:290f66b97ede0e552e1cb44a0fd8a74f9753ee635b50830a0b122fb72788d015", size = 5661932, upload-time = "2026-09-02T14:50:39.777Z" }, + { url = "https://files.pythonhosted.org/packages/70/c5/b71ffb289b15e2642e2a3cf6d468c44da39ea119061a99e5b05e3d10f217/lxml-6.1.3-cp315-cp315-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73fc05988ed20809450474ba760a87c8ad4e455fc09783c02195e56ec634b41a", size = 5249209, upload-time = "2026-09-02T14:50:42.141Z" }, + { url = "https://files.pythonhosted.org/packages/81/ea/9910da149a23932f9301652e57661cd9e42b0df18f12be21159b7255f92b/lxml-6.1.3-cp315-cp315-manylinux_2_31_armv7l.whl", hash = "sha256:dc3a44689eea43eab836e5c98a8ab015dc2419987d1ea6eafc7c590cdff86bed", size = 4704543, upload-time = "2026-09-02T14:50:44.634Z" }, + { url = "https://files.pythonhosted.org/packages/76/07/9290329cd188c62e22021f79df04ee0cc33d9a93b0d38bd65ccd452ad9d0/lxml-6.1.3-cp315-cp315-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:209c3ccbfe35a04ac6d24f0611f9d1cbf8025d49991b14acd935236234d6c156", size = 5261298, upload-time = "2026-09-02T14:50:47.301Z" }, + { url = "https://files.pythonhosted.org/packages/c9/0c/aba78bd3401cd99b73a0aed8e2b9b43e14be94fab3603d4bbc8a62365f2a/lxml-6.1.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:2f5b2a2b9811b853b39bfa41367c6d78747b8e3e80e07fc5a24aae295c1a4d7d", size = 5090453, upload-time = "2026-09-02T14:50:49.952Z" }, + { url = "https://files.pythonhosted.org/packages/8d/dc/fa4426c3355aa0216cbeb3911495b5f65a26e0df85859a89928fe28f0396/lxml-6.1.3-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:6a406d0b3cb207b0fa460ed4dc93e866f44f105da0169361cb18ff998a44c7f0", size = 4744709, upload-time = "2026-09-02T14:50:52.394Z" }, + { url = "https://files.pythonhosted.org/packages/be/2b/224fe7918658ab7c532ac2412f3c1eb28f71e6364fb07566262d0cc6a7b6/lxml-6.1.3-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:53258656846f5c48996b882fb4b135885e088a3ad3d96b4bc0530f95124d1f69", size = 5685802, upload-time = "2026-09-02T14:50:55.043Z" }, + { url = "https://files.pythonhosted.org/packages/21/44/7d480819b9adcae5f84dd8ac529132c6b7a578544398225cd20321adcd91/lxml-6.1.3-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:aa633613ff907ea91b9b0489a1f0da1b8725d8c6ccec6b77e8a1c9c235044bb0", size = 5249019, upload-time = "2026-09-02T14:50:57.985Z" }, + { url = "https://files.pythonhosted.org/packages/72/83/385a267ea1b6b283f2249dd827ef360a295e9db14e13ef4665a120c60d64/lxml-6.1.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:90f709b9accab6b2e4d14f5c8718203877a0486bcb3afd74d8b539ecd1e961d4", size = 5271886, upload-time = "2026-09-02T14:51:01.667Z" }, + { url = "https://files.pythonhosted.org/packages/d8/0d/f967b0eb172ae876855a402d6d9b11fa86e3e0c89ca9bbfeadf7ffbfa719/lxml-6.1.3-cp315-cp315-win32.whl", hash = "sha256:b4fc6b03b9d9d90557274f571ab30e7fbbfc527955536935d96f98b6817a86e4", size = 3662894, upload-time = "2026-09-02T14:51:45.173Z" }, + { url = "https://files.pythonhosted.org/packages/f4/48/d8a8c4160a29e663109ad520bac2deb37fcd014756d024561e8bc3e611ec/lxml-6.1.3-cp315-cp315-win_amd64.whl", hash = "sha256:33cadd956b667997e4de1635fce9541f2e8ede2038fcde8cf55aa14d571d1bad", size = 4074626, upload-time = "2026-09-02T14:51:47.77Z" }, + { url = "https://files.pythonhosted.org/packages/25/20/3e1395d34d19f9254625d0b567b81cf70d37d3417be074f4d63b94a2be3c/lxml-6.1.3-cp315-cp315-win_arm64.whl", hash = "sha256:8a330c0ee5fa318c7b5cbbaad882baeca3f570357e7eb25ab34bf31008150758", size = 3749495, upload-time = "2026-09-02T14:51:50.663Z" }, + { url = "https://files.pythonhosted.org/packages/8f/c6/7465ffd9c43883526a382df6fa4846c9d8d419214f7effbf65270e795471/lxml-6.1.3-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:0bf5a3e397df2ec4258eb5eea4c1ac6cf013ca1abd04a176903bff20a70021fe", size = 8857677, upload-time = "2026-09-02T14:51:05.109Z" }, + { url = "https://files.pythonhosted.org/packages/ed/eb/1f3a917e299df43c8162c3e6f64fc2cea3bcf277910f35bff5b8e5d39901/lxml-6.1.3-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:13d22c0d57355366b393936acf6b98a5e0edeadddd3fccbc6a846c50a76b8741", size = 4754522, upload-time = "2026-09-02T14:51:08.137Z" }, + { url = "https://files.pythonhosted.org/packages/d7/f9/f81b4bdb6efb7a596be29603d8758154d00a5f545db9f3cef9d9041c8f64/lxml-6.1.3-cp315-cp315t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cad7617727a96d189bd6f979d0fadf765198c7934e85f4edaba9bf3ad919a300", size = 5033744, upload-time = "2026-09-02T14:51:10.633Z" }, + { url = "https://files.pythonhosted.org/packages/c8/0f/26d9bfaacb319c86e0eca8a1a0bf1130d36a7afbd318883e23caea63763d/lxml-6.1.3-cp315-cp315t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cae82b5ca24b0c2beedb269f6e2a96f466acd926879ab00ae19f1a65cbf9ffb0", size = 5615269, upload-time = "2026-09-02T14:51:13.357Z" }, + { url = "https://files.pythonhosted.org/packages/5d/90/73675f3f4141350ed65d6fec533b107d4e802c5caa340cf111771edd86e0/lxml-6.1.3-cp315-cp315t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:69cafd61aea04ebb3502c93c2aaa568b12931ca0802231e0b5de76bf8b6e74bd", size = 5236280, upload-time = "2026-09-02T14:51:16.051Z" }, + { url = "https://files.pythonhosted.org/packages/fd/be/ed260767e7977de463a0f91f3f4fffcab85c0a2a024a21ffe1fa442c2c79/lxml-6.1.3-cp315-cp315t-manylinux_2_31_armv7l.whl", hash = "sha256:dc205732d593118cf701d986f40e9de7801bb2e371cb189ddbda9b7348f4d97e", size = 4650718, upload-time = "2026-09-02T14:51:19.102Z" }, + { url = "https://files.pythonhosted.org/packages/d0/fd/e9839d03b1e767f2725cf7d7d81b80d5f3f9fdc10ad8827e2479311b046e/lxml-6.1.3-cp315-cp315t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88e719b9437f148f7e1465df845c758dd1598618cbea3a2fd1e61a715542f2b2", size = 5243376, upload-time = "2026-09-02T14:51:21.606Z" }, + { url = "https://files.pythonhosted.org/packages/34/a5/4606e347e2788c301f677004aa83e28d24da9fe663a24380122af57be6fc/lxml-6.1.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:40983eabefd13da003e68170928c7acc011f0d095eefce5871a3c71c9385fb9a", size = 5092340, upload-time = "2026-09-02T14:51:24.21Z" }, + { url = "https://files.pythonhosted.org/packages/ea/99/3314a8661cdf30f493c55a87db283961dfaae08451976a2ca418958e1804/lxml-6.1.3-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:fad67b12ffe0f71e02b4932b04883cbc76a9072bbd30731409d3523cf058b011", size = 4758768, upload-time = "2026-09-02T14:51:26.813Z" }, + { url = "https://files.pythonhosted.org/packages/30/58/3bdc577f78ea8b7d72d39a84506f7001d5b28728f43e5b84891e3b7d9a4a/lxml-6.1.3-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:6cd11e7550d89e551a87dcec30f04b1fca32e86b68708aa01a4daa455d8605e5", size = 5649546, upload-time = "2026-09-02T14:51:29.453Z" }, + { url = "https://files.pythonhosted.org/packages/6a/e4/652633de1a2395949ebb7a8fc7d089aba12a2b45f0fefbc9d29e3e3ab3cf/lxml-6.1.3-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:ca0ec532ad2f5ba1e5ec120ac157769c57f01855b3d8bf37213f5d88abd9ba0a", size = 5234874, upload-time = "2026-09-02T14:51:32.262Z" }, + { url = "https://files.pythonhosted.org/packages/65/a6/c4581d171de30449304b4859bbd3607e9b40da13c0f88b68e6097c8d785e/lxml-6.1.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:e99e09ab7741f1281e2677f4c0058c7f5267d182530b09c87e4f6aa26adf3887", size = 5260043, upload-time = "2026-09-02T14:51:34.841Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d7/ed6ee6186a89e69ca4ea9658b2a278f46a5efe8b5d4db56c7197f18653fe/lxml-6.1.3-cp315-cp315t-win32.whl", hash = "sha256:ace1d2c83b2bd24db5940600541140e87a325e119cb32d5fa9ad720d7e76648e", size = 3901093, upload-time = "2026-09-02T14:51:37.234Z" }, + { url = "https://files.pythonhosted.org/packages/67/9d/11d10257a4a048d04195d638bb61f0246ce2448eb05f682bcbab25a257a8/lxml-6.1.3-cp315-cp315t-win_amd64.whl", hash = "sha256:b49638355ea3bebba70da783ccbc630fd72afa16bc46c54474bfa1f9a915bbc6", size = 4395446, upload-time = "2026-09-02T14:51:39.884Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b7/44edd7de434181c582892e68d1ffe6775ca403ce14aea07cb5a218a936cf/lxml-6.1.3-cp315-cp315t-win_arm64.whl", hash = "sha256:5a721a98c649855963811b59b55755b30566e7f7fc40bdc9803d66dee9f811cf", size = 3822836, upload-time = "2026-09-02T14:51:42.471Z" }, + { url = "https://files.pythonhosted.org/packages/ad/23/dc1fdf3a53f84ca88b6e942277ddb47954844a0ececea8cc5fa3c1324831/lxml-6.1.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4b061064b4a2fe8598a466d723d43dbcd5a610a5d5cfe02fb6226f5c17349f75", size = 3947704, upload-time = "2026-09-02T14:46:22.27Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ed/e36d547d6c958b5693b873504735cb4d0388d545945d66a7aed8983a720b/lxml-6.1.3-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8499d464de86fab0f102313cce32a9bed9ab1f06ec813cf025cb790964fbb765", size = 4220149, upload-time = "2026-09-02T14:46:24.907Z" }, + { url = "https://files.pythonhosted.org/packages/98/54/7f51e6b6cc0755f9b5fc6637748279e9f48289d917b3a47ac9fedf3318d3/lxml-6.1.3-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9e67324961ac9bbe616cce5100514d2e34d88665aeb07071e8b16eac55d06d94", size = 4329391, upload-time = "2026-09-02T14:46:27.111Z" }, + { url = "https://files.pythonhosted.org/packages/eb/9e/840b0d2e25c10c491b010d555b46e6e5264d3ad73a91557405fceb738c35/lxml-6.1.3-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d12669a2c419b0e8dc423d23dea24bb82f6f9cb829f32e04674b0ba40322a7c", size = 4262125, upload-time = "2026-09-02T14:46:29.199Z" }, + { url = "https://files.pythonhosted.org/packages/69/8f/42a41571dfc772c12628747f883d24c978053856825b99d7a187117b8079/lxml-6.1.3-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:97acecb11cbc411473f15b8d780df06d7a9f3a2aad9aca78364f56640c8fb70e", size = 4410104, upload-time = "2026-09-02T14:46:32.102Z" }, + { url = "https://files.pythonhosted.org/packages/f3/aa/27d93812be916f1f674b2035edd86d41c77745ff2ad84f58c25a7445a397/lxml-6.1.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f8b9c8ceebae6387d0dc77f7f4dbbfbfc962dba2efbfe6877486075a480726b4", size = 3510724, upload-time = "2026-09-02T14:46:34.122Z" }, + { url = "https://files.pythonhosted.org/packages/ec/c1/2433176de263cc3f51fd2c303f993d5bb7f1da3139a0f7d168116c0bfa7a/lxml-6.1.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d2765c18ce303149ee804b1f3dad11232726dd0a702d73a15cf19179ac8cc962", size = 3942969, upload-time = "2026-09-02T14:46:36.55Z" }, + { url = "https://files.pythonhosted.org/packages/7c/71/de7759096f480180fd9e43ff7c017860e2d2a9a43741ab093cbdf1820f07/lxml-6.1.3-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d5a748d12dd9b535e0a130f60dae9ddf0adafbabe61e7864f55c7436c84547a", size = 4213008, upload-time = "2026-09-02T14:46:38.784Z" }, + { url = "https://files.pythonhosted.org/packages/b8/9b/c2d09af47a34fa6c0c27473083812b449a411680bd04bbe609cde291ddc8/lxml-6.1.3-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:41096ec0740a58dad03d3ae0c7486d306d20becefb13ceb1649835ab3eb64167", size = 4322012, upload-time = "2026-09-02T14:46:41.031Z" }, + { url = "https://files.pythonhosted.org/packages/68/f3/bf56fee0403ebd995be8e78ec9aca566016487d1b3cbf755ebea8ccffbdb/lxml-6.1.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:415e3a115c0d510e329020012834d1c0aa1c581ee53a218603e38abbc1dea70a", size = 4257402, upload-time = "2026-09-02T14:46:43.134Z" }, + { url = "https://files.pythonhosted.org/packages/1c/1d/6da9cc086a20d9dd6bcbf7c5d9575f0331cca9a05e67dab02d15e828170b/lxml-6.1.3-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20428910dae17a1a93152a3ff2c0441d2f4932992c0797d65651dd0561f1792f", size = 4410889, upload-time = "2026-09-02T14:46:46.975Z" }, + { url = "https://files.pythonhosted.org/packages/03/5c/91fe48856f9f8089be3096fa4dbe4b3fb5526f3bf3e852ea9497f399cb9f/lxml-6.1.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bc8dd3d9c93e70c3df974a201ac2958b6d77b465d813c51d1f15fa8e645763ae", size = 3511258, upload-time = "2026-09-02T14:46:49.046Z" }, +] + +[[package]] +name = "markdown" +version = "3.10.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/6f/da4c6aea59b3001f2e8c0ec7497475aadaf3b021c10cab5b2858f0f32b26/markdown-3.10.3.tar.gz", hash = "sha256:3589362618f743188b4d955b874402bc814f4f83f544dc207719f4baa7d9c45f", size = 372596, upload-time = "2026-07-30T19:05:29.005Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/69/4a5af2bc115a9a33fefe51709749de8262be3f9ba063d1753a837cdbc49c/markdown-3.10.3-py3-none-any.whl", hash = "sha256:fa6c92a00a4a3c98b22728c64a935ae1928250ae65058a6ded814d2cc29a4cea", size = 110757, upload-time = "2026-07-30T19:05:27.883Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + [[package]] name = "markupsafe" version = "3.0.3" @@ -1644,6 +3088,169 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] +[[package]] +name = "marshmallow" +version = "3.26.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging", marker = "python_full_version < '3.11'" }, +] +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 = "matplotlib" +version = "3.10.9" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'win32'", + "python_full_version < '3.11' and sys_platform != 'win32'", +] +dependencies = [ + { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "cycler", marker = "python_full_version < '3.11'" }, + { name = "fonttools", marker = "python_full_version < '3.11'" }, + { name = "kiwisolver", marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "packaging", marker = "python_full_version < '3.11'" }, + { name = "pillow", marker = "python_full_version < '3.11'" }, + { name = "pyparsing", marker = "python_full_version < '3.11'" }, + { name = "python-dateutil", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/1b/4be5be87d43d327a0cf4de1a56e86f7f84c89312452406cf122efe2839e6/matplotlib-3.10.9.tar.gz", hash = "sha256:fd66508e8c6877d98e586654b608a0456db8d7e8a546eb1e2600efd957302358", size = 34811233, upload-time = "2026-04-24T00:14:13.539Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/6f/340b04986e67aac6f66c5145ce68bf72c64bed30f92c8913499a6e6b8f99/matplotlib-3.10.9-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77210dce9cb8153dffc967efaae990543392563d5a376d4dd8539bebcb0ed217", size = 8296625, upload-time = "2026-04-24T00:11:43.376Z" }, + { url = "https://files.pythonhosted.org/packages/bb/2f/127081eb83162053ebb9678ceac64220b93a663e0167432566e9c7c82aab/matplotlib-3.10.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1e7698ac9868428e84d2c967424803b2472ff7167d9d6590d4204ed775343c3b", size = 8188790, upload-time = "2026-04-24T00:11:46.556Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b7/d8bcec2626c35f96972bff656299fef4578113ea6193c8fdad324710410c/matplotlib-3.10.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1aa972116abb4c9d201bf245620b433726cb6856f3bef6a78f776a00f5c92d37", size = 8769389, upload-time = "2026-04-24T00:11:48.959Z" }, + { url = "https://files.pythonhosted.org/packages/12/49/b78e214a527ea732033b7f4d37f7afb504d74ba9d134bd47938230dfb8b1/matplotlib-3.10.9-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae2f11957b27ce53497dd4d7b235c4d4f1faf383dfb39d0c5beb833bff883294", size = 9589657, upload-time = "2026-04-24T00:11:51.915Z" }, + { url = "https://files.pythonhosted.org/packages/5f/15/5246f7b43beae19c74dfee651d58d6cc8112e06f77adb4e88cc04f2e3a23/matplotlib-3.10.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b049278ddce116aaa1c1377ebf58adea909132dfce0281cf7e3a1ea9fc2e2c65", size = 9651983, upload-time = "2026-04-24T00:11:54.766Z" }, + { url = "https://files.pythonhosted.org/packages/75/77/5acecfe672ba0fa1b8c0454f69ce155d1e6fc5852fa7206bf9afaf767121/matplotlib-3.10.9-cp310-cp310-win_amd64.whl", hash = "sha256:82834c3c292d24d3a8aae77cd2d20019de69d692a34a970e4fdb8d33e2ea3dda", size = 8199701, upload-time = "2026-04-24T00:11:58.389Z" }, + { url = "https://files.pythonhosted.org/packages/4c/8c/290f021104741fea63769c31494f5324c0cd249bf536a65a4350767b1f22/matplotlib-3.10.9-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:68cfdcede415f7c8f5577b03303dd94526cdb6d11036cecdc205e08733b2d2bb", size = 8306860, upload-time = "2026-04-24T00:12:01.207Z" }, + { url = "https://files.pythonhosted.org/packages/51/18/325cd32ece1120d1da51cc4e4294c6580190699490183fc2fe8cb6d61ec5/matplotlib-3.10.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfca0129678bd56379db26c52b5d77ed7de314c047492fbdc763aa7501710cfb", size = 8199254, upload-time = "2026-04-24T00:12:04.239Z" }, + { url = "https://files.pythonhosted.org/packages/79/db/e28c1b83e3680740aa78925f5fb2ae4d16207207419ad75ea9fe604f8676/matplotlib-3.10.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e436d155fa8a3399dc62683f8f5d0e2e50d25d0144a73edd73f82eec8f4abfb", size = 8777092, upload-time = "2026-04-24T00:12:06.793Z" }, + { url = "https://files.pythonhosted.org/packages/55/fa/3ce7adfe9ba101748f465211660d9c6374c876b671bdb8c2bb6d347e8b94/matplotlib-3.10.9-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56fc0bd271b00025c6edfdc7c2dcd247372c8e1544971d62e1dc7c17367e8bf9", size = 9595691, upload-time = "2026-04-24T00:12:09.706Z" }, + { url = "https://files.pythonhosted.org/packages/36/c4/6960a76686ed668f2c60f84e9799ba4c0d56abdb36b1577b60c1d061d1ec/matplotlib-3.10.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5a6104ed666402ba5106d7f36e0e0cdca4e8d7fa4d39708ca88019e2835a2eb", size = 9659771, upload-time = "2026-04-24T00:12:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0d/271aace3342157c64700c9ff4c59c7b392f3dbab393692e8db6fbe7ab96c/matplotlib-3.10.9-cp311-cp311-win_amd64.whl", hash = "sha256:d730e984eddf56974c3e72b6129c7ca462ac38dc624338f4b0b23eb23ecba00f", size = 8205112, upload-time = "2026-04-24T00:12:15.773Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ee/cb57ad4754f3e7b9174ce6ce66d9205fb827067e48a9f58ac09d7e7d6b77/matplotlib-3.10.9-cp311-cp311-win_arm64.whl", hash = "sha256:51bf0ddbdc598e060d46c16b5590708f81a1624cefbaaf62f6a81bf9285b8c80", size = 8132310, upload-time = "2026-04-24T00:12:18.645Z" }, + { url = "https://files.pythonhosted.org/packages/35/c6/5581e26c72233ebb2a2a6fed2d24fb7c66b4700120b813f51b0555acf0b6/matplotlib-3.10.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f0c3c28d9fbcc1fe7a03be236d73430cf6409c41fb2383a7ac52fe932b072cb1", size = 8319908, upload-time = "2026-04-24T00:12:21.323Z" }, + { url = "https://files.pythonhosted.org/packages/b7/18/4880dd762e40cd360c1bf06e890c5a97b997e91cb324602b1a19950ad5ce/matplotlib-3.10.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cb28c2bd769aa3e98322c6ab09854cbcc52ab69d2759d681bba3e327b2b320", size = 8216016, upload-time = "2026-04-24T00:12:23.4Z" }, + { url = "https://files.pythonhosted.org/packages/32/91/d024616abdba99e83120e07a20658976f6a343646710760c4a51df126029/matplotlib-3.10.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ae20801130378b82d647ff5047c07316295b68dc054ca6b3c13519d0ea624285", size = 8789336, upload-time = "2026-04-24T00:12:26.096Z" }, + { url = "https://files.pythonhosted.org/packages/5c/04/030a2f61ef2158f5e4c259487a92ac877732499fb33d871585d89e03c42d/matplotlib-3.10.9-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c63ebcd8b4b169eb2f5c200552ae6b8be8999a005b6b507ed76fb8d7d674fe2", size = 9604602, upload-time = "2026-04-24T00:12:29.052Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c2/541e4d09d87bb6b5830fc28b4c887a9a8cf4e1c6cee698a8c05552ae2003/matplotlib-3.10.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d75d11c949914165976c621b2324f9ef162af7ebf4b057ddf95dd1dba7e5edcf", size = 9670966, upload-time = "2026-04-24T00:12:32.131Z" }, + { url = "https://files.pythonhosted.org/packages/04/a1/4571fc46e7702de8d0c2dc54ad1b2f8e29328dea3ee90831181f7353d93c/matplotlib-3.10.9-cp312-cp312-win_amd64.whl", hash = "sha256:d091f9d758b34aaaaa6331d13574bf01891d903b3dec59bfff458ef7551de5d6", size = 8217462, upload-time = "2026-04-24T00:12:35.226Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d0/2269edb12aa30c13c8bcc9382892e39943ce1d28aab4ec296e0381798e81/matplotlib-3.10.9-cp312-cp312-win_arm64.whl", hash = "sha256:10cc5ce06d10231c36f40e875f3c7e8050362a4ee8f0ee5d29a6b3277d57bb42", size = 8136688, upload-time = "2026-04-24T00:12:37.442Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d3/8d4f6afbecb49fc04e060a57c0fce39ea51cc163a6bd87303ccd698e4fa6/matplotlib-3.10.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b580440f1ff81a0e34122051a3dfabb7e4b7f9e380629929bde0eff9af72165f", size = 8320331, upload-time = "2026-04-24T00:12:39.688Z" }, + { url = "https://files.pythonhosted.org/packages/63/d9/9e14bc7564bf92d5ffa801ae5fac819ce74b925dfb55e3ebde61a3bbad3e/matplotlib-3.10.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b1b745c489cd1a77a0dc1120a05dc87af9798faebc913601feb8c73d89bf2d1e", size = 8216461, upload-time = "2026-04-24T00:12:42.494Z" }, + { url = "https://files.pythonhosted.org/packages/8a/17/4402d0d14ccf1dfc70932600b68097fbbf9c898a4871d2cbbe79c7801a32/matplotlib-3.10.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8f3bcac1ca5ed000a6f4337d47ba67dfddf37ed6a46c15fd7f014997f7bf865f", size = 8790091, upload-time = "2026-04-24T00:12:44.789Z" }, + { url = "https://files.pythonhosted.org/packages/3e/0b/322aeec06dd9b91411f92028b37d447342770a24392aa4813e317064dad5/matplotlib-3.10.9-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a8d66a55def891c33147ba3ba9bfcabf0b526a43764c818acbb4525e5ed0838", size = 9605027, upload-time = "2026-04-24T00:12:47.583Z" }, + { url = "https://files.pythonhosted.org/packages/74/88/5f13482f55e7b00bcfc09838b093c2456e1379978d2a146844aae05350ad/matplotlib-3.10.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d843374407c4017a6403b59c6c81606773d136f3259d5b6da3131bc814542cc2", size = 9671269, upload-time = "2026-04-24T00:12:50.878Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e0/0840fd2f93da988ec660b8ad1984abe9f25d2aed22a5e394ff1c68c88307/matplotlib-3.10.9-cp313-cp313-win_amd64.whl", hash = "sha256:f4399f64b3e94cd500195490972ae1ee81170df1636fa15364d157d5bdd7b921", size = 8217588, upload-time = "2026-04-24T00:12:53.784Z" }, + { url = "https://files.pythonhosted.org/packages/47/b9/d706d06dd605c49b9f83a2aed8c13e3e5db70697d7a80b7e3d7915de6b17/matplotlib-3.10.9-cp313-cp313-win_arm64.whl", hash = "sha256:ba7b3b8ef09eab7df0e86e9ae086faa433efbfbdb46afcb3aa16aabf779469a8", size = 8136913, upload-time = "2026-04-24T00:12:56.501Z" }, + { url = "https://files.pythonhosted.org/packages/9b/45/6e32d96978264c8ca8c4b1010adb955a1a49cfaf314e212bbc8908f04a61/matplotlib-3.10.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:09218df8a93712bd6ea133e83a153c755448cf7868316c531cffcc43f69d1cc9", size = 8368019, upload-time = "2026-04-24T00:12:58.896Z" }, + { url = "https://files.pythonhosted.org/packages/86/0a/c8e3d3bba245f0f7fc424937f8ff7ef77291a36af3edb97ccd78aa93d84f/matplotlib-3.10.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:82368699727bfb7b0182e1aa13082e3c08e092fa1a25d3e1fd92405bff96f6d4", size = 8264645, upload-time = "2026-04-24T00:13:01.406Z" }, + { url = "https://files.pythonhosted.org/packages/3d/aa/5bf5a14fe4fed73a4209a155606f8096ff797aad89c6c35179026571133e/matplotlib-3.10.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3225f4e1edcb8c86c884ddf79ebe20ecd0a67d30188f279897554ccd8fded4dc", size = 8802194, upload-time = "2026-04-24T00:13:03.702Z" }, + { url = "https://files.pythonhosted.org/packages/dd/5e/b4be852d6bba6fd15893fadf91ff26ae49cb91aac789e95dde9d342e664f/matplotlib-3.10.9-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de2445a0c6690d21b7eb6ce071cebad6d40a2e9bdf10d039074a96ba19797b99", size = 9622684, upload-time = "2026-04-24T00:13:06.647Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/ed428c971139112ef730f62770654d609467346d09d4b62617e1afd68a5a/matplotlib-3.10.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b2b9516251cb89ff618d757daec0e2ed1bf21248013844a853d87ef85ab3081d", size = 9680790, upload-time = "2026-04-24T00:13:10.009Z" }, + { url = "https://files.pythonhosted.org/packages/e7/09/052e884aaf2b985c63cb79f715f1d5b6a3eaa7de78f6a52b9dbc077d5b53/matplotlib-3.10.9-cp313-cp313t-win_amd64.whl", hash = "sha256:e9fae004b941b23ff2edcf1567a857ed77bafc8086ffa258190462328434faf8", size = 8287571, upload-time = "2026-04-24T00:13:13.087Z" }, + { url = "https://files.pythonhosted.org/packages/f4/38/ae27288e788c35a4250491422f3db7750366fc8c97d6f36fbdecfc1f5518/matplotlib-3.10.9-cp313-cp313t-win_arm64.whl", hash = "sha256:6b63d9c7c769b88ab81e10dc86e4e0607cf56817b9f9e6cf24b2a5f1693b8e38", size = 8188292, upload-time = "2026-04-24T00:13:15.546Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e6/3bd8afd04949f02eabc1c17115ea5255e19cacd4d06fc5abdde4eeb0052c/matplotlib-3.10.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:172db52c9e683f5d12eaf57f0f54834190e12581fe1cc2a19595a8f5acb4e77d", size = 8321276, upload-time = "2026-04-24T00:13:18.318Z" }, + { url = "https://files.pythonhosted.org/packages/41/86/86231232fff41c9f8e4a1a7d7a597d349a02527109c3af7d618366122139/matplotlib-3.10.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97e35e8d39ccc85859095e01a53847432ba9a53ddf7986f7a54a11b73d0e143f", size = 8218218, upload-time = "2026-04-24T00:13:20.974Z" }, + { url = "https://files.pythonhosted.org/packages/85/8f/becc9722cafc64f5d2eb0b7c1bf5f585271c618a45dbd8fabeb021f898b6/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aba1615dabe83188e19d4f75a253c6a08423e04c1425e64039f800050a69de6b", size = 9608145, upload-time = "2026-04-24T00:13:23.228Z" }, + { url = "https://files.pythonhosted.org/packages/32/5d/f7e914f7d9325abff4057cee62c0fa70263683189f774473cbfb534cd13b/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34cf8167e023ad956c15f36302911d5406bd99a9862c1a8499ea6f7c0e015dc2", size = 9885085, upload-time = "2026-04-24T00:13:25.849Z" }, + { url = "https://files.pythonhosted.org/packages/a5/fd/fa69f2221534e80cc5772ac2b7d222011a2acafc2ec7216d5dd174c864ae/matplotlib-3.10.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:59476c6d29d612b8e9bb6ce8c5b631be6ba8f9e3a2421f22a02b192c7dd28716", size = 9672358, upload-time = "2026-04-24T00:13:28.906Z" }, + { url = "https://files.pythonhosted.org/packages/ab/1a/5a4f747a8b271cbb024946d2dd3c913ab5032ba430626f8c3528ada96b4b/matplotlib-3.10.9-cp314-cp314-win_amd64.whl", hash = "sha256:336b9acc64d309063126edcdaca00db9373af3c476bb94388fe9c5a53ad13e6f", size = 8349970, upload-time = "2026-04-24T00:13:31.904Z" }, + { url = "https://files.pythonhosted.org/packages/64/dc/95d60ecaefe30680a154b52ea96ab4b0dab547f1fd6aa12f5fb655e89cae/matplotlib-3.10.9-cp314-cp314-win_arm64.whl", hash = "sha256:2dc9477819ffd78ad12a20df1d9d6a6bd4fec6aaa9072681465fddca052f1456", size = 8272785, upload-time = "2026-04-24T00:13:34.511Z" }, + { url = "https://files.pythonhosted.org/packages/70/a0/005d68bc8b8418300ce6591f18586910a8526806e2ab663933d9f20a41e9/matplotlib-3.10.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:da4e09638420548f31c354032a6250e473c68e5a4e96899b4844cf39ddea23fe", size = 8367999, upload-time = "2026-04-24T00:13:36.962Z" }, + { url = "https://files.pythonhosted.org/packages/22/05/1236cc9290be70b2498af20ca348add76e3fffe7f67b477db5133a84f3ea/matplotlib-3.10.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:345f6f68ecc8da0ca56fad2ea08fde1a115eda530079eca185d50a7bc3e146c6", size = 8264543, upload-time = "2026-04-24T00:13:39.851Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c2/071f5a5ff6c5bd63aaaf2f45c811d9bf2ced94bde188d9e1a519e21d0cba/matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4edcfbd8565339aa62f1cd4012f7180926fdbe71850f7b0d3c379c175cd6b66c", size = 9622800, upload-time = "2026-04-24T00:13:42.296Z" }, + { url = "https://files.pythonhosted.org/packages/95/57/da7d1f10a85624b9e7db68e069dd94e58dc41dbf9463c5921632ecbe3661/matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6be157fe17fc37cb95ac1d7374cf717ce9259616edec911a78d9d26dae8522d4", size = 9888561, upload-time = "2026-04-24T00:13:45.026Z" }, + { url = "https://files.pythonhosted.org/packages/67/b2/ef8d6bb59b0edb6c16c968b70f548aa13b54348972def5aa6ac85df67145/matplotlib-3.10.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4e42042d54db34fda4e95a7bd3e5789c2a995d2dad3eb8850232ee534092fbbf", size = 9680884, upload-time = "2026-04-24T00:13:48.066Z" }, + { url = "https://files.pythonhosted.org/packages/61/1c/d21bfeb9931881ebe96bcfcff27c7ae4b160ae0ec291a714c42641a56d75/matplotlib-3.10.9-cp314-cp314t-win_amd64.whl", hash = "sha256:c27df8b3848f32a83d1767566595e43cfaa4460380974da06f4279a7ec143c39", size = 8432333, upload-time = "2026-04-24T00:13:51.008Z" }, + { url = "https://files.pythonhosted.org/packages/78/23/92493c3e6e1b635ccfff146f7b99e674808787915420373ac399283764c2/matplotlib-3.10.9-cp314-cp314t-win_arm64.whl", hash = "sha256:a49f1eadc84ca85fd72fa4e89e70e61bf86452df6f971af04b12c60761a0772c", size = 8324785, upload-time = "2026-04-24T00:13:53.633Z" }, + { url = "https://files.pythonhosted.org/packages/2c/2b/0e92ad0ac446633f928a1563db4aa8add407e1924faf0ded5b95b35afb27/matplotlib-3.10.9-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1872fb212a05b729e649754a72d5da61d03e0554d76e80303b6f83d1d2c0552b", size = 8293058, upload-time = "2026-04-24T00:13:56.339Z" }, + { url = "https://files.pythonhosted.org/packages/4b/23/74682fd369f5299ceda438fea2a0662e6383b85c9383fb9cdfcf04713e07/matplotlib-3.10.9-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:985f2238880e2e69093f588f5fe2e46771747febf0649f3cf7f7b7480875317f", size = 8186627, upload-time = "2026-04-24T00:13:58.623Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e8/368aab88f3c4cd8992800f31abfe0670c3e47540ba20a97e9fdbcde594b3/matplotlib-3.10.9-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6640f75af2c6148293caa0a2b39dd806a492dd66c8a8b04035813e33d0fd2585", size = 8764117, upload-time = "2026-04-24T00:14:01.684Z" }, + { url = "https://files.pythonhosted.org/packages/63/e2/9f66ca6a651a52abfe0d4964ce01439ed34f3f1e119de10ff3a07f403043/matplotlib-3.10.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:42fb814efabe95c06c1994d8ab5a8385f43a249e23badd3ba931d4308e5bca20", size = 8304420, upload-time = "2026-04-24T00:14:04.57Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e8/467c03568218792906aa87b5e7bb379b605e056ed0c74fe00c051786d925/matplotlib-3.10.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f76e640a5268850bfda54b5131b1b1941cc685e42c5fa98ed9f2d64038308cba", size = 8197981, upload-time = "2026-04-24T00:14:07.233Z" }, + { url = "https://files.pythonhosted.org/packages/6f/87/afead29192170917537934c6aff4b008c805fff7b1ccea0c79120d96beda/matplotlib-3.10.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3fc0364dfbe1d07f6d15c5ebd0c5bf89e126916e5a8667dd4a7a6e84c36653d4", size = 8774002, upload-time = "2026-04-24T00:14:09.816Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.11.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'win32'", +] +dependencies = [ + { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform != 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, + { name = "cycler", marker = "(python_full_version >= '3.11' and sys_platform != 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, + { name = "fonttools", marker = "(python_full_version >= '3.11' and sys_platform != 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, + { name = "kiwisolver", marker = "(python_full_version >= '3.11' and sys_platform != 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*' and sys_platform != 'win32'" }, + { name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and sys_platform != 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, + { name = "packaging", marker = "(python_full_version >= '3.11' and sys_platform != 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, + { name = "pillow", marker = "(python_full_version >= '3.11' and sys_platform != 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, + { name = "pyparsing", marker = "(python_full_version >= '3.11' and sys_platform != 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, + { name = "python-dateutil", marker = "(python_full_version >= '3.11' and sys_platform != 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/64/f9a391af28f518b11ad45a8a712353c94a0aefce09d3703200e5c54b610a/matplotlib-3.11.1.tar.gz", hash = "sha256:69647db5746941c793d6e445a4cd349323ffb87d9cc958c2ad84a659b4832d30", size = 32612045, upload-time = "2026-07-18T03:39:46.63Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/d0/791aa183dd88491555cf7d4be0b52b0bcf6c3c2a2c22c815a2e819bf53e2/matplotlib-3.11.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b7cf158e7add54a8d51ac9b5a84abd6d4e13ed4951b4f25f1c5139f41c2addb2", size = 9440302, upload-time = "2026-07-18T03:38:03.844Z" }, + { url = "https://files.pythonhosted.org/packages/35/74/82bbdf683a301f4478384c8aaba6903631a2ca18294b2d7655c9a542bffb/matplotlib-3.11.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d2ace7273b9a5061a3b420918a16fae1f2dc5dfee1abcc13aba71b5d94b1820c", size = 9268549, upload-time = "2026-07-18T03:38:06.144Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f0/9b4298911303f74e6d83e64a81d996c0616405ec95046fac7f17e4258b9e/matplotlib-3.11.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee55e9041211bf84302ab55ec3965df18dd90ae19f8b58332a7feaf208bfe83", size = 10024922, upload-time = "2026-07-18T03:38:08.236Z" }, + { url = "https://files.pythonhosted.org/packages/84/6f/0bc3c3d05b021db44c14bc379a7c0df7d57302aa15380c16fd4e63fd6a9b/matplotlib-3.11.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f4bdeea33a8d15a071dbfe6d119451b1d719c733ac666d65357082901a9099", size = 10832170, upload-time = "2026-07-18T03:38:10.276Z" }, + { url = "https://files.pythonhosted.org/packages/db/4d/e375f39acdb2af5a9342730618608e39790ec842e6f1b392863028781459/matplotlib-3.11.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b4c78ceb2f11bcac7389d305cda17aeb1f4586a857854ab5780bd3dd8dbfc407", size = 10916701, upload-time = "2026-07-18T03:38:12.512Z" }, + { url = "https://files.pythonhosted.org/packages/bc/be/fa26ed085b41298f64a8f9b7592c671bbf1acc8b0df124c1c5de96b859f8/matplotlib-3.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:7f33a781e12b1e53b278deb2f5373c2e55ec4f10727be3440c0cfb5cda9f944f", size = 9315331, upload-time = "2026-07-18T03:38:14.949Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f3/eb5bdf3b6e191b200db298b08bbc1638b7f3c82cdc8680f9d88bf72559ae/matplotlib-3.11.1-cp311-cp311-win_arm64.whl", hash = "sha256:67e4c3cd578c65ebd81bdc09a1b6592ceafee6dfafe116dc85dfcb647b5bbb18", size = 9003475, upload-time = "2026-07-18T03:38:17.205Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6c/7ef7ebcb2bd9739b2b66b18b076e077f44bb46fdbe28ca0506edb3c62c79/matplotlib-3.11.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e15ef41507f3d525f46154ac9e3ae785dacde9f20e593a25de8986267892ef74", size = 9453849, upload-time = "2026-07-18T03:38:19.593Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f8/6d0c312c8d9738e7d9677f09fe5c986b3239e651a7b73a2deb38b65e4a71/matplotlib-3.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:21a67b961a6d597bca54fae826cd20695ba4a6e4d05424a08da6e13e3176fd6b", size = 9283113, upload-time = "2026-07-18T03:38:21.95Z" }, + { url = "https://files.pythonhosted.org/packages/c9/cf/b4ad2cc81b6672ea29ea04e64e350a9f9b493b0908ccd884c67eeff8f7b2/matplotlib-3.11.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ba8f811b8ddfac493734d6af0b2dff96919d0c28ca0d641858dab4262777c6ea", size = 10035615, upload-time = "2026-07-18T03:38:24.315Z" }, + { url = "https://files.pythonhosted.org/packages/88/90/4e10e033d9b66589d8ed98b84c95cdbb57033d57c1f41339d7393dbd2f2e/matplotlib-3.11.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c52f7ad20ef476806ed212380b1d54d20310c8b86bdc2c9a68b51f0024a44472", size = 10842559, upload-time = "2026-07-18T03:38:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/88/eb/799612d0f8cd3e816a10fec59329fca52cd2353264df80378dfc541ae855/matplotlib-3.11.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8b14eb22961fe865efb0e4ff167e333e428908b00115a8d800ccb65ee108e481", size = 10927532, upload-time = "2026-07-18T03:38:28.532Z" }, + { url = "https://files.pythonhosted.org/packages/88/89/56649bbaa2fd12e20f3be03dbcc135b0c8676d88bac17977599e3eb442a0/matplotlib-3.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:88a2a27dd9691ae448dfae4b26f59036be90c3c28757edd3553a29559d00859f", size = 9333886, upload-time = "2026-07-18T03:38:30.477Z" }, + { url = "https://files.pythonhosted.org/packages/c1/11/4d124efbbad677b7b7552f6f85a3bd432d4232f95400cea98fcd2ae36ef3/matplotlib-3.11.1-cp312-cp312-win_arm64.whl", hash = "sha256:480194afceca4df2f137c2721227d3cba67121fbf4397b69cee7f83714b0a58a", size = 9007545, upload-time = "2026-07-18T03:38:32.833Z" }, + { url = "https://files.pythonhosted.org/packages/04/6c/4798363b7fb5644e309fe1fac30216e9146c9f70859d80d588c18caf5317/matplotlib-3.11.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6771b0cd7838c6a857a7209814158c0ad09bfef878db3033dd82d70ad101f191", size = 9454341, upload-time = "2026-07-18T03:38:35.001Z" }, + { url = "https://files.pythonhosted.org/packages/59/98/6acadbe7f98df19d274bc107ac58bb439fa75df82c33dc110d71a4a8501f/matplotlib-3.11.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2abdee5ffa2fe11b2d19f7a5c63b785fb7c28cc46c7bc1814156341d9d1a33e1", size = 9283627, upload-time = "2026-07-18T03:38:37.061Z" }, + { url = "https://files.pythonhosted.org/packages/24/ea/65cec46fe241390ccea1b1754207ee28eb71c5ab866bd5f22fe47e538fa4/matplotlib-3.11.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0a19dcf73406d3746d25a5ed42d713604c9a3e024d129b102852b0d941cb9f3", size = 10035860, upload-time = "2026-07-18T03:38:39.663Z" }, + { url = "https://files.pythonhosted.org/packages/c7/10/63fdccccbabe002fb0960876baabc5e3f24d9c1bb4cfb25651457f74b3a0/matplotlib-3.11.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7389b77ed2ab0552f46d9a90b81b7b8e6dfcdc42adc36c37a0865799843e0e3e", size = 10843594, upload-time = "2026-07-18T03:38:42.144Z" }, + { url = "https://files.pythonhosted.org/packages/98/51/a1155945bff7b91381875022ac1522c5dfdac0d006be8e7df389b3134eae/matplotlib-3.11.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c90be0b73568da4f662afac580956a76e308437e641b4a45aa08925eeb67d95f", size = 10927962, upload-time = "2026-07-18T03:38:44.302Z" }, + { url = "https://files.pythonhosted.org/packages/0d/3a/3d5e1f42dc761bf53401a62a83ff93389b37de9d2c093b2a3aa49ac34f1b/matplotlib-3.11.1-cp313-cp313-win_amd64.whl", hash = "sha256:68408341f2312836fbbdf6b3c78047f65b2d8752f5fd221c3e72d348f5b34f8b", size = 9334074, upload-time = "2026-07-18T03:38:46.616Z" }, + { url = "https://files.pythonhosted.org/packages/e2/db/3f5ea5a5b64060ef5e1ff60a19170423e41ce21b8497a6fe15a36e0b43e3/matplotlib-3.11.1-cp313-cp313-win_arm64.whl", hash = "sha256:0c1f44890d435c1b4ef52f701ad5828cb450ea97bcc83918fda6be74965d6cd2", size = 9007662, upload-time = "2026-07-18T03:38:49.112Z" }, + { url = "https://files.pythonhosted.org/packages/98/6e/c7ae5e0531425b69c0826b00ebbc264c85cab853f1cd6e096c9983c2cdc1/matplotlib-3.11.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:5e510088c27a89d53580a752f959146893563e63c330e161d159b0fee652af6f", size = 9503790, upload-time = "2026-07-18T03:38:51.527Z" }, + { url = "https://files.pythonhosted.org/packages/92/79/15be162e0a2ed546939674e2e97d0e33ec2447d86d4d4e611fa295bb178c/matplotlib-3.11.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:1524e2bdd48a93557aa47ddcfe9c225dfdd57d5a01a5c49128c20f0632980ee1", size = 9336148, upload-time = "2026-07-18T03:38:53.564Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7f/36ffe144fc4aacfe0e3ed2318f72b6755d1e73b041d619b4d393e60f5a66/matplotlib-3.11.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:11664c551345553db92e61cae6cf1376f138f8c47cafdf13b64b18f3e3e9e464", size = 10049244, upload-time = "2026-07-18T03:38:55.911Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5f/55812d68c0a840d3a463638f48c00ab1fe338518ec49a640cb6473b444af/matplotlib-3.11.1-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e1f8922ba31959cf6a9dfb51be64b7f7bc582801a3957dc0c2f3afcd3537adf", size = 10860798, upload-time = "2026-07-18T03:38:58.282Z" }, + { url = "https://files.pythonhosted.org/packages/7a/64/cca444b4eb5e6c768c44fc5e1f0b5211f20ca2b282778051996e996a2bdf/matplotlib-3.11.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83235693abde86e5e0129998f80ee39fc7f58e6d56a88fafb28a9278833e9d5f", size = 10943282, upload-time = "2026-07-18T03:39:00.465Z" }, + { url = "https://files.pythonhosted.org/packages/e5/0f/a49c329d394f2e9ef38506982107e8b04ecf94dd41a9d8423ff82cc737c7/matplotlib-3.11.1-cp313-cp313t-win_amd64.whl", hash = "sha256:9a076f4fc5cdc43fdf510f5981418d25c2db4973418d9f22d8bb3dc8045ada78", size = 9383532, upload-time = "2026-07-18T03:39:02.468Z" }, + { url = "https://files.pythonhosted.org/packages/e4/50/103e86afb806d8f64d04ede14e4cfc09dbfc25f512421ff85fdd6ebd59cf/matplotlib-3.11.1-cp313-cp313t-win_arm64.whl", hash = "sha256:216fbb93a74add02ddb4cb38ef5348f59ac00b3e84567eaf16598772d40e150a", size = 9059665, upload-time = "2026-07-18T03:39:04.607Z" }, + { url = "https://files.pythonhosted.org/packages/35/04/3079499fa8cb661ea66d13d6439d5a3ae6710a7afd5c7f72e08914f275f8/matplotlib-3.11.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:30c492d4ba9448595b6fd8708c6725963f8148e25c0d8842948da5b05f0ee8d3", size = 9456022, upload-time = "2026-07-18T03:39:07.041Z" }, + { url = "https://files.pythonhosted.org/packages/53/a2/69acfe84ec1f32930e801a5782a07fc5c79c8c6599a507b806d859d5da8e/matplotlib-3.11.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ac104be2768ffdd8655db9e71b768cbb45f2b9aa7b450cf1595e8f65d3822319", size = 9285475, upload-time = "2026-07-18T03:39:09.562Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b3/31b15a2ca56d4ddd6aaa1c884c2f51cf9a61cfaf5ca6f6fbd6343d38e6df/matplotlib-3.11.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be943cb68bc6660ead58c55b3aa6366cba2ef7feb06460fbcce32360376f19f", size = 10847102, upload-time = "2026-07-18T03:39:11.532Z" }, + { url = "https://files.pythonhosted.org/packages/64/0d/a17e966e620545c1548125af0b29ac812dd17b197a18a7462ac12fa859ee/matplotlib-3.11.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5af0dcda57d471440a7b5b623e70e0a61003518443d9098f211a96ecfbbc25be", size = 11131087, upload-time = "2026-07-18T03:39:13.764Z" }, + { url = "https://files.pythonhosted.org/packages/97/c5/5e100efdd67abb7de20befaa333612ef9bfc63417fb71398f904f25d083c/matplotlib-3.11.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3d3fd84082b1afbd9398466c81309e20045be20d48fe0fb18c43504d164cbbb2", size = 10929036, upload-time = "2026-07-18T03:39:16.888Z" }, + { url = "https://files.pythonhosted.org/packages/ce/04/d719a0a36930ecc8dfc801ff340f9dcfc4223f8ca5d39d06b4020032fff8/matplotlib-3.11.1-cp314-cp314-win_amd64.whl", hash = "sha256:9601a1e90be21e4884c53b4f3dc3ee0544654946f9975258d691f1c2e2f119c6", size = 9489571, upload-time = "2026-07-18T03:39:19.449Z" }, + { url = "https://files.pythonhosted.org/packages/48/65/facabdc2f1f6caba7e856db64dfedddca25f7608df07d96a1c8fd114fd3b/matplotlib-3.11.1-cp314-cp314-win_arm64.whl", hash = "sha256:ae30c6109848ac0f9fa36c5d6270938487614c47ba31860bd5361266dabc5685", size = 9164486, upload-time = "2026-07-18T03:39:21.424Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/18da6cd01cf96354534f98c468a25380c68ce582a2c9dd0cae12b04af4f2/matplotlib-3.11.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:dadfe80797174e2984aae3be0b77594a3c72d2c0a40fbd4a0de48d2728caf3ae", size = 9504876, upload-time = "2026-07-18T03:39:23.633Z" }, + { url = "https://files.pythonhosted.org/packages/79/b0/f0b63555a18b79d038c81fd6126f35fc4dfce0eaff48d96103348c7cf935/matplotlib-3.11.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:89b193b255f4f6f7948dbcee3691f4f341ab05d9a8874a67b45ddb4182922eda", size = 9336120, upload-time = "2026-07-18T03:39:25.797Z" }, + { url = "https://files.pythonhosted.org/packages/c6/dd/f210ec7c4a6f198d5567237048a93d0811fb5a1f1691f13320e592f95b41/matplotlib-3.11.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:191163532cdefcb1571ca38a6d7e6474baccde64495783e6ba47aa07ec4b9bbb", size = 10858033, upload-time = "2026-07-18T03:39:27.999Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d2/d6d5324507c5fbb316db48e258c09c2807f3de03d9af47017e120070926f/matplotlib-3.11.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9fdf1c818ab05d0e74002091ddaf414478a3a449ec9d51c8976d45be7e3a01e2", size = 11141827, upload-time = "2026-07-18T03:39:30.092Z" }, + { url = "https://files.pythonhosted.org/packages/0f/68/3c22e9320bdce2c4d2f1320643ef706db7a24cb7420eea28b97a2d67f5a8/matplotlib-3.11.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b937b9dba5f5f6c1e31c47abe2186c865c0914fd18f2ce0dfc39c9adcef5951d", size = 10943061, upload-time = "2026-07-18T03:39:32.356Z" }, + { url = "https://files.pythonhosted.org/packages/f6/4a/907ed190ee81a9df581e0ed5456134fc0f7cb55ffcfda2f9e54ca900761c/matplotlib-3.11.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f2912f647f3fbe1ccf085f91e213936f9101bead81a5e670565b1f1b3712f4fb", size = 9540074, upload-time = "2026-07-18T03:39:34.789Z" }, + { url = "https://files.pythonhosted.org/packages/23/d4/97c19b77e0a6e3b48581185bb65088f431cd20186076cc0f650a1757ea46/matplotlib-3.11.1-cp314-cp314t-win_arm64.whl", hash = "sha256:54d47b8ae8b579633a3902ca5b4ad6c1e132a5626d64447b2e22a66394e79987", size = 9213472, upload-time = "2026-07-18T03:39:37.141Z" }, + { url = "https://files.pythonhosted.org/packages/ee/38/ceb1d637c4db6d06141f3739e93af3321e7caaabe69b57ae48ffe3ee95b1/matplotlib-3.11.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:427258425f9a3fc4ed79a91f9e9b9aaf5a82cb6571e85dc14063cc6fbb993741", size = 9438045, upload-time = "2026-07-18T03:39:39.491Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/72ad8b58602d3a6ef1dfc4b65ecd01634ab65a2bdf494c9fe0e966dbf081/matplotlib-3.11.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:1ac697e591c11b6ad04679a73c2d2f9980fe9d9f0311fb414a2e329706343dfb", size = 9266127, upload-time = "2026-07-18T03:39:41.597Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6d/69552382fcc8e93d1f2763ef2665980a900a48b7f3a4c57ed290726d1cbc/matplotlib-3.11.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e4b9ac2f1f607ecda2af90a5232beee2af7582fce1cc30c4b6a1b012dc21ee99", size = 10019439, upload-time = "2026-07-18T03:39:43.78Z" }, +] + [[package]] name = "mcp" version = "1.28.1" @@ -1669,6 +3276,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e2/5e/d118fce19f87a2e7d8101c35c8ae0ec289098a4df0ff244cec23e415aca0/mcp-1.28.1-py3-none-any.whl", hash = "sha256:2726bca5e7193f61c5dde8b12500a6de2d9acf6d1a1c0be9e8c2e706437991df", size = 222620, upload-time = "2026-06-26T12:57:27.218Z" }, ] +[[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 = "mechanical-markdown" version = "0.8.0" @@ -1697,6 +3313,93 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/76/b90f9d48d43fbd80a79a20d3eab2e5109859c7a56dc663b23187385898f3/mistune-3.3.0-py3-none-any.whl", hash = "sha256:a758e578acda49d8195f9a860b132dae2cf7bf409381393b1c4e6e489a65397b", size = 61250, upload-time = "2026-06-21T13:11:37.938Z" }, ] +[[package]] +name = "ml-dtypes" +version = "0.6.0" +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.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*' and sys_platform != 'win32'" }, + { name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and sys_platform != 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/12/72/307d7c4bd0600601c7133fba5cb78af7db968152951c1cd473abb1cda782/ml_dtypes-0.6.0.tar.gz", hash = "sha256:5e60251d32ced5598972e4d5e06a2f044341f9291402551a3f6f0ec44f9299b0", size = 3032327, upload-time = "2026-08-13T14:14:40.215Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/15/01285c64133ea38abf3b990a704d7d30e50daea2806d150bcc4163495d35/ml_dtypes-0.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:bad8d1dd5bed060a29332b99d63d0e5c2969081e1c6ea54adfbccfdfa783be44", size = 566808, upload-time = "2026-08-13T14:13:50.012Z" }, + { url = "https://files.pythonhosted.org/packages/e7/54/850d9b8b35549182f7c7f2cf742ce75c853ee880101bbc51cca0d62732e3/ml_dtypes-0.6.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:008382aeab529df5d3f00501ad9a7dcd64494d4b5b1971fc4c79019e6c1f5010", size = 356865, upload-time = "2026-08-13T14:13:51.339Z" }, + { url = "https://files.pythonhosted.org/packages/e9/15/844f5402145ce73bec8eb3afeb9f41d2bf99e0c8617c93f9e9886f26b419/ml_dtypes-0.6.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6ec0d244a5bba12239025389ad88bbfb45f9f10e25ab4f678e9a4768ebd47532", size = 412036, upload-time = "2026-08-13T14:13:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/f8/63/efc9257a1ef0f53dfc76dedfe70d7d35118fbcdb810bb48cb7323ebd0b87/ml_dtypes-0.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:03ce583adfce34ad33aa9e1fc7a8344dcf90ea776cc4ef0e5a48d4eae84e5d20", size = 433668, upload-time = "2026-08-13T14:13:53.668Z" }, + { url = "https://files.pythonhosted.org/packages/b8/2c/318cd1a9014c63939ffe687e19559ae12831fcc37d66c71ad1f616f1ffd6/ml_dtypes-0.6.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f4f59f83c82ab480e924b988e7b1b4eb4de836dfcf5390c6f59148d1a00e1d02", size = 566813, upload-time = "2026-08-13T14:13:55.053Z" }, + { url = "https://files.pythonhosted.org/packages/d9/83/706b8a39449f0d55a7d5f7d07a169da4decfafae8a1f4983a9236d4b49e8/ml_dtypes-0.6.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7728c0420ec1c338564fc8b01015ff2d58567e70f17fedce5a0a7c0308c0d5b9", size = 356864, upload-time = "2026-08-13T14:13:56.249Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b1/135a7bf47633f5b9184f0d0316af819884124d12b40965064bd216266514/ml_dtypes-0.6.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c8e39b53e90afda8ce52859c93de4dba3e02b76d85dcf091cc469f9184c6dae", size = 412043, upload-time = "2026-08-13T14:13:57.614Z" }, + { url = "https://files.pythonhosted.org/packages/07/23/8870bb62d6e499d6bcbc1242b9f11689bae00a3d39d3684a9aefad8b6ee6/ml_dtypes-0.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:3035518e3e19add1a4cac9236ab22888b208a4074912514313ccb2d6d242cde8", size = 433670, upload-time = "2026-08-13T14:13:59.097Z" }, + { url = "https://files.pythonhosted.org/packages/cf/7a/5d8fbe24d0bffd0d7cb5165a89f8ab7c3de000f26d6705242aeed99d583c/ml_dtypes-0.6.0-cp311-cp311-win_arm64.whl", hash = "sha256:5a519c9e95a216fbcb8e759793ef7fb40793fc803ed839142d6dc5be9be5bc89", size = 551915, upload-time = "2026-08-13T14:14:00.368Z" }, + { url = "https://files.pythonhosted.org/packages/84/6a/441eb053b078954f7fea284dfb288701884d0a1404d39babb858e1649023/ml_dtypes-0.6.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5359c588cc62de6f78d7430f06b65853d884955494d86d6ad90b6dd64a3f3a08", size = 565447, upload-time = "2026-08-13T14:14:01.737Z" }, + { url = "https://files.pythonhosted.org/packages/ed/cf/87e8a6c57eed63a91782a0d229856ddf73e138ce004dd71e2799a9dcdb33/ml_dtypes-0.6.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:37da32aa97749251025666d62372775019594577b9c9e9cfda83bed48d778fdb", size = 360227, upload-time = "2026-08-13T14:14:02.938Z" }, + { url = "https://files.pythonhosted.org/packages/c7/f9/7d76c1eae866f5d4636401b31b6d6dd90e4b4ced1fa7cfdfcca9c60e4bd3/ml_dtypes-0.6.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b4a480aa8fd54a1805b8ac10f3f91763926a74f73c0c364c10f9231854f4170", size = 409890, upload-time = "2026-08-13T14:14:04.248Z" }, + { url = "https://files.pythonhosted.org/packages/ba/db/9c61ec2760b5cbfb1c6558d5c991a6d8fd3271053c32db20506a9a90272b/ml_dtypes-0.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:2a3e9d53925597fbffafd2a37048dadeddd0bdaba58058f6ae0869ed709a184d", size = 439333, upload-time = "2026-08-13T14:14:05.501Z" }, + { url = "https://files.pythonhosted.org/packages/6a/57/780ca3e5ab135b9fbdd8e5441abf5f801b30398371b691291e05ab9834c0/ml_dtypes-0.6.0-cp312-cp312-win_arm64.whl", hash = "sha256:6eaed129a4afe90694b8685e2f9b6294849f5eda4af9a15be83a4326eeebd775", size = 552268, upload-time = "2026-08-13T14:14:06.866Z" }, + { url = "https://files.pythonhosted.org/packages/50/51/fd1582b8f5ed8a9e7be0e161a6ea0dff70cb280479a12178df0b3a72700e/ml_dtypes-0.6.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:084dfe51a7ad58b171f05115f8226ed4233a454a1611371947e806e76f0c638d", size = 565468, upload-time = "2026-08-13T14:14:08.5Z" }, + { url = "https://files.pythonhosted.org/packages/d2/22/20fd70ca6ed12446cb92d5b2a7745bd185f9d8b8cdeeadad976574398e6b/ml_dtypes-0.6.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28d676428b104bb9717b0928bc5c5129f2d6b51b6727587cc4289e7bf8713cb5", size = 360232, upload-time = "2026-08-13T14:14:09.873Z" }, + { url = "https://files.pythonhosted.org/packages/89/a5/da8ae6c6f1babe4b68e3e55d43d39b529e29774f10e0910671a6b8c86eb8/ml_dtypes-0.6.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26b1f1fa4f0435a2946859823f6e2bf06796f1e9f10f5a05b08a5e3c8f46ff69", size = 410169, upload-time = "2026-08-13T14:14:11.036Z" }, + { url = "https://files.pythonhosted.org/packages/e2/55/4561acefa00fa4bcbfb82ca6a48578b41f372cd7dd7cdd6eb4720abc2e5f/ml_dtypes-0.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:fb87f46b4f7ad7b5d3ad8f4b452b024bd4229d44c8ff934798c1fe656210387a", size = 439357, upload-time = "2026-08-13T14:14:12.172Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5d/6a01538e507ef0ed5e879985b13a92467bf8960696fb1131f8b8cadc60ff/ml_dtypes-0.6.0-cp313-cp313-win_arm64.whl", hash = "sha256:57ed0d6b4ac5e7868361303a9c57fbcf63b768236ee14456f585dfcf260d0292", size = 552278, upload-time = "2026-08-13T14:14:13.539Z" }, + { url = "https://files.pythonhosted.org/packages/d9/7a/97dc35667b7c9db33c5344c673cd27f87e34771875ea7100138726132ac9/ml_dtypes-0.6.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:84fa136b8602c8c39e3b6cb24918960cd6f36cade7a70376f56770729cd56510", size = 562551, upload-time = "2026-08-13T14:14:14.774Z" }, + { url = "https://files.pythonhosted.org/packages/db/48/77f0ede10558d0d935da2e3276ed7e9c8cc2bad3463b9a0b66b03fc60be2/ml_dtypes-0.6.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:317be9967fb84b0ce4e80e6b1bf71213d21971621cf6f1e501a63602a95297bf", size = 360334, upload-time = "2026-08-13T14:14:16.079Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b1/1831dd8c9b06c013085d31a2ac4f03392d43bd36bfc6ff591a08bcedc1cf/ml_dtypes-0.6.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f490c003369ce60e514a0c3b12374f05274c101fee1bead6740ec8a564032b0", size = 409966, upload-time = "2026-08-13T14:14:17.477Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ad/9c32c53f823dda3742df19a79c10bc198365937873ea125ba65747440c23/ml_dtypes-0.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:d574c2b28921dc72e869df248f1a278f6eee176a1f237c8642e1a71eb15f3977", size = 457224, upload-time = "2026-08-13T14:14:18.608Z" }, + { url = "https://files.pythonhosted.org/packages/41/3d/dd98205418a13353d41c52bf5326d8cbec515aace46174e23c6ea01c2978/ml_dtypes-0.6.0-cp314-cp314-win_arm64.whl", hash = "sha256:f4adb4af61516510d786cf8c01851a66f6d3ddfa79e1144deaa5b40d8507231e", size = 568378, upload-time = "2026-08-13T14:14:19.843Z" }, + { url = "https://files.pythonhosted.org/packages/65/36/32e7beef3281fed74883451477ad976364323206dbfaa95e948ba788dac7/ml_dtypes-0.6.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:3e169214e0d80ff1c038e1b3017e33c23e43bdf948d42d31de8283111c7e2fa3", size = 590177, upload-time = "2026-08-13T14:14:20.971Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a2/99b3d9b3c984b3bd1e81d8244f1fa2f812e44060d853205b2df6271aa17c/ml_dtypes-0.6.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:573b11f3c327e17ef3826d266e676cf1149a1f3016f822a05f2306c55d8246bf", size = 363142, upload-time = "2026-08-13T14:14:22.463Z" }, + { url = "https://files.pythonhosted.org/packages/0c/fb/8091c0aee7f2712de99c7fd4b1642382644dec6a4962effe4f5b9d16a973/ml_dtypes-0.6.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b76fa1d3f92967d58289ac47ab7458ede66e6f3527fff3e59142aee57d9307cd", size = 430645, upload-time = "2026-08-13T14:14:23.737Z" }, + { url = "https://files.pythonhosted.org/packages/c4/6f/962d2c589513b5930d05b6eae5fbd22ad8bbcf26bb763449f3d8f912360f/ml_dtypes-0.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:3be9911d953f97cddded4b9961d7b650473b7e55806d20f6176f8356dfe7b38e", size = 465667, upload-time = "2026-08-13T14:14:25.04Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ca/bcb25e246edd19af5fa1cf6267040bd9977a7afca846e6cfd4a52078b44f/ml_dtypes-0.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:e74266ca8e97874a937b7646378c178025650a236584f7474d10d8086a6edea3", size = 572706, upload-time = "2026-08-13T14:14:26.296Z" }, + { url = "https://files.pythonhosted.org/packages/12/42/46cb442648e3c774d8cb25f2e1e41d496cdcc91fbe9c2a6f75c0b8df7af6/ml_dtypes-0.6.0-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:b1b503864fada3f74fabf8d9fee7b4c1cbe956301e6fdece975d5f77c2fce958", size = 562550, upload-time = "2026-08-13T14:14:27.542Z" }, + { url = "https://files.pythonhosted.org/packages/07/56/844eff5af7a2d1a09d75df12c70225c3a6b6a771f95876b2bf5f7d10ad44/ml_dtypes-0.6.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c6ad60af4102789a5c09824004beade2f7f28cd1cd581ee5c170d9dc2fbb00e", size = 360332, upload-time = "2026-08-13T14:14:28.767Z" }, + { url = "https://files.pythonhosted.org/packages/b6/29/b7165a3a76364a5baa6aa4ee82a0adf73a3c014b8cd126120b62cc087992/ml_dtypes-0.6.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4f1b9329a251e4affe3bb58f4d3e2db22a714396fd7ffb40d0b5db423c24d17", size = 409964, upload-time = "2026-08-13T14:14:30.023Z" }, + { url = "https://files.pythonhosted.org/packages/c8/2e/f61c54a0544b6a170ac1bb89bcf406af53fb2deffc5476b6d2d3df5ba13e/ml_dtypes-0.6.0-cp315-cp315-win_amd64.whl", hash = "sha256:488c99ab181a2f59d9ec3b12c5fa11ec904e92be2c4ba18cded54dd7501208fe", size = 457249, upload-time = "2026-08-13T14:14:31.213Z" }, + { url = "https://files.pythonhosted.org/packages/63/00/bee1bc9faa02a46e7a851019fd23f47ca1f906609edbec8b6ba5decc3cc3/ml_dtypes-0.6.0-cp315-cp315-win_arm64.whl", hash = "sha256:de9d14748dbf3968951436ef514a29c9d1fe438aa680d110134ee2f7a9f9df18", size = 568381, upload-time = "2026-08-13T14:14:32.548Z" }, + { url = "https://files.pythonhosted.org/packages/72/f7/9a5edede28f73185fd51d75030ef7f11d76997bab3a92427d986e54fe2eb/ml_dtypes-0.6.0-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:e25bb3b0ad1217b60626e4ed45b10ca170c41d99fbe44a12bebc1e07ec4aad55", size = 589877, upload-time = "2026-08-13T14:14:33.695Z" }, + { url = "https://files.pythonhosted.org/packages/fd/81/d5924a141b850b606eb027493c9c3ca3c665cca5163af3f5b6e5e3345503/ml_dtypes-0.6.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:31f1ce979d31a357e95aa81812f20412c8c954fa43c44ee3ead1e1c8a78575ef", size = 362788, upload-time = "2026-08-13T14:14:34.996Z" }, + { url = "https://files.pythonhosted.org/packages/59/8f/3298e3f334832bc28dd144af6b99cdc93502a8687e71922ea68b0a319929/ml_dtypes-0.6.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2d6149f3a57f405bcad5fb41e03218b8373936253f23e1ca84c0108abbc3392", size = 430823, upload-time = "2026-08-13T14:14:36.44Z" }, + { url = "https://files.pythonhosted.org/packages/93/d2/f2dbf118f42ce4c325a139c9236737f436b7f8e00cd18701c99ef2405e6f/ml_dtypes-0.6.0-cp315-cp315t-win_amd64.whl", hash = "sha256:ce7563e0b1a4482cbc1b4a6272145e54e4489e54fe7428f94908c3d87103abfa", size = 465119, upload-time = "2026-08-13T14:14:37.776Z" }, + { url = "https://files.pythonhosted.org/packages/5a/ff/bda40387b5c5c64254595f4d81a12351770856acc5de4e6d43606a31f161/ml_dtypes-0.6.0-cp315-cp315t-win_arm64.whl", hash = "sha256:f6cb525101b6b903779188c1e9e9490c343b455ab822883e02cf01e5547338d2", size = 572666, upload-time = "2026-08-13T14:14:38.993Z" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "msal" +version = "1.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b8/1f/10f9d47a63d3a2e61b2c43e15bee6b95682aab827018f9a1b97a80787e25/msal-1.38.0.tar.gz", hash = "sha256:4f10ff1257bacfd1781f22e85bd2b8d43ad1b490f3b6aafd7906671cadedd464", size = 203411, upload-time = "2026-08-24T10:22:46.053Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/ca/d768f77a27d81ed0a6884f2458f8613c31c79b2eb95defbeca2273fd0754/msal-1.38.0-py3-none-any.whl", hash = "sha256:765b9b98b6aa380ee8b8f1c75636e08863edaf0a953498955bd668650dde5d49", size = 131057, upload-time = "2026-08-24T10:22:47.485Z" }, +] + +[[package]] +name = "msal-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "msal" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/99/5d239b6156eddf761a636bded1118414d161bd6b7b37a9335549ed159396/msal_extensions-1.3.1.tar.gz", hash = "sha256:c5b0fd10f65ef62b5f1d62f4251d51cbcaf003fcedae8c91b040a488614be1a4", size = 23315, upload-time = "2025-03-14T23:51:03.902Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/75/bd9b7bb966668920f06b200e84454c8f3566b102183bc55c5473d96cb2b9/msal_extensions-1.3.1-py3-none-any.whl", hash = "sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca", size = 20583, upload-time = "2025-03-14T23:51:03.016Z" }, +] + [[package]] name = "msgpack" version = "1.2.1" @@ -1908,6 +3611,70 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, ] +[[package]] +name = "murmurhash" +version = "1.0.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/23/2e/88c147931ea9725d634840d538622e94122bceaf346233349b7b5c62964b/murmurhash-1.0.15.tar.gz", hash = "sha256:58e2b27b7847f9e2a6edf10b47a8c8dd70a4705f45dccb7bf76aeadacf56ba01", size = 13291, upload-time = "2025-11-14T09:51:15.272Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/3c/5e59e29fe971365d27f191a5cbf8a5fb492746e458604fe5d39810da4668/murmurhash-1.0.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f4989c16053a9a83b02c520dd00a31f0877d5fd2ab8a9b6b75ed9eba0e25c489", size = 27463, upload-time = "2025-11-14T09:49:53.158Z" }, + { url = "https://files.pythonhosted.org/packages/38/3d/ace00a9b82beaa99a8a7a52e98171cfbf13c0066d2f820e84a5d572e3bd0/murmurhash-1.0.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:899068ba3d7c371e7edd093852c634cce802fefd9aaddfcc0d2fda1d7433c7f9", size = 27714, upload-time = "2025-11-14T09:49:54.855Z" }, + { url = "https://files.pythonhosted.org/packages/10/0f/34f1c4f97424ea1bc72b1e3bdf61ac34f4c5555ec9163721f1e4cafe5b1d/murmurhash-1.0.15-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fe883982114de576c793fd1cf55945c8ee6453ad4c4785ac1a48f84e74fdc650", size = 122570, upload-time = "2025-11-14T09:49:55.977Z" }, + { url = "https://files.pythonhosted.org/packages/b9/75/0019717a16ce5a7b088fc50a3ecb513035e4196c5e569bf4a2e16bcc0414/murmurhash-1.0.15-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:342277d8d7f712d136507fb3ccdba26c076a34ca0f8d1b96f65f0daa556da2e9", size = 123194, upload-time = "2025-11-14T09:49:57.462Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a4/c1c95ce60b816c2255098164e424752779269c93f5d6dceaa213346789a2/murmurhash-1.0.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bc54facccb32fe1e97d6231edd4f3e2937467c35658b26aa35bbd6a87ebb7cb0", size = 122461, upload-time = "2025-11-14T09:49:58.686Z" }, + { url = "https://files.pythonhosted.org/packages/63/28/e1f79369a6e8d1a5901346ed2fd3a5c56e647d0b849044870c071cb64e1c/murmurhash-1.0.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e525bbd8e26e6b9ab1b56758a59b16c2fffd73bad2f7b8bf361c16f70ff1d980", size = 121676, upload-time = "2025-11-14T09:49:59.888Z" }, + { url = "https://files.pythonhosted.org/packages/1d/7c/e2be1f5387e5898f6551cf81c4220975858b9dbda4d471b133750945599a/murmurhash-1.0.15-cp310-cp310-win_amd64.whl", hash = "sha256:2224f30f7729717644745a6f513ea7662517dfe7b1867cf1588177f64c61df3c", size = 25156, upload-time = "2025-11-14T09:50:01.016Z" }, + { url = "https://files.pythonhosted.org/packages/74/07/0df6e1a753de68368662cbbb8f88558e2c877d3886ac12b30953fb8ed335/murmurhash-1.0.15-cp310-cp310-win_arm64.whl", hash = "sha256:8a181494b5f03ba831f9a13f2de3aab9ef591e508e57239043d65c5c592f5837", size = 23270, upload-time = "2025-11-14T09:50:01.99Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ca/77d3e69924a8eb4508bb4f0ad34e46adbeedeb93616a71080e61e53dad71/murmurhash-1.0.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f32307fb9347680bb4fe1cbef6362fb39bd994f1b59abd8c09ca174e44199081", size = 27397, upload-time = "2025-11-14T09:50:03.077Z" }, + { url = "https://files.pythonhosted.org/packages/e6/53/a936f577d35b245d47b310f29e5e9f09fcac776c8c992f1ab51a9fb0cee2/murmurhash-1.0.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:539d8405885d1d19c005f3a2313b47e8e54b0ee89915eb8dfbb430b194328e6c", size = 27692, upload-time = "2025-11-14T09:50:04.144Z" }, + { url = "https://files.pythonhosted.org/packages/4d/64/5f8cfd1fd9cbeb43fcff96672f5bd9e7e1598d1c970f808ecd915490dc20/murmurhash-1.0.15-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c4cd739a00f5a4602201b74568ddabae46ec304719d9be752fd8f534a9464b5e", size = 128396, upload-time = "2025-11-14T09:50:05.268Z" }, + { url = "https://files.pythonhosted.org/packages/ac/10/d9ce29d559a75db0d8a3f13ea12c7f541ec9de2afca38dc70418b890eedb/murmurhash-1.0.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:44d211bcc3ec203c47dac06f48ee871093fcbdffa6652a6cc5ea7180306680a8", size = 128687, upload-time = "2025-11-14T09:50:06.527Z" }, + { url = "https://files.pythonhosted.org/packages/48/cd/dc97ab7e68cdfa1537a56e36dbc846c5a66701cc39ecee2d4399fe61996c/murmurhash-1.0.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f9bf47101354fb1dc4b2e313192566f04ba295c28a37e2f71c692759acc1ba3c", size = 128198, upload-time = "2025-11-14T09:50:08.062Z" }, + { url = "https://files.pythonhosted.org/packages/53/73/32f2aaa22c1e4afae337106baf0c938abf36a6cc879cfee83a00461bbbf7/murmurhash-1.0.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c69b4d3bcd6233782a78907fe10b9b7a796bdc5d28060cf097d067bec280a5d", size = 127214, upload-time = "2025-11-14T09:50:09.265Z" }, + { url = "https://files.pythonhosted.org/packages/82/ed/812103a7f353eba2d83655b08205e13a38c93b4db0692f94756e1eb44516/murmurhash-1.0.15-cp311-cp311-win_amd64.whl", hash = "sha256:e43a69496342ce530bdd670264cb7c8f45490b296e4764c837ce577e3c7ebd53", size = 25241, upload-time = "2025-11-14T09:50:10.373Z" }, + { url = "https://files.pythonhosted.org/packages/eb/5f/2c511bdd28f7c24da37a00116ffd0432b65669d098f0d0260c66ac0ffdc2/murmurhash-1.0.15-cp311-cp311-win_arm64.whl", hash = "sha256:f3e99a6ee36ef5372df5f138e3d9c801420776d3641a34a49e5c2555f44edba7", size = 23216, upload-time = "2025-11-14T09:50:11.651Z" }, + { url = "https://files.pythonhosted.org/packages/b6/46/be8522d3456fdccf1b8b049c6d82e7a3c1114c4fc2cfe14b04cba4b3e701/murmurhash-1.0.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d37e3ae44746bca80b1a917c2ea625cf216913564ed43f69d2888e5df97db0cb", size = 27884, upload-time = "2025-11-14T09:50:13.133Z" }, + { url = "https://files.pythonhosted.org/packages/ed/cc/630449bf4f6178d7daf948ce46ad00b25d279065fc30abd8d706be3d87e0/murmurhash-1.0.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0861cb11039409eaf46878456b7d985ef17b6b484103a6fc367b2ecec846891d", size = 27855, upload-time = "2025-11-14T09:50:14.859Z" }, + { url = "https://files.pythonhosted.org/packages/ff/30/ea8f601a9bf44db99468696efd59eb9cff1157cd55cb586d67116697583f/murmurhash-1.0.15-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5a301decfaccfec70fe55cb01dde2a012c3014a874542eaa7cc73477bb749616", size = 134088, upload-time = "2025-11-14T09:50:15.958Z" }, + { url = "https://files.pythonhosted.org/packages/c9/de/c40ce8c0877d406691e735b8d6e9c815f36a82b499d358313db5dbe219d7/murmurhash-1.0.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:32c6fde7bd7e9407003370a07b5f4addacabe1556ad3dc2cac246b7a2bba3400", size = 133978, upload-time = "2025-11-14T09:50:17.572Z" }, + { url = "https://files.pythonhosted.org/packages/47/84/bd49963ecd84ebab2fe66595e2d1ed41d5e8b5153af5dc930f0bd827007c/murmurhash-1.0.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5d8b43a7011540dc3c7ce66f2134df9732e2bc3bbb4a35f6458bc755e48bde26", size = 132956, upload-time = "2025-11-14T09:50:18.742Z" }, + { url = "https://files.pythonhosted.org/packages/4f/7c/2530769c545074417c862583f05f4245644599f1e9ff619b3dfe2969aafc/murmurhash-1.0.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:43bf4541892ecd95963fcd307bf1c575fc0fee1682f41c93007adee71ca2bb40", size = 134184, upload-time = "2025-11-14T09:50:19.941Z" }, + { url = "https://files.pythonhosted.org/packages/84/a4/b249b042f5afe34d14ada2dc4afc777e883c15863296756179652e081c44/murmurhash-1.0.15-cp312-cp312-win_amd64.whl", hash = "sha256:f4ac15a2089dc42e6eb0966622d42d2521590a12c92480aafecf34c085302cca", size = 25647, upload-time = "2025-11-14T09:50:21.049Z" }, + { url = "https://files.pythonhosted.org/packages/13/bf/028179259aebc18fd4ba5cae2601d1d47517427a537ab44336446431a215/murmurhash-1.0.15-cp312-cp312-win_arm64.whl", hash = "sha256:4a70ca4ae19e600d9be3da64d00710e79dde388a4d162f22078d64844d0ebdda", size = 23338, upload-time = "2025-11-14T09:50:22.359Z" }, + { url = "https://files.pythonhosted.org/packages/29/2f/ba300b5f04dae0409202d6285668b8a9d3ade43a846abee3ef611cb388d5/murmurhash-1.0.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fe50dc70e52786759358fd1471e309b94dddfffb9320d9dfea233c7684c894ba", size = 27861, upload-time = "2025-11-14T09:50:23.804Z" }, + { url = "https://files.pythonhosted.org/packages/34/02/29c19d268e6f4ea1ed2a462c901eed1ed35b454e2cbc57da592fad663ac6/murmurhash-1.0.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1349a7c23f6092e7998ddc5bd28546cc31a595afc61e9fdb3afc423feec3d7ad", size = 27840, upload-time = "2025-11-14T09:50:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/e2/63/58e2de2b5232cd294c64092688c422196e74f9fa8b3958bdf02d33df24b9/murmurhash-1.0.15-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3ba6d05de2613535b5a9227d4ad8ef40a540465f64660d4a8800634ae10e04f", size = 133080, upload-time = "2025-11-14T09:50:26.566Z" }, + { url = "https://files.pythonhosted.org/packages/aa/9a/d13e2e9f8ba1ced06840921a50f7cece0a475453284158a3018b72679761/murmurhash-1.0.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fa1b70b3cc2801ab44179c65827bbd12009c68b34e9d9ce7125b6a0bd35af63c", size = 132648, upload-time = "2025-11-14T09:50:27.788Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e1/47994f1813fa205c84977b0ff51ae6709f8539af052c7491a5f863d82bdc/murmurhash-1.0.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:213d710fb6f4ef3bc11abbfad0fa94a75ffb675b7dc158c123471e5de869f9af", size = 131502, upload-time = "2025-11-14T09:50:29.339Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ea/90c1fd00b4aeb704fb5e84cd666b33ffd7f245155048071ffbb51d2bb57d/murmurhash-1.0.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b65a5c4e7f5d71f7ccac2d2b60bdf7092d7976270878cfec59d5a66a533db823", size = 132736, upload-time = "2025-11-14T09:50:30.545Z" }, + { url = "https://files.pythonhosted.org/packages/00/db/da73462dbfa77f6433b128d2120ba7ba300f8c06dc4f4e022c38d240a5f5/murmurhash-1.0.15-cp313-cp313-win_amd64.whl", hash = "sha256:9aba94c5d841e1904cd110e94ceb7f49cfb60a874bbfb27e0373622998fb7c7c", size = 25682, upload-time = "2025-11-14T09:50:31.624Z" }, + { url = "https://files.pythonhosted.org/packages/bb/83/032729ef14971b938fbef41ee125fc8800020ee229bd35178b6ede8ee934/murmurhash-1.0.15-cp313-cp313-win_arm64.whl", hash = "sha256:263807eca40d08c7b702413e45cca75ecb5883aa337237dc5addb660f1483378", size = 23370, upload-time = "2025-11-14T09:50:33.264Z" }, + { url = "https://files.pythonhosted.org/packages/10/83/7547d9205e9bd2f8e5dfd0b682cc9277594f98909f228eb359489baec1df/murmurhash-1.0.15-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:694fd42a74b7ce257169d14c24aa616aa6cd4ccf8abe50eca0557e08da99d055", size = 29955, upload-time = "2025-11-14T09:50:34.488Z" }, + { url = "https://files.pythonhosted.org/packages/b7/c7/3afd5de7a5b3ae07fe2d3a3271b327ee1489c58ba2b2f2159bd31a25edb9/murmurhash-1.0.15-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a2ea4546ba426390beff3cd10db8f0152fdc9072c4f2583ec7d8aa9f3e4ac070", size = 30108, upload-time = "2025-11-14T09:50:35.53Z" }, + { url = "https://files.pythonhosted.org/packages/02/69/d6637ee67d78ebb2538c00411f28ea5c154886bbe1db16c49435a8a4ab16/murmurhash-1.0.15-cp313-cp313t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:34e5a91139c40b10f98d0b297907f5d5267b4b1b2e5dd2eb74a021824f751b98", size = 164054, upload-time = "2025-11-14T09:50:36.591Z" }, + { url = "https://files.pythonhosted.org/packages/ab/4c/89e590165b4c7da6bf941441212a721a270195332d3aacfdfdf527d466ca/murmurhash-1.0.15-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:dc35606868a5961cf42e79314ca0bddf5a400ce377b14d83192057928d6252ec", size = 168153, upload-time = "2025-11-14T09:50:37.856Z" }, + { url = "https://files.pythonhosted.org/packages/07/7a/95c42df0c21d2e413b9fcd17317a7587351daeb264dc29c6aec1fdbd26f8/murmurhash-1.0.15-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:43cc6ac3b91ca0f7a5ae9c063ba4d6c26972c97fd7c25280ecc666413e4c5535", size = 164345, upload-time = "2025-11-14T09:50:39.346Z" }, + { url = "https://files.pythonhosted.org/packages/d0/22/9d02c880a88b83bb3ce7d6a38fb727373ab78d82e5f3d8d9fc5612219f90/murmurhash-1.0.15-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:847d712136cb462f0e4bd6229ee2d9eb996d8854eb8312dff3d20c8f5181fda5", size = 161990, upload-time = "2025-11-14T09:50:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/750232524e0dc262e8dcede6536dafc766faadd9a52f1d23746b02948ad8/murmurhash-1.0.15-cp313-cp313t-win_amd64.whl", hash = "sha256:2680851af6901dbe66cc4aa7ef8e263de47e6e1b425ae324caa571bdf18f8d58", size = 28812, upload-time = "2025-11-14T09:50:41.971Z" }, + { url = "https://files.pythonhosted.org/packages/ff/89/4ad9d215ef6ade89f27a72dc4e86b98ef1a43534cc3e6a6900a362a0bf0a/murmurhash-1.0.15-cp313-cp313t-win_arm64.whl", hash = "sha256:189a8de4d657b5da9efd66601b0636330b08262b3a55431f2379097c986995d0", size = 25398, upload-time = "2025-11-14T09:50:43.023Z" }, + { url = "https://files.pythonhosted.org/packages/1c/69/726df275edf07688146966e15eaaa23168100b933a2e1a29b37eb56c6db8/murmurhash-1.0.15-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7c4280136b738e85ff76b4bdc4341d0b867ee753e73fd8b6994288080c040d0b", size = 28029, upload-time = "2025-11-14T09:50:44.124Z" }, + { url = "https://files.pythonhosted.org/packages/59/8f/24ecf9061bc2b20933df8aba47c73e904274ea8811c8300cab92f6f82372/murmurhash-1.0.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d4d681f474830489e2ec1d912095cfff027fbaf2baa5414c7e9d25b89f0fab68", size = 27912, upload-time = "2025-11-14T09:50:45.266Z" }, + { url = "https://files.pythonhosted.org/packages/ba/26/fff3caba25aa3c0622114e03c69fb66c839b22335b04d7cce91a3a126d44/murmurhash-1.0.15-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d7e47c5746785db6a43b65fac47b9e63dd71dfbd89a8c92693425b9715e68c6e", size = 131847, upload-time = "2025-11-14T09:50:46.819Z" }, + { url = "https://files.pythonhosted.org/packages/df/e4/0f2b9fc533467a27afb4e906c33f32d5f637477de87dd94690e0c44335a6/murmurhash-1.0.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e8e674f02a99828c8a671ba99cd03299381b2f0744e6f25c29cadfc6151dc724", size = 132267, upload-time = "2025-11-14T09:50:48.298Z" }, + { url = "https://files.pythonhosted.org/packages/da/bf/9d1c107989728ec46e25773d503aa54070b32822a18cfa7f9d5f41bc17a5/murmurhash-1.0.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:26fd7c7855ac4850ad8737991d7b0e3e501df93ebaf0cf45aa5954303085fdba", size = 131894, upload-time = "2025-11-14T09:50:49.485Z" }, + { url = "https://files.pythonhosted.org/packages/0d/81/dcf27c71445c0e993b10e33169a098ca60ee702c5c58fcbde205fa6332a6/murmurhash-1.0.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cb8ebafae60d5f892acff533cc599a359954d8c016a829514cb3f6e9ee10f322", size = 132054, upload-time = "2025-11-14T09:50:50.747Z" }, + { url = "https://files.pythonhosted.org/packages/bc/32/e874a14b2d2246bd2d16f80f49fad393a3865d4ee7d66d2cae939a67a29a/murmurhash-1.0.15-cp314-cp314-win_amd64.whl", hash = "sha256:898a629bf111f1aeba4437e533b5b836c0a9d2dd12d6880a9c75f6ca13e30e22", size = 26579, upload-time = "2025-11-14T09:50:52.278Z" }, + { url = "https://files.pythonhosted.org/packages/af/8e/4fca051ed8ae4d23a15aaf0a82b18cb368e8cf84f1e3b474d5749ec46069/murmurhash-1.0.15-cp314-cp314-win_arm64.whl", hash = "sha256:88dc1dd53b7b37c0df1b8b6bce190c12763014492f0269ff7620dc6027f470f4", size = 24341, upload-time = "2025-11-14T09:50:53.295Z" }, + { url = "https://files.pythonhosted.org/packages/38/9c/c72c2a4edd86aac829337ab9f83cf04cdb15e5d503e4c9a3a243f30a261c/murmurhash-1.0.15-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:6cb4e962ec4f928b30c271b2d84e6707eff6d942552765b663743cfa618b294b", size = 30146, upload-time = "2025-11-14T09:50:54.705Z" }, + { url = "https://files.pythonhosted.org/packages/ac/d7/72b47ebc86436cd0aa1fd4c6e8779521ec389397ac11389990278d0f7a47/murmurhash-1.0.15-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5678a3ea4fbf0cbaaca2bed9b445f556f294d5f799c67185d05ffcb221a77faf", size = 30141, upload-time = "2025-11-14T09:50:55.829Z" }, + { url = "https://files.pythonhosted.org/packages/64/bb/6d2f09135079c34dc2d26e961c52742d558b320c61503f273eab6ba743d9/murmurhash-1.0.15-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ef19f38c6b858eef83caf710773db98c8f7eb2193b4c324650c74f3d8ba299e0", size = 163898, upload-time = "2025-11-14T09:50:56.946Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e2/9c1b462e33f9cb2d632056f07c90b502fc20bd7da50a15d0557343bd2fed/murmurhash-1.0.15-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22aa3ceaedd2e57078b491ed08852d512b84ff4ff9bb2ff3f9bf0eec7f214c9e", size = 168040, upload-time = "2025-11-14T09:50:58.234Z" }, + { url = "https://files.pythonhosted.org/packages/e8/73/8694db1408fcdfa73589f7df6c445437ea146986fa1e393ec60d26d6e30c/murmurhash-1.0.15-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bba0e0262c0d08682b028cb963ac477bd9839029486fa1333fc5c01fb6072749", size = 164239, upload-time = "2025-11-14T09:50:59.95Z" }, + { url = "https://files.pythonhosted.org/packages/2d/f9/8e360bdfc3c44e267e7e046f0e0b9922766da92da26959a6963f597e6bb5/murmurhash-1.0.15-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4fd8189ee293a09f30f4931408f40c28ccd42d9de4f66595f8814879339378bc", size = 161811, upload-time = "2025-11-14T09:51:01.289Z" }, + { url = "https://files.pythonhosted.org/packages/f9/31/97649680595b1096803d877ababb9a67c07f4378f177ec885eea28b9db6d/murmurhash-1.0.15-cp314-cp314t-win_amd64.whl", hash = "sha256:66395b1388f7daa5103db92debe06842ae3be4c0749ef6db68b444518666cdcc", size = 29817, upload-time = "2025-11-14T09:51:02.493Z" }, + { url = "https://files.pythonhosted.org/packages/76/66/4fce8755f25d77324401886c00017c556be7ca3039575b94037aff905385/murmurhash-1.0.15-cp314-cp314t-win_arm64.whl", hash = "sha256:c22e56c6a0b70598a66e456de5272f76088bc623688da84ef403148a6d41851d", size = 26219, upload-time = "2025-11-14T09:51:03.563Z" }, +] + [[package]] name = "mypy" version = "2.3.0" @@ -1991,82 +3758,778 @@ wheels = [ ] [[package]] -name = "ollama" -version = "0.6.1" +name = "networkx" +version = "3.4.2" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "httpx" }, - { name = "pydantic" }, +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'win32'", + "python_full_version < '3.11' and sys_platform != 'win32'", ] -sdist = { url = "https://files.pythonhosted.org/packages/9d/5a/652dac4b7affc2b37b95386f8ae78f22808af09d720689e3d7a86b6ed98e/ollama-0.6.1.tar.gz", hash = "sha256:478c67546836430034b415ed64fa890fd3d1ff91781a9d548b3325274e69d7c6", size = 51620, upload-time = "2025-11-13T23:02:17.416Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/47/4f/4a617ee93d8208d2bcf26b2d8b9402ceaed03e3853c754940e2290fed063/ollama-0.6.1-py3-none-any.whl", hash = "sha256:fc4c984b345735c5486faeee67d8a265214a31cbb828167782dc642ce0a2bf8c", size = 14354, upload-time = "2025-11-13T23:02:16.292Z" }, + { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263, upload-time = "2024-10-21T12:39:36.247Z" }, ] [[package]] -name = "opentelemetry-api" -version = "1.44.0" +name = "networkx" +version = "3.6.1" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, +resolution-markers = [ + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'win32'", ] -sdist = { url = "https://files.pythonhosted.org/packages/ee/8b/aa9e2d8b8dfa7c946f7dec5d1f8f6ba8eca062f43509a06bdb5ce93d26c0/opentelemetry_api-1.44.0.tar.gz", hash = "sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a", size = 72406, upload-time = "2026-07-16T15:25:32.678Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/6f/a04e900f465ff3221ccc395522503e2d10e79fa21f2723c8e177aae1e0d1/opentelemetry_api-1.44.0-py3-none-any.whl", hash = "sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef", size = 60018, upload-time = "2026-07-16T15:25:11.657Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, ] [[package]] -name = "opentelemetry-exporter-zipkin" -version = "1.11.1" +name = "nh3" +version = "0.3.7" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-exporter-zipkin-json" }, - { name = "opentelemetry-exporter-zipkin-proto-http" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1e/5e/e5b74775f3cca5b728a64aea00a29013f536c89d1f3f9c2514a662211c9c/opentelemetry-exporter-zipkin-1.11.1.tar.gz", hash = "sha256:1b5fc6993d04d9376185def150c857a8466cf83f16f58ce2fb09e5b166d995a5", size = 6326, upload-time = "2022-04-21T21:02:49.19Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/2f/022b27146d52d24b1b353b003359134788ecbcd6fcdf6283adbd57c0fbc8/nh3-0.3.7.tar.gz", hash = "sha256:71860d01c16f4d8c72e334e0674beb2b0899dbd0bf760de18932ef4390303848", size = 25662, upload-time = "2026-08-23T14:26:30.728Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/66/66/a2018bf84bd9d1fc6aba24a600d02e571f436dc7b8fe7b6a3c9e4d495e8a/opentelemetry_exporter_zipkin-1.11.1-py3-none-any.whl", hash = "sha256:756270c4eebe344ec32e15d5b04d65959e3bf7c8f338360b0d9d99981530fe96", size = 6998, upload-time = "2022-04-21T21:02:24.602Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/b594f0e86856b37e182fb663283da419eea6424972506e640e890885467f/nh3-0.3.7-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:91a4dab4e94d9fc54b9f67b1adfb23e81fab7ab43f33c3b8c97be9aa38f789ba", size = 1471147, upload-time = "2026-08-23T14:25:55.259Z" }, + { url = "https://files.pythonhosted.org/packages/1e/60/847a21339f095c4d4c655af31fa2d18b174585bcc210709facacc7ce205c/nh3-0.3.7-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eae64328e46a25785535afcb6885b6f182ecaf5ee8c88f8c075422db8aacc65b", size = 820463, upload-time = "2026-08-23T14:25:56.803Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7f/1a103e00aaf5e59f2dee4c2709aac609bb2d4bb74fddaf0dcfade11ed87b/nh3-0.3.7-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4968fe8d2db97c6f047659bf46a449fd8ec377f44ebf3e0a1b96c0d3a333ae32", size = 861456, upload-time = "2026-08-23T14:25:58.087Z" }, + { url = "https://files.pythonhosted.org/packages/d8/4a/e9c436089a0c80b928011ead0efd156aa7639a19b6064ef58dcedcab8369/nh3-0.3.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:be53a4825585f701955cb9baf49f478f56eb81e20294329fe4bc689dd5dd81fa", size = 1023930, upload-time = "2026-08-23T14:25:59.465Z" }, + { url = "https://files.pythonhosted.org/packages/04/5c/aa1468e3e281e78d2b3b7d762ccba59f681af355e971dbd255d5903f7b86/nh3-0.3.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:94fd6e59553fbb9ffd8ba71bbd5a54e3126ba01799a097ae30d5341d750bc6ac", size = 1102614, upload-time = "2026-08-23T14:26:00.869Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/57d186d9d3dd38905dc12dddb3484406cdf6aa0b1ce33639a2d277d4ee1c/nh3-0.3.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:18f4278ecd157d43cb35acd5aae9f35cfa79f546b4922bd86536adc0f6312102", size = 1059915, upload-time = "2026-08-23T14:26:02.388Z" }, + { url = "https://files.pythonhosted.org/packages/6b/53/097a5ad0b34b15d67a472ef849165a54209fa5fbd3e639801c6fe439ba28/nh3-0.3.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:808def0c8c07843e6e50dc84f532457bfa2cfd17417b219a5d9e7c773709331a", size = 1047402, upload-time = "2026-08-23T14:26:03.897Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/c57a2c70534418310889a65ccfac3525e62f0bc0a8613225903403755ce7/nh3-0.3.7-cp314-cp314t-win32.whl", hash = "sha256:874b7d67a067bd29a59223f6270fc30da4edd8e6d87fd219fc93bcbaa662c946", size = 619895, upload-time = "2026-08-23T14:26:05.105Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b7/efda1d0a611d940bdfde6893bde1ea6b7b7d48c31273aea48e35b822fd58/nh3-0.3.7-cp314-cp314t-win_amd64.whl", hash = "sha256:614dac4a4c36ad084e78447d16fe898dedd762e354a7ab9cda2984e82f67883d", size = 633456, upload-time = "2026-08-23T14:26:06.661Z" }, + { url = "https://files.pythonhosted.org/packages/1d/18/3ab564595cb88196f50d26e163ed0fd2acc731ab26ac615df91981885887/nh3-0.3.7-cp314-cp314t-win_arm64.whl", hash = "sha256:157ec1eb7a62f3d9a7badb8d82d89aa810e3e24e097eedfa481a25d0c8a99877", size = 611003, upload-time = "2026-08-23T14:26:07.813Z" }, + { url = "https://files.pythonhosted.org/packages/94/0d/c257754bf57f829f307aa226bbe136d3a1356b5a0d08324c7b6bd2a8aacd/nh3-0.3.7-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:6c3aa50eb26e9228238271db9f983cbc3b006dfbfeca2d4dc34c33ddc6ac5ea5", size = 1493959, upload-time = "2026-08-23T14:26:09.025Z" }, + { url = "https://files.pythonhosted.org/packages/07/42/a687e7091928806e514f89fa2666f25ec9bfe0a902fc4402b25e51ce408b/nh3-0.3.7-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f266d3f1b3647449923a8e406524632220dd5d8b647078dfe45b885d33d10479", size = 859615, upload-time = "2026-08-23T14:26:10.606Z" }, + { url = "https://files.pythonhosted.org/packages/85/05/b0e6bef633549a23347d5462aa288fcc42381e7918482062ca3cb456242a/nh3-0.3.7-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e8fd1ab205258b29254f72db377d99e2c96aa7653ef3b015ccab0420b094b506", size = 839872, upload-time = "2026-08-23T14:26:12.037Z" }, + { url = "https://files.pythonhosted.org/packages/17/40/2a0921d45b20828708bcb56887e47dcf8cae13818de5bf9a01308d348712/nh3-0.3.7-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:19f288c938ec6eef1f5d2c6cab47838e71fef8097e1c1233802be5a6230ba086", size = 1091325, upload-time = "2026-08-23T14:26:13.34Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d1/9d70e0e418a48280ec0ddc6c1b08b4b1136ebcc31a1625e57ff5c665fa51/nh3-0.3.7-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de2b2aab32ea303405debefdcfc58043d3e635fa3f67b9eb140d2b0e0c0d2563", size = 1042482, upload-time = "2026-08-23T14:26:14.667Z" }, + { url = "https://files.pythonhosted.org/packages/93/a7/02dd159d4e71f98607d8d4249cddb7561e77be1a8e4dec77d76e1b68fc99/nh3-0.3.7-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b7279d43323a25225df23576af6594a16693f61431170848b8b2ac21ad4f174", size = 946868, upload-time = "2026-08-23T14:26:16.094Z" }, + { url = "https://files.pythonhosted.org/packages/a6/ed/c5510c615dce55b6fcc364aa1838142f938beed64f5e4927490dfcaf4405/nh3-0.3.7-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70f5ac8626e899a4bab0ef74ca2f5bd602f49c7b739e6e5026b4afc6d63dac42", size = 832161, upload-time = "2026-08-23T14:26:17.272Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e3/3212c1a5b5745245d7f18885207bbddb34c56075f34dd682bd539aad55cc/nh3-0.3.7-cp38-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:5ffdfcb9a686ffb12765376bcfb6b5b55728516d3c0ee317d29982381ded3df8", size = 849791, upload-time = "2026-08-23T14:26:18.498Z" }, + { url = "https://files.pythonhosted.org/packages/20/64/9e36594efad6c290de4240d02cb2bd80c339a4ab1c4de66e599ffa6d9d81/nh3-0.3.7-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bc42bb1193c1e28a1e74c2cabaca178e118a7103e8832699fef8a2b3e2496493", size = 875473, upload-time = "2026-08-23T14:26:19.908Z" }, + { url = "https://files.pythonhosted.org/packages/00/0c/1a8985fd43fea5530c0ac890b6f0b423770ee72f111b70b7a77f2dec243a/nh3-0.3.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d56e76bd3cadb09b6b0cef364850811663734b348a25f5f587a2819c495367bd", size = 1036463, upload-time = "2026-08-23T14:26:21.536Z" }, + { url = "https://files.pythonhosted.org/packages/b2/5d/891e533b716cf00df76ad0ba6485dcfd14d59a6430a3cc99057c4c04004e/nh3-0.3.7-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:fd4a70efb45d5372174f718878eb7a35c12677626a63b2f103b23b833457dcac", size = 1116029, upload-time = "2026-08-23T14:26:22.907Z" }, + { url = "https://files.pythonhosted.org/packages/42/e5/ae8c0782fce74fb6fcf7234bb3d4017f37ce181b4f9d29369eab21c50a04/nh3-0.3.7-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:15f5fbf090f5c88d61c820e1fc1fceecb6520cca9fe85649c06b57ef9dc9ff62", size = 1076589, upload-time = "2026-08-23T14:26:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/26/a4/c3423351e8d864ad756e85e15f0c01433361f14d34e4ed156482c0518f2a/nh3-0.3.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6698a822132beedab80f131c08d8d0ac5a178ddeb488d02ca4b67716ecfac7af", size = 1058871, upload-time = "2026-08-23T14:26:25.674Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6a/478f153f1d7c0baaa3d1e8bb5fdcee3a6235f90fe44ea969a9d4e2b8c47a/nh3-0.3.7-cp38-abi3-win32.whl", hash = "sha256:6e4280115d44c3b278eef712a86748c1a723105cd79feec46952383117ab4e59", size = 630729, upload-time = "2026-08-23T14:26:26.932Z" }, + { url = "https://files.pythonhosted.org/packages/b4/b9/34433ccb1f0fe6968dabbb7d4bf5721c6221878ef07832748c06655a6a80/nh3-0.3.7-cp38-abi3-win_amd64.whl", hash = "sha256:618e3059caf41ccdf5dcccb3fa9df4cf6e4efe23d1382a8bbfca272a8a4f8bfc", size = 644462, upload-time = "2026-08-23T14:26:28.294Z" }, + { url = "https://files.pythonhosted.org/packages/f9/70/e140dffff6e808dc6343598df76e7e2407fd0f581de3524c75fba2e0cf24/nh3-0.3.7-cp38-abi3-win_arm64.whl", hash = "sha256:f04b7d333b27f13ca439da3cf1c75c2fba34f104969f6ce4ac8e7079699c2f4a", size = 621867, upload-time = "2026-08-23T14:26:29.547Z" }, ] [[package]] -name = "opentelemetry-exporter-zipkin-json" -version = "1.11.1" +name = "nltk" +version = "3.10.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-sdk" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b5/9f/4f1ce08f37eae9de39929ee9d247ca32dea0a4aa6425934cb542668d8f81/opentelemetry-exporter-zipkin-json-1.11.1.tar.gz", hash = "sha256:0190947d1ce6f1c90ad4fe799e39af77efd8e984ae5b74885da7b3290cdff875", size = 17819, upload-time = "2022-04-21T21:02:50.052Z" } + { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and sys_platform != 'win32'" }, + { name = "click", version = "8.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, + { name = "defusedxml", marker = "python_full_version < '3.11'" }, + { name = "joblib", marker = "python_full_version < '3.11'" }, + { name = "regex", marker = "python_full_version < '3.11'" }, + { name = "tqdm", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/e6/fe51d2bb1a3b446f59c5c8165999a9fee208bc346af90a7cbf7657bc0d75/nltk-3.10.3.tar.gz", hash = "sha256:bb9327a461c3811c2fa4900e03840401f2126adfb30c0072827c433bd2444ea4", size = 5137152, upload-time = "2026-08-12T23:46:37.258Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/74/d4369676d088434fede00d3592ac227b7502052ac031b72817d4a27e2171/opentelemetry_exporter_zipkin_json-1.11.1-py3-none-any.whl", hash = "sha256:405b4ef12207d55a768348471add1f59a5fd74dd0d0ad4a3281a0694a85fb76c", size = 16290, upload-time = "2022-04-21T21:02:26.117Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6d/ebd2af4640b12168fdf0cb74b6118df2f32a2f62ec7e0c06fbfd80706639/nltk-3.10.3-py3-none-any.whl", hash = "sha256:ff9598a8e20518ee0d557745890cc4435b9578489e2dcbc69c4f81fa060caf7c", size = 1798643, upload-time = "2026-08-12T23:44:13.478Z" }, ] [[package]] -name = "opentelemetry-exporter-zipkin-proto-http" -version = "1.11.1" +name = "numba" +version = "0.67.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-exporter-zipkin-json" }, - { name = "opentelemetry-sdk" }, - { name = "protobuf" }, - { name = "requests" }, + { name = "llvmlite" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/aa/cd/2bd290ba645528c456e1cb112fd4dd87f41f2f2e84857dcfbb6354b90bc8/opentelemetry-exporter-zipkin-proto-http-1.11.1.tar.gz", hash = "sha256:595fc729158e208c8e756553fb5ea52d85f6132e0ce161d0695be3f8c807a4bf", size = 19301, upload-time = "2022-04-21T21:02:51.031Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7a/90/2544f4e3a61e501d6c9a5418fd4b905323222693d54a02cab0106a0af865/numba-0.67.0.tar.gz", hash = "sha256:cd75aa535b33fa05d9d930b1ae8af9f97a2881e96d72dfb38ec9b78284d9f851", size = 2836515, upload-time = "2026-08-11T23:04:00.174Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/23/b84eb97c097d568571fa17346827384ca75a6912f05d70125e1170e479be/opentelemetry_exporter_zipkin_proto_http-1.11.1-py3-none-any.whl", hash = "sha256:08b40dae4ebdf06ad28c6a3d7b70bf9fac4328de56a53fbdd5ac461582bc6e21", size = 14146, upload-time = "2022-04-21T21:02:27.418Z" }, + { url = "https://files.pythonhosted.org/packages/af/2e/6e72b3edbb7c7d6b44b2ca9e1b62e91997415d181541ef47fc6957c59bf2/numba-0.67.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:8c0e88acd4341ddf40779db3c0228b9188aca7fcab5f5f3ce9949a1fc71e9a02", size = 2745135, upload-time = "2026-08-11T23:03:08.321Z" }, + { url = "https://files.pythonhosted.org/packages/e5/17/5358f24235ef1a5a80b7e28f3e1baa886c0bcf07dc68557009284e6ba698/numba-0.67.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6c8e9ba3f9602471e8c6f563ffcce8db8046741f0bafb782a052e41dc6b6861", size = 3821881, upload-time = "2026-08-11T23:03:11.172Z" }, + { url = "https://files.pythonhosted.org/packages/0e/18/2f00694248e32c53812baf3d36a7c656dbdd667c6993087b3da068f74b02/numba-0.67.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:694c81c6560b2b47e5fc1dc39c29175b907adf862d9af0af801453400a022a61", size = 3528397, upload-time = "2026-08-11T23:03:13.107Z" }, + { url = "https://files.pythonhosted.org/packages/7f/39/4175b074929938011bd4b564beb4e0fcffd46252e01f60602b57ffb02b06/numba-0.67.0-cp310-cp310-win_amd64.whl", hash = "sha256:ed333e0af4386294e7f03e550e01411856b6935e717d859225e0a7338c6b6795", size = 2815861, upload-time = "2026-08-11T23:03:15.072Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ed/55ba4e54ee878396de6b18e6533cc4a92fa519e8c82d55cf40f98c0a6831/numba-0.67.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:3fa3d1b27f96f2c0d54513d953d7197886aa1eaa7d2439a0eedc44d993fb181a", size = 2744821, upload-time = "2026-08-11T23:03:17.321Z" }, + { url = "https://files.pythonhosted.org/packages/be/78/3f3c45dbaec3cf02bbb1825731beca50f591227e95143d6bd7a64897641c/numba-0.67.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8c80c847301dc33dc8f84a97a952004023d9a05578ae4512b087176264cc1960", size = 3827182, upload-time = "2026-08-11T23:03:19.684Z" }, + { url = "https://files.pythonhosted.org/packages/a5/24/4e70cb86534283d859c3aea2302da523e41539b98dd6c3c4d0a42af95cda/numba-0.67.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e7a7b0121466f1e9a8a074b0545fe90e16389623abf979b5d7c299dca1294d7e", size = 3532817, upload-time = "2026-08-11T23:03:22.06Z" }, + { url = "https://files.pythonhosted.org/packages/26/4d/23dab7f4233be0fc34f54a169ed85238467cd24d8adf2498e5c12ea19dc7/numba-0.67.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfba1ac34f0363fb1a250a10e97240780d11e05227892f7286b26fbfd0ad58ce", size = 2815700, upload-time = "2026-08-11T23:03:23.812Z" }, + { url = "https://files.pythonhosted.org/packages/0d/58/915cddba90010348ed0444451132fdde9b000bcbaff1582029b5bf115d11/numba-0.67.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:6004d8d5f28d4028687fb2d972d629295b13685943bd2ed5cd8810c3b848e219", size = 2745050, upload-time = "2026-08-11T23:03:25.607Z" }, + { url = "https://files.pythonhosted.org/packages/bb/38/926757caaac18a66f057d7544a63620bf360a07d281c9f7ecadd2aa83963/numba-0.67.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f63d43db06b4756424d6d2484737c902e0ae944a0eec3e8b0b4de2c695b15caa", size = 3884596, upload-time = "2026-08-11T23:03:27.688Z" }, + { url = "https://files.pythonhosted.org/packages/4d/6d/58291dc58da39d98b32db7f044729f6d8d4920cd9622fbab3179b54ff4c4/numba-0.67.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76d3335aaeffb9dc88309420890e73497a00be08a7530441bc2b58ffe025bfa5", size = 3585290, upload-time = "2026-08-11T23:03:29.684Z" }, + { url = "https://files.pythonhosted.org/packages/6e/63/ab21828b4056afed71f9ecb40f4de26c2c19de731cc001961aca74b79464/numba-0.67.0-cp312-cp312-win_amd64.whl", hash = "sha256:50e2b72406c18cda5dd7431b0082cb85ea94e06c64c33607248fc8bef92cfb81", size = 2815645, upload-time = "2026-08-11T23:03:31.732Z" }, + { url = "https://files.pythonhosted.org/packages/49/dd/bd9fe772f6c84597b76cac229b3f2890f01a2c64fd70e48ceaae10dd65cb/numba-0.67.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:77e1c7173fee57a0d84e006c7e70346689d6cb3e7db503489bae58646b4eff7b", size = 2744872, upload-time = "2026-08-11T23:03:33.649Z" }, + { url = "https://files.pythonhosted.org/packages/a1/1c/c05609739cc41116d36e30cb2b41fb00f126bb52e1b0bac907051ad8a35d/numba-0.67.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9c4953387c77864b596d8296e2cfbdef82b0eea4166ab4864b05d226c51143e0", size = 3892004, upload-time = "2026-08-11T23:03:35.797Z" }, + { url = "https://files.pythonhosted.org/packages/4a/77/a5276ad4178250403e0e2251f3e1f8ac18feac779b0474a8bcb08558490d/numba-0.67.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88f6e0f5cb6c545e158b6ef0496c01b6d6958a7ccc6634a1576a94bbbab29ff2", size = 3591878, upload-time = "2026-08-11T23:03:37.845Z" }, + { url = "https://files.pythonhosted.org/packages/0f/80/d48f0ba7442516ceb5a1585f0c81d3aa531bc96bfcabcd9f8f925768c426/numba-0.67.0-cp313-cp313-win_amd64.whl", hash = "sha256:b68ad5125fe245339cc8dcc036081fc1ea482c5063387b9612a76ccd83dc91cd", size = 2815504, upload-time = "2026-08-11T23:03:39.736Z" }, + { url = "https://files.pythonhosted.org/packages/d7/16/345b1e4774a08247aafcfdb93d4e8d24a3646366cbe72de33053fc0de1b5/numba-0.67.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:f99f880ff25f418a67f9a1d00d0ddfbc63430f627b523e515085a592a7567f4b", size = 2745088, upload-time = "2026-08-11T23:03:41.864Z" }, + { url = "https://files.pythonhosted.org/packages/1b/36/e614ba2bc0f005ed0f37a6413f08fe705210297ddb9a37a475a8b9fdab61/numba-0.67.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5269245a675abdd3e2c35ec6bb2f250355effa9032514d8f2354f0d2d10854bd", size = 3861040, upload-time = "2026-08-11T23:03:43.842Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/30c42a1dbc4176cf355e8e8be61803732c55597b1332925fe233912a43d9/numba-0.67.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f074a8e23db78490f11a3930c940be758316c10ac5985be83d2f298dc080acf7", size = 3561811, upload-time = "2026-08-11T23:03:46.037Z" }, + { url = "https://files.pythonhosted.org/packages/18/6d/21bd16f770476e394c5e5f504935817967442a71251d6b86c244a2767980/numba-0.67.0-cp314-cp314-win_amd64.whl", hash = "sha256:4d576e62bf2c9370f61312b51573c4bb1f3fe96798bbab56730847a368a316c4", size = 2817421, upload-time = "2026-08-11T23:03:47.922Z" }, + { url = "https://files.pythonhosted.org/packages/95/06/bb41b0e59b9ff52c94a2f01db24f6437df058caebb377b5f372fc343a6a2/numba-0.67.0-cp314-cp314-win_arm64.whl", hash = "sha256:7930748ce8355d2a5a28602abab056a61fdc676d17377f27d17993905428171f", size = 2788885, upload-time = "2026-08-11T23:03:49.967Z" }, + { url = "https://files.pythonhosted.org/packages/10/7c/aa07151fbd0f4283f78de437cc196f9084789be89a2b4de3fdc2f6a4b414/numba-0.67.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:4a2ed006635bbd0fe45681ed49f3b4f4bad1abf0c233bcc5842c9e3a34cabd61", size = 2748150, upload-time = "2026-08-11T23:03:51.755Z" }, + { url = "https://files.pythonhosted.org/packages/74/62/b8174ca95a4cc1a7ba1520767734e016991545590b8fbde521b681701a9f/numba-0.67.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aa5f002f665bec321b950dacaa26ee009e1d720f6ac9d9856eed5efe1caa03a6", size = 3896986, upload-time = "2026-08-11T23:03:53.752Z" }, + { url = "https://files.pythonhosted.org/packages/8c/f9/3a7b6dbf81e01a48958b45ad2239edbc64707522ab17f11f9f18c44bf6d1/numba-0.67.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83ab968b0e0fa744eba03351282dd8000796e6ec8e4518f47bd3ed86c0a20c7b", size = 3614644, upload-time = "2026-08-11T23:03:55.794Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5b/248f5681c121ca853a9f4e39d342a3e01b8a0261b0275853eb3d0d56aa20/numba-0.67.0-cp314-cp314t-win_amd64.whl", hash = "sha256:00c964a5b94d3ae82d83ac162cd610755875b98dadb779fdde06e6bfcdbca47e", size = 2822870, upload-time = "2026-08-11T23:03:58.097Z" }, ] [[package]] -name = "opentelemetry-instrumentation" +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'win32'", + "python_full_version < '3.11' and sys_platform != 'win32'", +] +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/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, + { 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.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/49/ec46835a70be8fa6446c495126ac84fdb28cb2558e1620ffb87a10c8b64c/numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4", size = 16969194, upload-time = "2026-05-18T23:33:13.503Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0d/f5957185c0ee2f3e12f78715aa9e3b353fd83633316c8532b38faa37e3f6/numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d", size = 14964111, upload-time = "2026-05-18T23:33:17.795Z" }, + { url = "https://files.pythonhosted.org/packages/ad/40/40a40ee0ddf7ceb782c49af278894b686e586d65d8c1889c8b5da01a3d7d/numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8", size = 5469159, upload-time = "2026-05-18T23:33:20.654Z" }, + { url = "https://files.pythonhosted.org/packages/63/13/f9a8046535cb21deae82f8d03de9617e08882d274fad2539630761888228/numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538", size = 6798936, upload-time = "2026-05-18T23:33:22.987Z" }, + { url = "https://files.pythonhosted.org/packages/33/a8/6fa8c1a345a8c85dbb21932c447bee07c30a2c2a3f31e369c0a84b300147/numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47", size = 15966692, upload-time = "2026-05-18T23:33:26.62Z" }, + { url = "https://files.pythonhosted.org/packages/02/03/74fe2a4cb3817d94d86402f2506554130a2f01414e299b5a843e5a8a957f/numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93", size = 16918164, upload-time = "2026-05-18T23:33:29.955Z" }, + { url = "https://files.pythonhosted.org/packages/c5/80/3615be3313f7e7696609bc194b9f0101da809df79e859bdb84e0cd043f46/numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8", size = 17322877, upload-time = "2026-05-18T23:33:34.724Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ac/a691e0fe2675e370d0e08ff905adc49a1c8830e8cae03efe4477e92cd55d/numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6", size = 18651487, upload-time = "2026-05-18T23:33:38.217Z" }, + { url = "https://files.pythonhosted.org/packages/15/a7/9bc1cd626d7bf6869bfedf27b91b6ab5dd607758bf8e959d6fa80c6a59cb/numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8", size = 6233945, upload-time = "2026-05-18T23:33:41.331Z" }, + { url = "https://files.pythonhosted.org/packages/c5/31/7fc6239c12bce7e931463251cca4426c465e1876ba3cc785402ef4dd8f4e/numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147", size = 12608406, upload-time = "2026-05-18T23:33:44.131Z" }, + { url = "https://files.pythonhosted.org/packages/27/83/140f85a466595a16382996a1bf06b2b54bcd597488921b0c9daaeeda72af/numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577", size = 10479528, upload-time = "2026-05-18T23:33:50.725Z" }, + { url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" }, + { url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" }, + { url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" }, + { url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" }, + { url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" }, + { url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" }, + { url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" }, + { url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" }, + { url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" }, + { url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" }, + { url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" }, + { url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" }, + { url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" }, + { url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" }, + { url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" }, + { url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" }, + { url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" }, + { url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" }, + { url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" }, + { url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" }, + { url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" }, + { url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458, upload-time = "2026-05-18T23:35:38.353Z" }, + { url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559, upload-time = "2026-05-18T23:35:42.14Z" }, + { url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716, upload-time = "2026-05-18T23:35:45.377Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947, upload-time = "2026-05-18T23:35:47.926Z" }, + { url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197, upload-time = "2026-05-18T23:35:50.863Z" }, + { url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245, upload-time = "2026-05-18T23:35:54.752Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587, upload-time = "2026-05-18T23:35:58.355Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226, upload-time = "2026-05-18T23:36:02.845Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196, upload-time = "2026-05-18T23:36:05.92Z" }, + { url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334, upload-time = "2026-05-18T23:36:09.107Z" }, + { url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678, upload-time = "2026-05-18T23:36:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672, upload-time = "2026-05-18T23:36:16.473Z" }, + { url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731, upload-time = "2026-05-18T23:36:19.767Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805, upload-time = "2026-05-18T23:36:22.266Z" }, + { url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496, upload-time = "2026-05-18T23:36:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616, upload-time = "2026-05-18T23:36:29.652Z" }, + { url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145, upload-time = "2026-05-18T23:36:33.449Z" }, + { url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813, upload-time = "2026-05-18T23:36:37.369Z" }, + { url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982, upload-time = "2026-05-18T23:36:40.817Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908, upload-time = "2026-05-18T23:36:43.996Z" }, + { url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" }, + { url = "https://files.pythonhosted.org/packages/de/12/b422cc84439adc0d00de605bf4a308890ae5c26f2c71fbd73e5d08fbb0dd/numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662", size = 16847511, upload-time = "2026-05-18T23:36:50.673Z" }, + { url = "https://files.pythonhosted.org/packages/44/53/f481bef68011740f8849418d82db07230e825013f31f4eef5ba5b805316a/numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7", size = 14889064, upload-time = "2026-05-18T23:36:53.879Z" }, + { url = "https://files.pythonhosted.org/packages/7f/57/42ed575c10ced8af951d426bc4e1f8aff16fd851db33f067036215a7f860/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f", size = 5394157, upload-time = "2026-05-18T23:36:57.194Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ef/f66cc724fcc36c1e364c67f51ae9146090b8b584f27d58b97fdae3edd737/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c", size = 6708728, upload-time = "2026-05-18T23:36:59.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/9c/c531f2293b91265d8b48e9b329f54fdd7ffae73cb4134ea10cca4237e9cc/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0", size = 15798374, upload-time = "2026-05-18T23:37:02.674Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b0/413077f6b1153ed3cba361401c6783bbad6114804a000cc22eb71c13e190/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02", size = 16747286, upload-time = "2026-05-18T23:37:06.327Z" }, + { url = "https://files.pythonhosted.org/packages/15/ce/e5ec180bc41812edcd8daeb8639d205622c0e8c02259d8ab25a0201b3c2a/numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73", size = 12504263, upload-time = "2026-05-18T23:37:09.715Z" }, +] + +[[package]] +name = "numpy" +version = "2.5.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/13/01/11703282db468b85f6f7b8c7f22d058de5970d5c7e60a3a8aaa313c3de36/numpy-2.5.3.tar.gz", hash = "sha256:df2d5874ff183595a4ba404edd04f6bd9b5505c1d7708573f6a6c17489a67563", size = 20791231, upload-time = "2026-09-06T16:27:47.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/50/8fdbb16af64895706a45f06a4068e29db732ec180f3c1375f14123359138/numpy-2.5.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cb189f09db39283b26bfd061ec16189e14f71c6755207f72a0f7540867afe5b9", size = 16994982, upload-time = "2026-09-06T16:24:29.244Z" }, + { url = "https://files.pythonhosted.org/packages/60/39/789131c1188c078dcb3a1692e72e1e050c68b88ffe72c9ccaac9bcd7a9cd/numpy-2.5.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f59a878c33d6b88122d80d239bb3b845d58708750b0cb06a09aebb9b18ec696c", size = 12009327, upload-time = "2026-09-06T16:24:32.491Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/a312e95696e5f601914dd8b6dd844692ba61670807417e24b68e337b5c70/numpy-2.5.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a72f874bc9e10e4b8f80426fb49716d5141f64442a0c8418065093ec8017fbb0", size = 5445405, upload-time = "2026-09-06T16:24:35.071Z" }, + { url = "https://files.pythonhosted.org/packages/30/d0/5623a1707ed4fe16e3909fe3cf5ee3da004ae677ad23d83bbf3adf1a6faf/numpy-2.5.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:fc36dc566135b5eceec4cf89758fcb719266a019ef07dae1754ae7c9f617ef3e", size = 6783213, upload-time = "2026-09-06T16:24:37.253Z" }, + { url = "https://files.pythonhosted.org/packages/f1/32/84146fc020ad3c25f805f70ab60da46fe3c540a21369754a7e4369754b6f/numpy-2.5.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76c2c1e6bfa5c84adc6434dfbf013aa92096a7985221762c8f11fedfd20fff58", size = 15687872, upload-time = "2026-09-06T16:24:39.751Z" }, + { url = "https://files.pythonhosted.org/packages/65/af/aa78d1a88805456e212b65461354cd943197fb9acecc4c90fd12295123a3/numpy-2.5.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7e18c623bb5c95acb3b3328861272816ba199fb531921c5d6d0b675f1fde9e3", size = 16717410, upload-time = "2026-09-06T16:24:42.745Z" }, + { url = "https://files.pythonhosted.org/packages/3b/24/faa79d865e69a97ba17473b23a1b74094b2259c03e820c70297293b9ea49/numpy-2.5.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4f8929ee6c96bfbd7b4ed2032e0c03af86fe1826740ab61ddabf9072d06e57ff", size = 17040975, upload-time = "2026-09-06T16:24:45.961Z" }, + { url = "https://files.pythonhosted.org/packages/62/4a/8877e629445a7176297dffcaf9c485faa96a95d81728a62521ad55bd4c0f/numpy-2.5.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b5d93cf48f687479941d12b69c873ad2cc76bbd487f0091c2200636497f34034", size = 18476479, upload-time = "2026-09-06T16:24:49.35Z" }, + { url = "https://files.pythonhosted.org/packages/c8/db/35e1c2d38b04cbd5b731f9d71495e055e813197669d22b612f11748d2ff9/numpy-2.5.3-cp312-cp312-win32.whl", hash = "sha256:bf63afbe037eb5d2fe87fbcc7778e61da53ebaf21d938a4515aa73b62532a5d4", size = 6133378, upload-time = "2026-09-06T16:24:51.915Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/accf6d4f0c80c5d9ba9735d6b1550e444180599f34dec69ca01360f717ad/numpy-2.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:0a59a421a32580a009e8a1751345bf829631b990dc1794b80514ab722b435def", size = 12567828, upload-time = "2026-09-06T16:24:54.255Z" }, + { url = "https://files.pythonhosted.org/packages/22/43/1764aff32e4652526ae2f71fa8b3efd8d25c8a3d6926914454e47138ed1e/numpy-2.5.3-cp312-cp312-win_arm64.whl", hash = "sha256:ccb32e0525d29e8b0572eb84c9a57af0e7a4e615726927506f55063c62414034", size = 10485432, upload-time = "2026-09-06T16:24:57.278Z" }, + { url = "https://files.pythonhosted.org/packages/79/e5/8fb89cd46d14e35699d13bf943a5f5f441ecee8667120a1f6105ab89e349/numpy-2.5.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:66a78fe4556c60aceda5916f9eacd638b18e9e681016ec302dcb4682d6d4d034", size = 16991061, upload-time = "2026-09-06T16:25:00.411Z" }, + { url = "https://files.pythonhosted.org/packages/2f/06/9dc9e48b5e5e941c8b10350c5ff2d721da42a20517d911d15544246775ff/numpy-2.5.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92f30e89b8ee0ecf363033576c422b2f58fed6a80bed0aa48dff6d14c654663e", size = 12003676, upload-time = "2026-09-06T16:25:03.475Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2a/98282aa5b8f58b1157d440bb6282eed47e3632a5de53a714fbab17e659fe/numpy-2.5.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f9a2353b37a1a9e78fd82b27ad7e2a32a2d036604d18f02b05e3136c62ca3b09", size = 5439695, upload-time = "2026-09-06T16:25:05.978Z" }, + { url = "https://files.pythonhosted.org/packages/a1/f9/b6533d777be9d6ffd29dc1be0867e563e6e8cc9a220ff1b716adc317f060/numpy-2.5.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:ccbc4665079665c3cf3bab4db9f6b095370cd6437d66be549b6c2a1fd19e1958", size = 6779395, upload-time = "2026-09-06T16:25:08.599Z" }, + { url = "https://files.pythonhosted.org/packages/73/85/735720d04ec197c5dcfacdfc9922667c7f1f5f496a279b7ba4d7c74c4cc7/numpy-2.5.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c76d5dde9f445058f83d0c02af00557a4db91de9a9a57c0df87d1535001d654b", size = 15681750, upload-time = "2026-09-06T16:25:11.173Z" }, + { url = "https://files.pythonhosted.org/packages/3a/1b/3b16a9bc514a440a7a0883684111dcb1ef1aee960af2ca95da8fc775f124/numpy-2.5.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5fa86b80fd24bcd1aff83ad23be44ea323de3f787be8f8b15d4a65621e25321", size = 16708577, upload-time = "2026-09-06T16:25:14.171Z" }, + { url = "https://files.pythonhosted.org/packages/69/c4/386f397831b07328b639c96c5b62719346cf4baf07c68d927239752b1534/numpy-2.5.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd4cb9ad3c7889b9b3fe0a9a9fb5d2ed26f9879bff2608d9f01aed147a20d231", size = 17042047, upload-time = "2026-09-06T16:25:17.582Z" }, + { url = "https://files.pythonhosted.org/packages/5f/3e/a700ecbf36e85ae8328fd3b0e12eeddc22ed6358a64cb2bd913e0d195d65/numpy-2.5.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1302b90c0e52281681b2975adfe8a860cb7b12216a27b4b0b4207c44bf7bccf0", size = 18465724, upload-time = "2026-09-06T16:25:20.949Z" }, + { url = "https://files.pythonhosted.org/packages/41/ee/38e785e88a4045f6ad1d1f2808dcdfafdca48c760260c0587bf171e29fc9/numpy-2.5.3-cp313-cp313-win32.whl", hash = "sha256:1c80eabb4035ecf4ca9cd49cde8a9fdd69a729e63e6474887d1523ade7aa277f", size = 6129003, upload-time = "2026-09-06T16:25:23.664Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ec/100f2b1794ede74a9b3d7ec6b9736927f56713414c1dfe19ab6c383494bf/numpy-2.5.3-cp313-cp313-win_amd64.whl", hash = "sha256:71cad2b2a7451ab79d8f5e71b453485b6775963d5cf794179144a7463fe6e8ec", size = 12560965, upload-time = "2026-09-06T16:25:26.602Z" }, + { url = "https://files.pythonhosted.org/packages/80/b1/7dc825ca94c12acebbce4c37caa5e198695eb31424bc579679f32b1bb49d/numpy-2.5.3-cp313-cp313-win_arm64.whl", hash = "sha256:8e4dd766076855b5ff7ea52fa5f07ce26286726e0f8bff446b7739d02e6ea204", size = 10482343, upload-time = "2026-09-06T16:25:29.772Z" }, + { url = "https://files.pythonhosted.org/packages/70/78/cf416f15dc29375a229d9dfebf8db6e313f291580b39fa1a568b6052bb07/numpy-2.5.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:350ba9783ce969cf9f7ce6e6a9a58e1a6e2a19ca025b7ee448c4db727706212a", size = 16998686, upload-time = "2026-09-06T16:25:33.171Z" }, + { url = "https://files.pythonhosted.org/packages/9e/59/abcc2d8def4fd60eec7d87f92d27c13448ffd9ab14339bcc63a0d7a2fdea/numpy-2.5.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:012e66aca395d795496446e52aeeb5866312a5d4d3f27da270e5a0b43f70dc5c", size = 12013862, upload-time = "2026-09-06T16:25:36.748Z" }, + { url = "https://files.pythonhosted.org/packages/94/75/4640d2d6e4b64a049e48425a82728a41ef4adb61332d2cba68055774878b/numpy-2.5.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:adc1ada2662f8a5f960b8a10d9986897e7499ef07e06d4cfe7197f8cce923c07", size = 5449793, upload-time = "2026-09-06T16:25:39.476Z" }, + { url = "https://files.pythonhosted.org/packages/96/cd/625b57ae33d4ca560f32cc0b47b4a5922146d9beb998ddf773900d440a73/numpy-2.5.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:54a115e5a73b8fc44f0cebef486365a1894b5c9760685d4558b72b7c3eb846e0", size = 6785176, upload-time = "2026-09-06T16:25:42.069Z" }, + { url = "https://files.pythonhosted.org/packages/9c/72/12918652e7912ef9751e8694c88820fcd1908e0618cb23f5f3caa6004b7b/numpy-2.5.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be5a8381859b6da607c84f4f7d6847725f1cf1853ef8a2c9e115b7d58bef47dc", size = 15703377, upload-time = "2026-09-06T16:25:45.135Z" }, + { url = "https://files.pythonhosted.org/packages/45/8f/9beacf79ca7c650688ad0baa80931adb988fe6e6e5d5903c23cc3dbd70eb/numpy-2.5.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b0521d0f4aebb6e06189451025fa17a913287b13c03d5fe05c017333b654ea5b", size = 16711928, upload-time = "2026-09-06T16:25:48.461Z" }, + { url = "https://files.pythonhosted.org/packages/09/8d/41d0a56e1ac4c87495c897a211b1368691b7237aadabec8b3b8f3a74d48f/numpy-2.5.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9deb49575e5b0b94ed72c8a64ec4d033381adc27e9060ae842971f697ba96104", size = 17059507, upload-time = "2026-09-06T16:25:51.873Z" }, + { url = "https://files.pythonhosted.org/packages/08/1e/0dfbc5cc251d54e2af790f254d24ec38637fa97ec7d5d11de7ffed787098/numpy-2.5.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b00eefbcf0f292945c4b4dec2ae845389ef5bcdcd596e6e4328051db5b5ba694", size = 18471002, upload-time = "2026-09-06T16:25:55.233Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2c/dfa40f6991f8185c8c30ffd023dfcbb11888e823cfab9557b920f3bb7bed/numpy-2.5.3-cp314-cp314-win32.whl", hash = "sha256:c2381f82999704f818e2c987a865050e285ec3621262c66d40f5a96c8f899f8e", size = 6180485, upload-time = "2026-09-06T16:25:58.157Z" }, + { url = "https://files.pythonhosted.org/packages/a4/73/d2c08231e4fde7e415501fd02c715d96e98599b2d8384445933944152984/numpy-2.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:2c25dfa72943e4336ddb6b0ee4277b47a0c85bede0807530ec68103bf58e2c10", size = 12698179, upload-time = "2026-09-06T16:26:00.789Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e9/dcdcc9b95cf5f49815055573aee1b11cfbf5299f38a180e437ded050810f/numpy-2.5.3-cp314-cp314-win_arm64.whl", hash = "sha256:15aa985ac73a8db02db7663381aa109510449d3819d37206caed27b33a65a8a6", size = 10769383, upload-time = "2026-09-06T16:26:04.011Z" }, + { url = "https://files.pythonhosted.org/packages/49/c4/af8bc08a7ef4e1529a7c0cf24969accce316b783999802089a581ec99272/numpy-2.5.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ac7bb1c52d445bd4f8f7f97fefe6abc3a084dc4d63df50d79b17fa2b78e89297", size = 12132668, upload-time = "2026-09-06T16:26:07.138Z" }, + { url = "https://files.pythonhosted.org/packages/c5/ae/0f15eb56d4ec5e13c1f7ff04ff407f997d1acbadb45d3e1f2e2645a8f43c/numpy-2.5.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e6ab667ba76450084eb64013762c438ea76d9d29cc676dcd6c2e9892ba37f841", size = 5568580, upload-time = "2026-09-06T16:26:09.828Z" }, + { url = "https://files.pythonhosted.org/packages/23/fb/c72a8f25d4b6e96c354e7ab45ace3b27dc11e5d6a13b6c7d0cd6b08bf112/numpy-2.5.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:f7fabeb6cea87d65f3b926de33d03fb016cfdc29314c90974383b5582ae72891", size = 6882634, upload-time = "2026-09-06T16:26:12.524Z" }, + { url = "https://files.pythonhosted.org/packages/07/a9/968c90ed2ab15060c338e8137f1215b5a60756ae07328e0a60d1c6734df4/numpy-2.5.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fb6f8fb9ff0b3a69f52c66ce397b0246583e9f28616231b0e32ca49259a5fa6", size = 15748923, upload-time = "2026-09-06T16:26:15.092Z" }, + { url = "https://files.pythonhosted.org/packages/59/08/9df04103947b95e3b6b1f2ed1a70521f325647a31b82da6a2aae3a485508/numpy-2.5.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93e1f5447e2b1e479d7bd74701e84746b86450cff1fc368b132d195e2b8f8211", size = 16746748, upload-time = "2026-09-06T16:26:18.43Z" }, + { url = "https://files.pythonhosted.org/packages/41/a0/14c8d5fe5b53a334aabb653deb391c0fef49558f491880ea300ed6785224/numpy-2.5.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c00abe94c1a69d75d827dcf1c025b25c8a45d230b3bcd77a9020883a1b047653", size = 17111561, upload-time = "2026-09-06T16:26:22.113Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a6/d7e96e42f01522e154c32489640f16dfc4f6181d165d05fc3bec8c2c4999/numpy-2.5.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:536f963710a4e63934d80ac0dc4f478804a83e9a84b6828018f25d09953ada33", size = 18513945, upload-time = "2026-09-06T16:26:25.401Z" }, + { url = "https://files.pythonhosted.org/packages/25/39/3453afb7119d0449ef11c886874120ff180e2c337760e0e2d88f70f1a945/numpy-2.5.3-cp314-cp314t-win32.whl", hash = "sha256:4c8a6d2ebce6305fd82fbefca827775437147052a976ee7c94b36a0c1b52ac6c", size = 6335421, upload-time = "2026-09-06T16:26:28.175Z" }, + { url = "https://files.pythonhosted.org/packages/99/01/22815d2b19a1a746b1d45205cffebb3fe511a18acb75fba6c88491fc9894/numpy-2.5.3-cp314-cp314t-win_amd64.whl", hash = "sha256:9a37475425b431b4d060f23b4f52cd2f3aef6bc7c654bd760adf0040eec9d435", size = 12896420, upload-time = "2026-09-06T16:26:31.265Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ee/a7cbba67eeaff038dc29ca8b98a88396c8b0cc9c89d4924f4a27a5c9150b/numpy-2.5.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2d8240cb4c16fd831074aa2b2cf9fc54664d826341d61c372245b96a74a49a9a", size = 10857177, upload-time = "2026-09-06T16:26:34.167Z" }, + { url = "https://files.pythonhosted.org/packages/45/56/78194492883ff5eec90423fe56a3a44b154da047d88a6307f629713c584f/numpy-2.5.3-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:a6391fafaba97500887132cd582abc6e19452b1ac775a47caa7b24490e152058", size = 16996531, upload-time = "2026-09-06T16:26:37.287Z" }, + { url = "https://files.pythonhosted.org/packages/11/39/dd55c0af90bbab564b09ae3b0aa60ec5c02b900fa4f1ba23440525c8b32d/numpy-2.5.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:09d5a423c71ad5feb5625844ad58050e35df43871004b52ac9c0ad44a56775be", size = 12012569, upload-time = "2026-09-06T16:26:40.707Z" }, + { url = "https://files.pythonhosted.org/packages/b6/51/04f67d32e4862b281b1cb84ceeaed3421189a84fb6fb51a391cd6d5009f7/numpy-2.5.3-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:f9579f383d1bf9df80081e72760e84960a7fd4f88cf0c9e535a8597c9bb646f5", size = 5448498, upload-time = "2026-09-06T16:26:43.435Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c9/25b4dc0dd1344ec26c7319e84fd4e9809d2b5628f4e12decd618036e5178/numpy-2.5.3-cp315-cp315-macosx_14_0_x86_64.whl", hash = "sha256:86bff898a431c0fb71f7610b75726e75a54d47b37edc9d537f48de63bb3c0b90", size = 6783026, upload-time = "2026-09-06T16:26:46.374Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c7/29285be1e5232a6e7ee3268a33c85843f5a8ee93350c6465cddd66ebbf76/numpy-2.5.3-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f3ed25271581281f2fccb1adcedfcde4c07362eec69189b50baf6f90e3ae159", size = 15697322, upload-time = "2026-09-06T16:26:49.415Z" }, + { url = "https://files.pythonhosted.org/packages/55/49/bbad5335fb4996a16881f853ff3e0ba582f01720e55c89b1c06b8fc42a90/numpy-2.5.3-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ffdc76bfcae6b255dff75202c5e7feaf95b40246bc0a17944facc1fecf9f79ab", size = 16708995, upload-time = "2026-09-06T16:26:53.127Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e9/1df35483760b04a65ea44669f89dc64f30e5aca098b48ceb8b1310b0e0fe/numpy-2.5.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:116f96cadd935c6122e9228d676fe7ede19e741f5c8bb1c3cddbe0c51ccebea2", size = 17052508, upload-time = "2026-09-06T16:26:56.464Z" }, + { url = "https://files.pythonhosted.org/packages/b8/99/66e54da8265cc8be8a7382bf96edce17aaa2837d6f484432025932a3caa5/numpy-2.5.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:09ffa5d903faeaa5c4dd05009cf81c8bab9f2cb37c548b8d39b65b4cfa7c97f7", size = 18468224, upload-time = "2026-09-06T16:26:59.966Z" }, + { url = "https://files.pythonhosted.org/packages/01/bc/b5e90a91c115168d793dfd2ad9c69c438c2fe7a13a437e770bc5b078e732/numpy-2.5.3-cp315-cp315-win32.whl", hash = "sha256:e01c918ac3d48e18a927cf7b14a26a3e29ff2bdf2eacb976da0aecd6a43ed034", size = 6179919, upload-time = "2026-09-06T16:27:03.166Z" }, + { url = "https://files.pythonhosted.org/packages/37/ea/780748fd3985109075514ef8fc64cd25f943e40dde13a6d59141eb268fc8/numpy-2.5.3-cp315-cp315-win_amd64.whl", hash = "sha256:e931e4f499e0dc7ef29d269a8e5b35dd722e5d14be07df6240166ea7c6532fae", size = 12697656, upload-time = "2026-09-06T16:27:06.153Z" }, + { url = "https://files.pythonhosted.org/packages/b3/16/407be69a2a87c8cab64d95975a8977a426a29e138f07e276ec258f0fe4e5/numpy-2.5.3-cp315-cp315-win_arm64.whl", hash = "sha256:26e15e4aecd8617dfbaecb37d223e365d7b39411fba20454be2670a96aa74cb5", size = 10767601, upload-time = "2026-09-06T16:27:09.297Z" }, + { url = "https://files.pythonhosted.org/packages/44/bf/a97ffb01e41d50a32a9177aef942a4d0e389a3daf451d04e5f38ef6afb87/numpy-2.5.3-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:6cef4bb1706dfec49243c05d921eefb4e190d41e2528b30d8035ea1f36b4c24a", size = 17090092, upload-time = "2026-09-06T16:27:12.907Z" }, + { url = "https://files.pythonhosted.org/packages/d1/24/136c02f2c2af9a067a84d0c3aa10c99012c0476fa5066732fa4a4202557d/numpy-2.5.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:d1c89973648c85069c5046ad460f7b8a00218b29a2e42359ac8cc63e9ab94832", size = 12129429, upload-time = "2026-09-06T16:27:16.089Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6c/b47582d6597789bf946d5efbeb6b9e56fd8bcbd5efc6fbf51dbe1ea31eb3/numpy-2.5.3-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:214045a5bf00113a146ab9ee9730c44501af6723cdf1f6830932f7b5ef2e7af0", size = 5565452, upload-time = "2026-09-06T16:27:19.868Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/ef3cc6da73774202d4deae16bb321fd8298a4e0561e3539f8c4be237d916/numpy-2.5.3-cp315-cp315t-macosx_14_0_x86_64.whl", hash = "sha256:8617bbfae4486cf99c9f899966699428d19da931d06ca94ad3da986c76e15997", size = 6876736, upload-time = "2026-09-06T16:27:22.232Z" }, + { url = "https://files.pythonhosted.org/packages/9e/24/e3813329498596cb842703dcacac1741612ed9fb9c4e6a3e0c7e2ebbc597/numpy-2.5.3-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:595d020938c84e320bcf40ad71089e108eac0d377cd018e14a8c094f39e98d85", size = 15745777, upload-time = "2026-09-06T16:27:25.181Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9e/4e7a07fd0776dc2210cdacf2010be8665194d094defc10c419d7dea794cc/numpy-2.5.3-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f24021b9f22bc6301c37b196974a92c1c18dccedb6fef3dd252e95f2d6adbe4", size = 16746949, upload-time = "2026-09-06T16:27:28.576Z" }, + { url = "https://files.pythonhosted.org/packages/91/db/01674c0e20335057813a00c2ebd546ed25bff9ed7914f9bced00f8c55d94/numpy-2.5.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:71b39d9f935b6ec0f8753e3e2afb51e3efba6f2e05b68b32a40754d24bcd4a3c", size = 17108994, upload-time = "2026-09-06T16:27:31.946Z" }, + { url = "https://files.pythonhosted.org/packages/45/7a/584c5e71f8d378e57cac0b033891ed65c683ef90573ba4854e8c28203db0/numpy-2.5.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:6b05c171afb3aa07adbd20abc00aea86fe375beb0fdb9ef780ec5b7f63bab1c0", size = 18512266, upload-time = "2026-09-06T16:27:35.196Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d2/4e1014173aa3c55e6a756e0e567290743a6ab33a288460374d7ef6bcd239/numpy-2.5.3-cp315-cp315t-win32.whl", hash = "sha256:f54660b0eb6b0b9f36e7fe1cdfdff472028dd0d14acd9b9b65098efbad059469", size = 6330292, upload-time = "2026-09-06T16:27:38.149Z" }, + { url = "https://files.pythonhosted.org/packages/6c/b0/ff5658a58199b7bcaad87bf260eef6713d9d42cca4e028f935b4fc5fbac6/numpy-2.5.3-cp315-cp315t-win_amd64.whl", hash = "sha256:1aad64d99730d013cfc6debafed22783b4fc5a7f4b8bc744d2d8cf7dcc880551", size = 12884918, upload-time = "2026-09-06T16:27:40.965Z" }, + { url = "https://files.pythonhosted.org/packages/fb/0b/b12a2df5d1b774bd9007a6fdff9381145b6223d37f11afc9c37ab0efd9a1/numpy-2.5.3-cp315-cp315t-win_arm64.whl", hash = "sha256:befa1ae5bd6030b3f512b43ff3fa5290bbed6b84411a44244b14adf835f5b89d", size = 10850807, upload-time = "2026-09-06T16:27:43.868Z" }, +] + +[[package]] +name = "nvidia-cublas" +version = "13.1.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cuda-nvrtc", marker = "(python_full_version < '3.15' and sys_platform == 'emscripten') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/a1/0bd24ee8c8d03adac032fd2909426a00c88f8c57961b1277ded97f91119f/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b7a210458267ac818974c53038fbec2e969d5c99f305ab15c72522fa9f001dd5", size = 542848918, upload-time = "2026-04-08T18:46:22.985Z" }, + { url = "https://files.pythonhosted.org/packages/3b/cd/154ca20c38269e05eff77c1464e6c1da89f50a6390b565e9d82e06bc11e1/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:37936a16db8fe4ac1f065c2139360608a543a09275cb1a1af612e08cfa065436", size = 423138758, upload-time = "2026-04-08T18:46:58.655Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/2a/80353b103fc20ce05ef51e928daed4b6015db4aaa9162ed0997090fe2250/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_aarch64.whl", hash = "sha256:796bd679890ee55fb14a94629b698b6db54bcfd833d391d5e94017dd9d7d3151", size = 10310827, upload-time = "2025-09-04T08:26:42.012Z" }, + { url = "https://files.pythonhosted.org/packages/33/6d/737d164b4837a9bbd202f5ae3078975f0525a55730fe871d8ed4e3b952b0/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_x86_64.whl", hash = "sha256:4eb01c08e859bf924d222250d2e8f8b8ff6d3db4721288cf35d14252a4d933c8", size = 10715597, upload-time = "2025-09-04T08:26:51.312Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc" +version = "13.0.88" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/68/483a78f5e8f31b08fb1bb671559968c0ca3a065ac7acabfc7cee55214fd6/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:ad9b6d2ead2435f11cbb6868809d2adeeee302e9bb94bcf0539c7a40d80e8575", size = 90215200, upload-time = "2025-09-04T08:28:44.204Z" }, + { url = "https://files.pythonhosted.org/packages/b7/dc/6bb80850e0b7edd6588d560758f17e0550893a1feaf436807d64d2da040f/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d27f20a0ca67a4bb34268a5e951033496c5b74870b868bacd046b1b8e0c3267b", size = 43015449, upload-time = "2025-09-04T08:28:20.239Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime" +version = "13.0.96" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/4f/17d7b9b8e285199c58ce28e31b5c5bbaa4d8271af06a89b6405258245de2/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef9bcbe90493a2b9d810e43d249adb3d02e98dd30200d86607d8d02687c43f55", size = 2261060, upload-time = "2025-10-09T08:55:15.78Z" }, + { url = "https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f82250d7782aa23b6cfe765ecc7db554bd3c2870c43f3d1821f1d18aebf0548", size = 2243632, upload-time = "2025-10-09T08:55:36.117Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu13" +version = "9.24.0.43" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas", marker = "(python_full_version < '3.15' and sys_platform == 'emscripten') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/30/7c257e3d5cb4fecb147b93895c66e29c93f8e76d74b45bb418ff0587c4ec/nvidia_cudnn_cu13-9.24.0.43-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:a6812a554a1ff0413e9c52b84c26c050380649ab9615f9c16bded368ce9f421f", size = 650976863, upload-time = "2026-07-02T16:23:39.248Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ba/791cffd048fe5b044e620df55267e3e95c0e6e07d50b41e377c03dfc910f/nvidia_cudnn_cu13-9.24.0.43-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:71f181cd810e90f9b6023b01186fe82d13d65f0ec098581ee201d39fad769e4b", size = 553099438, upload-time = "2026-07-02T16:27:42.58Z" }, +] + +[[package]] +name = "nvidia-cufft" +version = "12.0.0.61" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink", marker = "(python_full_version < '3.15' and sys_platform == 'emscripten') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c44f692dce8fd5ffd3e3df134b6cdb9c2f72d99cf40b62c32dde45eea9ddad3", size = 214085489, upload-time = "2025-09-04T08:31:56.044Z" }, +] + +[[package]] +name = "nvidia-cufile" +version = "1.15.1.6" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08a3ecefae5a01c7f5117351c64f17c7c62efa5fffdbe24fc7d298da19cd0b44", size = 1223672, upload-time = "2025-09-04T08:32:22.779Z" }, + { url = "https://files.pythonhosted.org/packages/ab/73/cc4a14c9813a8a0d509417cf5f4bdaba76e924d58beb9864f5a7baceefbf/nvidia_cufile-1.15.1.6-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:bdc0deedc61f548bddf7733bdc216456c2fdb101d020e1ab4b88d232d5e2f6d1", size = 1136992, upload-time = "2025-09-04T08:32:14.119Z" }, +] + +[[package]] +name = "nvidia-curand" +version = "10.4.0.35" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/72/7c2ae24fb6b63a32e6ae5d241cc65263ea18d08802aaae087d9f013335a2/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:133df5a7509c3e292aaa2b477afd0194f06ce4ea24d714d616ff36439cee349a", size = 61962106, upload-time = "2025-08-04T10:21:41.128Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9f/be0a41ca4a4917abf5cb9ae0daff1a6060cc5de950aec0396de9f3b52bc5/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:1aee33a5da6e1db083fe2b90082def8915f30f3248d5896bcec36a579d941bfc", size = 59544258, upload-time = "2025-08-04T10:22:03.992Z" }, +] + +[[package]] +name = "nvidia-cusolver" +version = "12.0.4.66" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas", marker = "(python_full_version < '3.15' and sys_platform == 'emscripten') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "nvidia-cusparse", marker = "(python_full_version < '3.15' and sys_platform == 'emscripten') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "nvidia-nvjitlink", marker = "(python_full_version < '3.15' and sys_platform == 'emscripten') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, + { url = "https://files.pythonhosted.org/packages/5f/67/cba3777620cdacb99102da4042883709c41c709f4b6323c10781a9c3aa34/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0a759da5dea5c0ea10fd307de75cdeb59e7ea4fcb8add0924859b944babf1112", size = 200941980, upload-time = "2025-09-04T08:33:22.767Z" }, +] + +[[package]] +name = "nvidia-cusparse" +version = "12.6.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink", marker = "(python_full_version < '3.15' and sys_platform == 'emscripten') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, + { url = "https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b3c89c88d01ee0e477cb7f82ef60a11a4bcd57b6b87c33f789350b59759360b", size = 145942937, upload-time = "2025-09-04T08:33:58.029Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu13" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/e1/cdc1797eadf82d3a9a575a19b33fdc871a97edbec42c00b5b5e914f4aff4/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:4dca476c50bf4780d46cd0bfbd82e2bc10a08e4fef7950917ce8d7578d22a23f", size = 221051344, upload-time = "2025-09-05T18:49:51.289Z" }, + { url = "https://files.pythonhosted.org/packages/34/7d/2661f2fb3ac4302f3a246f5fc030213ac60c1fe0bce84f9783dbd831dbb7/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:786ce87568c303fadb5afcc7102d454cd3040d75f6f8626f5db460d1871f4dd0", size = 170148586, upload-time = "2025-09-05T18:50:50.248Z" }, +] + +[[package]] +name = "nvidia-nccl-cu13" +version = "2.30.7" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/21/a73174c6157101bdf1ffc22b517f76ff0082613989dd9bc8f43e8034caac/nvidia_nccl_cu13-2.30.7-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:ca786ffa5a647c75d4d1f5cc72a6c4f537947e2ba8823d7c8aaf768e7a7b9f77", size = 215983881, upload-time = "2026-06-09T03:23:15.633Z" }, + { url = "https://files.pythonhosted.org/packages/3f/34/c500f90c7ae641b8e0f98965b36b8a7ac79cc8b296e8d251fe3eb592ee54/nvidia_nccl_cu13-2.30.7-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:cefa7fdb9710efd0f39c5f1be1d61ff6fc9a996c451265bd7fbdcf9455ed4b50", size = 215965170, upload-time = "2026-06-09T03:23:39.73Z" }, +] + +[[package]] +name = "nvidia-nvjitlink" +version = "13.4.52" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/c1/091f198d7f87e31d67fa9680eec8f8e4c6f889881f729b759db36ff01612/nvidia_nvjitlink-13.4.52-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:90401db7e5a580a5067a468b3086e0b65f3b96ab3c42524afd741efc8a0e150a", size = 42452221, upload-time = "2026-09-09T18:01:32.895Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a9/225ff51e80de170be880cb88e992193bc8134a51059cc0a3952f967f62c5/nvidia_nvjitlink-13.4.52-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3a589e3732140839349545efd0db67426523459b12e6952c3c73b6d55900f200", size = 40419746, upload-time = "2026-09-09T18:01:25.103Z" }, +] + +[[package]] +name = "nvidia-nvshmem-cu13" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/0f/05cc9c720236dcd2db9c1ab97fff629e96821be2e63103569da0c9b72f19/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dc2a197f38e5d0376ad52cd1a2a3617d3cdc150fd5966f4aee9bcebb1d68fe9", size = 60215947, upload-time = "2025-09-06T00:32:20.022Z" }, + { url = "https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:290f0a2ee94c9f3687a02502f3b9299a9f9fe826e6d0287ee18482e78d495b80", size = 60412546, upload-time = "2025-09-06T00:32:41.564Z" }, +] + +[[package]] +name = "nvidia-nvtx" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/f3/d86c845465a2723ad7e1e5c36dcd75ddb82898b3f53be47ebd429fb2fa5d/nvidia_nvtx-13.0.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4936d1d6780fbe68db454f5e72a42ff64d1fd6397df9f363ae786930fd5c1cd4", size = 148047, upload-time = "2025-09-04T08:29:01.761Z" }, + { url = "https://files.pythonhosted.org/packages/a8/64/3708a90d1ebe202ffdeb7185f878a3c84d15c2b2c31858da2ce0583e2def/nvidia_nvtx-13.0.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb7780edb6b14107373c835bf8b72e7a178bac7367e23da7acb108f973f157a6", size = 148878, upload-time = "2025-09-04T08:28:53.627Z" }, +] + +[[package]] +name = "olefile" +version = "0.47" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/1b/077b508e3e500e1629d366249c3ccb32f95e50258b231705c09e3c7a4366/olefile-0.47.zip", hash = "sha256:599383381a0bf3dfbd932ca0ca6515acd174ed48870cbf7fee123d698c192c1c", size = 112240, upload-time = "2023-12-01T16:22:53.025Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/d3/b64c356a907242d719fc668b71befd73324e47ab46c8ebbbede252c154b2/olefile-0.47-py2.py3-none-any.whl", hash = "sha256:543c7da2a7adadf21214938bb79c83ea12b473a4b6ee4ad4bf854e7715e13d1f", size = 114565, upload-time = "2023-12-01T16:22:51.518Z" }, +] + +[[package]] +name = "ollama" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/5a/652dac4b7affc2b37b95386f8ae78f22808af09d720689e3d7a86b6ed98e/ollama-0.6.1.tar.gz", hash = "sha256:478c67546836430034b415ed64fa890fd3d1ff91781a9d548b3325274e69d7c6", size = 51620, upload-time = "2025-11-13T23:02:17.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/4f/4a617ee93d8208d2bcf26b2d8b9402ceaed03e3853c754940e2290fed063/ollama-0.6.1-py3-none-any.whl", hash = "sha256:fc4c984b345735c5486faeee67d8a265214a31cbb828167782dc642ce0a2bf8c", size = 14354, upload-time = "2025-11-13T23:02:16.292Z" }, +] + +[[package]] +name = "omegaconf" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "antlr4-python3-runtime", marker = "python_full_version < '3.11'" }, + { name = "pyyaml", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/3d/e4b57b8d9008c6ebe0d5eff901f91d5700cf7bdb8c8863df817463a7fd5e/omegaconf-2.3.1.tar.gz", hash = "sha256:e5e7de64aeebeddaf8e6d3f7a783b32ac2a01c0fbd9c878012caecb891a1f42a", size = 3298472, upload-time = "2026-06-11T05:05:12.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/0e/152509871bf30df6fc38569f52a2db9b55dd41aae957adae50a053ac7778/omegaconf-2.3.1-py3-none-any.whl", hash = "sha256:3d701d14e9a8828f1edd28bb70b725908b34277cdd72cf7d6a83f94dadc6b6a0", size = 79502, upload-time = "2026-06-11T05:05:09.954Z" }, +] + +[[package]] +name = "onnx" +version = "1.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ml-dtypes", marker = "python_full_version < '3.11' or python_full_version == '3.12.*' or sys_platform != 'win32'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*' and sys_platform != 'win32'" }, + { name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and sys_platform != 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, + { name = "protobuf", marker = "python_full_version < '3.11' or python_full_version == '3.12.*' or sys_platform != 'win32'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11' or python_full_version == '3.12.*' or sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/19/8ea73a64b368b75fe339771a20a02bc61ea1f551484c9e3d9d0bfbd0450f/onnx-1.22.0.tar.gz", hash = "sha256:ef40c0aaf0b643857ea9306fc7eddce17eaf9fb0407e4801f1fc5758443a38e0", size = 12024721, upload-time = "2026-06-15T12:50:05.354Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/04/471f234e2716c83f17a26e1b50cd64c39428373e91dd018aafb3d499c108/onnx-1.22.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:6d0ffffd63a4ecc21ddaeddd5bf02099cb701aa4243f2de00122726869065ca4", size = 20167110, upload-time = "2026-06-15T12:48:59.152Z" }, + { url = "https://files.pythonhosted.org/packages/99/40/540a2fe3c49ce1709ff2015de20d9a351264fb442f8998f92cf0ba7e279e/onnx-1.22.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33ce94119bbb7f05d9caea4ea7549f5185a54369f6bbc9f70171bd5ee6935bbc", size = 18892738, upload-time = "2026-06-15T12:49:02.139Z" }, + { url = "https://files.pythonhosted.org/packages/f8/0c/f41d5b89c38fb2ec410ab23c24fa110af786093b140644f7f953e436743b/onnx-1.22.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:87a3077958f66f9a26dec10077ac28326d9cec2cbe1f0b040947243449754573", size = 19110354, upload-time = "2026-06-15T12:49:05.031Z" }, + { url = "https://files.pythonhosted.org/packages/11/8e/9f41d132855e93c2808cdd4afab1b5af67bd5e82e4a4fa9248006e4df87e/onnx-1.22.0-cp310-cp310-win32.whl", hash = "sha256:8a5eccce2d5fc6c5046928a9aa7cdd9750ea4a586f8de341d3d40d820c35fdec", size = 17083595, upload-time = "2026-06-15T12:49:08.599Z" }, + { url = "https://files.pythonhosted.org/packages/e8/52/86caff81786a5428485795c79175ae2b12a630795bcb267b84e5f9e98450/onnx-1.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:5c1c0408a9d4b4df33851672e5fc7590b96301ee123396d608f9ab6f045ab06b", size = 17215270, upload-time = "2026-06-15T12:49:11.483Z" }, + { url = "https://files.pythonhosted.org/packages/0c/55/30825c02c92a0380ce84c3feeeec95d329fa77548ba58cb10ad4bbfd83c6/onnx-1.22.0-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:2d8f229a553fa440fe623ed7b36fca5e7762da3af871c3f8f8ce451df73e2914", size = 20167891, upload-time = "2026-06-15T12:49:14.212Z" }, + { url = "https://files.pythonhosted.org/packages/4b/24/cd4ab52ecaf41c3fbed674772ccbfe39041cb257b8471a47a37e48bff3f8/onnx-1.22.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1a89a7cb9ba13d78f009bdec448ec82a98972589734f157022a2bff7a5973a6", size = 18892720, upload-time = "2026-06-15T12:49:16.904Z" }, + { url = "https://files.pythonhosted.org/packages/2b/a0/c9d9d56ceadb1c0a90a7cbec5a0510520ab6538938944fa84548e4b5b054/onnx-1.22.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d0a2bdb15eb2b3cb65c438f3423d9620d14fdce32f92380e6bb1b2e09568ef5", size = 19110720, upload-time = "2026-06-15T12:49:19.812Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6e/e43e5a68d9cadde55df75310027f87127333a77e5ddcea14c73e96a10cac/onnx-1.22.0-cp311-cp311-win32.whl", hash = "sha256:239958534464612fbcb6ed23d5228aaa925b39b8773f58726809ffdccb4edd1c", size = 17083746, upload-time = "2026-06-15T12:49:22.935Z" }, + { url = "https://files.pythonhosted.org/packages/54/57/cc0a9f2cf4522e42829d089927b4b75924d32f50dca237482e7b741df003/onnx-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:8561a2c00041c07e08db0c228593b5b4694100398685f348532af7dbb84189da", size = 17215684, upload-time = "2026-06-15T12:49:26.084Z" }, + { url = "https://files.pythonhosted.org/packages/c9/99/0f049f9eaa06c8383060c5f0a338e3a6caac8822e6e326c9162f05abf95a/onnx-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:8907b9b9389893bc0dc6314cc00ee1e3a69844e48d689eacc6a0340411a7da58", size = 17210398, upload-time = "2026-06-15T12:49:29.091Z" }, + { url = "https://files.pythonhosted.org/packages/ee/6a/481561f1093834376ed493e4ca42a73e5be0d50031f2969c86593bdc7c96/onnx-1.22.0-cp312-abi3-macosx_12_0_universal2.whl", hash = "sha256:596fbf0490947533c1c1045ba860851dc9fb77471023dac9a71ba5b42ceab103", size = 20167081, upload-time = "2026-06-15T12:49:32.078Z" }, + { url = "https://files.pythonhosted.org/packages/84/55/b34fc2aa30aa54b4a775402d24c4082242c720283a274fe976ac8eb94480/onnx-1.22.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae5a563f281cd9d2845622cecf6c092a57e4ee1b138f66fdbbdd4200567a5e16", size = 18889249, upload-time = "2026-06-15T12:49:34.7Z" }, + { url = "https://files.pythonhosted.org/packages/09/a6/bd32357e6cc1ecb473afd78193d7231724f284435d2db25696ecfaaa1503/onnx-1.22.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:955e02e1f6d385b53d52f9cd7b9cdf5caf417c300bcfe3c64c6d542be763845b", size = 19106514, upload-time = "2026-06-15T12:49:37.424Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9d/3af461ac6c714b8b369cb71499659932f4f12cfb066250b62f7567c3d530/onnx-1.22.0-cp312-abi3-pyemscripten_2025_0_wasm32.whl", hash = "sha256:82e9f27fc1223cb06d68a56bed6f9d3caf3d0dad1b61bce45006d529b15bd94c", size = 16966387, upload-time = "2026-06-15T12:49:40.918Z" }, + { url = "https://files.pythonhosted.org/packages/d0/f0/68195b5e5a53e333faf2660f5352ee43738d0e42fc5216cc6b1871a9fbfb/onnx-1.22.0-cp312-abi3-win32.whl", hash = "sha256:cc8b66b312f8f03a53e268afb67180a2d97dd12cc79e2b61361c6c0073448016", size = 17081568, upload-time = "2026-06-15T12:49:43.398Z" }, + { url = "https://files.pythonhosted.org/packages/13/a8/734725bb703c5fabb687f79c79e51249475212b3eb37771ac4a4ac9b487f/onnx-1.22.0-cp312-abi3-win_amd64.whl", hash = "sha256:72ccebab3bac07215c204ce8848d42e78eaaa666badbf72d25cd359b9f269e3a", size = 17213290, upload-time = "2026-06-15T12:49:45.933Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2a/8ce48d8ae26a8761ad4e5dc771961b155c5c3c7c8540ec7f2f2d71b69af0/onnx-1.22.0-cp312-abi3-win_arm64.whl", hash = "sha256:f3c120dcdb70ad738f3c061b32798f408ea299eb69f84dd69ab4a6bf3c2ec01f", size = 17207030, upload-time = "2026-06-15T12:49:48.635Z" }, + { url = "https://files.pythonhosted.org/packages/f3/13/47323b97846387848efb1044ded11bb94b83526f3d1fbdb37c6480d4520f/onnx-1.22.0-cp314-cp314t-macosx_12_0_universal2.whl", hash = "sha256:19e45e4af88e3fe3261458d4b8cc461957ae2782a358a3560503569bf3b23b72", size = 20176465, upload-time = "2026-06-15T12:49:51.311Z" }, + { url = "https://files.pythonhosted.org/packages/13/0c/d3b8a7e7eee123938586c608bb9894b5723f2342b9450c0eec59fbec7099/onnx-1.22.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c21a0e59fd967a95b358e4a6e756d1f1eec2d304a83480f329f66e30d2bf0223", size = 18894028, upload-time = "2026-06-15T12:49:54.451Z" }, + { url = "https://files.pythonhosted.org/packages/b8/8a/da2a97ab46fe6e0cd9beb3ac14603a22f5be492f9ca347faf8233a07bb33/onnx-1.22.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2632406b8f523ef2e2873c363f90b20a3d88c0fbcfac757d3addffccf8f452c2", size = 19110420, upload-time = "2026-06-15T12:49:57.665Z" }, + { url = "https://files.pythonhosted.org/packages/b9/a3/ce984063017518307ebfaa545782fc400e593dc2d7fdf4f23ce4be1ed197/onnx-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:a3a39fc4643867aecb33417fdddb11e308ee79d2d4a584b9d50cc7aec2091b13", size = 17237547, upload-time = "2026-06-15T12:50:00.382Z" }, + { url = "https://files.pythonhosted.org/packages/00/50/257a880384a1dd502d543b0067945074d63cd17d0840e958355bc8197da8/onnx-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:8e268cdc0547e3949799ffd4a44451dc2b9080b57d0824a2db680b6ec65506f0", size = 17231391, upload-time = "2026-06-15T12:50:03.047Z" }, +] + +[[package]] +name = "onnxruntime" +version = "1.23.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'win32'", + "python_full_version < '3.11' and sys_platform != 'win32'", +] +dependencies = [ + { name = "coloredlogs", marker = "python_full_version < '3.11'" }, + { name = "flatbuffers", marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "packaging", marker = "python_full_version < '3.11'" }, + { name = "protobuf", marker = "python_full_version < '3.11'" }, + { name = "sympy", marker = "python_full_version < '3.11'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/d6/311b1afea060015b56c742f3531168c1644650767f27ef40062569960587/onnxruntime-1.23.2-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:a7730122afe186a784660f6ec5807138bf9d792fa1df76556b27307ea9ebcbe3", size = 17195934, upload-time = "2025-10-27T23:06:14.143Z" }, + { url = "https://files.pythonhosted.org/packages/db/db/81bf3d7cecfbfed9092b6b4052e857a769d62ed90561b410014e0aae18db/onnxruntime-1.23.2-cp310-cp310-macosx_13_0_x86_64.whl", hash = "sha256:b28740f4ecef1738ea8f807461dd541b8287d5650b5be33bca7b474e3cbd1f36", size = 19153079, upload-time = "2025-10-27T23:05:57.686Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4d/a382452b17cf70a2313153c520ea4c96ab670c996cb3a95cc5d5ac7bfdac/onnxruntime-1.23.2-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f7d1fe034090a1e371b7f3ca9d3ccae2fabae8c1d8844fb7371d1ea38e8e8d2", size = 15219883, upload-time = "2025-10-22T03:46:21.66Z" }, + { url = "https://files.pythonhosted.org/packages/fb/56/179bf90679984c85b417664c26aae4f427cba7514bd2d65c43b181b7b08b/onnxruntime-1.23.2-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4ca88747e708e5c67337b0f65eed4b7d0dd70d22ac332038c9fc4635760018f7", size = 17370357, upload-time = "2025-10-22T03:46:57.968Z" }, + { url = "https://files.pythonhosted.org/packages/cd/6d/738e50c47c2fd285b1e6c8083f15dac1a5f6199213378a5f14092497296d/onnxruntime-1.23.2-cp310-cp310-win_amd64.whl", hash = "sha256:0be6a37a45e6719db5120e9986fcd30ea205ac8103fd1fb74b6c33348327a0cc", size = 13467651, upload-time = "2025-10-27T23:06:11.904Z" }, + { url = "https://files.pythonhosted.org/packages/44/be/467b00f09061572f022ffd17e49e49e5a7a789056bad95b54dfd3bee73ff/onnxruntime-1.23.2-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:6f91d2c9b0965e86827a5ba01531d5b669770b01775b23199565d6c1f136616c", size = 17196113, upload-time = "2025-10-22T03:47:33.526Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a8/3c23a8f75f93122d2b3410bfb74d06d0f8da4ac663185f91866b03f7da1b/onnxruntime-1.23.2-cp311-cp311-macosx_13_0_x86_64.whl", hash = "sha256:87d8b6eaf0fbeb6835a60a4265fde7a3b60157cf1b2764773ac47237b4d48612", size = 19153857, upload-time = "2025-10-22T03:46:37.578Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d8/506eed9af03d86f8db4880a4c47cd0dffee973ef7e4f4cff9f1d4bcf7d22/onnxruntime-1.23.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbfd2fca76c855317568c1b36a885ddea2272c13cb0e395002c402f2360429a6", size = 15220095, upload-time = "2025-10-22T03:46:24.769Z" }, + { url = "https://files.pythonhosted.org/packages/e9/80/113381ba832d5e777accedc6cb41d10f9eca82321ae31ebb6bcede530cea/onnxruntime-1.23.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da44b99206e77734c5819aa2142c69e64f3b46edc3bd314f6a45a932defc0b3e", size = 17372080, upload-time = "2025-10-22T03:47:00.265Z" }, + { url = "https://files.pythonhosted.org/packages/3a/db/1b4a62e23183a0c3fe441782462c0ede9a2a65c6bbffb9582fab7c7a0d38/onnxruntime-1.23.2-cp311-cp311-win_amd64.whl", hash = "sha256:902c756d8b633ce0dedd889b7c08459433fbcf35e9c38d1c03ddc020f0648c6e", size = 13468349, upload-time = "2025-10-22T03:47:25.783Z" }, + { url = "https://files.pythonhosted.org/packages/1b/9e/f748cd64161213adeef83d0cb16cb8ace1e62fa501033acdd9f9341fff57/onnxruntime-1.23.2-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:b8f029a6b98d3cf5be564d52802bb50a8489ab73409fa9db0bf583eabb7c2321", size = 17195929, upload-time = "2025-10-22T03:47:36.24Z" }, + { url = "https://files.pythonhosted.org/packages/91/9d/a81aafd899b900101988ead7fb14974c8a58695338ab6a0f3d6b0100f30b/onnxruntime-1.23.2-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:218295a8acae83905f6f1aed8cacb8e3eb3bd7513a13fe4ba3b2664a19fc4a6b", size = 19157705, upload-time = "2025-10-22T03:46:40.415Z" }, + { url = "https://files.pythonhosted.org/packages/3c/35/4e40f2fba272a6698d62be2cd21ddc3675edfc1a4b9ddefcc4648f115315/onnxruntime-1.23.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76ff670550dc23e58ea9bc53b5149b99a44e63b34b524f7b8547469aaa0dcb8c", size = 15226915, upload-time = "2025-10-22T03:46:27.773Z" }, + { url = "https://files.pythonhosted.org/packages/ef/88/9cc25d2bafe6bc0d4d3c1db3ade98196d5b355c0b273e6a5dc09c5d5d0d5/onnxruntime-1.23.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f9b4ae77f8e3c9bee50c27bc1beede83f786fe1d52e99ac85aa8d65a01e9b77", size = 17382649, upload-time = "2025-10-22T03:47:02.782Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b4/569d298f9fc4d286c11c45e85d9ffa9e877af12ace98af8cab52396e8f46/onnxruntime-1.23.2-cp312-cp312-win_amd64.whl", hash = "sha256:25de5214923ce941a3523739d34a520aac30f21e631de53bba9174dc9c004435", size = 13470528, upload-time = "2025-10-22T03:47:28.106Z" }, + { url = "https://files.pythonhosted.org/packages/3d/41/fba0cabccecefe4a1b5fc8020c44febb334637f133acefc7ec492029dd2c/onnxruntime-1.23.2-cp313-cp313-macosx_13_0_arm64.whl", hash = "sha256:2ff531ad8496281b4297f32b83b01cdd719617e2351ffe0dba5684fb283afa1f", size = 17196337, upload-time = "2025-10-22T03:46:35.168Z" }, + { url = "https://files.pythonhosted.org/packages/fe/f9/2d49ca491c6a986acce9f1d1d5fc2099108958cc1710c28e89a032c9cfe9/onnxruntime-1.23.2-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:162f4ca894ec3de1a6fd53589e511e06ecdc3ff646849b62a9da7489dee9ce95", size = 19157691, upload-time = "2025-10-22T03:46:43.518Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a1/428ee29c6eaf09a6f6be56f836213f104618fb35ac6cc586ff0f477263eb/onnxruntime-1.23.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45d127d6e1e9b99d1ebeae9bcd8f98617a812f53f46699eafeb976275744826b", size = 15226898, upload-time = "2025-10-22T03:46:30.039Z" }, + { url = "https://files.pythonhosted.org/packages/f2/2b/b57c8a2466a3126dbe0a792f56ad7290949b02f47b86216cd47d857e4b77/onnxruntime-1.23.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8bace4e0d46480fbeeb7bbe1ffe1f080e6663a42d1086ff95c1551f2d39e7872", size = 17382518, upload-time = "2025-10-22T03:47:05.407Z" }, + { url = "https://files.pythonhosted.org/packages/4a/93/aba75358133b3a941d736816dd392f687e7eab77215a6e429879080b76b6/onnxruntime-1.23.2-cp313-cp313-win_amd64.whl", hash = "sha256:1f9cc0a55349c584f083c1c076e611a7c35d5b867d5d6e6d6c823bf821978088", size = 13470276, upload-time = "2025-10-22T03:47:31.193Z" }, + { url = "https://files.pythonhosted.org/packages/7c/3d/6830fa61c69ca8e905f237001dbfc01689a4e4ab06147020a4518318881f/onnxruntime-1.23.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9d2385e774f46ac38f02b3a91a91e30263d41b2f1f4f26ae34805b2a9ddef466", size = 15229610, upload-time = "2025-10-22T03:46:32.239Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ca/862b1e7a639460f0ca25fd5b6135fb42cf9deea86d398a92e44dfda2279d/onnxruntime-1.23.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2b9233c4947907fd1818d0e581c049c41ccc39b2856cc942ff6d26317cee145", size = 17394184, upload-time = "2025-10-22T03:47:08.127Z" }, +] + +[[package]] +name = "onnxruntime" +version = "1.30.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'win32'", +] +dependencies = [ + { name = "flatbuffers", marker = "(python_full_version >= '3.11' and sys_platform != 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*' and sys_platform != 'win32'" }, + { name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and sys_platform != 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, + { name = "packaging", marker = "(python_full_version >= '3.11' and sys_platform != 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, + { name = "protobuf", marker = "(python_full_version >= '3.11' and sys_platform != 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/b6/e2fa6dfc8c3cd19639d452c3120fb0e02f5176b4e24ecabb24b5362be725/onnxruntime-1.30.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:f36c87ec504f4f328e968ee322e4ba9682abe66280b3425625afcbed5333fed7", size = 21533037, upload-time = "2026-09-10T16:31:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/4f/1a/4d197d2676a866f6239cd2d06a95b6db40d47dc29d5bec9fb1493a13dd94/onnxruntime-1.30.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bac3aee7b40acd805659fd81a2acd6096312d5b0f014c97ddfe75fb4c1644627", size = 21332697, upload-time = "2026-09-10T16:31:08.118Z" }, + { url = "https://files.pythonhosted.org/packages/4f/2e/4c96278a99140d0307ccda6fd51e6e9d1ca7c9f6fdd1fa96ff375acfc713/onnxruntime-1.30.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:fd54b314ea385bcecac69ab431f020ba503e3878dad4ebb645fec5a24b041242", size = 23561046, upload-time = "2026-09-10T16:31:11.494Z" }, + { url = "https://files.pythonhosted.org/packages/66/68/b8aaade8c1883f569228f4b57201cce922bb7d4af6f977d619520a733430/onnxruntime-1.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:0edd0145a6e3fce8a1276491dc784d615e3c58bcb952c9b4e5c876d5c6a12ad7", size = 14309136, upload-time = "2026-09-10T16:31:14.181Z" }, + { url = "https://files.pythonhosted.org/packages/96/f1/fbda6fb08ac4db6c5c6f7c3e128e4d8fb398b52fe2fc3282c45cadedd18b/onnxruntime-1.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:16506cfa5d218850f2b46e0edce1b952121f2f84f8e0074dafe15f4787f0c8fb", size = 14167408, upload-time = "2026-09-10T16:31:16.505Z" }, + { url = "https://files.pythonhosted.org/packages/31/6f/48169f2e62b405bff5053cbd1d73fb5ce41ef7ecd13bb3bfcc191e689b8a/onnxruntime-1.30.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:001ed726c9bd5e2bc92faade7d37d889e9606a350b7d5529f0227df2e3bb57fd", size = 21544867, upload-time = "2026-09-10T16:31:19.876Z" }, + { url = "https://files.pythonhosted.org/packages/16/bd/cbc5b8f91963689fdd622f463508c01d0aa95d3f944747b1e0b1eb2160b8/onnxruntime-1.30.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:6c32a000d5139a38ba9349030b0032e3331acb559d596b22738d9d2b343a2b83", size = 21345202, upload-time = "2026-09-10T16:31:23.361Z" }, + { url = "https://files.pythonhosted.org/packages/34/35/e7f862dbacbc99fadd9b14a614e49c99bf0f35fd9927a82f096e3de33531/onnxruntime-1.30.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:fa688e7891a6aa206636fe7372e27ee75fd17713289f6b4fc7b190e0a7de9328", size = 23585654, upload-time = "2026-09-10T16:31:26.65Z" }, + { url = "https://files.pythonhosted.org/packages/a6/13/0f1699f6de549c9324bc9112a2a85b14c517904cd11b562a654643b755a1/onnxruntime-1.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:f3501472571f1b1eee50e017851e7929f5ea37312d2d8c2494a19e8fc58b4a38", size = 14311470, upload-time = "2026-09-10T16:31:31.273Z" }, + { url = "https://files.pythonhosted.org/packages/a2/17/02b13e5f51461f0453b18ab854e2d0bc1b6ec353241b05a2ee6b79e35d87/onnxruntime-1.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:dc4c706f1935ebb62356e6a095b047859badd854482c40560888e95c328ed262", size = 14175072, upload-time = "2026-09-10T16:31:34.541Z" }, + { url = "https://files.pythonhosted.org/packages/f0/75/508454c5d01f31641dabc597fe559594c931a520a2673031179319d0afd8/onnxruntime-1.30.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:05e4fc41711d1f4abd19a9124b5be7a65a506cb2670a7f14b16162ef13c58134", size = 21544990, upload-time = "2026-09-10T16:31:37.685Z" }, + { url = "https://files.pythonhosted.org/packages/89/06/e603c71f43f4fe3fd156a053af79cbed6e27a2c649f0988a67d97fedd39f/onnxruntime-1.30.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5327cf6aa15a02bad805fac8bd6882a62571e8b72f6f2938a8f37e6bd1966ce9", size = 21344996, upload-time = "2026-09-10T16:31:40.915Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a1/ede48ab5dc54907a2999362777f541e132639fb06628ded1932058aa8a36/onnxruntime-1.30.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:86f940afc801ea9681a4da8af84fbe95e1d9ea7d80903952cc1bfad54faad38f", size = 23585560, upload-time = "2026-09-10T16:31:44.941Z" }, + { url = "https://files.pythonhosted.org/packages/3c/dd/c57c529dbc6dd55eca24b12cfbeab1b6a690de72083824eca689085f55b0/onnxruntime-1.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:4b63041bd623a9a9ac5e353948436c6fa7f43edd12d6b4a4ebc340bca959ba93", size = 14311378, upload-time = "2026-09-10T16:31:48.034Z" }, + { url = "https://files.pythonhosted.org/packages/11/2f/ef00b45b911e7f2115273a24ed1b43e73c8c6c9ec17f9bfa13f8492581f8/onnxruntime-1.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:c389b6887fc95e0fcb80e89b156bc2cb18e662c29df55e9326fe64140e7d7b4f", size = 14175134, upload-time = "2026-09-10T16:31:50.799Z" }, + { url = "https://files.pythonhosted.org/packages/92/0a/284fd6fe701c9a8aff39dbc119c37582ca40e9cff1ea05425a7cc8606a02/onnxruntime-1.30.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e5224ba2b00284cb1c48b3edcd303c109245d66df9ec1b861858fd6de672a38e", size = 21349453, upload-time = "2026-09-10T16:31:53.898Z" }, + { url = "https://files.pythonhosted.org/packages/d6/72/4f4466f8fa1ec267a9ef5e2f3bd175c203d3da0facbdf4049dc93abbe91c/onnxruntime-1.30.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:3f9e002417f1e3bbb31ed43dafa4f22ca1b2b68832244fdee192fcaa1ae19bca", size = 23579837, upload-time = "2026-09-10T16:31:56.82Z" }, + { url = "https://files.pythonhosted.org/packages/6a/03/05c9a9234688757d2876ddecf80bba908561ee11debf125bc1a427ae6f48/onnxruntime-1.30.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:8b6169c16a48429890d2f4a0c774ebf54dfe9066a998514aad0518a16d398547", size = 21545313, upload-time = "2026-09-10T16:31:59.654Z" }, + { url = "https://files.pythonhosted.org/packages/c6/bc/1069e58b24779ba9d2fd479db5ecb3a15a6f49b585107c898819c0789558/onnxruntime-1.30.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:d2184fddb6798136e7c478244391ca82443f5c757f59f15bb9e5ad2da5e03175", size = 21346830, upload-time = "2026-09-10T16:32:02.275Z" }, + { url = "https://files.pythonhosted.org/packages/f1/38/8138eed225c5bc6ddfc05879ecac7dacc63c34b9b6f99be72839c1f6dc49/onnxruntime-1.30.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:8b611d24db2954545ce6bd9acd4670183cb368e4642450de7a9ab6474eb374ec", size = 23586333, upload-time = "2026-09-10T16:32:04.996Z" }, + { url = "https://files.pythonhosted.org/packages/4d/0a/748b86000fbf9518b5dfc5bdc1b924eeaca976203740fd9d30649e568ee6/onnxruntime-1.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:3bdd1752a8502ac1a7ccc6e878d16db6943df574d54ed7f01058a6806ae05be4", size = 14675319, upload-time = "2026-09-10T16:32:07.5Z" }, + { url = "https://files.pythonhosted.org/packages/2f/db/db8e4c0cf6f70f1311060560630fd21763648e01dfbba5195a2592c052ef/onnxruntime-1.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:83d543843cbd352cfa9996a6c7b92f8a480f114a18f95ff2a1acaf6921e85b6d", size = 14569360, upload-time = "2026-09-10T16:32:09.896Z" }, + { url = "https://files.pythonhosted.org/packages/24/0a/ec0a9d656e39b43887c378a5388b20c3b1e1ee43bded84580b0936466694/onnxruntime-1.30.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:265de607ba6f9814264e1d5d413fa7069d70f48e3049b5e460b3b04bdfcef294", size = 21348433, upload-time = "2026-09-10T16:32:12.772Z" }, + { url = "https://files.pythonhosted.org/packages/91/f0/40f74b7c00077e1e25627067ed98a70df1fef5c0e21b82849190d312554e/onnxruntime-1.30.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:67ad7f03433b6462c627d0f555dece80e6a26bc71e8542ced35cebd32142d1b7", size = 23579340, upload-time = "2026-09-10T16:32:15.532Z" }, +] + +[[package]] +name = "openai" +version = "1.109.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/a1/a303104dc55fc546a3f6914c842d3da471c64eec92043aef8f652eb6c524/openai-1.109.1.tar.gz", hash = "sha256:d173ed8dbca665892a6db099b4a2dfac624f94d20a93f46eb0b56aae940ed869", size = 564133, upload-time = "2025-09-24T13:00:53.075Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/2a/7dd3d207ec669cacc1f186fd856a0f61dbc255d24f6fdc1a6715d6051b0f/openai-1.109.1-py3-none-any.whl", hash = "sha256:6bcaf57086cf59159b8e27447e4e7dd019db5d29a438072fbd49c290c7e65315", size = 948627, upload-time = "2025-09-24T13:00:50.754Z" }, +] + +[[package]] +name = "opencv-python" +version = "5.0.0.93" +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.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*' and sys_platform != 'win32'" }, + { name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and sys_platform != 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/4c/a438d23e09ce2033c09f7b784ad2fbdb0adf529e434101ed28f142226f98/opencv_python-5.0.0.93.tar.gz", hash = "sha256:66aac3e5b5faa48d4025816592f3af19e4bfc2c68dec067bae2dbb4ca10aa9e2", size = 81802749, upload-time = "2026-07-02T06:59:53.815Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/75/76f6ade78f6102c61034f828e2a22616708df2c9504bc8d6af9dd8f73dc5/opencv_python-5.0.0.93-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:198a75138241810206a17c829dbcc40a7cb1841cda538ca86cbbfc6c7d95f898", size = 48322443, upload-time = "2026-07-02T05:50:25.466Z" }, + { url = "https://files.pythonhosted.org/packages/15/8c/bc1bda6aae69a32e9d84fc34153ba104cd25226861eb4aea33b2cea4860d/opencv_python-5.0.0.93-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:6bbc32f59e1b1a7db7b39c81f63d00625f041d333037fd8702f6da52cc39108b", size = 34782755, upload-time = "2026-07-02T05:51:30.556Z" }, + { url = "https://files.pythonhosted.org/packages/f4/8a/b04776ec45d2dea08a1b176f1829201db3515d4ed16c35f8fcc9fa7beb16/opencv_python-5.0.0.93-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2b4272e736836f66c2d176e43ab8101f3a00d45654916399f52e150c58981ac", size = 50614064, upload-time = "2026-07-02T06:53:22.604Z" }, + { url = "https://files.pythonhosted.org/packages/95/54/eb47866b94f2b5b42dde17644b78055ef1ee05aae59962c7290e55270803/opencv_python-5.0.0.93-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f8b6d0a212253dd26ad338c812f1f23ca118fdf05a9c8c6b9444f161aa8c5881", size = 71064711, upload-time = "2026-07-02T06:54:13.148Z" }, + { url = "https://files.pythonhosted.org/packages/93/da/962579f1e703cbf8c5422fd1f576467dcb3b5b0b0b81c1471c979764353a/opencv_python-5.0.0.93-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:08d5d91d967b58d6db86073b2ad3eaef88ca4ebdfd45c9059bf59f5ded0c7ad2", size = 49798576, upload-time = "2026-07-02T06:54:33.781Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4c/c73f828fdbcd37eaf21d08fa852544a3ca7c2dbb3ea76873d64f2ea413d1/opencv_python-5.0.0.93-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c8de2dec111122a02e8beb28e16c31904992dfd6186560b142a92c71403c1039", size = 73783032, upload-time = "2026-07-02T06:55:03.415Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4b/edaf83b996ca5a1a3d8ccad485706b9c6d4742b13b9c4586bf1c1e7d9423/opencv_python-5.0.0.93-cp37-abi3-win32.whl", hash = "sha256:4b4b1a34c79bf8d3738e3cfe9a9e67b51a79663f6b692cbdad8c31f570da4157", size = 35564734, upload-time = "2026-07-02T05:49:57.704Z" }, + { url = "https://files.pythonhosted.org/packages/21/f0/9fa6e85cb10c8eb36a0222d27e50fe381b86ce49a55446bf39f491727564/opencv_python-5.0.0.93-cp37-abi3-win_amd64.whl", hash = "sha256:f90ba04b8f73bc5c3814037699739f0156f597338a98f05956c684e7c3ca10d2", size = 44000345, upload-time = "2026-07-02T05:49:54.971Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ee/8b/aa9e2d8b8dfa7c946f7dec5d1f8f6ba8eca062f43509a06bdb5ce93d26c0/opentelemetry_api-1.44.0.tar.gz", hash = "sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a", size = 72406, upload-time = "2026-07-16T15:25:32.678Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/6f/a04e900f465ff3221ccc395522503e2d10e79fa21f2723c8e177aae1e0d1/opentelemetry_api-1.44.0-py3-none-any.whl", hash = "sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef", size = 60018, upload-time = "2026-07-16T15:25:11.657Z" }, +] + +[[package]] +name = "opentelemetry-exporter-zipkin" +version = "1.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-exporter-zipkin-json" }, + { name = "opentelemetry-exporter-zipkin-proto-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1e/5e/e5b74775f3cca5b728a64aea00a29013f536c89d1f3f9c2514a662211c9c/opentelemetry-exporter-zipkin-1.11.1.tar.gz", hash = "sha256:1b5fc6993d04d9376185def150c857a8466cf83f16f58ce2fb09e5b166d995a5", size = 6326, upload-time = "2022-04-21T21:02:49.19Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/66/a2018bf84bd9d1fc6aba24a600d02e571f436dc7b8fe7b6a3c9e4d495e8a/opentelemetry_exporter_zipkin-1.11.1-py3-none-any.whl", hash = "sha256:756270c4eebe344ec32e15d5b04d65959e3bf7c8f338360b0d9d99981530fe96", size = 6998, upload-time = "2022-04-21T21:02:24.602Z" }, +] + +[[package]] +name = "opentelemetry-exporter-zipkin-json" +version = "1.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-sdk" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/9f/4f1ce08f37eae9de39929ee9d247ca32dea0a4aa6425934cb542668d8f81/opentelemetry-exporter-zipkin-json-1.11.1.tar.gz", hash = "sha256:0190947d1ce6f1c90ad4fe799e39af77efd8e984ae5b74885da7b3290cdff875", size = 17819, upload-time = "2022-04-21T21:02:50.052Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/74/d4369676d088434fede00d3592ac227b7502052ac031b72817d4a27e2171/opentelemetry_exporter_zipkin_json-1.11.1-py3-none-any.whl", hash = "sha256:405b4ef12207d55a768348471add1f59a5fd74dd0d0ad4a3281a0694a85fb76c", size = 16290, upload-time = "2022-04-21T21:02:26.117Z" }, +] + +[[package]] +name = "opentelemetry-exporter-zipkin-proto-http" +version = "1.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-zipkin-json" }, + { name = "opentelemetry-sdk" }, + { name = "protobuf" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/cd/2bd290ba645528c456e1cb112fd4dd87f41f2f2e84857dcfbb6354b90bc8/opentelemetry-exporter-zipkin-proto-http-1.11.1.tar.gz", hash = "sha256:595fc729158e208c8e756553fb5ea52d85f6132e0ce161d0695be3f8c807a4bf", size = 19301, upload-time = "2022-04-21T21:02:51.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/23/b84eb97c097d568571fa17346827384ca75a6912f05d70125e1170e479be/opentelemetry_exporter_zipkin_proto_http-1.11.1-py3-none-any.whl", hash = "sha256:08b40dae4ebdf06ad28c6a3d7b70bf9fac4328de56a53fbdd5ac461582bc6e21", size = 14146, upload-time = "2022-04-21T21:02:27.418Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation" version = "0.65b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, { name = "packaging" }, - { name = "wrapt" }, + { name = "wrapt", version = "1.17.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "wrapt", version = "2.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/13/91/3c58961cb0360cd60509064734f0be4275383c8681d73c580a40ca83ddce/opentelemetry_instrumentation-0.65b0.tar.gz", hash = "sha256:071d9d9eced9bd6460444ec3b0c77229870ed05a881c22c84fdede58e4eed09b", size = 42689, upload-time = "2026-07-16T15:25:50.275Z" } wheels = [ @@ -2081,7 +4544,8 @@ dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, - { name = "wrapt" }, + { name = "wrapt", version = "1.17.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "wrapt", version = "2.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/67/80/0006818bfb54d5e3abb138607beff63561c2c920393ec8d18cc7e3d33810/opentelemetry_instrumentation_grpc-0.65b0.tar.gz", hash = "sha256:b05b3a1f476f350fdb12514ca1ebaed0d596095f91188c2efd26b3e6e2a4cc82", size = 31971, upload-time = "2026-07-16T15:26:07.304Z" } wheels = [ @@ -2095,7 +4559,8 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, - { name = "wrapt" }, + { name = "wrapt", version = "1.17.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "wrapt", version = "2.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/85/62/626ee68e07e38f792e741db351087fb7c40888e861097029faae595d91fe/opentelemetry_instrumentation_threading-0.65b0.tar.gz", hash = "sha256:aefd23eb16c5e7a7c6c6eacdcb6c6f269ed8ed3a1b458bf5dd555b878bf58937", size = 9080, upload-time = "2026-07-16T15:26:22.367Z" } wheels = [ @@ -2268,11 +4733,140 @@ wheels = [ [[package]] name = "packaging" -version = "26.0" +version = "24.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950, upload-time = "2024-11-08T09:47:47.202Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451, upload-time = "2024-11-08T09:47:44.722Z" }, +] + +[[package]] +name = "pandas" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'win32'", + "python_full_version < '3.11' and sys_platform != 'win32'", +] +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "python-dateutil", marker = "python_full_version < '3.11'" }, + { name = "pytz", marker = "python_full_version < '3.11'" }, + { name = "tzdata", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/f7/f425a00df4fcc22b292c6895c6831c0c8ae1d9fac1e024d16f98a9ce8749/pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c", size = 11555763, upload-time = "2025-09-29T23:16:53.287Z" }, + { url = "https://files.pythonhosted.org/packages/13/4f/66d99628ff8ce7857aca52fed8f0066ce209f96be2fede6cef9f84e8d04f/pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a", size = 10801217, upload-time = "2025-09-29T23:17:04.522Z" }, + { url = "https://files.pythonhosted.org/packages/1d/03/3fc4a529a7710f890a239cc496fc6d50ad4a0995657dccc1d64695adb9f4/pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1", size = 12148791, upload-time = "2025-09-29T23:17:18.444Z" }, + { url = "https://files.pythonhosted.org/packages/40/a8/4dac1f8f8235e5d25b9955d02ff6f29396191d4e665d71122c3722ca83c5/pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838", size = 12769373, upload-time = "2025-09-29T23:17:35.846Z" }, + { url = "https://files.pythonhosted.org/packages/df/91/82cc5169b6b25440a7fc0ef3a694582418d875c8e3ebf796a6d6470aa578/pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250", size = 13200444, upload-time = "2025-09-29T23:17:49.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/ae/89b3283800ab58f7af2952704078555fa60c807fff764395bb57ea0b0dbd/pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4", size = 13858459, upload-time = "2025-09-29T23:18:03.722Z" }, + { url = "https://files.pythonhosted.org/packages/85/72/530900610650f54a35a19476eca5104f38555afccda1aa11a92ee14cb21d/pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826", size = 11346086, upload-time = "2025-09-29T23:18:18.505Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" }, + { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281, upload-time = "2025-09-29T23:18:56.834Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453, upload-time = "2025-09-29T23:19:09.247Z" }, + { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361, upload-time = "2025-09-29T23:19:25.342Z" }, + { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702, upload-time = "2025-09-29T23:19:38.296Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, + { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, + { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, + { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, + { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" }, + { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" }, + { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" }, + { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" }, + { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" }, + { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" }, + { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" }, + { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" }, + { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" }, + { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" }, + { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" }, + { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" }, + { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" }, + { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" }, + { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" }, + { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" }, + { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" }, + { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, +] + +[[package]] +name = "pandas" +version = "3.0.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'win32'", +] +dependencies = [ + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*' and sys_platform != 'win32'" }, + { name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and sys_platform != 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, + { name = "python-dateutil", marker = "(python_full_version >= '3.11' and sys_platform != 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, + { name = "tzdata", marker = "(python_full_version >= '3.11' and sys_platform == 'emscripten') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/4f/5f3422a2afec5ffc46308b79e53291365a93748b498ac2e58bead0197916/pandas-3.0.5.tar.gz", hash = "sha256:dca3734d6ab7c906e6730f0788b0a1dbb9f2467731f9711f77995c8e9d62d712", size = 4658219, upload-time = "2026-07-22T22:19:28.819Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/ef/f1fd7431d635bf20015489bf0bd69c17fff1018de773540f651455a3916b/pandas-3.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2946e77e4a53cd248cbde631a12f0e51c8324ce354c3eba4d20147c1ad6f4282", size = 10397178, upload-time = "2026-07-22T22:17:48.274Z" }, + { url = "https://files.pythonhosted.org/packages/31/b4/0eafac990a431561187694126de01f9b12559549b4d86360c0c4bd870fde/pandas-3.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71ecc8fb7ed1a7aa4392316b5309a6347e8e7f832f38fd897846b3a1457a9298", size = 9990736, upload-time = "2026-07-22T22:17:52.388Z" }, + { url = "https://files.pythonhosted.org/packages/de/21/359880af3ea9b7cb23bea5b51e8e70ef3866c03be09da9a2787e18e330a8/pandas-3.0.5-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b173f5951ff6b8b0ec7675e20dff3c97b7e7a57dfcce387c2d7c5afe87cb7899", size = 10814438, upload-time = "2026-07-22T22:17:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/d1/50/d6cc4d7e508bbccf5d6027314a8312bc7ac73d0ec7f195f53838daafab40/pandas-3.0.5-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c0cf1dd9b55a22d105fc46c1b489af3bd42264fcba7c66297bf47a9a1d9c78a", size = 11323634, upload-time = "2026-07-22T22:17:56.858Z" }, + { url = "https://files.pythonhosted.org/packages/70/2b/d5f0a8c90dd0ae04e64ba53b871afb796ec026b615086d382ddc2ade729b/pandas-3.0.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0fac0010c75e4efb6b99e249c183a8993ce0dc95c240f9b120a5e67c727b7928", size = 11850860, upload-time = "2026-07-22T22:17:59.1Z" }, + { url = "https://files.pythonhosted.org/packages/5c/30/183aec2e19adf778a98d29b5729a0a68f4cc4ebf9b9c3b70d0297355bcb1/pandas-3.0.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:08d24fe11a17dc33bd6e937dc9c665f9cba08fbdc9f657f405713515febe300d", size = 12411100, upload-time = "2026-07-22T22:18:01.485Z" }, + { url = "https://files.pythonhosted.org/packages/fa/9a/31f4983f191af51ab2a8f2d0c7b33dff3a84da26533f982fff02c2f9e28b/pandas-3.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:b1261758dfb6cf12c3cff8300e21cefad30e7ec709abb4c24ac7318e6a52462a", size = 9968804, upload-time = "2026-07-22T22:18:03.903Z" }, + { url = "https://files.pythonhosted.org/packages/49/97/7886c89a39045c69ad82cbceaf3343810480c8ef49a216319ce8183860a6/pandas-3.0.5-cp311-cp311-win_arm64.whl", hash = "sha256:679f4e85b30ddb1515458ab1e788d3e260eae369b1f78da7a3aa4cac8ebf4a2a", size = 9205447, upload-time = "2026-07-22T22:18:06.134Z" }, + { url = "https://files.pythonhosted.org/packages/1c/54/1dc810ea558d1320b597aa140a514f2fdf1d2ea09c38cf556f13ea712ec9/pandas-3.0.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fa290c16964d4963fbfbc358928239cf3bd755b20e988ce944877def2f44471d", size = 10411717, upload-time = "2026-07-22T22:18:08.307Z" }, + { url = "https://files.pythonhosted.org/packages/68/56/fbe81c09195924d8b7b8d4461a20458fe80a6a5ed6b24f0314da684277e1/pandas-3.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2e26bb46934b8a2ca0c3de1d3d606fc5f6746584791b2db264d58cf370e08dc", size = 9957095, upload-time = "2026-07-22T22:18:10.6Z" }, + { url = "https://files.pythonhosted.org/packages/e0/51/fac252f4a913ed5eabf3c11b880a9e8d5a6c10f0b2129d0462212d238b4d/pandas-3.0.5-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73fa87b08a7ef706f8aafda39ddaccf2a99047bea62d8c88a0361bcafb2237bc", size = 10485458, upload-time = "2026-07-22T22:18:12.834Z" }, + { url = "https://files.pythonhosted.org/packages/12/98/e976540c1addf70442be7842a18cf70884a964abbf69442504f4d2939989/pandas-3.0.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d373ce03ffd84010ed9839fa73672a9c8256990532e158440c0085db7d914b34", size = 10998091, upload-time = "2026-07-22T22:18:15.209Z" }, + { url = "https://files.pythonhosted.org/packages/a4/8c/1f29b5be8d3fc47dd7567eb167fabba2085879b31e0287ce7cba6d3d2ff4/pandas-3.0.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2a29c53d85ea98c5e792c59ef82ee9fbe6ca902c0d0adb6b23f45ef894cd7bf6", size = 11499501, upload-time = "2026-07-22T22:18:17.689Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e2/bd9c98ad2df7b38bde002adde4cdf353519da51881634323b126c55997f9/pandas-3.0.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a5ad3b02ed6bc7d7ae9b70804b2c6aa31827489d150f8e623ce82491b82085d7", size = 12060559, upload-time = "2026-07-22T22:18:20.147Z" }, + { url = "https://files.pythonhosted.org/packages/f3/9a/ffbd852d58bd74a617fe2f8ee6a58a96982271ce41cf981eab22190b4a4b/pandas-3.0.5-cp312-cp312-pyemscripten_2024_0_wasm32.whl", hash = "sha256:b2acb4650527eec6822c3dadb2b771277b65e7dae7a267d4bccf65fd1bb3fbce", size = 7197652, upload-time = "2026-07-22T22:18:22.502Z" }, + { url = "https://files.pythonhosted.org/packages/70/b5/d2d3e9ae73362ba4229651b0ee1455cf78073a1ce585f6ff693782ce263e/pandas-3.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:80a611068e8a3ac23f7398c6c14eb46dc974e5cc9997f653e2dcfd1da74edd41", size = 9831691, upload-time = "2026-07-22T22:18:24.534Z" }, + { url = "https://files.pythonhosted.org/packages/52/51/dea1e89d6a6796b9c43f85a09b484ee03edb8a4c4842e73e200a8c11301c/pandas-3.0.5-cp312-cp312-win_arm64.whl", hash = "sha256:25ff585b972a18ef1fe9ffa3ac6544d9950508aa76832e5147640b6022821e49", size = 9105796, upload-time = "2026-07-22T22:18:27.064Z" }, + { url = "https://files.pythonhosted.org/packages/bf/09/7b95c4a0025227d6f118c4039b423412ac6a982db02864166185d812fbc7/pandas-3.0.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c1c05a767fe8e5b4fe9e1c29806829c582052eaedb9120a3da83ba3f69e24a5b", size = 10385742, upload-time = "2026-07-22T22:18:29.346Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0c/dc78fd8c4da477b4b5e8ad37295af352190d21ef63a9ee1bc071753074cc/pandas-3.0.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b86765f268b56f7e665b93bce9d5df69dee7f99e595cf8fb839483ab315942a3", size = 9932067, upload-time = "2026-07-22T22:18:31.833Z" }, + { url = "https://files.pythonhosted.org/packages/3e/71/3592c055cf44df9808550f9368ceda80ff2b224d355ef73fe251dcda1802/pandas-3.0.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c597ecf5616b5c420372c1d4d4c00dbbfba7398bea857dcc984347e1ea48417b", size = 10466756, upload-time = "2026-07-22T22:18:34.195Z" }, + { url = "https://files.pythonhosted.org/packages/e3/70/4363150359f95b4cb4bcbb34ca23572bb5495749a621a8f3d5a1ddfd293c/pandas-3.0.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b11c36e218331d0387cbe3a0a5f75162357a1d92d57b2b08a336ff94b19b2be", size = 10938525, upload-time = "2026-07-22T22:18:36.81Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d0/317e7a0c67c0e69fa905a0161409397a7dc2d46ff611f6ca4803352c042b/pandas-3.0.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cf52e1f61d229496da17dc7ab54acdee627357e7008fd4fecba3d0ba2937fa58", size = 11489303, upload-time = "2026-07-22T22:18:39.287Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8d/36dade89b49e4f9d5cbdbe863772581f98c0c6d78fc39ad4c557f6f2e17e/pandas-3.0.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:db172144bb56422bd157812f3b021eacc255451470b31e2c633c349490a1cfee", size = 11989004, upload-time = "2026-07-22T22:18:42.208Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ba/18c4ec8a746e177da05a9e7a7963781d8ea195780724f854601b6ebd6b78/pandas-3.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:0d298e951f23016ce4699951d044ae6418dbc91bf68cefca0f77666fcbb4e5c6", size = 9826896, upload-time = "2026-07-22T22:18:44.539Z" }, + { url = "https://files.pythonhosted.org/packages/de/ec/28a57266b753799a87b8bc79e7887ac6fd981b8c6d2978a0b7e7b6bd708c/pandas-3.0.5-cp313-cp313-win_arm64.whl", hash = "sha256:66266d3442a5e8b3c90274c2b8b230bee42dd1c286bc822cc2f9f2c7e12b883e", size = 9094790, upload-time = "2026-07-22T22:18:47.468Z" }, + { url = "https://files.pythonhosted.org/packages/51/2f/cf6aae281264f4463f0875bcbb15fd2bb6d291cc535187dad1732475e4a9/pandas-3.0.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2f264fc46911cc8131a7322a16199bbf8e353d27c10bb211f5bd0c814324dc36", size = 10390034, upload-time = "2026-07-22T22:18:49.818Z" }, + { url = "https://files.pythonhosted.org/packages/06/ec/5189518c7a7659c4bdcc6b1eb32c46c6f3c86b0661ffd84143d1112c7732/pandas-3.0.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:53730687fcd161883b24e10411c06d6a4c0f2275d2faf3bb2bc25deb4ba8007c", size = 9980065, upload-time = "2026-07-22T22:18:52.249Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f1/598503ce8d7e3c35601e0747ba288c7864baae66380725bc12f13f884dfe/pandas-3.0.5-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:960d3ebcf249f75206899fcd2c6de53f736b7265759ced0d3e559df0b8b709b0", size = 10545532, upload-time = "2026-07-22T22:18:54.813Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/ceae2adf7034e07e9910299fe412e1819c4f0dd520700a888bcb03625448/pandas-3.0.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e94c2c5ca43bd3ca32bf64d32308887b65e5f9bfd8023ea52755107a999f93b", size = 10963120, upload-time = "2026-07-22T22:18:57.42Z" }, + { url = "https://files.pythonhosted.org/packages/66/25/86e0f4451874eb79e688deeebe3c451fec4557f8952005818d800ee8ac7e/pandas-3.0.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e819dd5f62966b481a8cb649d3299ebd886a1ea91ed5a99bf7ce77c98d18ab94", size = 11563178, upload-time = "2026-07-22T22:18:59.729Z" }, + { url = "https://files.pythonhosted.org/packages/f3/45/8643daa3b4147e433adfcccefdd0380d3aad79d86b15d8999730fe1944d5/pandas-3.0.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3c5ed2e7c06e91d340dfd091d7934f9bc82e4a36b95f647f090b9d1c9ac649da", size = 12028708, upload-time = "2026-07-22T22:19:02.164Z" }, + { url = "https://files.pythonhosted.org/packages/96/58/ad979ae617615576e8aafd569c9d4b62f1191d896e38f51d66ba06f3b89a/pandas-3.0.5-cp314-cp314-win_amd64.whl", hash = "sha256:cd8f7c6dc98527058ee6264219343f5392240a6f1bfa654fc5d79023020d0c92", size = 9951806, upload-time = "2026-07-22T22:19:04.596Z" }, + { url = "https://files.pythonhosted.org/packages/69/32/7ac03886b304049a9d2625ee88f59af760d8a93bd30ed9239bce7b9869a8/pandas-3.0.5-cp314-cp314-win_arm64.whl", hash = "sha256:5183427f5a8156d480f30333777bc978be93650a49a7c01db26adffe95b31e85", size = 9238297, upload-time = "2026-07-22T22:19:06.836Z" }, + { url = "https://files.pythonhosted.org/packages/be/ed/1d1f2ee5547d5167face2376d11c8b2a4c7bfff5a416ee7a9046891fab1e/pandas-3.0.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:303da736987d481074ca720ada325f8bd80c64ebc2d45ed79b29df3aaa4a26ca", size = 10849690, upload-time = "2026-07-22T22:19:09.391Z" }, + { url = "https://files.pythonhosted.org/packages/57/55/17e17152e98fbb0c4b1e562bc65387a2f20a80db0f4a86bf8d3a0e4248d4/pandas-3.0.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3b2801bbb049d0136f6c213eae02b5fca969384fc2064dd728d8620552aa49da", size = 10509945, upload-time = "2026-07-22T22:19:11.773Z" }, + { url = "https://files.pythonhosted.org/packages/88/90/817d44dbf83facf9556f33576d9af0a241981e7bb5c00606c0bcb5df8dda/pandas-3.0.5-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cce3a9d11d2b1f82c69a27ec1f4948a170e2c403c4bbfa8cca62e3fdebe2ef3a", size = 10392197, upload-time = "2026-07-22T22:19:14.024Z" }, + { url = "https://files.pythonhosted.org/packages/f1/da/889f00c0a6f5aa1545add70abbf01502dff87ab577adb855bd631c54d2f2/pandas-3.0.5-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef01af4d8dc6cd2c8d6c7736f149574ef93fe043811eeb5e445f2647154b5040", size = 10862726, upload-time = "2026-07-22T22:19:16.351Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/f1e934fb3c98fce859c6147c6785816c7b5b9ab7821115c5d8c4de9842b9/pandas-3.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e2759e890db96dfcffdbd9b86c3c2cb6afaf58def482820317e06163ec1066cd", size = 11414864, upload-time = "2026-07-22T22:19:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/fe/be/d448af7d657d82e1888dd8551f79c6d6fb161080b5b9752d84d910ec2319/pandas-3.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b58b1b39d46a5862e3fb18f50d1a201398619d16a0f9f73f57eea5583cf0e63c", size = 11925105, upload-time = "2026-07-22T22:19:21.515Z" }, + { url = "https://files.pythonhosted.org/packages/29/c1/ccb4238212c8c4f496c584f3044d94e0c030ed8e1d68999db46c91c2242f/pandas-3.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:1c10461f6eeb35d8f05b6184c65c8b9991663b66c46b1d559b682cb34ae7c6ea", size = 10387612, upload-time = "2026-07-22T22:19:24.257Z" }, + { url = "https://files.pythonhosted.org/packages/d2/cf/6a51b2c38980e04c279fd2fa908a1b0982064e860444acfca4ec2e2c8359/pandas-3.0.5-cp314-cp314t-win_arm64.whl", hash = "sha256:3c5015fd1730fbf883647e88068176c839c102cea883ba1769a6f4593bfc1f8c", size = 9509776, upload-time = "2026-07-22T22:19:26.694Z" }, ] [[package]] @@ -2284,6 +4878,277 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, ] +[[package]] +name = "pdf2image" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/d8/b280f01045555dc257b8153c00dee3bc75830f91a744cd5f84ef3a0a64b1/pdf2image-1.17.0.tar.gz", hash = "sha256:eaa959bc116b420dd7ec415fcae49b98100dda3dd18cd2fdfa86d09f112f6d57", size = 12811, upload-time = "2024-01-07T20:33:01.965Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/33/61766ae033518957f877ab246f87ca30a85b778ebaad65b7f74fa7e52988/pdf2image-1.17.0-py3-none-any.whl", hash = "sha256:ecdd58d7afb810dffe21ef2b1bbc057ef434dabbac6c33778a38a3f7744a27e2", size = 11618, upload-time = "2024-01-07T20:32:59.957Z" }, +] + +[[package]] +name = "pdfminer-six" +version = "20260107" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "charset-normalizer" }, + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/a4/5cec1112009f0439a5ca6afa8ace321f0ab2f48da3255b7a1c8953014670/pdfminer_six-20260107.tar.gz", hash = "sha256:96bfd431e3577a55a0efd25676968ca4ce8fd5b53f14565f85716ff363889602", size = 8512094, upload-time = "2026-01-07T13:29:12.937Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/8b/28c4eaec9d6b036a52cb44720408f26b1a143ca9bce76cc19e8f5de00ab4/pdfminer_six-20260107-py3-none-any.whl", hash = "sha256:366585ba97e80dffa8f00cebe303d2f381884d8637af4ce422f1df3ef38111a9", size = 6592252, upload-time = "2026-01-07T13:29:10.742Z" }, +] + +[[package]] +name = "pi-heif" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/a2/70168b601b41bdf5726dfc8dc110eb4052a2e851fed9c9bdae95910e401d/pi_heif-1.4.0.tar.gz", hash = "sha256:e1199d9d41d9ecc877cf3ae7322ff099f6404574f2e62da47590cd4ecb9ec554", size = 17125614, upload-time = "2026-06-10T16:03:53.207Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/62/fdd32400ebe46a357aede2ee691edd06738c7b794345c153b2421a236301/pi_heif-1.4.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:a72f81f7837d298c283e893d4f8805a1eaea9c55df7dd0efe8615a6511bd5add", size = 1063529, upload-time = "2026-06-10T16:02:43.152Z" }, + { url = "https://files.pythonhosted.org/packages/f1/7e/06551546ed9d9169c2b83cd267f11e8cb3813e0223ca34b55be87233b41f/pi_heif-1.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eeea6d0291bb16f674b2de5088630c580a101b90bcca4990a7dca236959cbf9d", size = 959643, upload-time = "2026-06-10T16:02:44.897Z" }, + { url = "https://files.pythonhosted.org/packages/45/c5/8fc0df0452a4cdcd4cb5525708279bb9331f522e2c62d379b3f30bea7720/pi_heif-1.4.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c1d598f0afa3eccff8aa27d9bc4fbb68a39931d6937be57379fb9ee61d5cd8b8", size = 1352607, upload-time = "2026-06-10T16:02:46.39Z" }, + { url = "https://files.pythonhosted.org/packages/56/eb/702e960ea90a2098f9deab023ae20518258478d13349b9e4c1774b4fdb46/pi_heif-1.4.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:97e4048cbddc3d0d7b0c629f02c9d5e4f911b4ae8e7c088a0c315487b972f405", size = 1486573, upload-time = "2026-06-10T16:02:47.718Z" }, + { url = "https://files.pythonhosted.org/packages/e9/91/dbed0d95c1dc1c791e66332a59d49809d514c4558d760aa97f151a50bc52/pi_heif-1.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a8aba0545e1c54a05b8914665f2bda5ae4826e34eb779a3d2a7f613931d1d14d", size = 2335346, upload-time = "2026-06-10T16:02:49.13Z" }, + { url = "https://files.pythonhosted.org/packages/0c/8e/50eb30c18ab03c570fd5792286c65417ac6991db7e931e6181f0f24c33ca/pi_heif-1.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c1acbbc08fe2a85a58c1dd4c2bcc247f37b50f07e6842bc1d7ff6fa2162236fa", size = 2505847, upload-time = "2026-06-10T16:02:50.708Z" }, + { url = "https://files.pythonhosted.org/packages/03/f4/601f0a15a5ab5987f7e65d1cf416e07af1536b5bee5f2061bd66e3aa3744/pi_heif-1.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:fcfb9422c9c9c8f84d6a18ebf256b872556bf4f05697430e790a16bb197a8f26", size = 2383129, upload-time = "2026-06-10T16:02:52.181Z" }, + { url = "https://files.pythonhosted.org/packages/1e/de/9f1414c732781b7ac13a5879bbc9709aa17f97a7f176345ff7d68b62ae8a/pi_heif-1.4.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:b13e807418c60f1b6bda75a2afe9bab4dd1b47b4bf393fa45eff0f3bd884e74a", size = 1063526, upload-time = "2026-06-10T16:02:53.908Z" }, + { url = "https://files.pythonhosted.org/packages/e2/71/d027a0d3102cdf76da416a30754baabc6f94ae1b84dc9c467e572f9808f1/pi_heif-1.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dafa2ea7e7ff594f64fa60e0cd25b3dea9d200277d7aed9fb2b73835fa41fb6d", size = 959643, upload-time = "2026-06-10T16:02:55.278Z" }, + { url = "https://files.pythonhosted.org/packages/06/f0/a6a60e848466c506280fd996864c545ecf90d69a6a21701b543f5f8ee766/pi_heif-1.4.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8789b56a4eb1bda9ec3479e22537f20a3e42e98e145dfc4825ebf5ec945cdd9", size = 1354310, upload-time = "2026-06-10T16:02:56.584Z" }, + { url = "https://files.pythonhosted.org/packages/fb/b7/864ed7b2c1663b004e8bff2fe0cab23710839a636c65d405047b71a33678/pi_heif-1.4.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48554de1080094557f75d090bac1b8004850f11b2be91efe7de9d51d6d620f81", size = 1488131, upload-time = "2026-06-10T16:02:57.935Z" }, + { url = "https://files.pythonhosted.org/packages/88/c9/916e8850733699d21e3779e8614458145f646d4a47d8f671142cffaf1fa1/pi_heif-1.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a4f83306843c80ccd4784cfd73ea78a96a59abdfbb0fc16b2646bd76b8fff438", size = 2336858, upload-time = "2026-06-10T16:02:59.269Z" }, + { url = "https://files.pythonhosted.org/packages/78/b2/a667c3a5f027fdd3673efb212560828ea852527f46b99e850a3edc36076a/pi_heif-1.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:759dc764b45efcb62c1ad26355c7d46be1ed5d5e93e86fe19992ecfeab305e7a", size = 2507428, upload-time = "2026-06-10T16:03:00.932Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/b5047514725f4708d82dd6b1184694e5d32e3fc432558485c540f84da0f1/pi_heif-1.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:9b568e747e31dfc4ca09d5464e8d9475eb5958bf5289767c38280920f6454ba0", size = 2383130, upload-time = "2026-06-10T16:03:02.364Z" }, + { url = "https://files.pythonhosted.org/packages/7b/b4/abfa8d2264144454c1bb18b0339a35bddb1010c078429ced386eab58246e/pi_heif-1.4.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:58320bee5fac8923aa8e09439c7e8778ba4afb146d2cd658be9f4160cb72aec3", size = 1063771, upload-time = "2026-06-10T16:03:03.634Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8e/5367ffae4518b884218d3a49e28edcb0dbd28e71ed67a7eb0a1c82ac4f0c/pi_heif-1.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:04d5b253bc463839beb50ac9fdf9f13a79125ee80346a53030f43962e565f928", size = 959618, upload-time = "2026-06-10T16:03:05.302Z" }, + { url = "https://files.pythonhosted.org/packages/21/ac/96a896a984923bc0de64c6c68e0dcd6f0c2d9f33d5e13f8294fc58b2ea9c/pi_heif-1.4.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4cc3a6a55d31f7961d7bf16103a47704adf33c06209d9f86351caea2084c40a", size = 1352877, upload-time = "2026-06-10T16:03:06.739Z" }, + { url = "https://files.pythonhosted.org/packages/20/49/7d95867e0190e7396a4ee2d7951483d03c02d803f05d22cf90d401a6157d/pi_heif-1.4.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:482bf2d9b26f4e3399a304bb8d14e01aaf0b806f22615e2cc98a0bdbda895d22", size = 1487485, upload-time = "2026-06-10T16:03:08.153Z" }, + { url = "https://files.pythonhosted.org/packages/b4/eb/672a162c58e3a16acbdd8407e4dac8d4f86a945f3bcb61077c99f5204156/pi_heif-1.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4fab02de6b0e445d8e7ab2f232338e242d6b5b54c0bddf12959ede248f176518", size = 2335564, upload-time = "2026-06-10T16:03:09.748Z" }, + { url = "https://files.pythonhosted.org/packages/0e/bf/cd127c7cd1cfe6fcd7c49d203b1384ccda9bd0ae4748cdfd8dafb486cda1/pi_heif-1.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5b58514d1b76fb488e4f78ac4717371c8077f6422298d0fc9a21c2fa72d8907b", size = 2506643, upload-time = "2026-06-10T16:03:11.158Z" }, + { url = "https://files.pythonhosted.org/packages/3b/63/d3b8597da25b0dd62e587a7089ce40dbbaa72449b2680f26220fb6a7688a/pi_heif-1.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:81cb473227b2da35bfd7c69a12764f664d91e44b0ec2547cdfe5ca6b8b52cab9", size = 2383230, upload-time = "2026-06-10T16:03:12.512Z" }, + { url = "https://files.pythonhosted.org/packages/f1/68/3a1d43994865829ae5c2aea40cefa85940b709009c1dd03914a220fd21f9/pi_heif-1.4.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:1c7d6829532ff6ac195e63879b399530de8e693a621d3755581574da7ef74676", size = 1063754, upload-time = "2026-06-10T16:03:13.747Z" }, + { url = "https://files.pythonhosted.org/packages/c5/9e/22a938ed3d56451e42f4a0383fc16cdd04480b16244f4901ec92f1306b4a/pi_heif-1.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c95f5cf99285403698fc48436a05af01d4c38cbedc01949dd4d96f53590fbd33", size = 959625, upload-time = "2026-06-10T16:03:15.04Z" }, + { url = "https://files.pythonhosted.org/packages/77/09/e2f51a7569f5950d52e8d1b140f10bdd62aeac079b7e1af11fa16b269cbf/pi_heif-1.4.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cd041cdf552019737f665fdcdf9c831ef354785a92d930d7f546ba9959a0284", size = 1352920, upload-time = "2026-06-10T16:03:16.247Z" }, + { url = "https://files.pythonhosted.org/packages/50/a8/59742173bc4c9c4831fbea9c4da542b9e28782bea13df9dc9d7902451369/pi_heif-1.4.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fd498fabcdc77159414cccfd966ca48735497fd8c038d1b0135025f3cceac90", size = 1487546, upload-time = "2026-06-10T16:03:17.645Z" }, + { url = "https://files.pythonhosted.org/packages/26/b0/db7ccdda10f788f98fad033b39b332be6fb2c317f151022101be5c271f68/pi_heif-1.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:21a907c3ba6b72ffa97c308aa5678764be82b5510b4f0b67a9d50fdb99e0935e", size = 2335583, upload-time = "2026-06-10T16:03:19.105Z" }, + { url = "https://files.pythonhosted.org/packages/bc/21/f4758d8208f59229a0f9f796485772ed8d21e47d7c10b40e5277245bd1f5/pi_heif-1.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:406584a8f6d1d59f6d401af3d434f3e6a9df5c49681f87023c602f755c647b4e", size = 2506684, upload-time = "2026-06-10T16:03:20.587Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7f/f1c748d8fadfe4bbde5960c83abacef7fdefd7242b16002f37e4e5f54b94/pi_heif-1.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:7692b50e0305170502fdd0657b31818e724f9630629bdbc05a91b10a7979d786", size = 2383227, upload-time = "2026-06-10T16:03:21.954Z" }, + { url = "https://files.pythonhosted.org/packages/35/79/eb28acbc84d7a62121c90d97b4e5672f221d4cc1e856f81ac590d40a1080/pi_heif-1.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:13eceb34f2bfe06b1e59296e958a67cbe864adb10d30e825890e54111b895aa0", size = 1063777, upload-time = "2026-06-10T16:03:23.596Z" }, + { url = "https://files.pythonhosted.org/packages/f5/4a/49ca58461582c03984957d87066ff3f3106e1e97b4893442ca3f2e623e77/pi_heif-1.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3d5396f500e70de167307e0cabcc2b3895d2e623ad4243811f44d578b55c9e1c", size = 959661, upload-time = "2026-06-10T16:03:25.007Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b4/ccc8a00b74af6e7b33a670a5a58b82e9c04be9a95c02246de36dbe02ba8d/pi_heif-1.4.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5ecc06e86a0170ed3782ec27c854b5b334d44d7651d5c0bbff0c123ea44d5f9", size = 1353096, upload-time = "2026-06-10T16:03:26.292Z" }, + { url = "https://files.pythonhosted.org/packages/91/3d/2973bdea79a7886479089f596af4e99a6d980231d848daadb12a5fb92145/pi_heif-1.4.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:298a84c8865a2fa6a795bdd9e532bfa07dd87a6b289caff03b26a0993c06558b", size = 1487574, upload-time = "2026-06-10T16:03:28.009Z" }, + { url = "https://files.pythonhosted.org/packages/59/e4/013c64b9f5c300a8f5d2621ad6c4262e01b7311dccdb3734a9cadc4ebfc7/pi_heif-1.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:939f8eeb63a3897357807766cf0eea3fca3310427ec66a000b86ba1f30af21ab", size = 2335671, upload-time = "2026-06-10T16:03:29.348Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ba/c9873a94211eee70e142d764b9373abc423118e9b03fa8bbbb5406fbedee/pi_heif-1.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:229af3701358ccc3428081586fe4c11d7badc8dbf995aa686cc4f0344c3966dd", size = 2506758, upload-time = "2026-06-10T16:03:30.799Z" }, + { url = "https://files.pythonhosted.org/packages/a6/67/1373582c4bfa3ca2092befd5555fc52d581fc34ad4a5cb0d8401d7f847b4/pi_heif-1.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:743b971b9c0d35be8435702443596e3e9280084e68fc8d5c4de4b31fb12a1417", size = 2471355, upload-time = "2026-06-10T16:03:32.388Z" }, + { url = "https://files.pythonhosted.org/packages/f5/59/e539c90fb7f6c53b1f0f2d265fcd6e2010f99223664add8f5e08874d912f/pi_heif-1.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0da6168084d6c50f6a44538273f58d3aa2ec96ebdc3539efd65ecb98a1bb08e8", size = 1064733, upload-time = "2026-06-10T16:03:33.813Z" }, + { url = "https://files.pythonhosted.org/packages/21/7e/1815b5bcd40597dd9b2600f33e8d28fedeeb33fe0cc8c5f331b36c4c9b7d/pi_heif-1.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55f9b4567b84b72e39fd62982ab780631cad71ebe082b6834ce300fe46fcc389", size = 960460, upload-time = "2026-06-10T16:03:35.339Z" }, + { url = "https://files.pythonhosted.org/packages/d5/14/4c87a33d05b2c39c78bccd86e34dd807403e7e213f2fc1ae54ebb38f99c3/pi_heif-1.4.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0f2512d2e03ea33777c2ca3f1bec33db260bb9dd013ffd350a20fe28452d33f", size = 1358236, upload-time = "2026-06-10T16:03:36.68Z" }, + { url = "https://files.pythonhosted.org/packages/ee/eb/86ea7e4869f38144aab8981c0f1660cd20dc2be92d81ece65c6ce5afd922/pi_heif-1.4.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c791710e3d152637123ad204016fa6154040b1957e95fb1cb234014c423a0a3", size = 1491720, upload-time = "2026-06-10T16:03:38.643Z" }, + { url = "https://files.pythonhosted.org/packages/e1/16/22f0c4729cbf4c4d11100fea73e44b37e3167afc969ef5f77fca267f6c68/pi_heif-1.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:055610c9eceb621f51117ab1900d4a9a519824427428c93a3c546b4e39e72df3", size = 2340766, upload-time = "2026-06-10T16:03:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/1b/41/8935ea651b6c8d89bc1ff60e73f6137e654204c3b4e39f0bc588fded6c27/pi_heif-1.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9400e8f27b13646bcb3bac2fbdcd8903b6f8e03dc269aabb3d852f15245b85ee", size = 2510828, upload-time = "2026-06-10T16:03:41.621Z" }, + { url = "https://files.pythonhosted.org/packages/e7/75/82467e303e3e7eb08966381645bbaf1c5b1f1d21d40baa65dc51c56bb7fb/pi_heif-1.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:04f5648d670e33aef5d0bc6a0f1875efd6093217a826a6d123cbac6022434a70", size = 2471952, upload-time = "2026-06-10T16:03:43.427Z" }, + { url = "https://files.pythonhosted.org/packages/5f/4a/376d99ce769353df9445e76fe8f4072a854695d57da65d67d948c895dfd3/pi_heif-1.4.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:2395973961b19e7d72f31f8283279dd0112c20d892e35417ebe09fb8e54c6f51", size = 1051878, upload-time = "2026-06-10T16:03:44.833Z" }, + { url = "https://files.pythonhosted.org/packages/50/59/25f92e8bf833c67f90edb80734fdc50a71becde90d877d0d6f822883222a/pi_heif-1.4.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:2d1577df3172de68ec095cd9b7fc065f396093473f5826a6a82e07a7d26af2af", size = 956097, upload-time = "2026-06-10T16:03:46.206Z" }, + { url = "https://files.pythonhosted.org/packages/9c/22/a4ae6a69a89ce499dcfa26f306bf3de1802bd0e5231311c25ab05f4074c6/pi_heif-1.4.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c5fe6ca4b5b5e6c9d2d645d30c8cbd9a4fc4087aeb8b45f2872729a5f568b18", size = 1312163, upload-time = "2026-06-10T16:03:47.674Z" }, + { url = "https://files.pythonhosted.org/packages/14/0e/a9fac916c201aff86e51d1bb6ee1565334fe43fef18acedf3228af48a921/pi_heif-1.4.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13beb46287e190fbaeedcf157a2be6f9ac56a9e91d7a7a7de9264ea2f195643f", size = 1442979, upload-time = "2026-06-10T16:03:48.988Z" }, + { url = "https://files.pythonhosted.org/packages/c0/97/c9e13f864dd2647dc2a231bdb7fd4c8e78362eb9801f042feb0f2356b50b/pi_heif-1.4.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:43241a5fb8a34bd6c822f47022e9b53276db7f42379074e281da2053ca0eca0c", size = 2383479, upload-time = "2026-06-10T16:03:50.477Z" }, +] + +[[package]] +name = "pikepdf" +version = "10.13.0.post1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lxml" }, + { name = "packaging" }, + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/0e/6e74dd213537b71c945743a4b3112dbb430896ad68b8a6ad22e4468455d4/pikepdf-10.13.0.post1.tar.gz", hash = "sha256:4b73f926ebae81f04bf14527af330bd00bb268be767e0f189f7c4c3e4ad7ae0a", size = 4973186, upload-time = "2026-09-05T06:49:20.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/43/2810fb8416c876a555affdd34d7daedb5dcd2c2a31e49765c0fa12fadf8a/pikepdf-10.13.0.post1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:94e04ed92fe42b8bcebf8c40462868dca4eb92581dcc2be1f0c4b8ee23347386", size = 1846099, upload-time = "2026-09-05T06:47:58.132Z" }, + { url = "https://files.pythonhosted.org/packages/f2/97/a4ceda983aed695ced6b6015eee950277e9fbf603509e94d6726bc1c53e9/pikepdf-10.13.0.post1-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:c0b5127df90b0164dd846bddd0ed542326b2f69bd11f627afe48762cf16c2904", size = 1943284, upload-time = "2026-09-05T06:48:00.552Z" }, + { url = "https://files.pythonhosted.org/packages/85/b1/57506fd18c0266dda42440b5b6ba44967078c4365e5426c5d67ff14f0ffc/pikepdf-10.13.0.post1-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:43d70f244a4a1120a11cfe2227f8c03249fac6a7e3789a3116173102a3b9fe37", size = 2104990, upload-time = "2026-09-05T06:48:02.207Z" }, + { url = "https://files.pythonhosted.org/packages/cb/37/9e5b6eb29c4484a0e69d627a680512a03e2ef198c1023319ce5d202f7746/pikepdf-10.13.0.post1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0efb4faed9cbb59c1486f668af326096dac1ff0ca058eb48ec485742a9a655af", size = 2308162, upload-time = "2026-09-05T06:48:04.261Z" }, + { url = "https://files.pythonhosted.org/packages/2e/5e/9061d9d440900c1721dca35e8c5c88f2e046eec7529eace3e399f6a6a01c/pikepdf-10.13.0.post1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e19bab4320e4e8771b7816f7319ba37530fd28a40372840b75cab394ebf868e4", size = 3742316, upload-time = "2026-09-05T06:48:06.149Z" }, + { url = "https://files.pythonhosted.org/packages/da/d2/32eb6099f5376ce1d1da8c5e4ecb6edb8448ddf58e8690a34fc45946a7e6/pikepdf-10.13.0.post1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:90f17cb174db88e08075c5bfa3c619df00dd84abbad34bfa1edf84863f0b01a7", size = 3952808, upload-time = "2026-09-05T06:48:07.72Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a5/4b5f9160920266788b3a3984b05bfab79be780805d6e323e5266a7685587/pikepdf-10.13.0.post1-cp310-cp310-win_amd64.whl", hash = "sha256:a3e9104db5ea5b5a7c5fde417147900966a687de39ef91a5ea103fd4b773aee1", size = 3405581, upload-time = "2026-09-05T06:48:09.748Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c1/48c9c0ed2ed88ca5d9cdd7f16075385a05a812d781b61bfa7f2d5182b247/pikepdf-10.13.0.post1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:98a7305e330f797da02b543d3ad57a134c4a14c6ec6f8d86d91aa9dd130c425b", size = 1846300, upload-time = "2026-09-05T06:48:11.563Z" }, + { url = "https://files.pythonhosted.org/packages/e9/20/484a3a61664132dc8c4bd97e0b8291fa79f9f4a3b1e1ffd5b67ac41ed98a/pikepdf-10.13.0.post1-cp311-cp311-macosx_15_0_x86_64.whl", hash = "sha256:f3dedd02795626f17ee42d5c02ec4ec94e28aa47046ef430d4478454a8fbd07f", size = 1944179, upload-time = "2026-09-05T06:48:14.01Z" }, + { url = "https://files.pythonhosted.org/packages/66/38/797df7d60352fc5ec3943c425acaa15d032cd2e673b516861f449536db57/pikepdf-10.13.0.post1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7ba09ef5a5f26e38ee558d2a08223fee08a5ef1868962ae2d8590d4de3c8c92f", size = 2105054, upload-time = "2026-09-05T06:48:15.547Z" }, + { url = "https://files.pythonhosted.org/packages/a8/8a/1f003558c5c05cecf182af775839ad674fe23e06b314d0219b9d8422680a/pikepdf-10.13.0.post1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:365b94f2be7e2857c6cb5445b56dc52dc7417ba9f06c8a4282d9f521cb2d0fb8", size = 2308617, upload-time = "2026-09-05T06:48:17.216Z" }, + { url = "https://files.pythonhosted.org/packages/55/5b/0e7193ee8c7ca5b15f478918033a0fa78917b01249e1d4a4a65754644cd3/pikepdf-10.13.0.post1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f515a31c76cce043bbb7b781e77a343a4e26fa8f520ba337d30ddea0f7a0ce50", size = 3742337, upload-time = "2026-09-05T06:48:18.971Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f5/519e8728c04d05dcbd44d03b266a3f6acf8de3a0a6ed5ec0fa39ececddd7/pikepdf-10.13.0.post1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b63577c44fedf7ed6b971076f7c7ed0ff95a8bac627ac93b79e8b542214861a7", size = 3952988, upload-time = "2026-09-05T06:48:20.984Z" }, + { url = "https://files.pythonhosted.org/packages/f1/81/55bc65ae623941323c569e22b9c8d603e9c9f2f913eee49f729e8ad0e386/pikepdf-10.13.0.post1-cp311-cp311-win_amd64.whl", hash = "sha256:4bb5fe2090d246ad4b325d17f33a186f60f6763bba3d4ac3c4b863c8890e913a", size = 3406086, upload-time = "2026-09-05T06:48:22.73Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a5/598e72c72ed46046e297f15763dffda88424870724a4a22f599b815cb774/pikepdf-10.13.0.post1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2c6e83f8a1828ec79cdec4df8cc07209eaf10ed7e4f5a90a7356b254bacc07d5", size = 1845515, upload-time = "2026-09-05T06:48:24.477Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b3/29691a5e9ee915357c081730d8cc02f35f19b4155857556fbe562a4d83ba/pikepdf-10.13.0.post1-cp312-cp312-macosx_15_0_x86_64.whl", hash = "sha256:f2463f650efab46905b9e279f5c776faf96c65bb45c1acf9f2de0e8a6eec5fb7", size = 1944755, upload-time = "2026-09-05T06:48:26.332Z" }, + { url = "https://files.pythonhosted.org/packages/04/4e/201f553b9405424d7aefa3be997f0a1c787ac3a089a844f1fc42f412fe0e/pikepdf-10.13.0.post1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9613505f5b22203465d4224a3fd8cf69876ce8278442c6478ac6849f54724a30", size = 2102202, upload-time = "2026-09-05T06:48:28.816Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a3/bd7e7b321e8bfe4b8530da57d12c557a259bbd4b40e739960a1f2ea507cb/pikepdf-10.13.0.post1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f80ca046d984752cf6f08093debc193884bee91c01c104aa0236767711a20f", size = 2307194, upload-time = "2026-09-05T06:48:31.558Z" }, + { url = "https://files.pythonhosted.org/packages/23/b0/ca630f56015dfc6c4c8c81fdeb5f5099e7fca354f54a41dfe7f1382a5316/pikepdf-10.13.0.post1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1f76fcbe5d86f2ae6f231ba542cd04793d4c89e75bd5f62526b7412926ff2100", size = 3740157, upload-time = "2026-09-05T06:48:33.285Z" }, + { url = "https://files.pythonhosted.org/packages/be/48/7a84adc2fd14ec35e4b3d007575914de1ce0518e8c69b35ebee62fa2b142/pikepdf-10.13.0.post1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:092a9bf15739e931ecab15ec3baee5d9629dad90ea4e42b779f6b439d2d1e462", size = 3951601, upload-time = "2026-09-05T06:48:35.468Z" }, + { url = "https://files.pythonhosted.org/packages/87/83/11b16b2a235ddc3b1104a66b66f0324896f189c35d8e85ad9897784abc80/pikepdf-10.13.0.post1-cp312-cp312-win_amd64.whl", hash = "sha256:996a714a47cc725e3c48fe3901691d3353f3c2eeb9e0571aa2b16164abc40ff9", size = 3407846, upload-time = "2026-09-05T06:48:37.625Z" }, + { url = "https://files.pythonhosted.org/packages/10/f4/3636368760840cbc3ee512330024dd6f518d583c1bbbb1b551ca8e18f5e8/pikepdf-10.13.0.post1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:87141ada970386ff6640db54f0bda734d3bde7960d3ba04a76768b48e25028ca", size = 1845524, upload-time = "2026-09-05T06:48:39.309Z" }, + { url = "https://files.pythonhosted.org/packages/ce/dc/7bbfba253a0394a237b81be371a64f904f99636d579ec78ef5d92025fd2d/pikepdf-10.13.0.post1-cp313-cp313-macosx_15_0_x86_64.whl", hash = "sha256:55e53b4d8a4b1700f686f76e3a68411e421e962a4c8b1b90d00aab3f3e494a55", size = 1944829, upload-time = "2026-09-05T06:48:41.601Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a3/10367bfb93501a151cbb96b6973f7c049fef04040aea35cf6cedf579c3e4/pikepdf-10.13.0.post1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8fb8f82dc43056a4b4f891e78ee1db4e3ced75ba3e87b836f8e28c8771228928", size = 2102269, upload-time = "2026-09-05T06:48:43.222Z" }, + { url = "https://files.pythonhosted.org/packages/2d/bd/a68b5d9b4aef4d4b9c374cfdfd941623f52303d31adea13554568df42abe/pikepdf-10.13.0.post1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3b58ccb30b93ba400e6a6a83315b4830eea49f6f19e18d78241fe6b1c49fec2", size = 2307160, upload-time = "2026-09-05T06:48:45.192Z" }, + { url = "https://files.pythonhosted.org/packages/9a/59/47bd86d9e338d301c28416d52d0e154a0d6322cf6b957c9df5f3d7828aa2/pikepdf-10.13.0.post1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6b038cd5bcbb6c1952bcc271695eaf24c4199d72e45606ee5e467f837760820e", size = 3739722, upload-time = "2026-09-05T06:48:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/49/40/87fb6dddc9dce110429c72449e942174fd500f6fc4c9ff518a6b73057aa7/pikepdf-10.13.0.post1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b7b0cbb135de32ec3f41651a08ab294e3c18ae9fec32516a48d27e64a53a47f0", size = 3951549, upload-time = "2026-09-05T06:48:49.263Z" }, + { url = "https://files.pythonhosted.org/packages/8b/4c/534f23c8c196b1a7dc8672e503743e2a4766215125db474b261e33f37a9d/pikepdf-10.13.0.post1-cp313-cp313-win_amd64.whl", hash = "sha256:20c76343128ec41d5be58337b23c6f9f620fa7b8256a2d942460a48c07bbfe7b", size = 3407945, upload-time = "2026-09-05T06:48:51.543Z" }, + { url = "https://files.pythonhosted.org/packages/14/0e/86897bf5325824c1f2d9d8be89839baf11011b5392d92620cb0273fb9af5/pikepdf-10.13.0.post1-cp314-abi3-macosx_14_0_arm64.whl", hash = "sha256:51fae4a4a3c6549aa4c405896ff7010f3e43e0c4f407c0bcee071ef13d271202", size = 1845245, upload-time = "2026-09-05T06:48:53.575Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ed/923846b7511627f8564d09345e083f14674dfb193de92d27f1cc4650602e/pikepdf-10.13.0.post1-cp314-abi3-macosx_15_0_x86_64.whl", hash = "sha256:8cb976331cb8b03ec3465e06d9e7a3eadbadb7e622be888e70f918ac732a105e", size = 1943659, upload-time = "2026-09-05T06:48:55.823Z" }, + { url = "https://files.pythonhosted.org/packages/58/bb/fcb09ad4bd227bbb37a7e5b24de86f9ce9d462aa0c7ee18781899bfa378a/pikepdf-10.13.0.post1-cp314-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3e18d5a009bbe5f3ab18f916fb9e28f0c5b0d920736e3a85cc10c627ec633596", size = 2099423, upload-time = "2026-09-05T06:48:57.671Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c9/707f9ba96727fa366a650237e46f0e20a73b244592eb6b97a49e401e2b43/pikepdf-10.13.0.post1-cp314-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:414f42c83e5e6029870de1a988625dafc95781ebff10e15d82caeb5e69a83c9c", size = 2303539, upload-time = "2026-09-05T06:48:59.506Z" }, + { url = "https://files.pythonhosted.org/packages/22/15/79a2ccc354514a1321a161867a011fc917be158fc01a88da8c78ad18399d/pikepdf-10.13.0.post1-cp314-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f0eb89f06cad9231b9db54d81a22592b03b63924824a6b850febd2b75daa6546", size = 3737772, upload-time = "2026-09-05T06:49:01.245Z" }, + { url = "https://files.pythonhosted.org/packages/6e/85/a17440c2de64da71dc012b42e644b30ee4d98eb540c14d4e9f3538b73a9e/pikepdf-10.13.0.post1-cp314-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:544f1be1b1e5630a79cd182a8663c439504099eb9eae0d342a50173d9825bdf3", size = 3948342, upload-time = "2026-09-05T06:49:03.27Z" }, + { url = "https://files.pythonhosted.org/packages/27/ed/c64024ce23f0f3149217679f31dcf51f42c687d35d597894342e93321f6e/pikepdf-10.13.0.post1-cp314-abi3-win_amd64.whl", hash = "sha256:b69b89577b50617248185b6ad73cda0e4f15c57da11f7da8cc4032bf0d7d9ebe", size = 3500024, upload-time = "2026-09-05T06:49:05.259Z" }, + { url = "https://files.pythonhosted.org/packages/e9/65/15a796a3cf9fb17d41acc1ab6719e7d3dcdf1260e76909d0dd32ecc97ba7/pikepdf-10.13.0.post1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:ef4ed47d40aa44deb063feb4a88e8bcf1c8fa0183ce526dc4295f7cbdb1292f8", size = 1853638, upload-time = "2026-09-05T06:49:07.184Z" }, + { url = "https://files.pythonhosted.org/packages/aa/f3/5d49a511fd13b59b94c5ad673695d331fce5d2846ab1501646c2a3b35b5f/pikepdf-10.13.0.post1-cp314-cp314t-macosx_15_0_x86_64.whl", hash = "sha256:571efcd1d54e0dd817973c76c253feb6fb758c93bb0c16a893cc68f2a178d404", size = 1951768, upload-time = "2026-09-05T06:49:09.048Z" }, + { url = "https://files.pythonhosted.org/packages/06/de/f6bbd9653695f6e2ed3494a439f11f3a89450c004fe9bf8ee583201ea759/pikepdf-10.13.0.post1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2db9a18074ba112e7c517e8c21dfd8894cc13b8a37ccf49a5841192170fb68eb", size = 2107137, upload-time = "2026-09-05T06:49:10.788Z" }, + { url = "https://files.pythonhosted.org/packages/c0/aa/43b355681f05ea0b5808a26cba9e8e764686fa8dcebe0875db612664de40/pikepdf-10.13.0.post1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7962a75cf22d0d683b49ab19b8966e94dc7014d9aa6806f3c0d2b37fb9ae607", size = 2310991, upload-time = "2026-09-05T06:49:12.455Z" }, + { url = "https://files.pythonhosted.org/packages/a9/46/77574e9c4bded01afd7a3fe538f5432e396c3772c9bc1eab5d287aed00df/pikepdf-10.13.0.post1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b948e11f7dd3710f939194f00b4d75b0df87a30d53b31414128768ac25da77d8", size = 3744358, upload-time = "2026-09-05T06:49:14.428Z" }, + { url = "https://files.pythonhosted.org/packages/89/a8/4857df72cf4773553c2e6a82f93ee5e98c02f1c4e4877379e84d27384982/pikepdf-10.13.0.post1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:99f6afccd6119233e7133bd4c2ade48461de3ddd4269cc28a4f90cdf7c1372f5", size = 3955964, upload-time = "2026-09-05T06:49:16.656Z" }, + { url = "https://files.pythonhosted.org/packages/48/33/7fc9d6fcdbbcc419792eab6a4b1420bbe484b7c41d256a94582bb80ddade/pikepdf-10.13.0.post1-cp314-cp314t-win_amd64.whl", hash = "sha256:fb05fb7b42d85754219b55111aa61beff4e48da56069e971de1e5aca380c0ac1", size = 3532156, upload-time = "2026-09-05T06:49:18.742Z" }, +] + +[[package]] +name = "pillow" +version = "12.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/c2/669d88644cddb1485bd9534e63e8cf476c8e51cb3c3a1297677023505c0e/pillow-12.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6c0016e7b354317c4e9e525b937ac8596c38d2d232b419529b9cd7a1cd46e39a", size = 5392418, upload-time = "2026-07-01T11:53:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ba/3762f376a2948e3036488d773a146e0ae6ecc2ca03ac20e2615bd0b2ba02/pillow-12.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bcc33feacfaefce60c12fd500a277533bdc02b10a19f7f6d348763d8140bbba7", size = 4785287, upload-time = "2026-07-01T11:53:29.761Z" }, + { url = "https://files.pythonhosted.org/packages/07/50/b5d688cc9c52d4482f3d5bcab6ce20bc2a74a85d2343841c907444a3be2c/pillow-12.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5594fc43d548a7ed94949d139aa1341b270f1863f11cfd37f5a6c8b778a6b67f", size = 6253754, upload-time = "2026-07-01T11:53:32.298Z" }, + { url = "https://files.pythonhosted.org/packages/4e/89/36f4cd76cf4baf05c50ababb976249153f18c959171c7f6ba09a6f217260/pillow-12.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0606c8bf2cdefea14a43530f7657cbbb7ecf1c4222512492ef4a4434a9501ec", size = 6925605, upload-time = "2026-07-01T11:53:34.487Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c0/4de58cf6633b9e3a6061ef4be6fb91fc3c90b812ece886f531e3c523d777/pillow-12.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:85f998ea1848bc6757289e739cfbdda3a04adfd58b02fc018ce54d754a5ce468", size = 6327788, upload-time = "2026-07-01T11:53:36.433Z" }, + { url = "https://files.pythonhosted.org/packages/87/3c/14d53682a19550dbbaf3b598f807d5457646c510805a44c7d7891cd1cd1a/pillow-12.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:25b9b82bb22e6e2b3cd07b39c68b7b862001226cb3dff7130d1cb914121b39ed", size = 7036288, upload-time = "2026-07-01T11:53:38.712Z" }, + { url = "https://files.pythonhosted.org/packages/38/1d/36279e3c77efe034e4cc2b0393ee74ffdb5a62391dacbf9b916154f5f0b8/pillow-12.3.0-cp310-cp310-win32.whl", hash = "sha256:37dc8f7bbb66efe481bb60defacef820c950c24713fb44962ed6aa2a50966de1", size = 6472396, upload-time = "2026-07-01T11:53:40.781Z" }, + { url = "https://files.pythonhosted.org/packages/48/7c/8fa0039574c476d7c6fa57dd7c32a130436877c6ec1e5ce1cc8ec44878c1/pillow-12.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:300557495eb45ebb8aec96c2da9c4be642fbf7cd937278b4013ba894ea8eb0eb", size = 7226887, upload-time = "2026-07-01T11:53:42.764Z" }, + { url = "https://files.pythonhosted.org/packages/fa/17/e324be141d173c1c919428066c3259f21c1b8982e564e01a4a81e96dbdcf/pillow-12.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:514435a37670e3e5e08f3945b68718b6ed329bb84367777e16f9f4dfe1e61a0f", size = 2568039, upload-time = "2026-07-01T11:53:45.372Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c8/0a78b0e02d7ac54bc03e5321c9220da52f0c2ea83b21f7c40e7f3169c502/pillow-12.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756", size = 5392415, upload-time = "2026-07-01T11:53:47.162Z" }, + { url = "https://files.pythonhosted.org/packages/b2/5b/a02d30018abd97ced9f5a6c63d28597694a00d066516b9c1c6de45859fc9/pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6", size = 4785266, upload-time = "2026-07-01T11:53:49.079Z" }, + { url = "https://files.pythonhosted.org/packages/c8/98/766667a4be768150a202836acd9fad19c06824ca86c4286d3cf6b274964e/pillow-12.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd", size = 6263814, upload-time = "2026-07-01T11:53:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2d/ede717bc1144f63886c21fd349bb95860b0d1a21149ff16f2bb362b612b6/pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd", size = 6934408, upload-time = "2026-07-01T11:53:53.487Z" }, + { url = "https://files.pythonhosted.org/packages/a3/48/9c58b685e69d49c31af6c8eb9012055fab7e665785165c84796e2c73ce72/pillow-12.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c", size = 6337160, upload-time = "2026-07-01T11:53:55.457Z" }, + { url = "https://files.pythonhosted.org/packages/ff/fa/dc2a5c0ba6df93f67c31d34b808b7ce440b40cdbf96f0b81cde1d1e6fa93/pillow-12.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5", size = 7045172, upload-time = "2026-07-01T11:53:57.736Z" }, + { url = "https://files.pythonhosted.org/packages/86/a5/444817a4d4c4c2417df00513086ca196f388d8f9ef40c2e4ccd1ad1af54b/pillow-12.3.0-cp311-cp311-win32.whl", hash = "sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b", size = 6472232, upload-time = "2026-07-01T11:53:59.767Z" }, + { url = "https://files.pythonhosted.org/packages/63/c6/4bad1b18d132a50b27e1365e1ab163616f7a5bb56d330f66f9d1d9d4f9d4/pillow-12.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a", size = 7233653, upload-time = "2026-07-01T11:54:02.066Z" }, + { url = "https://files.pythonhosted.org/packages/fd/16/00f91ab7760dc842f5aad55217e80fc4a7067a0604535249bc8a2d6d9870/pillow-12.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26", size = 2568195, upload-time = "2026-07-01T11:54:04.622Z" }, + { url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" }, + { url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" }, + { url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" }, + { url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" }, + { url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" }, + { url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" }, + { url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" }, + { url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" }, + { url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684, upload-time = "2026-07-01T11:54:25.934Z" }, + { url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487, upload-time = "2026-07-01T11:54:27.935Z" }, + { url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433, upload-time = "2026-07-01T11:54:29.813Z" }, + { url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889, upload-time = "2026-07-01T11:54:31.97Z" }, + { url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109, upload-time = "2026-07-01T11:54:34.026Z" }, + { url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736, upload-time = "2026-07-01T11:54:36.131Z" }, + { url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129, upload-time = "2026-07-01T11:54:38.216Z" }, + { url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562, upload-time = "2026-07-01T11:54:40.354Z" }, + { url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439, upload-time = "2026-07-01T11:54:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287, upload-time = "2026-07-01T11:54:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691, upload-time = "2026-07-01T11:54:47.141Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185, upload-time = "2026-07-01T11:54:49.137Z" }, + { url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736, upload-time = "2026-07-01T11:54:51.156Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435, upload-time = "2026-07-01T11:54:53.414Z" }, + { url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262, upload-time = "2026-07-01T11:54:55.739Z" }, + { url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344, upload-time = "2026-07-01T11:54:57.657Z" }, + { url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131, upload-time = "2026-07-01T11:54:59.713Z" }, + { url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757, upload-time = "2026-07-01T11:55:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962, upload-time = "2026-07-01T11:55:03.93Z" }, + { url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171, upload-time = "2026-07-01T11:55:05.989Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116, upload-time = "2026-07-01T11:55:08.131Z" }, + { url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209, upload-time = "2026-07-01T11:55:10.408Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707, upload-time = "2026-07-01T11:55:12.745Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995, upload-time = "2026-07-01T11:55:14.736Z" }, + { url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503, upload-time = "2026-07-01T11:55:17.076Z" }, + { url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956, upload-time = "2026-07-01T11:55:19.448Z" }, + { url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855, upload-time = "2026-07-01T11:55:21.613Z" }, + { url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642, upload-time = "2026-07-01T11:55:24.006Z" }, + { url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281, upload-time = "2026-07-01T11:55:26.252Z" }, + { url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716, upload-time = "2026-07-01T11:55:28.318Z" }, + { url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" }, + { url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" }, + { url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" }, + { url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063, upload-time = "2026-07-01T11:55:37.941Z" }, + { url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549, upload-time = "2026-07-01T11:55:40.022Z" }, + { url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331, upload-time = "2026-07-01T11:55:41.98Z" }, + { url = "https://files.pythonhosted.org/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370, upload-time = "2026-07-01T11:55:44.028Z" }, + { url = "https://files.pythonhosted.org/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147, upload-time = "2026-07-01T11:55:46.073Z" }, + { url = "https://files.pythonhosted.org/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659, upload-time = "2026-07-01T11:55:48.264Z" }, + { url = "https://files.pythonhosted.org/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439, upload-time = "2026-07-01T11:55:50.503Z" }, + { url = "https://files.pythonhosted.org/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577, upload-time = "2026-07-01T11:55:52.697Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394, upload-time = "2026-07-01T11:55:55.149Z" }, + { url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375, upload-time = "2026-07-01T11:55:57.769Z" }, + { url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048, upload-time = "2026-07-01T11:55:59.975Z" }, + { url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006, upload-time = "2026-07-01T11:56:02.143Z" }, + { url = "https://files.pythonhosted.org/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509, upload-time = "2026-07-01T11:56:04.2Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167, upload-time = "2026-07-01T11:56:06.631Z" }, + { url = "https://files.pythonhosted.org/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237, upload-time = "2026-07-01T11:56:08.868Z" }, + { url = "https://files.pythonhosted.org/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047, upload-time = "2026-07-01T11:56:11.379Z" }, + { url = "https://files.pythonhosted.org/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440, upload-time = "2026-07-01T11:56:13.908Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895, upload-time = "2026-07-01T11:56:16.575Z" }, + { url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384, upload-time = "2026-07-01T11:56:18.855Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537, upload-time = "2026-07-01T11:56:21.214Z" }, + { url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491, upload-time = "2026-07-01T11:56:23.506Z" }, + { url = "https://files.pythonhosted.org/packages/75/18/2e8b40223153ccbc60df07f9e8928dc0c76202aa4e55ae9f53962b6510d6/pillow-12.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468", size = 5302510, upload-time = "2026-07-01T11:56:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/46/3e/51fabf59d5ab801ceab709453d3ab6b180083496579549de4c45ced6528a/pillow-12.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94", size = 4736058, upload-time = "2026-07-01T11:56:28.041Z" }, + { url = "https://files.pythonhosted.org/packages/bf/20/22fe9384b7949e25fb1293bcfc84fb82590ff4ea6b37c95b24d26d793d86/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e", size = 5237776, upload-time = "2026-07-01T11:56:30.263Z" }, + { url = "https://files.pythonhosted.org/packages/08/14/f6ba68107680ffa74b39985f3f30884e41318fbc4250caa423c79b4788bb/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3", size = 5860358, upload-time = "2026-07-01T11:56:32.68Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0169bc772ec491108b62f644f8ecf1fe5d8ae5ebafde2ee2142210166903/pillow-12.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a", size = 7231786, upload-time = "2026-07-01T11:56:35.046Z" }, +] + +[[package]] +name = "pinecone" +version = "7.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "pinecone-plugin-assistant" }, + { name = "pinecone-plugin-interface" }, + { name = "python-dateutil" }, + { name = "typing-extensions" }, + { name = "urllib3", marker = "python_full_version < '4'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fa/38/12731d4af470851b4963eba616605868a8599ef4df51c7b6c928e5f3166d/pinecone-7.3.0.tar.gz", hash = "sha256:307edc155621d487c20dc71b76c3ad5d6f799569ba42064190d03917954f9a7b", size = 235256, upload-time = "2025-06-27T20:03:51.498Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/a6/c5d54a5fb1de3983a8739c1a1660e7a7074db2cbadfa875b823fcf29b629/pinecone-7.3.0-py3-none-any.whl", hash = "sha256:315b8fef20320bef723ecbb695dec0aafa75d8434d86e01e5a0e85933e1009a8", size = 587563, upload-time = "2025-06-27T20:03:50.249Z" }, +] + +[[package]] +name = "pinecone-plugin-assistant" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/01/65c4c3a81732fa379f8e7f78a8c18aa57a1139f5b79d58b93a69f2fc8cb0/pinecone_plugin_assistant-1.8.0.tar.gz", hash = "sha256:8e8682cff30f9bae9243b384021aba71c91f4e6ef1650e9d63ee64aab83cba87", size = 150435, upload-time = "2025-08-31T14:31:18.046Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/49/62ab8e2f9098bf8593e36bbe6e729fcc0500bafca7d88be7b62eac66c8b0/pinecone_plugin_assistant-1.8.0-py3-none-any.whl", hash = "sha256:71ae42c3b4478d23138cbc4fe3505db561319a826f5aff4ef2e306a25ac56686", size = 259281, upload-time = "2025-08-31T14:31:16.587Z" }, +] + +[[package]] +name = "pinecone-plugin-interface" +version = "0.0.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/fb/e8a4063264953ead9e2b24d9b390152c60f042c951c47f4592e9996e57ff/pinecone_plugin_interface-0.0.7.tar.gz", hash = "sha256:b8e6675e41847333aa13923cc44daa3f85676d7157324682dc1640588a982846", size = 3370, upload-time = "2024-06-05T01:57:52.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/1d/a21fdfcd6d022cb64cef5c2a29ee6691c6c103c4566b41646b080b7536a5/pinecone_plugin_interface-0.0.7-py3-none-any.whl", hash = "sha256:875857ad9c9fc8bbc074dbe780d187a2afd21f5bfe0f3b08601924a61ef1bba8", size = 6249, upload-time = "2024-06-05T01:57:50.583Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -2293,6 +5158,66 @@ 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 = "preshed" +version = "3.0.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cymem", marker = "python_full_version >= '3.11'" }, + { name = "murmurhash", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/75/fe6b7bbd0dea530a001b0e24c331b21a0be2786e402abf3c57f5dce43d4b/preshed-3.0.13.tar.gz", hash = "sha256:d75f718bbfd97e992f7827e0fa7faf6a91bdd9c922d5baa4b50d62731396cb89", size = 18338, upload-time = "2026-03-23T08:57:31.378Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/7e/d55d8cdeefa78995eec15a11ae16cbd0581a0be2342527a64251fd948cef/preshed-3.0.13-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:42c58b07e8b431e33d0ad9922e896632453821cad8b09171b619b8c61101916f", size = 136920, upload-time = "2026-03-23T08:56:10.829Z" }, + { url = "https://files.pythonhosted.org/packages/10/bc/ee1f388a97c613e656d774b522b4ddc1cd32e984ca4eb1157c5d822e9011/preshed-3.0.13-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a06e27f4e5b9d7943840087828c6a0dae4a3475576d12c2e95b71abbb325a80b", size = 137576, upload-time = "2026-03-23T08:56:12.441Z" }, + { url = "https://files.pythonhosted.org/packages/a6/dd/24c5a576035df4043998e1069718dd7369e107ce9d169df2333d00461dbf/preshed-3.0.13-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8b82d7a7bb63d248a6cbbfcabb4a570c993d54d964e39dc5d85c14018ba2079e", size = 780270, upload-time = "2026-03-23T08:56:14.108Z" }, + { url = "https://files.pythonhosted.org/packages/e5/ab/fb0f6808fffad96c962ce254587cae2bb7df0fda3e6d6b481ce4f60f6c2d/preshed-3.0.13-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bef84b225d226af43adfee78ce5ddede72a6155ce5292c1a41dcd1f0b9c87c30", size = 779722, upload-time = "2026-03-23T08:56:15.721Z" }, + { url = "https://files.pythonhosted.org/packages/bf/7f/c9948dde95bf965c6af2c31f0dbbc6c7e5433b5de1c85f20644edf38c78c/preshed-3.0.13-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:70d502e081348df207d90f347f21770ed596822bb04eb3c3b32b7281579e90c6", size = 1775435, upload-time = "2026-03-23T08:56:17.655Z" }, + { url = "https://files.pythonhosted.org/packages/d2/57/8db29ac57b981ef19d1078001aa6c2055a3eed46998c1c93f3d1fdb86106/preshed-3.0.13-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:985cb9b097beda76cd13c01a0499707103e8915f888fa30f8aa8324ef2cc6b08", size = 1842612, upload-time = "2026-03-23T08:56:19.755Z" }, + { url = "https://files.pythonhosted.org/packages/38/e5/ead05efc423be237fba76a3bf0eeb492e5d3801504c096c3552c517f2f72/preshed-3.0.13-cp310-cp310-win_amd64.whl", hash = "sha256:867aa73abbf4ee3b4d7662148091c33a8c039271269e3a7f1e0ca995f91995c8", size = 121951, upload-time = "2026-03-23T08:56:21.138Z" }, + { url = "https://files.pythonhosted.org/packages/aa/80/9cf7f7c208046c97d4b2765f89545a6ea8cfefbd87f0141dde61e6f098ac/preshed-3.0.13-cp310-cp310-win_arm64.whl", hash = "sha256:2b704e46cb7b88f656ef16a3e5347b36525a1c53721d327a4ba1457404101f85", size = 109604, upload-time = "2026-03-23T08:56:22.537Z" }, + { url = "https://files.pythonhosted.org/packages/7c/d1/7bc39738388b38ff48cecbb326a9b2bb3f422bb32097be92e010f3162395/preshed-3.0.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5268c0e6fa96f50cdf87f516c2d4b32563c12706ee768e75c00e8d0098acd545", size = 136718, upload-time = "2026-03-23T08:56:23.889Z" }, + { url = "https://files.pythonhosted.org/packages/f6/65/de465b6801740140c2b5d2db6c312ca7937dcfd0442f1ae7d50dee529544/preshed-3.0.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:df642547a1a94079978a0ea8f4593ab4b8d3bd43f767bef0ef64d9a214f8c4c9", size = 137261, upload-time = "2026-03-23T08:56:25.303Z" }, + { url = "https://files.pythonhosted.org/packages/89/83/478ee078746a4a413c841542caebd2ea74b659475b8bf5f2e3724b6fe655/preshed-3.0.13-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09397592d333a77f88454e72b7f1f941b2afaf040b392b9e74898dbc4648cdf5", size = 821010, upload-time = "2026-03-23T08:56:26.455Z" }, + { url = "https://files.pythonhosted.org/packages/ee/2e/1ac761e973966893cd3a0ad3256360365276e2d1e779e351448981a1156a/preshed-3.0.13-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f8e6fe0620ed0f96a246d46447055c447e071cd8222731a045c235e8a758c918", size = 823096, upload-time = "2026-03-23T08:56:28.126Z" }, + { url = "https://files.pythonhosted.org/packages/3c/51/7824cfd85dd7fe547888de20228ebd87d9acd3708206d30b82211e382d23/preshed-3.0.13-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:502f93f49a22788203f02d3067d4ea077a0cca3864de6a792eae12e7ce589e14", size = 1812148, upload-time = "2026-03-23T08:56:29.755Z" }, + { url = "https://files.pythonhosted.org/packages/34/48/32160a24705d56179de6af838c10a0c735c955dae5f9e4bb344750b79bc2/preshed-3.0.13-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:acd4d89abeca3678c5d8c89b3cd351314465bc67c7fa053d2644f8513e543386", size = 1881154, upload-time = "2026-03-23T08:56:31.49Z" }, + { url = "https://files.pythonhosted.org/packages/ed/22/0344b50f8b1ad9e3aac08099c47e1aba91c81602fd117d2673f6606ecae6/preshed-3.0.13-cp311-cp311-win_amd64.whl", hash = "sha256:de87fbabb0f37c3c92d4dd9b94fc82ab73cdab4247cdfbd57ab3926caa983919", size = 122219, upload-time = "2026-03-23T08:56:32.74Z" }, + { url = "https://files.pythonhosted.org/packages/33/c4/812eeaa568510f396e27edab01100ca71418f032fd7098b107f12e572361/preshed-3.0.13-cp311-cp311-win_arm64.whl", hash = "sha256:5e2753779832e411e93eb727f3d409c0a6b7408e5ce4dd868076d8ece48c7693", size = 109308, upload-time = "2026-03-23T08:56:33.839Z" }, + { url = "https://files.pythonhosted.org/packages/39/fb/ccff23c44c04088c248539005fcda78b9014512a34d170c5360f02ad908b/preshed-3.0.13-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5d14eea14bd01291388928991d7df7d60b9fd19ae970e55006eb4d29b0c1e8eb", size = 138497, upload-time = "2026-03-23T08:56:35.321Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ce/cad5a8145881a771e6c0d002f2e585fc19b962f120860b54d32af5baa342/preshed-3.0.13-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f05b08ce92399c0655b5e0eb5a1cc1f9e295703ed3aabdfaf6538dfa8ae23d57", size = 138010, upload-time = "2026-03-23T08:56:36.399Z" }, + { url = "https://files.pythonhosted.org/packages/a7/a2/c5fed4fb3e946699259d11e4036a3cfdd8c89b3e542e3077d46781642425/preshed-3.0.13-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:62cf7f3113132891d6bba70ff547ad81c6fe50a31930bbbb8499f1d47cd122b7", size = 861498, upload-time = "2026-03-23T08:56:37.67Z" }, + { url = "https://files.pythonhosted.org/packages/51/94/8c9bc48a6ea4903f53a1a0031ce8e35687526949f25821762ef21493c007/preshed-3.0.13-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8b8de3f58043070a354477995acdd98626ce43e4193c708ebd0f694e467f5155", size = 868988, upload-time = "2026-03-23T08:56:39.324Z" }, + { url = "https://files.pythonhosted.org/packages/b6/df/ecd2f40055ff52527ca117ffbfafb888c1a3079b59fbabe03c5b8f9b7240/preshed-3.0.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:183b339956a9e1d7a4a00038a3b9587a734db9e8bd915939a49791bd1b372156", size = 1847382, upload-time = "2026-03-23T08:56:40.89Z" }, + { url = "https://files.pythonhosted.org/packages/e6/88/bdb244e40284ded3632a9f88c23bc80230bd7b2ae4a8b7f2cc91adead7a8/preshed-3.0.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2e77bed56aded7cbe5d28d6bd2178bc5b13eda0e0e464dab205fb578fa915000", size = 1919236, upload-time = "2026-03-23T08:56:42.616Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c9/c91ea56342e6c364fc69b444a1ac5432327857199c44032c9cc9dc4c3a23/preshed-3.0.13-cp312-cp312-win_amd64.whl", hash = "sha256:04d8f13f2986e5d11af5ac51f55ce3106c70c41b483d20ea392e6180bdd0f870", size = 122938, upload-time = "2026-03-23T08:56:44.271Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0b/6a99d99619fd83b14c696e2489caed7070647488d4d3ac0b723d35db2de0/preshed-3.0.13-cp312-cp312-win_arm64.whl", hash = "sha256:19318dc1cd8cac6663c6c830bf7e0002d2de853769fb03e056774e97c21bedfd", size = 109194, upload-time = "2026-03-23T08:56:45.346Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2a/401158195d6dc7f6aef0b354d74d0e95c9da124499448c2b3dbb95b71204/preshed-3.0.13-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0d0c14187dc0078d8a63bf190ec045a4d13e7748b6caeb557a7d575e411410b", size = 137289, upload-time = "2026-03-23T08:56:46.516Z" }, + { url = "https://files.pythonhosted.org/packages/88/8f/e20e64573988528785447a6893b2e7ab287ecfd85b3888e978b28812fd20/preshed-3.0.13-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7770987c2e57497cd26124a9be5f652b5b3ccd0def89859ab0da8bca6144a3de", size = 136847, upload-time = "2026-03-23T08:56:47.572Z" }, + { url = "https://files.pythonhosted.org/packages/b9/72/18168f881359c4482d312f8dc196371bdd61c1583a52b34390da4c88bbea/preshed-3.0.13-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4a7bc48220de579be6bdb0a8715482cf36e2a625a6fd5ad26c9f43485a4a23b5", size = 831478, upload-time = "2026-03-23T08:56:48.769Z" }, + { url = "https://files.pythonhosted.org/packages/fd/3a/3543476091087102775568cea9885dde3453569e9aeee365809108de572f/preshed-3.0.13-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e5c8462472f790c16708306aef3a102a762bd19dfe3d2f8ee08bd5e12f51b835", size = 839913, upload-time = "2026-03-23T08:56:49.937Z" }, + { url = "https://files.pythonhosted.org/packages/cf/65/b13f01329decc44ef53cfb6b4601ba85382dcb2a4ec78d9250f03a418066/preshed-3.0.13-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c046736239cc8d72670749b79b526e4111839a2fc461a58545d212797649129c", size = 1816452, upload-time = "2026-03-23T08:56:51.233Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c7/f1a996c6832234efd4d543041b582418d41ac480ee55c557ec9e65344637/preshed-3.0.13-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7c333f18e9a81c8a6de0603fd8781e17115324b117c445ca91abdf7bfb1abe49", size = 1888978, upload-time = "2026-03-23T08:56:52.591Z" }, + { url = "https://files.pythonhosted.org/packages/e3/b9/96fb71499049885ce19545903fdd38877bbc2be0da47e37c04d01f3e9f66/preshed-3.0.13-cp313-cp313-win_amd64.whl", hash = "sha256:461327f8dd36520dcf1fd55a671e0c3c2c97a2d95e22fc85faa31173f4785dda", size = 122134, upload-time = "2026-03-23T08:56:54.392Z" }, + { url = "https://files.pythonhosted.org/packages/ef/a7/32a4903019d936a2316fdd330bedddac287ac26326107d24fb76a1fbc60a/preshed-3.0.13-cp313-cp313-win_arm64.whl", hash = "sha256:35d6c5acb3ee3b12b87a551913063f0cec784055c2af16e028c19fe875f079d0", size = 108497, upload-time = "2026-03-23T08:56:55.816Z" }, + { url = "https://files.pythonhosted.org/packages/bb/b5/993886c98f5caaa6f07a648cac97a7c62a3093091cad65e1e43a1bd41cc4/preshed-3.0.13-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d2f1efae396cadab5f3890a2fd43d2ee65373ef9096ccbb805e51e8d8bcc563b", size = 137882, upload-time = "2026-03-23T08:56:56.878Z" }, + { url = "https://files.pythonhosted.org/packages/c6/86/b7fd137cbf140afd6c45e895946068a15f5b55642916de0075e6eb18581c/preshed-3.0.13-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8d6acc1f5031a535a55a6f7148e2f274554a8343a16309c700cebea0fe7aee8c", size = 138233, upload-time = "2026-03-23T08:56:58.318Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ca/21a7e79625614134273dfed32bca5bb4c2ec1313e33fbd12d41657536f1f/preshed-3.0.13-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7da9d931e7660dcdd757e5870269f0c159126d682ed73ed313971d199eb0f334", size = 834835, upload-time = "2026-03-23T08:56:59.48Z" }, + { url = "https://files.pythonhosted.org/packages/8f/3a/2dbd299516461831ae90e0d5b0637137bf28520c4e6dd0b01d6f1886659a/preshed-3.0.13-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d4ae5cfe075bb7a07982e382bca44f41ddf041f4d24cbd358e8cccfc049259b8", size = 834928, upload-time = "2026-03-23T08:57:01.075Z" }, + { url = "https://files.pythonhosted.org/packages/7c/d3/af654eba4f6587c4ee02c5043e62c194b0a1c4431ffef0c67b9518f6b61c/preshed-3.0.13-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7557963d0125a3a7bcdb2eb6948f3e45da31b5a7f066b55320de3dea22d7557f", size = 1820368, upload-time = "2026-03-23T08:57:02.351Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9b/ebcb2b9e8cb881e40b55b0bf450f8a6b187e2ef3ae0c685cce81d2d85026/preshed-3.0.13-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c4bc60dc994864095d784b7e4d77dba3e64188d169ac88722b699d175561fddb", size = 1888251, upload-time = "2026-03-23T08:57:04.158Z" }, + { url = "https://files.pythonhosted.org/packages/97/f7/c6c012779edcaa6e2cd092c554e98dc53e77f41205b07208655ba77e2327/preshed-3.0.13-cp314-cp314-win_amd64.whl", hash = "sha256:208dcebbe294bf1881ce33fb015d56ab2a7587aece85a09147727174207892e4", size = 125211, upload-time = "2026-03-23T08:57:05.83Z" }, + { url = "https://files.pythonhosted.org/packages/f8/82/390ef87d732ef64e673ef6bf9e5d898453986e979efa50fb3a400e2c0766/preshed-3.0.13-cp314-cp314-win_arm64.whl", hash = "sha256:cf8e1a7a1823b2a7765121446c630140ac6e8650c07a6efbf375e168d1fef4f7", size = 111942, upload-time = "2026-03-23T08:57:06.996Z" }, + { url = "https://files.pythonhosted.org/packages/80/3a/a9dde3167bcecb27ae82ce4567b5ab1aa3989113ae6814c092ce223cc4ef/preshed-3.0.13-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9ca43ecbc3783eda4d6ab3416ae2ecd9ef23dca5f53995843f69f7457bcd0677", size = 144997, upload-time = "2026-03-23T08:57:08.064Z" }, + { url = "https://files.pythonhosted.org/packages/74/d4/22d9355b50b6a13b407dcad0a81df83fb1d5602092d1f05834674dde8fda/preshed-3.0.13-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c8596e41a258ff213553a441e0bb3eb388fd8158e84a7bf3aae6d8ede2c166d3", size = 147294, upload-time = "2026-03-23T08:57:09.411Z" }, + { url = "https://files.pythonhosted.org/packages/70/42/a225ee83fdb306d2a503f21a627953b820f4e079c90c8a84338957cb8ff5/preshed-3.0.13-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4f8856ca3d88e9b250630d70abb4f260d8933151ddfb413024784b25b009868e", size = 952110, upload-time = "2026-03-23T08:57:10.592Z" }, + { url = "https://files.pythonhosted.org/packages/40/ba/09a9dfe3d22d7e745483fd5d7f2a82cd4d39c161f7d2daa0faa4bd6402be/preshed-3.0.13-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e5b2865aecbd2e1e10e5d19bb8bfad765863c1307c6c3e51f2a08bd64122409", size = 932217, upload-time = "2026-03-23T08:57:12.124Z" }, + { url = "https://files.pythonhosted.org/packages/6c/5c/e10e2e05133e7fcbd7c40536af1148c82dd24357b8f5726e2c7bc51cfd53/preshed-3.0.13-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:09f96b477c987755b3c945df214ea1c1c80bfb350e9f34e78da89585535b77e8", size = 1896542, upload-time = "2026-03-23T08:57:13.525Z" }, + { url = "https://files.pythonhosted.org/packages/37/aa/51e5b4109a4cdfae28c3613eeeb10764a3794ebef8de93ffbb109465bea3/preshed-3.0.13-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:670db59a52e1823b5f088c764df474e65b686592d4093adbeef14581c95ee2cb", size = 1959473, upload-time = "2026-03-23T08:57:15.706Z" }, + { url = "https://files.pythonhosted.org/packages/0e/6a/1d966f367a14c703dde629d150d996c1b727d442f620300b21c9ec1a24d1/preshed-3.0.13-cp314-cp314t-win_amd64.whl", hash = "sha256:b03e21b0bf95eb56e23973f32cabb930e94f352228652f81c0955dbd6967d904", size = 146229, upload-time = "2026-03-23T08:57:17.457Z" }, + { url = "https://files.pythonhosted.org/packages/22/80/368139067603e590a000122355f9c8576c8ebed4fb0b8849feaa2698489d/preshed-3.0.13-cp314-cp314t-win_arm64.whl", hash = "sha256:b980f3ea9bb74b7f94464bc3d6eb3c9162b6b79b531febd14c6465c24344d2cc", size = 119339, upload-time = "2026-03-23T08:57:18.882Z" }, +] + [[package]] name = "propcache" version = "0.4.1" @@ -2407,6 +5332,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, ] +[[package]] +name = "proto-plus" +version = "1.28.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/a6/4fbadcc2044034449b3f8f0ce82dcf3005d53f37c136642103fd4836a31c/proto_plus-1.28.4.tar.gz", hash = "sha256:5ff7ecad828e032a491fcb86947801768e32237f99dd049b649965b892ae9a63", size = 58679, upload-time = "2026-08-25T19:19:15.102Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/5d/0f04b85dafdc3250ced7f2592efc17dce7f40712e941e9632202481e600d/proto_plus-1.28.4-py3-none-any.whl", hash = "sha256:4b01341272f8a348db3f003b6143109f83ab43091019d5181b3fcdf500ab32aa", size = 50797, upload-time = "2026-08-25T19:18:12.338Z" }, +] + [[package]] name = "protobuf" version = "6.33.5" @@ -2422,6 +5359,181 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/57/bf/2086963c69bdac3d7cff1cc7ff79b8ce5ea0bec6797a017e1be338a46248/protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02", size = 170687, upload-time = "2026-01-29T21:51:32.557Z" }, ] +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "psycopg" +version = "3.3.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/73/8fb739d0f6bba247b9b93c9840c402a4f88545be5f1d4b02b23366371c00/psycopg-3.3.5.tar.gz", hash = "sha256:d0a3d9ccf5788af054cbd745278cb02401b5c312aeaafbf2c6144460aec47da4", size = 166508, upload-time = "2026-08-31T22:45:43.151Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/2e/d0a645bcaadde68bd6d93c43f02f14b0191bdda367ce3f7722abe3da744a/psycopg-3.3.5-py3-none-any.whl", hash = "sha256:ce5aa5cdb4f9379f00f487590e5890bfa7df9a164648c969ffa628505e21af4e", size = 213598, upload-time = "2026-08-31T22:39:02.184Z" }, +] + +[package.optional-dependencies] +binary = [ + { name = "psycopg-binary", marker = "implementation_name != 'pypy'" }, +] + +[[package]] +name = "psycopg-binary" +version = "3.3.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/cf/f0577fe9c08b67c3c203506a99c69bbcb2b357e46e2f5f5705878c1e466f/psycopg_binary-3.3.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cd0faa2475ab254ad1b507430131cf7f7f0be927ffdc03c32ad3b33d2ef63f42", size = 4720887, upload-time = "2026-08-31T22:39:12.731Z" }, + { url = "https://files.pythonhosted.org/packages/86/c7/4449033137966e6c72595613a7b0660cb8be5087e3adaf8425e9e85c76a0/psycopg_binary-3.3.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0d8a4b7ae47f3381e2ded89891d2455b809f4afb7e5b58086844abb8cfa420ea", size = 4770016, upload-time = "2026-08-31T22:39:21.194Z" }, + { url = "https://files.pythonhosted.org/packages/8f/c8/87fee9bb4ab25500d7f1e608411cc3025ee86b5ba37cec90b094a3f5a810/psycopg_binary-3.3.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:04f64b39830887c2c737b522cbfd6ad215d65e67ebfff674aa4cf21c02af487b", size = 5587115, upload-time = "2026-08-31T22:39:27.591Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e9/6b5a23e4e64c3f69f55d15d48586436e90c5c3fb589d9c969c1d2a76b559/psycopg_binary-3.3.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d06da67e9c687c6a6fdac9da4b17cbeb296ddd59bd01f6416ed4294bc57c5faf", size = 5259372, upload-time = "2026-08-31T22:39:34.186Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3c/bfcc638f6865998348fb94ecdad87751ca34ddd5c10ad8acafa099bcf14d/psycopg_binary-3.3.5-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:972cc28e943746e71ede254a4dfd1fdfcdd6dcadd6f375703849859f09377f24", size = 6853658, upload-time = "2026-08-31T22:39:41.085Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ee/0f5afd823875c56736ad0d4f88fdcb4375407cb01b61e5188f607714500d/psycopg_binary-3.3.5-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:553b5443cbc94fdb9b0e31b62acdf615e0780982d6b13752a15eb3d6c0dfd0d2", size = 5100080, upload-time = "2026-08-31T22:39:47.094Z" }, + { url = "https://files.pythonhosted.org/packages/53/89/1bdab84b79d1b9fb5a4fb82abf7e2e0e19952b2f417c3ce7a1e24f6c00c1/psycopg_binary-3.3.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fd5b047c9fd887b767d063845413e405f5de8ce1dc7a9d0da0637b77b836b469", size = 4619598, upload-time = "2026-08-31T22:39:52.158Z" }, + { url = "https://files.pythonhosted.org/packages/66/d1/a9f1a8c4b2fee79ff929fd94367619e4ab3bfe39ad2e3cb475bb1e1dfa07/psycopg_binary-3.3.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4901e5b9a31c230211a1871263b6594373bacd770b14ad9b14d349716cb69cbe", size = 4312051, upload-time = "2026-08-31T22:39:57.597Z" }, + { url = "https://files.pythonhosted.org/packages/3a/12/79f1ee831bca072053fad7e5623e3a569fdef3fc3df3fb815cc9b10a23db/psycopg_binary-3.3.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:5b981d25fc2dd13fa7328e40703ea8a03f3e9d855ec946431e96a23206d3b9fd", size = 4039401, upload-time = "2026-08-31T22:40:02.253Z" }, + { url = "https://files.pythonhosted.org/packages/c9/93/b786e6424684bd5e7b7d27729a93862a925d2c22af5d890272642aac42d8/psycopg_binary-3.3.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:39e70c8e3b5fad70e2970ea4cc502bf3b612018f128aa1c670f1fa78b9774543", size = 4344454, upload-time = "2026-08-31T22:40:06.832Z" }, + { url = "https://files.pythonhosted.org/packages/ca/0d/ab385a58e0e8849ec3c598ca44163e102afc4893b0d71b83c4b577654a06/psycopg_binary-3.3.5-cp310-cp310-win_amd64.whl", hash = "sha256:ae67072db949d0c094b747a8ec52ad0fa3c42b27842a5f746f3613d54dde3fba", size = 3664079, upload-time = "2026-08-31T22:40:14.649Z" }, + { url = "https://files.pythonhosted.org/packages/ed/67/ac6b3dfd2d495f8599f7179f8082af4b3be72caa44942ddb45e9613338eb/psycopg_binary-3.3.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c6bd84e4cf67930f26f015dec33f615472b9c5871d46408efe112dbc1bc021de", size = 4720383, upload-time = "2026-08-31T22:40:22.262Z" }, + { url = "https://files.pythonhosted.org/packages/2a/7f/c3870c7e5ba6b4444e3e5401c79d5e83afe2c7045844eabb25d5b39adecb/psycopg_binary-3.3.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fdbeb38c9b7ca8fa57a7bda3802bedb62f4494ad3dd46c7dd36dc3f77fd5093f", size = 4768854, upload-time = "2026-08-31T22:40:26.639Z" }, + { url = "https://files.pythonhosted.org/packages/0e/a2/04834fb8a2af14d42e48abeb61ace0a4fee2d6f516723d897cff16fce677/psycopg_binary-3.3.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:06de14ac978a2d53e864069fb5487075c6e3cfb0740f1bfd7017bc8b9942067f", size = 5588618, upload-time = "2026-08-31T22:40:36.524Z" }, + { url = "https://files.pythonhosted.org/packages/de/0d/2eed6e7f8c5ada9e08cbdca4b78083df12f5c2a8d13fa0c62cd6b2b1d762/psycopg_binary-3.3.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d8b66353b20e79bf7ac0a80f03ae97f522ccbbf909d687eec62f112e56c0276d", size = 5259471, upload-time = "2026-08-31T22:40:46.43Z" }, + { url = "https://files.pythonhosted.org/packages/96/66/2347889e693502e8473a60b3defac0300829007275a21980a2197c200418/psycopg_binary-3.3.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af5084124fb2fd16557073822519dfe8c389636a16adef661a4c0c3918733171", size = 6851181, upload-time = "2026-08-31T22:40:52.939Z" }, + { url = "https://files.pythonhosted.org/packages/b6/11/039d7bbdc8ac28292a2fb9f54a7385116c7d31a3bf9c87e71eda154b32b9/psycopg_binary-3.3.5-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1344fd57a19737554670e67aecabd4fb37cd7937f2925840009645d641117e5a", size = 5098916, upload-time = "2026-08-31T22:40:57.505Z" }, + { url = "https://files.pythonhosted.org/packages/d4/01/5f77284d0c682106279da7d9e26527ae99f3241a91f0549b033c84441404/psycopg_binary-3.3.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2719fe19a4da752c4110cc767716d0a5bdb760d1153d89018bb7c9c61717bde5", size = 4617043, upload-time = "2026-08-31T22:41:04.555Z" }, + { url = "https://files.pythonhosted.org/packages/8b/4a/c07000298ffa3eaf25a720a5fd4bc9326676e99712722ca1accb40071e66/psycopg_binary-3.3.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:893ce86a4b997f6ca1261a7826db2506727332a6ff66646fa7f024b39b5e630e", size = 4310458, upload-time = "2026-08-31T22:41:11.72Z" }, + { url = "https://files.pythonhosted.org/packages/c7/0a/cc7da5bb2bed198354f8dcc7c5dab3f1473edf2b6a177c1e8ec3fa9841ea/psycopg_binary-3.3.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a5e45e4bb68656253ce5c7a344c0a425c293581eba954d7fd7c2e4b2dc9f3038", size = 4039320, upload-time = "2026-08-31T22:41:20.783Z" }, + { url = "https://files.pythonhosted.org/packages/17/60/f65755f1ab74223b437f1d366f63915b6f5e1292234de47643f898c29faf/psycopg_binary-3.3.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ca8af7c0454cdce235d4aedcb5528857468f1490202d25e24fc7af40e176d563", size = 4342427, upload-time = "2026-08-31T22:41:27.068Z" }, + { url = "https://files.pythonhosted.org/packages/0e/3c/6b1ea5b5544fa51ee6da8e99e96ca6a639ed1e0da2e335467893f14d3182/psycopg_binary-3.3.5-cp311-cp311-win_amd64.whl", hash = "sha256:7b443f943abfe35aa5a776630cea27c9348aa66659286cee0b99084332252080", size = 3665235, upload-time = "2026-08-31T22:41:30.9Z" }, + { url = "https://files.pythonhosted.org/packages/5e/83/ba396428a4fb6b70f0dd41315ad86a2c14441b7214afd47b2d49cb450b78/psycopg_binary-3.3.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:25105f9b46bdf2a30fcb67f56976ed66f6855941ae16bc024192609b917d493c", size = 4700169, upload-time = "2026-08-31T22:41:39.712Z" }, + { url = "https://files.pythonhosted.org/packages/4f/56/b23c5978e55cf4effdc5a3e13a17d69a580a487993e01b7c3768dd24281c/psycopg_binary-3.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0249c3e960cdee686000eb77169fb6590105c05bacc37e057ccdffdcd8e6ebde", size = 4763037, upload-time = "2026-08-31T22:41:47.571Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d8/b41108bfe194b4098076c8b873b05ec2ca9582445eccfd9075970d7b948e/psycopg_binary-3.3.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5698ab5941a4d138c30fef858588e651fe7d583280cd6e41832825ad9e747750", size = 5546232, upload-time = "2026-08-31T22:41:55.817Z" }, + { url = "https://files.pythonhosted.org/packages/21/d1/0f244dfef389e52e9dc3056f2a9033d1f6901e97d24d9a9c8b836e32ab6b/psycopg_binary-3.3.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:682a17a57415c3ca1731eec018ed031f012ffcb81ba74806eb219cb396065672", size = 5227752, upload-time = "2026-08-31T22:42:03.776Z" }, + { url = "https://files.pythonhosted.org/packages/31/88/a4781365f09807fb91435e2d00096f3f6ae5d06bce15ef3c3c385dc22772/psycopg_binary-3.3.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d2a61e8147902771df7efe14062a3c8736347850d0d8befcf048235752504f2e", size = 6824658, upload-time = "2026-08-31T22:42:13.263Z" }, + { url = "https://files.pythonhosted.org/packages/65/f1/072c4a46287644694731b3e40fab120ebacf6d153cce7ebf7a5b208f5561/psycopg_binary-3.3.5-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f7e1e45aad410e20de45df2b159df68ff6c8dbf47a3501f806c4489b27f4ad2b", size = 5061439, upload-time = "2026-08-31T22:42:18.591Z" }, + { url = "https://files.pythonhosted.org/packages/e2/d6/1776a95c16941b8bbce89407cc7cf9ea3fd557efb503a40873c9e2b6394f/psycopg_binary-3.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c09775c549b40b274206e1b043c5e5b5af39666e85c98382a30bd05d23ab677b", size = 4588586, upload-time = "2026-08-31T22:42:25.23Z" }, + { url = "https://files.pythonhosted.org/packages/82/64/44ec87b9a74faebe966856307b65c0d35ee6321ce509cdd0e8d6a3f5338b/psycopg_binary-3.3.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:cf0e5e63ee86098299c673992053d556c489ba9ae6aca6cb6e24d16a8e0b09e6", size = 4265161, upload-time = "2026-08-31T22:42:31.864Z" }, + { url = "https://files.pythonhosted.org/packages/24/88/cf181df5651395a80afc520f5fefac72beb035c0c9d58ab7fcc880c9b221/psycopg_binary-3.3.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c065531e8c1815276f50dbfa283e3a7f022671414cdda6fa9a16794dd53b28f9", size = 3998727, upload-time = "2026-08-31T22:42:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/43/1e/c72a1107647db4bb3f534c7fe1b57b1957d9c099e795f5aa81f4a2e0c312/psycopg_binary-3.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:df9853b832b7b916e02ef68e0d5403a7dab2d5c1ddfe94f22b1b155eb862622f", size = 4310119, upload-time = "2026-08-31T22:42:46.403Z" }, + { url = "https://files.pythonhosted.org/packages/03/8d/452620608cafff164737e20b42ebffee8151b865ee171ba0d6692a560a44/psycopg_binary-3.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:35885e333020fc152d27bea1a494bef13b2e68f6fd92b6229015e93539152008", size = 3648197, upload-time = "2026-08-31T22:42:51.575Z" }, + { url = "https://files.pythonhosted.org/packages/e0/1c/e718752cc63cf4e99e4a10fd36e3a3364dabdd0819484a24c0d79fbb9685/psycopg_binary-3.3.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6e85d50b87257fb117675a19ee59daa7bf9a57f6431500adf7059df799232ef4", size = 4704421, upload-time = "2026-08-31T22:42:59.564Z" }, + { url = "https://files.pythonhosted.org/packages/af/cf/a0e748e27c09b92738e4460582d121ba1908be3e36791e150f435e54b332/psycopg_binary-3.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e5becd311f9af8d180bad372f51fb2252fd02cb2073056e2b170c9274f95fe7f", size = 4765054, upload-time = "2026-08-31T22:43:05.421Z" }, + { url = "https://files.pythonhosted.org/packages/39/62/0cbac0266d56c94dd1f702d9af2b5d54bf80b6e658048f4f2c5bd63dd7e3/psycopg_binary-3.3.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:19e5bf9872dbd164c220567fd385ba2309c7d9df1541f78343510c6b0f36a1b7", size = 5547137, upload-time = "2026-08-31T22:43:11.768Z" }, + { url = "https://files.pythonhosted.org/packages/fb/3a/73c6f8871f38fc07a9c0b4cbc9467beb116a2783cecf87dfa53900a396dc/psycopg_binary-3.3.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cb3b3bffebfe07110730626e76238161124f35ac87b748d663316a28d22f58b0", size = 5227577, upload-time = "2026-08-31T22:43:20.767Z" }, + { url = "https://files.pythonhosted.org/packages/59/7b/9f17b9f4d297b774dc574199c4dcd02dadc32a00c4056265918de5c70635/psycopg_binary-3.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2111f880add40fb03c60556069ad68e884a0908a74d2debafc603caf93b73552", size = 6824606, upload-time = "2026-08-31T22:43:33.698Z" }, + { url = "https://files.pythonhosted.org/packages/94/86/d84dadd94a004dbbb43ce0579f1f766fdc6b8cbef745090e24bea77b5283/psycopg_binary-3.3.5-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:40505676b1526b9ea387dace034040a8c8b0bcf984cd6bd4720a2ab15e813586", size = 5060854, upload-time = "2026-08-31T22:43:42.258Z" }, + { url = "https://files.pythonhosted.org/packages/30/d0/e5078be2c7d2490c0d6cd4cb7b3601aec44be2fa8b3fe927e9419b06004f/psycopg_binary-3.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5816472e3bb05615f33a741e0835043d1f4bf9709ff30d2f4aed71815cfc6b5e", size = 4589511, upload-time = "2026-08-31T22:43:50.012Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/08dfb5b18fa482e025864fd91a022310d0782c42e4cf5dda5d0e010b6790/psycopg_binary-3.3.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:358748fc4c8ccdc0e2bdf55420494930e19c3ade586ea9c3a6de3dad1f897311", size = 4268144, upload-time = "2026-08-31T22:43:56.993Z" }, + { url = "https://files.pythonhosted.org/packages/88/b9/cb01dc1d63f3241b49b2ca7fb9f98d0f5c76127f0dbb9440635a6ad0233e/psycopg_binary-3.3.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1ef2e498be47800f6202b9a2304c22646325ca6d54001b7c785bcfdb24a1e8ab", size = 4001036, upload-time = "2026-08-31T22:44:04.429Z" }, + { url = "https://files.pythonhosted.org/packages/95/49/7c17dd832c05b380562ff2ff5f6ab2bcaeca8b7fff2c2b355854368a5bdb/psycopg_binary-3.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:88e01aa2e938a45655a8a5213fc3a44ba78cb4cab8a569b3e0bcb3d1d0eaba16", size = 4313112, upload-time = "2026-08-31T22:44:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/7c/2c/b0b2f887185d6a2ec0b3bef948cc07656d2d1a5d96fa7f2bb03f6ef06ca4/psycopg_binary-3.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:ba466011569297114449df9d523438e1adeedf3e4f31ffb78e897ec3fef3076b", size = 3647313, upload-time = "2026-08-31T22:44:19.139Z" }, + { url = "https://files.pythonhosted.org/packages/46/7f/4e2395da194558533bd9c31f35e4dc58ecbbae6a7176b0d2f72d629e8a51/psycopg_binary-3.3.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:40f8b132c7243ef5f503f0b6f986bf16d38a51b0df1c6ba2577743f128be03e3", size = 4712612, upload-time = "2026-08-31T22:44:25.723Z" }, + { url = "https://files.pythonhosted.org/packages/ae/94/fdb2093c8ccd7048449156db526ad746740cf34fe9c01e5dc1b7a7a8b257/psycopg_binary-3.3.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c0cac998b9b1e82dec853d2e53b3d34d56a525cf231f9441a636cfd5992929a9", size = 4775139, upload-time = "2026-08-31T22:44:36.719Z" }, + { url = "https://files.pythonhosted.org/packages/31/52/5195e87960715f7be2005761b72d56fce4e7757d5333fd40384f071c2de1/psycopg_binary-3.3.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:479b96fd78149cfa10369dc53fbfb89ee729be13146b584a23dbc7e164c0cf1e", size = 5556807, upload-time = "2026-08-31T22:44:42.668Z" }, + { url = "https://files.pythonhosted.org/packages/f7/42/2d616210a91e1327516ed5ae71961aaa31bb740e4a61d7190f2221685a40/psycopg_binary-3.3.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f45d77e398542ce0937d9fa3cd9d84e9c5fc6b34c50a66404ae840bada312750", size = 5236206, upload-time = "2026-08-31T22:44:47.943Z" }, + { url = "https://files.pythonhosted.org/packages/08/2e/e54b0d4cc263b3526e3728bb50a69660d5e79e52255ccd2a1a71e40e6f9a/psycopg_binary-3.3.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98a388509306e5e08a4203253ac52846bc1b034e5cbd0ae6da1211593cc28594", size = 6838066, upload-time = "2026-08-31T22:44:55.701Z" }, + { url = "https://files.pythonhosted.org/packages/77/80/ec22a110f81a44c411965982097efeee86006bdd5a1f51f628318106c84c/psycopg_binary-3.3.5-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ab39e2794b95af61a2ff69e33e5ab6ac5df36e9ffea9a3b18e38b2aaca8c5ad5", size = 5072036, upload-time = "2026-08-31T22:45:02.838Z" }, + { url = "https://files.pythonhosted.org/packages/93/55/7bc3c3ac769ab4fe0c619f2179b4aab3ae043f4d478283dd8279d24c0be4/psycopg_binary-3.3.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9c071bf78e5c2e6efa40bc9089a954d7b41221347a72f35c6bf2d8c96e632f75", size = 4604058, upload-time = "2026-08-31T22:45:10.444Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/8e7414f7dc10b2959e664330cdcf393e412f67355975dafa876cef265264/psycopg_binary-3.3.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:14fdfd65a96ecbd8b586d14546105641f4a6ac7cbe335c786830ea4de94bbe60", size = 4284766, upload-time = "2026-08-31T22:45:17.881Z" }, + { url = "https://files.pythonhosted.org/packages/c1/72/33f293c1d3ee9114f47e9ef4880f31c9de864e84a9b09454c26e106c25ed/psycopg_binary-3.3.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:8dbd694f3741dd4ac5bc60b70e17f7841aefb3f0f38cef4d2756de270e03af43", size = 4011958, upload-time = "2026-08-31T22:45:24.792Z" }, + { url = "https://files.pythonhosted.org/packages/b7/c9/8e38840e5a7d006987bbc9acb29951912253fa50549a4db92b3aa535f089/psycopg_binary-3.3.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:14f432430fd9e1a9e7d9ab2fe14956c77f5d074ebdc556a1ad04e9a1bd3fca04", size = 4323273, upload-time = "2026-08-31T22:45:32.973Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c7/b7ebf601c307f93e7c4c4ebac0edc9db3b2729ca038efe700a18f86b5517/psycopg_binary-3.3.5-cp314-cp314-win_amd64.whl", hash = "sha256:df209e64674a34b41662c67fdc8b4e0ffd77d2136393790691d086a09f9a6cab", size = 3745885, upload-time = "2026-08-31T22:45:40.537Z" }, +] + +[[package]] +name = "pyasn1" +version = "0.6.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/9a/23310166d960def5897e91fe20e5b724601b02a22e84ba1f94232c0b7f67/pyasn1-0.6.4.tar.gz", hash = "sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81", size = 151262, upload-time = "2026-07-09T01:12:33.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl", hash = "sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b", size = 84410, upload-time = "2026-07-09T01:12:32.92Z" }, +] + +[[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 = "pycocotools" +version = "2.0.11" +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'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/df/32354b5dda963ffdfc8f75c9acf8828ef7890723a4ed57bb3ff2dc1d6f7e/pycocotools-2.0.11.tar.gz", hash = "sha256:34254d76da85576fcaf5c1f3aa9aae16b8cb15418334ba4283b800796bd1993d", size = 25381, upload-time = "2025-12-15T22:31:46.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/4b/0c040fcda2c4fa4827b1a64e3185d99d5f954e45cc9463ba7385a1173a77/pycocotools-2.0.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:484d33515353186aadba9e2a290d81b107275cdb9565084e31a5568a52a0b120", size = 160351, upload-time = "2025-12-15T22:30:53.998Z" }, + { url = "https://files.pythonhosted.org/packages/49/fe/861db6515824815eaabce27734653a6b100ddb22364b3345dd862b2c5b65/pycocotools-2.0.11-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca9f120f719ec405ad0c74ccfdb8402b0c37bd5f88ab5b6482a0de2efd5a36f4", size = 463947, upload-time = "2025-12-15T22:30:55.419Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a1/b4b49b85763043372e66baa10dffa42337cf4687d6db22546c27f3a4d732/pycocotools-2.0.11-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e40a3a898c6e5340b8d70cf7984868b9bff8c3d80187de9a3b661d504d665978", size = 472455, upload-time = "2025-12-15T22:30:56.895Z" }, + { url = "https://files.pythonhosted.org/packages/48/70/fac670296e6a2b45eb7434d0480b9af6cb85a8de4f4848b49b01154bc859/pycocotools-2.0.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7cd4cdfd2c676f30838aa0b1047441892fb4f97d70bf3df480bcc7a18a64d7d4", size = 457911, upload-time = "2025-12-15T22:30:58.377Z" }, + { url = "https://files.pythonhosted.org/packages/33/f5/6158de63354dfcb677c8da34a4d205cc532e3277338ab7e6dea1310ba8de/pycocotools-2.0.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:08c79789fd79e801ae4ecfcfeec32b31e36254e7a2b4019af28c104975d5e730", size = 476472, upload-time = "2025-12-15T22:30:59.736Z" }, + { url = "https://files.pythonhosted.org/packages/fc/01/46d2a782cda19ba1beb7c431f417e1e478f0bf1273fa5fe5d10de7c18d76/pycocotools-2.0.11-cp310-cp310-win_amd64.whl", hash = "sha256:f78cbb1a32d061fcad4bdba083de70a39a21c1c3d9235a3f77d8f007541ec5ef", size = 80165, upload-time = "2025-12-15T22:31:00.886Z" }, + { url = "https://files.pythonhosted.org/packages/ee/5c/6bd945781bb04c2148929183d1d67b05ce07996313b0f87bb88c6a805493/pycocotools-2.0.11-cp310-cp310-win_arm64.whl", hash = "sha256:e21311ea71f85591680d8992858e2d44a2a156dc3b2bf1c5c901c4a19348177b", size = 69358, upload-time = "2025-12-15T22:31:01.815Z" }, + { url = "https://files.pythonhosted.org/packages/b3/3f/41ce3fce61b7721158f21b61727eb054805babc0088cfa48506935b80a36/pycocotools-2.0.11-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:81bdceebb4c64e9265213e2d733808a12f9c18dfb14457323cc6b9af07fa0e61", size = 158947, upload-time = "2025-12-15T22:31:03.291Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9b/a739705b246445bd1376394bf9d1ec2dd292b16740e92f203461b2bb12ed/pycocotools-2.0.11-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1c05f91ccc658dfe01325267209c4b435da1722c93eeb5749fabc1d087b6882", size = 485174, upload-time = "2025-12-15T22:31:04.395Z" }, + { url = "https://files.pythonhosted.org/packages/34/70/7a12752784e57d8034a76c245c618a2f88a9d2463862b990f314aea7e5d6/pycocotools-2.0.11-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18ba75ff58cedb33a85ce2c18f1452f1fe20c9dd59925eec5300b2bf6205dbe1", size = 493172, upload-time = "2025-12-15T22:31:05.504Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fc/d703599ac728209dba08aea8d4bee884d5adabfcd9041abed1658d863747/pycocotools-2.0.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:693417797f0377fd094eb815c0a1e7d1c3c0251b71e3b3779fce3b3cf24793c5", size = 480506, upload-time = "2025-12-15T22:31:06.77Z" }, + { url = "https://files.pythonhosted.org/packages/81/d9/e1cfc320bbb2cd58c3b4398c3821cbe75d93c16ed3135ac9e774a18a02d3/pycocotools-2.0.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b6a07071c441d0f5e480a8f287106191582e40289d4e242dfe684e0c8a751088", size = 497595, upload-time = "2025-12-15T22:31:08.277Z" }, + { url = "https://files.pythonhosted.org/packages/a2/23/d17f6111c2a6ae8631d4fa90202bea05844da715d61431fbc34d276462d5/pycocotools-2.0.11-cp311-cp311-win_amd64.whl", hash = "sha256:8e159232adae3aef6b4e2d37b008bff107b26e9ed3b48e70ea6482302834bd34", size = 80519, upload-time = "2025-12-15T22:31:09.613Z" }, + { url = "https://files.pythonhosted.org/packages/00/4c/76b00b31a724c3f5ccdab0f85e578afb2ca38d33be0a0e98f1770cafd958/pycocotools-2.0.11-cp311-cp311-win_arm64.whl", hash = "sha256:4fc9889e819452b9c142036e1eabac8a13a8bd552d8beba299a57e0da6bfa1ec", size = 69304, upload-time = "2025-12-15T22:31:10.592Z" }, + { url = "https://files.pythonhosted.org/packages/87/12/2f2292332456e4e4aba1dec0e3de8f1fc40fb2f4fdb0ca1cb17db9861682/pycocotools-2.0.11-cp312-abi3-macosx_10_13_universal2.whl", hash = "sha256:a2e9634bc7cadfb01c88e0b98589aaf0bd12983c7927bde93f19c0103e5441f4", size = 147795, upload-time = "2025-12-15T22:31:11.519Z" }, + { url = "https://files.pythonhosted.org/packages/63/3c/68d7ea376aada9046e7ea2d7d0dad0d27e1ae8b4b3c26a28346689390ab2/pycocotools-2.0.11-cp312-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fd4121766cc057133534679c0ec3f9023dbd96e9b31cf95c86a069ebdac2b65", size = 398434, upload-time = "2025-12-15T22:31:12.558Z" }, + { url = "https://files.pythonhosted.org/packages/23/59/dc81895beff4e1207a829d40d442ea87cefaac9f6499151965f05c479619/pycocotools-2.0.11-cp312-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a82d1c9ed83f75da0b3f244f2a3cf559351a283307bd9b79a4ee2b93ab3231dd", size = 411685, upload-time = "2025-12-15T22:31:13.995Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0b/5a8a7de300862a2eb5e2ecd3cb015126231379206cd3ebba8f025388d770/pycocotools-2.0.11-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:89e853425018e2c2920ee0f2112cf7c140a1dcf5f4f49abd9c2da112c3e0f4b3", size = 390500, upload-time = "2025-12-15T22:31:15.138Z" }, + { url = "https://files.pythonhosted.org/packages/63/b5/519bb68647f06feea03d5f355c33c05800aeae4e57b9482b2859eb00752e/pycocotools-2.0.11-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:87af87b8d06d5b852a885a319d9362dca3bed9f8bbcc3feb6513acb1f88ea242", size = 409790, upload-time = "2025-12-15T22:31:16.326Z" }, + { url = "https://files.pythonhosted.org/packages/83/b4/f6708404ff494706b80e714b919f76dc4ec9845a4007affd6d6b0843f928/pycocotools-2.0.11-cp312-abi3-win_amd64.whl", hash = "sha256:ffe806ce535f5996445188f9a35643791dc54beabc61bd81e2b03367356d604f", size = 77570, upload-time = "2025-12-15T22:31:17.703Z" }, + { url = "https://files.pythonhosted.org/packages/6e/63/778cd0ddc9d4a78915ac0a72b56d7fb204f7c3fabdad067d67ea0089762e/pycocotools-2.0.11-cp312-abi3-win_arm64.whl", hash = "sha256:c230f5e7b14bd19085217b4f40bba81bf14a182b150b8e9fab1c15d504ade343", size = 64564, upload-time = "2025-12-15T22:31:18.652Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/31c81e99d596a20c137d8a2e7a25f39a88f88fada5e0b253fce7323ecf0d/pycocotools-2.0.11-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:fd72b9734e6084b217c1fc3945bfd4ec05bdc75a44e4f0c461a91442bb804973", size = 168931, upload-time = "2025-12-15T22:31:19.845Z" }, + { url = "https://files.pythonhosted.org/packages/5f/63/fdd488e4cd0fdc6f93134f2cd68b1fce441d41566e86236bf6156961ef9b/pycocotools-2.0.11-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7eb43b79448476b094240450420b7425d06e297880144b8ea6f01e9b4340e43", size = 484856, upload-time = "2025-12-15T22:31:21.231Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fc/c83648a8fb7ea3b8e2ce2e761b469807e6cadb81577bf1af31c4f2ef0d87/pycocotools-2.0.11-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c3546b93b39943347c4f5b0694b5824105cbe2174098a416bcad4acd9c21e957", size = 480994, upload-time = "2025-12-15T22:31:22.426Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2d/35e1122c0d007288aa9545be9549cbc7a4987b2c22f21d75045260a8b5b8/pycocotools-2.0.11-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:efd1694b2075f2f10c5828f10f6e6c4e44368841fd07dae385c3aa015c8e25f9", size = 467956, upload-time = "2025-12-15T22:31:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ff/30cfe8142470da3e45abe43a9842449ca0180d993320559890e2be19e4a5/pycocotools-2.0.11-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:368244f30eb8d6cae7003aa2c0831fbdf0153664a32859ec7fbceea52bfb6878", size = 474658, upload-time = "2025-12-15T22:31:24.883Z" }, + { url = "https://files.pythonhosted.org/packages/bc/62/254ca92604106c7a5af3258e589e465e681fe0166f9b10f97d8ca70934d6/pycocotools-2.0.11-cp313-cp313t-win_amd64.whl", hash = "sha256:ac8aa17263e6489aa521f9fa91e959dfe0ea3a5519fde2cbf547312cdce7559e", size = 89681, upload-time = "2025-12-15T22:31:26.025Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f0/c019314dc122ad5e6281de420adc105abe9b59d00008f72ef3ad32b1e328/pycocotools-2.0.11-cp313-cp313t-win_arm64.whl", hash = "sha256:04480330df5013f6edd94891a0ee8294274185f1b5093d1b0f23d51778f0c0e9", size = 70520, upload-time = "2025-12-15T22:31:26.999Z" }, + { url = "https://files.pythonhosted.org/packages/66/2b/58b35c88f2086c043ff1c87bd8e7bf36f94e84f7b01a5e00b6f5fabb92a7/pycocotools-2.0.11-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:a6b13baf6bfcf881b6d6ac6e23c776f87a68304cd86e53d1d6b9afa31e363c4e", size = 169883, upload-time = "2025-12-15T22:31:28.233Z" }, + { url = "https://files.pythonhosted.org/packages/24/c0/b970eefb78746c8b4f8b3fa1b49d9f3ec4c5429ef3c5d4bbcc55abebe478/pycocotools-2.0.11-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78bae4a9de9d34c4759754a848dfb3306f9ef1c2fcb12164ffbd3d013d008321", size = 486894, upload-time = "2025-12-15T22:31:29.283Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f7/db7436820a1948d96fa9764b6026103e808840979be01246049f2c1e7f94/pycocotools-2.0.11-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83d896f4310379849dfcfa7893afb0ff21f4f3cdb04ab3f61b05dd98953dd0ad", size = 483249, upload-time = "2025-12-15T22:31:31.687Z" }, + { url = "https://files.pythonhosted.org/packages/1e/a6/a14a12c9f50c41998fdc0d31fd3755bcbce124bac9abb1d6b99d1853cafd/pycocotools-2.0.11-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:eebd723503a2eb2c8b285f56ea3be1d9f3875cd7c40d945358a428db94f14015", size = 469070, upload-time = "2025-12-15T22:31:32.821Z" }, + { url = "https://files.pythonhosted.org/packages/46/de/aa4f65ece3da8e89310a1be00cad0700170fd13f41a3aaae2712291269d5/pycocotools-2.0.11-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bd7a1e19ef56a828a94bace673372071d334a9232cd32ae3cd48845a04d45c4f", size = 475589, upload-time = "2025-12-15T22:31:34.188Z" }, + { url = "https://files.pythonhosted.org/packages/44/6f/04a30df03ae6236b369b361df0c50531d173d03678978806aa2182e02d1e/pycocotools-2.0.11-cp314-cp314t-win_amd64.whl", hash = "sha256:63026e11a56211058d0e84e8263f74cbccd5e786fac18d83fd221ecb9819fcc7", size = 93863, upload-time = "2025-12-15T22:31:35.38Z" }, + { url = "https://files.pythonhosted.org/packages/da/05/8942b640d6307a21c3ede188e8c56f07bedf246fac0e501437dbda72a350/pycocotools-2.0.11-cp314-cp314t-win_arm64.whl", hash = "sha256:8cedb8ccb97ffe9ed2c8c259234fa69f4f1e8665afe3a02caf93f6ef2952c07f", size = 72038, upload-time = "2025-12-15T22:31:36.768Z" }, +] + [[package]] name = "pycparser" version = "3.0" @@ -2602,6 +5714,65 @@ crypto = [ { name = "cryptography" }, ] +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + +[[package]] +name = "pypdf" +version = "6.18.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c3/9fa0666f280552bd3833562d985b785fce1ddb3804937edd2fd6a3f2bdb3/pypdf-6.18.0.tar.gz", hash = "sha256:ae58b7d93c22c169ffb02c3b06321c45c4f223b4916536568adb57d789d95d01", size = 7024871, upload-time = "2026-09-07T16:48:16.444Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/a5/d5922a078c9a612327681d4793404f82998f4d66213add6fdc0659245856/pypdf-6.18.0-py3-none-any.whl", hash = "sha256:05b762b77bcb9dcb4a7c91fcf5dded585b25bee7269ab3d3001d7c55fa1b324b", size = 393848, upload-time = "2026-09-07T16:48:14.254Z" }, +] + +[[package]] +name = "pypdfium2" +version = "5.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/78/a52cb80611339ec95f35c7a10d7bfe7a6f97f3b50a35a9f94283d062512e/pypdfium2-5.13.0.tar.gz", hash = "sha256:7ca2d8e31bd8d0d40c496416b7d8bea423388669ffd494929f50e8c3a82326b8", size = 273639, upload-time = "2026-08-13T10:58:15.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/9c/a49050af85055054299c7fab658ac63f8fddde575774aecbf8f71c7a9e5f/pypdfium2-5.13.0-py3-none-android_23_arm64_v8a.whl", hash = "sha256:882f4bbd4b17a335b43603169a14cde9341de12b238acd5c39e690cbca7c4293", size = 3417299, upload-time = "2026-08-13T10:57:40.522Z" }, + { url = "https://files.pythonhosted.org/packages/50/ad/f23027328843ee2bdd05afe16bb101f5906befd0c70de35fa8c53f60a5ff/pypdfium2-5.13.0-py3-none-android_23_armeabi_v7a.whl", hash = "sha256:d96929bde3bd64c771ab3558ca1ffd7704cc4d872ab92cd9f8f8b8a20f7f36b8", size = 2864708, upload-time = "2026-08-13T10:57:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/08/99/1fe58428b69d2722dcbcfaa08ce71834a332c5b518fd58874bcef936b823/pypdfium2-5.13.0-py3-none-macosx_13_0_arm64.whl", hash = "sha256:da5c7b74eebf40b5c1fbe1de01aa1edc8827a79fb1efd999616bc20dcaf77ba4", size = 3507415, upload-time = "2026-08-13T10:57:43.978Z" }, + { url = "https://files.pythonhosted.org/packages/9f/41/06e26da88a4f5b4ed289325868717a186020661b7b221aa6df622711d31b/pypdfium2-5.13.0-py3-none-macosx_13_0_x86_64.whl", hash = "sha256:2abedfb5c70992b19c780ed58d7f7b929e8ce8ee52c9140158f44317c90ec6c7", size = 3670979, upload-time = "2026-08-13T10:57:45.607Z" }, + { url = "https://files.pythonhosted.org/packages/fe/31/f8210d53775f142be934336665b1d60e800c3f176f28c29b4908d945c518/pypdfium2-5.13.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ee8c2bb2e68b396ab4a763215ac100dacb6b96d0da5bebeb239a021aecc3a7e", size = 3676486, upload-time = "2026-08-13T10:57:47.267Z" }, + { url = "https://files.pythonhosted.org/packages/94/50/d339fa09fbe592564b100bfc76833170a1104a764a458ac2abfffcb632f2/pypdfium2-5.13.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07f58e91b8c45ca144a1ff3008faf3c73ef8a5e9fb32988831788363288228cd", size = 3400883, upload-time = "2026-08-13T10:57:49.189Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e0/b10cf41b5e9f0212d014c40635659c6ab95bb4fcc6fc47f5d3c571f8d57f/pypdfium2-5.13.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:46b2f5be9e7ae941ee4216e3d20b66f9dc3d81944a3d57756272de5275204709", size = 3803912, upload-time = "2026-08-13T10:57:50.865Z" }, + { url = "https://files.pythonhosted.org/packages/a7/d8/25ba4ce9a9059ece82f4514df0658fde0aa9bbeafe135e76017c052bf56f/pypdfium2-5.13.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d96beb7f379e6c76d874ca93fcd182ac3168dd499056407070f9927fb1061b8e", size = 4218231, upload-time = "2026-08-13T10:57:52.525Z" }, + { url = "https://files.pythonhosted.org/packages/d3/7c/74a2fb48e5b0d2402d9ca64b39074c722d67e9a8a2c58449a843a8c2329a/pypdfium2-5.13.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81df25c1ab4c13ff773102d3cbea1967511d079123b067fc077bd0c4d57d91d8", size = 3730077, upload-time = "2026-08-13T10:57:54.021Z" }, + { url = "https://files.pythonhosted.org/packages/59/12/8c922f00518c26dc47d3676cc09c1d3c95e991c1977e31067d23cc2215cb/pypdfium2-5.13.0-py3-none-manylinux_2_27_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d66a32d89fa5b4a2715810171239eb194df4aba604727483ab760512f3c6a851", size = 4031512, upload-time = "2026-08-13T10:57:55.736Z" }, + { url = "https://files.pythonhosted.org/packages/c6/48/a171d034c2dac01adcc57d3dad3c97ba11f19d916f421176002c9e02c904/pypdfium2-5.13.0-py3-none-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b90b0a5ac310bb34db8eb848e58fcab4e201e124e3cf3cb1ccb7b85293e034af", size = 3995485, upload-time = "2026-08-13T10:57:57.39Z" }, + { url = "https://files.pythonhosted.org/packages/36/2e/dcb24776d409bb9e5b7fb26a0c62a87b98ab0e30dfcca645eaf31e35123b/pypdfium2-5.13.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ada81c36483cd61d07e32bc7814620ee96256b4f421b913f566861bf91800248", size = 5016636, upload-time = "2026-08-13T10:57:59.181Z" }, + { url = "https://files.pythonhosted.org/packages/93/24/1fab8470fc6de6f4481f009c90757b1a1ee0a61d8e864ed273f72ffca855/pypdfium2-5.13.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:3826e521e895648983cb9ee6b934d4bf51552600043984f84e9c2b3b14b696f3", size = 4555251, upload-time = "2026-08-13T10:58:00.753Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ef/6e8dbea1eddcb55cf34172753ffccd39566333c803cc94d43c653f369f2f/pypdfium2-5.13.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5c029d7163a91f264eafab51fb442a84a33efd9fd83d5a06c0136a7857a3cc8d", size = 5263483, upload-time = "2026-08-13T10:58:02.48Z" }, + { url = "https://files.pythonhosted.org/packages/53/fe/2ff673730189a621c01f9193c74b0f6aa70d8740889fdf11949e1c541869/pypdfium2-5.13.0-py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:be2dccbde0ce7efe334ecd8f348df4308db360756ede4f0821d82dfc9a58caa8", size = 5144135, upload-time = "2026-08-13T10:58:04.351Z" }, + { url = "https://files.pythonhosted.org/packages/19/0b/759b9037c007317fa5c990dd3f6eff2b99d3fbced251d1e2512be92f2e2e/pypdfium2-5.13.0-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:bcd81394fe101405e026eedb3e40bef84635c1e5d974dd6036420eb6937753c6", size = 4648156, upload-time = "2026-08-13T10:58:06.036Z" }, + { url = "https://files.pythonhosted.org/packages/db/3b/ffe29679c52efe8eb02d77aa6656e6d6201395423329af018ebd5923a3d0/pypdfium2-5.13.0-py3-none-musllinux_1_2_s390x.whl", hash = "sha256:2ed32ff685f8e05e637c990bedbf5fca66727bf27718d8bc33eeab21ce0630d1", size = 5089852, upload-time = "2026-08-13T10:58:07.791Z" }, + { url = "https://files.pythonhosted.org/packages/7b/b6/cebacc1601ddfdcd1e6a1dc321533d215ceccf9b825fa9b91b11c6dc39fb/pypdfium2-5.13.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9c777edba28d1d5fd15435ed3a78ee2fdb93dd069be37cb53b559bc122793770", size = 5074153, upload-time = "2026-08-13T10:58:09.396Z" }, + { url = "https://files.pythonhosted.org/packages/54/40/cf14c4f534f817788966857afdedb90002198dca5ce4fe2c6ecb031955ae/pypdfium2-5.13.0-py3-none-win32.whl", hash = "sha256:d33ee7077db67478b75efe4b5ea9610fb96c5416a0bc4949227f0f59c34dfcd9", size = 3753164, upload-time = "2026-08-13T10:58:10.97Z" }, + { url = "https://files.pythonhosted.org/packages/5d/99/a37b6b902457569468ed5908c94e56cb6c4032541f02cf89f723d42a9148/pypdfium2-5.13.0-py3-none-win_amd64.whl", hash = "sha256:47dcca2a8d507b5fd24f94c3c9d48fb379430f097bc20f01beff6c963ffbcedb", size = 3885553, upload-time = "2026-08-13T10:58:12.709Z" }, + { url = "https://files.pythonhosted.org/packages/50/7f/d39f6e64375c2ffd50ea100e3c73af79085c880c2791eb7203bc61d8913f/pypdfium2-5.13.0-py3-none-win_arm64.whl", hash = "sha256:554a0b23376460af1410e3c915906895e2dac67a086b9e6ccde0643a795d3b0d", size = 3700026, upload-time = "2026-08-13T10:58:14.206Z" }, +] + +[[package]] +name = "pyreadline3" +version = "3.5.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/6d/f94028646d7bbe6d9d873c47ee7c246f2d29129d253f0d96cb6fcab70733/pyreadline3-3.5.6.tar.gz", hash = "sha256:61e53218b99656091ddb077df9e71f25850e72e030b6183b39c9b7e6e4f4a9bf", size = 100368, upload-time = "2026-05-14T17:55:04.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/5e/35c856e186b74678c24927847ad9895a51f1bc02a0c6126477a6c6040064/pyreadline3-3.5.6-py3-none-any.whl", hash = "sha256:8449b734232e42a5dcd74048e39b60db2839a4c38cf3ae2bf7707d58b5389c0d", size = 85243, upload-time = "2026-05-14T17:55:03.262Z" }, +] + [[package]] name = "pytest" version = "9.1.1" @@ -2646,6 +5817,19 @@ 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-docx" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lxml" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/f7/eddfe33871520adab45aaa1a71f0402a2252050c14c7e3009446c8f4701c/python_docx-1.2.0.tar.gz", hash = "sha256:7bc9d7b7d8a69c9c02ca09216118c86552704edc23bac179283f2e38f86220ce", size = 5723256, upload-time = "2025-06-16T20:46:27.921Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/00/1e03a4989fa5795da308cd774f05b704ace555a70f9bf9d3be057b680bcf/python_docx-1.2.0-py3-none-any.whl", hash = "sha256:3fd478f3250fbbbfd3b94fe1e985955737c145627498896a8a6bf81f4baf66c7", size = 252987, upload-time = "2025-06-16T20:46:22.506Z" }, +] + [[package]] name = "python-dotenv" version = "1.2.2" @@ -2655,6 +5839,24 @@ 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-iso639" +version = "2026.7.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/06/ca328611455fa6c959524154c10b7f0295bd75c44e7a43a4f3c12a45d6ff/python_iso639-2026.7.23.tar.gz", hash = "sha256:c176522e92cc76b3581cd354e5544330980a36b017d7c291717eface84eaff57", size = 174039, upload-time = "2026-07-23T13:19:00.862Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/5b/de4df88c0f0a621925e486bf23f904fe1f62ad092d6e1df00e7f60174df4/python_iso639-2026.7.23-py3-none-any.whl", hash = "sha256:d678638207411a02c06f28aa965b8081df0766f59ed311071960161fe3a77af2", size = 167905, upload-time = "2026-07-23T13:18:59.63Z" }, +] + +[[package]] +name = "python-magic" +version = "0.4.27" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/db/0b3e28ac047452d079d375ec6798bf76a036a08182dbb39ed38116a49130/python-magic-0.4.27.tar.gz", hash = "sha256:c1ba14b08e4a5f5c31a302b7721239695b2f0f058d125bd5ce1ee36b9d9d3c3b", size = 14677, upload-time = "2022-06-07T20:16:59.508Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/73/9f872cb81fc5c3bb48f7227872c28975f998f3e7c2b1c16e95e6432bbb90/python_magic-0.4.27-py2.py3-none-any.whl", hash = "sha256:c212960ad306f700aa0d01e5d7a325d20548ff97eb9920dcd29513174f0294d3", size = 13840, upload-time = "2022-06-07T20:16:57.763Z" }, +] + [[package]] name = "python-multipart" version = "0.0.31" @@ -2664,6 +5866,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5e/1e/7f7f299527a5a8ad90acd5f2f78dfa6c8495c6301a3205106ea68a84de96/python_multipart-0.0.31-py3-none-any.whl", hash = "sha256:8408153d68a9773291fc1da39a8b85a50044bddbabd2dd72e9229776b7b15e28", size = 29996, upload-time = "2026-06-04T08:27:47.804Z" }, ] +[[package]] +name = "python-oxmsg" +version = "0.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform != 'win32') or (python_full_version == '3.11.*' and sys_platform == 'win32') or (python_full_version == '3.14.*' and sys_platform == 'win32')" }, + { name = "click", version = "8.5.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32') or (python_full_version >= '3.11' and sys_platform != 'win32') or (python_full_version < '3.11' and sys_platform == 'win32') or (python_full_version >= '3.15' and sys_platform == 'win32')" }, + { name = "olefile" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/4e/869f34faedbc968796d2c7e9837dede079c9cb9750917356b1f1eda926e9/python_oxmsg-0.0.2.tar.gz", hash = "sha256:a6aff4deb1b5975d44d49dab1d9384089ffeec819e19c6940bc7ffbc84775fad", size = 34713, upload-time = "2025-02-03T17:13:47.415Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/67/f56c69a98c7eb244025845506387d0f961681657c9fcd8b2d2edd148f9d2/python_oxmsg-0.0.2-py3-none-any.whl", hash = "sha256:22be29b14c46016bcd05e34abddfd8e05ee82082f53b82753d115da3fc7d0355", size = 31455, upload-time = "2025-02-03T17:13:46.061Z" }, +] + [[package]] name = "python-ulid" version = "4.0.1" @@ -2676,6 +5893,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b4/15/8b39b36f55b6618ec4ca9b55134dcfd9c04cecbc72709f5d8e6bdebed9cd/python_ulid-4.0.1-py3-none-any.whl", hash = "sha256:6f1d69ceb97e99fe542df8476ebcd7a668284bf53ee14b3106bcc6a341a95ed9", size = 14609, upload-time = "2026-07-20T15:21:40.214Z" }, ] +[[package]] +name = "pytz" +version = "2026.3.post1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/48/fb042503b6ca6cd271261dc559fd6432f7d8c713153e9ec5c591af4dfc1c/pytz-2026.3.post1.tar.gz", hash = "sha256:2211d3fcf9a797d3405cac96ac7f61d80e6a644f72a3309607282fe8a2010c5d", size = 319745, upload-time = "2026-07-25T15:12:07.385Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/7b/39c34ca613b0b198cb866466651b26b045e2009864c5183c979a3b83f383/pytz-2026.3.post1-py2.py3-none-any.whl", hash = "sha256:dd95840dd199baea12d9cc096a1d452caa6596a1c1e4b5f3dbd1541855d5e815", size = 508283, upload-time = "2026-07-25T15:12:05.782Z" }, +] + [[package]] name = "pywin32" version = "311" @@ -2762,6 +5988,223 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] +[[package]] +name = "rapidfuzz" +version = "3.14.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'win32'", + "python_full_version < '3.11' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/21/ef6157213316e85790041254259907eb722e00b03480256c0545d98acd33/rapidfuzz-3.14.5.tar.gz", hash = "sha256:ba10ac57884ce82112f7ed910b67e7fb6072d8ef2c06e30dc63c0f604a112e0e", size = 57901753, upload-time = "2026-04-07T11:16:31.931Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/b1/d6d6e7737fe3d0eb2ac2ac337686420d538f83f28495acc3cc32201c0dbf/rapidfuzz-3.14.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:071d96b957a33b9296b9284b6350a0fb6d030b154a04efd7c15e56b98b79a517", size = 1953508, upload-time = "2026-04-07T11:13:37.733Z" }, + { url = "https://files.pythonhosted.org/packages/2b/7b/94c1c953ac818bdd88b43213a9d38e4a41e953b786af3c3b2444d4a8f96d/rapidfuzz-3.14.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:667f40fe9c81ad129b198d236881b00dd9e8314d9cc72d03c3e16bdfe5879051", size = 1160895, upload-time = "2026-04-07T11:13:39.278Z" }, + { url = "https://files.pythonhosted.org/packages/7f/60/a67a7ca7c2532c6c1a4b5cd797917780eed43798b82c98b6df734a086c95/rapidfuzz-3.14.5-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9fff308486bbd2c8c24f25e8e152c7594d3fe8db265a2d6a1ce24d58671127f", size = 1382245, upload-time = "2026-04-07T11:13:41.054Z" }, + { url = "https://files.pythonhosted.org/packages/95/ff/a42c9ce9f9e90ceb5b51136e0b8e8e6e5113ba0b45d986effbd671e7dddf/rapidfuzz-3.14.5-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dfa552338f51aec280f17b02d28bace1e162d1a84ccd80e3339a57f98aedb56b", size = 3163974, upload-time = "2026-04-07T11:13:42.662Z" }, + { url = "https://files.pythonhosted.org/packages/e3/3c/11e2d41075e6e48b7dad373631b379b7e40491f71d5412c5a98d3c58f60f/rapidfuzz-3.14.5-cp310-cp310-manylinux_2_39_riscv64.whl", hash = "sha256:068b3e965ca9d9ee4debe40001ae7c3938ba646308afd33cf0c66618147db65c", size = 1475540, upload-time = "2026-04-07T11:13:44.687Z" }, + { url = "https://files.pythonhosted.org/packages/29/fa/09be143dcc22c79f09cf90168a574725dbda49f02cbbd55d0447da8bec86/rapidfuzz-3.14.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:88b7d31ff1cc5e9bc0e4406e6b1fa00b6d37163d50bb58091e9b976ff1129faa", size = 2404128, upload-time = "2026-04-07T11:13:46.641Z" }, + { url = "https://files.pythonhosted.org/packages/32/f9/1aeb504cdcfde42881825e9c86f48238d4e01ba8a1530491e82eb17e5689/rapidfuzz-3.14.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:eacb434410b8d9ca99a8d42352ef085cf423e3c76c1f0b86be2fcba3bff2952c", size = 2508455, upload-time = "2026-04-07T11:13:48.726Z" }, + { url = "https://files.pythonhosted.org/packages/10/8e/b1b5eed8d887a29b0e18fd3222c46ca60fddfb528e7e1c41267ce42d5522/rapidfuzz-3.14.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:649712823f3abcdc48427147a5384fac15623ba435d0013959b52e6462521397", size = 4274060, upload-time = "2026-04-07T11:13:50.805Z" }, + { url = "https://files.pythonhosted.org/packages/e3/c4/7e5b0353693d4f47b8b0f96e941efc377cfb2034b67ef92d082ac4441a0f/rapidfuzz-3.14.5-cp310-cp310-win32.whl", hash = "sha256:13cb79c23ef5516e4c4e3830877be8b19aa75203636be1163d690d37803f6504", size = 1727457, upload-time = "2026-04-07T11:13:52.45Z" }, + { url = "https://files.pythonhosted.org/packages/d9/6e/f530a39b946fa71c009bc9c81fdb6b48a77bbc57ee8572ac0302b3bf6308/rapidfuzz-3.14.5-cp310-cp310-win_amd64.whl", hash = "sha256:f2073495a7f9b75e57e600747ac09510d67683fd64d3228e009740b7ef88f9fe", size = 1544657, upload-time = "2026-04-07T11:13:54.952Z" }, + { url = "https://files.pythonhosted.org/packages/bc/01/02fa075f9f59ff766d374fecbd042b3ac9782dcd5abc52d909a54f587eeb/rapidfuzz-3.14.5-cp310-cp310-win_arm64.whl", hash = "sha256:8166efddea49fdbc61185559f47593239e4794fd7c9044dd5a789d1a90af852d", size = 816587, upload-time = "2026-04-07T11:13:56.418Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f9/3c41a7be8855803f4f6c713b472226a98d31d41869d98f64f4ca790510d6/rapidfuzz-3.14.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e251126d48615e1f02b4a178f2cd0cd4f0332b8a019c01a2e10480f7552554b4", size = 1952372, upload-time = "2026-04-07T11:13:58.32Z" }, + { url = "https://files.pythonhosted.org/packages/9e/89/c2557e37531d03465193bff0ab9de70b468420a807d71a26a65100635459/rapidfuzz-3.14.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ab449c9abd0d4e1f8145dce0798a4c822a1a1933d613c764a641bea88b8bdab", size = 1159782, upload-time = "2026-04-07T11:14:00.127Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b2/ffeeb7eca1a897d51b998f4c0ef0281696c3b06abcca4f88f9def708ffe1/rapidfuzz-3.14.5-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb2829fedd672dd7107267189dabe2bbe07972801d636014417c6861eb89e358", size = 1383677, upload-time = "2026-04-07T11:14:01.696Z" }, + { url = "https://files.pythonhosted.org/packages/6b/d0/4539e42a2d596e068f7738f279638a4a74edd1fbb6f8594e2458058979c6/rapidfuzz-3.14.5-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3d50e5861872935fece391351cbb5ba21d1bced277cf5e1143d207a0a35f1925", size = 3168906, upload-time = "2026-04-07T11:14:03.29Z" }, + { url = "https://files.pythonhosted.org/packages/5e/1c/3ec897eb9d8b05308aa8ef6ae4ed64b088ad521a3f9d8ff469e7e97bc2b0/rapidfuzz-3.14.5-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:7092a216728f80c960bd6b3807275d1ee318b168986bd5dc523349581d4890b8", size = 1478176, upload-time = "2026-04-07T11:14:04.94Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ba/970c03a12ce20a5399e22afe9f8932fd4cd1265b8a8461d0e63b00eb4eae/rapidfuzz-3.14.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9669753caef7fdc6529f6adcc5883ed98d65976445d9322e7dbdb6b697feee13", size = 2402441, upload-time = "2026-04-07T11:14:07.228Z" }, + { url = "https://files.pythonhosted.org/packages/81/93/61d351cae60c1d0e21ba5ff1a1015ad045539ed215da9d6e302204ed887a/rapidfuzz-3.14.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:823b1b9d9230809d8edcc18872770764bfe8ef4357995e16744047c8ccf0e489", size = 2511628, upload-time = "2026-04-07T11:14:09.234Z" }, + { url = "https://files.pythonhosted.org/packages/87/52/374d2d4f60fd98155142a869323aa221e30868cfa1f15171a0f64070c247/rapidfuzz-3.14.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f0b2af76b7e7060c09e1a0dfa9410eb19369cbe6164509bff2ef94094b54d2b6", size = 4275480, upload-time = "2026-04-07T11:14:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/d8/04/82e7989bc9ec20a15b720a335c5cb6b0724bf6582013898f90a3280cfccd/rapidfuzz-3.14.5-cp311-cp311-win32.whl", hash = "sha256:c5801a89604c65ab4cc9e91b23bc4076d0ca80efd8c976fb63843d7879a85d7f", size = 1725627, upload-time = "2026-04-07T11:14:13.217Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b5/eca8ac5609bc9bcb02bb6ff87fa5983cc92b8772d66a431556ab8a8c178f/rapidfuzz-3.14.5-cp311-cp311-win_amd64.whl", hash = "sha256:d7ca16637c0ede8243f84074044bd0b2335a0341421f8227c85756de2d18c819", size = 1545977, upload-time = "2026-04-07T11:14:14.766Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e1/dbf318de28f65fa2cdd0a9dfbdee380f8199eb83b19259bc4f8592551b4e/rapidfuzz-3.14.5-cp311-cp311-win_arm64.whl", hash = "sha256:8c90cdf8516d9057e502aa6003cea71cf5ec27cc44699ca52412b502a04761bb", size = 816827, upload-time = "2026-04-07T11:14:16.788Z" }, + { url = "https://files.pythonhosted.org/packages/d3/e3/574435c6aafb80254c191ef40d7aca2cb2bb97a095ec9395e9fa59ac307a/rapidfuzz-3.14.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0d3378f471ef440473a396ce2f8e97ee12f89a78b495540e0a5617bbfe895638", size = 1944601, upload-time = "2026-04-07T11:14:18.771Z" }, + { url = "https://files.pythonhosted.org/packages/d0/1f/fbad3102a255ecc112ce9a7e779bacab7fd14398217be8868dc9082ba363/rapidfuzz-3.14.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e910eebca9fd0eba245c0555e764597e8a0cccb673a92da2dc2397050725f48", size = 1164293, upload-time = "2026-04-07T11:14:20.534Z" }, + { url = "https://files.pythonhosted.org/packages/88/37/a3eb7ff6121ed3a5f199a8c38cc86c8e481816f879cb0e0b738b078c9a7e/rapidfuzz-3.14.5-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:01550fe5f60fd176aa66b7611289d46dc4aa4b1b904874c7b6d1d54e581c5ec1", size = 1371999, upload-time = "2026-04-07T11:14:22.63Z" }, + { url = "https://files.pythonhosted.org/packages/79/72/97a9728c711c7c1b06e107d3f0623880fb4ef90e147ed13c551a1730e7cc/rapidfuzz-3.14.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48bee0b91bebfaec41e1081e351000659ab7570cc4598d617aa04d5bf827f9e6", size = 3145715, upload-time = "2026-04-07T11:14:24.508Z" }, + { url = "https://files.pythonhosted.org/packages/ed/54/d5caabbea233ac90c286c87c260e49d7641467e87438a18d858e41c82e91/rapidfuzz-3.14.5-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:7e580cb04ad849ae9b786fa21383c6b994b6e6c1444ad1cb9f22392759d72741", size = 1456304, upload-time = "2026-04-07T11:14:26.515Z" }, + { url = "https://files.pythonhosted.org/packages/fc/a7/2d1a81250ac8c01a0100c026018e76f0e7a097ff63e4c553e02a6938c6fb/rapidfuzz-3.14.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:09d6c9ba091854f07817055d795d604179c12a8f308ba4c7d56f3719dfea1646", size = 2389089, upload-time = "2026-04-07T11:14:28.635Z" }, + { url = "https://files.pythonhosted.org/packages/65/0d/c47c3872203ae88e6506997c0b576ad731f5261daa25d559be09c9756658/rapidfuzz-3.14.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1e989f86113be66574113b9c7bdf4793f3f863d248e47d911b355e05ca6b6b10", size = 2493404, upload-time = "2026-04-07T11:14:30.577Z" }, + { url = "https://files.pythonhosted.org/packages/8f/2f/71e0a5a3130792146c8a200a2dd1e52aa16f7c1074012e17f2601eea9a90/rapidfuzz-3.14.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ebd1a18e2e47bc0b292a07e6ed9c3642f8aaa672d12253885f599b50807a4f9", size = 4251709, upload-time = "2026-04-07T11:14:32.451Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/d39874901abacef325adb5b34ae416817c8486dfb4fb87c7a9b74ec5b072/rapidfuzz-3.14.5-cp312-cp312-win32.whl", hash = "sha256:9981d38a703b86f0e315a3cd229fd1906fe1d91c989ed121fb975b3c849f89f5", size = 1710069, upload-time = "2026-04-07T11:14:34.37Z" }, + { url = "https://files.pythonhosted.org/packages/85/0b/f65572c53de8a1c704bda707f63a447b67bdbe95d7cdc70d18885e191df5/rapidfuzz-3.14.5-cp312-cp312-win_amd64.whl", hash = "sha256:d8375e3da319593389727c3187ccaf3e0e84199accc530866b8e0f2b79af05e9", size = 1540630, upload-time = "2026-04-07T11:14:36.287Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c3/143be3a578f989758cae516f3270d5cbb49783a7bfdf57cc27a670e00456/rapidfuzz-3.14.5-cp312-cp312-win_arm64.whl", hash = "sha256:478b59bb018a6780d73f33e38d0b3ec5e968a6c1ed42876b993dd456b7aa20e8", size = 813137, upload-time = "2026-04-07T11:14:38.289Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/252803f2010ba699618cdc048b6e1f7cc1f433c08b4a9a17579b92ab0142/rapidfuzz-3.14.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ebd8fd343bf8492a1e60bcb6dc99f90f74f65d98d8241a6b3e1fed225b76ecd6", size = 1940205, upload-time = "2026-04-07T11:14:40.319Z" }, + { url = "https://files.pythonhosted.org/packages/ea/59/b2afd98e41af9cd54554a4c1c423d84cdd60e6b1c0a09496f033b55f60ec/rapidfuzz-3.14.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6737b35d5af7479c5bf9710f7b17edd9d2c43128d974d25fb4ea653e42c64609", size = 1159639, upload-time = "2026-04-07T11:14:42.52Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/7aa7e62c4c516a7af322ed0c4f0774208b72d457d0cfec808bad0df12f4a/rapidfuzz-3.14.5-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b002c7994cc9f2bc9d9856f0fbaee6e8072c983873846c92f25cefba5b2a925f", size = 1367194, upload-time = "2026-04-07T11:14:44.25Z" }, + { url = "https://files.pythonhosted.org/packages/90/79/2fc252a63bc91d3c3b234d0a3a6ad4ebc460037a23cdcdaf9285f986e6c9/rapidfuzz-3.14.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17a34330cd2a538c1ce5d400b61ba358c5b72c654b928ff87b362e88f8b864c7", size = 3151805, upload-time = "2026-04-07T11:14:46.21Z" }, + { url = "https://files.pythonhosted.org/packages/17/54/0c83508f2683ea70e2d05f8527eb07328acf7bb1e9d97a3bece5702378e7/rapidfuzz-3.14.5-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:95d937e74c1a7a1287dfb03b62a827be08ede10a155cf1af73bbf47f2b73ee6e", size = 1455667, upload-time = "2026-04-07T11:14:47.991Z" }, + { url = "https://files.pythonhosted.org/packages/71/1b/070175e873177814d58850a01ebe80e20ae11e93eb4da894d563988660fa/rapidfuzz-3.14.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:46b92a9970dcc34f0096901c792644094cab49554ac3547f35e3aebbdf0a3610", size = 2388246, upload-time = "2026-04-07T11:14:50.098Z" }, + { url = "https://files.pythonhosted.org/packages/c9/dd/77caf7aaf9c2be050ad1f128d7c24ff0f59079aa62c5f62f9df41c0af45e/rapidfuzz-3.14.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e012177c8e8a8a0754ae0d6027d63042aa5ff036d9f40f07cb3466a6082e21b8", size = 2494333, upload-time = "2026-04-07T11:14:52.303Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/dd7e1f2aa31a8fbbfc16b0610af1d770ffaf1287490f3c8c5b1c52da264f/rapidfuzz-3.14.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a2ae6f53f99c9a0eca7a0afc5b4e45fc73bc1dd4ac74c00509031d76df80ed98", size = 4258579, upload-time = "2026-04-07T11:14:54.538Z" }, + { url = "https://files.pythonhosted.org/packages/9c/0a/ac99e1ba347ba0e85e0bb60b74231d55fb93c0eff43f2920ccb413d0be08/rapidfuzz-3.14.5-cp313-cp313-win32.whl", hash = "sha256:4a60f0057231188e3bd30216f7b4e0f279b11fa4ec818bb6c1d9f014d1562fbc", size = 1709231, upload-time = "2026-04-07T11:14:56.524Z" }, + { url = "https://files.pythonhosted.org/packages/cf/cb/0e251d731b3166378644238e8f0cf9e89858c024e19f75ca9f7e3ae83fd5/rapidfuzz-3.14.5-cp313-cp313-win_amd64.whl", hash = "sha256:11bfc2ed8fbe4ab86bd516fadefab126f90e6dcadffa761739fcb304707dfd35", size = 1538519, upload-time = "2026-04-07T11:14:58.635Z" }, + { url = "https://files.pythonhosted.org/packages/30/6f/4548132acc947db6d5346a248e44a8b3a22d608ef30e770fb578caaf2d00/rapidfuzz-3.14.5-cp313-cp313-win_arm64.whl", hash = "sha256:b486b5218808f6f4dc471b114b1054e63553db69705c97da0271f47bd706aedd", size = 812628, upload-time = "2026-04-07T11:15:00.552Z" }, + { url = "https://files.pythonhosted.org/packages/00/60/69b177577290c5eab892c6f75fe89c3aff3f9ae80298a78d9372b1cecb9a/rapidfuzz-3.14.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:39ef8658aaf67d51667e7bdaf7096f432333377d8302ac43c70b5df8a4cf89b8", size = 1970231, upload-time = "2026-04-07T11:15:02.603Z" }, + { url = "https://files.pythonhosted.org/packages/48/38/2fd790052659cc4e2907b63c25433f0987864b445c1aeec1a302ef5ad948/rapidfuzz-3.14.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ad37a0be705b544af6296da8edddc260d10a8ae5462530fc9991f66498bb1f9", size = 1194394, upload-time = "2026-04-07T11:15:04.572Z" }, + { url = "https://files.pythonhosted.org/packages/80/f4/28430ad8472fc3536e8ebd51a864a226e979cfe924c6e3f83d111373aa74/rapidfuzz-3.14.5-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d45e06f60729e07d9b20c205f7e5cff90b6ef2584e852eecf46e045aea69627d", size = 1377051, upload-time = "2026-04-07T11:15:06.728Z" }, + { url = "https://files.pythonhosted.org/packages/77/7e/9aeacabcfd1e77397968362e5b98fe14248b8307011136b17daf99752a8e/rapidfuzz-3.14.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e52da10236aa6212de71b9e170bace65b64b129c0dea7fc243d6c9ce976f5074", size = 3160565, upload-time = "2026-04-07T11:15:08.667Z" }, + { url = "https://files.pythonhosted.org/packages/56/f4/db4dd7be0cd2f2022117ac5407d905f435d60e48baaea313a567ad27e865/rapidfuzz-3.14.5-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:440d30faaf682ca496170a7f0cc5453ec942e3e079f0fd802c9a7f938dfb50a3", size = 1442113, upload-time = "2026-04-07T11:15:11.138Z" }, + { url = "https://files.pythonhosted.org/packages/a4/99/0e9f6aa57f3e32a767216f797e56dc96b720fcecfb9d8ee907ecc82f8d66/rapidfuzz-3.14.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:56227a61fd3d17b0cd9793132431f3a3d07c8654be96794ba9f89fe0fc8b2d09", size = 2396618, upload-time = "2026-04-07T11:15:13.154Z" }, + { url = "https://files.pythonhosted.org/packages/60/94/44a78e39ffce17cbdd3e2b53b696acc751d5d153be0f499d052b07a4d904/rapidfuzz-3.14.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:2e83cd2e25bb4edd97b689d9979d9c3acccdaaf26ceac08212ceece202febcfa", size = 2478220, upload-time = "2026-04-07T11:15:15.193Z" }, + { url = "https://files.pythonhosted.org/packages/dd/df/454311469a09a507e9d784a35796742bec22e4cebe75551e2da4e0e290fd/rapidfuzz-3.14.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:af3b859726cd3374287e405e14b9634563c078c5531a4f62375508addebddad1", size = 4265027, upload-time = "2026-04-07T11:15:17.28Z" }, + { url = "https://files.pythonhosted.org/packages/fc/01/175465a9ab3e3b70ba669058372f009d1d49c1746e2dcd56b69df188d3a5/rapidfuzz-3.14.5-cp313-cp313t-win32.whl", hash = "sha256:8ce1d850b3c0178440efde9e884d98421b5e87ff925f364d6d79e23910d7593f", size = 1766814, upload-time = "2026-04-07T11:15:19.687Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a0/a9b84a47af06ebed94a1439eb2f02adebfb8628bcd30af1fe3e02f5ef56c/rapidfuzz-3.14.5-cp313-cp313t-win_amd64.whl", hash = "sha256:c84af70bcf34e99aee894e46a0f1ac77f17d0ef828179c387407642e2466d28a", size = 1582448, upload-time = "2026-04-07T11:15:21.98Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f1/5937800238b3f8248e70860d79f69ba8f73e764fff47e36bc9e2f26dbcc6/rapidfuzz-3.14.5-cp313-cp313t-win_arm64.whl", hash = "sha256:aac0ad28c686a5e72b81668b906c030ee28050b244544b8af68e12fb32543895", size = 832932, upload-time = "2026-04-07T11:15:24.358Z" }, + { url = "https://files.pythonhosted.org/packages/81/41/aa3ffb3355e62e1bf91f6599b3092e866bc88487a07c524004943c7676df/rapidfuzz-3.14.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1a31cc6d7d03e7318a0974c038959c59e19c752b81115f2e9138b3331cd64d45", size = 1943327, upload-time = "2026-04-07T11:15:26.266Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e1/c2141f1840a41e07ad2db6f724945f8f8ff3065463899a22939152dd6e09/rapidfuzz-3.14.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0298d357e2bc59d572da4db0bc631009b6f8f6c9bc8c11e99a12b833f16b6575", size = 1161755, upload-time = "2026-04-07T11:15:28.659Z" }, + { url = "https://files.pythonhosted.org/packages/ca/07/66e753eeaa353161d1d331b7dd517bb349b0bacfebe8496d7b26be26f81f/rapidfuzz-3.14.5-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:59b3dba758661a318995655435c6ab20a04ade79fa51e75bc8dc107cac8df280", size = 1376571, upload-time = "2026-04-07T11:15:31.225Z" }, + { url = "https://files.pythonhosted.org/packages/c8/85/9535df0b78ba51f478c9ce7eb6d1f85535cc31fe356773b48fd9d3e563ca/rapidfuzz-3.14.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4900143d82071bdda533b00300c40b14b963ff826b3642cc463b6dd0f036585e", size = 3156468, upload-time = "2026-04-07T11:15:33.428Z" }, + { url = "https://files.pythonhosted.org/packages/81/ee/b667eb93bba6dc4e0de658edd778e1619dc4d6aab68fa5e5c7f075152735/rapidfuzz-3.14.5-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:feedf219672eef83ea6be6f3bb093bba396a8560fc75be85ba225f082903df0a", size = 1458311, upload-time = "2026-04-07T11:15:35.557Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ce/479074f5624364a48df3403c538797ef22d3ac49c19dc76c3f79fcdcc70c/rapidfuzz-3.14.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:419e4397a36e2665ec992d8d64c20ba4b2a42500c76ecadeca78a4f19cb9cc32", size = 2398228, upload-time = "2026-04-07T11:15:37.669Z" }, + { url = "https://files.pythonhosted.org/packages/0b/15/a8982f649150fffbdcd6f17565974501f6ab33b2795267bffbd4a7ba905b/rapidfuzz-3.14.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:97131ab2be39043054ee28d99e09efe316e6d53449b7e962dfcf3c2de8b2b246", size = 2497226, upload-time = "2026-04-07T11:15:39.857Z" }, + { url = "https://files.pythonhosted.org/packages/19/52/5267c03ef6759831b7d4625a0c9c06e87baa2fae084b61ac9c388858317b/rapidfuzz-3.14.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:593c00dac4e30231c35bf3b4f1da8ec0998762e9e94425586a5d636fcd57f9d0", size = 4262283, upload-time = "2026-04-07T11:15:42.279Z" }, + { url = "https://files.pythonhosted.org/packages/71/c0/2579f343a97f5254c43bb5853baccc01488357dcb64a27bcb869b7888a4a/rapidfuzz-3.14.5-cp314-cp314-win32.whl", hash = "sha256:0084b687b02b4e569b46d8d6d4ad25659528e6081cd6d067ca453a69035f07e4", size = 1744614, upload-time = "2026-04-07T11:15:44.498Z" }, + { url = "https://files.pythonhosted.org/packages/17/eb/8edfed1e80119dc9c35b11df4bc701eea85622ad681fff0263b6961d3224/rapidfuzz-3.14.5-cp314-cp314-win_amd64.whl", hash = "sha256:5dfa89d78f22cd773054caff44827b846161a29f2dcf7e78b8f90d086621e502", size = 1588971, upload-time = "2026-04-07T11:15:46.86Z" }, + { url = "https://files.pythonhosted.org/packages/f6/04/5676df93c85cfa57a3045d8047318df9f3cd58c7b8a99340dd95f874795e/rapidfuzz-3.14.5-cp314-cp314-win_arm64.whl", hash = "sha256:67f3f9d2b444268ab53e47d31bab89954888d23c04c6789f2c727e51fe4b1d13", size = 834985, upload-time = "2026-04-07T11:15:49.411Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0d/4a8988cea658fe335048ddef8c876addff1b6daa3c9ca8ad65a5a2196e69/rapidfuzz-3.14.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:77eac0526899b3c3ad1454bb2b03cdb491d67358ec8ef0c9c48bd61b632b431d", size = 1972517, upload-time = "2026-04-07T11:15:51.819Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a3/f5cfd9965a9d9a9e32249159797c47b5d6299ea6d1629f9126b25f1c10a3/rapidfuzz-3.14.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b9c6bd754d11f6e78ac54e3d86b4b11dc1ba2f13e5fc958899574532897f5a99", size = 1196056, upload-time = "2026-04-07T11:15:54.292Z" }, + { url = "https://files.pythonhosted.org/packages/64/07/561c2e40cfd10e6630a7b0ac5a2a813aef50d944bcd1f3d260319d659d5b/rapidfuzz-3.14.5-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:738c96944d076deeaff70e92b65696ab4f7ecb8081d7791c5403a3257dfaf8ff", size = 1374732, upload-time = "2026-04-07T11:15:56.584Z" }, + { url = "https://files.pythonhosted.org/packages/c2/39/123bb94fee40e2fb3b7c49b80827c7ef42d838e18def3fc2fef5a3cf817a/rapidfuzz-3.14.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4c1bca487a17fe4226b4ffb2d30e799d2b274d692cffa76bd0746f56235fca3", size = 3166902, upload-time = "2026-04-07T11:15:58.768Z" }, + { url = "https://files.pythonhosted.org/packages/75/0a/45716fafc9fd2e028cf20b5ac5bc704887081cd312f84edb0e325599414b/rapidfuzz-3.14.5-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:af6a90a4ed2a48fa1a2d17e9d824e6c7c950bea5bad0b707c77fd55751e6bfef", size = 1452130, upload-time = "2026-04-07T11:16:01.453Z" }, + { url = "https://files.pythonhosted.org/packages/ca/49/4e96c413114398481c0a5b0086af32c364a18613c9a2ea578d17c4bea4ee/rapidfuzz-3.14.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bf5018938208d4597b2e679a4f8cff9fd252f1df53583130ae56281a21801b64", size = 2396308, upload-time = "2026-04-07T11:16:03.588Z" }, + { url = "https://files.pythonhosted.org/packages/89/b7/49fea9fc6878d59bd259d01dd1972d9b86117992b1c66d9b16f0a65273c3/rapidfuzz-3.14.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c0919d1f89ddf91129906705723118ea09754171e4116f5a5dbc667c7bc9b261", size = 2488210, upload-time = "2026-04-07T11:16:05.871Z" }, + { url = "https://files.pythonhosted.org/packages/0c/44/a1f732b93ffacbdad077b7c801149549b2938e1bece6addb5ad85ed74df8/rapidfuzz-3.14.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:93d8da883a35116d6813432177f35e570db5b0a5e30ecb0cbd7cb39c815735df", size = 4270621, upload-time = "2026-04-07T11:16:08.483Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ce/ff942d19fce5385054650bb71a58495ddda299d94661ccc4e6e7fa44868b/rapidfuzz-3.14.5-cp314-cp314t-win32.whl", hash = "sha256:0f23e37019ec07712d58976b1ab2b889f8649a7f7c2f626a2f34ea9139e79279", size = 1803950, upload-time = "2026-04-07T11:16:10.873Z" }, + { url = "https://files.pythonhosted.org/packages/5c/0f/9aafc63f9661222b819b391c187eed29fc90ad5935f9690e5ecc2d2047a4/rapidfuzz-3.14.5-cp314-cp314t-win_amd64.whl", hash = "sha256:7d5ca9c7832e6879a707296d1463685f7c243a27846227044504741640caec66", size = 1632357, upload-time = "2026-04-07T11:16:13.1Z" }, + { url = "https://files.pythonhosted.org/packages/70/a6/51fc1b0e61e3326e1c68a61cfd0c6b3c34c843681c4b1eefbf0596f59162/rapidfuzz-3.14.5-cp314-cp314t-win_arm64.whl", hash = "sha256:3e91dcd2549b8f8d843f98ba03a17e01f3d8b72ce942adbbb6761bc58ffce813", size = 855409, upload-time = "2026-04-07T11:16:15.787Z" }, + { url = "https://files.pythonhosted.org/packages/d9/ee/e71853bf82846c5c2174b924b71d8e8099fb05ff87c958a720380b434ba3/rapidfuzz-3.14.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:578e6051f6d5e6200c259b47a103cf06bb875ab5814d17333fc0b5c290b22f4c", size = 1888603, upload-time = "2026-04-07T11:16:18.223Z" }, + { url = "https://files.pythonhosted.org/packages/36/82/40f67b730f32be2ebad9f62add1571c754f52249254b2e88af094b907eee/rapidfuzz-3.14.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fbf1b8bb2695415b347f3727da1addca2acb82c9b97ac86bebf8b1bead1eb12d", size = 1120599, upload-time = "2026-04-07T11:16:20.682Z" }, + { url = "https://files.pythonhosted.org/packages/ef/9f/a3635cc4ec8fc6e14b46e7db1f7f8763d8c4bef33dcc124eea2e6cb2c8f3/rapidfuzz-3.14.5-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f4a8f5cc84c7ad6bffa0e9947b33eb343ad66e6b53e94fe54378a5508c5ed53", size = 1348524, upload-time = "2026-04-07T11:16:23.451Z" }, + { url = "https://files.pythonhosted.org/packages/cc/1b/2b229520f0b48464cfcd7aa758f74551d12c9bc4ab544022a60210aab064/rapidfuzz-3.14.5-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:97c6d85283629646fa87acc22c66b30ea9d4de7f6fdf887daa2e30fa041829b5", size = 3099302, upload-time = "2026-04-07T11:16:25.858Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b5/363906b1064fc6fe611783a61764927bbd91919aaaabe8cba82151ca93ef/rapidfuzz-3.14.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:dfef96543ced67d9513a422755db422ae1dc34dade0a1485e0b43e7342ed3ebf", size = 1509889, upload-time = "2026-04-07T11:16:28.487Z" }, +] + +[[package]] +name = "rapidfuzz" +version = "3.14.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/18/97/226c43b7b5d957bc3840ed52ea99eed261f99834c4619be7a4742cbaeafa/rapidfuzz-3.14.6.tar.gz", hash = "sha256:e13a8160d017b499ec7a2fa9d0ce1ae2e7377080815785819f966fb235d4eb60", size = 57955060, upload-time = "2026-08-30T21:45:51.097Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/09/144d6fcd84fadb124d282f727d197a92dc48ae279e80d4b7d23795ba164d/rapidfuzz-3.14.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1c0dd0d765184366b6e213a8af3b0b3bb39dad27943bbfb193515d4ff96ac82a", size = 1975267, upload-time = "2026-08-30T21:41:54.195Z" }, + { url = "https://files.pythonhosted.org/packages/b9/8f/17985248f0f651a518b543f802fa706b7810cbe96a434a5a9dc24f99b7d2/rapidfuzz-3.14.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0c61cade182f130c9903231946bd1074539121721693a918e7b70382ae802bd8", size = 1246874, upload-time = "2026-08-30T21:41:57.063Z" }, + { url = "https://files.pythonhosted.org/packages/de/8f/9cf3b552bb84911add3c86e014e8704d20ea4e274295686106dc010356ae/rapidfuzz-3.14.6-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3781cf14f9fc933d7198c2b25a8bbbd1a62b752746d5cd26de14957edc0e802f", size = 1394531, upload-time = "2026-08-30T21:41:58.745Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7f/c4824d855cb1f89f8db0802b7ae22705187be55e0ab2f9873b574a0a6713/rapidfuzz-3.14.6-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71a5bbfd00da1963f27dd1432068929694cf0e00007ae2b9c1ad2a187ec29a16", size = 1702106, upload-time = "2026-08-30T21:42:00.398Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ff/556d3aefbd1f115fcda6bdf3ea578405fcaa44c233b525fda583943f3692/rapidfuzz-3.14.6-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:eabaf06ca4896c59cfd9162480f0d37a15a2304ce2efe83ae2bbcfa1cf13534e", size = 2735203, upload-time = "2026-08-30T21:42:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/11/ae/a781ec62825990319483c82ef962b509e9ce22a67a9f97d63d70b2b175b9/rapidfuzz-3.14.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3d5d90bae3c6fb7ea34da968c9f23070e8440edb827a28b242580e0108110b14", size = 3180952, upload-time = "2026-08-30T21:42:03.918Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cc/a8cdeaa64db2e914f3475551b19ea2a6187b5458b50eac707e10f1bcf9d7/rapidfuzz-3.14.6-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:d6b58daadbe6974884ec39aee30cfb8bd2e126f8d03503f0069f70d5e84656a3", size = 1485205, upload-time = "2026-08-30T21:42:05.659Z" }, + { url = "https://files.pythonhosted.org/packages/09/4e/6394e8d79088124bf39a8103ac2ae166a3f62ffc67b51c4e869dfe38b6d1/rapidfuzz-3.14.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ab4386ef7c2cb3e5eb46e815be49715dfcd301bb9f0a431f18da7aa0007de54f", size = 2415347, upload-time = "2026-08-30T21:42:07.847Z" }, + { url = "https://files.pythonhosted.org/packages/f0/2e/92acf13a03c45884aabe9d637c620f5b7806e56bd6f6f8d8016f95614722/rapidfuzz-3.14.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:33a2f7faedaa3608c4876c41b448fc786d54e6cd7c6e732f7de466319b5a73c2", size = 2819438, upload-time = "2026-08-30T21:42:09.788Z" }, + { url = "https://files.pythonhosted.org/packages/95/54/3ed4286d9ebf0b623b021970a46d7befa053dd09c85cd213bfb2ad2a0bbc/rapidfuzz-3.14.6-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:adb160a100f6122aa45c78d686e198da3f9e815d4182e0c4fe730608479f7f9c", size = 2521065, upload-time = "2026-08-30T21:42:11.923Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ff/ae8ecf60ce25eab3accfe5a0c9ba6499b02c5e2ab03ee9defdf5475eb4e7/rapidfuzz-3.14.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ad60297c001d15af24338440bca85dfee8710e9e3222733c906b33e89d986166", size = 3319384, upload-time = "2026-08-30T21:42:14.191Z" }, + { url = "https://files.pythonhosted.org/packages/4a/1d/d39dfc6cdc5c1d0452d4af563c678f2d5821f0df306bc3ab9502f3555690/rapidfuzz-3.14.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3d5b1cfa67bbe6239a643bca1d986f8a07e0a045286c674946e1648c132baa46", size = 4297470, upload-time = "2026-08-30T21:42:16.667Z" }, + { url = "https://files.pythonhosted.org/packages/1b/f6/0a64983c5cf5b2ce8cf2ce4fc54ecd6b5ee6cd6a3af8b870657f28e31a07/rapidfuzz-3.14.6-cp311-cp311-win32.whl", hash = "sha256:46ddb42af4cad3ac9d5e0c97ee1e687500c529a1ad5cbf9c949ce35f6edd4537", size = 1902086, upload-time = "2026-08-30T21:42:18.576Z" }, + { url = "https://files.pythonhosted.org/packages/41/72/638db21d63041ba17c4ed482a8cd1fe6dc4d90bc84b2a28aaccc2611ff84/rapidfuzz-3.14.6-cp311-cp311-win_amd64.whl", hash = "sha256:737a57cbca3e5c16decac86e205727bcd4b99c52f77c48bb44123078c5cd9a7a", size = 1738042, upload-time = "2026-08-30T21:42:20.427Z" }, + { url = "https://files.pythonhosted.org/packages/10/f7/d0fb82451c1f0c701a742939120b32a092ac64bbacf8bf8fa21d61fc89e7/rapidfuzz-3.14.6-cp311-cp311-win_arm64.whl", hash = "sha256:19c1cda8198cc57ffd4ff69a1c02cbe4297e9ca7b506bca03ec584da0a9fe1ff", size = 1190829, upload-time = "2026-08-30T21:42:22.322Z" }, + { url = "https://files.pythonhosted.org/packages/03/d2/5a7646b185a61400220e4783d23461c1e864a9ee82ba443b18c218e2364b/rapidfuzz-3.14.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b46cecf27025e7a934332ade033e6a394da8a493f19fa1d835e3b2968a4ff7da", size = 1965178, upload-time = "2026-08-30T21:42:24.164Z" }, + { url = "https://files.pythonhosted.org/packages/8b/72/10fc4e414eeed7963e2f1c315c731cb68196f0478cb244c78a21f5ce8662/rapidfuzz-3.14.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1901414b135afb1a7f4b1ef940b95523b49cc5642aecf02af740f37567e98137", size = 1248230, upload-time = "2026-08-30T21:42:26.088Z" }, + { url = "https://files.pythonhosted.org/packages/39/e9/0794043c1a0af09cacdbb6a9e8b9b2079cdf73337e7c29b4a9f117415bb9/rapidfuzz-3.14.6-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96a548979cd939b2c69358a0f5088a408524fbf7454f04bf90939fa971e64310", size = 1380396, upload-time = "2026-08-30T21:42:27.97Z" }, + { url = "https://files.pythonhosted.org/packages/2f/73/9218cf4424ab86260ee88ebdb612c5ed4d9bfd6b6d1e2f3c3bf4599d13bf/rapidfuzz-3.14.6-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b22ef7e5e2341efc6216b666491022027b984e5aef93446064742f43f3c1d926", size = 1674037, upload-time = "2026-08-30T21:42:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/a1/f5/bad528b6dfc608a48838508f270c79332ab05592703c9a46504ba95e9eab/rapidfuzz-3.14.6-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f0d2d95c787d812b9106cfbcb94ad37a49f59df9287e00a75eb61afc246e8759", size = 2722897, upload-time = "2026-08-30T21:42:31.737Z" }, + { url = "https://files.pythonhosted.org/packages/13/da/49ab137f788a0e03e872d4c6b3d5c9c6c6bed4e4ccea381f69c4d186341b/rapidfuzz-3.14.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0debb5f43662ea84d2f0228a0c7407ff647f9c3d13f3b692efff0cde46eebce0", size = 3168023, upload-time = "2026-08-30T21:42:33.663Z" }, + { url = "https://files.pythonhosted.org/packages/59/33/81ca664a15194b8b4a7e863b534e36c057724f9709c7781e9400d0edf024/rapidfuzz-3.14.6-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:1d253e1fe44648242a0029b42ba23adf238ed2a7eb3d8ed0a03731a23f074ae0", size = 1474666, upload-time = "2026-08-30T21:42:35.5Z" }, + { url = "https://files.pythonhosted.org/packages/87/eb/b16f9f8cc255c8dc7c0d7712aa7e7c12a6fd85c8b2b56665f2a24222a941/rapidfuzz-3.14.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e06c6050c9bf6cd72305e3e6a293918b2b92cf2a067007585a53898624902e3c", size = 2402289, upload-time = "2026-08-30T21:42:37.309Z" }, + { url = "https://files.pythonhosted.org/packages/4a/73/eaa1ca89f6ab12c0fe7f943226ce4ad1d2c67eb281dfd706279771fcff5a/rapidfuzz-3.14.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d85a6e9180e53cde95c95dfeb05a2ac94ead4d9d803a8fd186d2719a678b8483", size = 2788332, upload-time = "2026-08-30T21:42:39.412Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ad/db927fbe23f621dd292a6332a19822703084617c0281a88156a8c138d4e0/rapidfuzz-3.14.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:35db2670f69fa3a4eb4741055581477ff92f2cf39e7e06f43ebcb97c2192fe7c", size = 2510540, upload-time = "2026-08-30T21:42:41.629Z" }, + { url = "https://files.pythonhosted.org/packages/2d/b2/8e9012968fab837babe1292edcbe1c972605f5b3af19c7fcac2ded731d39/rapidfuzz-3.14.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f9d93e5424d1e4c103b57906b8beba270e680afda3ffdff7ea3bc6173b37083c", size = 3299876, upload-time = "2026-08-30T21:42:43.803Z" }, + { url = "https://files.pythonhosted.org/packages/19/99/799ce99328ea97fe5d7510048ffea148b8ad4a838366f908691be52342a5/rapidfuzz-3.14.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9b0a501f37fb852c54469375baa25874246b3bbc8b6e21fb4cd186a32335868", size = 4277032, upload-time = "2026-08-30T21:42:46.08Z" }, + { url = "https://files.pythonhosted.org/packages/07/8a/995b4746c5bc1f561e64de1fa546927183fec7a369fe988716ef394a6d0a/rapidfuzz-3.14.6-cp312-cp312-win32.whl", hash = "sha256:9e974251a9833791bc557b46f975676a56c2d58946f795cd2964b095496dfdcc", size = 1887051, upload-time = "2026-08-30T21:42:48.265Z" }, + { url = "https://files.pythonhosted.org/packages/84/c4/12f01df5778227c8655fcd9b429fc001d43270f5d8d154edc9066bab1de3/rapidfuzz-3.14.6-cp312-cp312-win_amd64.whl", hash = "sha256:cfca36e4612208875e08611a779164b6cb8900ab8bbd3d82d4cfdfae9efbfac9", size = 1731992, upload-time = "2026-08-30T21:42:50.211Z" }, + { url = "https://files.pythonhosted.org/packages/19/8d/92217f0bc81ec458b4134ad53714b1be0cd3be21494227d73510b06467d6/rapidfuzz-3.14.6-cp312-cp312-win_arm64.whl", hash = "sha256:96bbd5a1c67d135334d02fae74f1d933fdda204ea03d544a59dab6b1cbfbf565", size = 1186693, upload-time = "2026-08-30T21:42:52.63Z" }, + { url = "https://files.pythonhosted.org/packages/a0/ad/4901a37256bc5027f3873ebd538b851349d7627d8aa2e91743c79b500f48/rapidfuzz-3.14.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:55dc9a55924b4ecfcf4a60a701bcfae7d9daf0129c41dc16139270d75be0996c", size = 1961301, upload-time = "2026-08-30T21:42:54.46Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d3/5a56e26db79c00191bc7c5387a04dfa5b6326c2c81c468a976ee2aa8fa15/rapidfuzz-3.14.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bba0e9fad4dbea80227cde9cef3aaa984a934a84aec5f7505532e19838b14769", size = 1244370, upload-time = "2026-08-30T21:42:56.425Z" }, + { url = "https://files.pythonhosted.org/packages/2b/12/0958686418e596961642c41e9162906363649e70f6a12cfcff212f77ccb3/rapidfuzz-3.14.6-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b34b7ee4f4f760690d6477163aabbec05705b5dd764cb6c3a6ba95aa1fffc42", size = 1377336, upload-time = "2026-08-30T21:42:58.687Z" }, + { url = "https://files.pythonhosted.org/packages/60/09/a0a70c35996fa5225c8cddca38e2e594c82518aeefa08edb5d875ce0d82b/rapidfuzz-3.14.6-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:abe92a70134c8b40790bb5c78b2a0a790686c26e83b6e99a456127ca141fe06a", size = 1670277, upload-time = "2026-08-30T21:43:00.798Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d7/b9deea614b32e933e37d77eecf539ffe2b41c0a922a6fd759993865e7ee5/rapidfuzz-3.14.6-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:659b41570fcc6e02631ac361c47cc8db9ad26d740e4be2177df1b63005a49174", size = 2722260, upload-time = "2026-08-30T21:43:02.655Z" }, + { url = "https://files.pythonhosted.org/packages/70/42/4bf9dc905df33bb4515895ff87f777d8df25a3617c0bf8f5d4716813d9ea/rapidfuzz-3.14.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6bb896f89a387219c671ebc33c4a636b222010cc3c5c83884a7fc8707bf0bbf9", size = 3165730, upload-time = "2026-08-30T21:43:04.632Z" }, + { url = "https://files.pythonhosted.org/packages/25/76/454acc3abfa6b958511d6e761f5a95e6c3128936a1eed4f23643c3267d8b/rapidfuzz-3.14.6-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:11d76bb2b2cd038df708ae18f521fb3a50af477cc5a0dffce812da43a2f1beb3", size = 1469515, upload-time = "2026-08-30T21:43:06.612Z" }, + { url = "https://files.pythonhosted.org/packages/2e/f9/29b0f0d7764423573d35db4970dd573b324f4d41abe74d48adca542bcf79/rapidfuzz-3.14.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:28e9ce91bd41a8203185887ef9b1541a891aa61c5c1cb2e46f1689cd4288d372", size = 2401073, upload-time = "2026-08-30T21:43:08.742Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f7/86ac824a7dd2b58729187cc31edebfa7805418f66d97d625010b7383d1de/rapidfuzz-3.14.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:864658e5a10d249a2277374e800f944fe990346d70eea6f3a51b712b6dd01984", size = 2786567, upload-time = "2026-08-30T21:43:11.048Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a6/39fc42e45eb8ee70304862523b2e55cfbd2561c560dd8da1071015fa0ff0/rapidfuzz-3.14.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:3c2444f5cd757ded2c3ba8b1734253b801b9b2ba9ecb3ee40cd505cebbfa7341", size = 2504907, upload-time = "2026-08-30T21:43:13.281Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ea/61f25272239ffef036eb3de1cc63372dfbff27193ca6f9f259d844f41a9c/rapidfuzz-3.14.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2cc9b5dde0ac89f7856f997ef917cac8e18e9dea473e9b3090a84bd600de6a91", size = 3298728, upload-time = "2026-08-30T21:43:15.518Z" }, + { url = "https://files.pythonhosted.org/packages/6d/02/f9bfff9e19e852b097afa837a8000592bcd714fe80827a76367b958771b8/rapidfuzz-3.14.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:faebff9b9a287fb673f9a66465a7e03043601c9bfe5e71c3f91b3f2e7b8a37f6", size = 4272030, upload-time = "2026-08-30T21:43:17.785Z" }, + { url = "https://files.pythonhosted.org/packages/b3/d4/5845698661cb23bc7935536c28f5b86b2b3606de1f54722c1cfac39f170a/rapidfuzz-3.14.6-cp313-cp313-win32.whl", hash = "sha256:4406b2517b85febcf9419f8fbcdfbd534872ea32608050f9562224933ca49a4c", size = 1886313, upload-time = "2026-08-30T21:43:20.173Z" }, + { url = "https://files.pythonhosted.org/packages/67/f1/5b7c56737b9e5af7523ea79e90df732e9e4b2fa66fe2b333ee013ea6e541/rapidfuzz-3.14.6-cp313-cp313-win_amd64.whl", hash = "sha256:c69fb0e064d10c79908dcda76d7ca8ecdf8393a39acbb74dbad3f709f2c60e95", size = 1728638, upload-time = "2026-08-30T21:43:22.169Z" }, + { url = "https://files.pythonhosted.org/packages/05/5e/fc1da16b7f5245a7cc61dc08f70391ddaa1c538be1cf92681e7c763b77a4/rapidfuzz-3.14.6-cp313-cp313-win_arm64.whl", hash = "sha256:a0c8bef04f6b1d9fdbb319576350af53151a64692d477db7d4844c220bc8e212", size = 1185777, upload-time = "2026-08-30T21:43:24.27Z" }, + { url = "https://files.pythonhosted.org/packages/67/9e/8f862d2c8d80ee02633f1c9ce3e5121ce955e61efae24a61a05dd8a55fef/rapidfuzz-3.14.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0f8d6718e7edacdb16455c0472e7552fd518decb91e91250c58784fd6163f54f", size = 1964420, upload-time = "2026-08-30T21:43:26.328Z" }, + { url = "https://files.pythonhosted.org/packages/3e/28/282e8c76b7dcc91e8f5aa1a594168d2136639f29dfda11384c6d36aabca0/rapidfuzz-3.14.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8fa7d45388dec34a86038f2a38380f4922b74b5dd8991247f629a531178db10f", size = 1246072, upload-time = "2026-08-30T21:43:28.475Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ae/8e0f714c55180667d66346e46a3d680dd9809bcee1c5f03557a58b4f2ef6/rapidfuzz-3.14.6-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:760ee152af5e8b4d241a469f933ba2d7215248618ae19770fec7d80d9e149db6", size = 1381829, upload-time = "2026-08-30T21:43:30.67Z" }, + { url = "https://files.pythonhosted.org/packages/eb/9a/4a106d68033a81c24ab71129e3016cc6a27a668f30f436e729cae79048e5/rapidfuzz-3.14.6-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dbe3378db3ae0453accf6196e2ed943f43d416cfacdcb8883db105bc14a0130f", size = 1676195, upload-time = "2026-08-30T21:43:32.862Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f0/b456a74d8e33051b76b3f156cf4d55f717614d68b44b6312ae1f5d85b31d/rapidfuzz-3.14.6-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9ddb0ddf3ee616fdc066add4ef05639c5cf59b58d83779b6023488e5435f6191", size = 2714364, upload-time = "2026-08-30T21:43:35.003Z" }, + { url = "https://files.pythonhosted.org/packages/6d/56/1203b46cedefc3f0c16e10d87123fdd4ec0f2e209f65cd2bf221ec669217/rapidfuzz-3.14.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:08bc63b88048376114d1e66cf8fa6926495d03bb873eb87854fa74cf6848a70b", size = 3167618, upload-time = "2026-08-30T21:43:37.625Z" }, + { url = "https://files.pythonhosted.org/packages/57/17/fa4a0853979b885ff27488d9b80e7c5c985dfed74c5021ea95a3b54ddfad/rapidfuzz-3.14.6-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:50cd6718bcda7ec5293635a9d0b3fb5906251013d3b99ca403ba9dfa8965f661", size = 1471360, upload-time = "2026-08-30T21:43:39.852Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f2/757615ab88f7922b4477f9c93356c4512d744ea042e3e2b41554aab5ec1e/rapidfuzz-3.14.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:63b0e84faec3c5706cae8ae51246ff103407d54efa32a615a548b7b67392ebcf", size = 2403946, upload-time = "2026-08-30T21:43:42.038Z" }, + { url = "https://files.pythonhosted.org/packages/8f/c3/1c2670ff528f7e625d7b552e7ebccd5c4dfdcb84dc08ee85d1bcc0cf1465/rapidfuzz-3.14.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9080a730fdcf3cb8a07464c90f9cf40c1b4ffc73a8375b56a8898aba619dda30", size = 2793123, upload-time = "2026-08-30T21:43:44.438Z" }, + { url = "https://files.pythonhosted.org/packages/5d/92/a01444687bb9a5a2679aa71325c227760e9c475cd02054b45fd8b219cb0c/rapidfuzz-3.14.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:178557c7a50c8c8d65369ede7f3d845bf23590a951c9a368caf166b105d58cf3", size = 2507361, upload-time = "2026-08-30T21:43:46.568Z" }, + { url = "https://files.pythonhosted.org/packages/98/90/43d80ba73fd297c744f7fe0a949af2a610b4b9be96688799c3e73d002b13/rapidfuzz-3.14.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:44f1cddbc2010700e2d88063d0ab64183efe2578d9b52770ce1cd283dda230c5", size = 3304287, upload-time = "2026-08-30T21:43:48.966Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e9/fd9a160699b72b6857551642fe109a1d0a86b06b7ecc0d2b4bbecbc6b61b/rapidfuzz-3.14.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:17081a0e904c12bb4ed49619a2bbb6528f6af00fe850e7ace22487bfd2aea455", size = 4273338, upload-time = "2026-08-30T21:43:51.574Z" }, + { url = "https://files.pythonhosted.org/packages/d0/72/3bc42217fadd07ea0ff9d249cc8001d6f285197c253db95d3a03aac8c254/rapidfuzz-3.14.6-cp314-cp314-win32.whl", hash = "sha256:9e00c8c9500aacbc0c52b66369f54533ecbdcb92e5aa87e160fc8e293000a696", size = 1927357, upload-time = "2026-08-30T21:43:53.851Z" }, + { url = "https://files.pythonhosted.org/packages/57/8d/3ea3bf93a2f22858e1b1298126db35cbf58592d05571ca757f2f16071b17/rapidfuzz-3.14.6-cp314-cp314-win_amd64.whl", hash = "sha256:41ee893c4d7d0fb1844f6cad966540a833784b3bad2c239a0d80195d9231cef4", size = 1783090, upload-time = "2026-08-30T21:43:56.202Z" }, + { url = "https://files.pythonhosted.org/packages/13/17/4add9d94236b37b6f857a3bf34d696b32304e3debc6830584fda95413ac6/rapidfuzz-3.14.6-cp314-cp314-win_arm64.whl", hash = "sha256:10576c39fe6a49fad0bf1069371a77300ce166a3f36d2900d2d0bae08f297104", size = 1221915, upload-time = "2026-08-30T21:43:58.335Z" }, + { url = "https://files.pythonhosted.org/packages/23/a4/af0509bffac37645841e2a6b55a4c6c46f7b2fc0757610b0cba0cbcfa900/rapidfuzz-3.14.6-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1b0a9546a7328d3cfc2f1385501db7c4c374fb566dc1a3b22ad56092846c0134", size = 1994141, upload-time = "2026-08-30T21:44:00.931Z" }, + { url = "https://files.pythonhosted.org/packages/67/da/d46da45e393937509111d4affa4db794fb064341735cfdcffe1f5f13a78a/rapidfuzz-3.14.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9989280902b9c4ecf7de95fbb906e94df0d8c047290ed315c7aa1760cec9b3de", size = 1279969, upload-time = "2026-08-30T21:44:03.253Z" }, + { url = "https://files.pythonhosted.org/packages/4a/8a/1db5582d5c9684c57b1e292dc88d70177233b570e684fe30736140697658/rapidfuzz-3.14.6-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc166efa4ca2fc9cc52e43784a54cbea95fc0e03e533f8266ef66b1c04c7cb76", size = 1381099, upload-time = "2026-08-30T21:44:05.402Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/a9dba69d174b4436c115fcd877a67745d355a859109e0f59955c14577519/rapidfuzz-3.14.6-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:32352a3ed1aad9c097d31fd4f2eece3030169e2de3dedde7a2fadc2652b768ad", size = 1638869, upload-time = "2026-08-30T21:44:07.51Z" }, + { url = "https://files.pythonhosted.org/packages/61/34/67915218f5f84ec2cda57560d81425929b8ea97956eb31283bf95768fefc/rapidfuzz-3.14.6-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ecb45d616002751b58914d5b7c2e66acd39e12242be12717a1258148a1b36526", size = 2687831, upload-time = "2026-08-30T21:44:09.709Z" }, + { url = "https://files.pythonhosted.org/packages/5e/80/07985e10b534dbdd48df0ddf2e42f9d27cf98dc44e09fe047fc4b38471f5/rapidfuzz-3.14.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f9ad513e3a3e045b60b421d5cd3887ae0a33b38fc6c6db3ea5e27c0a2e0412c", size = 3185373, upload-time = "2026-08-30T21:44:12.162Z" }, + { url = "https://files.pythonhosted.org/packages/91/09/db64291ce5f11c0f79486b435b49f5dc66680f605077cb011d282bf767b4/rapidfuzz-3.14.6-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:f35723caef8cc31b6f34209708fb172fc88bab0077c12e9b36bbb829baaf1b16", size = 1459628, upload-time = "2026-08-30T21:44:14.427Z" }, + { url = "https://files.pythonhosted.org/packages/d0/99/7eeaf6f7f42d4ec8b90db54c73f7c2a727e208b4db6fd5ea807e87133b9c/rapidfuzz-3.14.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:408b2e8e8c1ac71b57f0923cf964d6932539725e07b69e70ec66f22c4a403891", size = 2407348, upload-time = "2026-08-30T21:44:16.832Z" }, + { url = "https://files.pythonhosted.org/packages/19/bb/db04caff7bf26718e97592f8cc007988ef18eb088ebb0742addcb25f0819/rapidfuzz-3.14.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5667c56fdc902fa1e12449b5c042e8b1c7e9b30040db20c396fbdb3d0a750866", size = 2758630, upload-time = "2026-08-30T21:44:19.196Z" }, + { url = "https://files.pythonhosted.org/packages/3f/26/962fc396a56ec37146eb5331e55ae53d19dc564fd921f49a6d524c83ee05/rapidfuzz-3.14.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:76a122fc573df603deb5fb827df31bb5efbd0826b50bb7aeca8535a6e8c70cf9", size = 2494519, upload-time = "2026-08-30T21:44:21.687Z" }, + { url = "https://files.pythonhosted.org/packages/83/0f/d2067e23d9b7fb2aeb70a6b36173f0b2376635483f670aa5c47f17e55135/rapidfuzz-3.14.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:e221366e24709b9d41d5f9cc99053b04cfc575d429e956a82cfbc4c4e9e8860a", size = 3262241, upload-time = "2026-08-30T21:44:24.218Z" }, + { url = "https://files.pythonhosted.org/packages/ce/bd/05e48e21b1dd722b41c0cb8ab8867996f6e0c0a1b46e42921ace09799b0c/rapidfuzz-3.14.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:36710ff214b7a8049d26a9c81d99948026593cacb47663742c4119072b651ecd", size = 4296246, upload-time = "2026-08-30T21:44:26.911Z" }, + { url = "https://files.pythonhosted.org/packages/12/ce/f4b355f05b17bdb3a56f1c5e9bd864965dbb810f93d1b5d6044ecfcbd42d/rapidfuzz-3.14.6-cp314-cp314t-win32.whl", hash = "sha256:66ece6f5e2586c742fc3e0b8487e06783d27c6c24adcdcfdd7f306afbd8b5737", size = 1977694, upload-time = "2026-08-30T21:44:29.431Z" }, + { url = "https://files.pythonhosted.org/packages/4a/15/d2c20c57b357ec4157e74a197b3f622dbda0b2a82d1fc708ed7b262758f9/rapidfuzz-3.14.6-cp314-cp314t-win_amd64.whl", hash = "sha256:cab4a932cec02d09471e2c9f1434049ef5bfe1f6e646ff10939c222dc610ad60", size = 1827262, upload-time = "2026-08-30T21:44:31.683Z" }, + { url = "https://files.pythonhosted.org/packages/15/e5/c38c19fbc1de82980e05bd3adbe1dc7f3dd0680e38e868646082317572d6/rapidfuzz-3.14.6-cp314-cp314t-win_arm64.whl", hash = "sha256:b056ce19eaea2ea70c6a6fb387a605ca2af8979de5b9d507597e8012820ddb14", size = 1245604, upload-time = "2026-08-30T21:44:34.066Z" }, + { url = "https://files.pythonhosted.org/packages/10/37/b015bf56f88e9b18b81ad462f610e70cc1145a9df39154fcbe7ddf9f8868/rapidfuzz-3.14.6-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:bc3d74d18543ddfbc8babe1faadb19927a7999fd0d01181cce9e721c14c36ab6", size = 1964451, upload-time = "2026-08-30T21:44:36.695Z" }, + { url = "https://files.pythonhosted.org/packages/d2/1a/7b88284d85b4f7dfdf3038263e11eb11871472aa32902c7063a5fdd7a7c5/rapidfuzz-3.14.6-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:aaa83b633d877a05d549d2073629134998d1b3b9dbc114873d3ff4277984979f", size = 1245701, upload-time = "2026-08-30T21:44:38.841Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f3/444d939f4b6c3c86f67083cb792978f3f42c28f944e66e9152e910cd212a/rapidfuzz-3.14.6-cp315-cp315-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cbe6a62f71fcbca72acbf5a30e53380600369f257f951d664d81d30c0c598595", size = 1382770, upload-time = "2026-08-30T21:44:40.978Z" }, + { url = "https://files.pythonhosted.org/packages/23/a8/1830f07f7d3fcc56508135f130dbd24a917ddedb71107b04b2fbb33d5da9/rapidfuzz-3.14.6-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b82c21c30568e096ef2a9dda7d45c379e6141694e0472dac73bc4372ce13ccee", size = 3163591, upload-time = "2026-08-30T21:44:43.513Z" }, + { url = "https://files.pythonhosted.org/packages/10/e8/da76d94af820707dcbfce224b635fb7c389c19525426c31645c97bedd601/rapidfuzz-3.14.6-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:fc950bb77105a2717d03d9f9c9e21e9ace7df2b8e864dd91edef7e32fa143be2", size = 1467985, upload-time = "2026-08-30T21:44:45.871Z" }, + { url = "https://files.pythonhosted.org/packages/30/75/5cfc0d1491e3c60a8669e8e2b78942c4f395cccabfb9c73bc8b209664e29/rapidfuzz-3.14.6-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:c53a269bdbd71ffbc856d3db9e609478251001ee272507578fa838bc2bd421fe", size = 2405269, upload-time = "2026-08-30T21:44:48.376Z" }, + { url = "https://files.pythonhosted.org/packages/c3/81/9c522c26cfe1909714eb840856106f1e419a44c4e0de034a3eeb873da00b/rapidfuzz-3.14.6-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:bf4fb0f19c9dfce7a908c3e309753602ce3edb83bb74e9ff997e278765bf89df", size = 2507334, upload-time = "2026-08-30T21:44:50.903Z" }, + { url = "https://files.pythonhosted.org/packages/40/29/0bbd158eeddf05e5b581f89bf7c9f0cf330953579309b3806862d360a454/rapidfuzz-3.14.6-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:189ce2bf14938bfa003fbbe7e6da7584ed6ebbc4c560686255dbc20e2829f470", size = 4270388, upload-time = "2026-08-30T21:44:55.271Z" }, + { url = "https://files.pythonhosted.org/packages/be/be/2b67b32988cb96b7fa9461ff3436e275716df00f7817212ed0a1c1779062/rapidfuzz-3.14.6-cp315-cp315-win32.whl", hash = "sha256:7ca0f498bf771a87557e6d8b573aa6cf3daded58ae2eaeb6973618ce3e1615ad", size = 1927539, upload-time = "2026-08-30T21:44:57.796Z" }, + { url = "https://files.pythonhosted.org/packages/51/42/640e1bd16422392fbb6394def1f7dfd4d05bd13c986016ce4b3f91295430/rapidfuzz-3.14.6-cp315-cp315-win_amd64.whl", hash = "sha256:d4c5adb921b67dd79ffc0a14f92b9f8df3d012e66aab340b154ed87014229d93", size = 1783415, upload-time = "2026-08-30T21:45:00.091Z" }, + { url = "https://files.pythonhosted.org/packages/06/ba/c6966904eb7b3d1c6344e6c29245447625d156b11e9757b29adc3cb46037/rapidfuzz-3.14.6-cp315-cp315-win_arm64.whl", hash = "sha256:c9d135fb93709d707577da8a7a8ffc7283525a5b6d0ce55aa3724be5639ed65b", size = 1221931, upload-time = "2026-08-30T21:45:02.531Z" }, + { url = "https://files.pythonhosted.org/packages/ae/97/6dd7f10756eb703e11803c5c838191c2151112f632e29f5eacb1ed1cf86c/rapidfuzz-3.14.6-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:dd89abd1c4b3776c3471a817216830bd275441c8344bbda5d51a3bffe1e0fbdf", size = 1985107, upload-time = "2026-08-30T21:45:04.965Z" }, + { url = "https://files.pythonhosted.org/packages/75/4a/be587adefd9539a89cc6016bac44d222cda4c8212856759c82501fd89e4a/rapidfuzz-3.14.6-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:eab2d4680d7f438dbb1d484b187d59a943edea9c83f792c764a0c148a417a60a", size = 1272093, upload-time = "2026-08-30T21:45:07.304Z" }, + { url = "https://files.pythonhosted.org/packages/de/3f/982b2f1b2a16c46d4598829b6b2d7185921f146d5893630f917cb9d27542/rapidfuzz-3.14.6-cp315-cp315t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8683fefdd3484d64a191b3efbc8cbe9162c3eac891fd62d0a1b70e117ffcd434", size = 1371112, upload-time = "2026-08-30T21:45:09.699Z" }, + { url = "https://files.pythonhosted.org/packages/e6/12/2a1fe61cb9f0ac0dc4166bcb016df695047e75251481a197d47aa5ce8ea5/rapidfuzz-3.14.6-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2bc7af3a699371a941aac86dc8a79ac92adeb3c2add2aab02230e76068a0029e", size = 3175780, upload-time = "2026-08-30T21:45:12.266Z" }, + { url = "https://files.pythonhosted.org/packages/8d/01/abd33d0b7595643e598802a07466af388f1560d7b7cb70f442cc292f4067/rapidfuzz-3.14.6-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:40c2753e2d4dc96b25f8a25adc23ab0bb6cfd8bc8125a1753ac4b037d6ff6511", size = 1458364, upload-time = "2026-08-30T21:45:14.68Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8e/efc98b0cfb540f41661f6a8bf21b67807e221102e5e8fb1585233b39a3bd/rapidfuzz-3.14.6-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:36a37ddc729c33618d89fa221d3333b9b956dc38cf15d31301e6169d962399a3", size = 2398037, upload-time = "2026-08-30T21:45:17.434Z" }, + { url = "https://files.pythonhosted.org/packages/7f/c1/4d89214a453215d897cc76cd6e13937c8ea5dc9f8217993fe2b1eeaf39a5/rapidfuzz-3.14.6-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:635f242f4bdf05d1477fa409815bd73e5f78896773ace84997bc472ffeef685f", size = 2497044, upload-time = "2026-08-30T21:45:20.328Z" }, + { url = "https://files.pythonhosted.org/packages/a9/2d/70aacf6cb577470bdd6f06890d25ecb7ee8a56baa07b114d5877a93ecedd/rapidfuzz-3.14.6-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:40d0cd9c82083aeb30bae8dee265ae571e6748d0d7b222ddd777f33d95a3b712", size = 4284741, upload-time = "2026-08-30T21:45:22.983Z" }, + { url = "https://files.pythonhosted.org/packages/92/7d/943a04a134a5d333c00d3a77169226defef5e081be9219a765afc176dda0/rapidfuzz-3.14.6-cp315-cp315t-win32.whl", hash = "sha256:15da2b258908eb38853c1a6a58a1d09d9aad9c721e03a68c8ba691cd31dff739", size = 1974478, upload-time = "2026-08-30T21:45:25.475Z" }, + { url = "https://files.pythonhosted.org/packages/21/0e/8356ca3e190e2bcced9b80e374d95b0925c4716b51e65720a55399983f41/rapidfuzz-3.14.6-cp315-cp315t-win_amd64.whl", hash = "sha256:3d502769263318690d4f6638b08483979d1b88cdc7c6f087482eea935fde4031", size = 1823286, upload-time = "2026-08-30T21:45:28.368Z" }, + { url = "https://files.pythonhosted.org/packages/fb/04/a0b0e6324b6384d1ab40feb4d16400af3b3101d38cbd15957edd9d17cbe0/rapidfuzz-3.14.6-cp315-cp315t-win_arm64.whl", hash = "sha256:07c7aa0b1e4b9999a54f9e73317d6743ff85442c8ef7b7fbbe6b190fd37d9e75", size = 1243815, upload-time = "2026-08-30T21:45:31.187Z" }, + { url = "https://files.pythonhosted.org/packages/08/9a/7d4949406e2d391e160ead12036bba05e7c90e09bba77a782d33e7e6a1b0/rapidfuzz-3.14.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0844066900cdc9909ce4ab4fb5ba1d8e0c021252d770f2ea476f3443df1d22ef", size = 1912210, upload-time = "2026-08-30T21:45:33.653Z" }, + { url = "https://files.pythonhosted.org/packages/7c/00/a1a077f5cf90c9fa13b28c721f931529ad02748d418d7750590a388832a9/rapidfuzz-3.14.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:1398bd2c197b79bfc40b615999fd3599dc60265fdd5b59edc18156ae048c4cde", size = 1209219, upload-time = "2026-08-30T21:45:36.035Z" }, + { url = "https://files.pythonhosted.org/packages/48/69/a573c2e5e1b1a4f19e98a8fb3f6a792a44f5b8a067895a2654890ffd35a4/rapidfuzz-3.14.6-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2fc748d1fde4109e5d0dab27f1e61f53b3136a235dfee5a4fb579da44808b6a", size = 1361237, upload-time = "2026-08-30T21:45:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/ea/0b/375ebdfc4ca149e23793bb6b72461954ec64d0acbb826030787e88b90ff3/rapidfuzz-3.14.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b42536675c930cb76b7998bfc4d8e59cb35d8df47f2103020265743b6b2ccd2a", size = 3136631, upload-time = "2026-08-30T21:45:41.426Z" }, + { url = "https://files.pythonhosted.org/packages/55/56/799accc99532ecaaa2c1d04c7e594d6bb8f1afdddc327389c61196741cb8/rapidfuzz-3.14.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:1e6911e3a14971719ddc35af98f181d2e5369ab273a5a3488ab7685d23c31ad5", size = 1722739, upload-time = "2026-08-30T21:45:44.301Z" }, +] + [[package]] name = "redis" version = "8.0.1" @@ -2788,6 +6231,143 @@ 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.9.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/5c/f403115361de25809e8f785686ec7096e30fef73be9ae35aa51da4e80abb/regex-2026.9.10.tar.gz", hash = "sha256:1e321e2c84f0e52c457f5ea5944f796d6e8e09cb99738ea98dcc1bfe402a128d", size = 417072, upload-time = "2026-09-09T21:00:21.521Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/29/b4160140513c37ea8e7d87c467b5b7e658f53ca4cdcfe394b82ec768d0b4/regex-2026.9.10-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7dcad477c49c4c626a6c4fcd71b39a971aa217060cc40a6569fd24edcc0fa509", size = 493887, upload-time = "2026-09-09T20:56:21.178Z" }, + { url = "https://files.pythonhosted.org/packages/01/f9/6008e74d6076a980a48cb3eeaf80c1324db73f2eed89a8023e91669dad84/regex-2026.9.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d414c411c06fe0009eac33488fb1591c66b5c2673e342e452e7bb2fe63da8194", size = 295102, upload-time = "2026-09-09T20:56:22.687Z" }, + { url = "https://files.pythonhosted.org/packages/7e/cd/2f540fc813d1f71f18e1cc6861483133a0e6afb349a4e770fbdce4811d8d/regex-2026.9.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f2f43bf4e47ff7ce9e585558706d698c6204d0f80bf2207766382ed817c8e9f4", size = 290660, upload-time = "2026-09-09T20:56:23.835Z" }, + { url = "https://files.pythonhosted.org/packages/03/5f/9c990517587418d82406203d4608e17e02167c73fb36ef9a7a9d7ae64802/regex-2026.9.10-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24d12a625a37c89c2b09303402a06942f55f071b95a7916a49c17034c3d47cd5", size = 787864, upload-time = "2026-09-09T20:56:24.979Z" }, + { url = "https://files.pythonhosted.org/packages/6e/1f/bcc010de82e20c1822d22ae94d97faafd46ce8405599f3636622a9a7bd04/regex-2026.9.10-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ef4ce69ff97fbb44b46751cfea5e859ad0b66d1a50abf34954f0645f51e81671", size = 860487, upload-time = "2026-09-09T20:56:26.363Z" }, + { url = "https://files.pythonhosted.org/packages/58/3e/6e75baf9c78d90bf44187f5ce4ee5de51fde206e6981490a0a4418cde296/regex-2026.9.10-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:044bd4639b6bb409ec9e5d8b7accd57e02b4c4a4e2eafde916f8ae8006b3e40b", size = 905374, upload-time = "2026-09-09T20:56:27.741Z" }, + { url = "https://files.pythonhosted.org/packages/49/b2/be9fafe5e05888296fdd35d82ba9cbb67b0d352e9911fad3c458beb3a34a/regex-2026.9.10-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c3d95d7d9538b5b726dd6fcd7b6117a71e6565202f6d64f5845fb4d8f203f533", size = 796252, upload-time = "2026-09-09T20:56:29.074Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ac/4df639a88ce4fcdb0aedaba7818718254b888e4b60c7df640d5fc82232cb/regex-2026.9.10-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:faa911fbbcf8ac90bda0e0657d60768e3390954ef0588211d63a22add1cb1cd1", size = 790214, upload-time = "2026-09-09T20:56:30.586Z" }, + { url = "https://files.pythonhosted.org/packages/6b/2f/2bb3fef68f7ba134d45a2aa3c713078109ebca71ec5dbb84a42d7284abec/regex-2026.9.10-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ecb2e7acb18f8cc4a67f0ad986c0af291ea4dd385d0614ba9bc09d7f8bbb478c", size = 773413, upload-time = "2026-09-09T20:56:32.223Z" }, + { url = "https://files.pythonhosted.org/packages/7d/57/43737678e306ad6430732828ecf0632c72676eab783cc98c4da373e7ee96/regex-2026.9.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4971776b4f2bd7fd9a83eceb2cb2592cbe2924f639fe8045e6a9de5ba4bfcf25", size = 778331, upload-time = "2026-09-09T20:56:33.51Z" }, + { url = "https://files.pythonhosted.org/packages/b1/19/61f4158219d60ec727500f08f6f0aa86a036bb5c3ba0632204f35eeb92e8/regex-2026.9.10-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f8bdec659a8fa7af51a32b224b3b7c02bc415d54ffd35187b1d224176b17d607", size = 853498, upload-time = "2026-09-09T20:56:34.931Z" }, + { url = "https://files.pythonhosted.org/packages/6f/85/7c783b98eb777cd17e2566ea6343a350f6a7c13f41f43282c20348ed9303/regex-2026.9.10-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:3540734dbe241ebb3b87d5713781f6749a3e4d45480f506aa5fb5cbb0c37d249", size = 762708, upload-time = "2026-09-09T20:56:36.248Z" }, + { url = "https://files.pythonhosted.org/packages/dd/cd/a56fb347afd102dca21fc3ee9163ccf379678154ec2d0e3f139fb26e5eb9/regex-2026.9.10-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:87f5f75c109f08f5c602d68e1af54cead8165189c727b6ac946b30b9833a3ba4", size = 842488, upload-time = "2026-09-09T20:56:37.538Z" }, + { url = "https://files.pythonhosted.org/packages/59/54/5d38dc952ec93cd19943d31647806c7fbf380c74f36b747874b26a3070fe/regex-2026.9.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:88b02aa8d0ec9b6189fe933d425775882271c23700ac11fd26d1779b0f56fde3", size = 783557, upload-time = "2026-09-09T20:56:39.649Z" }, + { url = "https://files.pythonhosted.org/packages/63/81/c4c880460f22160046e9d4794d947935da8337d579e08c3492344be32d1d/regex-2026.9.10-cp310-cp310-win32.whl", hash = "sha256:13c52fc377792675f604a207a2ae5958c080f6854f7698d40d9ff034d95b1e76", size = 266938, upload-time = "2026-09-09T20:56:41.352Z" }, + { url = "https://files.pythonhosted.org/packages/f4/0a/5061dde95447f00acdeeb39780b32def0ce380c3e725aeee1f5b37b788bb/regex-2026.9.10-cp310-cp310-win_amd64.whl", hash = "sha256:e6b99181d184d0f5c7b36b8d12b94d1e9499cce6246594331f9edc5d2ea9fceb", size = 278251, upload-time = "2026-09-09T20:56:42.54Z" }, + { url = "https://files.pythonhosted.org/packages/c1/c8/b3f8dab9592d1089fb2cec58f5c1120ce1c5c74b2d12d50119e404187ee8/regex-2026.9.10-cp310-cp310-win_arm64.whl", hash = "sha256:abbfc1c33bf8efddcc43844aba61e036d74a918680dc3ce8ce2538b004eda0f9", size = 277308, upload-time = "2026-09-09T20:56:44.141Z" }, + { url = "https://files.pythonhosted.org/packages/65/82/36fdcd669c7c47e11bea67e3495011ec365be0942aae3ba74f82f92ed6a2/regex-2026.9.10-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:debc629e98b95abaea1cf3057ca296151f348c697c9b8a59d18013adb302c0dd", size = 493891, upload-time = "2026-09-09T20:56:45.767Z" }, + { url = "https://files.pythonhosted.org/packages/c8/57/bb6a8273bcb53077a1855fea0ff416fe4d4bcb1d9f350a9fe3b909853983/regex-2026.9.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9d772586951d7d6a5d162d48f414065e483b1c81ab38fd8ed97c78b05883421a", size = 295107, upload-time = "2026-09-09T20:56:47.359Z" }, + { url = "https://files.pythonhosted.org/packages/38/71/9d96d04c36556f057398a08dfc21aecc5409b5fef83fd7653a310227df7e/regex-2026.9.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b43456de605c8ee77eb75f07bc1ee44ba27f9cee22207deb77d495e954b7d953", size = 290649, upload-time = "2026-09-09T20:56:48.731Z" }, + { url = "https://files.pythonhosted.org/packages/af/cb/4dd3b7190b5a169c81a4d63b487d65e9fb23689b771f5aceb91d9e7f65bb/regex-2026.9.10-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f0a8b4928823bc8b217a1ab7bf3d90598909dec9a70fbbfe9a52cc4eca55990", size = 796358, upload-time = "2026-09-09T20:56:50.021Z" }, + { url = "https://files.pythonhosted.org/packages/3e/bb/aceb2329a2d54a3bbd95e54f08689692414674c213e8fc958cf253206e5e/regex-2026.9.10-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b91c37551bf39d75116c02b146956f65b9aa0337a4a652f4ae186983789d4001", size = 866258, upload-time = "2026-09-09T20:56:51.363Z" }, + { url = "https://files.pythonhosted.org/packages/ba/fc/f8deac9c4c475b6bc35f25565a2253df4d27d49dcb6b189d9432ae69294e/regex-2026.9.10-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0b9ba3b2765cdfe18f0f561a69f78a69701f2896654a81c711108d35d14e5099", size = 912779, upload-time = "2026-09-09T20:56:52.811Z" }, + { url = "https://files.pythonhosted.org/packages/74/dc/2dd4d56932f55ea412c9808f27a8661f93568bfee596ed04bc57a74e591f/regex-2026.9.10-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0acee94b480dd853e39434aa9a575f95385b1b4b8fa3feae56db363ca5cad782", size = 802962, upload-time = "2026-09-09T20:56:54.241Z" }, + { url = "https://files.pythonhosted.org/packages/46/a8/43edbb94e6c9caffd08bffca5b5c486d18845569fcad3953565b85dd71f1/regex-2026.9.10-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a41693eb3fc4b92e6127d113813c6c395237f7edd3224abf67609af48c690d11", size = 777136, upload-time = "2026-09-09T20:56:55.749Z" }, + { url = "https://files.pythonhosted.org/packages/78/2a/6de302fe3dfa94b75a414bffe5180c146967f96fc52fd0b3527a05ad7c40/regex-2026.9.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6888065672b341e5246f391ec16dc258a29218ac784172fd67c30d941544755b", size = 786593, upload-time = "2026-09-09T20:56:57.211Z" }, + { url = "https://files.pythonhosted.org/packages/ce/22/66bad9464b4b9122180d2fec86209d7f1bcaf4388d34f644139799f94a20/regex-2026.9.10-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8c07021a4faa3f092869adbd1f35cdc7a592276c807aeebc3ceb8ff1a638f0b4", size = 861247, upload-time = "2026-09-09T20:56:59.194Z" }, + { url = "https://files.pythonhosted.org/packages/3a/90/a862d6fd9ee3bc0c1f3a2fecffb325097608a4c9b57f0e35030f68c878f7/regex-2026.9.10-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:58c01f7b81079cf0817ba831ff4d9eff5d28be4a3ac76c353e6f09bd63f4c386", size = 767925, upload-time = "2026-09-09T20:57:01.031Z" }, + { url = "https://files.pythonhosted.org/packages/98/4a/aea909c0699aec3c5ee6eff93e4dabc0d310cfb0a5618094e1c5228b234d/regex-2026.9.10-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f0e2e5d23448b660d60a6ed85c46cc03b4b48bd276b8f4041d4a5fe2a4a0626b", size = 850772, upload-time = "2026-09-09T20:57:02.774Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a9/a55f1257d95871b0e042b42bf52f931b3a92b4d530def5201c5b9d597e20/regex-2026.9.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:94d096369b7cd96d15343fef5257fe39eff9d0e8758b92a0e15e358b92cdb2fc", size = 791475, upload-time = "2026-09-09T20:57:04.216Z" }, + { url = "https://files.pythonhosted.org/packages/60/96/04bb3dabe9ac7aa64ff758dc34ae0378d60f5279ccb66788e1f837a0cac0/regex-2026.9.10-cp311-cp311-win32.whl", hash = "sha256:7f8f10015866608fe4c043cec2e4fe4c39a94bb50e45091de4cdf4004b9ae4b0", size = 266937, upload-time = "2026-09-09T20:57:05.726Z" }, + { url = "https://files.pythonhosted.org/packages/0c/7f/2d39cd798871b683409cd14ab4458f60e53db86591028a8244c3155f2e92/regex-2026.9.10-cp311-cp311-win_amd64.whl", hash = "sha256:ce7c118cb102975f974585688357a717ffbf9dddd64ab0bb1bc93eb5b367cf95", size = 278263, upload-time = "2026-09-09T20:57:07.089Z" }, + { url = "https://files.pythonhosted.org/packages/13/5f/eb3a6fd0ef50719d105e77bcc8a94b5b6f4d2873819fcf481279c454ce73/regex-2026.9.10-cp311-cp311-win_arm64.whl", hash = "sha256:1e954e246466d5a1a78f563ce8364b5d7cb19e7adb0ccdec8f9c9610083187bc", size = 277305, upload-time = "2026-09-09T20:57:08.442Z" }, + { url = "https://files.pythonhosted.org/packages/32/c8/bfbe893e90ee0148bd2860dd086f09b5d2080ca2b125f740c2e118c16982/regex-2026.9.10-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:880ac684c27176464c00c3fdc456116364f5ebc70da07aad0c2d4a7ba45e98db", size = 496609, upload-time = "2026-09-09T20:57:09.987Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ac/56d5ae6efb759255c3b3db650a4be25f96a844ea3613b91e1e189a3b7294/regex-2026.9.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b9d36b03dc362aa40ffaaec9d9bd75e87763529563ec008c43b0e07782f5be7a", size = 297024, upload-time = "2026-09-09T20:57:11.35Z" }, + { url = "https://files.pythonhosted.org/packages/60/4b/0f2d5f6bbb791cc10f22f0ed16c487e630dde8fa8fa0bd92a2bfe21a4b20/regex-2026.9.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866de9f98df0611d7b62b3a8729d3284a64c0cc6edd90bb95a533e443a4939cb", size = 291905, upload-time = "2026-09-09T20:57:13.127Z" }, + { url = "https://files.pythonhosted.org/packages/89/51/3fb5fe0d32f4cf0bc982286722c729a8d6f522d2fa2d5d14a702d9fc87f8/regex-2026.9.10-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c66d54042a14a503907d81861b8a5235e6d1f03d4fbc1d8767f652eaf957ac1", size = 800055, upload-time = "2026-09-09T20:57:14.68Z" }, + { url = "https://files.pythonhosted.org/packages/89/46/ee507bd2f9d4420f26a594b35c551d7194b66f5d7897f63730fae6ec05c1/regex-2026.9.10-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:032da15431c890d376f53547f0a6219f4f4cd19f3e4f11bdc321453b5bd207e4", size = 871133, upload-time = "2026-09-09T20:57:16.674Z" }, + { url = "https://files.pythonhosted.org/packages/d1/75/cbaa90689684f91b1bc017e7f8c6d9425c6bd299108db02482dc51376d8a/regex-2026.9.10-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:23ac9a28180f274d7dd7651fa131ad5b02d343b75df4b040737f0356223895dd", size = 919627, upload-time = "2026-09-09T20:57:18.402Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f5/dcdf5e0d898024005cfcce631e3e934d111dfbe177ca0b7f253ae8a735a2/regex-2026.9.10-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e67f8843f0e4b931f1fa860bf3bbe4134b714c0155cc5c7c0d7ea450230aae0", size = 804587, upload-time = "2026-09-09T20:57:19.859Z" }, + { url = "https://files.pythonhosted.org/packages/73/70/eedfe81c29bae266a06ab4250978361a9bccd474704d88d4f4ef4506dff8/regex-2026.9.10-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bb7774924f8cd69f49cba0b3c2d679a6326f777e0e67d130ad5203e4df53f0d3", size = 777320, upload-time = "2026-09-09T20:57:21.62Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e5/86b207077efbcd91305700488f170b7eb1e1c54721cea74175273aa3b9a4/regex-2026.9.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0c32480f3371b75068decaf9e5da72c224e953830dd71e36e06cf80e30ea39d8", size = 790572, upload-time = "2026-09-09T20:57:23.048Z" }, + { url = "https://files.pythonhosted.org/packages/b2/92/f622c3b2323f4c035b98e80221740a442127ad7993135b814f52057430db/regex-2026.9.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d2d377fd1cad611b806cdd732d86b65f536c768209890cb442556548daa65a23", size = 865485, upload-time = "2026-09-09T20:57:24.658Z" }, + { url = "https://files.pythonhosted.org/packages/4a/be/34bd621d3d6ac906ad67e57ed56c40cd45f7d51b9c0328335e97a7cb8ecb/regex-2026.9.10-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c014641157e9049b0603b8daa5343bd408d9b757b709aaa0f373cd3fab2d7944", size = 767925, upload-time = "2026-09-09T20:57:26.268Z" }, + { url = "https://files.pythonhosted.org/packages/0d/28/ddbf7cba86f2adf5038c6c16aa829636ffc6e437f81bb0cbf302899cea5e/regex-2026.9.10-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1562aabd9d4eb09bd88a62ad97ed06800094b529ac43419e43020b9cefec79b0", size = 858800, upload-time = "2026-09-09T20:57:27.901Z" }, + { url = "https://files.pythonhosted.org/packages/d0/f1/e8d7656ff3d3bd32e881d32f540b4981c79dee61908d6b790a45966e6895/regex-2026.9.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2479171edccced52ef02b899558f88ab2c235fe05b93180fdcae1670aacd89e1", size = 791648, upload-time = "2026-09-09T20:57:29.786Z" }, + { url = "https://files.pythonhosted.org/packages/fc/65/eac1a79115c8475d8ff539602eb54031564d719fdea5a599c4b0a1a26d1b/regex-2026.9.10-cp312-cp312-win32.whl", hash = "sha256:239620b0e0681669367c0e218c8eb2551d9f8fe3b9fccfc8d0003377804e8348", size = 267326, upload-time = "2026-09-09T20:57:31.782Z" }, + { url = "https://files.pythonhosted.org/packages/ad/d4/4dcd0a05d3e97ca829165df1c39717f899d1d484dac8a6032439f2cb8d6d/regex-2026.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:4db7d00c4afbfbb55b8e17b1e371da11418ea9389b030acec63c1fa4c7ad4b86", size = 277933, upload-time = "2026-09-09T20:57:33.648Z" }, + { url = "https://files.pythonhosted.org/packages/55/f8/22617a80dee28f2451011eae36bc26b3d78c4994ba87b5281d60acf9b6c0/regex-2026.9.10-cp312-cp312-win_arm64.whl", hash = "sha256:c25a754bb81a2edcfc3b65eda50f017d736f818112ed43e8aafd595cb00678ae", size = 277447, upload-time = "2026-09-09T20:57:35.156Z" }, + { url = "https://files.pythonhosted.org/packages/20/90/d4452bf1ef7dbe406980e8b921a257024482203c1dafac535eae207611bc/regex-2026.9.10-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ef5a059ea1c6ee5d1c7e99a2484e628608d010921efe876c6f0e2029d2f35eca", size = 496408, upload-time = "2026-09-09T20:57:36.757Z" }, + { url = "https://files.pythonhosted.org/packages/6a/35/c763c6424a0f99d021d46dc1f9065147bb5a40c2b2cdf28d2ebdbcd96508/regex-2026.9.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dce932f8e3ba936475ea3d0d8b59f7b050a9e206e994f53f8fd80299871e87da", size = 296931, upload-time = "2026-09-09T20:57:38.811Z" }, + { url = "https://files.pythonhosted.org/packages/fa/68/241f88458b17c46ed2f80147a60a03b2ada7fb815c23b6bc76c298abb0a5/regex-2026.9.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d8c668af8f7bdb1d18739c27d30cd9f4b371495a883f75a002fb7a39d740fecd", size = 291741, upload-time = "2026-09-09T20:57:40.482Z" }, + { url = "https://files.pythonhosted.org/packages/90/9e/974d6de404c63e2d09525f4ddb99874c7ab8e1f781ccbe0dd3e26fa6f6e5/regex-2026.9.10-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6aebdd9a946de328b3f6f61dbf48dd064a36eb6dddf96e34ae6651d37f6e9383", size = 800088, upload-time = "2026-09-09T20:57:42.098Z" }, + { url = "https://files.pythonhosted.org/packages/9e/fd/3875b73f9e7ba3321dcaa02c19f650c05c61345328acf84599ac6f45ceed/regex-2026.9.10-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f2374c27deb189b282ec7e16106752c22ad39b056bbd8018960b1e4cc95d67a1", size = 871212, upload-time = "2026-09-09T20:57:44.03Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f5/2358e791c0e171194dd6a8b97b520579098a21397fb79dbe6b7edc9e3fa7/regex-2026.9.10-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e0dc78251154b66dc60211563fc115345da332eaa881e4e2523fb1edae3772f4", size = 919752, upload-time = "2026-09-09T20:57:45.691Z" }, + { url = "https://files.pythonhosted.org/packages/20/3b/000c79c3f9c06b7542225a5d3a7f9a85405da7224b3b9af94a491d07abea/regex-2026.9.10-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bafa41b0dd63669e5c0f8adf3d24819efeb73c847f492eb011212eb352e69041", size = 804578, upload-time = "2026-09-09T20:57:47.548Z" }, + { url = "https://files.pythonhosted.org/packages/30/6d/195eedb1de87f26639191e7487e41eb81e2ce255bc7563a64f3f5a95eb08/regex-2026.9.10-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ebb2ba68e4641a994061f70bf44ed448fba0b9b1d18c94ffb9efc1cca805b39b", size = 777345, upload-time = "2026-09-09T20:57:49.63Z" }, + { url = "https://files.pythonhosted.org/packages/79/11/11fe2b313fcd92cb75c583648f2746031b9f4da9e9ed4241204a5e8b3721/regex-2026.9.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:048a89ee797db10160bd2bd519286577a6b43a100279bd4b7d8456a3d69c80a0", size = 790556, upload-time = "2026-09-09T20:57:51.27Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c0/07ec9b4c43b0e16d62454971a5ab3886eccb0bfa161300a02d801ab28620/regex-2026.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:79e9432995e14c749d34209413de5e621ec8e67789bf4f46dbfabea9d06a2406", size = 865572, upload-time = "2026-09-09T20:57:53.163Z" }, + { url = "https://files.pythonhosted.org/packages/19/07/43bc9a9cf9fc8e37d2ba47980dfe4a6e151d2cf3ab969e0031e2a9b21484/regex-2026.9.10-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5847e22bbf959764d776937d791d034cc2d19b787e361c88d97e859e8dc68502", size = 767971, upload-time = "2026-09-09T20:57:54.805Z" }, + { url = "https://files.pythonhosted.org/packages/9c/49/3b9286a3a94f3c89ed4ddbe74e72bdde21c1a5eadd520d5f4ed4a61936cb/regex-2026.9.10-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c103b3b14e011774af4fb7e4617ad4d72b9171905cd3b231a70a4efd76e477d7", size = 858835, upload-time = "2026-09-09T20:57:56.627Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9e/e5d27ce9fee8e3ef95f886c7b6ecec211efa4cfc18bd73bd5cf26cca4741/regex-2026.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6b34a778c695d24e77c140e3b4c95da69282e34f2f6b02b55656aa4a0379f643", size = 791793, upload-time = "2026-09-09T20:57:58.313Z" }, + { url = "https://files.pythonhosted.org/packages/63/03/c28a6bebedc3e2d86ee27ec2de16f7ec0419dcd10e771d43dcc9c58a2e99/regex-2026.9.10-cp313-cp313-win32.whl", hash = "sha256:7abb38b8c40f3a235235a44da452c64b7b5c1d650ec6351027db0e090804f2e5", size = 267298, upload-time = "2026-09-09T20:58:00.009Z" }, + { url = "https://files.pythonhosted.org/packages/cd/fd/5c85fa6cfb8e034080bda5a72fa0a4df2b7777a35eb7e73c2799c2adda7a/regex-2026.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:20e8bfb07ad79a282f8b95b56fe67f9750b1b7f775724e4ba1f23cb296115ce4", size = 277894, upload-time = "2026-09-09T20:58:01.731Z" }, + { url = "https://files.pythonhosted.org/packages/c1/28/f5a25f6f65501675977fda35d9f61abb1468c4b87c0f73e536d8b21a60b8/regex-2026.9.10-cp313-cp313-win_arm64.whl", hash = "sha256:3bdeed3318a8eb2bbadc9c56347e0ff651639e934a47e168d05a3b12929fd0e7", size = 277436, upload-time = "2026-09-09T20:58:03.422Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ed/98e9b07d8bb9c765d07774f0b2c19b301b96d51f44630fea48951051c94e/regex-2026.9.10-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fd6bd89b9fc06018d35851cab0240adb7dd84d51941b19f6574ac90cd54e3ae5", size = 496662, upload-time = "2026-09-09T20:58:05.118Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a8/9dfe9be48378b47c5a8f04b0f200ea225f9ee0f8e93f010433f661a37878/regex-2026.9.10-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ef4c0a9dfdc90581b90b1b95a8c3d1557f8ff8f5a2a53536d26314de699d1468", size = 297115, upload-time = "2026-09-09T20:58:06.822Z" }, + { url = "https://files.pythonhosted.org/packages/2b/e4/5d1f005a3825ec49842ad349061c1c26e6d42f47ccf105e6e5aa6aeed392/regex-2026.9.10-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:14caa05ce39ec70437af5aac8814c50ee6628f4a90353871c059692f448a164f", size = 291896, upload-time = "2026-09-09T20:58:08.675Z" }, + { url = "https://files.pythonhosted.org/packages/be/15/44ce83fca50c6058f42b62fa8300a8030eea7e4e5a973a2dd33db0f557fb/regex-2026.9.10-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3264132d576847ab5f88bb83e7debe67854bf165b3ea613bd467312b6099536a", size = 800534, upload-time = "2026-09-09T20:58:10.339Z" }, + { url = "https://files.pythonhosted.org/packages/af/6e/a62e070a5a033643b287489576f02ae6a9c584c337d62349e340a2b4d001/regex-2026.9.10-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5cef9f3d14796500ea834c41dbe688f1f6b23c7024dc23e8a794d7ebaf5d71d0", size = 872038, upload-time = "2026-09-09T20:58:12.186Z" }, + { url = "https://files.pythonhosted.org/packages/f3/06/8b8e2483949b1329df10c5b615e85d332066deb426b11332b799629b9201/regex-2026.9.10-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d278ad30ec83b6b9202685b0f80b741a51ea3ca7f0595ebda96e7628b6398876", size = 918927, upload-time = "2026-09-09T20:58:13.903Z" }, + { url = "https://files.pythonhosted.org/packages/bb/64/9b56f69100d3afdbc9c4fa6e302764f9cb717fbc06a9d50558d98ca89cd2/regex-2026.9.10-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05fb018cfe7144585fc83882405906ff84994a2d154afc2509ecc7752c51f864", size = 803694, upload-time = "2026-09-09T20:58:15.949Z" }, + { url = "https://files.pythonhosted.org/packages/07/43/d00d59a7c8fd0e070ae8457a8743597f45ad9682b100f57b9c9c405fbdfd/regex-2026.9.10-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6fd555fc9abef50c530869690b2daca054c8811a7aff632d11f9a7b2590b2742", size = 777770, upload-time = "2026-09-09T20:58:17.628Z" }, + { url = "https://files.pythonhosted.org/packages/46/6b/a11d0446484efbc9eb67abec133f254c6d66a1568b8f3fb36d39a73a1129/regex-2026.9.10-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8d5c4518235a2ec1611e57af85fa488d529c1106aacff12adadcedf8687012cd", size = 791234, upload-time = "2026-09-09T20:58:19.476Z" }, + { url = "https://files.pythonhosted.org/packages/12/09/bcd24e78b373fd4f98090caa43eade439223f6703b909be79b9efd9ab0ab/regex-2026.9.10-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:175cf49ce7a994c88b8f15e3cb17cdb66a48ebb2d36de736b8205033db950f89", size = 866259, upload-time = "2026-09-09T20:58:21.683Z" }, + { url = "https://files.pythonhosted.org/packages/8b/c6/c57ba5e94222a813260af4c17ced92d40cdb44737eb6b50979688310b6a3/regex-2026.9.10-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b71649169a9fcf30b395ee01047fa7ad6654a4c900ca75b23c04dedcce6a1f8c", size = 768219, upload-time = "2026-09-09T20:58:23.842Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a2/3820037587d00901ace96c5864335c2cd1b899d5263ea0dd2261359e0bee/regex-2026.9.10-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8ba1f78bd4fef2d8f84b894ec28ac3481afe6cc07aaa253ad4717ef7b3fe6bcb", size = 858582, upload-time = "2026-09-09T20:58:25.607Z" }, + { url = "https://files.pythonhosted.org/packages/47/f0/f9a838ca6219ae4821de0175e4548db73ef56de5ec08d032fb427732fa07/regex-2026.9.10-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:217e98ba5fc8908ed8ffd4ebac04753a0c831067cbfb495b9821b94cc61eaa76", size = 791405, upload-time = "2026-09-09T20:58:27.406Z" }, + { url = "https://files.pythonhosted.org/packages/f7/bf/67d71cc4e13ae2e0022d21243ca069e0868701a99eb29cdecd13d356e694/regex-2026.9.10-cp314-cp314-win32.whl", hash = "sha256:b298cdc33c5cc6969ff07f0fba19cc73e0fd8576373c50935feadaca2f6b4405", size = 272702, upload-time = "2026-09-09T20:58:29.116Z" }, + { url = "https://files.pythonhosted.org/packages/c1/38/40a93e72703a741235115ed1b1e5f6b869917677b7643005034ce1611d70/regex-2026.9.10-cp314-cp314-win_amd64.whl", hash = "sha256:c32818b28bcd153b25b63038348a9fe9b9fbcddb60df43f204c3ab55eeb57f77", size = 281170, upload-time = "2026-09-09T20:58:30.899Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ac/e95387f00617c16bb41b786d900bacd17eab88afa81b4f263df532b3a731/regex-2026.9.10-cp314-cp314-win_arm64.whl", hash = "sha256:75242f44a3e283106077be4ab717bc535e4701c9d54ad69e195945c22f137a1d", size = 281511, upload-time = "2026-09-09T20:58:32.594Z" }, + { url = "https://files.pythonhosted.org/packages/39/e5/a4b12262edc488a8a7a95b672db317dd8aa9bf2fab98297f9c91bb11ad4d/regex-2026.9.10-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:2dd9286093c71afc8f55ef035c5b9d2776641fd72c6535f1febc92d0b0be9666", size = 501128, upload-time = "2026-09-09T20:58:34.306Z" }, + { url = "https://files.pythonhosted.org/packages/55/c8/9ca31c0fa5197ded8614c8ae0e105bcff2d979025ffa3c75580baa334e2f/regex-2026.9.10-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:71879292c9c7ac67b1680345b16daba1be937cb027362cfa04e68f65db2dcfdd", size = 299428, upload-time = "2026-09-09T20:58:36.296Z" }, + { url = "https://files.pythonhosted.org/packages/2b/d1/ee7662561735f90475443c3ca1977e5cecaeaa8f29620dec75580aebc839/regex-2026.9.10-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5ccd139b2061132e7b265cfb4b4721baeb9f8928b81415304abf1ec7e3181c26", size = 294494, upload-time = "2026-09-09T20:58:37.964Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b1/333168e45ed6cfe71f6d17e21e5f54725f44d171f84edd12469a2f739227/regex-2026.9.10-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e7327795089ddb44912dce1434e1d7244be2e9fb48fcc2d6782936af7a3062db", size = 814925, upload-time = "2026-09-09T20:58:40.438Z" }, + { url = "https://files.pythonhosted.org/packages/bc/a5/df1b38536d0a3b24a030eb4130ce98b403cc925220ff530f5313a7c436eb/regex-2026.9.10-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ff4d7b14ea19e50c8d9d6d83f45bd9b45cbb624c07ac1fa54db0a019049abed7", size = 873323, upload-time = "2026-09-09T20:58:42.349Z" }, + { url = "https://files.pythonhosted.org/packages/93/ea/aa71fe62dd63a8336bbdec1ff002a6c53b40ccfca95961c18ee4f09bf03c/regex-2026.9.10-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4f0407474ffac8e5e89d93ca41d60891e29f0ab8423eb66ff292d850a86a0843", size = 923080, upload-time = "2026-09-09T20:58:44.488Z" }, + { url = "https://files.pythonhosted.org/packages/86/38/49f8d6fd34fc1a9c75b5b96364ebdde702a8144e5ed63d2213c58d454c27/regex-2026.9.10-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1ad10a135fa0b4e4a462a61d07c6654d7518cfdb5cb8da08f9ff7d61384af1fe", size = 821284, upload-time = "2026-09-09T20:58:46.56Z" }, + { url = "https://files.pythonhosted.org/packages/32/45/91a977c96d4be13d1ade8208c260c26841c836d4c91745e1828a30589070/regex-2026.9.10-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9fbd2e5d8002dc49a6129fb321ec51c57a025e752ed525ddce0ba9223c4350a7", size = 789256, upload-time = "2026-09-09T20:58:48.473Z" }, + { url = "https://files.pythonhosted.org/packages/2d/79/4d110bf01bf9651bf9b3f88a6d9fa7e643e0586921a18432b25a478edbfa/regex-2026.9.10-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4a761ea45f2ad74c575ef5850ea514cef97302a552d3c7c9d1a1a870d4661d6c", size = 803722, upload-time = "2026-09-09T20:58:50.286Z" }, + { url = "https://files.pythonhosted.org/packages/e7/67/b587a0d3bbac2635309ed9c40c197120c39e1ec0afb8813cbed1338fdd75/regex-2026.9.10-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:75aa39d3f4f1650eea84e46b0d8cefe77dd5478c10e3d0aaf0b0f00493475a7a", size = 870085, upload-time = "2026-09-09T20:58:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/07/fc/0827bca20ddba1d70fa5111a2a64e6b6b38bfdab43fc435022b54172a111/regex-2026.9.10-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f5c629df03adec31ee505dda3c8988f106c9390e4cbd343600036eb8b3d6724f", size = 776970, upload-time = "2026-09-09T20:58:54.478Z" }, + { url = "https://files.pythonhosted.org/packages/9b/03/ab9d08d30568ca868791bfb99551db60b947f05e2e65dcd1261e87083c21/regex-2026.9.10-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3a66e40a1a20de96a2fee00ed67e11012b62d85b277688258677fd19997addb7", size = 863611, upload-time = "2026-09-09T20:58:56.432Z" }, + { url = "https://files.pythonhosted.org/packages/5e/6d/8063ae86b543ae878a7d6e7ba21ebf0af4af06161230e0012e2d652320c6/regex-2026.9.10-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:968c1e33edd9a104d1bf24c8d476c72de7e3839ae7f894b37e9e4f4739fdeeca", size = 804064, upload-time = "2026-09-09T20:58:59.066Z" }, + { url = "https://files.pythonhosted.org/packages/c5/63/b19305c4b8d3d7867699f7d83c43550273d91a2f31793452d87edb1d259f/regex-2026.9.10-cp314-cp314t-win32.whl", hash = "sha256:fbc4e2f3cb7ce8436154e6483079e7d35eeb321a952fa936e180300630d8b873", size = 274607, upload-time = "2026-09-09T20:59:00.942Z" }, + { url = "https://files.pythonhosted.org/packages/32/b8/1695072a512a49060294024e23b945eeb87675b02c06e53a92a1b42b0bfd/regex-2026.9.10-cp314-cp314t-win_amd64.whl", hash = "sha256:c37fa93bf18bf4f90b01c0fa9f11ea567ee4b7dd8bf96e63663e5edc37aa38cf", size = 283944, upload-time = "2026-09-09T20:59:02.876Z" }, + { url = "https://files.pythonhosted.org/packages/a9/9f/65bac17f39991a67e22f8b3c849fdc02a56147c97029b5351494341959b4/regex-2026.9.10-cp314-cp314t-win_arm64.whl", hash = "sha256:ffc2da104e43db716ce30cef9f28049a1faa6aca385dd8771b033268d0730b07", size = 283780, upload-time = "2026-09-09T20:59:05.009Z" }, + { url = "https://files.pythonhosted.org/packages/67/ca/1d1f83bc2f8fff4f186266ac82d73254e686565530cec9ab5228fb5c63dc/regex-2026.9.10-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:6afcad14310f1311d077553ed374b42a5e538f85a8c884b4e38e52de091c8077", size = 496869, upload-time = "2026-09-09T20:59:07.051Z" }, + { url = "https://files.pythonhosted.org/packages/33/42/4217510286501a2ebcd372b781b4754ac961e043fa13ef8dce803c44d89c/regex-2026.9.10-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:3fb4ae8cf83ef4e9addd43b2da31a9f45be816a8036fae8af59c8998b72718e2", size = 297121, upload-time = "2026-09-09T20:59:09.007Z" }, + { url = "https://files.pythonhosted.org/packages/55/a7/595468ed0bbccd94be92c6b5d67736ba128b204d942429a8485c2693914d/regex-2026.9.10-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:f7d4656e17ab736e9415a6442a345bfc97bb8b7dcce47884bb74a37f70f08d0c", size = 292139, upload-time = "2026-09-09T20:59:10.859Z" }, + { url = "https://files.pythonhosted.org/packages/29/1c/ac92c123e0ab9bea75a904272171b356940bd6139e4a35de44f2254dca8f/regex-2026.9.10-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35ba3bab0c45079735f55ac61526774de1d84bc4a0333cc554e1a4ab74913924", size = 802375, upload-time = "2026-09-09T20:59:12.823Z" }, + { url = "https://files.pythonhosted.org/packages/8d/16/d349f6fa9f908162359004e4f067353a0146e8074d24989490778244be44/regex-2026.9.10-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ff6b3267318661dfddf6b3628663e00e5946bd0a5c8fa678537a1401f0388f91", size = 872328, upload-time = "2026-09-09T20:59:15.352Z" }, + { url = "https://files.pythonhosted.org/packages/df/81/251b5aef23147057e926346fdb7c8c352d0568f65a492e4e9eb6120f6446/regex-2026.9.10-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:030fa9e23624e39b3b94e46b90a5abd1a1678eb2f58fcdd3fd6c27526bf91c7e", size = 919594, upload-time = "2026-09-09T20:59:17.31Z" }, + { url = "https://files.pythonhosted.org/packages/d0/6e/1f25319dc1b9cf4b7ffa303f3d16ca53260fe92aff45e81aa1b3c6c7cba2/regex-2026.9.10-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fbc8314436353e097c050e11b01a6c11433579437ed0579730157676ef59e2f", size = 807394, upload-time = "2026-09-09T20:59:19.328Z" }, + { url = "https://files.pythonhosted.org/packages/cc/fc/3da6b3dffddf12d5e96cbbe6f5e65ebcd64f92e3388a6690a53e0878232d/regex-2026.9.10-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7e6c0b5ec6ddee4032247585dc491b0fa58627745b66a705728703a3f0331231", size = 786018, upload-time = "2026-09-09T20:59:21.592Z" }, + { url = "https://files.pythonhosted.org/packages/bb/6f/1cc86dafddc912ef44c5ccd4f729060be8c6735600932664eb6d0e25469b/regex-2026.9.10-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:bf29611e5376fec8f795879bb5c6153a76c3a292573d173c26784042b01eb840", size = 793504, upload-time = "2026-09-09T20:59:23.734Z" }, + { url = "https://files.pythonhosted.org/packages/b1/cb/11692e29388d006211163627fcefa0c79917ab9f94d46a1b2254fe5efc81/regex-2026.9.10-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:ec8855f08c17895a26fbf5f19ed829722e19b34a96629e49a43c92974924026b", size = 866766, upload-time = "2026-09-09T20:59:25.798Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/3631df969830f94d83fcfc5fc71a7b39caad905e18f7b17350a94135d625/regex-2026.9.10-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:94c5ce3bc41d226b4eb89ca3f842b2e28c031487fb1f34eb2153d98235831325", size = 775825, upload-time = "2026-09-09T20:59:28.51Z" }, + { url = "https://files.pythonhosted.org/packages/4e/8a/762892a8e3e21d119eacaffde848aa247e6cf4e1d90c46011671e86b1b9e/regex-2026.9.10-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:9ce239acb15843ab03976626af810a4424b0409689ec2bbc52088ab5479ab487", size = 858901, upload-time = "2026-09-09T20:59:30.656Z" }, + { url = "https://files.pythonhosted.org/packages/3f/f0/78048fada5c2f61d795efb26ea24f820a5564c4aa26574920284d45cd656/regex-2026.9.10-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:c22df8dd6373bbe3898e77429ffc85594300e39d752fd0e68a31e59d37899376", size = 796208, upload-time = "2026-09-09T20:59:32.852Z" }, + { url = "https://files.pythonhosted.org/packages/aa/58/632681f7b9aaa3d83b40e5862ba46364a453c8eb2bc7d43fece3dc31f972/regex-2026.9.10-cp315-cp315-win32.whl", hash = "sha256:1aa309ab7ba89a62d6cf70dbd38d4176440bce3c7001ab86256704cf4c18c6eb", size = 272704, upload-time = "2026-09-09T20:59:34.927Z" }, + { url = "https://files.pythonhosted.org/packages/5d/64/81cce28754c37037b1fe740b6d7a556d51d97cf935ea4547bfab104f43f7/regex-2026.9.10-cp315-cp315-win_amd64.whl", hash = "sha256:58da726d3e766c0b3f5a3997dfaf0275898a1107b8191cdd6b0437fe45fd817d", size = 281182, upload-time = "2026-09-09T20:59:36.86Z" }, + { url = "https://files.pythonhosted.org/packages/33/28/5a13a340c9c759e863a0e7f765d323601d03d6538c8a627ed628662e083b/regex-2026.9.10-cp315-cp315-win_arm64.whl", hash = "sha256:75f9297b16fcb588a1f8d8a55dabef3c0c20b0c7bac43c87ceaaaf1a825c12f4", size = 281512, upload-time = "2026-09-09T20:59:38.875Z" }, + { url = "https://files.pythonhosted.org/packages/f7/38/a3caebcd5105be90708071db20bd261b0961b8ea4fe5e8be45c2632519b5/regex-2026.9.10-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:1270cdec69248592bbe38a0b263ed58d907b891bd2b93703e225c317e421bda1", size = 501336, upload-time = "2026-09-09T20:59:40.987Z" }, + { url = "https://files.pythonhosted.org/packages/ee/b5/ec8887b2658bf0a5df143c7c1fcd562b0abd2fa208c04ebf15c6607c9bb2/regex-2026.9.10-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:681ed38664b64c6617d3c3c332018d1948c77e139c5ea667c1886efa671e426f", size = 299318, upload-time = "2026-09-09T20:59:43.039Z" }, + { url = "https://files.pythonhosted.org/packages/d5/58/84724a9eccf6e8cd46f7e4534576093e2476a5eeb55e2453aa6606e86059/regex-2026.9.10-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:8e127d9a80cbf1c3276bb465c6d047e8705e97b58c2b8f2f0c0a69c336b44b37", size = 294847, upload-time = "2026-09-09T20:59:44.954Z" }, + { url = "https://files.pythonhosted.org/packages/50/03/70ccc5e53905984abf8eab63eebd3ce740522ef8c8d392622b64d79ef290/regex-2026.9.10-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:990797e765d89a423880052c68b61c31afe701de94a8c060f61c40605ca6c727", size = 814359, upload-time = "2026-09-09T20:59:47.194Z" }, + { url = "https://files.pythonhosted.org/packages/29/53/40f7a11ec547e4a947883c9d5e8a075f6d4f59af2b8dbcaa8bf5b504aca0/regex-2026.9.10-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e5e4a6e0734a685d13b9685622bb503bdbb2927f8b0df025a5085f0ea067475b", size = 875586, upload-time = "2026-09-09T20:59:49.537Z" }, + { url = "https://files.pythonhosted.org/packages/0b/9d/83f3e022d99ce601727c4ef5f7b527753901ce4509ed89a1bb6a2263380a/regex-2026.9.10-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:63bb62cf62217dc38c8a6b2b61b165b0e4eb8fa93b0aba12139251c0986a8fa3", size = 920990, upload-time = "2026-09-09T20:59:51.776Z" }, + { url = "https://files.pythonhosted.org/packages/b2/64/dfdb367d8f4f5c9b8ccb4b59789c2ce996f2c69c3b8192aa986fe7d92ec4/regex-2026.9.10-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cb76a9c4e07a6a47849726af0ed14c41741a182f097f134a8cf29c1bc0f4dde8", size = 818660, upload-time = "2026-09-09T20:59:54.428Z" }, + { url = "https://files.pythonhosted.org/packages/88/a6/fb7d0b64487913845834f319aa84f8377d55305958a5db7e19c128798366/regex-2026.9.10-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:31e4df2b11d48f61d511019bc1ee9b477055f17c352b68fe72db7a98b14d603c", size = 794976, upload-time = "2026-09-09T20:59:56.799Z" }, + { url = "https://files.pythonhosted.org/packages/3e/1c/23484edaae387ea0d31f4d414463211e3516c8aa210ce8d0640f5aff3502/regex-2026.9.10-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:cf377960d2ac37d987394a9dbaa75e91338c41a46d41e1d25e90125e7b3ee2dc", size = 804081, upload-time = "2026-09-09T20:59:59.39Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e1/e30d138f13aecec99ea9aecef7e31563de6ec6a2f5ce1d65fc506aee33fe/regex-2026.9.10-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:c8fbd9cb30c68c1686b94029b9ef845d5870d3d65baf66cb126b676849b9d72b", size = 870430, upload-time = "2026-09-09T21:00:03.304Z" }, + { url = "https://files.pythonhosted.org/packages/53/dc/81f9ce86f7ae4f57901543597c95751fa01c41a672ec1636dd913f8a000a/regex-2026.9.10-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:53e182b6b04d0011909b47d51a2d72d908de07c7b1c7f16b3adda2204d723bc1", size = 783327, upload-time = "2026-09-09T21:00:05.68Z" }, + { url = "https://files.pythonhosted.org/packages/a9/1b/d7bf8f91534740f6a8ca17e5ac9c5337903baf527c8de240fbac1bbedfd0/regex-2026.9.10-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:0aa7589394230e0f0a422ab6b90841ff12c87e855e7aaf75d192a54a5f124548", size = 860874, upload-time = "2026-09-09T21:00:08.41Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1d/52cc88364aca7c9013dfe9abe0fea67f8d394efe384d07798300a6e2f27d/regex-2026.9.10-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:1b891f77554bff991804cee24b78b40789f7d5993a24c7907bc7025fd2a70c8d", size = 806705, upload-time = "2026-09-09T21:00:10.768Z" }, + { url = "https://files.pythonhosted.org/packages/cb/51/37a194df7707f92f33173260ba7b221d4ec376419a53babaec50019a2804/regex-2026.9.10-cp315-cp315t-win32.whl", hash = "sha256:5bef622850cf760154719d4e0d74b0a855962432995168e250069899ae12fe8f", size = 274780, upload-time = "2026-09-09T21:00:14.003Z" }, + { url = "https://files.pythonhosted.org/packages/77/12/3227a52970d90908b230f15b2c86df903f49ac72c9eedf4f6e8b5bb5e1a7/regex-2026.9.10-cp315-cp315t-win_amd64.whl", hash = "sha256:07b45ba5c94b8fcb30cb6c56a11f715c57533a3017964504322ea52690a27b72", size = 283936, upload-time = "2026-09-09T21:00:16.275Z" }, + { url = "https://files.pythonhosted.org/packages/f3/bc/c567c5a61671f04d30e83f20b496b465432879196574d051007285576205/regex-2026.9.10-cp315-cp315t-win_arm64.whl", hash = "sha256:f70b9f0e39c2dba1d9da6bf7ef7c377cad7277f8440e9a69be05ede529ff024c", size = 283747, upload-time = "2026-09-09T21:00:19.113Z" }, +] + [[package]] name = "requests" version = "2.33.0" @@ -2815,6 +6395,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, ] +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + [[package]] name = "rpds-py" version = "0.30.0" @@ -2963,15 +6556,240 @@ wheels = [ ] [[package]] -name = "s3transfer" -version = "0.16.0" +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 = "safetensors" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/06/f955dbbb1859e3bd23c8ac6141af5106e7ad5fedec4a3a6e3d60f94b7001/safetensors-0.8.0.tar.gz", hash = "sha256:fabaf3e0f18a6618d9b36560682562157f77c2b71fcffc7b432be2baed9d753d", size = 325846, upload-time = "2026-06-09T07:52:25.563Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/a0/f718cda65b05407d228f97602cf60dca269c979867aa5beb25410de26cd3/safetensors-0.8.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c554f85858e05226d3c2828e32395e677434685d6d94594a41643361c5e837f0", size = 473568, upload-time = "2026-06-09T07:52:18.829Z" }, + { url = "https://files.pythonhosted.org/packages/f5/b1/fa7c600e7dceae12e9606c7578cbc9ff1e1ed55844883ee5c92205e86226/safetensors-0.8.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:c80201d22cbf405b80647a60ada77bba06c8fba2da2743ba1e89cdcc39a81f25", size = 484562, upload-time = "2026-06-09T07:52:17.518Z" }, + { url = "https://files.pythonhosted.org/packages/09/7d/65a7de0af421317bb36a067241e4235fff194eed60b961ed6d3f59a3fc60/safetensors-0.8.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a46e5ff292c356d6991e60942ba7f79817682d3a2cef0702136448cb9c4d235", size = 502844, upload-time = "2026-06-09T07:52:07.624Z" }, + { url = "https://files.pythonhosted.org/packages/91/4f/3175c9d75634e0e0dda0082794193521035edd7c70a6f212bf33ca06ddf4/safetensors-0.8.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4124502b78f03534117c848f87a39b8f31e577b15eff423bf8bfb95f2a8c30d0", size = 511823, upload-time = "2026-06-09T07:52:09.565Z" }, + { url = "https://files.pythonhosted.org/packages/20/87/846c289e7aa2299eff406335717cf43ce8777194ece8aad75772e0411615/safetensors-0.8.0-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7bc0a787ba8a35be368ee3574edfa2b1ad389eebd0a72e482ae275490e3f6c98", size = 633461, upload-time = "2026-06-09T07:52:11.128Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/8d64d9df2c45d5ded401df889d0ad90882804ca172d79ec4f0df8f727fe0/safetensors-0.8.0-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:040070828e36dc8e122178bbbd5830ff9e97920affb84cbe0f46442497bed358", size = 545148, upload-time = "2026-06-09T07:52:13.603Z" }, + { url = "https://files.pythonhosted.org/packages/28/50/f203ff3a3ddfe19308efc83c5a3a29ed02bf786732ec35e68bf9162f3365/safetensors-0.8.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd6f3f93c9a0a7cc2788ee63fb763353d4bd2e89b0751bc78fcf7dda00bea774", size = 516040, upload-time = "2026-06-09T07:52:16.29Z" }, + { url = "https://files.pythonhosted.org/packages/46/fb/cdaed17ceb2948784fd9c36b6fd3e951b608547cea81a48e8ee6f8cfdfcb/safetensors-0.8.0-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:fcdd41ec4628fee5799f807c73c353629130fbd942aa23d83c623dd6c9d52d78", size = 513832, upload-time = "2026-06-09T07:52:12.37Z" }, + { url = "https://files.pythonhosted.org/packages/0d/49/1e15de264dcc3b77943d2d0c56a95809956883b1c2d6d585c792523f180b/safetensors-0.8.0-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8e9f537aa183a38ace122d27303dcd986b26bd2a7591f9181d7f0c396f4677ca", size = 559930, upload-time = "2026-06-09T07:52:14.743Z" }, + { url = "https://files.pythonhosted.org/packages/2a/43/bf38443278eab4b1be1fce2931e2b012ad9cb7df52ada751d0aab8f7659a/safetensors-0.8.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:87eec7ffed2b809f05a398a8becb7d013f19f7837cd15d9748580d6cf30dbaf4", size = 678670, upload-time = "2026-06-09T07:52:20.032Z" }, + { url = "https://files.pythonhosted.org/packages/72/e3/68cd3fa5b48488e84add63e04cb12f3bc28ae4638c06d4508c6e88823d0e/safetensors-0.8.0-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:4a95ae2b05d7726d751da4ebf626a2ca782b706e101bd894c95bc2450b1cffcc", size = 786679, upload-time = "2026-06-09T07:52:21.322Z" }, + { url = "https://files.pythonhosted.org/packages/29/4b/1c19c509d56e01f4fbb3d0a2e597450f6cc04d1d56cf52defb0a62dfd715/safetensors-0.8.0-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:3ae091f16662658bdc019a4ff6cb4c085bb7d725eb5978b183ffd265863b6d2d", size = 765683, upload-time = "2026-06-09T07:52:22.594Z" }, + { url = "https://files.pythonhosted.org/packages/27/43/41c1621732edd934d868a00d1b891584c892a7b62a9aab82ea5a0a5623ee/safetensors-0.8.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8e080062fcde23be189565e1c3305d16751a218ecf9412c8601e64204eb6f846", size = 722361, upload-time = "2026-06-09T07:52:23.924Z" }, + { url = "https://files.pythonhosted.org/packages/8e/3f/73ccf82579412b4a71c4ca673f10b5f1f888d7cf5af7fe24f27d30307be4/safetensors-0.8.0-cp310-abi3-win32.whl", hash = "sha256:2ddf52eac562eda224f99acfa7889d02968c1fd59a5b011ae7d8137c37e9c02d", size = 342401, upload-time = "2026-06-09T07:52:28.895Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6d/3fba214c1e5e0f69991677ec3bc17023f0421776975e1de0c682dca475e2/safetensors-0.8.0-cp310-abi3-win_amd64.whl", hash = "sha256:096ec1a98435df7beb08853bb5aa9081a84f23d0adc67ed1a0a10550f608373f", size = 355540, upload-time = "2026-06-09T07:52:27.832Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fc/7eedc3510d97878876e32774eebbeb61c43f148a96e915c84229a3e967aa/safetensors-0.8.0-cp310-abi3-win_arm64.whl", hash = "sha256:f7838e5135a406ad3e02efdcb8cf2e5397d368b0154537c4fec682dbc544d452", size = 340500, upload-time = "2026-06-09T07:52:26.745Z" }, +] + +[[package]] +name = "scipy" +version = "1.15.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'win32'", + "python_full_version < '3.11' and sys_platform != 'win32'", +] +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c", size = 38702770, upload-time = "2025-05-08T16:04:20.849Z" }, + { url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253", size = 30094511, upload-time = "2025-05-08T16:04:27.103Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f", size = 22368151, upload-time = "2025-05-08T16:04:31.731Z" }, + { url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92", size = 25121732, upload-time = "2025-05-08T16:04:36.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82", size = 35547617, upload-time = "2025-05-08T16:04:43.546Z" }, + { url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40", size = 37662964, upload-time = "2025-05-08T16:04:49.431Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e", size = 37238749, upload-time = "2025-05-08T16:04:55.215Z" }, + { url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c", size = 40022383, upload-time = "2025-05-08T16:05:01.914Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13", size = 41259201, upload-time = "2025-05-08T16:05:08.166Z" }, + { url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b", size = 38714255, upload-time = "2025-05-08T16:05:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba", size = 30111035, upload-time = "2025-05-08T16:05:20.152Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65", size = 22384499, upload-time = "2025-05-08T16:05:24.494Z" }, + { url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1", size = 25152602, upload-time = "2025-05-08T16:05:29.313Z" }, + { url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889", size = 35503415, upload-time = "2025-05-08T16:05:34.699Z" }, + { url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982", size = 37652622, upload-time = "2025-05-08T16:05:40.762Z" }, + { url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9", size = 37244796, upload-time = "2025-05-08T16:05:48.119Z" }, + { url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594", size = 40047684, upload-time = "2025-05-08T16:05:54.22Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb", size = 41246504, upload-time = "2025-05-08T16:06:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735, upload-time = "2025-05-08T16:06:06.471Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284, upload-time = "2025-05-08T16:06:11.686Z" }, + { url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958, upload-time = "2025-05-08T16:06:15.97Z" }, + { url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454, upload-time = "2025-05-08T16:06:20.394Z" }, + { url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199, upload-time = "2025-05-08T16:06:26.159Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455, upload-time = "2025-05-08T16:06:32.778Z" }, + { url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140, upload-time = "2025-05-08T16:06:39.249Z" }, + { url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549, upload-time = "2025-05-08T16:06:45.729Z" }, + { url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184, upload-time = "2025-05-08T16:06:52.623Z" }, + { url = "https://files.pythonhosted.org/packages/73/18/ec27848c9baae6e0d6573eda6e01a602e5649ee72c27c3a8aad673ebecfd/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759", size = 38728256, upload-time = "2025-05-08T16:06:58.696Z" }, + { url = "https://files.pythonhosted.org/packages/74/cd/1aef2184948728b4b6e21267d53b3339762c285a46a274ebb7863c9e4742/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62", size = 30109540, upload-time = "2025-05-08T16:07:04.209Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d8/59e452c0a255ec352bd0a833537a3bc1bfb679944c4938ab375b0a6b3a3e/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb", size = 22383115, upload-time = "2025-05-08T16:07:08.998Z" }, + { url = "https://files.pythonhosted.org/packages/08/f5/456f56bbbfccf696263b47095291040655e3cbaf05d063bdc7c7517f32ac/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730", size = 25163884, upload-time = "2025-05-08T16:07:14.091Z" }, + { url = "https://files.pythonhosted.org/packages/a2/66/a9618b6a435a0f0c0b8a6d0a2efb32d4ec5a85f023c2b79d39512040355b/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825", size = 35174018, upload-time = "2025-05-08T16:07:19.427Z" }, + { url = "https://files.pythonhosted.org/packages/b5/09/c5b6734a50ad4882432b6bb7c02baf757f5b2f256041da5df242e2d7e6b6/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7", size = 37269716, upload-time = "2025-05-08T16:07:25.712Z" }, + { url = "https://files.pythonhosted.org/packages/77/0a/eac00ff741f23bcabd352731ed9b8995a0a60ef57f5fd788d611d43d69a1/scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11", size = 36872342, upload-time = "2025-05-08T16:07:31.468Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/4379be86dd74b6ad81551689107360d9a3e18f24d20767a2d5b9253a3f0a/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126", size = 39670869, upload-time = "2025-05-08T16:07:38.002Z" }, + { url = "https://files.pythonhosted.org/packages/87/2e/892ad2862ba54f084ffe8cc4a22667eaf9c2bcec6d2bff1d15713c6c0703/scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163", size = 40988851, upload-time = "2025-05-08T16:08:33.671Z" }, + { url = "https://files.pythonhosted.org/packages/1b/e9/7a879c137f7e55b30d75d90ce3eb468197646bc7b443ac036ae3fe109055/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8", size = 38863011, upload-time = "2025-05-08T16:07:44.039Z" }, + { url = "https://files.pythonhosted.org/packages/51/d1/226a806bbd69f62ce5ef5f3ffadc35286e9fbc802f606a07eb83bf2359de/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5", size = 30266407, upload-time = "2025-05-08T16:07:49.891Z" }, + { url = "https://files.pythonhosted.org/packages/e5/9b/f32d1d6093ab9eeabbd839b0f7619c62e46cc4b7b6dbf05b6e615bbd4400/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e", size = 22540030, upload-time = "2025-05-08T16:07:54.121Z" }, + { url = "https://files.pythonhosted.org/packages/e7/29/c278f699b095c1a884f29fda126340fcc201461ee8bfea5c8bdb1c7c958b/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb", size = 25218709, upload-time = "2025-05-08T16:07:58.506Z" }, + { url = "https://files.pythonhosted.org/packages/24/18/9e5374b617aba742a990581373cd6b68a2945d65cc588482749ef2e64467/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723", size = 34809045, upload-time = "2025-05-08T16:08:03.929Z" }, + { url = "https://files.pythonhosted.org/packages/e1/fe/9c4361e7ba2927074360856db6135ef4904d505e9b3afbbcb073c4008328/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb", size = 36703062, upload-time = "2025-05-08T16:08:09.558Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8e/038ccfe29d272b30086b25a4960f757f97122cb2ec42e62b460d02fe98e9/scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4", size = 36393132, upload-time = "2025-05-08T16:08:15.34Z" }, + { url = "https://files.pythonhosted.org/packages/10/7e/5c12285452970be5bdbe8352c619250b97ebf7917d7a9a9e96b8a8140f17/scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5", size = 38979503, upload-time = "2025-05-08T16:08:21.513Z" }, + { url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097, upload-time = "2025-05-08T16:08:27.627Z" }, +] + +[[package]] +name = "scipy" +version = "1.17.1" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.11.*' and sys_platform != 'win32'", +] dependencies = [ - { name = "botocore" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*' and sys_platform != 'win32'" }, ] -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" } +sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/df/75/b4ce781849931fef6fd529afa6b63711d5a733065722d0c3e2724af9e40a/scipy-1.17.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec", size = 31613675, upload-time = "2026-02-23T00:16:00.13Z" }, + { url = "https://files.pythonhosted.org/packages/f7/58/bccc2861b305abdd1b8663d6130c0b3d7cc22e8d86663edbc8401bfd40d4/scipy-1.17.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696", size = 28162057, upload-time = "2026-02-23T00:16:09.456Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ee/18146b7757ed4976276b9c9819108adbc73c5aad636e5353e20746b73069/scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee", size = 20334032, upload-time = "2026-02-23T00:16:17.358Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e6/cef1cf3557f0c54954198554a10016b6a03b2ec9e22a4e1df734936bd99c/scipy-1.17.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd", size = 22709533, upload-time = "2026-02-23T00:16:25.791Z" }, + { url = "https://files.pythonhosted.org/packages/4d/60/8804678875fc59362b0fb759ab3ecce1f09c10a735680318ac30da8cd76b/scipy-1.17.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c", size = 33062057, upload-time = "2026-02-23T00:16:36.931Z" }, + { url = "https://files.pythonhosted.org/packages/09/7d/af933f0f6e0767995b4e2d705a0665e454d1c19402aa7e895de3951ebb04/scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4", size = 35349300, upload-time = "2026-02-23T00:16:49.108Z" }, + { url = "https://files.pythonhosted.org/packages/b4/3d/7ccbbdcbb54c8fdc20d3b6930137c782a163fa626f0aef920349873421ba/scipy-1.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444", size = 35127333, upload-time = "2026-02-23T00:17:01.293Z" }, + { url = "https://files.pythonhosted.org/packages/e8/19/f926cb11c42b15ba08e3a71e376d816ac08614f769b4f47e06c3580c836a/scipy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082", size = 37741314, upload-time = "2026-02-23T00:17:12.576Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" }, + { url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" }, + { url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" }, + { url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" }, + { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" }, + { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" }, + { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" }, + { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" }, + { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" }, + { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" }, + { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" }, + { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" }, + { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" }, + { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" }, + { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" }, + { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" }, + { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" }, + { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" }, +] + +[[package]] +name = "scipy" +version = "1.18.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'win32'", +] +dependencies = [ + { name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and sys_platform != 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/74/66de6258867beb2ef08f35f9f2ac017a52cacd5081714d239ff1a442d458/scipy-1.18.1.tar.gz", hash = "sha256:52c4b7422442aba924d03ad4019852b08a92e64ea187b933135687bfe2747307", size = 30781235, upload-time = "2026-08-21T23:28:50.599Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/f7/240c110c08693826b4513a52f5717d62ec7c7af72f2920821247c03b17b3/scipy-1.18.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:457fd7a2a8edeb044ab6ffbc0aa03ff6cd18491356e5e0c834d76ce621b916d1", size = 31111061, upload-time = "2026-08-21T23:23:44.522Z" }, + { url = "https://files.pythonhosted.org/packages/05/4a/78c6285577c375e7cf27277ea8ee6961224327f1e1a0c44af5f17f23635c/scipy-1.18.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:e708533e8b2ae2497d65346538a7dcc92814410b25b81432eac66de0f2af8265", size = 28733332, upload-time = "2026-08-21T23:23:50.015Z" }, + { url = "https://files.pythonhosted.org/packages/a5/f6/a5b82f8abbe14d134691b8b903696f701d25a081353a29dc655c364d9e62/scipy-1.18.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:7bbf207c4453ce1ad2e00b17313852b33310b83090c2311bdaf97f93c0380d12", size = 20475078, upload-time = "2026-08-21T23:23:54.138Z" }, + { url = "https://files.pythonhosted.org/packages/23/22/0858a0bbd6b3e825ceb8cd9baf9eaf3b2f2b1d77727eb6be40500bcdc92f/scipy-1.18.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:78c0665edead396b1abb4897c41a5c1d9bf090c8a637a4c20a61678e0a264e66", size = 23108904, upload-time = "2026-08-21T23:23:57.824Z" }, + { url = "https://files.pythonhosted.org/packages/75/9a/2e71719f31eaefe0e3a1706c4a1ded94e664bfd95ffca2b219a671faee01/scipy-1.18.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c085faa2cfa879c5141df483f836f4d691045a078224a670fa570fa01612d89", size = 34025113, upload-time = "2026-08-21T23:24:02.209Z" }, + { url = "https://files.pythonhosted.org/packages/df/64/ff35eb9e54894cf471ff4716abd3c81eb0a0626869217ce3e6ba4ccf17d7/scipy-1.18.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f55fa87b6c612ecd6b058f167c53231b1d14e412efe361d3d6e38b3631c73218", size = 35344199, upload-time = "2026-08-21T23:24:07.844Z" }, + { url = "https://files.pythonhosted.org/packages/d3/af/c5538be1792f7034c12c7db6ee67cace58253c7b87b122d68253eaf5de89/scipy-1.18.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c35d74ce0e193ff740c2f2be2ac913ddc232fe6c1ff40b26cfecb9c670c63314", size = 35639587, upload-time = "2026-08-21T23:24:13.05Z" }, + { url = "https://files.pythonhosted.org/packages/91/4c/075e4f66471bac101141ac739e9e135549be1bae584571bd03a530c056e1/scipy-1.18.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d2924a03db38dc2e848bca2fe9f077dafb891480b91a00a0963a8cf86dfc31c1", size = 37480330, upload-time = "2026-08-21T23:24:19.608Z" }, + { url = "https://files.pythonhosted.org/packages/39/e7/979fd14e75008623df31ba70d6bb144700f68feadcea042021c06a05bf82/scipy-1.18.1-cp312-cp312-win_amd64.whl", hash = "sha256:5e4d44984abc0020154ea81b247adeddcc3ac5527b975ff798bd1ba0adc513c2", size = 36658278, upload-time = "2026-08-21T23:24:25.463Z" }, + { url = "https://files.pythonhosted.org/packages/c7/0b/e1525354ff9d7d5feb6d1b31af6d14072e5c91e9607b421fa1ec889660b3/scipy-1.18.1-cp312-cp312-win_arm64.whl", hash = "sha256:d65d448389b8436493abcf629cc94ad0cf32aecaf06e1acca1de53cc795f2f12", size = 24400588, upload-time = "2026-08-21T23:24:30.579Z" }, + { url = "https://files.pythonhosted.org/packages/b6/55/4540ee0f9c42a9ad7109d0d1a8cc70de54c3572b01c6693a2b1c70e90ceb/scipy-1.18.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:3ab3523da44749156e1f68b464dc56af11ae4cbc5c739a49d05f32b982eca9f3", size = 31089958, upload-time = "2026-08-21T23:24:35.8Z" }, + { url = "https://files.pythonhosted.org/packages/2a/f5/769f36d14922b8071a43e95d24d18b6bdafad10d7f5cf647867e1ac052bc/scipy-1.18.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e6fb6a55cc0ba97b59a1f288fb86dc6fce8bdfc0fffcbfd015e3a954bf2a2d93", size = 28715106, upload-time = "2026-08-21T23:24:40.775Z" }, + { url = "https://files.pythonhosted.org/packages/9a/d7/21d890274f75ea37a8209d5519e72da3da90302e3b9fb8397a0918386a62/scipy-1.18.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ea324d9dd34c38bfb9bec8ca4d1b407db97dbb74029f566b8e322b1b6fe56fe6", size = 20456846, upload-time = "2026-08-21T23:24:45.066Z" }, + { url = "https://files.pythonhosted.org/packages/ec/01/798430ecea2e78ec7c02663d5f71c007bb6abeca931080debd40d7fa55ea/scipy-1.18.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:75b00eb8fb802090aa903f4ea1c7f5a584779f967361e68b7e98e531cc2d7174", size = 23087986, upload-time = "2026-08-21T23:24:49.539Z" }, + { url = "https://files.pythonhosted.org/packages/e6/5f/4634e9d35c68496e4e34cb6946eafab044458e6cedab42b40b6588e475b6/scipy-1.18.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d416b16cccfd70fbf62400e84d0bb2f4e6af519a45557f1692c749b37f14b315", size = 33998146, upload-time = "2026-08-21T23:24:54.714Z" }, + { url = "https://files.pythonhosted.org/packages/41/48/6450ed9243315322bbc19ac57b9b70d66a20bf1d38d124c96bc4bf6af9ea/scipy-1.18.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fdaf5ea890a6183d0565f51a61799d67081bd5b1cf03c5f4b3fd3732108625c9", size = 35312578, upload-time = "2026-08-21T23:25:00.44Z" }, + { url = "https://files.pythonhosted.org/packages/00/bd/bf5a4be6a3525676499f6dff307991739ff6fdcad1481b1aeb6745339f58/scipy-1.18.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c825cef2f49e46753726a7181a8e199804a912b29519ada542c6ebc654951899", size = 35612621, upload-time = "2026-08-21T23:25:06.144Z" }, + { url = "https://files.pythonhosted.org/packages/bd/4e/3c45c33e00a77996c4b1cb707929f833ba7b1d522ee29f882512c330676d/scipy-1.18.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e3b417bf8c2c7c16e8f58ad91db17783ec911ac16e7b50eb6eab6e809b4f5b07", size = 37457323, upload-time = "2026-08-21T23:25:12.483Z" }, + { url = "https://files.pythonhosted.org/packages/93/0e/e0348fbc0dbab65c114cf78957e7dfeb49f8e8b556b4d930cc12ff195e18/scipy-1.18.1-cp313-cp313-win_amd64.whl", hash = "sha256:559ed65f60c1af5a03f3912605a1b5114f522c7c32fb23c3376ae8f03219fe28", size = 36622841, upload-time = "2026-08-21T23:25:18.722Z" }, + { url = "https://files.pythonhosted.org/packages/50/a8/6a77f5f267c555108f0a864b6db714363dab567a8266422a79a385f9232b/scipy-1.18.1-cp313-cp313-win_arm64.whl", hash = "sha256:cd479fc04dd9401e3b4f49e76518768ef99c4f517a98c284eb091fd725719adf", size = 24399315, upload-time = "2026-08-21T23:25:23.458Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/d8eb4e280ddb56a4ab2c6f02ee49b56b23f6e977cf0802fd6d68dbef14f5/scipy-1.18.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:83de5453a7799afc9048b4616bd085cef126e36412f0ea2f6370c36a2a3a51e7", size = 31090936, upload-time = "2026-08-21T23:25:28.686Z" }, + { url = "https://files.pythonhosted.org/packages/2a/49/59ea385dc3a62ff498ddf3cfff7c2b41b0f9f9d3c4122b3f1dcb6d6327fe/scipy-1.18.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:9554bcc6d715ee87a633a3cc8e7703c6628b100dd29cb8a2efc4c0533c7ff729", size = 28725221, upload-time = "2026-08-21T23:25:33.244Z" }, + { url = "https://files.pythonhosted.org/packages/70/e8/6b0c288c50942d78193696c9f15f9a0874f5178aa0ddf40f83d9924b3e8d/scipy-1.18.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:011413b7426b75012840e35649e00fe0a2c3bae89fed433876e3a99251572efc", size = 20466839, upload-time = "2026-08-21T23:25:37.516Z" }, + { url = "https://files.pythonhosted.org/packages/4b/e0/54fd3793c729e3b936782f181b59cbb1205bf250ab605a16cb1ba61cdd5e/scipy-1.18.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:88f0e784020649f88ea48c9f5ddfa403bf9205820667c0914740b392035afb82", size = 23089121, upload-time = "2026-08-21T23:25:42.019Z" }, + { url = "https://files.pythonhosted.org/packages/0b/56/030af62bea3cf878e0028515dff78c123b01633606a879b63f42d2db99cc/scipy-1.18.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d3ab0e8c69a17dd3559eab8cbb88f258e285c94d572c2719033f90f83290c89", size = 34053851, upload-time = "2026-08-21T23:25:47.998Z" }, + { url = "https://files.pythonhosted.org/packages/6b/89/2a844506d49651e9aa1af6ef95b6bd8031cb1d5a4375edec6155037e04cf/scipy-1.18.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac0333bdf38309aa3dcbe7e3fa7ea29e7a2c37c6ea306a757b700ded8e4596ad", size = 35329183, upload-time = "2026-08-21T23:25:53.522Z" }, + { url = "https://files.pythonhosted.org/packages/eb/56/c7370c3640e92ac9613cbf26cb3f729f9b12ddf1727b55b94b53b24d6f48/scipy-1.18.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:911de823097db8b63f034299d12662db93344e6ffa0b881cbb57748974b70168", size = 35672551, upload-time = "2026-08-21T23:25:59.387Z" }, + { url = "https://files.pythonhosted.org/packages/24/16/ec8536f351421f8bf60a1120930638f83790f4710b8230446aca3d6159d4/scipy-1.18.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:95298364e251be3e60249facbeeca03631d3bb7584f85879516ec55ac717b81f", size = 37469416, upload-time = "2026-08-21T23:26:05.432Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/d73da0d28f16c45bb9b0a5691b91610b0275c5ef0eb5e43c87cf2dc1bf31/scipy-1.18.1-cp314-cp314-win_amd64.whl", hash = "sha256:78a0d7c918e74a232394117160e7e3db503377572a45bcef8826e4ab8a35feba", size = 37362755, upload-time = "2026-08-21T23:26:11.366Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/e996e4dc74e10e227b1e14db5eaf6608bb6dd33884a64851c38f18dd4249/scipy-1.18.1-cp314-cp314-win_arm64.whl", hash = "sha256:cbf38d043c1aa4ab306e1ada6ab6eddacc3322a20b7af1b30bc93254b366fe09", size = 25036090, upload-time = "2026-08-21T23:26:15.887Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c9/c00213f92309d753b48903e6a451b87eb52ff5b7a16e789d1568bbf221c4/scipy-1.18.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0fcb3c93519f27bb4f0c4b0f7802cdcaca7fcf93267b75edda2e9f4e8a55cbd7", size = 31485550, upload-time = "2026-08-21T23:26:20.776Z" }, + { url = "https://files.pythonhosted.org/packages/74/b2/e3067c487982d4eeab2938928529410370c06fea84a4d3f4925e7d96647d/scipy-1.18.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:ddef79fb382df40104a19bb7151b3b23e57c1778fcf857c71ceecd9bd264513f", size = 29174642, upload-time = "2026-08-21T23:26:25.395Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ab/374c9fe2d1ec014e576c781a4b5d8e1ba340e8f6b4638c16f711d2b194f0/scipy-1.18.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0e82073ecc7acc6436fac4b31674109c7e1d3e596789767eda01258a8c9e8123", size = 20916357, upload-time = "2026-08-21T23:26:30.112Z" }, + { url = "https://files.pythonhosted.org/packages/90/38/223915c88a17317cafbf8ca2a42b11c265a9fb1e804aa665544132b5fe8a/scipy-1.18.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:8bcf3c1ba5d6456e2effd30fcbd3459b044d683fcdac79a2e6830f0bdf7de487", size = 23482611, upload-time = "2026-08-21T23:26:34.846Z" }, + { url = "https://files.pythonhosted.org/packages/c4/d1/db0948da8ca57a80b36520ef0a768b967d99f3af65f4b6f1bf6362ad4dd4/scipy-1.18.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cfbf154f2ba187f2ed6cce2639efff7d105f1140573642c0161615b6d91d6a87", size = 34143202, upload-time = "2026-08-21T23:26:40.4Z" }, + { url = "https://files.pythonhosted.org/packages/87/53/39d046cc7574ed6acacb6bd5723e220107ece80bff12faaf3efc4ddeede4/scipy-1.18.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a1d33a7836f7ddc1993427966a0823468ec41bcbdb1a9f9942d1d7e57f803ba3", size = 35380876, upload-time = "2026-08-21T23:26:46.1Z" }, + { url = "https://files.pythonhosted.org/packages/f9/da/32e0e799d875a85ca57d9bde6c78148afcc0e38276df683d95854eadc8c3/scipy-1.18.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f4b8bc363b6d65ee2152bec57568e3c52639bb34c46057b09857a307ed5e21d", size = 35770885, upload-time = "2026-08-21T23:26:51.533Z" }, + { url = "https://files.pythonhosted.org/packages/88/2e/f97a666d362fee68b18f41c9c30ed502ca5c98b549749bfcb52a8b74d1eb/scipy-1.18.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:11c423f1049c5755ad4409af52a9ada1cff96fe9b50795d4af3619f292901239", size = 37525424, upload-time = "2026-08-21T23:26:56.751Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d5/a9e765a84654ebba8479a1fd1b059ced1af72b168a3b2a3a46540ea38d20/scipy-1.18.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c24acac1e18912761c4700239bbc1fd32f615af690f1584d49b35859be51324d", size = 37416961, upload-time = "2026-08-21T23:27:01.546Z" }, + { url = "https://files.pythonhosted.org/packages/ee/16/e79e0d1c63ef698879d85439d37e9fb434e3b804e506a6991038d086ebd9/scipy-1.18.1-cp314-cp314t-win_arm64.whl", hash = "sha256:9f2897bf7737392ad0d5213ea7b6add72a4edf5679b3153106aeb88b6507b3b9", size = 25331848, upload-time = "2026-08-21T23:27:05.884Z" }, + { url = "https://files.pythonhosted.org/packages/be/4f/1bd37c883b67163e2ca1f60977a399500e6879c15defecac62831c8d078d/scipy-1.18.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:eb0dfcf4e28a99c12c999744a2ff67c9b06200e20401c7c88186e33552a46331", size = 31091484, upload-time = "2026-08-21T23:27:11.051Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c5/ba929d7feb9b2332f96827c12e0e924b61973b59b4dea383b603372c65ce/scipy-1.18.1-cp315-cp315-macosx_12_0_arm64.whl", hash = "sha256:30f464bee641fa8e282577c7dce027308403213c6ca8270bba73285c91024bc5", size = 28725057, upload-time = "2026-08-21T23:27:15.9Z" }, + { url = "https://files.pythonhosted.org/packages/a4/19/68f1c50f609d955d230e66d25d02bd3e1e167ec540232135354fb9a4b9e3/scipy-1.18.1-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:1bca3b943fc2567ea49cd02c99abde49da4d5178ec46f624bd8255cda8755beb", size = 20466734, upload-time = "2026-08-21T23:27:20.044Z" }, + { url = "https://files.pythonhosted.org/packages/ef/6d/319fa29b73d1802fa80b32a6eaf3f5be456ef81526da2716a9493bcb5501/scipy-1.18.1-cp315-cp315-macosx_14_0_x86_64.whl", hash = "sha256:c9d18a33309122074ea483dd92dd444189166b8b2ec429fe9ed5ac73c7a0aa23", size = 23089664, upload-time = "2026-08-21T23:27:24.345Z" }, + { url = "https://files.pythonhosted.org/packages/b7/db/30992f9b51a63de671daf3888ffd18378b6cb9ec9f2c972264238ffa7fd6/scipy-1.18.1-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82f201b4c878551d48558337aab270d3c6cca5507b8737c8d8a608d234cccde0", size = 34054035, upload-time = "2026-08-21T23:27:29.409Z" }, + { url = "https://files.pythonhosted.org/packages/91/d4/bf3e735dc0b9d5a8ff45079d2540e17d3aff7a2f0048dd8f552ffd031d2b/scipy-1.18.1-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ac49ea97594532dd44b7136094d35f5440fa06e6d9c6384a74c01764df388c5", size = 35333883, upload-time = "2026-08-21T23:27:34.293Z" }, + { url = "https://files.pythonhosted.org/packages/19/93/12d78ce9f871fe945fca588d32644e6e63f553c2a35c564d73f3b22a3313/scipy-1.18.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:ceb30a00ce7c92d459819443d29ca486d882b83fb6738bdcbb2a1cce94ac5daa", size = 35673124, upload-time = "2026-08-21T23:27:39.059Z" }, + { url = "https://files.pythonhosted.org/packages/70/cd/886219313a1012a48e6ae0ec4f302c837151beb92e1ff0d709ef8fdfc488/scipy-1.18.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f29633129f9fa7e88a3f0fca835de2d030bfc9643f7799e1a0c46cee24d38fc7", size = 37470753, upload-time = "2026-08-21T23:27:44.435Z" }, + { url = "https://files.pythonhosted.org/packages/17/6c/a776888ce618bee54fbde26172f0f46ac1da70d27b63861797fe78e1904b/scipy-1.18.1-cp315-cp315-win_amd64.whl", hash = "sha256:92c14f5bdbfb6216315ce33e78080474082de8b3830122ba97809bfbe65f75c0", size = 37361483, upload-time = "2026-08-21T23:27:49.334Z" }, + { url = "https://files.pythonhosted.org/packages/ab/09/97b651691322ebee97999b017ffc18a15a0b815103844c97e8da9d469731/scipy-1.18.1-cp315-cp315-win_arm64.whl", hash = "sha256:e402cf31eb68f453dbb2d36fc6d722b33f24a55d68b2ae1d92fa6305ca71c298", size = 25035883, upload-time = "2026-08-21T23:27:53.596Z" }, + { url = "https://files.pythonhosted.org/packages/ed/0f/9ec20467bbabd0d44e2a77d0fd3d124f884b4d67df92af82c91d2d6a486f/scipy-1.18.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:2a0b02f9fc46f8520330c23d45e6560db7e3a0d927232139427637f98943e11d", size = 31474926, upload-time = "2026-08-21T23:27:57.993Z" }, + { url = "https://files.pythonhosted.org/packages/8a/58/dcb79161e56efbedc50079fcd2f5fe427a0ebb53022eb476aa73c015ad8f/scipy-1.18.1-cp315-cp315t-macosx_12_0_arm64.whl", hash = "sha256:1d73131e358976663dd969e1fb4ed1404b815cd977eaaedc3b3a133ba2d81c35", size = 29164940, upload-time = "2026-08-21T23:28:03.062Z" }, + { url = "https://files.pythonhosted.org/packages/71/d3/1eeea80c817fcb8ef7bd4a05a58824977a0e57a375cfc3d7ea7c911c01ad/scipy-1.18.1-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:bff0b729edd992766136b34e39cc76bc2fad905aa58897ee72a9cd000a6d8443", size = 20906742, upload-time = "2026-08-21T23:28:07.642Z" }, + { url = "https://files.pythonhosted.org/packages/54/46/e59350428b6099301a20128108c995e2eb175a43f383af9a346e38824f9b/scipy-1.18.1-cp315-cp315t-macosx_14_0_x86_64.whl", hash = "sha256:10ac20c69d880f77f375db44c22e3e6a644f9fefa291d4cd2fb9790a89fc99fd", size = 23472183, upload-time = "2026-08-21T23:28:12.109Z" }, + { url = "https://files.pythonhosted.org/packages/89/31/cc91623fa98f0621766a0f0aaaadb2c66de74a7ea7e3837164f6e4354260/scipy-1.18.1-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33a834464fdabc0f26a45508df31b3cc5d028e04dbf6c5ed398541418e0a12fe", size = 34130796, upload-time = "2026-08-21T23:28:17.906Z" }, + { url = "https://files.pythonhosted.org/packages/fc/3e/8572ef536957ddb8aa81bb4090d9e25f257e3b4e05d97deb54319deb8a3a/scipy-1.18.1-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49023963c193dacee096301452f223ee24d86ec5807f8df93c0f7221d119e305", size = 35374253, upload-time = "2026-08-21T23:28:23.732Z" }, + { url = "https://files.pythonhosted.org/packages/b5/c6/59fdeffb4f1435299f93d9dc8140b43ad2916e6cfc944be6c3041fcec86d/scipy-1.18.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:d84a09d0dad90ba6525d8ac1c2334b33e64bf3ccfe9e841f02feb867a22681e4", size = 35758543, upload-time = "2026-08-21T23:28:29.431Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d9/135be205d9de8783193aff9cc3bf483a03a38e4b29432c954e8cb66ac14e/scipy-1.18.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:179ce34a8d0fe273d8883ba59e17e052247d08973dfcb743ca52bb1cce2d60b0", size = 37521946, upload-time = "2026-08-21T23:28:35.245Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a2/5b7d5270621ab7cfa3f7766067bf95dc360b5efb6394694e8143b4156e2b/scipy-1.18.1-cp315-cp315t-win_amd64.whl", hash = "sha256:5632e3ae3d09197c446310cd5187de63e28448ce22f0f67b2b93d97503c0c230", size = 37408295, upload-time = "2026-08-21T23:28:40.724Z" }, + { url = "https://files.pythonhosted.org/packages/63/ad/741c19fcb66755ff953daf9243af8480e4bf3d7fbe57583c178c7d2b6b51/scipy-1.18.1-cp315-cp315t-win_arm64.whl", hash = "sha256:eda632a7981f69730d6281f451db9c1c370993a2c0d7ddb43e2a809a2862b83a", size = 25319710, upload-time = "2026-08-21T23:28:45.713Z" }, ] [[package]] @@ -2983,6 +6801,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5d/40/e1e72872c6354b306daef1703549e8e83b4d43cfea356311bf722a043752/setuptools-83.0.0-py3-none-any.whl", hash = "sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3", size = 1008090, upload-time = "2026-07-04T15:31:20.885Z" }, ] +[[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" @@ -2992,6 +6819,184 @@ 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 = "smart-open" +version = "8.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt", version = "2.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/53/9c513747547fd595d5c143259129ea8b9c3ea2f6b7bb9dcea2b1966ded3c/smart_open-8.0.1.tar.gz", hash = "sha256:18b1c4496003c6902be17c15f032b5c319f307c89c6ae9e6b028b508bed8b2cf", size = 61882, upload-time = "2026-07-15T13:56:10.492Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/96/325b8c507ccecc50421fecc0345a502ee6e4a44785af3c4e6ecbadad624a/smart_open-8.0.1-py3-none-any.whl", hash = "sha256:3e97f90e92a952cb57863dfe132082c400a52eeeb27c067692fb51dbcc5b0089", size = 73504, upload-time = "2026-07-15T13:56:09.033Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.9.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/99/a6ca3beb3ccacb41fb3321d8a60e5566f9e6467601ef8eba6a17e1b89778/soupsieve-2.9.2.tar.gz", hash = "sha256:4a55d8cf158a9c2e587fa4922f1bbb91d68ac829e2d6f25403a85747c71daf74", size = 122445, upload-time = "2026-08-07T00:57:24.801Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/dc/ad025c1ee131eba60c69f4dd5779b18fcf1e6b21a343e2162a84d5d133c7/soupsieve-2.9.2-py3-none-any.whl", hash = "sha256:8089a26fd974ca7a1f30276d3d8492ab266ab15af581642dfe8aa162e0c1c823", size = 37370, upload-time = "2026-08-07T00:57:23.524Z" }, +] + +[[package]] +name = "spacy" +version = "3.8.16" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "catalogue", marker = "python_full_version >= '3.11'" }, + { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version == '3.11.*' and sys_platform == 'win32') or (python_full_version == '3.14.*' and sys_platform == 'win32')" }, + { name = "click", version = "8.5.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32') or (python_full_version >= '3.11' and sys_platform != 'win32') or (python_full_version >= '3.15' and sys_platform == 'win32')" }, + { name = "confection", marker = "python_full_version >= '3.11'" }, + { name = "cymem", marker = "python_full_version >= '3.11'" }, + { name = "jinja2", marker = "python_full_version >= '3.11'" }, + { name = "murmurhash", marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "packaging", marker = "python_full_version >= '3.11'" }, + { name = "preshed", marker = "python_full_version >= '3.11'" }, + { name = "pydantic", marker = "python_full_version >= '3.11'" }, + { name = "requests", marker = "python_full_version >= '3.11'" }, + { name = "setuptools", marker = "python_full_version >= '3.11'" }, + { name = "spacy-legacy", marker = "python_full_version >= '3.11'" }, + { name = "spacy-loggers", marker = "python_full_version >= '3.11'" }, + { name = "srsly", marker = "python_full_version >= '3.11'" }, + { name = "thinc", marker = "python_full_version >= '3.11'" }, + { name = "tqdm", marker = "python_full_version >= '3.11'" }, + { name = "typer", marker = "python_full_version >= '3.11'" }, + { name = "wasabi", marker = "python_full_version >= '3.11'" }, + { name = "weasel", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/5d/b0b4cd2f6e8a0470e50f7f0cfc1cea6e2f25572e97fb5d404c7941d5e88a/spacy-3.8.16.tar.gz", hash = "sha256:a3d19da23637cc396b42d22fc33852680f675d8ddcb847d3e2a0d094712d1794", size = 1330989, upload-time = "2026-08-24T10:05:57.936Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/e8/bc1a6f836dd1a18011c8fc14d9887b962dc2ba0117938d9bb1cba8e3f3da/spacy-3.8.16-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:32fe82bfbe6711a4687e427c8eec44af7cd5e09322a252b35bc8166aa84b9025", size = 7005717, upload-time = "2026-08-24T10:03:50.201Z" }, + { url = "https://files.pythonhosted.org/packages/db/09/7eece818622dee4a2e0944aa1ee1275c25da6ef151d3a243c378a9097521/spacy-3.8.16-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b022ebde7465334c0631f74e0cd21dff257ee5f80ca68918c56fd64681b49463", size = 6910474, upload-time = "2026-08-24T10:03:52.457Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d1/ca928dde89792ba589c973d7c120ec791ccbd18d539edb3d05dc738ab4e7/spacy-3.8.16-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8681eb07f6b0e48fb3ba4a5f7f2582c0fc6df991d240b431b628131029c4ade3", size = 32871958, upload-time = "2026-08-24T10:03:55.507Z" }, + { url = "https://files.pythonhosted.org/packages/3e/de/bd2bb6a49e3859f5e4fa569f79f674042a340a57cc0a47448ed8a5354865/spacy-3.8.16-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f08555e5204da7d9c3f8440d9d88dc3011edcfc1fd21689069091da1973c883b", size = 33407019, upload-time = "2026-08-24T10:03:59.002Z" }, + { url = "https://files.pythonhosted.org/packages/4e/97/3ed2dcc002e15b82611555d20f5fb6c3da8c1710321ab464fdfaf5a0dc97/spacy-3.8.16-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6a1523ce0a1358936fa1abdc3a43f2ec558d4166ac5c2f246b33fd88e3f238aa", size = 33260017, upload-time = "2026-08-24T10:04:02.39Z" }, + { url = "https://files.pythonhosted.org/packages/bf/17/92e5b56d697e3a62f2d9edc8374ce6d016ad2ed40305217659d1fe570f58/spacy-3.8.16-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:49fe8d6a6cf343777caf59a3d4ba76f81cee0dfc1abe78be571cdc2db0d77672", size = 34206474, upload-time = "2026-08-24T10:04:06.273Z" }, + { url = "https://files.pythonhosted.org/packages/d1/0c/edc60bf4d5cd1103fa9639316e5a70999174ad53a5c8f479f598da1b4872/spacy-3.8.16-cp310-cp310-win_amd64.whl", hash = "sha256:81fe468596678c7bf717650b12352c8e4d174c72d050dcc01c8ee3c6c781ccad", size = 16359363, upload-time = "2026-08-24T10:04:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/a5/e3/92254f9eaade465592672dcc696362024c24b266ad3c418c6de0b8ffb8fa/spacy-3.8.16-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc5a850ba2ce371ac13893ef4153a7f3cf0d7fee8ca4ddea21fac2d2628d7ea0", size = 6991062, upload-time = "2026-08-24T10:04:12.372Z" }, + { url = "https://files.pythonhosted.org/packages/f4/82/d1696b985a8eba24565b8273257968f4c69b69f3f3908a8f8a89f6632956/spacy-3.8.16-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb07d4b8255be6b6ff3a34ada93173176d0937d2023b35302a34eed3364681c0", size = 6891423, upload-time = "2026-08-24T10:04:14.373Z" }, + { url = "https://files.pythonhosted.org/packages/bb/8d/13ac8597d0907eb306e4afe0214648f72ee3b8643cdb85f79054d73f7400/spacy-3.8.16-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e91c4f9a320d964a27ffae1b62e37bb6e8c391aa837890a082d82bd9a89ed43", size = 34186254, upload-time = "2026-08-24T10:04:17.688Z" }, + { url = "https://files.pythonhosted.org/packages/10/3e/39e910b91b9a6cae61dbde9e42196c37388ba4f2d3abb88ebf1e9e4a303e/spacy-3.8.16-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bc4e59799dcb0eb4823e5946515d3ca0d503ff78ef2502172d4b59ee0fc567ec", size = 34678935, upload-time = "2026-08-24T10:04:21.98Z" }, + { url = "https://files.pythonhosted.org/packages/00/46/bf8e69d1bcc35a4bd798ad82e2496290aa7f6b98a47cb2c9b1375e04353d/spacy-3.8.16-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ccd74917536fa82896f31c66db5f230301ec2662b7ad3c0d8c3eb790b0fc6121", size = 34558988, upload-time = "2026-08-24T10:04:26.175Z" }, + { url = "https://files.pythonhosted.org/packages/7f/44/433cec1610c82f677374a8fc2db3ee2618fd06c16f5bbf13aea6d2796292/spacy-3.8.16-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e67052bebdeba53847d3d09058f814708d587c4fb2216c65239964a341da2280", size = 35495139, upload-time = "2026-08-24T10:04:30.521Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/25ffc146d6784613a6283e2ae38252fdbbd84ebbf373f2dad379fe9dac2e/spacy-3.8.16-cp311-cp311-win_amd64.whl", hash = "sha256:9e89bebe168ec8714b21f0225950f0525d0ef87109cfcb11e5d18ebb1f4e658a", size = 16342860, upload-time = "2026-08-24T10:04:33.712Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c5/fd293e3baf1c6f3cf2dc844c87074b81a189faa7d7fa5ec3960d53e56ba1/spacy-3.8.16-cp311-cp311-win_arm64.whl", hash = "sha256:97bb04bd81a3690c45dfd62d4bd19584aa544bb955c8b497fb488256d10ac54c", size = 15698056, upload-time = "2026-08-24T10:04:36.474Z" }, + { url = "https://files.pythonhosted.org/packages/60/b2/33e8e4cac876090c5d8b4016b23ca2647f66c2a87c518ea010047e78ee1f/spacy-3.8.16-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e045765035e9760f38637101f41a7c89d3b69659dcc70e716bb4011a95969ac9", size = 6565218, upload-time = "2026-08-24T10:04:38.825Z" }, + { url = "https://files.pythonhosted.org/packages/d2/1e/247e43597b10576eccfed55af7999663f00ae934394acebef724cabdc1b7/spacy-3.8.16-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:44a641085abbe3a09ea56a89f2e50b5f51aea6cf69213b70305fb48e341b883b", size = 6460523, upload-time = "2026-08-24T10:04:40.873Z" }, + { url = "https://files.pythonhosted.org/packages/0e/8c/2b150ea6e9b667b710e7365328edfdd3c6729af4e48124baa80c4784c26b/spacy-3.8.16-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2c46c35467d963a62dc0407c99d3a18562d4c85ee887a57e4a07dde020f1a38", size = 34901687, upload-time = "2026-08-24T10:04:44.138Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9c/3b5d64a72ac40cf584134c6639ac67f8f2d504eb081c91da3ad2ffae5c04/spacy-3.8.16-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8d0f63c3124d0a34a37e9b519d004cee1269744be07f91f843902e6c9f3e557a", size = 35496883, upload-time = "2026-08-24T10:04:48.383Z" }, + { url = "https://files.pythonhosted.org/packages/45/16/8c9d9afed3afbfd585e876c330ec8dcff878a64bc99c108355c8b59ea954/spacy-3.8.16-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:397a80c4d09a6237eebaaee00a2e5f8732ff4cc165f94679b899f30179d38ea8", size = 35012008, upload-time = "2026-08-24T10:04:52.983Z" }, + { url = "https://files.pythonhosted.org/packages/32/dd/eeaf28004591f0d282da809194d9f7bfb6a1c0626ca0d7d69e8b5436751f/spacy-3.8.16-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2810fd2ce41f6a8dde62642dad0d11fd9db7cb6a1cd40b1c8b70a01586e6ff9f", size = 36028110, upload-time = "2026-08-24T10:04:56.874Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c0/4122fa6c3670d504ad5a3094c9b512f0d0908f66cefeb8c8c3349f8c6a28/spacy-3.8.16-cp312-cp312-win_amd64.whl", hash = "sha256:5991c334e71c23b798c25e0d403295dde4d2d1fb58c2e075450b964db03c05ea", size = 15180961, upload-time = "2026-08-24T10:05:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/36/55/2fd66419866455289c090cd177faf7d3ee360f36b2eea5e4c74b2c541a58/spacy-3.8.16-cp312-cp312-win_arm64.whl", hash = "sha256:770cc0581fc06c0723cb1488dc1d1da0570801168da786674626f342d903ed37", size = 14552405, upload-time = "2026-08-24T10:05:03.382Z" }, + { url = "https://files.pythonhosted.org/packages/a5/5a/ebe53a3cd1edf5f8cb4b9465ae2383b0b4f6a2c8bd96afa0408f682df4bb/spacy-3.8.16-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f42f257404e749d9048d3b3b97004692210057d38e03c2f156817258bf6daf2b", size = 6585155, upload-time = "2026-08-24T10:05:05.818Z" }, + { url = "https://files.pythonhosted.org/packages/29/f8/80f9ddf288c494b3a7b737bff3b938ef70328c49aaf0a90738aa90b638b8/spacy-3.8.16-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ce120d4077050352f344b987354be3e3fddb207436b537cf91a891837657ce2c", size = 6465153, upload-time = "2026-08-24T10:05:08.32Z" }, + { url = "https://files.pythonhosted.org/packages/7d/5f/b039865cf4e2fa82c8defc737c37af4480e70a56d1e1c380865b3df89f54/spacy-3.8.16-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f765cb6cbef82b5d98c46936a1385e87fb05919433fbc6b953e3c093ac30f8ef", size = 34527537, upload-time = "2026-08-24T10:05:11.65Z" }, + { url = "https://files.pythonhosted.org/packages/12/48/60048a3558f591fbaf11a73b342262699178cf44dd6fb88827118be34335/spacy-3.8.16-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:111d817b32755d869e5ed6cc258b55c50c6687f47b78f6ebb2c14b1ce5ee707c", size = 35151231, upload-time = "2026-08-24T10:05:15.911Z" }, + { url = "https://files.pythonhosted.org/packages/96/6e/3e09ebd5635e1c6a2b92baee5d593a907908ce1b38fb1f011dbb0d66c411/spacy-3.8.16-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8a8a2bf3eb3486a0992176b77ac1d38ca9c669623941fdb8d3dddacb44dfd28e", size = 34649694, upload-time = "2026-08-24T10:05:20.225Z" }, + { url = "https://files.pythonhosted.org/packages/78/8c/b31440943778f8c6a63dc568df5d0d3b4a58c9472784416474e71d04eca5/spacy-3.8.16-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a237491463e351755f0167546f6a821d42971c5275a219a62d468f19f654138b", size = 35664934, upload-time = "2026-08-24T10:05:24.369Z" }, + { url = "https://files.pythonhosted.org/packages/55/f4/a613999ef17bf8252d4e6a62609b9d16a932e1cfd56f9c682e5dd82d91ba/spacy-3.8.16-cp313-cp313-win_amd64.whl", hash = "sha256:cc7e449aec9a313bc037ef5ea45fb0ac99135d92dace8421d68414d16be39543", size = 15163063, upload-time = "2026-08-24T10:05:27.428Z" }, + { url = "https://files.pythonhosted.org/packages/15/6e/97039de4f188ae3c69b5de9c852b4c5c5252896d2bc3d9ed1af93655ba1d/spacy-3.8.16-cp313-cp313-win_arm64.whl", hash = "sha256:024ce6408ea00c7f8c6387a6de65bb67aad40c51c9d63705303c5cb9a8ef51b0", size = 14540066, upload-time = "2026-08-24T10:05:30.354Z" }, + { url = "https://files.pythonhosted.org/packages/3a/18/d495cb375546ea29f74224854e605daa08a3e438530aa0d311ad11699833/spacy-3.8.16-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:dc17227717aa254b63c90161d8de4ec672ca5bd8e5c92effba2a8510498ee355", size = 6603726, upload-time = "2026-08-24T10:05:32.809Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c9/6f12f115f672627f7cc9cc10201b6ae2e59f1907b30f38cf48e120108942/spacy-3.8.16-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39304dd9800065c09fa440983ac75cf444469b159959171c0e0d761b3e854d62", size = 6508263, upload-time = "2026-08-24T10:05:34.775Z" }, + { url = "https://files.pythonhosted.org/packages/f2/68/ec6fbae239df6e1ba1b8c7187fb9d3654b908c9789be5288004b8665fd56/spacy-3.8.16-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04fd0206c9f33a0542a40049211528742b5492ef5f971d06b151b9cc49b9237b", size = 34427589, upload-time = "2026-08-24T10:05:38.32Z" }, + { url = "https://files.pythonhosted.org/packages/c4/1c/439d28bda90d057e0688c80c89487174e8dae4988268abea461edaf4f28b/spacy-3.8.16-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c51eac85344784ca7b184f0c3f7da0fca47c354d63e05e733d90cae35a2ecc4", size = 34800189, upload-time = "2026-08-24T10:05:42.371Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c8/5b2392b10f6e7f1d2f8851bef3ddf5a62c949912ef842a62c7f191d6cadd/spacy-3.8.16-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b741266d901222dde979a802d5e9f3cf3d9bf77a15be3137b387f62905a74d57", size = 34587881, upload-time = "2026-08-24T10:05:46.195Z" }, + { url = "https://files.pythonhosted.org/packages/64/60/89d411d014ba7edc9603cdacacb7df88ca2b5a7cbde2771dff3f72a7c29d/spacy-3.8.16-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5e32a51b115674d3f42c6cde696c583dc1594f5bff6b430de4aba4c753d47f93", size = 35369907, upload-time = "2026-08-24T10:05:49.947Z" }, + { url = "https://files.pythonhosted.org/packages/bb/22/f3f45881d0f5cd7c3ee4011f4c5ffafa76d4ee520649b3eb04440d6dd67c/spacy-3.8.16-cp314-cp314-win_amd64.whl", hash = "sha256:86227a0a0d3dfee3f3dcc15f73c1387b586d77b075348afc250ffafea88ffcec", size = 15202134, upload-time = "2026-08-24T10:05:52.922Z" }, + { url = "https://files.pythonhosted.org/packages/24/2f/0f2470625e61a3f58792e8fd94ac5fd0682057c9918cac09c3aef3ca203d/spacy-3.8.16-cp314-cp314-win_arm64.whl", hash = "sha256:15908539b375bd8e3c627a076dac793b8fd790c5945f3a696837dd70776a4fc0", size = 14603383, upload-time = "2026-08-24T10:05:55.59Z" }, +] + +[[package]] +name = "spacy-legacy" +version = "3.0.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/79/91f9d7cc8db5642acad830dcc4b49ba65a7790152832c4eceb305e46d681/spacy-legacy-3.0.12.tar.gz", hash = "sha256:b37d6e0c9b6e1d7ca1cf5bc7152ab64a4c4671f59c85adaf7a3fcb870357a774", size = 23806, upload-time = "2023-01-23T09:04:15.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/55/12e842c70ff8828e34e543a2c7176dac4da006ca6901c9e8b43efab8bc6b/spacy_legacy-3.0.12-py2.py3-none-any.whl", hash = "sha256:476e3bd0d05f8c339ed60f40986c07387c0a71479245d6d0f4298dbd52cda55f", size = 29971, upload-time = "2023-01-23T09:04:13.45Z" }, +] + +[[package]] +name = "spacy-loggers" +version = "1.0.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/67/3d/926db774c9c98acf66cb4ed7faf6c377746f3e00b84b700d0868b95d0712/spacy-loggers-1.0.5.tar.gz", hash = "sha256:d60b0bdbf915a60e516cc2e653baeff946f0cfc461b452d11a4d5458c6fe5f24", size = 20811, upload-time = "2023-09-11T12:26:52.323Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/78/d1a1a026ef3af911159398c939b1509d5c36fe524c7b644f34a5146c4e16/spacy_loggers-1.0.5-py3-none-any.whl", hash = "sha256:196284c9c446cc0cdb944005384270d775fdeaf4f494d8e269466cfa497ef645", size = 22343, upload-time = "2023-09-11T12:26:50.586Z" }, +] + +[[package]] +name = "srsly" +version = "2.5.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "catalogue", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2b/db/f794f219a6c788b881252d2536a8c4a97d2bdaadc690391e1cb53d123d71/srsly-2.5.3.tar.gz", hash = "sha256:08f98dbecbff3a31466c4ae7c833131f59d3655a0ad8ac749e6e2c149e2b0680", size = 490881, upload-time = "2026-03-23T11:56:59.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/67/e6d4decfb0cdc95b54c60854a1a6d1702983c39206c2b9f70f4ab18b17c8/srsly-2.5.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c812302a9acfe171e82f680b7ad642014cd017380b2c678441b3da4fb513c498", size = 657202, upload-time = "2026-03-23T11:55:34.938Z" }, + { url = "https://files.pythonhosted.org/packages/cc/5d/cb8b093d0836e59c152de6dfdb5db80c6408b00def0123f26d24bffde480/srsly-2.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:91688edb1f49110870d2c215db2cf445f1763c14173698ead0818908c51fb2a1", size = 657951, upload-time = "2026-03-23T11:55:36.571Z" }, + { url = "https://files.pythonhosted.org/packages/71/a1/5d2fb4c6a8e0e39dd1fb23bdd8feb1f2525ce90b28946f9f58ac5d3a039c/srsly-2.5.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1fd6c35c65c4d2435ae5bfb57b59682cf9b61606318a2a761856be9d7cc2d9e3", size = 1119766, upload-time = "2026-03-23T11:55:38.351Z" }, + { url = "https://files.pythonhosted.org/packages/ff/83/0862ffac8c06ed595dd1e28f261c37956585b9cf6b9bd049f8430a4c2daf/srsly-2.5.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b9df76d5a6bbf50967589bd42df3c522dd88babea2be745a507f56b41ab40626", size = 1120674, upload-time = "2026-03-23T11:55:39.644Z" }, + { url = "https://files.pythonhosted.org/packages/f1/06/42f72bab50876a708a10e6fc026ae8c7f185507d9f27544fa4ee8567c5fd/srsly-2.5.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a595958d0b1ff6d59c2570a3f0d1c8e36ab9f89d6e1b9c96fa7eb5e1a8698510", size = 1078505, upload-time = "2026-03-23T11:55:41.299Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f4/dfb86bc5c3abee267fb2f34895ea80d0159a084987a93d56ed1bf5ebefe4/srsly-2.5.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bc0ad5be2aeb9ff29c8512848d39d7c63fdd4bfbb5516bc523f5de5a77e55e6d", size = 1090635, upload-time = "2026-03-23T11:55:42.7Z" }, + { url = "https://files.pythonhosted.org/packages/2a/a6/561b46eff4477191dd649e09dd9b88afc44aad7ce204c45f4e45ad04861d/srsly-2.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:d2b8cfd8aee4d06ab335d359e4095d206102300a5e105a4b4bc69acca42427a6", size = 651653, upload-time = "2026-03-23T11:55:44.429Z" }, + { url = "https://files.pythonhosted.org/packages/dc/05/b122a1afaf8e8644d10f0203ad5174993910e6f727843089f0d48b444340/srsly-2.5.3-cp310-cp310-win_arm64.whl", hash = "sha256:c378afcb7dd7c42f426a66112496c949fc39e5883de6817d86e60afa51720ccc", size = 639118, upload-time = "2026-03-23T11:55:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/9a/36/5d7bb412d52e9cca787f9bfe838b596367189b254e50bf90f234a97184bf/srsly-2.5.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:785a09216ac31570fb301ddb9f61ee73d1f18f8b9561f712dce0b8ac8628bc88", size = 656760, upload-time = "2026-03-23T11:55:47.155Z" }, + { url = "https://files.pythonhosted.org/packages/d6/dc/124f008cd2be3e887e972cbdeb17c5aee0f42093eca02c7cfd63bb5daf19/srsly-2.5.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0017c7d2a0cd9a4f1bdc00d946b45edcf90bb0e271e8f084c1ce542bf6708c32", size = 657503, upload-time = "2026-03-23T11:55:48.681Z" }, + { url = "https://files.pythonhosted.org/packages/35/8a/2c97244ebab125d55f1bfb7bb94e9572b3e819410dffd6a040eca1112350/srsly-2.5.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:66ebae2c70305987341519ec1a720072a3cb3e4b1d52ac0e9e841f4d02658d3d", size = 1139161, upload-time = "2026-03-23T11:55:50.179Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ea/ecd396188f7591d80b89665f7af9e3ae02e42683daef57033ad7993ad3f9/srsly-2.5.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4ca4a068f6e14d84113a02fcb875c6b50a6285a12938c0e7a157eb3a63c50a86", size = 1142438, upload-time = "2026-03-23T11:55:52.607Z" }, + { url = "https://files.pythonhosted.org/packages/9c/65/143e2e143c53d498ad0956f69d0e09189aa7a6e0ee6017758c285ba1ab2d/srsly-2.5.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e283fa2a8f7350fb9fb70ecdee28d59d39c92f4c7f1cc90a44d6b86db3b3a8b3", size = 1101783, upload-time = "2026-03-23T11:55:53.906Z" }, + { url = "https://files.pythonhosted.org/packages/6b/86/1392a5593de0cd3d08c2d6c071b877c84358a37f63172c4e9cb71706842d/srsly-2.5.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9ffc97e22730ea97b00f7c303ccc60b1305e786afadb2a4a46578dafa4d29da0", size = 1115876, upload-time = "2026-03-23T11:55:55.624Z" }, + { url = "https://files.pythonhosted.org/packages/d4/a5/6193aa4c08e488821538fcbce2282449e228fd2183ed67d118bb5ccd8b54/srsly-2.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:f09b551f6c3e334652831ac68c770ee4284741ce0a3895bf1ccf2a1178d66cdd", size = 651733, upload-time = "2026-03-23T11:55:56.964Z" }, + { url = "https://files.pythonhosted.org/packages/66/a8/a73181743b6d237026615ca75c3fb3e4780736f1390550a7350d0c7f1149/srsly-2.5.3-cp311-cp311-win_arm64.whl", hash = "sha256:21cf09e417d3e4f3fbf7dd337fd6d948c97abd01896b9b4cb80e81cd9778a73a", size = 639124, upload-time = "2026-03-23T11:55:58.532Z" }, + { url = "https://files.pythonhosted.org/packages/02/cc/e9f7fcec4cc92ad8bad6316c4241638b8cf7380382d4489d94ec6c436452/srsly-2.5.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:71e51c046ccbeefb86524c6b1e17574f579c6ac4dc8ea4a09437d3e8f88342d3", size = 658379, upload-time = "2026-03-23T11:55:59.85Z" }, + { url = "https://files.pythonhosted.org/packages/21/e4/fea4512e9785f58509b2cf67d993323848e583161b5fcfdc7dd9d7c1f3df/srsly-2.5.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f73c0db911552e94fe2016e1759d261d2f47926f68826664cada3723c87006a", size = 658513, upload-time = "2026-03-23T11:56:01.239Z" }, + { url = "https://files.pythonhosted.org/packages/20/b1/53591681b6ff2699a4f97b2d5552ba196eaa6a979b0873605f4c04b5f7ee/srsly-2.5.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c1ac27ae5f4bb9163c7d2c45fc8ec173aac3d92e32086d9472b326c5c6e570e", size = 1172265, upload-time = "2026-03-23T11:56:02.589Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c9/741e29f534919a944a16da4184924b1d3404c4bf60716ab2b91be771d1e3/srsly-2.5.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:99026bcd9cbd3211cc36517400b04ca0fc5d3e412b14daf84ee6e65f67d9a2d8", size = 1180873, upload-time = "2026-03-23T11:56:03.944Z" }, + { url = "https://files.pythonhosted.org/packages/89/57/5554f786eccf78b2750d6ac63be126e1b67badec2cb409dd611cf6f8c52b/srsly-2.5.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:07d682679e639eb46ff7e6da4a92714f4d5ffe351d088ee66f221e9b1f8865bb", size = 1120437, upload-time = "2026-03-23T11:56:05.283Z" }, + { url = "https://files.pythonhosted.org/packages/eb/95/9b4f73b1be3692f86d72ccc131c8e50f26f824d5c8830a59390bcc5b60ef/srsly-2.5.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8e0542d85d6b55cf2934050d6ffcb1cd76c768dcf9572e7467002cf087bb366d", size = 1137376, upload-time = "2026-03-23T11:56:06.613Z" }, + { url = "https://files.pythonhosted.org/packages/5a/de/89ca640ca1953c4612279ce515d0af35658df3c06cdb324329bc91b4a7e1/srsly-2.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:598f1e494c18cacb978299d77125415a586417081959f8ec3f068b32d97f8933", size = 652459, upload-time = "2026-03-23T11:56:07.994Z" }, + { url = "https://files.pythonhosted.org/packages/6d/4f/7ab6d49e36d9cc72ee15746cabd116eb6f338be8a06c1882968ee9d6c7d7/srsly-2.5.3-cp312-cp312-win_arm64.whl", hash = "sha256:4b1b721cd3ad1a9b2343519aadc786a4d09d5c0666962d49852eb12d6ec3fe26", size = 638411, upload-time = "2026-03-23T11:56:09.31Z" }, + { url = "https://files.pythonhosted.org/packages/9d/5c/12901e3794f4158abc6da750725aad6c2afddb1e4227b300fe7c71f66957/srsly-2.5.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e67b6bbacbfadea5e100266d2797f2d4cec9883ea4dc84a5537673850036a8d8", size = 656750, upload-time = "2026-03-23T11:56:10.708Z" }, + { url = "https://files.pythonhosted.org/packages/04/61/181c26370995f96f56f1b64b801e3ca1e0d703fc36506ae28606d62369fb/srsly-2.5.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:348c231b4477d8fe86603131d0f166d2feac9c372704dfc4398be71cc5b6fb07", size = 656746, upload-time = "2026-03-23T11:56:12.28Z" }, + { url = "https://files.pythonhosted.org/packages/77/c6/35876c78889f8ffe11ed3521644e666c3aef20ea31527b70f47456cf35c2/srsly-2.5.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b0938c2978c91ae1ef9c1f2ba35abb86330e198fb23469e356eba311e02233ee", size = 1155762, upload-time = "2026-03-23T11:56:14.075Z" }, + { url = "https://files.pythonhosted.org/packages/3e/da/40b71ca9906c8eb8f8feb6ac11d33dad458c85a56e1de764b96d402168a0/srsly-2.5.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f6a837954429ecbe6dcdd27390d2fb4c7d01a3f99c9ffcf9ce66b2a6dd1b738", size = 1161092, upload-time = "2026-03-23T11:56:15.778Z" }, + { url = "https://files.pythonhosted.org/packages/dc/14/c0dd30cc8b93ce8137ff4766f743c882440ce49195fffc5d50eaeef311a6/srsly-2.5.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3576c125c486ce2958c2047e8858fe3cfc9ea877adfa05203b0986f9badee355", size = 1109984, upload-time = "2026-03-23T11:56:17.056Z" }, + { url = "https://files.pythonhosted.org/packages/08/f3/34354f183d8faafc631585571224b54d1b4b67e796972c36519c074ca355/srsly-2.5.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5fb59c42922e095d1ea36085c55bc16e2adb06a7bfe57b24d381e0194ae699f2", size = 1128409, upload-time = "2026-03-23T11:56:18.761Z" }, + { url = "https://files.pythonhosted.org/packages/a4/d9/5531f8a19492060b4e76e4ab06aca6f096fb5128fe18cc813d1772daf653/srsly-2.5.3-cp313-cp313-win_amd64.whl", hash = "sha256:111805927f05f5db440aeeacb85ce43da0b19ce7b2a09567a9ef8d30f3cc4d83", size = 650820, upload-time = "2026-03-23T11:56:20.096Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8a/62fb7a971eca29e12f03fb9ddacb058548c14d33e5b5675ff0f85839cc7b/srsly-2.5.3-cp313-cp313-win_arm64.whl", hash = "sha256:0f106b0a700ab56e4a7c431b0f1444009ab6cb332edc7bbf6811c2a43f4722cb", size = 637278, upload-time = "2026-03-23T11:56:21.439Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5b/e4ef43c2a381711230af98d4c94a5323df48d6a7899ee652e05bf889290e/srsly-2.5.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:39c13d552a9f9674a12cdcdc66b0c2f02f3430d0cd04c5f9cf598824c2bd3d65", size = 661294, upload-time = "2026-03-23T11:56:23.29Z" }, + { url = "https://files.pythonhosted.org/packages/92/2d/ebce7f3717e52cd0a01f4ec570f388f3b7098526794fcf1ad734e0b8f852/srsly-2.5.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:14c930767cc169611a2dc14e23bc7638cfb616d6f79029700ade033607343540", size = 660952, upload-time = "2026-03-23T11:56:24.908Z" }, + { url = "https://files.pythonhosted.org/packages/22/47/a8f3e9b214be2624c8e8a78d38ca7b1d4e26b92d57018412e4bfc4abe89a/srsly-2.5.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2f2d464f0d0237e32fb53f0ec6f05418652c550e772b50e9918e83a1577cba4d", size = 1154554, upload-time = "2026-03-23T11:56:26.608Z" }, + { url = "https://files.pythonhosted.org/packages/d6/71/2a89dc3180a51e633a87a079ca064225f4aaf46c7b2a5fc720e28f261d98/srsly-2.5.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d18933248a5bb0ad56a1bae6003a9a7f37daac2ecb0c5bcbfaaf081b317e1c84", size = 1155746, upload-time = "2026-03-23T11:56:28.102Z" }, + { url = "https://files.pythonhosted.org/packages/b8/36/72e5ce3153927ca404b6f5bf5280e6ff3399c11557df472b153945468e0a/srsly-2.5.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7ea5412ea229e571ac9738cbe14f845cc06c8e4e956afb5f42061ccd087ef31f", size = 1112374, upload-time = "2026-03-23T11:56:29.591Z" }, + { url = "https://files.pythonhosted.org/packages/04/b2/0895de109c28eca0d41a811ab7c076d4e4a505e8466f06bae22f5180a1dd/srsly-2.5.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8d3988970b4cf7d03bdd5b5169302ff84562dd2e1e0f84aeb34df3e5b5dc19bf", size = 1127732, upload-time = "2026-03-23T11:56:31.458Z" }, + { url = "https://files.pythonhosted.org/packages/c7/79/a37fa7759797fbdfe0a2e029ab13e78b1e81e191220d2bb8ff57d869aefb/srsly-2.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:6a02d7dcc16126c8fae1c1c09b2072798a1dc482ab5f9c52b12c7114dac47325", size = 656467, upload-time = "2026-03-23T11:56:33.14Z" }, + { url = "https://files.pythonhosted.org/packages/d7/25/0dae019b3b90ad9037f91de4c390555cdaac9460a93ad62b02b03babdff5/srsly-2.5.3-cp314-cp314-win_arm64.whl", hash = "sha256:1c9129c4abe31903ff7996904a51afdd5428060de6c3d12af49a4da5e8df2821", size = 643040, upload-time = "2026-03-23T11:56:34.448Z" }, + { url = "https://files.pythonhosted.org/packages/3a/44/72dd5285b2e05435d98b0797f101d91d9b345d491ddc1fdb9bd09e27ccb8/srsly-2.5.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:29d5d01ba4c2e9c01f936e5e6d5babc4a47b38c9cbd6e1ec23f6d5a49df32605", size = 666200, upload-time = "2026-03-23T11:56:35.753Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ad/002c71b87fc3f648c9bf0ec47de0c3822bf2c95c8896a589dd03e7fd3977/srsly-2.5.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5c8df4039426d99f0148b5743542842ab96b82daded0b342555e15a639927757", size = 667409, upload-time = "2026-03-23T11:56:37.172Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/2cea3d5e80aeecfc4ece9e7e1783e7792cc3bad7ab85ab585882e1db4e38/srsly-2.5.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:06a43d63bde2e8cccadb953d7fff70b18196ca286b65dd2ad16006d65f3f8166", size = 1265941, upload-time = "2026-03-23T11:56:38.825Z" }, + { url = "https://files.pythonhosted.org/packages/aa/38/8a4d7e86dd0370a2e5af251b646000197bb5b7e0f9aa360c71bbfb253d0d/srsly-2.5.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:808cfafc047f0dec507a34c8fa8e4cda5722737fd33577df73452f52f7aca644", size = 1250693, upload-time = "2026-03-23T11:56:40.449Z" }, + { url = "https://files.pythonhosted.org/packages/99/05/340129de5ea7b237271b12f8a6962cfa7eb0c5a3056794626d348c5ae7c7/srsly-2.5.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:71d4cbe2b2a1335c76ed0acae2dc862163787d8b01a705e1949796907ed94ccd", size = 1242408, upload-time = "2026-03-23T11:56:41.8Z" }, + { url = "https://files.pythonhosted.org/packages/01/cb/d7fee7ab27c6aa2e3f865fb7b50ba18c81a4c763bba12bdf53df246441bc/srsly-2.5.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:565f69083d33cb329cfc74317da937fb3270c0f40fabc1b4488702d8074b4a3e", size = 1242749, upload-time = "2026-03-23T11:56:43.246Z" }, + { url = "https://files.pythonhosted.org/packages/d8/d1/9bad3a0f2fa7b72f4e0cf1d267b00513092d20ef538c47f72823ae4f7656/srsly-2.5.3-cp314-cp314t-win_amd64.whl", hash = "sha256:8ac016ffaeac35bc010992b71bf8afdd39d458f201c8138d84cf78778a936e6c", size = 673783, upload-time = "2026-03-23T11:56:44.875Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ae/57d1d7af907e20c077e113e0e4976f87b82c0a415403d99284a262229dd0/srsly-2.5.3-cp314-cp314t-win_arm64.whl", hash = "sha256:d822083fe26ec6728bd8c273ac121fc4ab3864a0fdf0cf0ff3efb188fcd209ed", size = 650229, upload-time = "2026-03-23T11:56:46.148Z" }, +] + [[package]] name = "sse-starlette" version = "3.3.2" @@ -3042,6 +7047,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d8/d6/2fd27147f53c45e546c0c542c6ea4a24b93c1f3908dcbad164624e5ddfa6/strands_agents-1.50.2-py3-none-any.whl", hash = "sha256:39c6b755e579e0b631ea01af78a0a201019320670b87adcf0b5c8dd1e5a2cef8", size = 638411, upload-time = "2026-07-27T20:38:46.12Z" }, ] +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath", marker = "python_full_version < '3.11' or python_full_version == '3.12.*' or sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + [[package]] name = "tenacity" version = "9.1.4" @@ -3060,6 +7077,113 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5", size = 7734, upload-time = "2025-12-29T12:55:20.718Z" }, ] +[[package]] +name = "thinc" +version = "8.3.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blis", marker = "python_full_version >= '3.11'" }, + { name = "catalogue", marker = "python_full_version >= '3.11'" }, + { name = "confection", marker = "python_full_version >= '3.11'" }, + { name = "cymem", marker = "python_full_version >= '3.11'" }, + { name = "murmurhash", marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "packaging", marker = "python_full_version >= '3.11'" }, + { name = "preshed", marker = "python_full_version >= '3.11'" }, + { name = "pydantic", marker = "python_full_version >= '3.11'" }, + { name = "setuptools", marker = "python_full_version >= '3.11'" }, + { name = "srsly", marker = "python_full_version >= '3.11'" }, + { name = "wasabi", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/46/76df95f2c327f9a9cef30c1523bf285627897097163584dcf5f77b2ebce2/thinc-8.3.13.tar.gz", hash = "sha256:68e658549fc1eb3ff92aed5147fcbb9c15d6e9cc0e623b4d0998d16522ffb4f9", size = 194640, upload-time = "2026-03-23T07:22:36.41Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/e3/df570d55f38250d153e209d998f60e334026ea60cf9a887cffb85d7ee9bf/thinc-8.3.13-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:84fb50fe572a1860165f2e7a640c7cb70d43d6962366e69f643fa9a27e4a2127", size = 846996, upload-time = "2026-03-23T07:21:32.701Z" }, + { url = "https://files.pythonhosted.org/packages/ec/72/e97c9cb863ef0a645ba069c24e0981bfaedf8241ba199512ebcd64ba090a/thinc-8.3.13-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3dac18a0fb0a42f711c2ce9c02cbb090385aecae92089aa17b9dfd808a542013", size = 815368, upload-time = "2026-03-23T07:21:34.392Z" }, + { url = "https://files.pythonhosted.org/packages/b6/7a/9283f52b1210dc052b795e22ec739d13929b914d1289e49336bede34c4eb/thinc-8.3.13-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e08b1577a56e7315770af280aabd8fa5f2a1fb6afd1c50a4183c06e907faf558", size = 3885033, upload-time = "2026-03-23T07:21:35.772Z" }, + { url = "https://files.pythonhosted.org/packages/93/9a/aa8f2e19819c02781b282c3a9cfb57c76ff1fbe0b6deaa1ffd04dc920894/thinc-8.3.13-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:303477eb51b9b39c94a7fc7967ee8a039eca1ca37d95dcce1234c83b95b4ee9f", size = 3912947, upload-time = "2026-03-23T07:21:37.219Z" }, + { url = "https://files.pythonhosted.org/packages/51/fa/ea7c67667b8a875178bea5a42dc9c8b0622c34e7eba3d8e42874f2c4b4c1/thinc-8.3.13-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d7a9654f9ca362a4be7f5e590fdfee26e2e2084da9fd3306032ec037e99f2f8e", size = 4887518, upload-time = "2026-03-23T07:21:38.76Z" }, + { url = "https://files.pythonhosted.org/packages/36/44/99c391e951e3b706b9a7552ced720e9ec3bddd6707a99d53e4354ebefa45/thinc-8.3.13-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e1f8d13bf92ee10595c40692fd4cf8e7bbe73bd9f260107e975fd5dbee1af42b", size = 5044691, upload-time = "2026-03-23T07:21:40.257Z" }, + { url = "https://files.pythonhosted.org/packages/ce/cf/9d95fb5f12d76ad1c7570a9a38da2f2f60dba721c87630bfabaabef91bc3/thinc-8.3.13-cp310-cp310-win_amd64.whl", hash = "sha256:e7f046d8914055cad51e83ff0da1a892acb73cd58556d7c1a5d4015a3766a899", size = 1795372, upload-time = "2026-03-23T07:21:41.709Z" }, + { url = "https://files.pythonhosted.org/packages/b4/72/ca06842a007e8c794e8c59462f242cdfd6167d7cc9d0155ad004b194b015/thinc-8.3.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4565102638038a01a2193c7f5d41ccbd6233fbdcb1f1b184322a06add4f51f18", size = 844359, upload-time = "2026-03-23T07:21:43.017Z" }, + { url = "https://files.pythonhosted.org/packages/48/44/e6aef092f478d263f72eb3933b55a6f37ba97c6a0ea0a61d13fbf9bf0c19/thinc-8.3.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:859fbd9d9b16af5278da23589b4afbe2ab6b0dd615df4d3229b7c4e67cd3107e", size = 812089, upload-time = "2026-03-23T07:21:44.618Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8a/9ce0424d456cd3580cc3a855b23a7ff86b81d5299fceb496a2f56f06c1c0/thinc-8.3.13-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a518d5c761a0f2341e530e867de133dc3ed814558365b2a68ec53b89c482a43f", size = 4101388, upload-time = "2026-03-23T07:21:46.135Z" }, + { url = "https://files.pythonhosted.org/packages/ad/51/ec91c0434bd9a1096ab874bbd6dc110c5089d7fc513137e6af59bd051eec/thinc-8.3.13-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:81337dfbee37f58f36c0c70f9a819dce1b32cdc13d959181e10de079621f6ac6", size = 4131972, upload-time = "2026-03-23T07:21:48.403Z" }, + { url = "https://files.pythonhosted.org/packages/ff/67/e30dea753c90cff5cb9e5feb34948fdb89a6774b84d849585b49e16a730e/thinc-8.3.13-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fbc0ee16edd260c6a4a9e365ff36d0a682c9e7ca6d7b985682659ef2e3e73826", size = 5101283, upload-time = "2026-03-23T07:21:49.991Z" }, + { url = "https://files.pythonhosted.org/packages/00/e9/b7544eddababa16e548b26a96fff29eeb307ce938df5fa4af9371fe8ed5d/thinc-8.3.13-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0355c37e40d1a9fc2a1b8e9c2e294d8586f6baa97bcac6b9002f2dddb4b82ae9", size = 5264488, upload-time = "2026-03-23T07:21:51.747Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a9/49391a40d703efc0f7a451310373261835f71fd3e6e2e8cfc08ee02f78ad/thinc-8.3.13-cp311-cp311-win_amd64.whl", hash = "sha256:0a0fa13dcfe4b319c3a396432c1dbff30d3de37dbbdee559e76600ee2b9486df", size = 1795058, upload-time = "2026-03-23T07:21:53.424Z" }, + { url = "https://files.pythonhosted.org/packages/c1/31/fd5348d44beda12a3ee415cbba9ed4fd0b17ce65db1d473c38a29a8d6153/thinc-8.3.13-cp311-cp311-win_arm64.whl", hash = "sha256:cd8a2b714c061969eee65802965167a6ada1fe708d82fe176d98dcb95ebe182a", size = 1721215, upload-time = "2026-03-23T07:21:55.027Z" }, + { url = "https://files.pythonhosted.org/packages/3e/af/f7c1ebfe92eb5d27d7f2f3da67a11e2eb57bc30ab1553279af6dc65b65a8/thinc-8.3.13-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:77a41f66285321d20aaedaea1e87d7cd48dca6d2427bed1867ec7cba7109fc8d", size = 821097, upload-time = "2026-03-23T07:21:56.698Z" }, + { url = "https://files.pythonhosted.org/packages/45/8f/69d7338575d98df85d0b54c0f5fc277dba72587fe9ab846ecdd12a998bcb/thinc-8.3.13-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3710d318b4e5460cf366a6f7b5ddbefb5d39dbd4cfa408222750fdc6c27c4411", size = 791932, upload-time = "2026-03-23T07:21:58.38Z" }, + { url = "https://files.pythonhosted.org/packages/4b/a5/21d010c81e81e1589e5ccb4950e521804d13726e541e87f644c51815673b/thinc-8.3.13-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5a08c87143a6d20177652dca1ec0dc815d88216d8fc62594a57e8bc45bf5ed49", size = 3854219, upload-time = "2026-03-23T07:21:59.819Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ff/6914bf370bd1d604d89e6dfb46b97d10cd9b00d42ff8c036283e92314a8c/thinc-8.3.13-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4b5ec9ff313819e7d8667794a3559463fa89ff45aaa73e3fd8d6273b1e0d7a7f", size = 3903307, upload-time = "2026-03-23T07:22:01.652Z" }, + { url = "https://files.pythonhosted.org/packages/f3/3d/5572b47fa155fb3388c071515b74024fa17a6efd1df9406da378f0aa84ef/thinc-8.3.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5c9a48f2bc1e04f138240ed5f9b815a9141a5de26accd0f08fa0137fcefed258", size = 4836882, upload-time = "2026-03-23T07:22:03.565Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f0/a8d77c7bac089697c6df302cc3c936a1ab36a4720deae889e6f1dbcbd0eb/thinc-8.3.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:79a29a44d76bd02f5ac0624268c6e42b3576ae472c791a8ae9c2d813ae789b59", size = 5033398, upload-time = "2026-03-23T07:22:05.045Z" }, + { url = "https://files.pythonhosted.org/packages/21/82/5651bb1f904d04220fc7670035ada921bf0638e2cff6444d67c12887a968/thinc-8.3.13-cp312-cp312-win_amd64.whl", hash = "sha256:ed1dc709ac4f2f03b710457889e4e02f05de51bc8456980c241d0b28798bc7cb", size = 1721248, upload-time = "2026-03-23T07:22:06.749Z" }, + { url = "https://files.pythonhosted.org/packages/94/8d/683703de021ffbe46833d722b70f49ffbbca8e5bd6876256977555d92d7d/thinc-8.3.13-cp312-cp312-win_arm64.whl", hash = "sha256:c6a049703a6011c8fe26ee41af7e70272145594140d82f79bb23de619c6a6525", size = 1645777, upload-time = "2026-03-23T07:22:08.104Z" }, + { url = "https://files.pythonhosted.org/packages/af/b9/7b46942176df459d1804a9e77b0976f7c56f3abf3ec7485d0e5f836a0382/thinc-8.3.13-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c2811dfd8d46d8b5d3b39051b23e64006b2994a5143b1978b436938018792af8", size = 817337, upload-time = "2026-03-23T07:22:09.538Z" }, + { url = "https://files.pythonhosted.org/packages/a7/79/53085a72cd8f4fc4e6e313d05ea5aa98e870684f4a0fb318a9875fc0a964/thinc-8.3.13-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5593e6300cb1ebe0c0e546e9c9fb49e7c2627a0aa688795cd4f995a8b820d2ec", size = 788120, upload-time = "2026-03-23T07:22:11.215Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3e/d61b462b16da95ac6885f95bb395e672040ee594833e571a6edcffd234f5/thinc-8.3.13-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f697174d3fb474966ce50b430bbafa101a6d2f7ffb559dac4b5c59389ef72d22", size = 3844666, upload-time = "2026-03-23T07:22:12.67Z" }, + { url = "https://files.pythonhosted.org/packages/78/4c/898cc654bb123734c71ec5a425c02ca34439517d01ce1c95a6563295580e/thinc-8.3.13-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e9c7c5c104737b414c8c4ec578e67d78b6c859afe25cbc0684402e721415bd7f", size = 3890658, upload-time = "2026-03-23T07:22:14.668Z" }, + { url = "https://files.pythonhosted.org/packages/cd/56/1abdbf0a4ad628e8a05d6516fe0745969649d805367a3dccad8ee872981b/thinc-8.3.13-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7a99d0e242d1ccd23f9ae6bea7cd502f8626efa65c156b91d84581d0356696c3", size = 4819933, upload-time = "2026-03-23T07:22:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/f1/22/b84dbdc6be5055bbdb2a7352e2c393f67e8593c137f1b83c82bf1e062b6e/thinc-8.3.13-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e676edd21a747afbe3e6b9f3fca8b962e36d146ded03b070cb0c28e2dfbe9499", size = 5018099, upload-time = "2026-03-23T07:22:18.356Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a8/763cd7ba949334c9d2cddc92dadb68b344cb9546dc01b8d4a733dcaa16c1/thinc-8.3.13-cp313-cp313-win_amd64.whl", hash = "sha256:8ad40307f20e83f77af28ff5c6be0b86af7a8b251d1231c545508d2763157d8f", size = 1720309, upload-time = "2026-03-23T07:22:19.81Z" }, + { url = "https://files.pythonhosted.org/packages/f5/15/a11f7bb3cbc97dfecf32a90552f5a8f8a5c99316a99c6c17bdabf5baf256/thinc-8.3.13-cp313-cp313-win_arm64.whl", hash = "sha256:723949cab11d1925c15447928513a718276316cec6e0de28337cca0a62be0521", size = 1644606, upload-time = "2026-03-23T07:22:21.339Z" }, + { url = "https://files.pythonhosted.org/packages/80/40/f4937d113912c6d669ffe982356ab29dcb6c7fe3be926a15981dbbb6a91c/thinc-8.3.13-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7badb0be4825535e6362c19e8a41872b65409e9da46d3453a391b843a0720865", size = 817024, upload-time = "2026-03-23T07:22:23.005Z" }, + { url = "https://files.pythonhosted.org/packages/d2/00/4d4ed1a11ba2920b85a03a0683b16d97dc5beb2e78078dbf0e13e43bcea7/thinc-8.3.13-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:565300b7e13de799e5abff00d445f537e9256cf7da4dcb0d0f005fc16748a29e", size = 792096, upload-time = "2026-03-23T07:22:24.349Z" }, + { url = "https://files.pythonhosted.org/packages/44/5d/dc33d6932be8721af2ef76b4a3a6e8020648630eabae61fb916d2a861d1d/thinc-8.3.13-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c17cef1900a1aba7e1487493d16b8aa0a8633116f1b2a51c6649a4000697f17b", size = 3842215, upload-time = "2026-03-23T07:22:25.836Z" }, + { url = "https://files.pythonhosted.org/packages/af/bc/a6d37d8dadc2c5b524f51192413481160c42c9dd6105e8d5551531623225/thinc-8.3.13-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f4f26d1eec9b2a6a8f2e0298a5515d13eb06d70730d0d9e1040bb329e12bf3fb", size = 3849253, upload-time = "2026-03-23T07:22:27.845Z" }, + { url = "https://files.pythonhosted.org/packages/7a/59/ce9c7067f1dfe5985875927de9cf7a79f9dae3e69487fd650dfba558029d/thinc-8.3.13-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a61a31fd0ce3c2771cf4901ba6df70e774ffe32febf1024c5b43d63575cd58fe", size = 4831163, upload-time = "2026-03-23T07:22:29.395Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a8/f57819347fc4d8bef2204d15fcbb9d7dff2d6cdd5f83d5ed91456ddacc55/thinc-8.3.13-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ba8119daf84a12259ae4d251d36426417bafa0b34108890b4b7e2b50966bd990", size = 4986051, upload-time = "2026-03-23T07:22:30.933Z" }, + { url = "https://files.pythonhosted.org/packages/05/ef/a82214bb7c7c1e2d92b69e1a7654be90cfab180082c6108e45a98af2422c/thinc-8.3.13-cp314-cp314-win_amd64.whl", hash = "sha256:433e3826e018da489f1a8068e6de677f6eff3cc93991a599d90f12cd1bc26cdc", size = 1740382, upload-time = "2026-03-23T07:22:32.869Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ef/1648fda54e9689058335ff54f650a7a314db2a42e21af1b83949b2dc748e/thinc-8.3.13-cp314-cp314-win_arm64.whl", hash = "sha256:11754fada9ad5ba2e02d5f3f234f940e24015b82333db58372f4a6aedad9b43f", size = 1667687, upload-time = "2026-03-23T07:22:34.967Z" }, +] + +[[package]] +name = "timm" +version = "1.0.29" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub", version = "1.16.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and sys_platform != 'win32'" }, + { name = "huggingface-hub", version = "1.31.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform != 'win32') or (python_full_version < '3.11' and sys_platform == 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, + { name = "pyyaml", marker = "python_full_version < '3.11' or python_full_version == '3.12.*' or sys_platform != 'win32'" }, + { name = "safetensors", marker = "python_full_version < '3.11' or python_full_version == '3.12.*' or sys_platform != 'win32'" }, + { name = "torch", marker = "python_full_version < '3.11' or python_full_version == '3.12.*' or sys_platform != 'win32'" }, + { name = "torchvision", marker = "python_full_version < '3.11' or python_full_version == '3.12.*' or sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/72/58b8363e22508418d5fac897ac68e2c33f01f84d9d471edc38cbb6523dc6/timm-1.0.29.tar.gz", hash = "sha256:1af8d12bb7e15c5f96e98d60b7b8319cd7f31730778bf1af58e912a7ce171576", size = 2497559, upload-time = "2026-08-28T15:07:31.007Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/b7/19267ec60740ef1899d265272bff3623f19fe7f7bf1022fb098766db2134/timm-1.0.29-py3-none-any.whl", hash = "sha256:4266486e355c9beb9432e689de580d09e2efe7fa92a9a79e76c03e7763eac724", size = 2633555, upload-time = "2026-08-28T15:07:29.348Z" }, +] + +[[package]] +name = "tokenizers" +version = "0.23.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub", version = "1.16.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and sys_platform != 'win32'" }, + { name = "huggingface-hub", version = "1.31.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform != 'win32') or (python_full_version < '3.11' and sys_platform == 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/1e/bc6587c5ab643b2e17776cace9070a2ae73549c86bffac9934a600bf3c31/tokenizers-0.23.2.tar.gz", hash = "sha256:7f0f085686b9de0d0079e6f874ae053600db64c5d13049e0bbc0119926d25aac", size = 385745, upload-time = "2026-09-03T08:55:42.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/ed/8a443528baa6fac8dfe8c3b75b038c63ac92bb539bcabe311e227c718173/tokenizers-0.23.2-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:85a9a357a3764aecc904ee76bdaf8cf1ad8e5a67a1b929a487c4a39b49ed0e90", size = 3148852, upload-time = "2026-09-03T08:55:30.874Z" }, + { url = "https://files.pythonhosted.org/packages/67/49/22da045a91732384d3a3771816bf188dc5a1f702c32e635afa7c679c0bef/tokenizers-0.23.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:986670e43691469dcee610ea0f846f91a8f84e91fc6f7a48d4c064414c0ec2bf", size = 3101593, upload-time = "2026-09-03T08:55:28.587Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4d/8f569ed49372a3ed8e57099bd515055fd48d7c95912c4307cda6973c2168/tokenizers-0.23.2-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a37039b5dfc4af84eb3ef0a92f4307e28936c8f9adccba2629d36f652e9bf7a2", size = 3516830, upload-time = "2026-09-03T08:55:14.741Z" }, + { url = "https://files.pythonhosted.org/packages/2a/de/e2f14c8919d5bf51874051d00d6c7b7e0e8bde6c6a2dbeddda7f642896ff/tokenizers-0.23.2-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b7e37ba198f24150f523e1242e83c4970de4a525480586be5dcc24d9add32c5", size = 3407975, upload-time = "2026-09-03T08:55:16.842Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bd/93c69152d02ef06ce47aed8b2bf4952dcf733c935a62791873932b2934d9/tokenizers-0.23.2-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43e4f2071e3cc8d5d86421c874aebc82659bb51a68bcdef5a0da75ee89511ccb", size = 3748165, upload-time = "2026-09-03T08:55:24.769Z" }, + { url = "https://files.pythonhosted.org/packages/2d/b7/56b84b80bc96942bba8eb23751a9e8a1fce4faaf4390425e7083f721c98c/tokenizers-0.23.2-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:325fee2e0418a9dc6c9ecf736a5f5f0db7875183ace9549ae339da76f7a1fbb7", size = 4024165, upload-time = "2026-09-03T08:55:18.806Z" }, + { url = "https://files.pythonhosted.org/packages/9b/8a/0175e216f005c2fe08238292663aa41e4c802b216e71047a69a0e9fc6fa3/tokenizers-0.23.2-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:950d7c9426fa72406a0ffeacdbc0bb9985f5db20eb8b263f29c79aaf83105703", size = 3591899, upload-time = "2026-09-03T08:55:22.752Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ca/ca6b93c7820df123b2662a9469e8facc826ccc94e98fdd0d615f6431e73a/tokenizers-0.23.2-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41c2f84d172449b4dadb9cdc508e3e364076613c35b16e76ecfe47a60d1e3305", size = 3386843, upload-time = "2026-09-03T08:55:26.584Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a4/4f9106d317b14a80aefea9f0e3a8d07ef25f856a7607eb7f5ab894281fcb/tokenizers-0.23.2-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:12f0835dc2ee694746a76adf7b1567d4346a4a502ebe93fb1f5f80ea49799b78", size = 3577314, upload-time = "2026-09-03T08:55:20.825Z" }, + { url = "https://files.pythonhosted.org/packages/8d/6a/1552b70fb0d9ab074fd3fc961435d01364e79c9058481822c3af6e8d402c/tokenizers-0.23.2-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:eb2f9c8a24da020ea8c11a01a19c1c2547912d92121ae4a01cfbca46125dee40", size = 9967367, upload-time = "2026-09-03T08:55:33.188Z" }, + { url = "https://files.pythonhosted.org/packages/06/01/3ccb3a956c7528b2507b8a9714155c4baf86af593039db6ea375dd0c96c3/tokenizers-0.23.2-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:f486f402f6f9abee5bb032553736813af0c710a86b2e0ca592634c55cea1f835", size = 9811886, upload-time = "2026-09-03T08:55:35.642Z" }, + { url = "https://files.pythonhosted.org/packages/fa/73/7038e612d48bda1599457f712f6bd3854eae1a9dc9c13aa47f835349db48/tokenizers-0.23.2-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:bef235815a067b2648caf6dcc7a71091b0b0fff9ee8057f6451eb9335fae52ef", size = 10146224, upload-time = "2026-09-03T08:55:38.391Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d8/8e9e4e0b287a338d8f88976729628c9d22e8a54cfaf9777018a7f7cb58a0/tokenizers-0.23.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5c56bda1511921587789163e524d196ed8284174ac23abd7685d5ea8da6c4718", size = 10256304, upload-time = "2026-09-03T08:55:40.977Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1f/c79a01f671a49728ebb0b61f7ff9ea45663b66cab40bc0858e9859b25c16/tokenizers-0.23.2-cp310-abi3-win32.whl", hash = "sha256:debf978920d93ba9c219bd67cc4bbfaf912c9039e41e7a28b91ec15e3728c95a", size = 2592809, upload-time = "2026-09-03T08:55:48.02Z" }, + { url = "https://files.pythonhosted.org/packages/db/f7/0a69ac6b82dbccf3f71add938a161c497952749294b8dd6dfe03a819dc40/tokenizers-0.23.2-cp310-abi3-win_amd64.whl", hash = "sha256:2e96f5699d5249c9c64aa8412e044f727aae3a4098cf830f9901ec1afc361cde", size = 2863236, upload-time = "2026-09-03T08:55:46.193Z" }, + { url = "https://files.pythonhosted.org/packages/d7/b0/dee84cb44175be1b4c35bd2f770727494e78f0bb38e571a623ade94dbebb/tokenizers-0.23.2-cp310-abi3-win_arm64.whl", hash = "sha256:e49c394456dd9985787fec76132438ba3fb8911f857b1bf3d40119f9292d41aa", size = 2729352, upload-time = "2026-09-03T08:55:44.345Z" }, +] + [[package]] name = "tomli" version = "2.4.0" @@ -3114,6 +7238,161 @@ wheels = [ { 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 = "torch" +version = "2.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-bindings", marker = "python_full_version < '3.15' and sys_platform == 'linux'" }, + { name = "cuda-toolkit", extra = ["cublas", "cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "sys_platform == 'linux'" }, + { name = "filelock", marker = "python_full_version < '3.11' or python_full_version == '3.12.*' or sys_platform != 'win32'" }, + { name = "fsspec", marker = "python_full_version < '3.11' or python_full_version == '3.12.*' or sys_platform != 'win32'" }, + { name = "jinja2", marker = "python_full_version < '3.11' or python_full_version == '3.12.*' or sys_platform != 'win32'" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform != 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, + { name = "nvidia-cudnn-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu13", marker = "sys_platform == 'linux'" }, + { name = "setuptools", marker = "python_full_version < '3.11' or python_full_version == '3.12.*' or sys_platform != 'win32'" }, + { name = "sympy", marker = "python_full_version < '3.11' or python_full_version == '3.12.*' or sys_platform != 'win32'" }, + { name = "triton", marker = "python_full_version < '3.15' and sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11' or python_full_version == '3.12.*' or sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/28/14c2462ade87a149a794cfb1fbff9c9810638afbaf67122d9bc116270069/torch-2.14.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:2fb30099be1fec9d163a6dd05ace176305ce8cf914b4d7a5f4f6d7a326aa4b60", size = 127245743, upload-time = "2026-09-02T13:42:48.266Z" }, + { url = "https://files.pythonhosted.org/packages/25/08/31e9f27c4af038b8541e02cd0377a92a2629fc800f7213fa2264c0e051bd/torch-2.14.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:96383c62423c3f2767023c4a94774b6435e2c006691daf396e9ddb55d412fbc6", size = 453985993, upload-time = "2026-09-02T13:43:11.736Z" }, + { url = "https://files.pythonhosted.org/packages/8b/b7/4113c03a90423f94c82f8212b033423e78b71036a94b96c2cf2c1ef3b5eb/torch-2.14.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:d30207cb89e713dfd6afb5d592b30d9ef252acf80407c5543157ea7b07c4d9a0", size = 554552101, upload-time = "2026-09-02T13:43:43.953Z" }, + { url = "https://files.pythonhosted.org/packages/19/df/ebf2d5d82ee3d89c5e066239b097e779c42696f0609bfa8e30628c1ba53f/torch-2.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:d70c2b41bde81efde59fcefb147b9247fd062497e778677661b7de1bee5c8c99", size = 124096098, upload-time = "2026-09-02T13:42:53.127Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2c/0c9757a1ea1fff8ccc4faff1d1c87e98a69542b5d4b606e50f7518f72e58/torch-2.14.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ae530ddd3f3b94248b77f1fd3313c4f3405bd0d17283d505b81574816767133a", size = 127277244, upload-time = "2026-09-02T13:42:57.855Z" }, + { url = "https://files.pythonhosted.org/packages/ed/30/eb2f6cf77cb8f2d5e7ce4343882a856783521f51b557673d78d1d7bb8661/torch-2.14.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:4dbea8a10d80b10dff2be1ed467d2617b704fe6bf76bbfad4194774c8f6d886b", size = 453993896, upload-time = "2026-09-02T13:43:29.231Z" }, + { url = "https://files.pythonhosted.org/packages/8d/be/c0c8a845bf00552960d2f1f6a00b6ac9750fd312f55429c3e8c6c0e3aac4/torch-2.14.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8d9e232b6376c62f3090237889fb1cb6887c0ece9f73e6c1052e80fc1481f999", size = 554583496, upload-time = "2026-09-02T13:44:10.199Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5b/ed9d177f6a133389d5fa89e298f3e2a879b62952aa928ac6ddd04467f225/torch-2.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:6981872df75eb5409c439050d36b67cb57d88b4e11080516e2aa7410f6730705", size = 124096356, upload-time = "2026-09-02T13:43:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/17/76/bb4770f56cf6d8971671dbcbb7493e5a6a15ad2825f4e359b02c27c38297/torch-2.14.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:c1f844f1c750e87df4b68bc3afbc0e2b0c7ef19d7b8f666e48bdcf6a0c4f0056", size = 127303200, upload-time = "2026-09-02T13:43:20.311Z" }, + { url = "https://files.pythonhosted.org/packages/fe/7b/ec44bacf2c8886b85ba4ca2285e8b09f2dff5d9c99e6a031326954082cb5/torch-2.14.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:ada340e62591d06a2bcc2d68170f20f45f0b0665d372dc510a8ed7eb3b1d609a", size = 454010251, upload-time = "2026-09-02T13:44:24.927Z" }, + { url = "https://files.pythonhosted.org/packages/15/71/49399acd41f750a906c686bd23c08a2001ccb8dd25f2971003c2ed89c1dd/torch-2.14.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:fecffb58f51fd643d213acd68da21cc3fc19bea05a3bc64b4ee55128f47a4963", size = 554620488, upload-time = "2026-09-02T13:44:49.442Z" }, + { url = "https://files.pythonhosted.org/packages/be/16/9489b137112040f9911d7527e452854f21cc4e499ce0da79864e6a7451a7/torch-2.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:cad84f41bbdf3dcf333ce394aeeaf25237c4d94fd6b659ba5eb813c829978823", size = 124114011, upload-time = "2026-09-02T13:43:55.034Z" }, + { url = "https://files.pythonhosted.org/packages/ca/40/0db773452c2a62b37761d3f418acf933d381f9e87077036fb57c2a386c37/torch-2.14.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:9d4b1022a5d9b71282ec67ad0d9e7235870096b8a246dc1c32d6ea1fc83dc998", size = 127311393, upload-time = "2026-09-02T13:43:59.607Z" }, + { url = "https://files.pythonhosted.org/packages/13/36/537fd9da2adad49e7b2bb20741398625bee548493158274e87369a8eed56/torch-2.14.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:731b9ebdea402b8b1996d4c2ae613b16660e559b19e47bdc45d970568bc91c53", size = 454010525, upload-time = "2026-09-02T13:45:03.719Z" }, + { url = "https://files.pythonhosted.org/packages/21/f1/39bd13b21f57d1982b7f3ddf663f01c7266e2957714880744eba9e8c8d11/torch-2.14.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:84bf384779a10c02fc3c6bdbab71a9cb66b0dd93c652d1ed5d6dfc0cb37e5962", size = 554619993, upload-time = "2026-09-02T13:45:28.209Z" }, + { url = "https://files.pythonhosted.org/packages/89/a8/683d9c44737554b67ca76dd2db4f42258a0f014246cb511293e51e0154bd/torch-2.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0e7cf18cb0d8bd666b6120932e29c7aef3502b61a08b44da4839580c539a7cdb", size = 124113865, upload-time = "2026-09-02T13:44:33.705Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/1241e7db5ccc2455f8735bd6b1becfad39916206ad18001c4c0014d139e2/torch-2.14.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:860423e970f2ce02c4476e8e2d1350131b1c5c5a5e4912180e78b50b53241efa", size = 127321431, upload-time = "2026-09-02T13:44:38.236Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/bbeba65e3fbb4b8f61ea19cf4ebea9f4268cc6fe64e6f7d38bfb6cc152eb/torch-2.14.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:b985c7defeb8d28691b7513ebaecc64946c5f68ec770e9f4ddba39688864f46a", size = 454027631, upload-time = "2026-09-02T13:45:43.73Z" }, + { url = "https://files.pythonhosted.org/packages/50/75/8d2b9a7e724759470c209489b79260cac537f091cc9aac8001c8d2bc845c/torch-2.14.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:b2cd92bce63d40bf6fc2e5d840fc2f0063bd180a242daa761cb6088cc2f46e27", size = 554623549, upload-time = "2026-09-02T13:46:04.252Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1e/a5475c00b0555e686333e6b4036f2213e7cbea021a772ce6f9ced4dcbd2f/torch-2.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:44b044b9f6f633d982839422a57433d6a1da520037fd88e0c8a47efde589b3b8", size = 124110863, upload-time = "2026-09-02T13:45:12.764Z" }, + { url = "https://files.pythonhosted.org/packages/a5/49/bbaee76337742a42d2c5b0296ea62252567050e1d821bc2283b2e72ba6fb/torch-2.14.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:553aec938d37d77b783bcf801e638cad068870e7643c47eadac21eb180f551ed", size = 127653463, upload-time = "2026-09-02T13:45:17.1Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/6cc7a511bab384fbe8f4b8ddeecdd22e724b2162d6f15076f07a0a7ef15b/torch-2.14.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:cd8cd8f714d511ccdca907282d1da3d9be8322d4e1520b9c3bce39a5c1318a4b", size = 454009927, upload-time = "2026-09-02T13:46:19.637Z" }, + { url = "https://files.pythonhosted.org/packages/b6/da/0f04fe15fd05bf3f613a359b14acb44c35ae06fe69da0aefa8b5cdb4f9f9/torch-2.14.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:d2526f71e6638133b97b3cd2881ece3df521460a4727030e8ae2a72a7d3ae31d", size = 554580530, upload-time = "2026-09-02T13:46:34.818Z" }, + { url = "https://files.pythonhosted.org/packages/f8/c3/72ae1f02747b1f012e1975743e48cd608f83095d7f9ce58de78b79248b35/torch-2.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:731784e3914843c6bcc7aba3987ff7610ac57dbbc816a5d6b9b62e04c240a641", size = 124400194, upload-time = "2026-09-02T13:45:53.555Z" }, +] + +[[package]] +name = "torchvision" +version = "0.29.0" +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.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*' and sys_platform != 'win32'" }, + { name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and sys_platform != 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, + { name = "pillow", marker = "python_full_version < '3.11' or python_full_version == '3.12.*' or sys_platform != 'win32'" }, + { name = "torch", marker = "python_full_version < '3.11' or python_full_version == '3.12.*' or sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/65/cc2c08a1b4baa5787e76e887b473f18403c2d943bdd7d1ffa60f41bfae3f/torchvision-0.29.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:ce3b58631f0b7c8828e8675301bff5cb0623aaed704e3ac91079e89607753205", size = 1813728, upload-time = "2026-09-02T13:46:52.886Z" }, + { url = "https://files.pythonhosted.org/packages/5a/7f/9fb0d64fdb490086e7158d225632f298727d55b8a9552d4d3b0d0a7cba2a/torchvision-0.29.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:add88479361b2bf08790338fce4ba11a11fdc1ae508e80a3a013daf1c0d2893d", size = 7586007, upload-time = "2026-09-02T13:46:54.171Z" }, + { url = "https://files.pythonhosted.org/packages/08/4c/5ba83223106dc369e1e71648c8d719da1070d652cfd276254ff11d9251f7/torchvision-0.29.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:710df2c4f03ab6da161520c10d9467f4c0201ca314fd1432499a48c9d7a5818f", size = 7430724, upload-time = "2026-09-02T13:46:55.719Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ee/dc1b327b0e48075a355e86577f055f64d90dc07e8ae6029bafb9c47be18c/torchvision-0.29.0-cp310-cp310-win_amd64.whl", hash = "sha256:784c8b8de9e81e02dea093373bc35040791378df2daec0017e657985183bf862", size = 1376786, upload-time = "2026-09-02T13:46:56.981Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/6b32740f7ae0ecf8772297f7e61e842b127b11de5c2c07efd9aeeaed0690/torchvision-0.29.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:bbe15455b59a6c9d822584fd3faaaac50ee972101d7bb7e2887da456e148a3ea", size = 1813735, upload-time = "2026-09-02T13:46:58.108Z" }, + { url = "https://files.pythonhosted.org/packages/59/0a/2c5114537cbf4ac374607a41d976911657db9f656bfa42a278470ca8d886/torchvision-0.29.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:dc5717feef9a0b430b052895db71f1d695939575b74344da981120e9f4da7620", size = 7585959, upload-time = "2026-09-02T13:46:59.536Z" }, + { url = "https://files.pythonhosted.org/packages/a6/2d/a656bfa09b98d01d4baf30114a5befca1de2f7a16133d94aae01d7cd6942/torchvision-0.29.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:85fa54bec1f7d9227b5e4e201ed4bac92247ee10fb19148d92f9836e3e91d5c3", size = 7430717, upload-time = "2026-09-02T13:47:01.281Z" }, + { url = "https://files.pythonhosted.org/packages/78/26/dc21cec3eace48d944258a437e293093c69ddb56684e9dd1b5016f7e2154/torchvision-0.29.0-cp311-cp311-win_amd64.whl", hash = "sha256:a292ca7044236a2702ec3d4664de135e01fc9fb3ae9422db3fecc819546e831c", size = 1376792, upload-time = "2026-09-02T13:47:02.645Z" }, + { url = "https://files.pythonhosted.org/packages/a7/d2/9daad500db2ab880eca0f0a569afd73e1afdd49f22c94560c85546dd87c2/torchvision-0.29.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:994687b818cac0e6d34cb407a41458e2c3262cb7db927465841c9522ba7ebb92", size = 1813740, upload-time = "2026-09-02T13:47:03.933Z" }, + { url = "https://files.pythonhosted.org/packages/41/14/702cd035fab1b8dfe944d06827cc4628c3b18e5d7ddaa676bf72c9be1c2c/torchvision-0.29.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:b7a736eaa6b2e22476c95ceb62986d195f3e600cdd7d196d7329aa2dfc951994", size = 7586112, upload-time = "2026-09-02T13:47:05.708Z" }, + { url = "https://files.pythonhosted.org/packages/27/d7/3cf1992414fd712af4865ae5f8f5758d9003c1ed82624908d54f7afb4342/torchvision-0.29.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:0e631a2f8b24732672d35224d2574ff89a78d56bef5364ef6094a625fa5f1771", size = 7430773, upload-time = "2026-09-02T13:47:07.178Z" }, + { url = "https://files.pythonhosted.org/packages/ef/a8/59152b945c09840f582c40e38aa4128777625af7f2883ce2d77133f96a46/torchvision-0.29.0-cp312-cp312-win_amd64.whl", hash = "sha256:0e3b99c62f7f095153887330c2c60c9ebc2da84dfbe08eea564a41ec361629cf", size = 1376791, upload-time = "2026-09-02T13:47:11.283Z" }, + { url = "https://files.pythonhosted.org/packages/81/56/4e122b6b59269cb2e6a8f7ef969cff7b1eca1499c48784805ad156d3720b/torchvision-0.29.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:28a3964a9e6db34354d4ca440d2c2038deeef24e7219e8d6f49d9d174f44830c", size = 1813739, upload-time = "2026-09-02T13:47:08.63Z" }, + { url = "https://files.pythonhosted.org/packages/d7/da/1418b42ed642521262d2b276ffb698772f06827202dca9065a66824f69bf/torchvision-0.29.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:83db0299170502b038640444214c99d7b54bad72db447f0554deb8d2c4f02f94", size = 7523336, upload-time = "2026-09-02T13:47:09.769Z" }, + { url = "https://files.pythonhosted.org/packages/34/78/afa8f85e1f3cb276202ad3af9e36765feee1f0c5ec2942d8522b321e5afc/torchvision-0.29.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:942a3b3fb4e981e1abb7846679ee3882dfc733533a30b68df0dadf6c06f238a9", size = 7430683, upload-time = "2026-09-02T13:47:15.141Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1e/72fa361d60e46b4f003ebdc68508467f46d4ef1011650a80258b40d7cf46/torchvision-0.29.0-cp313-cp313-win_amd64.whl", hash = "sha256:ebf54f6744556ebd8d7b5f9b6515e2060e5d3156723970dc257bf093f63b2170", size = 1376791, upload-time = "2026-09-02T13:47:12.471Z" }, + { url = "https://files.pythonhosted.org/packages/af/6a/ccdf22f6ee57e862aed41ab70e735f6302b8330bf304d8125f60d84413ba/torchvision-0.29.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:01a029fa1b2eacac27e1e4f9e0dba24d112bcbd523261b294e4b4a1c7f8330bc", size = 1813734, upload-time = "2026-09-02T13:47:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/3a/7e/3cffb013454af4c126a40e97cff7ac91dff692b749bcf80754ae97ada225/torchvision-0.29.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:f9e9627d5036cac8e6de76aeb2f1acc67841ff5ff4b63c4cf78ff954efeb073e", size = 7523354, upload-time = "2026-09-02T13:47:13.833Z" }, + { url = "https://files.pythonhosted.org/packages/97/90/ae1d76e76c5d97b70539cd38df309777d66020d33b346301e79a7a8c132a/torchvision-0.29.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:e8fdf234d76dca6fc47bc5f4af86ecf1ef57fab6285a7b21681bdcbb494e4c1b", size = 7430722, upload-time = "2026-09-02T13:47:21.359Z" }, + { url = "https://files.pythonhosted.org/packages/ac/dd/f5571d0c2aedc3d363f9b818de8fbb810a0d2b2ab0293de6003f4291ce15/torchvision-0.29.0-cp314-cp314-win_amd64.whl", hash = "sha256:bb74d08d65785b51c61d4d4ec167bfa50a5ea644b57555a33ba2f4435b0d05dd", size = 1376792, upload-time = "2026-09-02T13:47:18.279Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f0/bc564a8ed409db4d7ce72215a676c3cbda9559779a53e8ecc477ad11d2c5/torchvision-0.29.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:80789f9b50f5277302e7020c8128344e0269b12f6e3b5855034e56bb7081b874", size = 1813737, upload-time = "2026-09-02T13:47:22.801Z" }, + { url = "https://files.pythonhosted.org/packages/80/55/b39dba8e3d574428e7f00f212a3ee5a2c2d0ab5a25a95338a7c724a87850/torchvision-0.29.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:9b99a7385da8d706f2dc14cacffb8cbf653d48fc37013a38c41a36659acc3731", size = 7523376, upload-time = "2026-09-02T13:47:19.631Z" }, + { url = "https://files.pythonhosted.org/packages/84/b3/67692c3785702c24d6c9c95026a558791366f2dc48823aec2321f23a0858/torchvision-0.29.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:2428a13706a354ee3901fd63e3f1ef8c2a59f6e20d24cce13210b4e61be08f7a", size = 7430679, upload-time = "2026-09-02T13:47:24.3Z" }, + { url = "https://files.pythonhosted.org/packages/07/8e/0e3682cad7f6b2aba93585bc3b792f8087e4e3fb916c0f6638eacdb3170c/torchvision-0.29.0-cp314-cp314t-win_amd64.whl", hash = "sha256:bd12e152640d1024fca15deaf6f39c53702e12eb398db4b2b038a5a97ab85f7f", size = 1376794, upload-time = "2026-09-02T13:47:25.923Z" }, +] + +[[package]] +name = "tqdm" +version = "4.70.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/3b/6c24bec5be5e743ffd99576daa5cc077722fc7d5bbc00bd133fa0c698dc6/tqdm-4.70.0.tar.gz", hash = "sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220", size = 795438, upload-time = "2026-07-27T11:33:15.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/1c/01bfd571a64e7f270e6bab5e33777debe0edc56759233ce84f27dec92d14/tqdm-4.70.0-py3-none-any.whl", hash = "sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953", size = 80184, upload-time = "2026-07-27T11:33:13.167Z" }, +] + +[[package]] +name = "transformers" +version = "5.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub", version = "1.16.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and sys_platform != 'win32'" }, + { name = "huggingface-hub", version = "1.31.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform != 'win32') or (python_full_version < '3.11' and sys_platform == 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*' and sys_platform != 'win32'" }, + { name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and sys_platform != 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, + { name = "packaging", marker = "python_full_version < '3.11' or python_full_version == '3.12.*' or sys_platform != 'win32'" }, + { name = "pyyaml", marker = "python_full_version < '3.11' or python_full_version == '3.12.*' or sys_platform != 'win32'" }, + { name = "regex", marker = "python_full_version < '3.11' or python_full_version == '3.12.*' or sys_platform != 'win32'" }, + { name = "safetensors", marker = "python_full_version < '3.11' or python_full_version == '3.12.*' or sys_platform != 'win32'" }, + { name = "tokenizers", marker = "python_full_version < '3.11' or python_full_version == '3.12.*' or sys_platform != 'win32'" }, + { name = "tqdm", marker = "python_full_version < '3.11' or python_full_version == '3.12.*' or sys_platform != 'win32'" }, + { name = "typer", marker = "python_full_version < '3.11' or python_full_version == '3.12.*' or sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/9e/750649904a065007a838981785b2bd8d9ff26154c6c341ac67d0b7f82c68/transformers-5.17.0.tar.gz", hash = "sha256:a153be279169b55b92d8000bf4af294aed684503d091cca7804da2dd8a9de000", size = 9817878, upload-time = "2026-09-09T15:39:56.886Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/d0/c502b60d684adbd98a8dc7d5bb866842772b816ac4354e4608be240041ae/transformers-5.17.0-py3-none-any.whl", hash = "sha256:78ec1ce21579b38dfb83950a0658cd119f87212a2fcfdff478096ce9d6c03801", size = 12295140, upload-time = "2026-09-09T15:39:53.746Z" }, +] + +[[package]] +name = "triton" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/ae/6b01bdd0dc082e190f5b04aa70fa955f968ac3fc70fd71dde71aeb835a8f/triton-3.8.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:398f4b009c7ab08ed9aeb1d1282dee822945b53f399a5e12a4fecb643cf2007d", size = 226352592, upload-time = "2026-08-28T16:07:49.055Z" }, + { url = "https://files.pythonhosted.org/packages/65/65/18d0871f353eb170b0b9c562cd65f6ab9619b6e7b8aaecea9709e9ad16a6/triton-3.8.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6d914c52f89dc942b1819db959e7c07a5999f678ec760f677625c706b7e0d743", size = 247838081, upload-time = "2026-08-28T15:55:34.877Z" }, + { url = "https://files.pythonhosted.org/packages/a3/cf/d21c9b1a4d1df9ba3aed218069f24f762a4b9565c5a362bef6f110c08e72/triton-3.8.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:372285307d4c44ee74cee32de0b4f04bd157e071427e38c6f4ee3e3beb2194f4", size = 226467015, upload-time = "2026-08-28T16:08:05.871Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/2c3d3823726d5ad5359f7065b02b984925158b74611c1e3987f6a37461fb/triton-3.8.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:68988ac85d5e7086baeda0ddc175af9667db7529b3c5e11a5c0601b8bef2200a", size = 247945226, upload-time = "2026-08-28T15:55:43.795Z" }, + { url = "https://files.pythonhosted.org/packages/87/07/0f8cd8e8db0472334253efdaaab3d0819fea27aa99bf0e7f1aeea4ceb5ae/triton-3.8.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a9c404c69ed4a39e8ec632eaf6b9fe058a060bf98979c177f6ef666f06bb8d50", size = 226474486, upload-time = "2026-08-28T16:08:18.29Z" }, + { url = "https://files.pythonhosted.org/packages/c1/09/b7012e5bfae67640f268aa584caa80fe1674f6b0da949046b679972c33e3/triton-3.8.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e91ffa46d095b252248297292dd22bcbacd53a125a0c2eefbbbf74925a320bc3", size = 247972921, upload-time = "2026-08-28T15:55:53.157Z" }, + { url = "https://files.pythonhosted.org/packages/87/4d/4c564374bcdadb166fccbf3e45aee0d4a473f88d341761bd2fefe3b8e8c1/triton-3.8.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7004666652f500ed854a86988e4b3d69d247188b5d2092b5df1e44f4a954099", size = 226476793, upload-time = "2026-08-28T16:08:30.956Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b6/3394d5548404c1cabd1dadadd28d0b3f9478db1dff8180da53bb3f0a1e19/triton-3.8.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f0497218e26b7d79773ad9c2a3fa3b539ee69f587a13fac2e552b1d322a8015", size = 247975122, upload-time = "2026-08-28T15:56:04.112Z" }, + { url = "https://files.pythonhosted.org/packages/b8/59/bf0e9493118bb353ab59a5d6a65db3618d9b314417cc1459f0121e0ec5c9/triton-3.8.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f6b48d0591929a3867973acac3dccd4e058585f91bfb41022de496c9ffab304", size = 226488654, upload-time = "2026-08-28T16:08:47.141Z" }, + { url = "https://files.pythonhosted.org/packages/93/d9/08c75f3459f19ad00425b564058e40efa4bcd79b816064cf27499303ea42/triton-3.8.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:387dae4cb0089a7b6ba1a428ae0782b65c4c58f57d94617cb22ca8593d8ccbca", size = 247972313, upload-time = "2026-08-28T15:56:14.007Z" }, + { url = "https://files.pythonhosted.org/packages/7c/34/429c5592181cfb7361a0a8e0bff218e7224b726709d75da2472b3e819f70/triton-3.8.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b84e7d512490ba529111260fa6f7cad8b254a6bb5fbdf41d5ef9a5e57f52d0a", size = 226591133, upload-time = "2026-08-28T16:09:02.271Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d1/aa8a3e935c37efee7945984fdb64d7e0851bf6d920afd97b2d21f9d23360/triton-3.8.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74217bb56ed8692759227758e4c4b3bd2d608a209c1a7a081bf361fb4c2c1bf9", size = 248077577, upload-time = "2026-08-28T15:56:24.94Z" }, +] + +[[package]] +name = "typer" +version = "0.27.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/f7/57713ba479fd405eb76de31404b2c744c289e336b2d999511ebf51e496f7/typer-0.27.2.tar.gz", hash = "sha256:269b7eb9d3c202ca84b4bc9618cb04ebb43d3d4d1e567e4c768607232c05f945", size = 204045, upload-time = "2026-08-28T10:26:55.046Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/bf/205d0004930ede8f542fb58f601526fccf4ae7626075ca1e6c4de5d3d652/typer-0.27.2-py3-none-any.whl", hash = "sha256:b3a5fc4342d5fc8fda8fc3010b1cf117e9249aab7fae800c2eff62fd3842d97d", size = 123130, upload-time = "2026-08-28T10:26:53.752Z" }, +] + [[package]] name = "types-grpcio" version = "1.0.0.20251009" @@ -3162,6 +7441,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, ] +[[package]] +name = "typing-inspect" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +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" @@ -3174,6 +7466,276 @@ 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 = "2026.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415", size = 198674, upload-time = "2026-07-10T08:50:37.887Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168, upload-time = "2026-07-10T08:50:36.46Z" }, +] + +[[package]] +name = "unstructured" +version = "0.18.32" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'win32'", + "python_full_version < '3.11' and sys_platform != 'win32'", +] +dependencies = [ + { name = "backoff", marker = "python_full_version < '3.11'" }, + { name = "beautifulsoup4", marker = "python_full_version < '3.11'" }, + { name = "charset-normalizer", marker = "python_full_version < '3.11'" }, + { name = "dataclasses-json", marker = "python_full_version < '3.11'" }, + { name = "emoji", marker = "python_full_version < '3.11'" }, + { name = "filetype", marker = "python_full_version < '3.11'" }, + { name = "html5lib", marker = "python_full_version < '3.11'" }, + { name = "langdetect", marker = "python_full_version < '3.11'" }, + { name = "lxml", marker = "python_full_version < '3.11'" }, + { name = "nltk", marker = "python_full_version < '3.11'" }, + { name = "numba", marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "psutil", marker = "python_full_version < '3.11'" }, + { name = "python-iso639", marker = "python_full_version < '3.11'" }, + { name = "python-magic", marker = "python_full_version < '3.11'" }, + { name = "python-oxmsg", marker = "python_full_version < '3.11'" }, + { name = "rapidfuzz", version = "3.14.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "requests", marker = "python_full_version < '3.11'" }, + { name = "tqdm", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "unstructured-client", version = "0.42.12", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "wrapt", version = "1.17.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/65/b73d84ede08fc2defe9c59d85ebf91f78210a424986586c6e39784890c8e/unstructured-0.18.32.tar.gz", hash = "sha256:40a7cf4a4a7590350bedb8a447e37029d6e74b924692576627b4edb92d70e39d", size = 1707730, upload-time = "2026-02-10T22:28:22.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/e7/35298355bdb917293dc3e179304e737ce3fe14247fb5edf09fddddc98409/unstructured-0.18.32-py3-none-any.whl", hash = "sha256:c832ecdf467f5a869cc5e91428459e4b9ed75a16156ce3fab8f41ff64d840bc7", size = 1794965, upload-time = "2026-02-10T22:28:20.301Z" }, +] + +[package.optional-dependencies] +docx = [ + { name = "python-docx", marker = "python_full_version < '3.11'" }, +] +md = [ + { name = "markdown", marker = "python_full_version < '3.11'" }, +] +pdf = [ + { name = "effdet", marker = "python_full_version < '3.11'" }, + { name = "google-cloud-vision", marker = "python_full_version < '3.11'" }, + { name = "onnx", marker = "python_full_version < '3.11'" }, + { name = "onnxruntime", version = "1.23.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "pdf2image", marker = "python_full_version < '3.11'" }, + { name = "pdfminer-six", marker = "python_full_version < '3.11'" }, + { name = "pi-heif", marker = "python_full_version < '3.11'" }, + { name = "pikepdf", marker = "python_full_version < '3.11'" }, + { name = "pypdf", marker = "python_full_version < '3.11'" }, + { name = "unstructured-inference", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "unstructured-pytesseract", marker = "python_full_version < '3.11'" }, +] + +[[package]] +name = "unstructured" +version = "0.27.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'win32'", +] +dependencies = [ + { name = "beautifulsoup4", marker = "python_full_version >= '3.11'" }, + { name = "charset-normalizer", marker = "python_full_version >= '3.11'" }, + { name = "emoji", marker = "python_full_version >= '3.11'" }, + { name = "filelock", marker = "python_full_version >= '3.11'" }, + { name = "filetype", marker = "python_full_version >= '3.11'" }, + { name = "html5lib", marker = "python_full_version >= '3.11'" }, + { name = "installer", marker = "python_full_version >= '3.11'" }, + { name = "langdetect", marker = "python_full_version >= '3.11'" }, + { name = "lxml", marker = "python_full_version >= '3.11'" }, + { name = "nh3", marker = "python_full_version >= '3.11'" }, + { name = "numba", marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "psutil", marker = "python_full_version >= '3.11'" }, + { name = "python-iso639", marker = "python_full_version >= '3.11'" }, + { name = "python-magic", marker = "python_full_version >= '3.11'" }, + { name = "python-oxmsg", marker = "python_full_version >= '3.11'" }, + { name = "rapidfuzz", version = "3.14.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "regex", marker = "python_full_version >= '3.11'" }, + { name = "requests", marker = "python_full_version >= '3.11'" }, + { name = "spacy", marker = "python_full_version >= '3.11'" }, + { name = "tqdm", marker = "python_full_version >= '3.11'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.11'" }, + { name = "unstructured-client", version = "0.46.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "wrapt", version = "2.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/54/89/40ba552526ae54b80bc8daeea048b8cd7e409fec5795f6e70ce3207eb430/unstructured-0.27.5.tar.gz", hash = "sha256:68c72b9ea17fb34e0037126d3eca23f76290b9e5171b221d6d078dcff2ffd77d", size = 1560203, upload-time = "2026-08-28T21:18:29.131Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/56/3eb070e467178d763a27587f40a3a8fa8277ef7b087d13b286a5ec25d711/unstructured-0.27.5-py3-none-any.whl", hash = "sha256:4624145cb102296425ed0c4917711f8068746680f73718ac819b54568a5d293e", size = 1620083, upload-time = "2026-08-28T21:18:26.983Z" }, +] + +[package.optional-dependencies] +docx = [ + { name = "python-docx", marker = "python_full_version >= '3.11'" }, +] +md = [ + { name = "markdown", marker = "python_full_version >= '3.11'" }, +] +pdf = [ + { name = "google-cloud-vision", marker = "python_full_version >= '3.11'" }, + { name = "pdf2image", marker = "python_full_version >= '3.11'" }, + { name = "pdfminer-six", marker = "python_full_version >= '3.11'" }, + { name = "pi-heif", marker = "python_full_version >= '3.11'" }, + { name = "pikepdf", marker = "python_full_version >= '3.11'" }, + { name = "pypdf", marker = "python_full_version >= '3.11'" }, + { name = "unstructured-inference", version = "1.6.13", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform != 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, + { name = "unstructured-pytesseract", marker = "python_full_version >= '3.11'" }, +] + +[[package]] +name = "unstructured-client" +version = "0.42.12" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'win32'", + "python_full_version < '3.11' and sys_platform != 'win32'", +] +dependencies = [ + { name = "aiofiles", marker = "python_full_version < '3.11'" }, + { name = "cryptography", marker = "python_full_version < '3.11'" }, + { name = "httpcore", marker = "python_full_version < '3.11'" }, + { name = "httpx", marker = "python_full_version < '3.11'" }, + { name = "pydantic", marker = "python_full_version < '3.11'" }, + { name = "pypdf", marker = "python_full_version < '3.11'" }, + { name = "pypdfium2", marker = "python_full_version < '3.11'" }, + { name = "requests-toolbelt", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/ca/73904d53e486af2f1d9d8baaf43d2a74b3d67e5f533834f5d51056471339/unstructured_client-0.42.12.tar.gz", hash = "sha256:50eb6717d8c6513b14b309fce8d6551354e433da982b7a9161a889d8e6a11166", size = 94714, upload-time = "2026-03-25T20:24:21.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/80/fbf02ec3c566a3e383a5649385096834a2a981832f1432c3a8797b29185a/unstructured_client-0.42.12-py3-none-any.whl", hash = "sha256:fe6f217066a0c308ba7213185524506dbfc3bb9d35df0ab79549291e9728a012", size = 220154, upload-time = "2026-03-25T20:24:20.288Z" }, +] + +[[package]] +name = "unstructured-client" +version = "0.46.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'win32'", +] +dependencies = [ + { name = "aiofiles", marker = "python_full_version >= '3.11'" }, + { name = "httpcore", marker = "python_full_version >= '3.11'" }, + { name = "httpx", marker = "python_full_version >= '3.11'" }, + { name = "pydantic", marker = "python_full_version >= '3.11'" }, + { name = "pypdf", marker = "python_full_version >= '3.11'" }, + { name = "pypdfium2", marker = "python_full_version >= '3.11'" }, + { name = "requests-toolbelt", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/02/170f8186ff068ca468fe5927c1042936e9484100d1bd1bc39728dcad3099/unstructured_client-0.46.2.tar.gz", hash = "sha256:f7de567c7c677475309142c719bc3e9824197cf2a5c44b8456aed67f77919136", size = 104385, upload-time = "2026-08-24T15:43:13.912Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/00/80f36aac1c853edc63a3baea8ba1016a4cd4cb49a3fb25d071bdc00e8c67/unstructured_client-0.46.2-py3-none-any.whl", hash = "sha256:42fe6d56631fcfb4e942db2c9304002aa39d7169fe4fc657760f013afb32e720", size = 169646, upload-time = "2026-08-24T15:43:12.413Z" }, +] + +[[package]] +name = "unstructured-inference" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'win32'", + "python_full_version < '3.11' and sys_platform != 'win32'", +] +dependencies = [ + { name = "accelerate", marker = "python_full_version < '3.11'" }, + { name = "huggingface-hub", version = "1.16.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and sys_platform != 'win32'" }, + { name = "huggingface-hub", version = "1.31.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, + { name = "matplotlib", version = "3.10.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "onnx", marker = "python_full_version < '3.11'" }, + { name = "onnxruntime", version = "1.23.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "opencv-python", marker = "python_full_version < '3.11'" }, + { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "pdfminer-six", marker = "python_full_version < '3.11'" }, + { name = "pypdfium2", marker = "python_full_version < '3.11'" }, + { name = "python-multipart", marker = "python_full_version < '3.11'" }, + { name = "rapidfuzz", version = "3.14.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "timm", marker = "python_full_version < '3.11'" }, + { name = "torch", marker = "python_full_version < '3.11'" }, + { name = "transformers", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/10/8f3bccfa9f1e0101a402ae1f529e07876541c6b18004747f0e793ed41f9e/unstructured_inference-1.2.0.tar.gz", hash = "sha256:19ca28512f3649c70a759cf2a4e98663e942a1b83c1acdb9506b0445f4862f23", size = 45732, upload-time = "2026-01-30T20:57:58.019Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/3b/349cd091b590a6f1dbfebcb5fee0ea7b0b6ef6520df58794c9582567a24f/unstructured_inference-1.2.0-py3-none-any.whl", hash = "sha256:60a1635aa8e97a9e7daed1a129836f51c26588e0d2062c9cc6a5a17e6d40cb6a", size = 49443, upload-time = "2026-01-30T20:57:56.617Z" }, +] + +[[package]] +name = "unstructured-inference" +version = "1.6.13" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'win32'", +] +dependencies = [ + { name = "accelerate", marker = "(python_full_version >= '3.11' and sys_platform != 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, + { name = "huggingface-hub", version = "1.31.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform != 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, + { name = "matplotlib", version = "3.11.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform != 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*' and sys_platform != 'win32'" }, + { name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and sys_platform != 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, + { name = "onnx", marker = "(python_full_version >= '3.11' and sys_platform != 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, + { name = "onnxruntime", version = "1.30.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform != 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, + { name = "opencv-python", marker = "(python_full_version >= '3.11' and sys_platform != 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, + { name = "pandas", version = "3.0.5", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform != 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, + { name = "pdfminer-six", marker = "(python_full_version >= '3.11' and sys_platform != 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, + { name = "pypdfium2", marker = "(python_full_version >= '3.11' and sys_platform != 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, + { name = "rapidfuzz", version = "3.14.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform != 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*' and sys_platform != 'win32'" }, + { name = "scipy", version = "1.18.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and sys_platform != 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, + { name = "timm", marker = "(python_full_version >= '3.11' and sys_platform != 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, + { name = "torch", marker = "(python_full_version >= '3.11' and sys_platform != 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, + { name = "transformers", marker = "(python_full_version >= '3.11' and sys_platform != 'win32') or (python_full_version == '3.12.*' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/ce/1e58008b8416884edfa6d18d65b635cde892189512d2e7531f91553b34d0/unstructured_inference-1.6.13.tar.gz", hash = "sha256:4efa2f3f517370aa09166c669cdc1addee71531e95bfbec7e6e3be66be90c147", size = 50137, upload-time = "2026-06-11T22:27:33.146Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/c0/6598b1420605382e7ed879ff7e1d51e12cc7a82e46971c7a61c3740c77db/unstructured_inference-1.6.13-py3-none-any.whl", hash = "sha256:73b727260e4a43133276727c371b57f7d4d92c68e8a7529a28e9576755356524", size = 57451, upload-time = "2026-06-11T22:27:31.91Z" }, +] + +[[package]] +name = "unstructured-pytesseract" +version = "0.3.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/b1/4b3a976b76549f22c3f5493a622603617cbe08804402978e1dac9c387997/unstructured.pytesseract-0.3.15.tar.gz", hash = "sha256:4b81bc76cfff4e2ef37b04863f0e48bd66184c0b39c3b2b4e017483bca1a7394", size = 15703, upload-time = "2025-03-05T00:59:17.516Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/6d/adb955ecf60811a3735d508974bbb5358e7745b635dc001329267529c6f2/unstructured.pytesseract-0.3.15-py3-none-any.whl", hash = "sha256:a3f505c5efb7ff9f10379051a7dd6aa624b3be6b0f023ed6767cc80d0b1613d1", size = 14992, upload-time = "2025-03-05T00:59:15.962Z" }, +] + [[package]] name = "urllib3" version = "2.7.0" @@ -3217,7 +7779,8 @@ name = "uvicorn" version = "0.52.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click" }, + { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform != 'win32') or (python_full_version == '3.11.*' and sys_platform == 'win32') or (python_full_version == '3.14.*' and sys_platform == 'win32')" }, + { name = "click", version = "8.5.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32') or (python_full_version >= '3.11' and sys_platform != 'win32') or (python_full_version < '3.11' and sys_platform == 'win32') or (python_full_version >= '3.15' and sys_platform == 'win32')" }, { name = "h11" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] @@ -3226,6 +7789,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/d5/68e6e9bca63c0badf67002890a46d3784c958de45b65e1275ec583ca1f06/uvicorn-0.52.1-py3-none-any.whl", hash = "sha256:e4403f9d93188cf9d1088e9f40e3acd12630e2df8675316704379a7fc20fff6a", size = 79859, upload-time = "2026-08-01T18:19:29.294Z" }, ] +[[package]] +name = "wasabi" +version = "1.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/f9/054e6e2f1071e963b5e746b48d1e3727470b2a490834d18ad92364929db3/wasabi-1.1.3.tar.gz", hash = "sha256:4bb3008f003809db0c3e28b4daf20906ea871a2bb43f9914197d540f4f2e0878", size = 30391, upload-time = "2024-05-31T16:56:18.99Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/7c/34330a89da55610daa5f245ddce5aab81244321101614751e7537f125133/wasabi-1.1.3-py3-none-any.whl", hash = "sha256:f76e16e8f7e79f8c4c8be49b4024ac725713ab10cd7f19350ad18a8e3f71728c", size = 27880, upload-time = "2024-05-31T16:56:16.699Z" }, +] + [[package]] name = "watchdog" version = "6.0.0" @@ -3258,6 +7833,35 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, ] +[[package]] +name = "weasel" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cloudpathlib", marker = "python_full_version >= '3.11'" }, + { name = "confection", marker = "python_full_version >= '3.11'" }, + { name = "httpx", marker = "python_full_version >= '3.11'" }, + { name = "packaging", marker = "python_full_version >= '3.11'" }, + { name = "pydantic", marker = "python_full_version >= '3.11'" }, + { name = "smart-open", marker = "python_full_version >= '3.11'" }, + { name = "srsly", marker = "python_full_version >= '3.11'" }, + { name = "typer", marker = "python_full_version >= '3.11'" }, + { name = "wasabi", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/e5/e272bb9a045105a1fdf4b798d8086f5932a178f4d738f17a74f5c9e0ae9a/weasel-1.0.0.tar.gz", hash = "sha256:7b129b44c90cc543b760532974ca1e4eb30dad2aa2026f57bdce66354ae610fc", size = 38682, upload-time = "2026-03-20T08:10:25.266Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/07/57ebf7a6798b016c064bd0ca81b4c6a99daa4dc377b898bc7b41eb6b5af0/weasel-1.0.0-py3-none-any.whl", hash = "sha256:89518acee027f49d743126c3502d35e6dd14f5768be5c37c9af47c171b6005cc", size = 50713, upload-time = "2026-03-20T08:10:23.637Z" }, +] + +[[package]] +name = "webencodings" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/a0/8fd707bcb776a7be556bad06a2ea5fb9bd519df78ef8e26f70ccf0f38bff/webencodings-0.6.1.tar.gz", hash = "sha256:565f9ad031c702dae404e27a099e3e09186a3ab1b9520f06d215502b651fd910", size = 15001, upload-time = "2026-08-15T14:22:57.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/c6/040cbc72480d789a5f40d63fb484d3106554c4dfa2d2b70ad5022057750f/webencodings-0.6.1-py3-none-any.whl", hash = "sha256:7fab6269c8bf237c657876b52058ccb182e861518d1c695c1a9aaa8c1c105d5b", size = 8745, upload-time = "2026-08-15T14:22:56.31Z" }, +] + [[package]] name = "websockets" version = "15.0.1" @@ -3345,6 +7949,10 @@ wheels = [ name = "wrapt" version = "1.17.3" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'win32'", + "python_full_version < '3.11' and sys_platform != 'win32'", +] sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3f/23/bb82321b86411eb51e5a5db3fb8f8032fd30bd7c2d74bfe936136b2fa1d6/wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04", size = 53482, upload-time = "2025-08-12T05:51:44.467Z" }, @@ -3410,6 +8018,116 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, ] +[[package]] +name = "wrapt" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/65/ba/8dc25478ed234dacc7d83c671634f347d0bdfb65bf0502f41879cf2f15a9/wrapt-2.4.0.tar.gz", hash = "sha256:7082fc1f94b020ac275870c4af71b09cff22876fe6e9c4c0ad01ea21d217b288", size = 161179, upload-time = "2026-08-30T04:41:51.424Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/51/8698f7646b9e6f4deca78995c59df25760c047eacfa595262205497b0b07/wrapt-2.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:643e45aa88698c8aae938c50e61940985d4ab9e53ea666d3e8e4eb86a4820d0f", size = 95416, upload-time = "2026-08-30T04:39:07.662Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3c/a4620a03518eb133131aa54ae6973ba273a6f7244f2f771002be5f2db938/wrapt-2.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4597d19904b4aa97331d8bb651ac626d9397727e717942cf11bd7699ff97aa45", size = 95897, upload-time = "2026-08-30T04:39:09.455Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e6/ce8d8da15f825d55523ef1af03763de15d62c559c826516726fcafa072b6/wrapt-2.4.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:635cc171ddfd72edff10e295a02daa65edaa1c0ba619ad11eeed15cd2258c5df", size = 209578, upload-time = "2026-08-30T04:39:11.028Z" }, + { url = "https://files.pythonhosted.org/packages/a5/83/d5bf17b460fcf280f22ddfdfb8b674f5c213e46cad048d4bb2cfd43ce58d/wrapt-2.4.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:932a8892265df7b71257c30e5752635bc1f06a8c4e264024ff031bdf9bb10918", size = 212288, upload-time = "2026-08-30T04:39:12.601Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ec/bda24f3b18046274dde04feb56fd80ae1bf85f89110ddc86ad45d0dd2f06/wrapt-2.4.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5b8fff692f74782de89ba9d7b526a7cc398569b6a988ddc848159cc033c86237", size = 201612, upload-time = "2026-08-30T04:39:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/a3/98/e8d30eaef0f531831c91e6ec6cf9c2c95ada3f8111463a393bf395445c78/wrapt-2.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a27d9653e0f88aa06598954337a545fc3f75bc811df897157a8614846d18d9c", size = 210648, upload-time = "2026-08-30T04:39:15.948Z" }, + { url = "https://files.pythonhosted.org/packages/45/9d/eb138df0a2d85953885a6b14768d3416e55f94728bb45b1fc7fa3dcde246/wrapt-2.4.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:9c884240e7415d3a384e70a15ceea0e884cc9289bcc254afd6412d4e7cf99c47", size = 199700, upload-time = "2026-08-30T04:39:17.697Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/38c3d26493869740aec6f4f5eebd51df0781ee703d554310e4e4d12ea2ce/wrapt-2.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37ba372e9ae71ec43e165b5db05f52e71f7c07dafb9d6a254ef7128112dce751", size = 199774, upload-time = "2026-08-30T04:39:19.16Z" }, + { url = "https://files.pythonhosted.org/packages/b0/07/820553cbd7a94abf1fb3d324f19c09e7bcd73d8f36fca6164f6997c9130c/wrapt-2.4.0-cp310-cp310-win32.whl", hash = "sha256:07daab5babb7edaf89413f5c8bd638474540fb2643b5dfb685bdc0680c96803a", size = 91196, upload-time = "2026-08-30T04:39:20.705Z" }, + { url = "https://files.pythonhosted.org/packages/a2/67/fab3ec749a0bb831ab0993d3eff2ca90032b858ab9e0c0b1932f663e7e43/wrapt-2.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:ef9797bf7c6f9ad9d294538c4f9a64ef3dbbadb63590a9a067393fd49ba28b0f", size = 96080, upload-time = "2026-08-30T04:39:22.274Z" }, + { url = "https://files.pythonhosted.org/packages/fb/05/4295a347b2f8772cead2b45f0d4049902242fcb46a5f4488c0e8b0951681/wrapt-2.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:11ccb5f3de2047ef91408464abdc04682e40e7d7bc9614885d2abcaa7e2ef149", size = 92994, upload-time = "2026-08-30T04:39:23.63Z" }, + { url = "https://files.pythonhosted.org/packages/44/f0/f2f25fe8d516e63354ce4b027d4dc8d824bbf1f5f173f0bb83ce1bcbf706/wrapt-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a67ec80d15ac199d4a9a04a33f3039a1c219c9bf1c07b1b0422497613f167fb9", size = 95620, upload-time = "2026-08-30T04:39:25.116Z" }, + { url = "https://files.pythonhosted.org/packages/81/29/8e1d699fd15591e58f375e1eb5ce444aa955645edc53d09d86cf41d8aa2e/wrapt-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fc1b2cebd6d8db9b4ac0adc817c08b4901922e85604ae2a69aecb5217b2c09d8", size = 95795, upload-time = "2026-08-30T04:39:26.619Z" }, + { url = "https://files.pythonhosted.org/packages/ff/81/63c2fde1f11d008596ef86631afb37a8cb250eec62382003b7d12efd0071/wrapt-2.4.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e52c6a5be3284719e53b629ccfa565c146e604e861de35e861c94f7622806eb5", size = 217752, upload-time = "2026-08-30T04:39:28.301Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e9/373bc7c86eb41f6ab2e5608afac0bda11130b870c4dff4f4d1f25ffafe8a/wrapt-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9905bceb7b2833559518574ad6259d2ec9ffd111a0aa330ca685db74478e1ae3", size = 219872, upload-time = "2026-08-30T04:39:30.085Z" }, + { url = "https://files.pythonhosted.org/packages/49/95/a599d1095b6a271ef91ebb6852f7b4cfc7462d0aff7f4cc1fc3e6437193d/wrapt-2.4.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:abc347e92f9202c8ac1d5c1626a800fd5e56e13433f0651b26dddda5b421ac79", size = 205822, upload-time = "2026-08-30T04:39:32.027Z" }, + { url = "https://files.pythonhosted.org/packages/71/59/14ea2e24c2546da9a08cf9da8fcb2a8ada40ddca0c9a4f26f6a559e49efb/wrapt-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:52f01626f1d2bc54585954cd8b4931f81003b0ac8dad61c741f43014bc9a0f0b", size = 217806, upload-time = "2026-08-30T04:39:33.671Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e8/ff294f964325a6451ff413aa918f5d55a197303b813d0ee0a16ecf3c9bd1/wrapt-2.4.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:811a36628d8b76724b980d508d576e5c5ecae1073b6ec4b4eb21646921906fe6", size = 203892, upload-time = "2026-08-30T04:39:35.103Z" }, + { url = "https://files.pythonhosted.org/packages/fe/45/34c1b0172f0c36305c26c2ffddc8f0bae7a43d78d6b32b00b1b043f77fec/wrapt-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b33df90f3d1e5b1c8811830b11a3e718b4f3a2823b748fa9be1688cb82b193f1", size = 207087, upload-time = "2026-08-30T04:39:36.55Z" }, + { url = "https://files.pythonhosted.org/packages/bc/a2/db3b55e29d04b685c761b1d6aca9f84a9c4f7e1c93543f23ba8a180a2a3f/wrapt-2.4.0-cp311-cp311-win32.whl", hash = "sha256:be535bdfbedda84cb8ebc6a80955dfd03d46840c13470486bd038f089e38b172", size = 91301, upload-time = "2026-08-30T04:39:38.114Z" }, + { url = "https://files.pythonhosted.org/packages/c6/55/c9fd1bf55e144082da6d62313d38f1449707bca16b76af4abbd5492f91e6/wrapt-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:a1117c63a39ba4d1b884e658089e512412d5174217ea1b4fe570977e42a5b129", size = 96308, upload-time = "2026-08-30T04:39:39.432Z" }, + { url = "https://files.pythonhosted.org/packages/6e/89/68d6c10590e74c496046f9fcbbb6ef80a2eca823f924305bf79acb65cccd/wrapt-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:637fd6a18bb668a0c27b4767dcbc2fa93119c90da735bd2669fdde2d7b59fab3", size = 92839, upload-time = "2026-08-30T04:39:40.845Z" }, + { url = "https://files.pythonhosted.org/packages/f0/22/581a0b44349d5babe526c958f365b8126e0fbd8fc2810e80446c47358050/wrapt-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ef4e2d6e399ce6eecc80179a6b9ef6544f121288f95fc132bc36c9d9503903af", size = 96374, upload-time = "2026-08-30T04:39:42.335Z" }, + { url = "https://files.pythonhosted.org/packages/5d/90/095984648cec62a786bb27c0b50f6cfa5856d1e073ba1006fe148d190084/wrapt-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b9b32d5e4f0a179cef5075cc79b79d6d3482c44c434c12969e48c6719e06d95", size = 96178, upload-time = "2026-08-30T04:39:43.789Z" }, + { url = "https://files.pythonhosted.org/packages/f8/fd/b20e3cb3cab35131b515edf18e8cd777dff680fc76fc00919481f4e536af/wrapt-2.4.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d7dbbdbfdacb85c2d962fa52db791c77943fd777d600d74c95af2d53b32f5a94", size = 227806, upload-time = "2026-08-30T04:39:45.264Z" }, + { url = "https://files.pythonhosted.org/packages/08/75/c8dfba5e0caf17cd0718a0cbbe76cb85e637a2d65183fb728232419f6fca/wrapt-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39cd68df4dff79f5336f9c745c06259d204bcb42d504040c9c91eac9e2abb39c", size = 229004, upload-time = "2026-08-30T04:39:47.068Z" }, + { url = "https://files.pythonhosted.org/packages/42/05/d4853fbd33e5860b10d5aec690f563547a92a82e61fb8bb2d4ece1ce3570/wrapt-2.4.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2a9f1a2f75bb95257cc5744e255e10a5a86e923f328b40ad3dbf9d8d03430013", size = 208934, upload-time = "2026-08-30T04:39:48.73Z" }, + { url = "https://files.pythonhosted.org/packages/a3/66/23d0e8de9b411fd198af5121627587563657370c8d509fbe5ea8adb3df79/wrapt-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8763ad01e3725b7751a4575f38bbcc19c0aa0822fec91c5c5bd21ce3ce7e1d2b", size = 225709, upload-time = "2026-08-30T04:39:50.287Z" }, + { url = "https://files.pythonhosted.org/packages/01/37/3b357bc90530d510ae59ae7ac48265c482ae899e47637ca4436645688b40/wrapt-2.4.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9125c6dbe8b88c00dd8ef4fc1e55757e8eb4720b6b2b2cc610a45bd32bd28c57", size = 207090, upload-time = "2026-08-30T04:39:51.78Z" }, + { url = "https://files.pythonhosted.org/packages/6d/0c/d8a5c6dbcc2d221308223bcea4130c6332454a855cb4dbd5dcb2360b13b2/wrapt-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:28f5de1526831b8f173889a436e289fe181ede8c66c9feb669d1aca8fd602eaf", size = 216269, upload-time = "2026-08-30T04:39:53.641Z" }, + { url = "https://files.pythonhosted.org/packages/92/93/cc9fc8fef1d3d25edaa1c2dc2337b556dc1d0613ddc1c4a6fe9ee08ad705/wrapt-2.4.0-cp312-cp312-win32.whl", hash = "sha256:a9ca1cdb3f7facb4990c7739ea5afbaceeb6728d066feedde03a4cfe83b29b03", size = 91187, upload-time = "2026-08-30T04:39:55.38Z" }, + { url = "https://files.pythonhosted.org/packages/ad/ec/a7b10705172bdb669b9687a8ff68bbe5f566437d2a49ad6d976af48b6d10/wrapt-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:8b464316489fb2fca0669ea0f8f07290054a0f26fc72982d3e4cf95469628ba9", size = 96423, upload-time = "2026-08-30T04:39:56.81Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/e838ac6463a1a1a1817b2f184ee2aa20c54692b80368c5063403c8d2461c/wrapt-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:db1285071ea09a7767fac608e7b5c7b03c09833b06186875a359905fbc659d29", size = 93003, upload-time = "2026-08-30T04:39:58.237Z" }, + { url = "https://files.pythonhosted.org/packages/19/86/f9de4e11582ff96ad2199eeeceaa17faa27bbdc599243f520070c4f3de07/wrapt-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5c5c4c728cd22a36e4b8bb5df4a7d3bccaa865d27725b36eeb3b6f18fb2e1bc2", size = 96041, upload-time = "2026-08-30T04:39:59.575Z" }, + { url = "https://files.pythonhosted.org/packages/c3/ab/1dbf50802bea3b46192fd0dc39bb0eb2e77a064c813b2bbd88d2888ad49f/wrapt-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7de5b8d94417e55c02be50cc226e0ae1209bbc73813bf691dff3979c94438115", size = 96269, upload-time = "2026-08-30T04:40:01.182Z" }, + { url = "https://files.pythonhosted.org/packages/cb/a3/a3b5cde1cd06e04b6e95134eb3187a0a7da607a530e7795b221d4e4fa819/wrapt-2.4.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6436e2bda993a3eb69a1b317fc831c8ebcafb5704c390859ebd49f81218c4bbb", size = 225787, upload-time = "2026-08-30T04:40:02.715Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f7/d100f6c348b7669f19119cf890dcd4764623e2233af065586d110e0cd99e/wrapt-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e084558fbd112d2e1e34b0f5c71e45a3405bdad51a17150368a959bcf6697964", size = 226649, upload-time = "2026-08-30T04:40:04.647Z" }, + { url = "https://files.pythonhosted.org/packages/52/c6/3af8df515d5d7e92306957536f3468c6bdfecbe3659f99dbf09a468c2c4c/wrapt-2.4.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e78c947e18fadfd690c9420c30a96d221feeb93fc8f1cc00509b370ac16c3114", size = 206760, upload-time = "2026-08-30T04:40:06.332Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c5/40d355552bd3eb6c5186e26051c19b573d24d7896de42caa7937d6b5ca9f/wrapt-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:08d8378c4514ac8dcc0ace76044cf87a873e6a52b5e6109834c8fb9037f4441b", size = 223467, upload-time = "2026-08-30T04:40:07.829Z" }, + { url = "https://files.pythonhosted.org/packages/40/ab/d198eebdb39f0d7e182e771e590a36673489cd58cebdad8aa273dcf28e04/wrapt-2.4.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:93180c2199784dd6a1075b33f9ed636bd0966821edbece6b3d5379b1c4f0bb7d", size = 205358, upload-time = "2026-08-30T04:40:09.344Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0e/974a60672ad507d39a3d8a1c6351ef37fe65b07240d000ceba5d2b83e9e9/wrapt-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d5e5eb76fb87e62752af751d2dcd9d1cd986b12037d2e1363d109ba716029e8", size = 214654, upload-time = "2026-08-30T04:40:10.923Z" }, + { url = "https://files.pythonhosted.org/packages/cc/5a/8b2db70206db0a4246758e0472ce344cb9636217113ef70640fc8d2ce874/wrapt-2.4.0-cp313-cp313-win32.whl", hash = "sha256:49bb5a572469e0e18163a8ec2aa972135a0929899ecbe627665f274506e1b5b4", size = 91171, upload-time = "2026-08-30T04:40:12.895Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1e/e782b511c680dbe7369c92e7d981484aacca0cda584da1f28a84cd9a8e1a/wrapt-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:b1737f46b1e4a81eb93500a7f2854319e1c7a86e8863fb050b7b4daadd5a4178", size = 96178, upload-time = "2026-08-30T04:40:14.336Z" }, + { url = "https://files.pythonhosted.org/packages/9f/62/095ba31123fa5dd482d6183c05200b061314aabbd5442c010aba4b03ff1c/wrapt-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:f1e9e088094f4895f84ab043e7d59401df137d663efbf1e80c82144882960830", size = 92949, upload-time = "2026-08-30T04:40:15.935Z" }, + { url = "https://files.pythonhosted.org/packages/1f/dd/1f269e4daf0c992f675e1ca2de6b1683b761c6d0aeb6c7b4b412486823ea/wrapt-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:788e473d1a6786d29d577b1e2bd95e214c09cdafde84907c522c31069c9acfac", size = 96386, upload-time = "2026-08-30T04:40:17.584Z" }, + { url = "https://files.pythonhosted.org/packages/be/42/7ecef06d33c0121c68d66a8a695efe67ebaa57218c1c61c585eca2a6117a/wrapt-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:947bd4b3438167b3638bf5477cb83a068a586ffb6d331ac427f39839c2b93b3c", size = 96532, upload-time = "2026-08-30T04:40:19.116Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e3/8fdc9eba0e6cbbfe8303e1e807d734691309a27970b2ea458d099f1a46b0/wrapt-2.4.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3a69161cae7f0dca44c89c1d14146b4a0508a0c3cad98b3f2db1f4e9016c94ba", size = 228775, upload-time = "2026-08-30T04:40:20.604Z" }, + { url = "https://files.pythonhosted.org/packages/f4/77/4ac5882abfb29bf9821c5fa5cf9f30241a194e0f47faa2682b9b29765278/wrapt-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0536f5d85ff6a157ebe7e0fe08c5479943742cf1ce59569075a66159efcbc495", size = 229029, upload-time = "2026-08-30T04:40:22.186Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c5/8a3608311a02faf3e5c072da38d06a7c623150fc258e29f18fe377d91703/wrapt-2.4.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5f041ed6a4d571010944bd6cfad9072db463e1851877b6d3227467a44af37456", size = 210436, upload-time = "2026-08-30T04:40:23.953Z" }, + { url = "https://files.pythonhosted.org/packages/de/90/e0cbc43f435fd39df25460e9f173e7b96f3dac5c7f66be41c7227166f021/wrapt-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f7fed45dbadf5d98a52bfff9624d3cca00affeb9543d493c9632b7a53cdd35c9", size = 226586, upload-time = "2026-08-30T04:40:25.507Z" }, + { url = "https://files.pythonhosted.org/packages/81/6c/7e5f2143228635ec139ef6df733dc477049f7d96a0c49deb23944a73ed6a/wrapt-2.4.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5cc2e7c7b6032e11a2b367a9baadaf0c5241feff2d8205260d87f1aa6dbdf84b", size = 208880, upload-time = "2026-08-30T04:40:27.128Z" }, + { url = "https://files.pythonhosted.org/packages/10/16/1de84402bb7a0916e10739bf6586e031244172b299e87c8cff2a04baf9ff/wrapt-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:72826910a1cf5a081234720fd43011304b899acfee219af49148155b4d795533", size = 216689, upload-time = "2026-08-30T04:40:28.844Z" }, + { url = "https://files.pythonhosted.org/packages/20/19/cd6bd5050381a541b44be97c4e0994eed60c5f439f4314f95eb5777d6c1a/wrapt-2.4.0-cp314-cp314-win32.whl", hash = "sha256:0eca69c9e93518240abe8801fb9b2726116a6e48172e4564c2651a2e14521747", size = 91581, upload-time = "2026-08-30T04:40:30.592Z" }, + { url = "https://files.pythonhosted.org/packages/a4/f8/b642f3184619adde676ad449030bcbeae6cc78ea07a92f0b5fddeec4c4e6/wrapt-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:63b94f401d7ae3a9a3027472fd3a3ff38afd2ed293b2f0b3b84a6d133a9f99a3", size = 96510, upload-time = "2026-08-30T04:40:32.1Z" }, + { url = "https://files.pythonhosted.org/packages/4d/3b/3415a18b91221261eeac85bf8ee23dfb0e2a39d76b9703a797efca177439/wrapt-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:6b3e082d43f592fcd381aee46354a11ce887a813ce5bbcedd9766fd681723c09", size = 93648, upload-time = "2026-08-30T04:40:33.563Z" }, + { url = "https://files.pythonhosted.org/packages/ac/90/80cf6a09e9599a11249775928df9bb790b82471e4312b847a861ffb2c2ed/wrapt-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:09064c7be688c38c3ff125ce86bc26b69b5d78dd56062c3ddd9c814b2a25f1e1", size = 99615, upload-time = "2026-08-30T04:40:35.134Z" }, + { url = "https://files.pythonhosted.org/packages/b2/da/c1d3245abb911a42584f8f7e9781995bdc41345c7affba75cf7e376c85ac/wrapt-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4f8ddff4bbb75916be36da5169b8b9d475b59a1bd24acdb7551bb2c71be9aaac", size = 100031, upload-time = "2026-08-30T04:40:36.641Z" }, + { url = "https://files.pythonhosted.org/packages/84/46/8ec4941d0abbb010df7caf0a34840ca0128177389843b0f5ef2f9ee48ac5/wrapt-2.4.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e9f8017443595870aa31f46125553a5c55ce95a26a267b96261baee6ba566d83", size = 269389, upload-time = "2026-08-30T04:40:38.212Z" }, + { url = "https://files.pythonhosted.org/packages/14/b5/a0ae1b431cc1f49a545d32b8b678a5788c50583ecf0ecb85dc0c7f95b4f6/wrapt-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:328eb2d978ca3a6ae25f8d8fe560bf8f4bc9778b5932e7b142664eef05b92e8f", size = 281081, upload-time = "2026-08-30T04:40:40.045Z" }, + { url = "https://files.pythonhosted.org/packages/c7/24/dfaf53dd3bdb0703524a9367b48e2a64ea86433fcc854b5f14be6a8e0e39/wrapt-2.4.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7a057d376d994da6bd1bbf955ecfda699aa7353826f98847f5605e1801abdfd4", size = 249637, upload-time = "2026-08-30T04:40:41.657Z" }, + { url = "https://files.pythonhosted.org/packages/3e/27/bdd82044d7503c2bfa78afcc89881f82a1b82b5d2013aabab853d339ce2a/wrapt-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3367a5212212c9393e0d3ca6ae029b3a8fa40c5896e4a985d43fe8a4b8322f0d", size = 275322, upload-time = "2026-08-30T04:40:43.408Z" }, + { url = "https://files.pythonhosted.org/packages/c4/82/04f4228eb3fb348d660dd1ea7225e53665b1809df2273ff4861d4d33b741/wrapt-2.4.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c4fca1e63af6675af3df7cdfcd5a0c878b5e655c7e48611ced9dc8d62183a11d", size = 247292, upload-time = "2026-08-30T04:40:45.457Z" }, + { url = "https://files.pythonhosted.org/packages/a2/20/67b2968fa9200458446c51b36a435adb6906083428b70fafb4caf92d4dc2/wrapt-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:694005fdc3002ade0f21641408c588028abde03c85961f3ba7727d8bead3ed6b", size = 264586, upload-time = "2026-08-30T04:40:47.079Z" }, + { url = "https://files.pythonhosted.org/packages/d7/fd/0db9ba03e08a7663f52455e95520c723f567bc037bffc6699950fcc456c4/wrapt-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:332d9bad7e9b718974bb2a576504c4956f45b4a0fcd7b3bb7827279167550464", size = 93752, upload-time = "2026-08-30T04:40:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/3f/87/ced171220935c696b157207385fa6be5675558a74655479f071d95a00f1d/wrapt-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6d57264c9dfcf37d2bf0b0fbec68d0f6184fc5617267619ada04d03e8b0231f3", size = 99890, upload-time = "2026-08-30T04:40:50.407Z" }, + { url = "https://files.pythonhosted.org/packages/a3/af/4a10c9a6d3b7ae41f830978c28d33a59ceb29537bd6875d2abfe78db4b41/wrapt-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f43af38a642c3d6062e9740d8f5cc0feb5dbe0da516702df892147393b8cb14d", size = 96033, upload-time = "2026-08-30T04:40:51.933Z" }, + { url = "https://files.pythonhosted.org/packages/a0/df/3a0b6225ab88bd47090df70391c059a3308057638f8fc0ae32e8ac9d1886/wrapt-2.4.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:430fde1a116df3ceb5c29035de1da6609b70e680d9b8ce3ee624422f3fe0978c", size = 96389, upload-time = "2026-08-30T04:40:53.555Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6f/803b0d0e14de11781f0e938e6f7d6e29e79652139fe70d7513460357ac78/wrapt-2.4.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:7d28f8f35a02d49f75f57fa4e755db4ba33f65841c0de64cd65b253916f5bf06", size = 96557, upload-time = "2026-08-30T04:40:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e8/46571e1218d0494604a7aadc4c898c738c4b179052327ee1e57e278cebd6/wrapt-2.4.0-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:efd9a4be6785295e471f71efdf5682bd11d5b822b9665e6e1b4844917cf2f7ac", size = 229230, upload-time = "2026-08-30T04:40:56.703Z" }, + { url = "https://files.pythonhosted.org/packages/78/2e/0cab15fcaec56096a5734feace3620bc01edc885653be04bd756f84a6784/wrapt-2.4.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75529a2fb569a671cf162f762c1b576f569f571b55ec7f3481258ca842ba507f", size = 229444, upload-time = "2026-08-30T04:40:58.51Z" }, + { url = "https://files.pythonhosted.org/packages/e7/9e/a92c049371a2675f98a0381ab2951f984866d1ba4de0e0771d6a31fdaa2b/wrapt-2.4.0-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66e7512c0d324cc37bba1def2be1fc365cbb685d3aa393a8f6f4d2d00202881d", size = 212482, upload-time = "2026-08-30T04:41:00.224Z" }, + { url = "https://files.pythonhosted.org/packages/ee/3b/8b5b57d0ff24edcd3421dbaeb4e94c89be3616824e47708f4e13f25ae3d7/wrapt-2.4.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:5f3bdfc35c83b562fcaebc0f24593045e5ed9f3b633adafd35222718a0ec38fa", size = 227017, upload-time = "2026-08-30T04:41:01.918Z" }, + { url = "https://files.pythonhosted.org/packages/0e/20/124b40bfd9585848db5a5aa6741d0c8dbf378dd995c6c2d95f090d9cf540/wrapt-2.4.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:d5f45bead708e2c0014be5e98531ce7202916b098a208c7be83c6ceb0a2559fa", size = 210498, upload-time = "2026-08-30T04:41:03.617Z" }, + { url = "https://files.pythonhosted.org/packages/4b/bf/89db9d5a80a9f2af52b24bdfdb5392be80bc0f0fd39fc39d1aab72afd0bd/wrapt-2.4.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:d294576fddac636589e4deccfe782e8f429da10f167c1985c4d51071de3672b7", size = 217046, upload-time = "2026-08-30T04:41:05.473Z" }, + { url = "https://files.pythonhosted.org/packages/3b/0b/021c9d6ce64c639894bffdaa7a895ddd4187abfefb2873ce55e536cd9d56/wrapt-2.4.0-cp315-cp315-win32.whl", hash = "sha256:0191d717dfbb8e519e7bfd4775e5b9bd57e359b3a09ab5db1ea47f6025b4d845", size = 91591, upload-time = "2026-08-30T04:41:07.086Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d3/6ebd944041cea0ac4a108a4739510ed2dc891a3f3216e4f7bf0650f5b5a6/wrapt-2.4.0-cp315-cp315-win_amd64.whl", hash = "sha256:e8df31a126a0a247c1aa379e30873839de03912dea09ca360c680f3625d815df", size = 96517, upload-time = "2026-08-30T04:41:08.671Z" }, + { url = "https://files.pythonhosted.org/packages/96/84/7c5e52e450f80ba76fd0282dccf7c79cd004ebd8ccabd0903064d3d2c56e/wrapt-2.4.0-cp315-cp315-win_arm64.whl", hash = "sha256:e9e7e94472f0e3f1447caf27e1939eb384d0e87972a35a05f5c2e0968e9c01af", size = 93652, upload-time = "2026-08-30T04:41:10.258Z" }, + { url = "https://files.pythonhosted.org/packages/35/89/f08ff45d7646de29750932805cc3b1e86b6ac3128015b293ed45fa8efe86/wrapt-2.4.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:8828369b7d3e93c547cc8ad931b5a57b4e8d174035c82762fb1091e7d05ac9f5", size = 99610, upload-time = "2026-08-30T04:41:11.933Z" }, + { url = "https://files.pythonhosted.org/packages/4b/c2/f9a3c40901a36c6bb7ecaff8e1e54af78fa7fa0b95a0e54d13d3a24c8a0a/wrapt-2.4.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:413e757dce7a43fcda8bb8441994b1127492ffac6a5803af777d44516df8c6e2", size = 100064, upload-time = "2026-08-30T04:41:13.492Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e1/e2437f17f2a1ec292056e2fcafe1248269ebc39502f2ffe79424bf86f8a6/wrapt-2.4.0-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:75944792cf6b99262d649d55710bf5901f7013fbb212c7a1d736b97a20517607", size = 269421, upload-time = "2026-08-30T04:41:15.238Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d0/c98d6548dc4c7d12ab9baa192234ca1a57e141afd283252b448faddbd9ef/wrapt-2.4.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:648d1d4f94e8a0a1656675c755f40d2f0ee5fe92c449ab45326f4ecc2738cbe8", size = 281452, upload-time = "2026-08-30T04:41:16.939Z" }, + { url = "https://files.pythonhosted.org/packages/a3/57/673168e00aa03725148ce621ed201b75df4e787a57acd48fecefd2725600/wrapt-2.4.0-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a112a1bfdd2621e4344cb0a32dbaab80636b32dac1b055d03fbb2a67d806d1db", size = 250358, upload-time = "2026-08-30T04:41:18.716Z" }, + { url = "https://files.pythonhosted.org/packages/78/0b/f2e576de5bf53ef5b578470104ea93f33e273a704c825131bc1719fffc42/wrapt-2.4.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:0972cd025f4c86fa2d8abd953d9f875779935343af58b4ce019ff89573fc65bd", size = 275654, upload-time = "2026-08-30T04:41:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/9347b2e236346b1ba4cb28b82b205b8a377bb2da9417cb81bbe3d25816d7/wrapt-2.4.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:c246aaed719dcdb62eeb7b8d9306a6237777226ef3baad35919c4ae134c91ce7", size = 248662, upload-time = "2026-08-30T04:41:22.371Z" }, + { url = "https://files.pythonhosted.org/packages/a5/36/3b84d9e1ac8393bf2c94272760a2d361dc394ac30301e6d6dbd6583ade2d/wrapt-2.4.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:1656de3835f760781c9b974bce07d8c04edb9c9ad7ad67264aee69cd68a1db09", size = 264813, upload-time = "2026-08-30T04:41:24.116Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a2/de7b1de1702667b4a048318e301e26887268c17b07c8b9797cea06b10aee/wrapt-2.4.0-cp315-cp315t-win32.whl", hash = "sha256:d8e6e1e5dc684dfce7c33fc8b67a08ba2af94f3a45cfc70d5c1d6a839d2caf97", size = 93753, upload-time = "2026-08-30T04:41:25.793Z" }, + { url = "https://files.pythonhosted.org/packages/09/50/4e7ef58c4eb058861ceddc0d1f94a6ed87f62e1cb27783c60b2897ef7e58/wrapt-2.4.0-cp315-cp315t-win_amd64.whl", hash = "sha256:85ed3c67fd39e8d9a36c224758cb6f2f4eb277d07ea677930caa0008c18ec002", size = 99888, upload-time = "2026-08-30T04:41:27.305Z" }, + { url = "https://files.pythonhosted.org/packages/68/64/d15740c763dd0ddea2338ad42e3bd4a84f8702e16083e7ff61674c504a13/wrapt-2.4.0-cp315-cp315t-win_arm64.whl", hash = "sha256:36b56a4fba13b34ed8ff307557325fff215de0a58b5dbaef2c50e4d8aa39dbd1", size = 96039, upload-time = "2026-08-30T04:41:29.062Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/fafe0002f572ced999c792cfe8b05d39269c63d8193d15d25bd828bcad7a/wrapt-2.4.0-py3-none-any.whl", hash = "sha256:18aabd9301d06026f5900538051773d6f87f65ae02cdc60de482df978513dc0a", size = 73713, upload-time = "2026-08-30T04:41:49.805Z" }, +] + [[package]] name = "xxhash" version = "3.6.0"