diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c3c5e0a6..bf508796 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -3,7 +3,7 @@ name: CI
on:
pull_request:
branches:
- - master
+ - main
- 'feature/**'
jobs:
@@ -13,7 +13,7 @@ jobs:
contents: read
strategy:
matrix:
- python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
+ python-version: ['3.11', '3.12', '3.13', '3.14']
steps:
- uses: actions/checkout@v4
diff --git a/.github/workflows/release-docs.yml b/.github/workflows/release-docs.yml
new file mode 100644
index 00000000..8eddf6a5
--- /dev/null
+++ b/.github/workflows/release-docs.yml
@@ -0,0 +1,94 @@
+name: Release Documentation
+
+# Builds the docs with Zensical (the successor to Material for MkDocs) via
+# scripts/zensical_build.py and publishes the result to GitHub Pages. The script
+# collects the top-level README (-> index.md) and the notebooks/ tutorials into
+# the docs tree and pre-converts the notebooks with nbconvert, because Zensical
+# does not run the mkdocs-jupyter plugin.
+#
+# The published site is served at https://presidio-research.dataprivacystack.org
+# (custom domain, set via the CNAME written below). GitHub Pages then 301-
+# redirects the default https://data-privacy-stack.github.io/presidio-research/
+# URL to that custom domain.
+#
+# One-time repo setup (by a maintainer), required for the custom domain:
+# 1. Settings -> Pages -> Build and deployment -> Source: "Deploy from a branch",
+# Branch: gh-pages / (root).
+# 2. Settings -> Pages -> Custom domain: presidio-research.dataprivacystack.org
+# 3. DNS: add a CNAME record presidio-research -> data-privacy-stack.github.io
+
+on:
+ release:
+ types: [published]
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+# The custom domain served by GitHub Pages for this site. Written into the
+# gh-pages branch so it survives each publish (which wipes the branch contents).
+env:
+ DOCS_CNAME: presidio-research.dataprivacystack.org
+
+jobs:
+ github-pages-release:
+ name: Build with Zensical and push to GitHub Pages
+ runs-on: ubuntu-latest
+
+ permissions:
+ contents: write # Required for pushing to the gh-pages branch
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0 # Fetch all history for proper gh-pages deployment
+ persist-credentials: true # So that the token is available for pushing
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.12'
+
+ - name: Install dependencies
+ run: |
+ pip install -r docs/requirements-docs.txt
+
+ - name: Build docs
+ env:
+ # README repo-relative links are absolutised against this ref.
+ DOCS_REF: ${{ github.event.release.tag_name || github.ref_name }}
+ run: |
+ python scripts/zensical_build.py build
+
+ - name: Deploy to GitHub Pages
+ run: |
+ set -euo pipefail
+
+ git config user.name "github-actions[bot]"
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
+
+ publish_dir="$(mktemp -d)"
+ trap 'git worktree remove --force "$publish_dir"' EXIT
+
+ # Create gh-pages if it does not exist yet, otherwise reuse it.
+ if git ls-remote --exit-code --heads origin gh-pages >/dev/null 2>&1; then
+ git fetch origin gh-pages:refs/remotes/origin/gh-pages
+ git worktree add --detach "$publish_dir" origin/gh-pages
+ else
+ git worktree add --detach "$publish_dir"
+ fi
+
+ find "$publish_dir" -mindepth 1 -maxdepth 1 ! -name .git -exec rm -rf {} +
+ cp -a site/. "$publish_dir"/
+ touch "$publish_dir/.nojekyll"
+ printf '%s\n' "$DOCS_CNAME" > "$publish_dir/CNAME"
+
+ git -C "$publish_dir" add -A
+ if git -C "$publish_dir" diff --cached --quiet; then
+ echo "No documentation changes to publish."
+ exit 0
+ fi
+
+ git -C "$publish_dir" commit -m "docs: publish Zensical documentation"
+ git -C "$publish_dir" push origin HEAD:gh-pages
diff --git a/.gitignore b/.gitignore
index a74f3f84..a4d3c60c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -199,3 +199,11 @@ datasets/
*.svg
!synth_dataset_v2.json
test_us002.py
+
+# Zensical docs build (scripts/zensical_build.py)
+/site
+/zensical.yml
+.zensical-build/
+.docs-venv/
+# Docs brand asset is committed despite the blanket *.svg ignore above.
+!docs/assets/dps-icon.svg
diff --git a/AGENTS.md b/AGENTS.md
index e502d1f7..9ce1975b 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -8,7 +8,7 @@
## CanonicalMapper
- Single-phase (Identify-only) — no projection phase
-- `analyze(results_df, min_severity='WARNING')` — COLLISION_SAME_BRANCH (INFO) hidden unless `min_severity='INFO'`
+- `analyze(results_df, min_severity='WARNING')` — informational COLLISION_SAME_BRANCH issues are hidden unless `min_severity='INFO'`; mixed gold depths are ERROR
- `get_mapped_results_dataframe()` returns `MappedResults` (frozen dataclass with `.original`, `.binary`, `.branch`, `.detailed`)
- `get_mapping()` returns `{label: resolved}` dict — UNRESOLVED labels excluded
- `get_issues()` filters by `_min_severity` — call after `analyze()` or `map()`
@@ -18,7 +18,7 @@
- COLLISION_CROSS_BRANCH (WARNING, blocking) — only raised when cross-branch co-occurrences outnumber same-branch ones for the prediction label
- PREDICTION_ONLY (WARNING, blocking)
- DATASET_ONLY (WARNING, non-blocking)
-- COLLISION_SAME_BRANCH (INFO, non-blocking)
+- COLLISION_SAME_BRANCH (INFO when projection is unambiguous; ERROR when gold mixes depths on one branch)
## _Resolution dataclass fields
- `tier` — identification tier (EXACT, COUNTRY, COUNTRY_FALLBACK, FUZZY, UNRESOLVED)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f7b4a358..025feeba 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,16 +2,45 @@
## Unreleased
-## Version 0.3.2
+### Features
+
+- **Python 3.14 support** — `requires-python` is now `>=3.11,<3.15`, `uv.lock` has been regenerated for the wider range, and the locked `spacy` moves to 3.8.16, the first release that declares 3.14 support. CI runs the test suite on 3.11 through 3.14.
### Behavior Changes
+- **Predictions are projected to the deepest annotated ancestor during canonical mapping** — the gold vocabulary decides the granularity, per prediction. A `NAME` prediction is mapped to `PERSON` when the dataset annotates `PERSON`, and `DATE` is mapped to `DATE_TIME` when the dataset annotates `DATE_TIME`. A prediction with no annotated ancestor is left unchanged, so a coarser prediction is never pushed down onto a finer gold label and siblings are never conflated. Datasets that annotate several depths on one branch (e.g. `PERSON` and `TITLE` in `data/synth_dataset_v2.json`) need no mapping decision: `TITLE` predictions stay `TITLE` while `NAME` predictions become `PERSON`, so every annotated depth keeps its own metrics. Mixed annotation depths are reported as an INFO issue. Low-IoU errors are attributed to the projected scoring label.
- **Two-sided (asymmetric) span counting in `SpanEvaluator`** — recall is now counted per annotation and precision per prediction span, replacing per-annotation counting of predictions that could count one prediction span several times (once per annotation it overlapped) or count a group of spans as a single prediction. Every annotation gets exactly one verdict (`TP + FN == num_annotated`), and every prediction span enters `num_predicted` exactly once, as either credited or FP. Precision is now `(num_predicted - false_positives) / num_predicted`; `true_positives` counts covered annotations and may exceed the number of credited predictions (one wide span covering two annotations is two recall hits but one credited prediction), so `true_positives / num_predicted` is no longer a valid precision formula for downstream consumers. Practical effects: a group of same-type spans that jointly fail the combined-IoU test now counts one FP per span (previously one per group); a too-wide span missing several annotations counts one FP (previously one per missed annotation); a span that matches one annotation and merely brushes another is no longer punished twice (FN only, no extra FP). Fixes the old inconsistency where an annotation could be counted as both FN and TP, and `num_predicted` could drift above or below the actual number of predicted spans depending on gold layout.
- **Single-span coverage uses exact pairwise IoU** — when exactly one prediction overlaps an annotation, coverage is measured with the exact pairwise `Span.iou`; the combined-IoU path (which slightly inflates values at span boundaries) is reserved for genuine multi-span coverage. Borderline single-span matches at a threshold boundary may flip compared to previous releases (e.g. IoU 0.4706 previously computed as 0.50 no longer passes τ=0.5).
- **One confusion-matrix cell per span** — a wrong-type detection at IoU >= threshold is recorded as a single `(annotation type, predicted type)` cell representing both the gold and the prediction; neither is additionally written to the `"O"` row/column. Documented in `docs/span_matching_strategies.md`.
### Bug Fixes
+- **Hierarchy projection now honours a custom hierarchy** — the full-depth view used for branch and detailed projection was built from a module-level default hierarchy, so a `CanonicalMapper` constructed with a custom `EntityHierarchy` projected against the built-in taxonomy instead of its own. The full-depth view is now derived from the mapper's configured hierarchy.
+
+## Version 0.3.2
+
+### Features
+
+- **Branch-level aliases** — non-leaf hierarchy nodes can now declare raw aliases via a reserved `_aliases` key (e.g. `"LOCATION": {"_aliases": ["LOC"], ...}`), mirroring the alias lists that leaf nodes already have. `add_alias()` on a branch node now records the alias instead of creating a spurious child leaf. The reserved key is skipped by every tree-walk, so it never becomes a canonical entity.
+
+### Breaking Changes
+
+- **`LOC`, `ORG` and `PER` are no longer canonical entities** — they were empty leaf nodes under `LOCATION`/`ORGANIZATION`/`PERSON` > `NAME` and are now branch-level aliases of `LOCATION`/`ORGANIZATION`/`PERSON`. Coarse dataset labels like TAB's `LOC`/`ORG`/`PER` therefore match a model's `LOCATION`/`ORGANIZATION`/`PERSON` at the exact (leaf) level, not only at the branch level. Concretely:
+ - `canonicalize("LOC")` returns `"LOCATION"` (was `"LOC"`), and likewise for `ORG` and `PER`.
+ - `LOC`/`ORG`/`PER` no longer appear in `all_canonical_entities` or `canonical_to_branch`.
+ - `get_depth("LOC")` returns `2` (was `3`), because `LOC` now denotes the depth-2 `LOCATION` branch. `get_depth("PER")` returns `2` (was `3`).
+ - `CanonicalMapper.map()` no longer accepts `LOC`/`ORG`/`PER` as resolution *targets*, since targets must be canonical entities. Such mappings are also no longer needed — the labels resolve on their own.
+ - `to_branch("LOC")` still returns `"LOCATION"`, unchanged; `to_branch("PER")` still returns `"PERSON"`.
+
+### Behavior Changes
+
+- **`to_branch()` and `get_depth()` now resolve raw aliases**, not just canonical names. Previously a raw alias (e.g. `COMPANYNAME`, `QQ`) was passed through unchanged by `to_branch` and raised in `get_depth`; both now resolve it first. Unknown labels are still returned as-is by `to_branch`.
+- **`add_alias()` accepts an alias as its subject**, so `add_alias("LOC", ...)` works as well as `add_alias("LOCATION", ...)`.
+- **`add_alias()` now raises `ValueError` instead of silently no-opping** when the alias is already claimed by a descendant of the target (e.g. adding `CITY` to the `LOCATION` branch, where `CITY` already resolves to `ADDRESS`). The hierarchy is left unmodified — an alias the target already owns is preserved. It also raises `KeyError` if the reserved `_aliases` key is passed as the entity name.
+- **A branch alias shadowed by one of its own descendants logs a warning at construction time**, so collisions declared statically in `definitions.py` are no longer silent.
+
+### Bug Fixes
+
- **Span merging no longer depends on the DataFrame index** — `SpanEvaluator` mixed sentence-relative token positions with DataFrame index labels when checking whether two same-type spans are adjacent. With the global index produced by `predict_dataset()`, the between-tokens lookup read the wrong rows — or none at all — for every sentence except the one starting at row 0, and an empty lookup counts as "adjacent", silently merging same-type spans separated by regular words (e.g. the two PERSON spans in "John visited Berlin with Mary" became one). Span counts (`num_annotated`, `num_predicted`, `true_positives`) were deflated symmetrically for gold and predictions, so headline precision/recall could still look plausible. The evaluator now uses sentence-relative positions throughout and produces identical results for any DataFrame index.
## Version 0.3.1
diff --git a/README.md b/README.md
index cb60ac6b..27d64f4b 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,8 @@ It also includes a fake data generator that creates synthetic sentences based on
### Using notebooks
-The easiest way to get started is by reviewing the notebooks.
+The easiest way to get started is by reviewing the notebooks.
+
- [Notebook 1](notebooks/1_Generate_data.ipynb): Shows how to use the PII data generator.
- [Notebook 2](notebooks/2_PII_EDA.ipynb): Shows a simple analysis of the PII dataset.
- [Notebook 3](notebooks/3_Split_by_pattern_number.ipynb): Provides tools to split the dataset into train/test/validation sets while avoiding leakage due to the same pattern appearing in multiple folds (only applicable for synthetically generated data).
@@ -36,6 +37,7 @@ python -m spacy download en_core_web_lg # for NER
#### From source
To install the package:
+
1. Clone the repo
2. Install all dependencies:
@@ -67,13 +69,13 @@ Note that some dependencies (such as Flair and Stanza) are no longer supported.
## 1. Data generation
-See [Data Generator README](presidio_evaluator/data_generator/README.md) for more details.
+See the [Data Generation docs](docs/data_generation.md) for more details.
The data generation process takes a file with templates, e.g. `My name is {{name}}`.
Then, it creates new synthetic sentences by sampling templates and PII values.
Furthermore, it tokenizes the data, creates tags (either IO/BIO/BILUO) and spans for the newly created samples.
-- For information on data generation/augmentation, see the data generator [README](presidio_evaluator/data_generator/README.md).
+- For information on data generation/augmentation, see the [Data Generation docs](docs/data_generation.md).
- For an example for running the generation process, see [this notebook](notebooks/1_Generate_data.ipynb).
- For an understanding of the underlying fake PII data used, see this [exploratory data analysis notebook](notebooks/2_PII_EDA.ipynb).
@@ -89,6 +91,7 @@ analyzing, modeling and evaluating data and models. Specifically,
see [data_objects.py](presidio_evaluator/data_objects.py).
The standardized structure, `List[InputSample]`, can be translated into different formats:
+
- CoNLL
- To CoNLL:
```python
diff --git a/docs/CNAME b/docs/CNAME
new file mode 100644
index 00000000..46f786dd
--- /dev/null
+++ b/docs/CNAME
@@ -0,0 +1 @@
+presidio-research.dataprivacystack.org
\ No newline at end of file
diff --git a/docs/adr/ADR-002-entity-mapping.md b/docs/adr/ADR-002-entity-mapping.md
index 4b9d35f2..eef1c790 100644
--- a/docs/adr/ADR-002-entity-mapping.md
+++ b/docs/adr/ADR-002-entity-mapping.md
@@ -18,6 +18,7 @@ The core use case is **comparing multiple models against the same dataset**. The
the evaluation contract; models are the variable.
A flat `dict[str, str]` mapping is insufficient because:
+
- Many labels are aliases for the same concept (`FIRST_NAME`, `NAME_GIVEN`, `GIVENNAME` → `NAME`). Maintaining a hand-crafted dict for hundreds of model vocabularies is burdensome.
- Labels exist in a hierarchy — `NAME` is a sub-type of `PERSON`. A model predicting `PERSON` on a `NAME`-annotated token is partially correct, not wrong. A flat dict cannot express this.
- Unresolved labels need to be surfaced and triaged before evaluation; a dict silently drops or mismaps them.
@@ -60,7 +61,7 @@ BIO/BIOES/BILOU prefixes are stripped transparently before lookup (`B-PERSON`
| **COLLISION_CROSS_BRANCH** | WARNING | A prediction label and annotation label co-occur on the same tokens but map to different hierarchy branches. This may be a **vocabulary mismatch** (e.g., the model calls it `ORG` while the dataset calls it `COMPANY`) that can be fixed with `map()`. Even when not remapped, it is surfaced so the user is aware of the mismatch when interpreting results. | Surfaced in audit table with token counts; does not block |
| **PREDICTION_ONLY** | WARNING | Prediction entity in hierarchy but never annotated by the dataset | Surfaced in audit table; does not block |
| **DATASET_ONLY** | WARNING | Annotation entity never predicted by the model (nor any descendant) | Surfaced in audit table; does not block |
-| **COLLISION_SAME_BRANCH** | INFO | A prediction label and annotation label co-occur on the same tokens and map to the **same hierarchy branch** but at different depths (e.g., model predicts `PERSON`, dataset annotates `NAME`). This is not a mapping problem — it is handled correctly by the hierarchical evaluation step (see ADR-003). | Shown inline in audit table for awareness; does not block |
+| **COLLISION_SAME_BRANCH** | INFO | Annotation and prediction labels use the **same hierarchy branch** at different depths. `CanonicalMapper` projects each prediction to the deepest annotated ancestor of its own label, so this is never a mapping decision — including when the annotations themselves mix depths on one branch. | Surfaced in audit table for awareness; does not block |
Issues are ordered by severity (ERROR > WARNING > INFO), then by affected token count (descending).
@@ -76,6 +77,7 @@ By default, issues at WARNING level and above are surfaced. The user can control
`get_mapped_results_dataframe()` raises `IncompleteMapping` only if `UNRESOLVED` issues remain.
To resolve:
+
- `mapper.map({"MY_LABEL": "CANONICAL"})` — map to a known hierarchy entity
- `mapper.map({"MY_LABEL": None})` — suppress from evaluation entirely
diff --git a/docs/adr/ADR-003-hierarchical-evaluation.md b/docs/adr/ADR-003-hierarchical-evaluation.md
index 039bcffb..7ee4829d 100644
--- a/docs/adr/ADR-003-hierarchical-evaluation.md
+++ b/docs/adr/ADR-003-hierarchical-evaluation.md
@@ -2,12 +2,39 @@
## Status
-Proposed
+Superseded
## Date
2026-04-29 (revised 2026-04-30)
+## Superseding implementation
+
+The evaluator-side descendant-credit design below was not adopted. Hierarchy-aware
+granularity projection is implemented in `CanonicalMapper` instead, keeping
+`SpanEvaluator` and `TokenEvaluator` independent of the hierarchy.
+
+**The projection rule: the gold annotation decides the granularity.** Each prediction
+is projected to the **deepest annotated ancestor-or-self of its own label**; if the
+prediction has no annotated ancestor, it is left unchanged.
+
+Because the walk follows the prediction's own ancestor chain, the rule is per
+prediction rather than per branch:
+
+- A prediction more specific than the gold is credited to the gold label it falls
+ under (`NAME` → `PERSON` when the dataset annotates `PERSON`).
+- A prediction coarser than the gold is never pushed downward and stays a
+ detailed-level mismatch (`PERSON` over a `NAME` gold).
+- Siblings are never conflated, because neither is an ancestor of the other
+ (`DATE` never credits a `TIME` gold).
+- Mixed annotation depths on one branch need no decision. When a dataset annotates
+ both `PERSON` (depth 2) and `TITLE` (depth 3), a `TITLE` prediction stays `TITLE`
+ and a `NAME` prediction becomes `PERSON`, so each annotated depth keeps its own
+ metrics. This is reported as an INFO issue only.
+- Low-IoU errors are attributed to the projected scoring label.
+
+The remainder of this ADR is retained as the original proposal and rationale.
+
## Context
The current evaluation pipeline produces a single F1 score per entity type. Before evaluation
@@ -72,6 +99,7 @@ At each level L, for each token:
4. The prediction is **FP** if it is non-`O` when the annotation is `O`.
This means:
+
- A token annotated at depth 2 (`PERSON`) is evaluated at L2 using the depth-2 label — any prediction of `PERSON` or a descendant (`NAME`, `TITLE`, etc.) counts as TP.
- A token annotated at depth 3 (`TITLE`) requires the prediction to be `TITLE` or a descendant to score TP at L2; a prediction of `PERSON` (ancestor) is FP+FN.
@@ -79,6 +107,7 @@ Standard precision / recall / F1 apply at each level without modification. No ne
definitions are needed.
The levels correspond to the natural structure of `EntityHierarchy`:
+
- **L0**: PII vs. non-PII (binary)
- **L1**: depth-2 branch node (PERSON, LOCATION, CONTACT, …)
- **L2**: the annotation's own depth (depth-3 for depth-3 annotations; depth-2 for depth-2 annotations)
@@ -151,7 +180,9 @@ It further can shows the confusion matrix and error analysis for each level.
- **Three scores instead of one** — reporting becomes more complex. Users need guidance on which level to optimize for their use case.
- **Requires `EntityHierarchy` for every label** — labels that are `UNRESOLVED` cannot be placed at any level and must still be handled before multi-level evaluation can run.
- **L0 precision is trivially high for entity-rich models** — a model that fires on everything gets near-perfect L0 recall at the cost of precision; L0 alone is not enough. The multi-level view makes this visible rather than hiding it.
-- **Descendant-credit rule requires hierarchy access in SpanEvaluator** — a minor change to the evaluator is needed to pass the hierarchy and apply the rule.
+- **The original descendant-credit rule would have required hierarchy access in
+ `SpanEvaluator`** — this trade-off was avoided by moving projection into
+ `CanonicalMapper`.
## Alternatives Considered
diff --git a/docs/assets/dps-icon.svg b/docs/assets/dps-icon.svg
new file mode 100644
index 00000000..5e43e4c1
--- /dev/null
+++ b/docs/assets/dps-icon.svg
@@ -0,0 +1,65 @@
+
+
\ No newline at end of file
diff --git a/docs/data_generation.md b/docs/data_generation.md
new file mode 100644
index 00000000..5d798355
--- /dev/null
+++ b/docs/data_generation.md
@@ -0,0 +1,84 @@
+# Data Generation
+
+The `PresidioSentenceFaker` generates sentences from templates (e.g. `my name is {{person}}`) where the placeholders
+are replaced with fake PII entities, along with metadata about the spans (the start and end of each entity) for model training and evaluation.
+
+## Scenarios
+
+There are two main scenarios for using the `PresidioSentenceFaker`:
+
+1. Create a fake dataset for evaluation or training purposes, given a list of predefined templates
+(uses [this file](https://github.com/data-privacy-stack/presidio-research/blob/main/presidio_evaluator/data_generator/raw_data/templates.txt) by default)
+2. Augment an existing labeled dataset with additional fake values.
+
+In both scenarios the process is similar. In scenario 2, the existing dataset is first translated into templates,
+and then scenario 1 is applied.
+
+## Process
+
+This generator heavily relies on the [Faker package](https://www.github.com/joke2k/faker) with a few differences:
+
+1. `PresidioSentenceFaker` returns not only fake text, but also the spans in which fake entities appear in the text.
+2. `Faker` samples each value independently.
+In many cases, we would want to keep the semantic dependency between two values.
+For example, for the template `My name is {{name}} and my email is {{email}}`,
+we would prefer a result which has the name within the email address,
+such as `My name is Mike and my email is mike1243@gmail.com`.
+For this functionality, a new `RecordGenerator` (based on Faker's `Generator` class) is implemented.
+It accepts a dictionary / pandas DataFrame, and favors returning objects from the same record (if possible).
+
+## Example
+
+For a full example, see the [Generate Data notebook](notebooks/1_Generate_data.ipynb).
+
+`PresidioSentenceFaker` provides a high-level interface for using the full power of the `presidio_evaluator`
+package. Its results use the presidio PII entities, not the `Faker` entities.
+It is loaded by default with template strings, and the additional Presidio Entity Providers.
+
+```python
+from presidio_evaluator.data_generator import PresidioSentenceFaker
+
+record_generator = PresidioSentenceFaker(locale='en', lower_case_ratio=0.05)
+fake_records = record_generator.generate_new_fake_sentences(1500)
+
+# Print the spans of the first sample
+print(fake_records[0].fake)
+print(fake_records[0].spans)
+```
+
+The process at a high level is the following:
+
+1. Translate a NER dataset (e.g. CONLL or OntoNotes) into a list of
+templates: `My name is John` -> `My name is [PERSON]`
+2. Construct a `PresidioSentenceFaker` instance by:
+ - Choosing your appropriate locale, e.g. `en_US`
+ - Choosing the lower case ratio
+ - Passing in your list of templates (or default to those provided)
+ - Optionally extend with provided templates accessible via `from presidio_evaluator.data_generator import presidio_templates_file_path`
+ - Passing in any custom entity providers (or default to those provided)
+ - Optionally extend with inbuilt presidio entity providers accessible via `from presidio_evaluator.data_generator import presidio_additional_entity_providers`
+ - Adding a mapping from the output provider entity type to a Presidio recognized entity type where appropriate
+ - e.g. For a `TownProvider` which outputs entity type of `town`, execute `PresidioSentenceFaker.ENTITY_TYPE_MAPPING['town'] = 'GPE'`)
+ - Passing in a DataFrame representing your underlying PII records (or default to those provided)
+ - Optionally extend with inbuilt presidio entity providers accessible via `from presidio_evaluator.data_generator.faker_extensions.datasets import load_fake_person_df`
+ - Adding any additional aliases required by your dataset by adding to `PresidioSentenceFaker.PROVIDER_ALIASES`
+ - e.g. if the entity providers support "name" but your dataset templates contain "person", you can add this alias
+ with `PresidioSentenceFaker.PROVIDER_ALIASES['name'] = 'person'`)
+3. Generate sentences
+4. Split the generated dataset into train/test/validation while making sure
+that samples from the same template would only appear in one set
+5. Adapt datasets for the various models (Spacy, Flair, CRF, sklearn)
+6. Train models
+7. Evaluate using one of the [evaluation notebooks](https://github.com/data-privacy-stack/presidio-research/tree/main/notebooks/models)
+
+Notes:
+
+- For steps 5, 6, 7 see the [home page](index.md).
+
+
+*Copyright notice:*
+
+Fake Name Generator identities by the Fake Name Generator are licensed under a
+Creative Commons Attribution-Share Alike 3.0 United States License.
+Fake Name Generator and the Fake Name Generator logo
+are trademarks of Corban Works, LLC.
diff --git a/docs/entity_hierarchy.md b/docs/entity_hierarchy.md
index 68d65ccf..e1afb820 100644
--- a/docs/entity_hierarchy.md
+++ b/docs/entity_hierarchy.md
@@ -11,6 +11,7 @@ If you compare these labels directly, everything looks like a mismatch — even
Presidio Evaluator solves this with a **shared vocabulary** of canonical entity names. Every label — from any model or dataset — gets mapped to one of these canonical names before evaluation using a **two-phase process**:
**Phase 1 — Identify:** each label is matched to a canonical entity through five tiers (in priority order):
+
1. Exact match in the alias map
2. Country-prefix strip (e.g. `GERMANY_PASSPORT_NUMBER` → `PASSPORT`)
3. Country-prefix fallback (tries removing leading country code)
@@ -18,18 +19,79 @@ Presidio Evaluator solves this with a **shared vocabulary** of canonical entity
5. `UNRESOLVED` — flagged for manual resolution
Examples:
+
- `EMAIL`, `email_address`, `EMAILADDRESS` → all become `EMAIL_ADDRESS`
- `B-PERSON`, `PERSON-I` → BIO tags are stripped, both become `NAME`
- `GERMANY_PASSPORT_NUMBER` → country prefix is recognized, becomes `PASSPORT`
- `CREDITCARD`, `credit_card` → case and delimiters don't matter, becomes `FINANCIAL`
-**Phase 2 — Project:** canonical entities are projected onto the *canonical surface* — a set of entities at a
-depth computed by majority vote from the **dataset annotation labels**. Depth-2 ancestors that have
-multiple depth-3 descendants trigger a `COLLISION_AMBIGUOUS` issue; descendants that have exactly
-one matching ancestor on the canonical surface are auto-fixed as `COLLISION_TRIVIAL`.
+**Phase 2 — Project:** each canonical prediction is projected upward to the **deepest dataset
+annotation label that is an ancestor-or-self of it**. For example, `NAME` is projected to `PERSON`
+when the annotations use `PERSON`. A prediction with no annotated ancestor is left unchanged, so a
+less-specific prediction is never projected downward and sibling entities are never conflated.
+Annotations that mix depths on one branch need no decision — they are reported as
+`COLLISION_SAME_BRANCH` (INFO). A label on a different branch is reported as
+`COLLISION_CROSS_BRANCH` (WARNING).
Labels that can't be resolved automatically are flagged for you to handle manually.
+## Aliases
+
+An alias is a raw label that resolves to a canonical entity. Aliases are how the same concept written
+different ways (`EMAIL`, `email_address`, `EMAILADDRESS`) all reach one canonical name. Matching is
+normalized — case, underscores, dashes and BIO prefixes are ignored.
+
+**Leaf aliases** are declared as the list value of a leaf node:
+
+```python
+"EMAIL_ADDRESS": ["EMAIL", "EMAILADDRESS", "E_MAIL"]
+```
+
+**Branch aliases** are declared with the reserved `_aliases` key, so a non-leaf node can carry synonyms
+of its own:
+
+```python
+"LOCATION": {
+ "_aliases": ["LOC"], # LOC resolves to LOCATION itself
+ "ADDRESS": {...},
+ "GPE": [...],
+}
+```
+
+This matters for coarse corpora. TAB, for example, labels entities `LOC`/`ORG`/`PER`; without branch
+aliases those could only ever match a model's `LOCATION`/`ORGANIZATION`/`PERSON` at the *branch*
+level, understating exact-level scores. The reserved key is skipped by every tree-walk, so `_aliases`
+never becomes an entity itself.
+
+**Adding an alias at runtime** — works for both leaf and branch nodes, and the target may itself be
+given as an alias:
+
+```python
+h = EntityHierarchy()
+h.add_alias("EMAIL_ADDRESS", "ELECTRONIC_MAIL") # leaf
+h.canonicalize("ELECTRONIC_MAIL") # -> 'EMAIL_ADDRESS'
+
+h.add_alias("LOCATION", "GEOGRAPHIC_LOCATION") # branch -> stored under _aliases
+h.add_alias("LOC", "LOCALITY") # target given as an alias
+h.canonicalize("LOCALITY") # -> 'LOCATION'
+```
+
+`add_alias()` raises `ValueError` if the alias is already claimed by a descendant of the target — branch
+aliases are applied before the descent into their own subtree, so a descendant would win and the alias
+would never resolve. The hierarchy is left unchanged in that case. Declaring such a collision statically
+in `definitions.py` logs a warning at construction time.
+
+**Looking up an alias** — `canonicalize()`, `to_branch()` and `get_depth()` all accept raw aliases:
+
+```python
+h.canonicalize("LOC") # -> 'LOCATION'
+h.to_branch("LOC") # -> 'LOCATION'
+h.get_depth("LOC") # -> 2
+```
+
+Note that aliases are not canonical entities: `LOC` does not appear in `all_canonical_entities`, and it
+cannot be used as a `map()` *target*.
+
## The canonical vocabulary
The canonical entities are organized in a hierarchy. At the default evaluation level (depth 3), there are entities like `EMAIL_ADDRESS`, `PASSPORT`, `NAME`, `STREET_ADDRESS`, etc. These sit under broader categories:
@@ -47,11 +109,13 @@ The canonical entities are organized in a hierarchy. At the default evaluation l
(Full list: `DEMOGRAPHIC`, `EMPLOYMENT`, `DEVICE_IDENTIFIER`, `BIOMETRIC`, `NETWORK_IDENTIFIER`, `AUTHENTICATION`, `VEHICLE_PII`, `LEGAL_PII`, `TRAVEL_PII`, `EDUCATION`)
-The **evaluation depth is data-driven**: `CanonicalMapper` computes a weighted majority vote across the
-annotation labels in your results DataFrame and selects depth 2 or 3 (capped at 3). Depth 3 is the most
-common outcome when a dataset uses fine-grained entity types like `EMAIL_ADDRESS`, `NAME`, or `SSN`.
+The **evaluation granularity is annotation-driven and decided per prediction**. `CanonicalMapper`
+projects each prediction to the deepest annotated ancestor of its own label. A branch may therefore
+carry several annotated depths at once: with `PERSON` and `TITLE` both annotated, a `TITLE` prediction
+stays `TITLE` while a `NAME` prediction becomes `PERSON`, so each annotated depth keeps its own
+metrics and no mapping decision is required.
-For more on why this approach was chosen over alternatives, see [why_canonical_entity_mapping.md](why_canonical_entity_mapping.md).
+For more on why this approach was chosen over alternatives, see [Why canonical entity mapping](why_canonical_entity_mapping.md).
## Typical workflow
@@ -69,12 +133,12 @@ mapper.render_html() # visual overview in Jupyter
for issue in mapper.get_issues():
print(f"[{issue.severity.value}] {issue.type.value}: {issue.labels}")
-# 4. Fix WARNING/ERROR issues before extracting the DataFrame
+# 4. Fix ERROR issues before extracting the DataFrame
# map to a canonical entity, or None to suppress
mapper.map({"MY_CUSTOM_LABEL": "EMAIL_ADDRESS"})
mapper.map({"JUNK_LABEL": None}) # None = exclude from evaluation
-# 5. get_mapped_results_dataframe() raises IncompleteMapping if WARNING/ERROR issues remain
+# 5. get_mapped_results_dataframe() raises IncompleteMapping if ERROR issues remain
try:
mapped_df = mapper.get_mapped_results_dataframe()
except IncompleteMapping:
@@ -93,14 +157,16 @@ mapper.map({"MY_CUSTOM_LABEL": "EMAIL_ADDRESS", "JUNK_LABEL": None})
mapper.analyze(results_df)
```
-**Multi-model comparison:** the canonical surface (set of entities used for evaluation) is locked after the
-first `analyze()` call. Subsequent `analyze()` calls for other models reuse the same surface, ensuring
-all models are evaluated on the same entity set:
+**Multi-model comparison:** analyze each model against the same annotation vocabulary. Each `analyze()`
+call resets inferred state while preserving explicit `map()` decisions, so the annotations independently
+define the same branch-specific projection targets:
```python
mapper = CanonicalMapper()
-mapper.analyze(model_a_df) # locks canonical surface from dataset annotations
-mapper.analyze(model_b_df) # reuses the same locked canonical surface
+mapper.analyze(model_a_df)
+model_a_results = mapper.get_mapped_results_dataframe()
+mapper.analyze(model_b_df)
+model_b_results = mapper.get_mapped_results_dataframe()
```
---
@@ -112,13 +178,12 @@ When you call `mapper.analyze(df)`, the mapper checks for problems that could si
| Type | Severity | Meaning |
|------|----------|---------|
| `UNRESOLVED` | ERROR | Label could not be matched — blocks `get_mapped_results_dataframe()` |
-| `COLLISION_AMBIGUOUS` | WARNING | Depth-2 label maps to multiple depth-3 entities — blocks extraction |
-| `COLLISION_CROSS_BRANCH` | WARNING | Label maps across hierarchy branches — blocks extraction |
-| `PREDICTION_ONLY` | WARNING | Label only in predictions, not dataset — blocks extraction |
-| `COLLISION_TRIVIAL` | INFO | Auto-fixed: descendant collapsed to single ancestor |
+| `COLLISION_CROSS_BRANCH` | WARNING | Label maps across hierarchy branches — review before evaluation |
+| `PREDICTION_ONLY` | WARNING | Label only in predictions, not dataset — review before evaluation |
+| `COLLISION_SAME_BRANCH` | INFO | Different depths on one branch — handled automatically by projecting each prediction to its deepest annotated ancestor |
| `DATASET_ONLY` | INFO | Label only in dataset annotations (model never predicts it) |
-> **Warning**: `get_mapped_results_dataframe()` raises `IncompleteMapping` if any WARNING or ERROR issues remain. INFO issues are non-blocking.
+> **Warning**: `get_mapped_results_dataframe()` raises `IncompleteMapping` if any ERROR issues remain. WARNING and INFO issues are non-blocking but should still be reviewed.
### 1. A label could not be resolved to any canonical entity
@@ -157,10 +222,10 @@ mapper.map({"MY_LOCATION": "ADDRESS"}) # go specific
mapper.map({"MY_CITY": "LOCATION"}) # go broad
```
-**Fix 2. — align to the depth the canonical surface uses.** The canonical surface is computed automatically
-from the dataset annotations. If your dataset uses depth-2 labels (e.g. `PERSON`), the canonical surface
-will be at depth 2, and depth-3 model labels will auto-collapse. If the dataset uses depth-3 labels,
-use `map()` to remap the model's depth-2 label to a specific depth-3 target:
+**Fix 2. — align annotation depth on the branch.** If your dataset uses a single depth-2 label
+(e.g. `PERSON`) on a branch, deeper model labels auto-collapse to it. If annotations use depth 3,
+a depth-2 prediction is intentionally not projected downward; use `map()` only when you can choose
+the correct specific target:
```python
mapper.map({"PERSON": "NAME"}) # pick the right depth-3 entity
```
@@ -196,15 +261,14 @@ mapper.map({"STREET_NUMBER": None})
**Fix 2. — add the missing annotations:** If the model is actually finding real PII that was missed during annotation, the right fix is to go back and annotate those spans.
-### 5. The canonical surface is locked — a new model uses different entities
+### 5. A new model uses different entities
-The canonical surface (set of entities used for evaluation) is **locked after the first `analyze()` call**.
-This ensures all models are evaluated on the same entity set for fair comparison.
+Each `analyze()` call derives projection targets from that DataFrame's annotations. Explicit mappings
+created with `map()` are preserved, but inferred labels and issues are recalculated.
-**Example:** You evaluated model A and locked the surface at depth 3. Model B predicts a label that
-would have changed the canonical depth if analyzed alone. It's projected onto the existing locked surface.
+**Example:** Model B predicts a new label that model A did not use. It is resolved against the hierarchy
+and projected only when it is more specific than the single annotation depth on its branch.
-**This is intentional.** The dataset annotations define the ground truth, so the canonical surface is
-anchored to the first model's dataset. If model B has labels that don't fit the surface, they appear
-as `COLLISION_AMBIGUOUS` or `PREDICTION_ONLY` issues — resolve them with `map()` before extracting
-results.
+For fair comparisons, pass the same gold annotations for every model and apply the same explicit mapping
+policy. New model labels may appear as `COLLISION_CROSS_BRANCH` or `PREDICTION_ONLY` warnings; review
+them and use `map()` when a manual decision is required.
diff --git a/docs/mapping_scenarios.md b/docs/mapping_scenarios.md
index 6b020374..b96eeb8a 100644
--- a/docs/mapping_scenarios.md
+++ b/docs/mapping_scenarios.md
@@ -32,9 +32,9 @@ The model emits thousands of fine-grained labels; the dataset uses ≤10 broad c
**Real example (Notebook 5):** The OpenMed HuggingFace model predicts `PERSON`, `LOCATION`, `ORGANIZATION`, etc., while the synth dataset labels `city`, `country`, `street_address`, `state`, `county`, `coordinate`, `postcode` as separate entities.
-**Issue type produced:** `COLLISION_AMBIGUOUS` (WARNING) — blocking. When a depth-2 ancestor like `LOCATION` is seen in predictions but the canonical surface is at depth 3 (computed by majority vote from the dataset annotations), the mapper can't determine which depth-3 entity to project onto. The user must call `map({'LOCATION': 'LOC'})` (or another appropriate target) to resolve.
+**Issue type produced:** `COLLISION_SAME_BRANCH` (INFO) — non-blocking. Less-specific predictions such as `LOCATION` remain mismatches at the detailed level because the mapper never projects downward.
-**Projection rules in action:** If instead the dataset uses depth-2 labels and the canonical surface locks at depth 2, then fine-grained model labels like `STREET_ADDRESS` auto-collapse to `LOCATION` as `COLLISION_TRIVIAL` (INFO, non-blocking).
+**Projection rules in action:** If the dataset uses depth-2 labels, fine-grained model labels like `STREET_ADDRESS` auto-collapse to `LOCATION` — also reported as `COLLISION_SAME_BRANCH` (INFO, non-blocking).
---
@@ -111,8 +111,9 @@ The same string alias appears under multiple canonical entities in the hierarchy
| `MRN` / `MEDICAL_RECORD_NUMBER` | `PHI → MRN` | alias of `PATIENT_ID` | Same concept, two canonical targets |
**Issue types produced:**
-- `COLLISION_CROSS_BRANCH` (WARNING) — blocking. Raised when a label resolves to a canonical entity that has co-occurring labels on the same tokens mapping to a different hierarchy branch. Must be resolved with `map()` before extracting results.
-- `COLLISION_AMBIGUOUS` (WARNING) — blocking. Raised when a depth-2 ancestor maps to multiple depth-3 entities on the canonical surface (the top co-occurring candidate is shown in `overlap_counts`). Use `map({'LABEL': 'CANONICAL'})` to pick the right one.
+
+- `COLLISION_CROSS_BRANCH` (WARNING) — non-blocking; review before evaluation. Raised when a label resolves to a canonical entity that has co-occurring labels on the same tokens mapping to a different hierarchy branch. Use `map()` if the mismatch is a vocabulary difference rather than a genuine model error.
+- `COLLISION_SAME_BRANCH` (INFO) — non-blocking. More-specific predictions are projected up to the deepest annotated ancestor of their own label. Mixed annotation depths on a branch are reported for awareness and need no `map()` decision.
---
@@ -130,7 +131,8 @@ The model finds PII types the dataset creators never labeled — every detection
**Real example (Notebook 5):** After mapping, several Presidio predictions had no dataset counterpart.
-**Issue type produced:** `PREDICTION_ONLY` (WARNING) — blocking. These labels inflate precision with false positives. You have three resolution options:
+**Issue type produced:** `PREDICTION_ONLY` (WARNING) — non-blocking; review before evaluation. These labels inflate precision with false positives. You have three resolution options:
+
1. **Suppress** — `mapper.map({'CREDIT_CARD': None})` excludes the label from evaluation entirely
2. **Remap** — `mapper.map({'CREDIT_CARD': 'FINANCIAL'})` counts detections against the `FINANCIAL` annotation set
3. **Keep as FP** — if you want these counted as false positives deliberately, this isn't directly supported; suppression is the recommended path
@@ -166,7 +168,7 @@ The dataset and model operate at different hierarchy depths, producing ancestor
**Real example (Notebook 5):** Dataset labels `city`, `street_address`, `postcode` (depth 3), model predicts `LOCATION` (depth 2).
-**How the new API handles this:** The canonical depth is computed automatically by majority vote from the dataset annotations. If the dataset is predominantly depth-3, the canonical surface is depth-3 and depth-2 model predictions trigger `COLLISION_AMBIGUOUS` (WARNING). Resolve with `map({'LOCATION': 'LOC'})` to pick the right depth-3 target.
+**How the new API handles this:** The dataset uses depth-3 annotations, so the depth-2 `LOCATION` prediction is reported as `COLLISION_SAME_BRANCH` (INFO). It matches at the branch level but remains a mismatch at the detailed level because predictions are not projected downward.
---
diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt
new file mode 100644
index 00000000..9d9665d6
--- /dev/null
+++ b/docs/requirements-docs.txt
@@ -0,0 +1,20 @@
+# Dependencies for building the Presidio-Research documentation with Zensical,
+# the successor to Material for MkDocs. Build with scripts/zensical_build.py,
+# which collects the README (-> index.md) and the notebook tutorials into a
+# staging docs tree, pre-converts the notebooks, and emits a generated
+# zensical.yml. See that script's module docstring for the full flow.
+#
+# pip install -r docs/requirements-docs.txt
+# python scripts/zensical_build.py # build to ./site
+# python scripts/zensical_build.py serve # local preview
+#
+# Zensical does not run MkDocs plugins, so mkdocs / mkdocs-material /
+# mkdocs-jupyter are intentionally absent. nbconvert renders the notebooks to
+# Markdown (no execution), which keeps the build light — the heavy
+# presidio_evaluator ML dependencies are not needed to build the docs.
+zensical
+nbconvert
+pymdown-extensions
+markdown
+pyyaml
+pygments>=2.10
diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css
new file mode 100644
index 00000000..948039e3
--- /dev/null
+++ b/docs/stylesheets/extra.css
@@ -0,0 +1,604 @@
+/* =========================================================================
+ Data Privacy Stack — Presidio docs theme
+ Violet/indigo brand built around the "Inky" octopus mascot.
+ Light-first, calm neutral canvas; the mascot carries the colour.
+ Type: Plus Jakarta Sans (display), Montserrat (brand byline) + Inter (body).
+ ========================================================================= */
+
+@import url("https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@500;600;700;800&family=Montserrat:wght@400;600&family=Inter:wght@400;500;600;700&display=swap");
+
+/* ---- Brand tokens ------------------------------------------------------- */
+:root {
+ --dps-ink: #11131a;
+ --dps-navy: #131d4a; /* octopus ink — chrome / header */
+ --dps-navy-light: #2a356b;
+ --dps-navy-dark: #0d1437;
+ --dps-indigo: #4f46e5; /* primary link accent */
+ --dps-indigo-dark: #4338ca;
+ --dps-violet: #7c3aed; /* interactive accent */
+ --dps-violet-bright: #8957e8;
+ --dps-cyan: #22d3ee;
+ --dps-muted: #5b6472;
+ --dps-bg: #fbfbfd;
+ --dps-card: #ffffff;
+ --dps-border: #e6e8ee;
+
+ --dps-gradient: linear-gradient(135deg, #7c8be8 0%, #8957e8 100%);
+ --dps-gradient-text: linear-gradient(120deg, #7d4dcb 0%, #6366f1 55%, #81c0f1 100%);
+ --dps-radius: 14px;
+ --dps-shadow: 0 1px 2px rgba(17, 19, 26, 0.04), 0 14px 34px rgba(17, 19, 26, 0.09);
+
+ --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
+ --ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
+
+ --md-text-font: "Inter";
+}
+
+/* =========================================================================
+ 1. Light scheme (default)
+ ========================================================================= */
+[data-md-color-scheme="default"] {
+ --md-primary-fg-color: var(--dps-navy);
+ --md-primary-fg-color--light: var(--dps-navy-light);
+ --md-primary-fg-color--dark: var(--dps-navy-dark);
+ --md-primary-bg-color: #ffffff;
+ --md-primary-bg-color--light: rgba(255, 255, 255, 0.72);
+
+ --md-accent-fg-color: var(--dps-violet);
+ --md-accent-fg-color--transparent: rgba(124, 58, 237, 0.1);
+
+ --md-default-bg-color: var(--dps-bg);
+ --md-default-fg-color: var(--dps-ink);
+ --md-default-fg-color--light: #3a4252;
+ --md-default-fg-color--lighter: var(--dps-muted);
+ --md-default-fg-color--lightest: var(--dps-border);
+
+ --md-typeset-a-color: var(--dps-indigo);
+ --md-typeset-mark-color: rgba(137, 87, 232, 0.22);
+
+ --md-code-bg-color: #f4f4f9;
+ --md-code-fg-color: #2a2f45;
+
+ --md-footer-bg-color: var(--dps-navy-dark);
+ --md-footer-bg-color--dark: #0a0f24;
+}
+
+/* =========================================================================
+ 2. Dark scheme (slate)
+ ========================================================================= */
+[data-md-color-scheme="slate"] {
+ --md-hue: 230;
+
+ --md-primary-fg-color: #0d142e;
+ --md-primary-fg-color--light: #1a2452;
+ --md-primary-fg-color--dark: #080d20;
+ --md-primary-bg-color: #f3f4ff;
+
+ --md-accent-fg-color: #a99dff;
+ --md-accent-fg-color--transparent: rgba(169, 157, 255, 0.12);
+
+ --md-default-bg-color: #0e1116;
+ --md-default-fg-color: #e6e8ef;
+ --md-default-fg-color--light: #c2c7d4;
+ --md-default-fg-color--lighter: #9aa3b2;
+ --md-default-fg-color--lightest: #2a3344;
+
+ --md-typeset-a-color: #8b83ff;
+ --md-typeset-mark-color: rgba(139, 131, 255, 0.28);
+
+ --md-code-bg-color: #161b24;
+ --md-code-fg-color: #d6dae6;
+
+ --md-footer-bg-color: #0a0d12;
+ --md-footer-bg-color--dark: #07090d;
+}
+
+/* dark surface cards */
+[data-md-color-scheme="slate"] {
+ --dps-card: #161b24;
+ --dps-border: #222a38;
+ --dps-bg: #0e1116;
+ --dps-muted: #9aa3b2;
+ --dps-shadow: 0 1px 2px rgba(0, 0, 0, 0.3), 0 16px 40px rgba(0, 0, 0, 0.45);
+}
+
+/* =========================================================================
+ 3. Typography
+ ========================================================================= */
+body,
+.md-typeset {
+ font-feature-settings: "kern", "liga", "calt";
+ -webkit-font-smoothing: antialiased;
+}
+
+.md-typeset h1,
+.md-typeset h2,
+.md-typeset h3,
+.md-typeset h4,
+.md-header__title,
+.dps-hero__title {
+ font-family: "Plus Jakarta Sans", "Inter", "Segoe UI", sans-serif;
+ letter-spacing: -0.02em;
+}
+
+.md-typeset h1 {
+ font-weight: 700;
+ color: var(--md-default-fg-color);
+}
+
+.md-typeset h2 {
+ font-weight: 700;
+ margin-top: 2.2em;
+}
+
+/* =========================================================================
+ 4. Header / nav chrome
+ ========================================================================= */
+.md-header {
+ box-shadow: 0 1px 0 rgba(255, 255, 255, 0.06);
+}
+
+.md-header__button.md-logo img {
+ height: 1.7rem;
+ width: auto;
+}
+
+.md-tabs {
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
+}
+
+.md-tabs__link {
+ opacity: 0.78;
+ transition: opacity 200ms var(--ease-out);
+}
+
+.md-tabs__link:hover,
+.md-tabs__link--active {
+ opacity: 1;
+}
+
+/* Active nav item accent in sidebar */
+.md-nav__link--active,
+.md-nav__item .md-nav__link--active {
+ color: var(--md-typeset-a-color);
+ font-weight: 600;
+}
+
+/* =========================================================================
+ 5. Links, focus, selection
+ ========================================================================= */
+.md-typeset a {
+ text-decoration-color: color-mix(in srgb, var(--md-typeset-a-color) 35%, transparent);
+ text-underline-offset: 2px;
+ transition: color 160ms var(--ease-out);
+}
+
+.md-typeset a:hover {
+ color: var(--md-accent-fg-color);
+}
+
+*:focus-visible {
+ outline: 2px solid var(--md-accent-fg-color);
+ outline-offset: 2px;
+ border-radius: 3px;
+}
+
+::selection {
+ background: rgba(137, 87, 232, 0.22);
+}
+
+/* =========================================================================
+ 6. Buttons
+ ========================================================================= */
+.md-typeset .md-button {
+ border-radius: 10px;
+ border-width: 1px;
+ font-weight: 600;
+ padding: 0.5em 1.15em;
+ transition: transform 180ms var(--ease-out), box-shadow 180ms var(--ease-out),
+ background-color 180ms var(--ease-out), color 180ms var(--ease-out);
+}
+
+.md-typeset .md-button--primary {
+ background: var(--dps-violet);
+ border-color: var(--dps-violet);
+ color: #fff;
+}
+
+.md-typeset .md-button--primary:hover {
+ background: var(--dps-indigo-dark);
+ border-color: var(--dps-indigo-dark);
+ box-shadow: 0 8px 22px rgba(79, 70, 229, 0.32);
+}
+
+.md-typeset .md-button:not(.md-button--primary):hover {
+ border-color: var(--md-accent-fg-color);
+ color: var(--md-accent-fg-color);
+ background: var(--md-accent-fg-color--transparent);
+}
+
+/* =========================================================================
+ 7. Code, tables, admonitions, cards
+ ========================================================================= */
+.md-typeset pre > code,
+.md-typeset .highlight {
+ border-radius: 12px;
+}
+
+.md-typeset code {
+ border-radius: 5px;
+ font-size: 0.84em;
+}
+
+.md-typeset table:not([class]) {
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: 0 0 0 1px var(--dps-border);
+ border: none;
+}
+
+.md-typeset table:not([class]) th {
+ background: color-mix(in srgb, var(--md-primary-fg-color) 6%, transparent);
+ font-weight: 600;
+}
+
+.md-typeset .admonition,
+.md-typeset details {
+ border-radius: var(--dps-radius);
+ border-left-width: 3px;
+ box-shadow: var(--dps-shadow);
+}
+
+/* Material "grid cards" polish */
+.md-typeset .grid.cards > ul > li,
+.md-typeset .grid > .card {
+ border: 1px solid var(--dps-border);
+ border-radius: var(--dps-radius);
+ background: var(--dps-card);
+ box-shadow: var(--dps-shadow);
+ transition: border-color 200ms var(--ease-out), box-shadow 200ms var(--ease-out);
+}
+
+.md-typeset .grid.cards > ul > li:hover {
+ border-color: color-mix(in srgb, var(--md-accent-fg-color) 55%, var(--dps-border));
+ box-shadow: 0 2px 4px rgba(17, 19, 26, 0.05), 0 18px 44px rgba(79, 70, 229, 0.12);
+}
+
+/* =========================================================================
+ 8. Footer
+ ========================================================================= */
+.md-footer-meta {
+ background: var(--md-footer-bg-color--dark);
+ --md-default-fg-color: var(--md-primary-bg-color);
+ --md-default-fg-color--light: color-mix(in srgb, var(--md-primary-bg-color) 72%, transparent);
+}
+
+/* =========================================================================
+ 9. Home hero
+ ========================================================================= */
+.dps-hero {
+ display: grid;
+ grid-template-columns: minmax(0, 150px) minmax(0, 1fr);
+ align-items: center;
+ gap: 1.75rem;
+ margin: 0.5rem 0 3rem;
+ padding: 1.75rem 2.5rem;
+ border-radius: 22px;
+ position: relative;
+ overflow: hidden;
+ background:
+ radial-gradient(120% 140% at 100% 0%, rgba(137, 87, 232, 0.12) 0%, transparent 55%),
+ radial-gradient(120% 140% at 0% 100%, rgba(34, 211, 238, 0.1) 0%, transparent 50%),
+ var(--dps-card);
+ border: 1px solid var(--dps-border);
+ box-shadow: var(--dps-shadow);
+}
+
+.dps-hero__eyebrow {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.45rem;
+ font-family: "Inter", sans-serif;
+ font-size: 0.74rem;
+ font-weight: 600;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ color: var(--dps-violet);
+ background: rgba(137, 87, 232, 0.1);
+ border: 1px solid rgba(137, 87, 232, 0.22);
+ padding: 0.3rem 0.7rem;
+ border-radius: 999px;
+}
+
+[data-md-color-scheme="slate"] .dps-hero__eyebrow {
+ color: #c7c2ff;
+ background: rgba(169, 157, 255, 0.12);
+ border-color: rgba(169, 157, 255, 0.28);
+}
+
+.dps-hero__body {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 0.4rem;
+ container-type: inline-size;
+ min-width: 0;
+}
+
+.md-typeset .dps-hero__title {
+ font-family: "Plus Jakarta Sans", "Inter", "Segoe UI", sans-serif;
+ font-size: clamp(2.5rem, 11vw, 7rem);
+ font-size: clamp(2.5rem, 21cqi, 8rem);
+ line-height: 0.9;
+ font-weight: 700;
+ letter-spacing: -0.045em;
+ margin: 0;
+ background: var(--dps-gradient-text);
+ -webkit-background-clip: text;
+ background-clip: text;
+ -webkit-text-fill-color: transparent;
+ color: transparent;
+}
+
+.md-typeset .dps-hero__byline {
+ display: inline-flex;
+ align-items: baseline;
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: 0.38em;
+ max-width: 100%;
+ font-family: "Montserrat", "Inter", sans-serif;
+ font-size: clamp(0.95rem, 4.6cqi, 1.3rem);
+ font-weight: 400;
+ letter-spacing: 0.005em;
+ white-space: nowrap;
+ margin: 0;
+ color: var(--md-default-fg-color--lighter);
+}
+
+.md-typeset .dps-hero__byline-brand {
+ font-family: "Montserrat", "Inter", sans-serif;
+ font-weight: 600;
+ letter-spacing: 0.02em;
+ color: var(--md-default-fg-color--light);
+}
+
+.dps-hero__tagline {
+ font-size: 1.08rem;
+ line-height: 1.55;
+ color: var(--md-default-fg-color--light);
+ max-width: 34ch;
+ margin: 0 0 1.6rem;
+}
+
+.dps-hero__cta {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.75rem;
+}
+
+.dps-hero__cta .md-button {
+ margin: 0;
+}
+
+.dps-hero__media {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.md-typeset .dps-hero__mascot {
+ width: 100%;
+ max-width: 140px;
+ height: auto;
+ filter: drop-shadow(0 18px 32px rgba(79, 70, 229, 0.26));
+ animation: dps-float 6s var(--ease-in-out) infinite;
+ will-change: transform;
+}
+
+@keyframes dps-float {
+ 0%, 100% { transform: translateY(0) rotate(-0.6deg); }
+ 50% { transform: translateY(-14px) rotate(0.6deg); }
+}
+
+/* Feature strip under the hero */
+.dps-features {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 1rem;
+ margin: 0 0 2.5rem;
+}
+
+.dps-feature {
+ border: 1px solid var(--dps-border);
+ border-radius: var(--dps-radius);
+ background: var(--dps-card);
+ padding: 1.25rem 1.25rem 1.1rem;
+ box-shadow: var(--dps-shadow);
+ transition: transform 200ms var(--ease-out), border-color 200ms var(--ease-out),
+ box-shadow 200ms var(--ease-out);
+}
+
+.dps-feature:hover {
+ border-color: color-mix(in srgb, var(--md-accent-fg-color) 55%, var(--dps-border));
+ box-shadow: 0 2px 4px rgba(17, 19, 26, 0.05), 0 18px 44px rgba(79, 70, 229, 0.12);
+}
+
+.dps-feature__icon {
+ display: inline-flex;
+ width: 2.3rem;
+ height: 2.3rem;
+ align-items: center;
+ justify-content: center;
+ border-radius: 10px;
+ background: var(--dps-gradient);
+ color: #fff;
+ margin-bottom: 0.7rem;
+}
+
+.dps-feature__icon svg {
+ width: 1.25rem;
+ height: 1.25rem;
+}
+
+.dps-feature h3 {
+ font-size: 1rem;
+ margin: 0 0 0.3rem;
+ font-weight: 700;
+}
+
+.dps-feature p {
+ font-size: 0.86rem;
+ line-height: 1.5;
+ color: var(--md-default-fg-color--lighter);
+ margin: 0;
+}
+
+/* =========================================================================
+ 10. Responsive
+ ========================================================================= */
+@media screen and (max-width: 76.1875em) {
+ .dps-hero {
+ grid-template-columns: 1fr;
+ text-align: center;
+ padding: 2.25rem 1.5rem;
+ gap: 1.5rem;
+ }
+ .dps-hero__media { order: -1; }
+ .md-typeset .dps-hero__mascot { max-width: 120px; }
+ .dps-hero__tagline { margin-inline: auto; }
+ .dps-hero__cta { justify-content: center; }
+ .dps-hero__body { align-items: center; }
+ .dps-features { grid-template-columns: 1fr; }
+}
+
+@media screen and (max-width: 44.984375em) {
+ .dps-hero {
+ grid-template-columns: 1fr;
+ text-align: center;
+ gap: 0.35rem;
+ margin: 0.25rem 0 1.35rem;
+ padding: 0.85rem 1rem;
+ border-radius: 18px;
+ }
+
+ .dps-hero__media {
+ display: flex;
+ order: -1;
+ }
+
+ .md-typeset .dps-hero__mascot {
+ max-width: 48px;
+ filter: drop-shadow(0 8px 14px rgba(79, 70, 229, 0.18));
+ }
+
+ .dps-hero__body {
+ align-items: center;
+ gap: 0.25rem;
+ }
+
+ .md-typeset .dps-hero__title {
+ font-size: clamp(2rem, 11vw, 2.65rem);
+ line-height: 0.95;
+ }
+
+ .md-typeset .dps-hero__byline {
+ font-size: clamp(0.75rem, 3.8vw, 0.95rem);
+ line-height: 1.25;
+ white-space: normal;
+ }
+}
+
+/* =========================================================================
+ 11. Motion preferences
+ ========================================================================= */
+@media (prefers-reduced-motion: reduce) {
+ .dps-hero__mascot { animation: none; }
+ *,
+ *::before,
+ *::after {
+ animation-duration: 0.001ms !important;
+ transition-duration: 0.001ms !important;
+ }
+}
+
+/* =========================================================================
+ 12. Converted notebook content (Jupyter → Markdown under Zensical)
+ mkdocs-jupyter used to ship Jupyter's own CSS. Zensical renders the
+ notebook Markdown natively, so pandas tables, figures and cell outputs
+ are styled here with the brand tokens — matching the scheme light + dark.
+ ========================================================================= */
+
+/* Responsive images: safety net so wide plots / DICOM frames never overflow */
+.md-typeset img {
+ max-width: 100%;
+ height: auto;
+}
+
+/* Notebook figure outputs (matplotlib / DICOM): frame them so they read as
+ intentional and stay legible against the dark canvas. */
+.md-typeset img[alt="png"],
+.md-typeset img[alt="jpeg"],
+.md-typeset img[alt="svg"] {
+ display: block;
+ margin: 1.3rem auto;
+ padding: 0.65rem;
+ background: #ffffff;
+ border: 1px solid var(--dps-border);
+ border-radius: 12px;
+ box-shadow: var(--dps-shadow);
+}
+
+/* pandas DataFrame tables: they ship as
.
+ Reset the legacy bordered look, adopt the site table treatment, and scroll
+ horizontally so wide frames never break the layout on narrow viewports. */
+.md-typeset table.dataframe {
+ display: block;
+ width: max-content;
+ max-width: 100%;
+ overflow-x: auto;
+ border: 1px solid var(--dps-border);
+ border-radius: 12px;
+ border-collapse: separate;
+ border-spacing: 0;
+ font-size: 0.78rem;
+ line-height: 1.4;
+ box-shadow: 0 0 0 1px var(--dps-border);
+ -webkit-overflow-scrolling: touch;
+}
+
+.md-typeset table.dataframe th,
+.md-typeset table.dataframe td {
+ border: 0;
+ border-bottom: 1px solid var(--dps-border);
+ padding: 0.4rem 0.85rem;
+ text-align: left;
+ white-space: nowrap;
+ vertical-align: top;
+}
+
+.md-typeset table.dataframe thead th {
+ background: color-mix(in srgb, var(--md-primary-fg-color) 7%, var(--dps-card));
+ color: var(--md-default-fg-color);
+ font-weight: 600;
+ border-bottom: 2px solid color-mix(in srgb, var(--md-accent-fg-color) 35%, var(--dps-border));
+}
+
+.md-typeset table.dataframe tbody th {
+ font-weight: 600;
+ color: var(--md-default-fg-color--light);
+ background: color-mix(in srgb, var(--md-default-fg-color) 3%, transparent);
+}
+
+.md-typeset table.dataframe tbody tr:nth-child(even) > td {
+ background: color-mix(in srgb, var(--md-default-fg-color) 3%, transparent);
+}
+
+.md-typeset table.dataframe tbody tr:hover > td {
+ background: var(--md-accent-fg-color--transparent);
+}
+
+.md-typeset table.dataframe tr:last-child td,
+.md-typeset table.dataframe tr:last-child th {
+ border-bottom: 0;
+}
diff --git a/docs/why_canonical_entity_mapping.md b/docs/why_canonical_entity_mapping.md
index f41a2779..05639523 100644
--- a/docs/why_canonical_entity_mapping.md
+++ b/docs/why_canonical_entity_mapping.md
@@ -1,6 +1,6 @@
# Why canonical entity mapping?
-This document explains the design decisions behind the canonical entity mapping approach used in Presidio Evaluator. For the taxonomy structure and usage guide, see [entity_hierarchy.md](entity_hierarchy.md).
+This document explains the design decisions behind the canonical entity mapping approach used in Presidio Evaluator. For the taxonomy structure and usage guide, see [Entity hierarchy](entity_hierarchy.md).
## Approaches to entity label mapping
@@ -56,13 +56,13 @@ Depth 2 (the domain branches) is useful for coarse-grained comparison (e.g. "did
Depth 3 provides enough specificity for meaningful evaluation without fragmenting into micro-types that no realistic
model distinguishes. Depth-4+ entities exist for completeness but are intentionally aggregated upward during evaluation.
-**Depth is now data-driven:** rather than accepting a fixed `canonical_depth` parameter, `CanonicalMapper` computes
-the evaluation depth automatically via a weighted majority vote over the annotation labels in your results DataFrame.
-Each annotation label is mapped to a canonical entity, its depth is measured (capped at 3), and the weighted average
-determines the canonical surface. Depth 3 is the most common outcome for datasets that use fine-grained entity types like
-`EMAIL_ADDRESS`, `NAME`, or `SSN`. Depth 2 results when the dataset predominantly uses broad categories like `PERSON`
-or `LOCATION`.
+**Granularity is now annotation-driven and decided per prediction:** rather than accepting a fixed
+`canonical_depth` parameter, `CanonicalMapper` projects each prediction to the deepest annotation
+label that is an ancestor-or-self of it. More-specific predictions are credited to the gold label
+they fall under, while less-specific predictions are not projected downward. A branch may carry
+several annotated depths at once — with `PERSON` and `TITLE` both annotated, a `TITLE` prediction
+stays `TITLE` and a `NAME` prediction becomes `PERSON`.
-This means no manual tuning is required — the canonical surface reflects the granularity of the ground truth data.
-Multi-model comparisons are consistent because the canonical surface is locked after the first `analyze()` call and reused
-for all subsequent models.
+This requires no manual tuning and no mapping decision. Multi-model comparisons remain consistent
+because the projection depends only on the annotation vocabulary, which is the same for every model
+evaluated on that dataset.
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 00000000..2a13f6fe
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,101 @@
+site_name: Presidio Research
+site_url: https://data-privacy-stack.github.io/presidio-research
+site_description: Evaluation and data-science tooling for Presidio and PII detection models.
+site_author: Data Privacy Stack
+copyright: 'Data Privacy Stack'
+
+repo_url: https://github.com/data-privacy-stack/presidio-research/
+edit_uri: ""
+
+# The Home page and the notebook pages are *collected* into the docs tree at
+# build time by scripts/zensical_build.py (README.md -> index.md, and the
+# notebooks/ tutorials -> notebooks/*.md). They are not committed under docs/;
+# the build script stages and converts them before Zensical runs.
+nav:
+ - Home: index.md
+ - Concepts:
+ - Evaluation strategies:
+ - Home: evaluation.md
+ - Token evaluation: token_evaluation.md
+ - Span evaluation: span_evaluation.md
+ - Span matching strategies: span_matching_strategies.md
+ - Entity Mapping:
+ - Entity hierarchy: entity_hierarchy.md
+ - Why canonical entity mapping: why_canonical_entity_mapping.md
+ - Mapping scenarios: mapping_scenarios.md
+ - Data generation: data_generation.md
+ - Tutorials:
+ - Generate data: notebooks/1_Generate_data.ipynb
+ - PII dataset EDA: notebooks/2_PII_EDA.ipynb
+ - Split by pattern: notebooks/3_Split_by_pattern_number.ipynb
+ - Evaluate Presidio Analyzer: notebooks/4_Evaluate_Presidio_Analyzer.ipynb
+ - Evaluate a custom Analyzer: notebooks/5_Evaluate_Custom_Presidio_Analyzer.ipynb
+ - Interactive entity mapping: notebooks/6_Interactive_Entity_Mapping.ipynb
+ - Presidio ↗: https://presidio.dataprivacystack.org
+
+theme:
+ name: material
+ palette:
+ - media: "(prefers-color-scheme: light)"
+ scheme: default
+ primary: custom
+ accent: custom
+ toggle:
+ icon: material/weather-night
+ name: Switch to dark mode
+ - media: "(prefers-color-scheme: dark)"
+ scheme: slate
+ primary: custom
+ accent: custom
+ toggle:
+ icon: material/weather-sunny
+ name: Switch to light mode
+ font:
+ text: Inter
+ code: JetBrains Mono
+ logo: assets/dps-icon.svg
+ favicon: assets/dps-icon.svg
+ icon:
+ repo: fontawesome/brands/github
+ features:
+ - navigation.instant
+ - navigation.instant.progress
+ - content.tabs.link
+ - content.code.copy
+ - content.code.annotate
+ - navigation.tabs
+ - navigation.tabs.sticky
+ - navigation.top
+ - navigation.footer
+ - search.suggest
+ - search.highlight
+ - toc.follow
+
+extra_css:
+ - stylesheets/extra.css
+
+plugins:
+ - search
+
+extra:
+ social:
+ - icon: fontawesome/brands/github
+ link: https://github.com/data-privacy-stack/presidio-research
+ - icon: fontawesome/brands/discord
+ link: https://discord.gg/MewtBGFquB
+ - icon: fontawesome/solid/envelope
+ link: mailto:presidio@dataprivacystack.org
+
+markdown_extensions:
+ - meta
+ - admonition
+ - pymdownx.highlight
+ - pymdownx.superfences
+ - pymdownx.pathconverter
+ - pymdownx.tabbed:
+ alternate_style: true
+ - pymdownx.superfences:
+ custom_fences:
+ - name: mermaid
+ class: mermaid
+ format: !!python/name:pymdownx.superfences.fence_code_format
diff --git a/notebooks/5_Evaluate_Custom_Presidio_Analyzer.ipynb b/notebooks/5_Evaluate_Custom_Presidio_Analyzer.ipynb
index a4a1bb49..2dcfe7ea 100644
--- a/notebooks/5_Evaluate_Custom_Presidio_Analyzer.ipynb
+++ b/notebooks/5_Evaluate_Custom_Presidio_Analyzer.ipynb
@@ -864,15 +864,17 @@
}
],
"source": [
- "# Resolve all blocking (WARNING+) issues. Adjust mappings for your model's labels.\n",
- "# PREDICTION_ONLY and COLLISION_CROSS_BRANCH are suppressed here — remap if appropriate.\n",
+ "# Resolve blocking issues. Adjust mappings for your model's labels.\n",
+ "# Unresolved and prediction-only labels are suppressed here — remap if appropriate.\n",
+ "# Mixed gold depths need no decision: each prediction is projected to the\n",
+ "# deepest annotated ancestor of its own label.\n",
"resolutions = {}\n",
"for issue in mapper.get_issues():\n",
- " if issue.severity.value in (\"warning\", \"error\"):\n",
- " for lbl in issue.labels:\n",
- " resolutions[lbl] = None # suppress — adjust as needed\n",
+ " if issue.type.value in (\"unresolved\", \"prediction_only\"):\n",
+ " resolutions.update(dict.fromkeys(issue.labels))\n",
"\n",
"if resolutions:\n",
+ " print(\"Applying explicit mappings before evaluation:\", resolutions)\n",
" mapper.map(resolutions)\n",
"\n",
"mapped_results = mapper.get_mapped_results_dataframe()\n",
diff --git a/notebooks/6_Interactive_Entity_Mapping.ipynb b/notebooks/6_Interactive_Entity_Mapping.ipynb
index 62246acc..36876754 100644
--- a/notebooks/6_Interactive_Entity_Mapping.ipynb
+++ b/notebooks/6_Interactive_Entity_Mapping.ipynb
@@ -471,19 +471,21 @@
"id": "5d82a510",
"metadata": {},
"source": [
- "## 5. Majority-Vote Depth Auto-Discovery\n",
+ "## 5. Annotation-Driven Branch Granularity\n",
"\n",
- "The canonical surface is computed automatically from the **annotation** labels in the results DataFrame.\n",
- "Each annotation label is resolved to a canonical entity, its depth in the hierarchy is measured\n",
- "(capped at 3), and a weighted majority vote determines the canonical depth.\n",
+ "Evaluation granularity is computed from the **annotation** labels in the results DataFrame.\n",
+ "Each prediction is projected to the deepest annotation label that is an ancestor-or-self of it.\n",
+ "A prediction with no annotated ancestor is left unchanged.\n",
"\n",
- "| Scenario | Majority depth | Canonical surface |\n",
- "|----------|---------------|-------------|\n",
- "| Most annotations are `NAME`, `EMAIL_ADDRESS`, `SSN` (depth 3) | 3 | depth-3 entities |\n",
- "| Most annotations are `PERSON`, `LOCATION` (depth 2) | 2 | depth-2 entities |\n",
+ "| Annotated labels | Prediction | Detailed scoring label |\n",
+ "|------------------|------------|------------------------|\n",
+ "| `PERSON` | `NAME` | `PERSON` (projected upward) |\n",
+ "| `NAME` | `PERSON` | `PERSON` (not projected downward) |\n",
+ "| `PERSON`, `TITLE` | `TITLE` | `TITLE` (annotated in its own right) |\n",
+ "| `PERSON`, `TITLE` | `NAME` | `PERSON` (deepest annotated ancestor) |\n",
"\n",
- "The canonical surface is **locked** after the first `analyze()` call and does not change on subsequent\n",
- "calls. This ensures consistent evaluation across multiple models on the same dataset.\n"
+ "A branch may carry several annotated depths at once — no mapping decision is required.\n",
+ "Each `analyze()` call recalculates inferred mappings while preserving explicit `map()` decisions.\n"
]
},
{
diff --git a/presidio_evaluator/data_generator/README.md b/presidio_evaluator/data_generator/README.md
index 6b23c37a..ecd34617 100644
--- a/presidio_evaluator/data_generator/README.md
+++ b/presidio_evaluator/data_generator/README.md
@@ -1,84 +1,9 @@
# Data Generation
-The `PresidioSentenceFaker` generates sentences from templates (e.g. `my name is {{person}}`) where the placeholders
-are replaced with fake PII entities, along with metadata about the spans (the start and end of each entity) for model training and evaluation.
+📖 The data generation documentation now lives in the Presidio-Research docs:
-## Scenarios
+- **Online:**
+- **Source:** [`docs/data_generation.md`](../../docs/data_generation.md)
-There are two main scenarios for using the `PresidioSentenceFaker`:
-
-1. Create a fake dataset for evaluation or training purposes, given a list of predefined templates
-(uses [this file](raw_data/templates.txt) by default)
-2. Augment an existing labeled dataset with additional fake values.
-
-In both scenarios the process is similar. In scenario 2, the existing dataset is first translated into templates,
-and then scenario 1 is applied.
-
-## Process
-
-This generator heavily relies on the [Faker package](https://www.github.com/joke2k/faker) with a few differences:
-
-1. `PresidioSentenceFaker` returns not only fake text, but also the spans in which fake entities appear in the text.
-2. `Faker` samples each value independently.
-In many cases, we would want to keep the semantic dependency between two values.
-For example, for the template `My name is {{name}} and my email is {{email}}`,
-we would prefer a result which has the name within the email address,
-such as `My name is Mike and my email is mike1243@gmail.com`.
-For this functionality, a new `RecordGenerator` (based on Faker's `Generator` class) is implemented.
-It accepts a dictionary / pandas DataFrame, and favors returning objects from the same record (if possible).
-
-## Example
-
-For a full example, see the [Generate Data Notebook](../../notebooks/1_Generate_data.ipynb).
-
-`PresidioSentenceFaker` provides a high-level interface for using the full power of the `presidio_evaluator`
-package. Its results use the presidio PII entities, not the `Faker` entities.
-It is loaded by default with template strings, and the additional Presidio Entity Providers.
-
-```python
-from presidio_evaluator.data_generator import PresidioSentenceFaker
-
-record_generator = PresidioSentenceFaker(locale='en', lower_case_ratio=0.05)
-fake_records = record_generator.generate_new_fake_sentences(1500)
-
-# Print the spans of the first sample
-print(fake_records[0].fake)
-print(fake_records[0].spans)
-```
-
-The process at a high level is the following:
-
-1. Translate a NER dataset (e.g. CONLL or OntoNotes) into a list of
-templates: `My name is John` -> `My name is [PERSON]`
-2. Construct a `PresidioSentenceFaker` instance by:
- - Choosing your appropriate locale, e.g. `en_US`
- - Choosing the lower case ratio
- - Passing in your list of templates (or default to those provided)
- - Optionally extend with provided templates accessible via `from presidio_evaluator.data_generator import presidio_templates_file_path`
- - Passing in any custom entity providers (or default to those provided)
- - Optionally extend with inbuilt presidio entity providers accessible via `from presidio_evaluator.data_generator import presidio_additional_entity_providers`
- - Adding a mapping from the output provider entity type to a Presidio recognized entity type where appropriate
- - e.g. For a `TownProvider` which outputs entity type of `town`, execute `PresidioSentenceFaker.ENTITY_TYPE_MAPPING['town'] = 'GPE'`)
- - Passing in a DataFrame representing your underlying PII records (or default to those provided)
- - Optionally extend with inbuilt presidio entity providers accessible via `from presidio_evaluator.data_generator.faker_extensions.datasets import load_fake_person_df`
- - Adding any additional aliases required by your dataset by adding to `PresidioSentenceFaker.PROVIDER_ALIASES`
- - e.g. if the entity providers support "name" but your dataset templates contain "person", you can add this alias
- with `PresidioSentenceFaker.PROVIDER_ALIASES['name'] = 'person'`)
-3. Generate sentences
-4. Split the generated dataset into train/test/validation while making sure
-that samples from the same template would only appear in one set
-5. Adapt datasets for the various models (Spacy, Flair, CRF, sklearn)
-6. Train models
-7. Evaluate using one of the [evaluation notebooks](../../notebooks/models)
-
-Notes:
-
-- For steps 5, 6, 7 see the main [README](../../README.md).
-
-
-*Copyright notice:*
-
-Fake Name Generator identities by the Fake Name Generator are licensed under a
-Creative Commons Attribution-Share Alike 3.0 United States License.
-Fake Name Generator and the Fake Name Generator logo
-are trademarks of Corban Works, LLC.
+It covers the `PresidioSentenceFaker` scenarios, the generation process, and a
+full end-to-end example.
diff --git a/presidio_evaluator/data_generator/faker_extensions/providers.py b/presidio_evaluator/data_generator/faker_extensions/providers.py
index bc6ecf48..a81ca820 100644
--- a/presidio_evaluator/data_generator/faker_extensions/providers.py
+++ b/presidio_evaluator/data_generator/faker_extensions/providers.py
@@ -298,7 +298,7 @@ def load_hospitals(self, hospital_file: str) -> list[str]:
if hospital_file:
hospitals = pd.read_csv(hospital_file)
- if "name" not in self.hospitals:
+ if "name" not in hospitals:
print(
"Unable to retrieve hospital names, "
"file is missing column named 'name'",
@@ -326,16 +326,30 @@ def load_wiki_hospitals(
}
"""
+ headers = {
+ "User-Agent": (
+ "presidio-research "
+ "(https://github.com/data-privacy-stack/presidio-research)"
+ )
+ }
try:
- r = requests.get(url, params={"format": "json", "query": query}, timeout=10)
+ r = requests.get(
+ url,
+ params={"format": "json", "query": query},
+ headers=headers,
+ timeout=10,
+ )
if r.status_code != 200:
- print("Unable to read hospitals from WikiData, returning an empty list")
+ print(
+ "Unable to read hospitals from WikiData "
+ f"(status code {r.status_code}), returning default hospital list"
+ )
return self.default_list
data = r.json()
- bindings = data["results"].get("bindings", [])
+ bindings = data.get("results", {}).get("bindings", [])
hospitals = [self.deep_get(x, ["label_en", "value"]) for x in bindings]
hospitals = [x for x in hospitals if "no key" not in x]
- return hospitals
+ return hospitals if hospitals else self.default_list
except OSError:
warnings.warn(
"Can't download hospitals data. Returning default list", stacklevel=2
diff --git a/presidio_evaluator/entity_mapping/data_objects.py b/presidio_evaluator/entity_mapping/data_objects.py
index 0f5835e0..3eff8341 100644
--- a/presidio_evaluator/entity_mapping/data_objects.py
+++ b/presidio_evaluator/entity_mapping/data_objects.py
@@ -24,7 +24,9 @@ class IssueType(Enum):
DATASET_ONLY: Entity has annotations but no prediction maps to it.
COLLISION_SAME_BRANCH: Label and co-occurring annotation share the same hierarchy branch
at different depths (e.g. model predicts PERSON, dataset uses NAME).
- Informational — handled automatically by hierarchical evaluation.
+ Each prediction is projected to the deepest annotated
+ ancestor of its own label, so this never blocks — including
+ when the annotations themselves mix depths on one branch.
"""
UNRESOLVED = "unresolved"
@@ -89,8 +91,9 @@ class MappedResults:
binary: Labels resolved to ``"PII"`` (any non-O) or ``"O"``.
branch: Labels resolved to the depth-2 branch ancestor
(e.g. ``FIRST_NAME`` → ``PERSON``).
- detailed: Labels resolved to the hierarchy node at native depth
- (e.g. ``FIRST_NAME`` → ``NAME``). Suppressed → ``"O"``.
+ detailed: Labels resolved to the hierarchy node at native depth, with
+ each prediction projected upward to the deepest annotated
+ ancestor of its own label. Suppressed → ``"O"``.
"""
original: pd.DataFrame
diff --git a/presidio_evaluator/entity_mapping/definitions.py b/presidio_evaluator/entity_mapping/definitions.py
index 3fa6a69a..2f2b0ce1 100644
--- a/presidio_evaluator/entity_mapping/definitions.py
+++ b/presidio_evaluator/entity_mapping/definitions.py
@@ -11,6 +11,7 @@
HIERARCHY: dict = {
"PII": {
"PERSON": {
+ "_aliases": ["PER"],
"NAME": {
"FIRST_NAME": [
"FIRSTNAME",
@@ -31,13 +32,13 @@
"FULL_NAME": [
"FULLNAME",
"DOCTOR",
+ "PATIENT",
"PATIENT_NAME",
"DOCTOR_NAME",
"HCW",
"NAME_MEDICAL_PROFESSIONAL",
],
"MAIDEN_NAME": [],
- "PER": [],
},
"PREFIX": [],
"SUFFIX": [],
@@ -87,6 +88,7 @@
"SOCIAL_HANDLE": ["QQ"], # QQ: Chinese messaging platform ID
},
"LOCATION": {
+ "_aliases": ["LOC"],
"ADDRESS": {
"STREET_ADDRESS": [
"STREET",
@@ -129,10 +131,10 @@
],
"LOCATION_OTHER": ["LOCATION-OTHER", "ORDINALDIRECTION"],
"GPE": ["GLOBAL_POLITICAL_ENTITY"],
- "LOC": [],
"GEO": [],
},
"ORGANIZATION": {
+ "_aliases": ["ORG"],
"COMPANY": [
"COMPANYNAME",
"COMPANY_ID",
@@ -148,7 +150,6 @@
"HOSPITAL_NAME",
],
"OTHER_ORG": [],
- "ORG": [],
},
"EMPLOYMENT": {
"JOB_TITLE": [
@@ -361,7 +362,6 @@
},
"PHI": {
"PATIENT_ID": [
- "PATIENT",
"MEDICALRECORD",
"MEDICAL_RECORD_NUMBER",
"MEDICAL_RECORD",
diff --git a/presidio_evaluator/entity_mapping/hierarchy.py b/presidio_evaluator/entity_mapping/hierarchy.py
index c72f8355..6a4f9557 100644
--- a/presidio_evaluator/entity_mapping/hierarchy.py
+++ b/presidio_evaluator/entity_mapping/hierarchy.py
@@ -5,6 +5,7 @@
import copy
import difflib
+import logging
import re
from presidio_evaluator.entity_mapping.definitions import (
@@ -20,6 +21,15 @@
_BIO_PREFIX_RE = re.compile(r"^[BIOELSU]-(.+)$", re.IGNORECASE)
_BIO_SUFFIX_RE = re.compile(r"^(.+)-[BIOELSU]$", re.IGNORECASE)
+logger = logging.getLogger(__name__)
+
+# Reserved key that lets a BRANCH (non-leaf) node declare raw aliases, e.g.
+# "LOCATION": {"_aliases": ["LOC"], "ADDRESS": {...}, ...}
+# Leaf nodes already carry their aliases as a list value; branch nodes are dicts
+# and previously had nowhere to declare synonyms. This key is skipped by every
+# tree-walk so it never becomes a canonical entity itself.
+BRANCH_ALIASES_KEY = "_aliases"
+
class EntityHierarchy:
"""
@@ -90,36 +100,76 @@ def get_branch(self, raw_label: str) -> list[str]:
return branch
def get_depth(self, entity: str) -> int:
- """Return the depth of a canonical entity in the hierarchy tree.
+ """Return the depth of an entity in the hierarchy tree.
Depth is defined as the length of the ancestor path:
PII=1, PERSON=2, NAME=3, FIRST_NAME=4.
- :param entity: A canonical entity name.
+ :param entity: A canonical entity name, or any raw alias of one.
:return: depth (int >= 1).
:raises EntityNotMappedError: if entity is not found.
"""
branch = self.canonical_to_branch.get(entity)
if branch is None:
- raise EntityNotMappedError(
- f"Canonical entity {entity!r} has no branch in hierarchy"
- )
+ # Accept raw aliases (including branch-level ones such as "LOC")
+ # by resolving them to their canonical name first.
+ canonical = self.raw_to_canonical.get(self.normalize(entity))
+ if canonical is not None:
+ branch = self.canonical_to_branch.get(canonical)
+ if branch is None:
+ raise EntityNotMappedError(f"Entity {entity!r} has no branch in hierarchy")
return len(branch)
def add_alias(self, entity_name: str, alias: str) -> None:
- """Add a raw alias for an existing entity."""
+ """Add a raw alias for an existing entity (leaf or branch).
+
+ *entity_name* may be a canonical name or any raw alias of one.
+ """
found = self._find_node(entity_name)
+ if found is None:
+ # Fall back to alias resolution so e.g. add_alias("LOC", x) works
+ # even though "LOC" is itself an alias of the LOCATION branch.
+ canonical = self.raw_to_canonical.get(self.normalize(entity_name))
+ if canonical is not None:
+ found = self._find_node(canonical)
if found is None:
raise KeyError(f"Entity {entity_name!r} not found in hierarchy")
parent_dict, key = found
value = parent_dict[key]
if isinstance(value, list):
- if alias not in value:
- value.append(alias)
+ added_to = value
else:
- value[alias] = []
+ # Branch (non-leaf) node: record the alias under the reserved key so
+ # it maps to this branch, instead of creating a spurious child leaf.
+ added_to = value.setdefault(BRANCH_ALIASES_KEY, [])
+ # Track whether THIS call appended, so a rollback never deletes an alias
+ # that was already there (e.g. re-adding an alias the target already owns).
+ appended = alias not in added_to
+ if appended:
+ added_to.append(alias)
self._rebuild()
+ # A branch alias is applied before the recursive descent into its own
+ # subtree, so a descendant with the same normalized name wins. Rather
+ # than persist an alias that silently never resolves, roll back and say
+ # so — the caller's intent could not be honoured.
+ #
+ # The alias is correct when it resolves wherever the TARGET resolves:
+ # for a node below canonical_depth that is the canonical ancestor, not
+ # the node's own name.
+ expected = self.raw_to_canonical.get(self.normalize(key))
+ resolved = self.raw_to_canonical.get(self.normalize(alias))
+ if resolved != expected:
+ if appended:
+ added_to.remove(alias)
+ if not added_to and added_to is not value:
+ value.pop(BRANCH_ALIASES_KEY, None)
+ self._rebuild()
+ raise ValueError(
+ f"Alias {alias!r} already resolves to {resolved!r}, "
+ f"so it cannot be added to {key!r}",
+ )
+
@staticmethod
def _strip_bio(label: str) -> str:
"""Strip a single BIO/BIOES/BILOU/BILUO prefix or suffix (e.g. B-PERSON → PERSON)."""
@@ -139,6 +189,10 @@ def _collect_all_raw(value) -> list[str]:
items.extend(value)
elif isinstance(value, dict):
for k, v in value.items():
+ if k == BRANCH_ALIASES_KEY:
+ # Collect the aliases themselves, never the reserved key name.
+ items.extend(v)
+ continue
items.append(k)
items.extend(EntityHierarchy._collect_all_raw(v))
return items
@@ -152,6 +206,8 @@ def _build_alias_map(
"""Build a normalized-label → canonical-name lookup dict by walking the hierarchy tree."""
mapping: dict[str, str] = {}
for key, value in node.items():
+ if key == BRANCH_ALIASES_KEY:
+ continue # reserved: branch aliases, applied by the parent below
if depth >= canonical_depth:
mapping[EntityHierarchy.normalize(key)] = key
for alias in EntityHierarchy._collect_all_raw(value):
@@ -162,6 +218,9 @@ def _build_alias_map(
mapping[EntityHierarchy.normalize(alias)] = key
elif isinstance(value, dict):
mapping[EntityHierarchy.normalize(key)] = key
+ # Branch-level aliases: raw synonyms of this non-leaf node.
+ for alias in value.get(BRANCH_ALIASES_KEY, []):
+ mapping[EntityHierarchy.normalize(alias)] = key
mapping.update(
EntityHierarchy._build_alias_map(value, canonical_depth, depth + 1),
)
@@ -176,6 +235,8 @@ def _collect_canonical_nodes(
"""Collect the names of all canonical-depth leaf nodes from the hierarchy tree."""
result: list[str] = []
for key, value in node.items():
+ if key == BRANCH_ALIASES_KEY:
+ continue # reserved: not a canonical entity
if depth >= canonical_depth:
result.append(key)
elif isinstance(value, list):
@@ -202,6 +263,8 @@ def _build_branch_map(
current_path = []
result: dict[str, list[str]] = {}
for key, value in node.items():
+ if key == BRANCH_ALIASES_KEY:
+ continue # reserved: not a canonical entity
path = current_path + [key]
if depth >= canonical_depth:
result[key] = path
@@ -233,6 +296,39 @@ def _rebuild(self) -> None:
self.hierarchy,
self.canonical_depth,
)
+ self._warn_on_shadowed_branch_aliases()
+
+ def _warn_on_shadowed_branch_aliases(self) -> None:
+ """Warn about branch aliases that a descendant silently overrides.
+
+ Branch aliases are written into the lookup before the recursive descent
+ into their own subtree, so a descendant with the same normalized name
+ wins. `add_alias()` rejects that at call time, but a collision baked
+ into the hierarchy definition would otherwise pass unnoticed.
+ """
+ for branch_key, alias in self._collect_branch_aliases(self.hierarchy):
+ expected = self.raw_to_canonical.get(self.normalize(branch_key))
+ resolved = self.raw_to_canonical.get(self.normalize(alias))
+ if expected is not None and resolved != expected:
+ logger.warning(
+ "Branch alias %r on %r is shadowed by %r and will never "
+ "resolve to %r.",
+ alias,
+ branch_key,
+ resolved,
+ expected,
+ )
+
+ @staticmethod
+ def _collect_branch_aliases(node: dict) -> list[tuple[str, str]]:
+ """Return (branch_name, alias) for every branch-level alias in the tree."""
+ found: list[tuple[str, str]] = []
+ for key, value in node.items():
+ if key == BRANCH_ALIASES_KEY or not isinstance(value, dict):
+ continue
+ found.extend((key, a) for a in value.get(BRANCH_ALIASES_KEY, []))
+ found.extend(EntityHierarchy._collect_branch_aliases(value))
+ return found
def _resolve_remainder(self, remainder: str, threshold: float) -> str:
"""Canonicalize the document-type portion of a country-prefixed label, falling back to NATIONAL_ID."""
@@ -317,6 +413,14 @@ def to_branch(self, label: str | None) -> str:
if label is None or label == "O":
return "O"
branch_path = self.canonical_to_branch.get(label)
+ if branch_path is None:
+ # Not a canonical name — it may be a raw alias (leaf or branch
+ # level, e.g. "LOC"). Resolve it before giving up, so callers
+ # feeding raw dataset labels get the right branch instead of a
+ # silent pass-through into a different bucket.
+ canonical = self.raw_to_canonical.get(self.normalize(label))
+ if canonical is not None:
+ branch_path = self.canonical_to_branch.get(canonical)
if branch_path is None or len(branch_path) < 2:
return label
return branch_path[1]
@@ -329,6 +433,11 @@ def _find_node(
"""Return (parent_dict, key) for the first node matching name in the hierarchy tree, or None."""
if tree is None:
tree = self.hierarchy
+ if name == BRANCH_ALIASES_KEY:
+ # Reserved key: it is not an entity, so it must not be addressable
+ # as one (otherwise add_alias("_aliases", x) would silently attach x
+ # to whichever branch happens to be found first).
+ return None
for key, value in tree.items():
if key == name:
return (tree, key)
diff --git a/presidio_evaluator/entity_mapping/mapper.py b/presidio_evaluator/entity_mapping/mapper.py
index a4bfb647..6f0c24fd 100644
--- a/presidio_evaluator/entity_mapping/mapper.py
+++ b/presidio_evaluator/entity_mapping/mapper.py
@@ -28,10 +28,6 @@ def _get_renderer_class(): # noqa: ANN201
return MapperRenderer
-# Full-depth hierarchy used for branch lookups and depth calculations.
-# Constructed once at module load; never mutated.
-_FULL_HIERARCHY = EntityHierarchy(canonical_depth=10)
-
_SEVERITY_ORDER = {
IssueSeverity.ERROR: 0,
IssueSeverity.WARNING: 1,
@@ -121,6 +117,30 @@ def pending(self) -> list[str]:
lbl for lbl, rec in self._records.items() if rec.tier == "UNRESOLVED"
)
+ @property
+ def _full_hierarchy(self) -> EntityHierarchy:
+ """Return a full-depth view of the mapper's current hierarchy."""
+ return EntityHierarchy(
+ hierarchy=self._hierarchy.hierarchy,
+ canonical_depth=10,
+ )
+
+ def _resolved_annotation_paths(
+ self,
+ hierarchy: EntityHierarchy,
+ ) -> list[tuple[str, str, list[str]]]:
+ """Return raw label, resolved label, and path for active annotations."""
+ annotations = []
+ for label, record in self._records.items():
+ if self._label_annotation_counts.get(label, 0) == 0:
+ continue
+ if record.resolved is None or record.tier in ("UNRESOLVED", "NONE"):
+ continue
+ path = hierarchy.canonical_to_branch.get(record.resolved)
+ if path:
+ annotations.append((label, record.resolved, path))
+ return annotations
+
# -- Analysis -------------------------------------------------------------
def analyze(
@@ -137,8 +157,9 @@ def analyze(
:param results_df: DataFrame with annotation and prediction columns.
:param min_severity: Minimum severity to surface via get_issues() and
render_html(). Accepts 'ERROR', 'WARNING', 'INFO' (or IssueSeverity
- enum values). Default is 'WARNING'. COLLISION_SAME_BRANCH (INFO)
- is only shown when min_severity='INFO'.
+ enum values). Default is 'WARNING'. Informational
+ COLLISION_SAME_BRANCH issues are only shown when min_severity='INFO';
+ mixed annotation depths on one branch are ERROR.
:param min_collision_count: Minimum number of cross-branch token
co-occurrences required to raise a COLLISION_CROSS_BRANCH warning.
Collisions with fewer co-occurrences than this threshold are silently
@@ -304,7 +325,7 @@ def _auto_resolve_one(self, label: str) -> _Resolution | None:
def _detect_issues(self) -> None:
"""Detect all issues (single-phase identification) and sort them."""
self._issues.clear()
- h_full = _FULL_HIERARCHY
+ h_full = self._full_hierarchy
def _branch_key(resolved: str | None) -> str | None:
if not resolved:
@@ -608,6 +629,67 @@ def _branch_key(resolved: str | None) -> str | None:
)
# ── COLLISION_SAME_BRANCH (INFO) ─────────────────────────────────────
+ # Each prediction is projected to the deepest annotated ancestor of its
+ # own label, so mixed annotation depths resolve per prediction and never
+ # block. They are still surfaced because they change how detailed scores
+ # read: each annotated depth keeps its own metrics.
+ annotation_labels_by_branch: dict[str, list[tuple[str, str, int]]] = {}
+ root_annotations: list[tuple[str, str, int]] = []
+ resolved_annotations = self._resolved_annotation_paths(h_full)
+ for ann_lbl, resolved, ann_path in resolved_annotations:
+ item = (ann_lbl, resolved, len(ann_path))
+ if len(ann_path) == 1:
+ root_annotations.append(item)
+ else:
+ annotation_labels_by_branch.setdefault(ann_path[1], []).append(item)
+
+ ambiguous_branches: set[str] = set()
+ mixed_depth_groups: list[tuple[str, list[tuple[str, str, int]]]] = []
+ if root_annotations and annotation_labels_by_branch:
+ deeper = [
+ item
+ for branch_labels in annotation_labels_by_branch.values()
+ for item in branch_labels
+ ]
+ mixed_depth_groups.append(
+ (root_annotations[0][1], root_annotations + deeper)
+ )
+ ambiguous_branches.update(annotation_labels_by_branch)
+ else:
+ for branch, labels_at_branch in annotation_labels_by_branch.items():
+ if len({depth for _, _, depth in labels_at_branch}) <= 1:
+ continue
+ ambiguous_branches.add(branch)
+ mixed_depth_groups.append((branch, labels_at_branch))
+
+ for branch, items in mixed_depth_groups:
+ labels = sorted({label for label, _, _ in items})
+ resolved_depths = ", ".join(
+ f"{resolved} (depth {depth})"
+ for resolved, depth in sorted(
+ {(resolved, depth) for _, resolved, depth in items},
+ key=lambda item: (item[1], item[0]),
+ )
+ )
+ self._issues.append(
+ MappingIssue(
+ type=IssueType.COLLISION_SAME_BRANCH,
+ severity=IssueSeverity.INFO,
+ message=(
+ f"Annotations on the {branch!r} branch use multiple "
+ f"hierarchy depths: {resolved_depths}. Each prediction is "
+ "projected to the deepest annotated ancestor of its own "
+ "label, so every annotated depth keeps its own metrics and "
+ "no mapping decision is required."
+ ),
+ labels=labels,
+ annotation_count=sum(
+ self._label_annotation_counts.get(label, 0) for label in labels
+ ),
+ prediction_count=0,
+ )
+ )
+
# Prediction label co-occurs with annotation label(s) on same branch but
# different depth (e.g. prediction=PERSON depth-2, annotation=NAME depth-3).
for pred_lbl, rec_pred in self._records.items():
@@ -619,6 +701,8 @@ def _branch_key(resolved: str | None) -> str | None:
if len(pred_branch) < 2:
continue
pred_branch_key = pred_branch[1]
+ if pred_branch_key in ambiguous_branches:
+ continue
pred_depth = len(pred_branch)
same_branch_overlap: dict[str, int] = {}
@@ -656,8 +740,9 @@ def _branch_key(resolved: str | None) -> str | None:
f"{pred_lbl!r} (→ {rec_pred.resolved!r}, depth {pred_depth}) "
f"co-occurs with same-branch annotation(s) at different "
f"depth: {ann_str}. "
- f"Handled automatically by hierarchical evaluation "
- f"(branch/detailed projection)."
+ f"More-specific predictions are projected to the deepest "
+ f"annotated ancestor during detailed mapping; less-specific "
+ f"predictions remain mismatches."
),
labels=[pred_lbl],
annotation_count=0,
@@ -680,8 +765,8 @@ def _branch_key(resolved: str | None) -> str | None:
def get_issues(self) -> list[MappingIssue]:
"""Return issues from the last analyze() call, filtered by min_severity.
- Issues with severity below min_severity are excluded. COLLISION_SAME_BRANCH
- (INFO) is only returned when min_severity='INFO'.
+ Issues with severity below min_severity are excluded. Informational
+ COLLISION_SAME_BRANCH issues are only returned when min_severity='INFO'.
"""
min_order = _SEVERITY_ORDER[self._min_severity]
return [i for i in self._issues if _SEVERITY_ORDER[i.severity] <= min_order]
@@ -825,10 +910,11 @@ def get_mapped_results_dataframe(self) -> MappedResults:
- ``.original`` — raw input labels, unmodified.
- ``.binary`` — any non-O label → ``"PII"``; suppressed/O → ``"O"``.
- ``.branch`` — depth-2 branch ancestor (e.g. ``NAME`` → ``PERSON``).
- - ``.detailed`` — hierarchy node at native depth (e.g. ``FIRST_NAME`` → ``NAME``).
+ - ``.detailed`` — resolved hierarchy nodes, with each prediction
+ projected upward to the deepest annotated ancestor of its own label.
:raises RuntimeError: if analyze() has not been called.
- :raises IncompleteMapping: if any UNRESOLVED (ERROR) issues remain.
+ :raises IncompleteMapping: if any blocking mapping error remains.
"""
if self._results_df is None:
raise RuntimeError(
@@ -839,6 +925,11 @@ def get_mapped_results_dataframe(self) -> MappedResults:
raise IncompleteMapping(blocking)
df = self._results_df
+ h_full = self._full_hierarchy
+
+ annotation_vocabulary = {
+ resolved for _, resolved, _ in self._resolved_annotation_paths(h_full)
+ }
def _resolve(label: str) -> str | None:
"""Return the resolved hierarchy node for a raw label, or None if suppressed."""
@@ -854,25 +945,48 @@ def _level(label: str, level: str) -> str:
if resolved is None or resolved == "O":
return "O"
if level == "binary":
- return _FULL_HIERARCHY.to_binary(resolved)
+ return h_full.to_binary(resolved)
if level == "branch":
- return _FULL_HIERARCHY.to_branch(resolved)
+ return h_full.to_branch(resolved)
# detailed — hierarchy node at native depth
return resolved
+ def _project_prediction(label: str) -> str:
+ """Project a prediction to the deepest gold label that covers it.
+
+ Walks the prediction's own ancestor path from deepest to shallowest
+ and returns the first label present in the annotation vocabulary.
+ A prediction with no annotated ancestor is left unchanged, so a
+ coarser prediction is never pushed down onto a finer gold label.
+ """
+ resolved = _resolve(label)
+ if resolved is None or resolved == "O":
+ return "O"
+ path = h_full.canonical_to_branch.get(resolved)
+ if not path:
+ return resolved
+ for ancestor in reversed(path):
+ if ancestor in annotation_vocabulary:
+ return ancestor
+ return resolved
+
original = df.copy()
binary = df.copy()
binary["annotation"] = df["annotation"].map(lambda x: _level(x, "binary"))
- binary["prediction"] = df["prediction"].map(lambda x: _level(x, "binary"))
+ binary["prediction"] = df["prediction"].map(
+ lambda x: h_full.to_binary(_project_prediction(x))
+ )
branch = df.copy()
branch["annotation"] = df["annotation"].map(lambda x: _level(x, "branch"))
- branch["prediction"] = df["prediction"].map(lambda x: _level(x, "branch"))
+ branch["prediction"] = df["prediction"].map(
+ lambda x: h_full.to_branch(_project_prediction(x))
+ )
detailed = df.copy()
detailed["annotation"] = df["annotation"].map(lambda x: _level(x, "detailed"))
- detailed["prediction"] = df["prediction"].map(lambda x: _level(x, "detailed"))
+ detailed["prediction"] = df["prediction"].map(_project_prediction)
return MappedResults(
original=original,
diff --git a/presidio_evaluator/entity_mapping/rendering.py b/presidio_evaluator/entity_mapping/rendering.py
index 393b35d9..3561f5a3 100644
--- a/presidio_evaluator/entity_mapping/rendering.py
+++ b/presidio_evaluator/entity_mapping/rendering.py
@@ -361,6 +361,16 @@ def _label_table(pairs: list, token_col: dict) -> str: # noqa: ANN001
for issue in m.get_issues():
sty = sev_style[issue.severity]
why = gap_why.get(issue.type, "")
+ if (
+ issue.type == IssueType.COLLISION_SAME_BRANCH
+ and issue.overlap_counts is None
+ ):
+ why = (
+ "The gold annotation vocabulary uses multiple hierarchy "
+ "depths on one branch. Each prediction is projected to the "
+ "deepest annotated ancestor of its own label, so every annotated "
+ "depth keeps its own metrics. No action required."
+ )
lbl_tags = " ".join(
f'{lbl}'
diff --git a/presidio_evaluator/evaluation/base_evaluator.py b/presidio_evaluator/evaluation/base_evaluator.py
index 25a9fff5..55356d03 100644
--- a/presidio_evaluator/evaluation/base_evaluator.py
+++ b/presidio_evaluator/evaluation/base_evaluator.py
@@ -6,7 +6,6 @@
import pandas as pd
from presidio_evaluator import InputSample
-from presidio_evaluator.entity_mapping.hierarchy import EntityHierarchy
from presidio_evaluator.evaluation import EvaluationResult
from presidio_evaluator.evaluation.skipwords import get_skip_words
from presidio_evaluator.models import BaseModel
@@ -19,23 +18,6 @@
GENERIC_ENTITIES = ("PII", "ID", "PII", "PHI", "ID_NUM", "NUMBER", "NUM", "GENERIC_PII")
-def _to_l1(entity: str, hierarchy: EntityHierarchy) -> str:
- """Map an entity label to its depth-2 (branch) ancestor."""
- if entity == "O":
- return "O"
- branch = hierarchy.canonical_to_branch.get(entity)
- if branch is None:
- return entity # unknown entity — pass through unchanged
- if len(branch) >= 2:
- return branch[1] # e.g. ['PII', 'PERSON', 'NAME'] -> 'PERSON'
- return branch[0] # depth-1 node (PII itself)
-
-
-def _to_l0(entity: str) -> str:
- """Map any non-O entity label to 'PII'."""
- return "O" if entity == "O" else "PII"
-
-
class DeprecationError(RuntimeError):
"""Raised when a deprecated method that has been fully removed is called."""
@@ -300,7 +282,19 @@ def calculate_hierarchical_scores(
)
return {
- "binary": self.calculate_score_on_df(results.binary, beta=beta),
- "branch": self.calculate_score_on_df(results.branch, beta=beta),
- "detailed": self.calculate_score_on_df(results.detailed, beta=beta),
+ "binary": self.calculate_score_on_df(
+ results.binary,
+ beta=beta,
+ allow_generic_entities=False,
+ ),
+ "branch": self.calculate_score_on_df(
+ results.branch,
+ beta=beta,
+ allow_generic_entities=False,
+ ),
+ "detailed": self.calculate_score_on_df(
+ results.detailed,
+ beta=beta,
+ allow_generic_entities=False,
+ ),
}
diff --git a/presidio_evaluator/evaluation/span_evaluator.py b/presidio_evaluator/evaluation/span_evaluator.py
index b5a2a3df..53c92264 100644
--- a/presidio_evaluator/evaluation/span_evaluator.py
+++ b/presidio_evaluator/evaluation/span_evaluator.py
@@ -400,6 +400,7 @@ def calculate_score_on_df(
beta: float = 2,
level: Literal["entity", "pii", "both"] = "both",
evaluation_result: EvaluationResult | None = None,
+ allow_generic_entities: bool = True,
**kwargs,
) -> EvaluationResult:
"""
@@ -418,6 +419,11 @@ def calculate_score_on_df(
:param beta: (float) F-beta parameter (default 2).
:param evaluation_result: (EvaluationResult | None) Optional existing
EvaluationResult to accumulate into.
+ :param allow_generic_entities: (bool) Accepted for signature compatibility with
+ :class:`TokenEvaluator` and with
+ :meth:`BaseEvaluator.calculate_hierarchical_scores`, which passes it
+ for every level. Span evaluation compares entity types exactly and
+ has no generic-entity shortcut, so this parameter has no effect here.
:return: (EvaluationResult) Result with the requested metrics populated —
``per_type`` for "entity", the ``pii_*`` fields for "pii",
both for "both".
diff --git a/presidio_evaluator/evaluation/token_evaluator.py b/presidio_evaluator/evaluation/token_evaluator.py
index f3e3ce92..32028839 100644
--- a/presidio_evaluator/evaluation/token_evaluator.py
+++ b/presidio_evaluator/evaluation/token_evaluator.py
@@ -29,6 +29,7 @@ def compare(
self,
input_sample: InputSample,
prediction: list[str],
+ allow_generic_entities: bool = True,
) -> tuple[Counter, list[ModelError]]:
"""
Compares ground truth tags (annotation) and predicted (prediction)
@@ -72,6 +73,7 @@ def compare(
cur_prediction,
cur_token,
results,
+ allow_generic_entities=allow_generic_entities,
)
if reverted:
continue
@@ -135,6 +137,7 @@ def __revert_known_errors(
current_prediction: str,
current_token: str | Token,
results: Counter[tuple[str, str]],
+ allow_generic_entities: bool = True,
) -> bool:
reverted = False
@@ -143,14 +146,22 @@ def __revert_known_errors(
results[(current_annotation, current_prediction)] -= 1
reverted = True
- if current_prediction in self.generic_entities and current_annotation != "O":
+ if (
+ allow_generic_entities
+ and current_prediction in self.generic_entities
+ and current_annotation != "O"
+ ):
# Ignore cases where the prediction is generic
results[(current_annotation, current_prediction)] -= 1
# Add a result which assumes the generic equals the specific
results[(current_annotation, current_annotation)] += 1
reverted = True
- elif current_annotation in self.generic_entities and current_prediction != "O":
+ elif (
+ allow_generic_entities
+ and current_annotation in self.generic_entities
+ and current_prediction != "O"
+ ):
# Ignore cases where the prediction is generic
results[(current_annotation, current_prediction)] -= 1
# Add a result which assumes the generic equals the specific
@@ -202,6 +213,7 @@ def calculate_score_on_df(
results_df: pd.DataFrame,
beta: float = 2.0,
level: Literal["entity", "pii", "both"] = "both",
+ allow_generic_entities: bool = True,
**kwargs,
) -> EvaluationResult:
"""
@@ -219,6 +231,9 @@ def calculate_score_on_df(
:param beta: F-beta parameter for score calculation (default 2.0).
:param level: Which metrics to compute. One of ``"entity"``, ``"pii"``,
or ``"both"`` (default).
+ :param allow_generic_entities: Whether generic labels such as ``PII`` may
+ satisfy a more-specific annotation. Hierarchical scoring disables
+ this because granularity projection is handled by CanonicalMapper.
:return: EvaluationResult with the requested precision/recall/F metrics.
"""
evaluation_results: list[EvaluationResult] = []
@@ -238,6 +253,7 @@ def calculate_score_on_df(
results, errors = self.compare(
input_sample=input_sample,
prediction=predictions,
+ allow_generic_entities=allow_generic_entities,
)
evaluation_results.append(
EvaluationResult(
diff --git a/pyproject.toml b/pyproject.toml
index 4405e129..d6749ae9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ version = "0.3.2"
description = "A framework for evaluating Presidio's Named Entity Recognition performance"
readme = "README.md"
license = {text = "MIT"}
-requires-python = ">=3.11,<3.14"
+requires-python = ">=3.11,<3.15"
authors = [
{name = "Presidio contributors"}
]
diff --git a/scripts/zensical_build.py b/scripts/zensical_build.py
new file mode 100644
index 00000000..11a188eb
--- /dev/null
+++ b/scripts/zensical_build.py
@@ -0,0 +1,368 @@
+#!/usr/bin/env python3
+"""Build (or serve) the Presidio-Research documentation with Zensical.
+
+Zensical (https://zensical.org) is the successor to Material for MkDocs. It can
+read an existing ``mkdocs.yml`` natively, but it does **not** run MkDocs plugins
+and it does not (yet) render Jupyter notebooks. Presidio-Research keeps its
+source of truth outside ``docs/``: the landing page is the top-level
+``README.md`` and the tutorials are notebooks under ``notebooks/``.
+
+So, before invoking Zensical, this script *collects* those sources into a
+staging copy of ``docs/``:
+
+ * ``README.md`` -> ``/docs/index.md`` (the site Home)
+ * ``notebooks/*.ipynb`` -> ``/docs/notebooks/*.ipynb``
+
+and then pre-converts every collected notebook to Markdown with ``nbconvert``
+so Zensical renders it natively (full theme, nav, search and TOC) instead of
+serving a raw ``.ipynb`` download.
+
+Notebook support is accepted on the Zensical backlog but unscheduled:
+ - request : https://github.com/zensical/zensical/issues/52
+ - backlog : https://github.com/zensical/backlog/issues/9
+
+Steps
+-----
+1. Load ``mkdocs.yml`` and collect every ``*.ipynb`` referenced in the nav.
+2. Mirror ``docs/`` into ``.zensical-build/docs/`` so the real source tree
+ stays untouched (Material-compatible, minimal diff, easy to rebase).
+3. Collect ``README.md`` -> ``index.md`` and the nav notebooks into the staging
+ tree, absolutise the README's repo-relative links, and convert each notebook
+ to Markdown (images land in a sibling ``_files/`` directory).
+4. Rewrite in-repo ``*.ipynb`` links to the converted ``*.md`` pages.
+5. Emit a generated ``zensical.yml`` whose ``docs_dir`` is the staging tree,
+ whose nav points at the ``.md`` files, and which drops MkDocs-only plugins.
+6. Run ``zensical build`` (default) or ``zensical serve`` against that config.
+
+Executables are taken from ``PATH`` by default. Override with:
+
+ JUPYTER_BIN path to the ``jupyter`` entry point (default: ``jupyter``)
+ ZENSICAL_BIN path to the ``zensical`` entry point (default: ``zensical``)
+
+Usage
+-----
+ python scripts/zensical_build.py # build to ./site
+ python scripts/zensical_build.py serve -a :8001 # serve (extra args passed on)
+"""
+
+from __future__ import annotations
+
+import os
+import re
+import shutil
+import subprocess
+import sys
+import urllib.parse
+from pathlib import Path
+
+import yaml
+
+REPO_ROOT = Path(__file__).resolve().parent.parent
+MKDOCS_CONFIG = REPO_ROOT / "mkdocs.yml"
+GENERATED_CONFIG = REPO_ROOT / "zensical.yml"
+
+# Where the README's repo-relative links (to source files that are not part of
+# the docs site) are re-pointed. Branch can be overridden in CI via DOCS_REF.
+REPO_SLUG = "data-privacy-stack/presidio-research"
+DOCS_REF = os.environ.get("DOCS_REF", "main")
+GH_BLOB = f"https://github.com/{REPO_SLUG}/blob/{DOCS_REF}"
+GH_RAW = f"https://raw.githubusercontent.com/{REPO_SLUG}/{DOCS_REF}"
+
+# The site Home page is collected from the top-level README.
+INDEX_SOURCE = REPO_ROOT / "README.md"
+
+# Everything Zensical-specific is generated into this staging tree so the real
+# ``docs/`` stays pristine (Material-compatible, minimal diff, easy rebase).
+STAGING_DIR = REPO_ROOT / ".zensical-build"
+STAGING_DOCS = STAGING_DIR / "docs"
+STAGING_DOCS_REL = STAGING_DOCS.relative_to(REPO_ROOT).as_posix()
+
+
+# --------------------------------------------------------------------------- #
+# YAML loading
+# --------------------------------------------------------------------------- #
+class _TolerantLoader(yaml.SafeLoader):
+ """SafeLoader that tolerates MkDocs' ``!!python/name:`` tags.
+
+ We only need to *read* the config to discover notebooks; the generated
+ config is produced by string transformation so these tags round-trip
+ untouched.
+ """
+
+
+def _ignore_python_name(loader, suffix, node): # noqa: ANN001, ARG001
+ return None
+
+
+_TolerantLoader.add_multi_constructor(
+ "tag:yaml.org,2002:python/name:", _ignore_python_name
+)
+
+
+def _collect_notebook_refs(nav) -> list[str]:
+ """Return every ``*.ipynb`` path referenced in a MkDocs nav structure."""
+ found: list[str] = []
+
+ def walk(node) -> None:
+ if isinstance(node, str):
+ if node.endswith(".ipynb"):
+ found.append(node)
+ elif isinstance(node, list):
+ for item in node:
+ walk(item)
+ elif isinstance(node, dict):
+ for value in node.values():
+ walk(value)
+
+ walk(nav)
+ # De-duplicate while preserving order.
+ return list(dict.fromkeys(found))
+
+
+# --------------------------------------------------------------------------- #
+# Staging
+# --------------------------------------------------------------------------- #
+def _stage_docs(src: Path, dst: Path) -> None:
+ """Mirror the real docs tree into the staging dir (source stays untouched)."""
+ if shutil.which("rsync"):
+ dst.mkdir(parents=True, exist_ok=True)
+ subprocess.run(["rsync", "-a", "--delete", f"{src}/", f"{dst}/"], check=True)
+ else:
+ if dst.exists():
+ shutil.rmtree(dst)
+ shutil.copytree(src, dst)
+
+
+# --------------------------------------------------------------------------- #
+# Collection (README -> index.md, notebooks/ -> docs)
+# --------------------------------------------------------------------------- #
+# Markdown link/image or HTML href/src whose URL is captured in group 2.
+_URL_RE = re.compile(r"""(!?\]\(|(?:href|src)=["'])([^)"'\s]+)""")
+
+
+def _absolutise_readme_links(text: str, notebook_rels: set[str]) -> str:
+ """Re-point the README's repo-relative links so they work on the site.
+
+ Links to collected notebooks are left relative (the notebook-link pass later
+ rewrites them to the rendered ``.md`` page). Any other repo-relative link is
+ re-pointed at GitHub so it does not 404 on the docs site; images use the raw
+ host, everything else the blob host. Absolute URLs and pure anchors are left
+ untouched.
+ """
+
+ def repl(match: re.Match) -> str:
+ prefix, url = match.group(1), match.group(2)
+ if url.startswith(("http://", "https://", "//", "mailto:", "#")):
+ return match.group(0)
+ path, _, frag = url.partition("#")
+ clean_path = path.lstrip("./")
+ # Notebook links become on-site pages; leave them for the .ipynb pass.
+ if clean_path in notebook_rels:
+ return match.group(0)
+ # Links into docs/ resolve to on-site pages: the docs tree is the site
+ # root, so drop the leading ``docs/`` and keep the link relative.
+ if clean_path.startswith("docs/") and clean_path.endswith(".md"):
+ rel = clean_path[len("docs/") :]
+ return f"{prefix}{rel}" + (f"#{frag}" if frag else "")
+ is_image = prefix.startswith("!") or prefix.startswith(("src=",))
+ base = GH_RAW if is_image else GH_BLOB
+ clean = path.lstrip("./")
+ return f"{prefix}{base}/{clean}" + (f"#{frag}" if frag else "")
+
+ return _URL_RE.sub(repl, text)
+
+
+def _collect_sources(notebook_rels: list[str], docs_dir: Path) -> None:
+ """Collect README (-> index.md) and nav notebooks into the staging docs."""
+ # 1) README.md -> index.md, with repo-relative links absolutised.
+ if not INDEX_SOURCE.exists():
+ raise SystemExit(f"error: index source not found: {INDEX_SOURCE}")
+ nb_names = {rel.lstrip("./") for rel in notebook_rels}
+ index_text = INDEX_SOURCE.read_text(encoding="utf-8")
+ index_text = _absolutise_readme_links(index_text, nb_names)
+ (docs_dir / "index.md").write_text(index_text, encoding="utf-8")
+ print(f" collected README.md -> {docs_dir.name}/index.md")
+
+ # 2) notebooks referenced in nav (paths are docs-relative and mirror the
+ # repo layout, e.g. ``notebooks/1_Generate_data.ipynb``).
+ for rel in notebook_rels:
+ src = REPO_ROOT / rel
+ if not src.exists():
+ print(f" ! skip (missing notebook): {rel}", file=sys.stderr)
+ continue
+ dst = docs_dir / rel
+ dst.parent.mkdir(parents=True, exist_ok=True)
+ shutil.copy2(src, dst)
+ print(f" collected {rel}")
+
+
+# --------------------------------------------------------------------------- #
+# Notebook conversion
+# --------------------------------------------------------------------------- #
+def _convert_notebooks(notebooks: list[str], docs_dir: Path) -> list[Path]:
+ """Convert notebooks to Markdown in place; return generated paths."""
+ jupyter = os.environ.get("JUPYTER_BIN", "jupyter")
+ generated: list[Path] = []
+
+ for rel in notebooks:
+ src = docs_dir / rel
+ if not src.exists():
+ print(f" ! skip (missing): {rel}", file=sys.stderr)
+ continue
+
+ out_md = src.with_suffix(".md")
+ print(f" - {rel} -> {out_md.relative_to(docs_dir)}")
+ subprocess.run(
+ [
+ jupyter,
+ "nbconvert",
+ "--to",
+ "markdown",
+ "--output",
+ src.stem,
+ "--output-dir",
+ str(src.parent),
+ str(src),
+ ],
+ check=True,
+ stdout=subprocess.DEVNULL,
+ )
+ # Drop the raw .ipynb from the staging tree so Zensical does not also
+ # copy it into the build output as a downloadable asset.
+ src.unlink()
+ generated.append(out_md)
+
+ return generated
+
+
+# --------------------------------------------------------------------------- #
+# Link rewriting
+# --------------------------------------------------------------------------- #
+# Matches the URL of a Markdown link ``](...ipynb`` or an HTML ``href="...ipynb``.
+# The match stops at ``.ipynb`` so any ``#fragment`` is preserved untouched.
+_LINK_RE = re.compile(r"""(\]\(|href=["'])([^)"'#?\s]+\.ipynb)""")
+
+
+def _rewrite_links(docs_dir: Path, notebooks: list[str]) -> None:
+ """Point in-repo ``*.ipynb`` links at the converted ``*.md`` pages.
+
+ Only links that resolve to a converted notebook are touched — external
+ (GitHub) links and links to non-converted notebooks are left alone.
+ """
+ converted = {(docs_dir / rel).resolve() for rel in notebooks}
+ total = 0
+
+ for md in docs_dir.rglob("*.md"):
+ base = md.parent
+ text = md.read_text(encoding="utf-8")
+ hits = 0
+
+ def repl(match: re.Match) -> str:
+ nonlocal hits
+ prefix, url = match.group(1), match.group(2)
+ if url.startswith(("http://", "https://", "//", "mailto:")):
+ return match.group(0)
+ target = (base / urllib.parse.unquote(url)).resolve()
+ if target in converted:
+ hits += 1
+ return prefix + url[: -len(".ipynb")] + ".md"
+ return match.group(0)
+
+ new = _LINK_RE.sub(repl, text)
+ if hits:
+ md.write_text(new, encoding="utf-8")
+ total += hits
+
+ print(f" rewrote {total} notebook link(s) to .md")
+
+
+# --------------------------------------------------------------------------- #
+# Generated config
+# --------------------------------------------------------------------------- #
+def _write_generated_config(raw: str, notebooks: list[str]) -> None:
+ """Rewrite nav .ipynb -> .md, drop MkDocs-only plugins, point at staging."""
+ text = raw
+ for rel in notebooks:
+ text = text.replace(rel, rel[: -len(".ipynb")] + ".md")
+
+ # Remove any ``- mkdocs-*:`` plugin entry and its indented children (Zensical
+ # does not run MkDocs plugins; ``search`` is built in).
+ text = re.sub(
+ r"[ \t]*-[ \t]*mkdocs-[^\n]*\n(?:[ \t]+[^\n]*\n)*",
+ "",
+ text,
+ )
+
+ # Build from the staging copy (converted notebooks + rewritten links).
+ if re.search(r"(?m)^docs_dir:.*$", text):
+ text = re.sub(r"(?m)^docs_dir:.*$", f"docs_dir: {STAGING_DOCS_REL}", text)
+ else:
+ text = f"docs_dir: {STAGING_DOCS_REL}\n" + text
+
+ banner = (
+ "# AUTOGENERATED by scripts/zensical_build.py - DO NOT EDIT.\n"
+ "# Source of truth is mkdocs.yml. The Home page and notebook tutorials\n"
+ "# are collected into a staging docs tree and notebook nav entries have\n"
+ "# been converted to Markdown, because Zensical does not run MkDocs\n"
+ "# plugins or render notebooks (backlog: zensical/backlog#9).\n"
+ f"# docs_dir points at the generated staging tree ({STAGING_DOCS_REL}).\n\n"
+ )
+ GENERATED_CONFIG.write_text(banner + text, encoding="utf-8")
+ print(f" wrote {GENERATED_CONFIG.relative_to(REPO_ROOT)}")
+
+
+# --------------------------------------------------------------------------- #
+# Main
+# --------------------------------------------------------------------------- #
+def main(argv: list[str]) -> int:
+ """Stage docs, collect+convert sources, write zensical.yml, build or serve."""
+ command = argv[0] if argv else "build"
+ passthrough = argv[1:]
+ if command not in {"build", "serve"}:
+ # Treat unknown first arg as a passthrough flag for `build`.
+ command, passthrough = "build", argv
+
+ raw = MKDOCS_CONFIG.read_text(encoding="utf-8")
+ config = yaml.load(raw, Loader=_TolerantLoader)
+ docs_dir = REPO_ROOT / config.get("docs_dir", "docs")
+
+ notebooks = _collect_notebook_refs(config.get("nav", []))
+
+ print(f"Staging docs -> {STAGING_DOCS_REL}")
+ _stage_docs(docs_dir, STAGING_DOCS)
+
+ print("Collecting sources (README + notebooks)...")
+ _collect_sources(notebooks, STAGING_DOCS)
+
+ print(f"Converting {len(notebooks)} notebook(s)...")
+ _convert_notebooks(notebooks, STAGING_DOCS)
+
+ print("Rewriting notebook links...")
+ _rewrite_links(STAGING_DOCS, notebooks)
+
+ print("Generating Zensical config...")
+ _write_generated_config(raw, notebooks)
+
+ zensical = os.environ.get("ZENSICAL_BIN", "zensical")
+ cmd = [zensical, command, "-f", str(GENERATED_CONFIG), *passthrough]
+ print(f"Running: {' '.join(cmd)}")
+
+ # ``serve`` is interactive/long-running, so run it once. ``build`` can be
+ # killed by the OS on memory spikes; that crash is transient, so retry a
+ # couple of times before giving up.
+ if command != "build":
+ return subprocess.run(cmd, check=False).returncode
+
+ attempts = max(1, int(os.environ.get("ZENSICAL_BUILD_RETRIES", "3")))
+ rc = 0
+ for attempt in range(1, attempts + 1):
+ rc = subprocess.run(cmd, check=False).returncode
+ if rc == 0:
+ return 0
+ print(f" zensical build failed (exit {rc}), attempt {attempt}/{attempts}")
+ print(f" zensical build still failing after {attempts} attempt(s)")
+ return rc
+
+
+if __name__ == "__main__":
+ raise SystemExit(main(sys.argv[1:]))
diff --git a/tests/data_generator/test_providers.py b/tests/data_generator/test_providers.py
index cc389fe2..bed5eba1 100644
--- a/tests/data_generator/test_providers.py
+++ b/tests/data_generator/test_providers.py
@@ -1,3 +1,5 @@
+from unittest.mock import MagicMock, patch
+
from faker import Faker
from presidio_evaluator.data_generator.faker_extensions import (
@@ -26,3 +28,30 @@ def test_hospital_provider():
faker.add_provider(HospitalProvider)
element = faker.hospital_name()
assert element
+
+
+@patch("presidio_evaluator.data_generator.faker_extensions.providers.requests.get")
+def test_hospital_provider_sends_user_agent_header(mock_get):
+ mock_response = MagicMock()
+ mock_response.status_code = 200
+ mock_response.json.return_value = {"results": {"bindings": []}}
+ mock_get.return_value = mock_response
+
+ faker = Faker()
+ faker.add_provider(HospitalProvider)
+
+ mock_get.assert_called_once()
+ _, call_kwargs = mock_get.call_args
+ assert "User-Agent" in call_kwargs["headers"]
+
+
+@patch("presidio_evaluator.data_generator.faker_extensions.providers.requests.get")
+def test_hospital_provider_falls_back_on_non_200_response(mock_get):
+ mock_response = MagicMock()
+ mock_response.status_code = 403
+ mock_get.return_value = mock_response
+
+ faker = Faker()
+ provider = HospitalProvider(faker)
+
+ assert provider.hospitals == provider.default_list
diff --git a/tests/entity_mapping/test_canonical_mapper.py b/tests/entity_mapping/test_canonical_mapper.py
index 763edaf8..3e72c76a 100644
--- a/tests/entity_mapping/test_canonical_mapper.py
+++ b/tests/entity_mapping/test_canonical_mapper.py
@@ -51,6 +51,19 @@ def test_custom_hierarchy_dict(self):
mapper = CanonicalMapper(hierarchy=custom)
assert "MY_TYPE" in mapper._hierarchy.all_canonical_entities
+ def test_hierarchy_mutation_after_construction_updates_projection(self):
+ from presidio_evaluator.entity_mapping import EntityHierarchy # noqa: PLC0415
+
+ hierarchy = EntityHierarchy(
+ hierarchy={"PII": {"CUSTOM_PARENT": {"CUSTOM_CHILD": []}}},
+ canonical_depth=10,
+ )
+ mapper = CanonicalMapper(hierarchy=hierarchy)
+ hierarchy.add_alias("CUSTOM_CHILD", "NEW_CHILD_ALIAS")
+ mapper.analyze(_make_df(["CUSTOM_PARENT"], ["NEW_CHILD_ALIAS"]))
+ mapped = mapper.get_mapped_results_dataframe()
+ assert mapped.detailed["prediction"].tolist() == ["CUSTOM_PARENT"]
+
def test_repr(self):
mapper = CanonicalMapper()
assert "CanonicalMapper" in repr(mapper)
@@ -219,6 +232,44 @@ def test_collision_same_branch_info(self):
assert issue.severity == IssueSeverity.INFO
assert issue.overlap_counts is not None
+ def test_mixed_annotation_depths_are_informational(self):
+ df = _make_df(["PERSON", "NAME"], ["NAME", "NAME"])
+ mapper = CanonicalMapper()
+ mapper.analyze(df, min_severity="INFO")
+ issues = [
+ issue
+ for issue in mapper.get_issues()
+ if issue.type == IssueType.COLLISION_SAME_BRANCH
+ and "multiple hierarchy depths" in issue.message
+ ]
+ assert len(issues) == 1
+ assert issues[0].severity == IssueSeverity.INFO
+ assert issues[0].resolution_options == []
+
+ mapped = mapper.get_mapped_results_dataframe()
+ assert mapped.detailed["annotation"].tolist() == ["PERSON", "NAME"]
+ # NAME is itself annotated, so NAME predictions stay at NAME.
+ assert mapped.detailed["prediction"].tolist() == ["NAME", "NAME"]
+
+ def test_root_and_deeper_annotations_report_one_mixed_depth_info(self):
+ df = _make_df(["PII", "PERSON", "NAME"], ["PII", "PERSON", "NAME"])
+ mapper = CanonicalMapper()
+ mapper.analyze(df, min_severity="INFO")
+ issues = [
+ issue
+ for issue in mapper.get_issues()
+ if issue.type == IssueType.COLLISION_SAME_BRANCH
+ and "multiple hierarchy depths" in issue.message
+ ]
+
+ assert len(issues) == 1
+ assert issues[0].severity == IssueSeverity.INFO
+ assert issues[0].labels == ["NAME", "PERSON", "PII"]
+ # Every gold depth is preserved; each prediction matches its own gold.
+ mapped = mapper.get_mapped_results_dataframe()
+ assert mapped.detailed["annotation"].tolist() == ["PII", "PERSON", "NAME"]
+ assert mapped.detailed["prediction"].tolist() == ["PII", "PERSON", "NAME"]
+
def test_cross_branch_suppressed_when_same_branch_hit_exists(self):
# LOCATION has same-branch hits (predicted on STREET_ADDRESS tokens) AND
# cross-branch FPs (predicted on NAME tokens) → Case 1 → no warning.
@@ -613,22 +664,44 @@ def test_scenario1_binary_both_sides_become_pii(self):
assert set(r.binary["annotation"].unique()) == {"PII"}
assert set(r.binary["prediction"].unique()) == {"PII"}
- def test_scenario1_branch_annotation_stays_pii_prediction_becomes_person(self):
- """PII (depth-1) has no depth-2 ancestor; stays as PII. PERSON/NAME → PERSON."""
+ def test_scenario1_branch_projects_predictions_to_pii(self):
+ """A root-level gold vocabulary projects all descendant predictions to PII."""
r = self._mapped(["PII", "PII"], ["PERSON", "NAME"])
assert r.branch["annotation"].tolist() == ["PII", "PII"]
- assert r.branch["prediction"].tolist() == ["PERSON", "PERSON"]
+ assert r.branch["prediction"].tolist() == ["PII", "PII"]
- def test_scenario1_detailed_annotation_stays_pii_prediction_uses_native_depth(self):
- """PII stays PII; PERSON stays PERSON; NAME stays NAME at detailed level."""
+ def test_scenario1_detailed_projects_predictions_to_pii(self):
+ """Detailed mapping uses the single root-level gold target."""
r = self._mapped(["PII", "PII"], ["PERSON", "NAME"])
assert r.detailed["annotation"].tolist() == ["PII", "PII"]
- assert r.detailed["prediction"].tolist() == ["PERSON", "NAME"]
+ assert r.detailed["prediction"].tolist() == ["PII", "PII"]
# ------------------------------------------------------------------
# Scenario 2: dataset=depths 2 & 3, model=depth-1 (PII)
# ------------------------------------------------------------------
+ def test_scenario2_mixed_depth_gold_is_informational_only(self):
+ """Mixed gold depths are reported but never block mapping."""
+ df = _make_df(["PERSON", "NAME"], ["PII", "PII"])
+ mapper = CanonicalMapper()
+ mapper.analyze(df, min_severity="INFO")
+ blocking = [
+ issue
+ for issue in mapper.get_issues()
+ if issue.severity == IssueSeverity.ERROR
+ ]
+ assert blocking == []
+ mixed = [
+ issue
+ for issue in mapper.get_issues()
+ if issue.type == IssueType.COLLISION_SAME_BRANCH
+ and "multiple hierarchy depths" in issue.message
+ ]
+ assert len(mixed) == 1
+ assert mixed[0].severity == IssueSeverity.INFO
+ assert mixed[0].resolution_options == []
+ assert mixed[0].labels == ["NAME", "PERSON"]
+
def test_scenario2_original_preserves_raw_labels(self):
r = self._mapped(["PERSON", "NAME"], ["PII", "PII"])
assert r.original["annotation"].tolist() == ["PERSON", "NAME"]
@@ -639,53 +712,51 @@ def test_scenario2_binary_both_sides_become_pii(self):
assert set(r.binary["annotation"].unique()) == {"PII"}
assert set(r.binary["prediction"].unique()) == {"PII"}
- def test_scenario2_branch_annotation_becomes_person_prediction_stays_pii(self):
- """PERSON → PERSON, NAME → PERSON at branch; PII stays PII."""
+ def test_scenario2_branch_annotations_become_person_prediction_stays_pii(self):
+ """Both gold depths land on the PERSON branch; PII is too coarse to project."""
r = self._mapped(["PERSON", "NAME"], ["PII", "PII"])
assert r.branch["annotation"].tolist() == ["PERSON", "PERSON"]
assert r.branch["prediction"].tolist() == ["PII", "PII"]
- def test_scenario2_detailed_annotation_native_prediction_stays_pii(self):
- """PERSON stays PERSON, NAME stays NAME; PII stays PII at detailed."""
+ def test_scenario2_detailed_keeps_each_gold_depth_distinct(self):
+ """No collapsing: PERSON and NAME each keep their own detailed row."""
r = self._mapped(["PERSON", "NAME"], ["PII", "PII"])
assert r.detailed["annotation"].tolist() == ["PERSON", "NAME"]
assert r.detailed["prediction"].tolist() == ["PII", "PII"]
+ def test_scenario2_predictions_keep_their_depth_when_both_depths_are_gold(self):
+ """NAME is itself annotated, so a NAME prediction is not folded into PERSON."""
+ r = self._mapped(["PERSON", "NAME"], ["NAME", "PERSON"])
+ assert r.detailed["annotation"].tolist() == ["PERSON", "NAME"]
+ assert r.detailed["prediction"].tolist() == ["NAME", "PERSON"]
+
# ------------------------------------------------------------------
# Scenario 3: dataset=depth-2 + depth-3; model=depth-2 only
# Annotations: PERSON, NAME, LOCATION, GPE
# Predictions: PERSON, PERSON, LOCATION, LOCATION
# ------------------------------------------------------------------
- def test_scenario3_binary_all_pii(self):
- r = self._mapped(
- ["PERSON", "NAME", "LOCATION", "GPE"],
- ["PERSON", "PERSON", "LOCATION", "LOCATION"],
- )
- assert set(r.binary["annotation"].unique()) == {"PII"}
- assert set(r.binary["prediction"].unique()) == {"PII"}
-
- def test_scenario3_branch_depth3_annotations_fold_to_depth2(self):
- """NAME→PERSON, GPE→LOCATION at branch; depth-2 annotations/predictions unchanged."""
- r = self._mapped(
+ def test_scenario3_mixed_depth_gold_reports_one_info_per_branch(self):
+ df = _make_df(
["PERSON", "NAME", "LOCATION", "GPE"],
["PERSON", "PERSON", "LOCATION", "LOCATION"],
)
- assert r.branch["annotation"].tolist() == [
- "PERSON",
- "PERSON",
- "LOCATION",
- "LOCATION",
- ]
- assert r.branch["prediction"].tolist() == [
- "PERSON",
- "PERSON",
- "LOCATION",
- "LOCATION",
+ mapper = CanonicalMapper()
+ mapper.analyze(df, min_severity="INFO")
+ assert [
+ i for i in mapper.get_issues() if i.severity == IssueSeverity.ERROR
+ ] == []
+ mixed = [
+ issue
+ for issue in mapper.get_issues()
+ if issue.type == IssueType.COLLISION_SAME_BRANCH
+ and "multiple hierarchy depths" in issue.message
]
+ assert len(mixed) == 2
+ assert all(issue.severity == IssueSeverity.INFO for issue in mixed)
- def test_scenario3_detailed_depth2_annotations_stay_depth3_annotations_stay(self):
- """At detailed: PERSON stays PERSON, NAME stays NAME, GPE stays GPE; predictions stay depth-2."""
+ def test_scenario3_detailed_credits_shallow_gold_and_misses_deep_gold(self):
+ """Depth-2 gold is matched by the depth-2 prediction; depth-3 gold is not."""
r = self._mapped(
["PERSON", "NAME", "LOCATION", "GPE"],
["PERSON", "PERSON", "LOCATION", "LOCATION"],
@@ -739,11 +810,11 @@ def test_scenario5_branch_both_become_person(self):
assert r.branch["annotation"].tolist() == ["PERSON"]
assert r.branch["prediction"].tolist() == ["PERSON"]
- def test_scenario5_detailed_annotation_is_person_prediction_is_name(self):
- """PERSON stays PERSON; FIRST_NAME resolves to NAME at canonical_depth=3."""
+ def test_scenario5_detailed_prediction_projects_to_person(self):
+ """The more-specific NAME prediction projects to the PERSON gold depth."""
r = self._mapped(["PERSON"], ["FIRST_NAME"])
assert r.detailed["annotation"].tolist() == ["PERSON"]
- assert r.detailed["prediction"].tolist() == ["NAME"]
+ assert r.detailed["prediction"].tolist() == ["PERSON"]
# ---------------------------------------------------------------------------
@@ -866,6 +937,15 @@ def test_collision_same_branch_card_hidden_at_warning(self):
# The gap card text for COLLISION_SAME_BRANCH should NOT appear in gap_cards
assert "Handled automatically" not in html
+ def test_mixed_depth_card_explains_deepest_ancestor_projection(self):
+ df = _make_df(["PERSON", "NAME"], ["NAME", "NAME"])
+ mapper = CanonicalMapper()
+ mapper.analyze(df, min_severity="INFO")
+ html = MapperRenderer(mapper).build_html()
+ assert "multiple hierarchy depths" in html
+ assert "deepest annotated ancestor" in html
+ assert "No action required" in html
+
# ---------------------------------------------------------------------------
# EntityHierarchy.get_depth
@@ -943,3 +1023,73 @@ def test_full_pipeline_with_unresolved(self):
mapper.map({"UNKNOWN_XYZZY_99": "NAME"})
result = mapper.get_mapped_results_dataframe()
assert isinstance(result, MappedResults)
+
+
+# ---------------------------------------------------------------------------
+# Regression: the repository's primary dataset must map without intervention
+# ---------------------------------------------------------------------------
+
+#: Entity types annotated in ``data/synth_dataset_v2.json`` (used by notebooks 4 & 5).
+#: ``PERSON`` (depth 2) and ``TITLE`` (depth 3) sit on the same branch at
+#: different depths, which is the case that motivated the deepest-ancestor rule.
+_SYNTH_V2_ENTITIES = [
+ "AGE",
+ "CREDIT_CARD",
+ "DATE_TIME",
+ "DOMAIN_NAME",
+ "EMAIL_ADDRESS",
+ "GPE",
+ "IBAN_CODE",
+ "IP_ADDRESS",
+ "NRP",
+ "ORGANIZATION",
+ "PERSON",
+ "PHONE_NUMBER",
+ "STREET_ADDRESS",
+ "TITLE",
+ "US_DRIVER_LICENSE",
+ "US_SSN",
+ "ZIP_CODE",
+]
+
+
+class TestSynthDatasetV2Vocabulary:
+ """Pin that the shipped dataset's gold vocabulary needs no mapping decision."""
+
+ def test_full_vocabulary_maps_without_blocking(self):
+ df = _make_df(_SYNTH_V2_ENTITIES, _SYNTH_V2_ENTITIES)
+ mapper = CanonicalMapper()
+ mapper.analyze(df)
+ assert [
+ i for i in mapper.get_issues() if i.severity == IssueSeverity.ERROR
+ ] == []
+ mapper.get_mapped_results_dataframe() # must not raise
+
+ def test_title_survives_detailed_mapping(self):
+ """TITLE must keep its own detailed rows rather than collapsing into PERSON."""
+ df = _make_df(_SYNTH_V2_ENTITIES, _SYNTH_V2_ENTITIES)
+ mapper = CanonicalMapper()
+ mapper.analyze(df)
+ detailed = mapper.get_mapped_results_dataframe().detailed
+ assert "TITLE" in set(detailed["annotation"])
+ assert "TITLE" in set(detailed["prediction"])
+
+ def test_person_and_title_need_no_resolution(self):
+ """Minimal reproduction of the PERSON (depth-2) + TITLE (depth-3) collision."""
+ df = _make_df(["PERSON", "TITLE", "PERSON"], ["NAME", "TITLE", "PERSON"])
+ mapper = CanonicalMapper()
+ mapper.analyze(df)
+ detailed = mapper.get_mapped_results_dataframe().detailed
+ assert detailed["annotation"].tolist() == ["PERSON", "TITLE", "PERSON"]
+ # NAME has no annotated NAME ancestor, so it credits PERSON; TITLE is
+ # annotated in its own right and is therefore preserved.
+ assert detailed["prediction"].tolist() == ["PERSON", "TITLE", "PERSON"]
+
+ def test_coarse_prediction_does_not_steal_credit_from_title(self):
+ """A PERSON prediction over a TITLE gold stays a mismatch."""
+ df = _make_df(["PERSON", "TITLE"], ["PERSON", "PERSON"])
+ mapper = CanonicalMapper()
+ mapper.analyze(df)
+ detailed = mapper.get_mapped_results_dataframe().detailed
+ assert detailed["annotation"].tolist() == ["PERSON", "TITLE"]
+ assert detailed["prediction"].tolist() == ["PERSON", "PERSON"]
diff --git a/tests/entity_mapping/test_entity_hierarchy.py b/tests/entity_mapping/test_entity_hierarchy.py
index c30bd6cc..2a908686 100644
--- a/tests/entity_mapping/test_entity_hierarchy.py
+++ b/tests/entity_mapping/test_entity_hierarchy.py
@@ -5,6 +5,7 @@
EntityHierarchy,
EntityNotMappedError,
)
+from presidio_evaluator.entity_mapping.hierarchy import BRANCH_ALIASES_KEY
_h = EntityHierarchy()
@@ -58,8 +59,8 @@ def test_location_alias(self):
assert _h.canonicalize("LOCATION") == "LOCATION"
def test_organization_alias(self):
- # ORG is its own canonical leaf node
- assert _h.canonicalize("ORG") == "ORG"
+ # ORG is a branch-level alias of ORGANIZATION (see BRANCH_ALIASES_KEY)
+ assert _h.canonicalize("ORG") == "ORGANIZATION"
# ---------------------------------------------------------------------------
@@ -242,9 +243,10 @@ def test_location(self):
assert _h.get_branch("LOCATION") == ["PII", "LOCATION"]
def test_organization(self):
+ # ORG is now a branch-level alias of ORGANIZATION
branch = _h.get_branch("ORG")
assert branch is not None
- assert branch[-1] == "ORG"
+ assert branch[-1] == "ORGANIZATION"
# ---------------------------------------------------------------------------
@@ -445,6 +447,15 @@ def test_add_alias_unknown_entity_raises(self):
with pytest.raises(KeyError):
h.add_alias("NONEXISTENT_ENTITY", "SOME_ALIAS")
+ def test_add_alias_reserved_key_raises(self):
+ # The reserved branch-alias key is not an entity, so it must not be
+ # addressable as one — otherwise the alias would be silently attached
+ # to whichever branch happens to be found first in the tree walk.
+ h = EntityHierarchy()
+ with pytest.raises(KeyError):
+ h.add_alias(BRANCH_ALIASES_KEY, "SOME_ALIAS")
+ assert h.normalize("SOME_ALIAS") not in h.raw_to_canonical
+
def test_add_alias_rebuilds_lookup(self):
h = EntityHierarchy()
h.add_alias("SSN", "MY_SSN")
@@ -495,3 +506,172 @@ def test_exact_country_fuzzy_suffix_canonicalize(self):
def test_get_branch_after_fuzzy_country(self):
canonical = self.h.canonicalize("ARGENTENIAN_TAX_ID")
assert self.h.get_branch(canonical) == ["PII", "GOVERNMENT_ID", "TAX_ID"]
+
+
+# ---------------------------------------------------------------------------
+# Branch-level aliases (BRANCH_ALIASES_KEY)
+# ---------------------------------------------------------------------------
+
+
+class TestBranchAliases:
+ """Branch (non-leaf) nodes can declare raw aliases via the reserved
+ `_aliases` key; those resolve to the branch and never become entities."""
+
+ def setup_method(self):
+ self.h = EntityHierarchy()
+
+ def test_loc_resolves_to_location(self):
+ assert self.h.canonicalize("LOC") == "LOCATION"
+
+ def test_org_resolves_to_organization(self):
+ assert self.h.canonicalize("ORG") == "ORGANIZATION"
+
+ def test_loc_and_location_share_one_leaf(self):
+ assert self.h.canonicalize("LOC") == self.h.canonicalize("LOCATION")
+
+ def test_org_and_organization_share_one_leaf(self):
+ assert self.h.canonicalize("ORG") == self.h.canonicalize("ORGANIZATION")
+
+ def test_branch_alias_gets_branch_path(self):
+ assert self.h.get_branch("LOC") == ["PII", "LOCATION"]
+ assert self.h.get_branch("ORG") == ["PII", "ORGANIZATION"]
+
+ def test_reserved_key_is_not_a_canonical_entity(self):
+ assert "_aliases" not in self.h.all_canonical_entities
+ assert "_aliases" not in self.h.canonical_to_branch
+
+ def test_unrelated_leaves_unchanged(self):
+ assert self.h.canonicalize("EMAIL") == "EMAIL_ADDRESS"
+ assert self.h.canonicalize("GPE") == "GPE"
+ assert self.h.canonicalize("GEO") == "GEO"
+
+ def test_add_alias_on_branch_node(self):
+ h = EntityHierarchy()
+ h.add_alias("LOCATION", "GEOGRAPHIC_LOCATION")
+ assert h.canonicalize("GEOGRAPHIC_LOCATION") == "LOCATION"
+ # the alias must not leak in as a standalone canonical entity
+ assert "GEOGRAPHIC_LOCATION" not in h.all_canonical_entities
+
+ def test_add_alias_on_branch_is_idempotent(self):
+ h = EntityHierarchy()
+ h.add_alias("ORGANIZATION", "FIRM")
+ h.add_alias("ORGANIZATION", "FIRM")
+ assert h.canonicalize("FIRM") == "ORGANIZATION"
+
+ def test_reserved_key_never_leaks_at_deep_nodes(self):
+ # _aliases on a canonical-depth dict node must not surface the literal
+ # "_aliases" string as a resolvable alias.
+ import copy
+
+ from presidio_evaluator.entity_mapping.hierarchy import BRANCH_ALIASES_KEY
+
+ h = EntityHierarchy()
+ hier = copy.deepcopy(h.hierarchy)
+ hier["PII"]["DEMOGRAPHIC"]["PHYSICAL_DESCRIPTOR"][BRANCH_ALIASES_KEY] = [
+ "BODY_DESC"
+ ]
+ h2 = EntityHierarchy(hierarchy=hier)
+ assert h2.normalize(BRANCH_ALIASES_KEY) not in h2.raw_to_canonical
+ assert h2.canonicalize("BODY_DESC") == "PHYSICAL_DESCRIPTOR"
+
+ # ── behavior change: LOC/ORG are aliases, no longer canonical entities ──
+
+ def test_loc_and_org_are_no_longer_canonical_entities(self):
+ # They were empty leaf nodes before; they are branch aliases now.
+ assert "LOC" not in self.h.all_canonical_entities
+ assert "ORG" not in self.h.all_canonical_entities
+ assert "LOC" not in self.h.canonical_to_branch
+ assert "ORG" not in self.h.canonical_to_branch
+
+ def test_to_branch_resolves_branch_aliases(self):
+ # Regression guard: to_branch must not silently pass a raw alias
+ # through into a different bucket.
+ assert self.h.to_branch("LOC") == "LOCATION"
+ assert self.h.to_branch("ORG") == "ORGANIZATION"
+
+ def test_to_branch_resolves_leaf_aliases(self):
+ assert self.h.to_branch("COMPANYNAME") == "ORGANIZATION"
+
+ def test_to_branch_passes_through_unknown_labels(self):
+ assert self.h.to_branch("NOT_AN_ENTITY") == "NOT_AN_ENTITY"
+
+ def test_get_depth_resolves_branch_aliases(self):
+ assert self.h.get_depth("LOC") == self.h.get_depth("LOCATION")
+ assert self.h.get_depth("ORG") == self.h.get_depth("ORGANIZATION")
+
+ def test_add_alias_accepts_an_alias_as_the_subject(self):
+ h = EntityHierarchy()
+ h.add_alias("LOC", "LOCALITY")
+ assert h.canonicalize("LOCALITY") == "LOCATION"
+
+ def test_branch_alias_at_non_default_canonical_depth(self):
+ # CanonicalMapper builds EntityHierarchy(canonical_depth=10).
+ for depth in (2, 3, 4, 10):
+ h = EntityHierarchy(canonical_depth=depth)
+ assert h.canonicalize("LOC") == "LOCATION", f"depth={depth}"
+ assert h.canonicalize("ORG") == "ORGANIZATION", f"depth={depth}"
+ assert BRANCH_ALIASES_KEY not in h.all_canonical_entities
+
+ def test_branch_alias_shadowed_by_descendant_is_reported(self):
+ # "CITY" already resolves to ADDRESS; adding it as a LOCATION branch
+ # alias cannot win, and must not silently pretend to have worked.
+ h = EntityHierarchy()
+ before = h.canonicalize("CITY")
+ with pytest.raises(ValueError, match="already resolves"):
+ h.add_alias("LOCATION", "CITY")
+ assert h.canonicalize("CITY") == before
+
+ def test_per_resolves_to_person(self):
+ assert self.h.canonicalize("PER") == "PERSON"
+ assert self.h.to_branch("PER") == "PERSON"
+ assert "PER" not in self.h.all_canonical_entities
+
+ def test_failed_add_alias_keeps_existing_alias(self):
+ # "VRN" is already an alias of both LICENSE_PLATE_NUMBER and
+ # LICENSE_PLATE. Re-adding it to the former must raise WITHOUT
+ # stripping the alias it already owns.
+ h = EntityHierarchy()
+ before = h.canonicalize("VRN")
+ with pytest.raises(ValueError, match="already resolves"):
+ h.add_alias("LICENSE_PLATE_NUMBER", "VRN")
+ assert h.canonicalize("VRN") == before
+ node = h._find_node("LICENSE_PLATE_NUMBER")
+ assert "VRN" in node[0][node[1]]
+
+ def test_statically_shadowed_branch_alias_warns(self, caplog):
+ import copy
+ import logging
+
+ hier = copy.deepcopy(HIERARCHY)
+ # "CITY" already resolves to ADDRESS, so this branch alias is dead.
+ hier["PII"]["LOCATION"][BRANCH_ALIASES_KEY] = ["LOC", "CITY"]
+ with caplog.at_level(logging.WARNING):
+ EntityHierarchy(hierarchy=hier)
+ assert any("shadowed" in r.message for r in caplog.records)
+
+ def test_clean_hierarchy_emits_no_shadow_warning(self, caplog):
+ import logging
+
+ with caplog.at_level(logging.WARNING):
+ EntityHierarchy()
+ assert not [r for r in caplog.records if "shadowed" in r.message]
+
+ def test_i2b2_patient_is_a_name_not_a_record_number(self):
+ # In the i2b2/n2c2 2014 de-identification schema, PATIENT and DOCTOR are
+ # both subtypes of the NAME category; MEDICALRECORD is the ID subtype.
+ # "PATIENT" tags a person's name ("Yosef Villegas"), so it must resolve
+ # like DOCTOR and PATIENT_NAME, not like a medical record number.
+ for label in ("PATIENT", "DOCTOR", "PATIENT_NAME"):
+ assert self.h.canonicalize(label) == "NAME"
+ assert self.h.to_branch(label) == "PERSON"
+
+ # ...while the record-number aliases stay in PHI.
+ for label in ("MEDICALRECORD", "MEDICAL_RECORD"):
+ assert self.h.canonicalize(label) == "PATIENT_ID"
+ assert self.h.to_branch(label) == "PHI"
+
+ # MEDICAL_RECORD_NUMBER is listed under both PATIENT_ID and MRN, and MRN
+ # currently wins. That pre-existing shadowing is out of scope here; it is
+ # asserted at branch level so this test does not silently encode which
+ # leaf happens to win.
+ assert self.h.to_branch("MEDICAL_RECORD_NUMBER") == "PHI"
diff --git a/tests/entity_mapping/test_hierarchical_evaluation.py b/tests/entity_mapping/test_hierarchical_evaluation.py
index 0b1a07a4..8a51a9cb 100644
--- a/tests/entity_mapping/test_hierarchical_evaluation.py
+++ b/tests/entity_mapping/test_hierarchical_evaluation.py
@@ -3,9 +3,14 @@
import pandas as pd
import pytest
-from presidio_evaluator.entity_mapping import CanonicalMapper, MappedResults
+from presidio_evaluator.entity_mapping import (
+ CanonicalMapper,
+ EntityHierarchy,
+ MappedResults,
+)
from presidio_evaluator.evaluation import EvaluationResult
from presidio_evaluator.evaluation.span_evaluator import SpanEvaluator
+from presidio_evaluator.evaluation.token_evaluator import TokenEvaluator
# ---------------------------------------------------------------------------
# Helpers
@@ -33,6 +38,25 @@ def _make_results(annotations: list[str], predictions: list[str]) -> MappedResul
return mapper.get_mapped_results_dataframe()
+def _make_single_sentence_results(
+ annotations: list[str],
+ predictions: list[str],
+) -> MappedResults:
+ """Build MappedResults where all tokens belong to one sentence."""
+ df = pd.DataFrame(
+ {
+ "sentence_id": [0] * len(annotations),
+ "token": [f"tok{i}" for i in range(len(annotations))],
+ "annotation": annotations,
+ "prediction": predictions,
+ "start_indices": [i * 5 for i in range(len(annotations))],
+ }
+ )
+ mapper = CanonicalMapper()
+ mapper.analyze(df)
+ return mapper.get_mapped_results_dataframe()
+
+
# ---------------------------------------------------------------------------
# Return type
# ---------------------------------------------------------------------------
@@ -147,14 +171,89 @@ def test_mismatch_penalised(self):
class TestGranularityBonus:
- def test_specific_prediction_scores_tp_at_all_levels(self):
- """Model predicting NAME (depth-3) on NAME annotation gets TP at all levels."""
- results = _make_results(["NAME", "O"], ["NAME", "O"])
- scores = _evaluator.calculate_hierarchical_scores(results)
+ @pytest.mark.parametrize(
+ ("annotation", "prediction"),
+ [
+ ("PERSON", "NAME"),
+ ("DATE_TIME", "DATE"),
+ ],
+ )
+ @pytest.mark.parametrize("evaluator_type", [SpanEvaluator, TokenEvaluator])
+ def test_specific_prediction_scores_tp_at_all_levels(
+ self,
+ annotation,
+ prediction,
+ evaluator_type,
+ ):
+ """A descendant prediction satisfies a less-specific annotation."""
+ results = _make_results([annotation, "O"], [prediction, "O"])
+ scores = evaluator_type(skip_words=[]).calculate_hierarchical_scores(results)
for level in ("binary", "branch", "detailed"):
assert scores[level].pii_f == pytest.approx(1.0, abs=1e-6), (
f"Expected perfect score at {level}"
)
+ assert scores["detailed"].per_type[annotation].true_positives == 1
+ assert scores["detailed"].results[(annotation, annotation)] == 1
+
+ def test_multiple_descendant_spans_combine_for_span_iou(self):
+ """Different descendants can jointly cover one broader annotation."""
+ results = _make_single_sentence_results(
+ ["PERSON", "PERSON"],
+ ["NAME", "TITLE"],
+ )
+ scores = SpanEvaluator(
+ skip_words=[],
+ iou_threshold=1.0,
+ char_based=False,
+ ).calculate_hierarchical_scores(results)
+ person = scores["detailed"].per_type["PERSON"]
+ assert person.true_positives == 1
+ assert person.num_predicted == 1
+ assert person.false_positives == 0
+ assert person.false_negatives == 0
+
+ def test_low_iou_descendants_use_the_projected_type(self):
+ """A failed descendant match is attributed to the mapped scoring label."""
+ results = _make_single_sentence_results(
+ ["PERSON", "PERSON", "PERSON", "PERSON"],
+ ["NAME", "O", "NAME", "O"],
+ )
+ scores = SpanEvaluator(
+ skip_words=[],
+ iou_threshold=1.0,
+ char_based=False,
+ ).calculate_hierarchical_scores(results)
+ detailed = scores["detailed"]
+ # Two NAME spans jointly fail to cover the PERSON annotation: one FN,
+ # and each failed span is its own false positive, both attributed to
+ # the projected PERSON label rather than to NAME.
+ assert detailed.per_type["PERSON"].false_negatives == 1
+ assert detailed.per_type["PERSON"].num_predicted == 2
+ assert detailed.per_type["PERSON"].false_positives == 2
+ assert detailed.results[("O", "PERSON")] == 2
+ assert detailed.results[("O", "NAME")] == 0
+
+ def test_custom_hierarchy_is_used_for_descendant_credit(self):
+ """Callers can score descendant relationships from a custom taxonomy."""
+ hierarchy = EntityHierarchy(
+ hierarchy={"PII": {"CUSTOM_PARENT": {"CUSTOM_CHILD": []}}},
+ canonical_depth=10,
+ )
+ df = pd.DataFrame(
+ {
+ "sentence_id": [0],
+ "token": ["value"],
+ "annotation": ["CUSTOM_PARENT"],
+ "prediction": ["CUSTOM_CHILD"],
+ "start_indices": [0],
+ }
+ )
+ mapper = CanonicalMapper(hierarchy=hierarchy)
+ mapper.analyze(df)
+ scores = SpanEvaluator(skip_words=[]).calculate_hierarchical_scores(
+ mapper.get_mapped_results_dataframe()
+ )
+ assert scores["detailed"].per_type["CUSTOM_PARENT"].true_positives == 1
def test_coarse_prediction_scores_tp_at_binary_branch_but_not_detailed(self):
"""Model predicting PERSON (depth-2) on NAME: TP at binary/branch but not detailed."""
@@ -168,6 +267,18 @@ def test_coarse_prediction_scores_tp_at_binary_branch_but_not_detailed(self):
assert name_detailed is not None
assert name_detailed.recall == pytest.approx(0.0, abs=1e-6)
+ def test_coarse_generic_prediction_is_not_forgiven_for_tokens(self):
+ """Hierarchy scoring must not treat a coarse PII prediction as exact."""
+ results = _make_results(["PERSON"], ["PII"])
+ direct = TokenEvaluator(skip_words=[]).calculate_score_on_df(results.detailed)
+ assert direct.per_type["PERSON"].true_positives == 1
+
+ scores = TokenEvaluator(skip_words=[]).calculate_hierarchical_scores(results)
+ person = scores["detailed"].per_type["PERSON"]
+ assert person.true_positives == 0
+ assert person.false_negatives == 1
+ assert scores["detailed"].per_type["PII"].false_positives == 1
+
def test_calculate_score_on_df_matches_branch_level(self):
"""calculate_score_on_df(results.branch) == scores['branch']."""
results = _make_results(
@@ -213,8 +324,8 @@ def test_scenario1_binary_is_perfect(self):
assert pii_m.recall == pytest.approx(1.0, abs=1e-6)
assert pii_m.precision == pytest.approx(1.0, abs=1e-6)
- def test_scenario1_branch_pii_annotation_misses_person_predictions(self):
- """S1 branch: PII annotation (depth-1, stays PII) vs PERSON predictions → FN/FP."""
+ def test_scenario1_branch_credits_descendant_predictions(self):
+ """S1 branch: specific predictions satisfy the PII annotation."""
results = _make_results(
["PII", "PII", "PII"],
["PERSON", "NAME", "FIRST_NAME"],
@@ -222,17 +333,14 @@ def test_scenario1_branch_pii_annotation_misses_person_predictions(self):
scores = _evaluator.calculate_hierarchical_scores(results)
pii_m = scores["branch"].per_type.get("PII")
assert pii_m is not None
- assert pii_m.recall == pytest.approx(0.0, abs=1e-6)
- # All depth-2/3/4 predictions collapse to PERSON at branch
- person_m = scores["branch"].per_type.get("PERSON")
- assert person_m is not None
- assert person_m.false_positives > 0
+ assert pii_m.recall == pytest.approx(1.0, abs=1e-6)
+ assert pii_m.precision == pytest.approx(1.0, abs=1e-6)
- def test_scenario1_detailed_pii_annotation_misses_granular_predictions(self):
- """S1 detailed: PII annotation stays PII; predictions stay at their canonical depth.
+ def test_scenario1_detailed_credits_descendant_predictions(self):
+ """S1 detailed: more-specific predictions satisfy the PII annotation.
Note: FIRST_NAME resolves to NAME at the default canonical_depth=3, so only
- PERSON and NAME appear as distinct prediction labels in per_type.
+ PERSON and NAME appear as distinct prediction labels before scoring.
"""
results = _make_results(
["PII", "PII", "PII"],
@@ -241,28 +349,26 @@ def test_scenario1_detailed_pii_annotation_misses_granular_predictions(self):
scores = _evaluator.calculate_hierarchical_scores(results)
pii_m = scores["detailed"].per_type.get("PII")
assert pii_m is not None
- assert pii_m.recall == pytest.approx(0.0, abs=1e-6)
- # PERSON and NAME appear as FPs (FIRST_NAME collapses to NAME at canonical_depth=3)
- assert scores["detailed"].per_type.get("PERSON") is not None
- assert scores["detailed"].per_type.get("NAME") is not None
+ assert pii_m.recall == pytest.approx(1.0, abs=1e-6)
+ assert pii_m.precision == pytest.approx(1.0, abs=1e-6)
# ------------------------------------------------------------------
- # Scenario 2: dataset=depths 2, 3, 4; model=depth-1 (PII)
+ # Scenario 2: dataset=depth-3; model=depth-1 (PII)
# ------------------------------------------------------------------
def test_scenario2_binary_is_perfect(self):
"""S2 binary: granular annotations collapse to PII, matching PII predictions."""
results = _make_results(
- ["PERSON", "NAME", "FIRST_NAME"],
+ ["NAME", "NAME", "NAME"],
["PII", "PII", "PII"],
)
scores = _evaluator.calculate_hierarchical_scores(results)
assert scores["binary"].pii_recall == pytest.approx(1.0, abs=1e-6)
def test_scenario2_branch_person_annotations_miss_pii_predictions(self):
- """S2 branch: PERSON/NAME/FIRST_NAME all become PERSON; PII prediction stays PII → mismatch."""
+ """S2 branch: NAME becomes PERSON; PII prediction remains too coarse."""
results = _make_results(
- ["PERSON", "NAME", "FIRST_NAME"],
+ ["NAME", "NAME", "NAME"],
["PII", "PII", "PII"],
)
scores = _evaluator.calculate_hierarchical_scores(results)
@@ -273,23 +379,16 @@ def test_scenario2_branch_person_annotations_miss_pii_predictions(self):
assert pii_m is not None
assert pii_m.false_positives > 0
- def test_scenario2_detailed_all_annotation_types_miss(self):
- """S2 detailed: each granular annotation type gets recall=0 vs the coarse PII prediction.
-
- Note: FIRST_NAME resolves to NAME at canonical_depth=3, so per_type only contains
- PERSON and NAME (with num_annotated=2 for NAME, covering both NAME and FIRST_NAME tokens).
- """
+ def test_scenario2_detailed_annotation_type_misses(self):
+ """S2 detailed: NAME gets recall=0 against the coarse PII prediction."""
results = _make_results(
- ["PERSON", "NAME", "FIRST_NAME"],
+ ["NAME", "NAME", "NAME"],
["PII", "PII", "PII"],
)
scores = _evaluator.calculate_hierarchical_scores(results)
- for entity in ("PERSON", "NAME"):
- m = scores["detailed"].per_type.get(entity)
- assert m is not None, f"Expected {entity} in detailed per_type"
- assert m.recall == pytest.approx(0.0, abs=1e-6), (
- f"Expected recall=0 for {entity} at detailed level"
- )
+ name = scores["detailed"].per_type.get("NAME")
+ assert name is not None
+ assert name.recall == pytest.approx(0.0, abs=1e-6)
# ------------------------------------------------------------------
# Scenario 3: dataset=2×depth-2 + 2×depth-3; model=depth-2 only
@@ -397,18 +496,15 @@ def test_scenario5_branch_is_tp_because_first_name_collapses_to_person(self):
assert person_m.recall == pytest.approx(1.0, abs=1e-6)
assert person_m.precision == pytest.approx(1.0, abs=1e-6)
- def test_scenario5_detailed_misses_because_labels_differ(self):
+ def test_scenario5_detailed_credits_more_specific_prediction(self):
"""S5 detailed: PERSON annotation vs FIRST_NAME prediction (→NAME at canonical_depth=3).
FIRST_NAME resolves to NAME, so the prediction entity is NAME at detailed level.
- NAME prediction does not match PERSON annotation → FN for PERSON, FP for NAME.
+ NAME is a descendant of PERSON, so the prediction receives full credit.
"""
results = _make_results(["PERSON"], ["FIRST_NAME"])
scores = _evaluator.calculate_hierarchical_scores(results)
person_m = scores["detailed"].per_type.get("PERSON")
assert person_m is not None
- assert person_m.recall == pytest.approx(0.0, abs=1e-6)
- # FIRST_NAME resolves to NAME at canonical_depth=3 → NAME is the FP
- name_m = scores["detailed"].per_type.get("NAME")
- assert name_m is not None
- assert name_m.false_positives > 0
+ assert person_m.recall == pytest.approx(1.0, abs=1e-6)
+ assert person_m.precision == pytest.approx(1.0, abs=1e-6)
diff --git a/tests/integration/test_notebook.py b/tests/integration/test_notebook.py
index 0966a34d..72b13b4f 100644
--- a/tests/integration/test_notebook.py
+++ b/tests/integration/test_notebook.py
@@ -7,7 +7,10 @@
from presidio_analyzer import AnalyzerEngine
from presidio_evaluator import InputSample
-from presidio_evaluator.entity_mapping import CanonicalMapper, IssueType
+from presidio_evaluator.entity_mapping import (
+ CanonicalMapper,
+ IssueType,
+)
from presidio_evaluator.evaluation import ModelError, Plotter, SpanEvaluator
from presidio_evaluator.evaluation.token_evaluator import TokenEvaluator
from presidio_evaluator.experiment_tracking import get_experiment_tracker
diff --git a/uv.lock b/uv.lock
index dea969a4..43b32673 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1,12 +1,15 @@
version = 1
revision = 3
-requires-python = ">=3.11, <3.14"
+requires-python = ">=3.11, <3.15"
resolution-markers = [
- "python_full_version >= '3.12' and sys_platform == 'win32'",
+ "python_full_version >= '3.14' and sys_platform == 'win32'",
+ "python_full_version >= '3.14' and sys_platform == 'emscripten'",
+ "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'",
"python_full_version < '3.12' and sys_platform == 'win32'",
- "python_full_version >= '3.12' and sys_platform == 'emscripten'",
+ "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'",
"python_full_version < '3.12' and sys_platform == 'emscripten'",
- "python_full_version >= '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
"python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'",
]
@@ -71,6 +74,16 @@ dependencies = [
]
sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/db8af0df73c1cf454f71b2bbe5e356b8c1f8041c979f505b3d3186e520a9/argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d", size = 1783441, upload-time = "2025-07-30T10:02:05.147Z" }
wheels = [
+ { url = "https://files.pythonhosted.org/packages/60/97/3c0a35f46e52108d4707c44b95cfe2afcafc50800b5450c197454569b776/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:3d3f05610594151994ca9ccb3c771115bdb4daef161976a266f0dd8aa9996b8f", size = 54393, upload-time = "2025-07-30T10:01:40.97Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/f4/98bbd6ee89febd4f212696f13c03ca302b8552e7dbf9c8efa11ea4a388c3/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8b8efee945193e667a396cbc7b4fb7d357297d6234d30a489905d96caabde56b", size = 29328, upload-time = "2025-07-30T10:01:41.916Z" },
+ { url = "https://files.pythonhosted.org/packages/43/24/90a01c0ef12ac91a6be05969f29944643bc1e5e461155ae6559befa8f00b/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3c6702abc36bf3ccba3f802b799505def420a1b7039862014a65db3205967f5a", size = 31269, upload-time = "2025-07-30T10:01:42.716Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/d3/942aa10782b2697eee7af5e12eeff5ebb325ccfb86dd8abda54174e377e4/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1c70058c6ab1e352304ac7e3b52554daadacd8d453c1752e547c76e9c99ac44", size = 86558, upload-time = "2025-07-30T10:01:43.943Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/82/b484f702fec5536e71836fc2dbc8c5267b3f6e78d2d539b4eaa6f0db8bf8/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2fd3bfbff3c5d74fef31a722f729bf93500910db650c925c2d6ef879a7e51cb", size = 92364, upload-time = "2025-07-30T10:01:44.887Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/c1/a606ff83b3f1735f3759ad0f2cd9e038a0ad11a3de3b6c673aa41c24bb7b/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4f9665de60b1b0e99bcd6be4f17d90339698ce954cfd8d9cf4f91c995165a92", size = 85637, upload-time = "2025-07-30T10:01:46.225Z" },
+ { url = "https://files.pythonhosted.org/packages/44/b4/678503f12aceb0262f84fa201f6027ed77d71c5019ae03b399b97caa2f19/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ba92837e4a9aa6a508c8d2d7883ed5a8f6c308c89a4790e1e447a220deb79a85", size = 91934, upload-time = "2025-07-30T10:01:47.203Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/c7/f36bd08ef9bd9f0a9cff9428406651f5937ce27b6c5b07b92d41f91ae541/argon2_cffi_bindings-25.1.0-cp314-cp314t-win32.whl", hash = "sha256:84a461d4d84ae1295871329b346a97f68eade8c53b6ed9a7ca2d7467f3c8ff6f", size = 28158, upload-time = "2025-07-30T10:01:48.341Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/80/0106a7448abb24a2c467bf7d527fe5413b7fdfa4ad6d6a96a43a62ef3988/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b55aec3565b65f56455eebc9b9f34130440404f27fe21c3b375bf1ea4d8fbae6", size = 32597, upload-time = "2025-07-30T10:01:49.112Z" },
+ { url = "https://files.pythonhosted.org/packages/05/b8/d663c9caea07e9180b2cb662772865230715cbd573ba3b5e81793d580316/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:87c33a52407e4c41f3b70a9c2d3f6056d88b10dad7695be708c5021673f55623", size = 28231, upload-time = "2025-07-30T10:01:49.92Z" },
{ url = "https://files.pythonhosted.org/packages/1d/57/96b8b9f93166147826da5f90376e784a10582dd39a393c99bb62cfcf52f0/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500", size = 54121, upload-time = "2025-07-30T10:01:50.815Z" },
{ url = "https://files.pythonhosted.org/packages/0a/08/a9bebdb2e0e602dde230bdde8021b29f71f7841bd54801bcfd514acb5dcf/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44", size = 29177, upload-time = "2025-07-30T10:01:51.681Z" },
{ url = "https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0", size = 31090, upload-time = "2025-07-30T10:01:53.184Z" },
@@ -192,6 +205,13 @@ wheels = [
{ 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]]
@@ -258,6 +278,28 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
{ url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
{ url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
+ { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
+ { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
+ { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
+ { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
+ { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
]
[[package]]
@@ -323,6 +365,38 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f1/11/897052ea6af56df3eef3ca94edafee410ca699ca0c7b87960ad19932c55e/charset_normalizer-3.4.6-cp313-cp313-win32.whl", hash = "sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d", size = 143940, upload-time = "2026-03-15T18:51:36.15Z" },
{ url = "https://files.pythonhosted.org/packages/a1/5c/724b6b363603e419829f561c854b87ed7c7e31231a7908708ac086cdf3e2/charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389", size = 154101, upload-time = "2026-03-15T18:51:37.876Z" },
{ url = "https://files.pythonhosted.org/packages/01/a5/7abf15b4c0968e47020f9ca0935fb3274deb87cb288cd187cad92e8cdffd/charset_normalizer-3.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f", size = 143109, upload-time = "2026-03-15T18:51:39.565Z" },
+ { url = "https://files.pythonhosted.org/packages/25/6f/ffe1e1259f384594063ea1869bfb6be5cdb8bc81020fc36c3636bc8302a1/charset_normalizer-3.4.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8", size = 294458, upload-time = "2026-03-15T18:51:41.134Z" },
+ { url = "https://files.pythonhosted.org/packages/56/60/09bb6c13a8c1016c2ed5c6a6488e4ffef506461aa5161662bd7636936fb1/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421", size = 199277, upload-time = "2026-03-15T18:51:42.953Z" },
+ { url = "https://files.pythonhosted.org/packages/00/50/dcfbb72a5138bbefdc3332e8d81a23494bf67998b4b100703fd15fa52d81/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2", size = 218758, upload-time = "2026-03-15T18:51:44.339Z" },
+ { url = "https://files.pythonhosted.org/packages/03/b3/d79a9a191bb75f5aa81f3aaaa387ef29ce7cb7a9e5074ba8ea095cc073c2/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30", size = 215299, upload-time = "2026-03-15T18:51:45.871Z" },
+ { url = "https://files.pythonhosted.org/packages/76/7e/bc8911719f7084f72fd545f647601ea3532363927f807d296a8c88a62c0d/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db", size = 206811, upload-time = "2026-03-15T18:51:47.308Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/40/c430b969d41dda0c465aa36cc7c2c068afb67177bef50905ac371b28ccc7/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8", size = 193706, upload-time = "2026-03-15T18:51:48.849Z" },
+ { url = "https://files.pythonhosted.org/packages/48/15/e35e0590af254f7df984de1323640ef375df5761f615b6225ba8deb9799a/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815", size = 202706, upload-time = "2026-03-15T18:51:50.257Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/bd/f736f7b9cc5e93a18b794a50346bb16fbfd6b37f99e8f306f7951d27c17c/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a", size = 202497, upload-time = "2026-03-15T18:51:52.012Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/ba/2cc9e3e7dfdf7760a6ed8da7446d22536f3d0ce114ac63dee2a5a3599e62/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43", size = 193511, upload-time = "2026-03-15T18:51:53.723Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/cb/5be49b5f776e5613be07298c80e1b02a2d900f7a7de807230595c85a8b2e/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0", size = 220133, upload-time = "2026-03-15T18:51:55.333Z" },
+ { url = "https://files.pythonhosted.org/packages/83/43/99f1b5dad345accb322c80c7821071554f791a95ee50c1c90041c157ae99/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1", size = 203035, upload-time = "2026-03-15T18:51:56.736Z" },
+ { url = "https://files.pythonhosted.org/packages/87/9a/62c2cb6a531483b55dddff1a68b3d891a8b498f3ca555fbcf2978e804d9d/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f", size = 216321, upload-time = "2026-03-15T18:51:58.17Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/79/94a010ff81e3aec7c293eb82c28f930918e517bc144c9906a060844462eb/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815", size = 208973, upload-time = "2026-03-15T18:51:59.998Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/57/4ecff6d4ec8585342f0c71bc03efaa99cb7468f7c91a57b105bcd561cea8/charset_normalizer-3.4.6-cp314-cp314-win32.whl", hash = "sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d", size = 144610, upload-time = "2026-03-15T18:52:02.213Z" },
+ { url = "https://files.pythonhosted.org/packages/80/94/8434a02d9d7f168c25767c64671fead8d599744a05d6a6c877144c754246/charset_normalizer-3.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f", size = 154962, upload-time = "2026-03-15T18:52:03.658Z" },
+ { url = "https://files.pythonhosted.org/packages/46/4c/48f2cdbfd923026503dfd67ccea45c94fd8fe988d9056b468579c66ed62b/charset_normalizer-3.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e", size = 143595, upload-time = "2026-03-15T18:52:05.123Z" },
+ { url = "https://files.pythonhosted.org/packages/31/93/8878be7569f87b14f1d52032946131bcb6ebbd8af3e20446bc04053dc3f1/charset_normalizer-3.4.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866", size = 314828, upload-time = "2026-03-15T18:52:06.831Z" },
+ { url = "https://files.pythonhosted.org/packages/06/b6/fae511ca98aac69ecc35cde828b0a3d146325dd03d99655ad38fc2cc3293/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc", size = 208138, upload-time = "2026-03-15T18:52:08.239Z" },
+ { url = "https://files.pythonhosted.org/packages/54/57/64caf6e1bf07274a1e0b7c160a55ee9e8c9ec32c46846ce59b9c333f7008/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e", size = 224679, upload-time = "2026-03-15T18:52:10.043Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/cb/9ff5a25b9273ef160861b41f6937f86fae18b0792fe0a8e75e06acb08f1d/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077", size = 223475, upload-time = "2026-03-15T18:52:11.854Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/97/440635fc093b8d7347502a377031f9605a1039c958f3cd18dcacffb37743/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f", size = 215230, upload-time = "2026-03-15T18:52:13.325Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/24/afff630feb571a13f07c8539fbb502d2ab494019492aaffc78ef41f1d1d0/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e", size = 199045, upload-time = "2026-03-15T18:52:14.752Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/17/d1399ecdaf7e0498c327433e7eefdd862b41236a7e484355b8e0e5ebd64b/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484", size = 211658, upload-time = "2026-03-15T18:52:16.278Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/38/16baa0affb957b3d880e5ac2144caf3f9d7de7bc4a91842e447fbb5e8b67/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7", size = 210769, upload-time = "2026-03-15T18:52:17.782Z" },
+ { url = "https://files.pythonhosted.org/packages/05/34/c531bc6ac4c21da9ddfddb3107be2287188b3ea4b53b70fc58f2a77ac8d8/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff", size = 201328, upload-time = "2026-03-15T18:52:19.553Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/73/a5a1e9ca5f234519c1953608a03fe109c306b97fdfb25f09182babad51a7/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e", size = 225302, upload-time = "2026-03-15T18:52:21.043Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/f6/cd782923d112d296294dea4bcc7af5a7ae0f86ab79f8fefbda5526b6cfc0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659", size = 211127, upload-time = "2026-03-15T18:52:22.491Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/c5/0b6898950627af7d6103a449b22320372c24c6feda91aa24e201a478d161/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602", size = 222840, upload-time = "2026-03-15T18:52:24.113Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/25/c4bba773bef442cbdc06111d40daa3de5050a676fa26e85090fc54dd12f0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407", size = 216890, upload-time = "2026-03-15T18:52:25.541Z" },
+ { url = "https://files.pythonhosted.org/packages/35/1a/05dacadb0978da72ee287b0143097db12f2e7e8d3ffc4647da07a383b0b7/charset_normalizer-3.4.6-cp314-cp314t-win32.whl", hash = "sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579", size = 155379, upload-time = "2026-03-15T18:52:27.05Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/7a/d269d834cb3a76291651256f3b9a5945e81d0a49ab9f4a498964e83c0416/charset_normalizer-3.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4", size = 169043, upload-time = "2026-03-15T18:52:28.502Z" },
+ { url = "https://files.pythonhosted.org/packages/23/06/28b29fba521a37a8932c6a84192175c34d49f84a6d4773fa63d05f9aff22/charset_normalizer-3.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c", size = 148523, upload-time = "2026-03-15T18:52:29.956Z" },
{ url = "https://files.pythonhosted.org/packages/2a/68/687187c7e26cb24ccbd88e5069f5ef00eba804d36dde11d99aad0838ab45/charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69", size = 61455, upload-time = "2026-03-15T18:53:23.833Z" },
]
@@ -427,6 +501,28 @@ wheels = [
{ 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" },
@@ -457,6 +553,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/47/a2/e5079a032fb85cf6005046ca92bbd78b0c82dad2b5751ab8c311659da06f/cryptography-48.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9bd3f92d76217892b15df84ca256c2c113d386fdda7a7d8691aeeced976507c6", size = 4979117, upload-time = "2026-06-09T22:31:05.845Z" },
{ url = "https://files.pythonhosted.org/packages/b7/a0/8f50cae9c74e718ed769d63ed5c74bd0ea830c9550a74629cebd1b9c7bc7/cryptography-48.0.1-cp311-abi3-win32.whl", hash = "sha256:b9a32b876490d66c8bcc9963ef220199569748434ab01a9d6aaeabf88e7f5158", size = 3304154, upload-time = "2026-06-09T22:32:16.845Z" },
{ url = "https://files.pythonhosted.org/packages/c5/69/0572c77dbace6fef72f33755bd52ea399c71367250d366237f8691826b9e/cryptography-48.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:39489bfca54c7a1f6b297efcd8bc608ab92d16c4ca631b0cad4da46724588b24", size = 3817138, upload-time = "2026-06-09T22:32:00.388Z" },
+ { url = "https://files.pythonhosted.org/packages/42/06/3e768b4c3bc78201583fa35a0e18f640dd782ff41afba88f8545481a8874/cryptography-48.0.1-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:f817adc181390bd54f2f700107a7419040fb7c1bdf2fc26f36551a06a68c3345", size = 7989830, upload-time = "2026-06-09T22:31:07.8Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/13/6476736484b94041110c8340a3eb63962fea4975baea8cb4a512adb44d4d/cryptography-48.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d5d30989c6917b478b5817902e85fddaea2261efa8648383d965381ccb9e1ac4", size = 4689201, upload-time = "2026-06-09T22:31:09.745Z" },
+ { url = "https://files.pythonhosted.org/packages/79/62/65a87f34d2a431546e2509b85d55e8c90df86d668f6731da64d538512ac2/cryptography-48.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:df637c05205ea7c1d7fbcbe54bbfea648a52951155f997af13d895d0ecc96991", size = 4702822, upload-time = "2026-06-09T22:32:24.409Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/59/810b5204b0a9b10f4b6bc06bd551a8b609803cd931806bc3b71884b225e5/cryptography-48.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:869c3b8a53bfe27147832df48b32adadf558249d50e76cb3769d40e986b13265", size = 4694875, upload-time = "2026-06-09T22:32:08.737Z" },
+ { url = "https://files.pythonhosted.org/packages/24/dc/d8ca05ffea724eec6d232ea6f18e74c269eb6bdfdcc9bfba689790d1325f/cryptography-48.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:e361afba8918070d376df76f408a4f67fec0ee9cff81a99e48fe9a233ef59e17", size = 5290385, upload-time = "2026-06-09T22:31:15.212Z" },
+ { url = "https://files.pythonhosted.org/packages/03/8c/3be6cb4da181f5bb6c19cf560c2359d60644a6b5fc5b57854e528f47b296/cryptography-48.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:d069066deead00ac7f090be101be875a06855908f7ec004c27b8fefb4acfb411", size = 4737082, upload-time = "2026-06-09T22:32:22.66Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/f6/d5f60a5a1434dbfd949e227fd0065d194c7e6b6ac526b17f5c06152b8231/cryptography-48.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:09f73a725d582cef64b91281a322cd798d14a33b2b6f2b7ad9531dc336d84c02", size = 4325328, upload-time = "2026-06-09T22:32:10.777Z" },
+ { url = "https://files.pythonhosted.org/packages/17/b7/ba75dd947a14b6ad907b01ae8f6b5b348cdd1b48142f0063dee9e20c1d9d/cryptography-48.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:15254441469dd6bf027039453288e2072124f8b6603563f5d759e1c9b69273fa", size = 4694530, upload-time = "2026-06-09T22:31:53.105Z" },
+ { url = "https://files.pythonhosted.org/packages/62/29/50d6b9e8aff12d8b67afaeb3569335e32dc83a5723e3bbded24fdac9f809/cryptography-48.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:8ace4507d1e6533c125f4fac754f8bb8b6a74c08e92179dabd7e16571a3efbf3", size = 5245046, upload-time = "2026-06-09T22:31:25.774Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/04/618f4115cfc0add0838c82507aa18a346089428da8653ad38b3ff36f5cb3/cryptography-48.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:b4e391975f038e66432328639620a4aff2d307513b004f1ca06d6225bced815c", size = 4736660, upload-time = "2026-06-09T22:32:12.676Z" },
+ { url = "https://files.pythonhosted.org/packages/24/9c/06e062462a0de28a3b3911322eded4c16deb9f441b1b7575d3dc59488ab5/cryptography-48.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42fcd8e26fe555d9b3577a135f5091fefa0aa4e99129c23fb56787a1bd4ada72", size = 4822229, upload-time = "2026-06-09T22:31:17.062Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/be/0561971eaaee4b8a0e7d5113c536921063ab91aaf23278ac374eaf881e11/cryptography-48.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c1400da5e32a43253392277eac7490a60e497d810a63dd5608d71bbd7af507c9", size = 4966364, upload-time = "2026-06-09T22:31:32.842Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/27/728c77876f12b000820b69ae490f3c4083775e79e07827e9e60be07ad209/cryptography-48.0.1-cp314-cp314t-win32.whl", hash = "sha256:0df56b056bc17c1b7d6821dfa65216e62bd232d8ab05eb3db44e71d235651471", size = 3278498, upload-time = "2026-06-09T22:31:29.154Z" },
+ { url = "https://files.pythonhosted.org/packages/06/e3/79a612c6d7b1e6ee0edd43633d53035bec2cfb78c82b76f7864f39e36f34/cryptography-48.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:9de21387aa95e2a895823d0745b430bed4f33503ba9ab5e0b5311f33e37d66d2", size = 3798790, upload-time = "2026-06-09T22:31:56.697Z" },
{ url = "https://files.pythonhosted.org/packages/ca/6c/00fa2a95997164c8b2072ce327c23d4ab20809ccc323ea5fab91e53a4bba/cryptography-48.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:4fdc69f8e4316bcf0c8c8ec1f26f285d12e8142d88d96c876a59a03be3f6ae67", size = 7987408, upload-time = "2026-06-09T22:32:20.777Z" },
{ url = "https://files.pythonhosted.org/packages/b0/d9/45f309a7e4e5f3f8f121d6d3be9e94024a7726ec598d6e08ae04edb2f04d/cryptography-48.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48fe40804d4caa2288f24e70ca8c64c42dd826da0ad7e4f1b41b2128d679e6c8", size = 4690196, upload-time = "2026-06-09T22:31:54.74Z" },
{ url = "https://files.pythonhosted.org/packages/5f/9f/a1bc8bcc798811b8527eb374bbccf30a3f3e806829d967118222bf1125eb/cryptography-48.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:86be3b1b0b6bf09482fb50a979c508d2950ed95f5621ec77f4e385962006b83a", size = 4696782, upload-time = "2026-06-09T22:31:45.615Z" },
@@ -526,6 +636,22 @@ wheels = [
{ 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]]
@@ -546,6 +672,10 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/90/83/fb33dcea789ed6018f8da20c5a9bc9d82adc65c0c990faed43f7c955da46/debugpy-1.8.20-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:84562982dd7cf5ebebfdea667ca20a064e096099997b175fe204e86817f64eaf", size = 4293272, upload-time = "2026-01-29T23:03:50.169Z" },
{ url = "https://files.pythonhosted.org/packages/a6/25/b1e4a01bfb824d79a6af24b99ef291e24189080c93576dfd9b1a2815cd0f/debugpy-1.8.20-cp313-cp313-win32.whl", hash = "sha256:da11dea6447b2cadbf8ce2bec59ecea87cc18d2c574980f643f2d2dfe4862393", size = 5331208, upload-time = "2026-01-29T23:03:51.547Z" },
{ url = "https://files.pythonhosted.org/packages/13/f7/a0b368ce54ffff9e9028c098bd2d28cfc5b54f9f6c186929083d4c60ba58/debugpy-1.8.20-cp313-cp313-win_amd64.whl", hash = "sha256:eb506e45943cab2efb7c6eafdd65b842f3ae779f020c82221f55aca9de135ed7", size = 5372930, upload-time = "2026-01-29T23:03:53.585Z" },
+ { url = "https://files.pythonhosted.org/packages/33/2e/f6cb9a8a13f5058f0a20fe09711a7b726232cd5a78c6a7c05b2ec726cff9/debugpy-1.8.20-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:9c74df62fc064cd5e5eaca1353a3ef5a5d50da5eb8058fcef63106f7bebe6173", size = 2538066, upload-time = "2026-01-29T23:03:54.999Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/56/6ddca50b53624e1ca3ce1d1e49ff22db46c47ea5fb4c0cc5c9b90a616364/debugpy-1.8.20-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:077a7447589ee9bc1ff0cdf443566d0ecf540ac8aa7333b775ebcb8ce9f4ecad", size = 4269425, upload-time = "2026-01-29T23:03:56.518Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/d9/d64199c14a0d4c476df46c82470a3ce45c8d183a6796cfb5e66533b3663c/debugpy-1.8.20-cp314-cp314-win32.whl", hash = "sha256:352036a99dd35053b37b7803f748efc456076f929c6a895556932eaf2d23b07f", size = 5331407, upload-time = "2026-01-29T23:03:58.481Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/d9/1f07395b54413432624d61524dfd98c1a7c7827d2abfdb8829ac92638205/debugpy-1.8.20-cp314-cp314-win_amd64.whl", hash = "sha256:a98eec61135465b062846112e5ecf2eebb855305acc1dfbae43b72903b8ab5be", size = 5372521, upload-time = "2026-01-29T23:03:59.864Z" },
{ url = "https://files.pythonhosted.org/packages/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl", hash = "sha256:5be9bed9ae3be00665a06acaa48f8329d2b9632f15fd09f6a9a8c8d9907e54d7", size = 5337658, upload-time = "2026-01-29T23:04:17.404Z" },
]
@@ -661,6 +791,22 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1f/ae/b41f8628ec0be3c1b934fc12b84f4576a5c646119db4d3bdd76a217c90b5/fonttools-4.62.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:403d28ce06ebfc547fbcb0cb8b7f7cc2f7a2d3e1a67ba9a34b14632df9e080f9", size = 5094920, upload-time = "2026-03-13T13:53:29.329Z" },
{ url = "https://files.pythonhosted.org/packages/f2/f6/53a1e9469331a23dcc400970a27a4caa3d9f6edbf5baab0260285238b884/fonttools-4.62.1-cp313-cp313-win32.whl", hash = "sha256:93c316e0f5301b2adbe6a5f658634307c096fd5aae60a5b3412e4f3e1728ab24", size = 2279928, upload-time = "2026-03-13T13:53:32.352Z" },
{ url = "https://files.pythonhosted.org/packages/38/60/35186529de1db3c01f5ad625bde07c1f576305eab6d86bbda4c58445f721/fonttools-4.62.1-cp313-cp313-win_amd64.whl", hash = "sha256:7aa21ff53e28a9c2157acbc44e5b401149d3c9178107130e82d74ceb500e5056", size = 2330514, upload-time = "2026-03-13T13:53:34.991Z" },
+ { url = "https://files.pythonhosted.org/packages/36/f0/2888cdac391807d68d90dcb16ef858ddc1b5309bfc6966195a459dd326e2/fonttools-4.62.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fa1d16210b6b10a826d71bed68dd9ec24a9e218d5a5e2797f37c573e7ec215ca", size = 2864442, upload-time = "2026-03-13T13:53:37.509Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/b2/e521803081f8dc35990816b82da6360fa668a21b44da4b53fc9e77efcd62/fonttools-4.62.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:aa69d10ed420d8121118e628ad47d86e4caa79ba37f968597b958f6cceab7eca", size = 2410901, upload-time = "2026-03-13T13:53:40.55Z" },
+ { url = "https://files.pythonhosted.org/packages/00/a4/8c3511ff06e53110039358dbbdc1a65d72157a054638387aa2ada300a8b8/fonttools-4.62.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd13b7999d59c5eb1c2b442eb2d0c427cb517a0b7a1f5798fc5c9e003f5ff782", size = 4999608, upload-time = "2026-03-13T13:53:42.798Z" },
+ { url = "https://files.pythonhosted.org/packages/28/63/cd0c3b26afe60995a5295f37c246a93d454023726c3261cfbb3559969bb9/fonttools-4.62.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8d337fdd49a79b0d51c4da87bc38169d21c3abbf0c1aa9367eff5c6656fb6dae", size = 4912726, upload-time = "2026-03-13T13:53:45.405Z" },
+ { url = "https://files.pythonhosted.org/packages/70/b9/ac677cb07c24c685cf34f64e140617d58789d67a3dd524164b63648c6114/fonttools-4.62.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d241cdc4a67b5431c6d7f115fdf63335222414995e3a1df1a41e1182acd4bcc7", size = 4951422, upload-time = "2026-03-13T13:53:48.326Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/10/11c08419a14b85b7ca9a9faca321accccc8842dd9e0b1c8a72908de05945/fonttools-4.62.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c05557a78f8fa514da0f869556eeda40887a8abc77c76ee3f74cf241778afd5a", size = 5060979, upload-time = "2026-03-13T13:53:51.366Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/3c/12eea4a4cf054e7ab058ed5ceada43b46809fce2bf319017c4d63ae55bb4/fonttools-4.62.1-cp314-cp314-win32.whl", hash = "sha256:49a445d2f544ce4a69338694cad575ba97b9a75fff02720da0882d1a73f12800", size = 2283733, upload-time = "2026-03-13T13:53:53.606Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/67/74b070029043186b5dd13462c958cb7c7f811be0d2e634309d9a1ffb1505/fonttools-4.62.1-cp314-cp314-win_amd64.whl", hash = "sha256:1eecc128c86c552fb963fe846ca4e011b1be053728f798185a1687502f6d398e", size = 2335663, upload-time = "2026-03-13T13:53:56.23Z" },
+ { url = "https://files.pythonhosted.org/packages/42/c5/4d2ed3ca6e33617fc5624467da353337f06e7f637707478903c785bd8e20/fonttools-4.62.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1596aeaddf7f78e21e68293c011316a25267b3effdaccaf4d59bc9159d681b82", size = 2947288, upload-time = "2026-03-13T13:53:59.397Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/e9/7ab11ddfda48ed0f89b13380e5595ba572619c27077be0b2c447a63ff351/fonttools-4.62.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8f8fca95d3bb3208f59626a4b0ea6e526ee51f5a8ad5d91821c165903e8d9260", size = 2449023, upload-time = "2026-03-13T13:54:01.642Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/10/a800fa090b5e8819942e54e19b55fc7c21fe14a08757c3aa3ca8db358939/fonttools-4.62.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee91628c08e76f77b533d65feb3fbe6d9dad699f95be51cf0d022db94089cdc4", size = 5137599, upload-time = "2026-03-13T13:54:04.495Z" },
+ { url = "https://files.pythonhosted.org/packages/37/dc/8ccd45033fffd74deb6912fa1ca524643f584b94c87a16036855b498a1ed/fonttools-4.62.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f37df1cac61d906e7b836abe356bc2f34c99d4477467755c216b72aa3dc748b", size = 4920933, upload-time = "2026-03-13T13:54:07.557Z" },
+ { url = "https://files.pythonhosted.org/packages/99/eb/e618adefb839598d25ac8136cd577925d6c513dc0d931d93b8af956210f0/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:92bb00a947e666169c99b43753c4305fc95a890a60ef3aeb2a6963e07902cc87", size = 5016232, upload-time = "2026-03-13T13:54:10.611Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/5f/9b5c9bfaa8ec82def8d8168c4f13615990d6ce5996fe52bd49bfb5e05134/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bdfe592802ef939a0e33106ea4a318eeb17822c7ee168c290273cbd5fabd746c", size = 5042987, upload-time = "2026-03-13T13:54:13.569Z" },
+ { url = "https://files.pythonhosted.org/packages/90/aa/dfbbe24c6a6afc5c203d90cc0343e24bcbb09e76d67c4d6eef8c2558d7ba/fonttools-4.62.1-cp314-cp314t-win32.whl", hash = "sha256:b820fcb92d4655513d8402d5b219f94481c4443d825b4372c75a2072aa4b357a", size = 2348021, upload-time = "2026-03-13T13:54:16.98Z" },
+ { url = "https://files.pythonhosted.org/packages/13/6f/ae9c4e4dd417948407b680855c2c7790efb52add6009aaecff1e3bc50e8e/fonttools-4.62.1-cp314-cp314t-win_amd64.whl", hash = "sha256:59b372b4f0e113d3746b88985f1c796e7bf830dd54b28374cd85c2b8acd7583e", size = 2414147, upload-time = "2026-03-13T13:54:19.416Z" },
{ url = "https://files.pythonhosted.org/packages/fd/ba/56147c165442cc5ba7e82ecf301c9a68353cede498185869e6e02b4c264f/fonttools-4.62.1-py3-none-any.whl", hash = "sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd", size = 1152647, upload-time = "2026-03-13T13:54:22.735Z" },
]
@@ -705,6 +851,14 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/21/a6/cbd4188b22abd80ebd0edbb2b3e87f2633e958983519980815fb8314eae5/hf_xet-1.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:fca58a2ae4e6f6755cc971ac6fcdf777ea9284d7e540e350bb000813b9a3008d", size = 4428287, upload-time = "2026-03-13T06:58:42.601Z" },
{ url = "https://files.pythonhosted.org/packages/b2/4e/84e45b25e2e3e903ed3db68d7eafa96dae9a1d1f6d0e7fc85120347a852f/hf_xet-1.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:163aab46854ccae0ab6a786f8edecbbfbaa38fcaa0184db6feceebf7000c93c0", size = 3665574, upload-time = "2026-03-13T06:58:53.881Z" },
{ url = "https://files.pythonhosted.org/packages/ee/71/c5ac2b9a7ae39c14e91973035286e73911c31980fe44e7b1d03730c00adc/hf_xet-1.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:09b138422ecbe50fd0c84d4da5ff537d27d487d3607183cd10e3e53f05188e82", size = 3528760, upload-time = "2026-03-13T06:58:52.187Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/0f/fcd2504015eab26358d8f0f232a1aed6b8d363a011adef83fe130bff88f7/hf_xet-1.4.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:949dcf88b484bb9d9276ca83f6599e4aa03d493c08fc168c124ad10b2e6f75d7", size = 3796493, upload-time = "2026-03-13T06:58:39.267Z" },
+ { url = "https://files.pythonhosted.org/packages/82/56/19c25105ff81731ca6d55a188b5de2aa99d7a2644c7aa9de1810d5d3b726/hf_xet-1.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:41659966020d59eb9559c57de2cde8128b706a26a64c60f0531fa2318f409418", size = 3555797, upload-time = "2026-03-13T06:58:37.546Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/e3/8933c073186849b5e06762aa89847991d913d10a95d1603eb7f2c3834086/hf_xet-1.4.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c588e21d80010119458dd5d02a69093f0d115d84e3467efe71ffb2c67c19146", size = 4212127, upload-time = "2026-03-13T06:58:30.539Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/01/f89ebba4e369b4ed699dcb60d3152753870996f41c6d22d3d7cac01310e1/hf_xet-1.4.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a296744d771a8621ad1d50c098d7ab975d599800dae6d48528ba3944e5001ba0", size = 3987788, upload-time = "2026-03-13T06:58:29.139Z" },
+ { url = "https://files.pythonhosted.org/packages/84/4d/8a53e5ffbc2cc33bbf755382ac1552c6d9af13f623ed125fe67cc3e6772f/hf_xet-1.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f563f7efe49588b7d0629d18d36f46d1658fe7e08dce3fa3d6526e1c98315e2d", size = 4188315, upload-time = "2026-03-13T06:58:48.017Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/b8/b7a1c1b5592254bd67050632ebbc1b42cc48588bf4757cb03c2ef87e704a/hf_xet-1.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5b2e0132c56d7ee1bf55bdb638c4b62e7106f6ac74f0b786fed499d5548c5570", size = 4428306, upload-time = "2026-03-13T06:58:49.502Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/0c/40779e45b20e11c7c5821a94135e0207080d6b3d76e7b78ccb413c6f839b/hf_xet-1.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:2f45c712c2fa1215713db10df6ac84b49d0e1c393465440e9cb1de73ecf7bbf6", size = 3665826, upload-time = "2026-03-13T06:58:59.88Z" },
+ { url = "https://files.pythonhosted.org/packages/51/4c/e2688c8ad1760d7c30f7c429c79f35f825932581bc7c9ec811436d2f21a0/hf_xet-1.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:6d53df40616f7168abfccff100d232e9d460583b9d86fa4912c24845f192f2b8", size = 3529113, upload-time = "2026-03-13T06:58:58.491Z" },
{ url = "https://files.pythonhosted.org/packages/b4/86/b40b83a2ff03ef05c4478d2672b1fc2b9683ff870e2b25f4f3af240f2e7b/hf_xet-1.4.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:71f02d6e4cdd07f344f6844845d78518cc7186bd2bc52d37c3b73dc26a3b0bc5", size = 3800339, upload-time = "2026-03-13T06:58:36.245Z" },
{ url = "https://files.pythonhosted.org/packages/64/2e/af4475c32b4378b0e92a587adb1aa3ec53e3450fd3e5fe0372a874531c00/hf_xet-1.4.2-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:e9b38d876e94d4bdcf650778d6ebbaa791dd28de08db9736c43faff06ede1b5a", size = 3559664, upload-time = "2026-03-13T06:58:34.787Z" },
{ url = "https://files.pythonhosted.org/packages/3c/4c/781267da3188db679e601de18112021a5cb16506fe86b246e22c5401a9c4/hf_xet-1.4.2-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:77e8c180b7ef12d8a96739a4e1e558847002afe9ea63b6f6358b2271a8bdda1c", size = 4217422, upload-time = "2026-03-13T06:58:27.472Z" },
@@ -856,9 +1010,12 @@ name = "ipython"
version = "9.11.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
- "python_full_version >= '3.12' and sys_platform == 'win32'",
- "python_full_version >= '3.12' and sys_platform == 'emscripten'",
- "python_full_version >= '3.12' 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 == 'emscripten'",
+ "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
+ "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'",
+ "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'",
+ "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
]
dependencies = [
{ name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" },
@@ -1275,6 +1432,36 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1b/bd/877056304626943ff0f1f44c08f584300c199b887cb3176cd7e34f1515f1/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:fc4d3f1fb9ca0ae9f97b095963bc6326f1dbfd3779d6679a1e016b9baaa153d3", size = 2597482, upload-time = "2026-03-09T13:14:34.971Z" },
{ url = "https://files.pythonhosted.org/packages/75/19/c60626c47bf0f8ac5dcf72c6c98e266d714f2fbbfd50cf6dab5ede3aaa50/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f443b4825c50a51ee68585522ab4a1d1257fac65896f282b4c6763337ac9f5d2", size = 2394328, upload-time = "2026-03-09T13:14:36.816Z" },
{ url = "https://files.pythonhosted.org/packages/47/84/6a6d5e5bb8273756c27b7d810d47f7ef2f1f9b9fd23c9ee9a3f8c75c9cef/kiwisolver-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:893ff3a711d1b515ba9da14ee090519bad4610ed1962fbe298a434e8c5f8db53", size = 68410, upload-time = "2026-03-09T13:14:38.695Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/d7/060f45052f2a01ad5762c8fdecd6d7a752b43400dc29ff75cd47225a40fd/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8df31fe574b8b3993cc61764f40941111b25c2d9fea13d3ce24a49907cd2d615", size = 123231, upload-time = "2026-03-09T13:14:41.323Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/a7/78da680eadd06ff35edef6ef68a1ad273bad3e2a0936c9a885103230aece/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1d49a49ac4cbfb7c1375301cd1ec90169dfeae55ff84710d782260ce77a75a02", size = 66489, upload-time = "2026-03-09T13:14:42.534Z" },
+ { url = "https://files.pythonhosted.org/packages/49/b2/97980f3ad4fae37dd7fe31626e2bf75fbf8bdf5d303950ec1fab39a12da8/kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e", size = 64063, upload-time = "2026-03-09T13:14:44.759Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/f9/b06c934a6aa8bc91f566bd2a214fd04c30506c2d9e2b6b171953216a65b6/kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac", size = 1475913, upload-time = "2026-03-09T13:14:46.247Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/f0/f768ae564a710135630672981231320bc403cf9152b5596ec5289de0f106/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e7f886f47ab881692f278ae901039a234e4025a68e6dfab514263a0b1c4ae05", size = 1282782, upload-time = "2026-03-09T13:14:48.458Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/9f/1de7aad00697325f05238a5f2eafbd487fb637cc27a558b5367a5f37fb7f/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5060731cc3ed12ca3a8b57acd4aeca5bbc2f49216dd0bec1650a1acd89486bcd", size = 1300815, upload-time = "2026-03-09T13:14:50.721Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/c2/297f25141d2e468e0ce7f7a7b92e0cf8918143a0cbd3422c1ad627e85a06/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a4aa69609f40fce3cbc3f87b2061f042eee32f94b8f11db707b66a26461591a", size = 1347925, upload-time = "2026-03-09T13:14:52.304Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/d3/f4c73a02eb41520c47610207b21afa8cdd18fdbf64ffd94674ae21c4812d/kiwisolver-1.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:d168fda2dbff7b9b5f38e693182d792a938c31db4dac3a80a4888de603c99554", size = 991322, upload-time = "2026-03-09T13:14:54.637Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/46/d3f2efef7732fcda98d22bf4ad5d3d71d545167a852ca710a494f4c15343/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:413b820229730d358efd838ecbab79902fe97094565fdc80ddb6b0a18c18a581", size = 2232857, upload-time = "2026-03-09T13:14:56.471Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/ec/2d9756bf2b6d26ae4349b8d3662fb3993f16d80c1f971c179ce862b9dbae/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5124d1ea754509b09e53738ec185584cc609aae4a3b510aaf4ed6aa047ef9303", size = 2329376, upload-time = "2026-03-09T13:14:58.072Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/9f/876a0a0f2260f1bde92e002b3019a5fabc35e0939c7d945e0fa66185eb20/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e4415a8db000bf49a6dd1c478bf70062eaacff0f462b92b0ba68791a905861f9", size = 1982549, upload-time = "2026-03-09T13:14:59.668Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/4f/ba3624dfac23a64d54ac4179832860cb537c1b0af06024936e82ca4154a0/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d618fd27420381a4f6044faa71f46d8bfd911bd077c555f7138ed88729bfbe79", size = 2494680, upload-time = "2026-03-09T13:15:01.364Z" },
+ { url = "https://files.pythonhosted.org/packages/39/b7/97716b190ab98911b20d10bf92eca469121ec483b8ce0edd314f51bc85af/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5092eb5b1172947f57d6ea7d89b2f29650414e4293c47707eb499ec07a0ac796", size = 2297905, upload-time = "2026-03-09T13:15:03.925Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/36/4e551e8aa55c9188bca9abb5096805edbf7431072b76e2298e34fd3a3008/kiwisolver-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e", size = 75086, upload-time = "2026-03-09T13:15:07.775Z" },
+ { url = "https://files.pythonhosted.org/packages/70/15/9b90f7df0e31a003c71649cf66ef61c3c1b862f48c81007fa2383c8bd8d7/kiwisolver-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:fa6248cd194edff41d7ea9425ced8ca3a6f838bfb295f6f1d6e6bb694a8518df", size = 66577, upload-time = "2026-03-09T13:15:09.139Z" },
+ { url = "https://files.pythonhosted.org/packages/17/01/7dc8c5443ff42b38e72731643ed7cf1ed9bf01691ae5cdca98501999ed83/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d1ffeb80b5676463d7a7d56acbe8e37a20ce725570e09549fe738e02ca6b7e1e", size = 125794, upload-time = "2026-03-09T13:15:10.525Z" },
+ { url = "https://files.pythonhosted.org/packages/46/8a/b4ebe46ebaac6a303417fab10c2e165c557ddaff558f9699d302b256bc53/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc4d8e252f532ab46a1de9349e2d27b91fce46736a9eedaa37beaca66f574ed4", size = 67646, upload-time = "2026-03-09T13:15:12.016Z" },
+ { url = "https://files.pythonhosted.org/packages/60/35/10a844afc5f19d6f567359bf4789e26661755a2f36200d5d1ed8ad0126e5/kiwisolver-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6783e069732715ad0c3ce96dbf21dbc2235ab0593f2baf6338101f70371f4028", size = 65511, upload-time = "2026-03-09T13:15:13.311Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/8a/685b297052dd041dcebce8e8787b58923b6e78acc6115a0dc9189011c44b/kiwisolver-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7c4c09a490dc4d4a7f8cbee56c606a320f9dc28cf92a7157a39d1ce7676a657", size = 1584858, upload-time = "2026-03-09T13:15:15.103Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/80/04865e3d4638ac5bddec28908916df4a3075b8c6cc101786a96803188b96/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a075bd7bd19c70cf67c8badfa36cf7c5d8de3c9ddb8420c51e10d9c50e94920", size = 1392539, upload-time = "2026-03-09T13:15:16.661Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/01/77a19cacc0893fa13fafa46d1bba06fb4dc2360b3292baf4b56d8e067b24/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bdd3e53429ff02aa319ba59dfe4ceeec345bf46cf180ec2cf6fd5b942e7975e9", size = 1405310, upload-time = "2026-03-09T13:15:18.229Z" },
+ { url = "https://files.pythonhosted.org/packages/53/39/bcaf5d0cca50e604cfa9b4e3ae1d64b50ca1ae5b754122396084599ef903/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cdcb35dc9d807259c981a85531048ede628eabcffb3239adf3d17463518992d", size = 1456244, upload-time = "2026-03-09T13:15:20.444Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/7a/72c187abc6975f6978c3e39b7cf67aeb8b3c0a8f9790aa7fd412855e9e1f/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:70d593af6a6ca332d1df73d519fddb5148edb15cd90d5f0155e3746a6d4fcc65", size = 1073154, upload-time = "2026-03-09T13:15:22.039Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/ca/cf5b25783ebbd59143b4371ed0c8428a278abe68d6d0104b01865b1bbd0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:377815a8616074cabbf3f53354e1d040c35815a134e01d7614b7692e4bf8acfa", size = 2334377, upload-time = "2026-03-09T13:15:23.741Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/e5/b1f492adc516796e88751282276745340e2a72dcd0d36cf7173e0daf3210/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0255a027391d52944eae1dbb5d4cc5903f57092f3674e8e544cdd2622826b3f0", size = 2425288, upload-time = "2026-03-09T13:15:25.789Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/e5/9b21fbe91a61b8f409d74a26498706e97a48008bfcd1864373d32a6ba31c/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9", size = 2063158, upload-time = "2026-03-09T13:15:27.63Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/02/83f47986138310f95ea95531f851b2a62227c11cbc3e690ae1374fe49f0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e3aafb33aed7479377e5e9a82e9d4bf87063741fc99fc7ae48b0f16e32bdd6f", size = 2597260, upload-time = "2026-03-09T13:15:29.421Z" },
+ { url = "https://files.pythonhosted.org/packages/07/18/43a5f24608d8c313dd189cf838c8e68d75b115567c6279de7796197cfb6a/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7a116ae737f0000343218c4edf5bd45893bfeaff0993c0b215d7124c9f77646", size = 2394403, upload-time = "2026-03-09T13:15:31.517Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/b5/98222136d839b8afabcaa943b09bd05888c2d36355b7e448550211d1fca4/kiwisolver-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1dd9b0b119a350976a6d781e7278ec7aca0b201e1a9e2d23d9804afecb6ca681", size = 79687, upload-time = "2026-03-09T13:15:33.204Z" },
+ { url = "https://files.pythonhosted.org/packages/99/a2/ca7dc962848040befed12732dff6acae7fb3c4f6fc4272b3f6c9a30b8713/kiwisolver-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:58f812017cd2985c21fbffb4864d59174d4903dd66fa23815e74bbc7a0e2dd57", size = 70032, upload-time = "2026-03-09T13:15:34.411Z" },
{ url = "https://files.pythonhosted.org/packages/1c/fa/2910df836372d8761bb6eff7d8bdcb1613b5c2e03f260efe7abe34d388a7/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:5ae8e62c147495b01a0f4765c878e9bfdf843412446a247e28df59936e99e797", size = 130262, upload-time = "2026-03-09T13:15:35.629Z" },
{ url = "https://files.pythonhosted.org/packages/0f/41/c5f71f9f00aabcc71fee8b7475e3f64747282580c2fe748961ba29b18385/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f6764a4ccab3078db14a632420930f6186058750df066b8ea2a7106df91d3203", size = 138036, upload-time = "2026-03-09T13:15:36.894Z" },
{ url = "https://files.pythonhosted.org/packages/fa/06/7399a607f434119c6e1fdc8ec89a8d51ccccadf3341dee4ead6bd14caaf5/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c31c13da98624f957b0fb1b5bae5383b2333c2c3f6793d9825dd5ce79b525cb7", size = 194295, upload-time = "2026-03-09T13:15:38.22Z" },
@@ -1357,6 +1544,28 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" },
{ url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" },
{ url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" },
+ { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
+ { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
+ { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
+ { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
+ { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
+ { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
+ { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
+ { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
+ { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
+ { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
+ { 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]]
@@ -1404,6 +1613,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c0/3d/8b94a481456dfc9dfe6e39e93b5ab376e50998cddfd23f4ae3b431708f16/matplotlib-3.10.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a", size = 9614000, upload-time = "2025-12-10T22:56:05.411Z" },
{ url = "https://files.pythonhosted.org/packages/bd/cd/bc06149fe5585ba800b189a6a654a75f1f127e8aab02fd2be10df7fa500c/matplotlib-3.10.8-cp313-cp313t-win_amd64.whl", hash = "sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958", size = 8220043, upload-time = "2025-12-10T22:56:07.551Z" },
{ url = "https://files.pythonhosted.org/packages/e3/de/b22cf255abec916562cc04eef457c13e58a1990048de0c0c3604d082355e/matplotlib-3.10.8-cp313-cp313t-win_arm64.whl", hash = "sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5", size = 8062075, upload-time = "2025-12-10T22:56:09.178Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/43/9c0ff7a2f11615e516c3b058e1e6e8f9614ddeca53faca06da267c48345d/matplotlib-3.10.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f", size = 8262481, upload-time = "2025-12-10T22:56:10.885Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/ca/e8ae28649fcdf039fda5ef554b40a95f50592a3c47e6f7270c9561c12b07/matplotlib-3.10.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b", size = 8151473, upload-time = "2025-12-10T22:56:12.377Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/6f/009d129ae70b75e88cbe7e503a12a4c0670e08ed748a902c2568909e9eb5/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d", size = 9553896, upload-time = "2025-12-10T22:56:14.432Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/26/4221a741eb97967bc1fd5e4c52b9aa5a91b2f4ec05b59f6def4d820f9df9/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008", size = 9824193, upload-time = "2025-12-10T22:56:16.29Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/f3/3abf75f38605772cf48a9daf5821cd4f563472f38b4b828c6fba6fa6d06e/matplotlib-3.10.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c", size = 9615444, upload-time = "2025-12-10T22:56:18.155Z" },
+ { url = "https://files.pythonhosted.org/packages/93/a5/de89ac80f10b8dc615807ee1133cd99ac74082581196d4d9590bea10690d/matplotlib-3.10.8-cp314-cp314-win_amd64.whl", hash = "sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11", size = 8272719, upload-time = "2025-12-10T22:56:20.366Z" },
+ { url = "https://files.pythonhosted.org/packages/69/ce/b006495c19ccc0a137b48083168a37bd056392dee02f87dba0472f2797fe/matplotlib-3.10.8-cp314-cp314-win_arm64.whl", hash = "sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8", size = 8144205, upload-time = "2025-12-10T22:56:22.239Z" },
+ { url = "https://files.pythonhosted.org/packages/68/d9/b31116a3a855bd313c6fcdb7226926d59b041f26061c6c5b1be66a08c826/matplotlib-3.10.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50", size = 8305785, upload-time = "2025-12-10T22:56:24.218Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/90/6effe8103f0272685767ba5f094f453784057072f49b393e3ea178fe70a5/matplotlib-3.10.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908", size = 8198361, upload-time = "2025-12-10T22:56:26.787Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/65/a73188711bea603615fc0baecca1061429ac16940e2385433cc778a9d8e7/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a", size = 9561357, upload-time = "2025-12-10T22:56:28.953Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/3d/b5c5d5d5be8ce63292567f0e2c43dde9953d3ed86ac2de0a72e93c8f07a1/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1", size = 9823610, upload-time = "2025-12-10T22:56:31.455Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/4b/e7beb6bbd49f6bae727a12b270a2654d13c397576d25bd6786e47033300f/matplotlib-3.10.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c", size = 9614011, upload-time = "2025-12-10T22:56:33.85Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/e6/76f2813d31f032e65f6f797e3f2f6e4aab95b65015924b1c51370395c28a/matplotlib-3.10.8-cp314-cp314t-win_amd64.whl", hash = "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b", size = 8362801, upload-time = "2025-12-10T22:56:36.107Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/49/d651878698a0b67f23aa28e17f45a6d6dd3d3f933fa29087fa4ce5947b5a/matplotlib-3.10.8-cp314-cp314t-win_arm64.whl", hash = "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f", size = 8192560, upload-time = "2025-12-10T22:56:38.008Z" },
{ url = "https://files.pythonhosted.org/packages/04/30/3afaa31c757f34b7725ab9d2ba8b48b5e89c2019c003e7d0ead143aabc5a/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1", size = 8249198, upload-time = "2025-12-10T22:56:45.584Z" },
{ url = "https://files.pythonhosted.org/packages/48/2f/6334aec331f57485a642a7c8be03cb286f29111ae71c46c38b363230063c/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a", size = 8136817, upload-time = "2025-12-10T22:56:47.339Z" },
{ url = "https://files.pythonhosted.org/packages/73/e4/6d6f14b2a759c622f191b2d67e9075a3f56aaccb3be4bb9bb6890030d0a0/matplotlib-3.10.8-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2", size = 8713867, upload-time = "2025-12-10T22:56:48.954Z" },
@@ -1477,6 +1700,22 @@ wheels = [
{ 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]]
@@ -1638,6 +1877,27 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ed/81/9f24708953cd30be9ee36ec4778f4b112b45165812f2ada4cc5ea1c1f254/numpy-2.4.3-cp313-cp313t-win32.whl", hash = "sha256:be3b8487d725a77acccc9924f65fd8bce9af7fac8c9820df1049424a2115af6c", size = 6082814, upload-time = "2026-03-09T07:57:36.491Z" },
{ url = "https://files.pythonhosted.org/packages/e2/9e/52f6eaa13e1a799f0ab79066c17f7016a4a8ae0c1aefa58c82b4dab690b4/numpy-2.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1ec84fd7c8e652b0f4aaaf2e6e9cc8eaa9b1b80a537e06b2e3a2fb176eedcb26", size = 12452673, upload-time = "2026-03-09T07:57:38.281Z" },
{ url = "https://files.pythonhosted.org/packages/c4/04/b8cece6ead0b30c9fbd99bb835ad7ea0112ac5f39f069788c5558e3b1ab2/numpy-2.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:120df8c0a81ebbf5b9020c91439fccd85f5e018a927a39f624845be194a2be02", size = 10290907, upload-time = "2026-03-09T07:57:40.747Z" },
+ { url = "https://files.pythonhosted.org/packages/70/ae/3936f79adebf8caf81bd7a599b90a561334a658be4dcc7b6329ebf4ee8de/numpy-2.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5884ce5c7acfae1e4e1b6fde43797d10aa506074d25b531b4f54bde33c0c31d4", size = 16664563, upload-time = "2026-03-09T07:57:43.817Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/62/760f2b55866b496bb1fa7da2a6db076bef908110e568b02fcfc1422e2a3a/numpy-2.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:297837823f5bc572c5f9379b0c9f3a3365f08492cbdc33bcc3af174372ebb168", size = 14702161, upload-time = "2026-03-09T07:57:46.169Z" },
+ { url = "https://files.pythonhosted.org/packages/32/af/a7a39464e2c0a21526fb4fb76e346fb172ebc92f6d1c7a07c2c139cc17b1/numpy-2.4.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:a111698b4a3f8dcbe54c64a7708f049355abd603e619013c346553c1fd4ca90b", size = 5208738, upload-time = "2026-03-09T07:57:48.506Z" },
+ { url = "https://files.pythonhosted.org/packages/29/8c/2a0cf86a59558fa078d83805589c2de490f29ed4fb336c14313a161d358a/numpy-2.4.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:4bd4741a6a676770e0e97fe9ab2e51de01183df3dcbcec591d26d331a40de950", size = 6543618, upload-time = "2026-03-09T07:57:50.591Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/b8/612ce010c0728b1c363fa4ea3aa4c22fe1c5da1de008486f8c2f5cb92fae/numpy-2.4.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54f29b877279d51e210e0c80709ee14ccbbad647810e8f3d375561c45ef613dd", size = 15680676, upload-time = "2026-03-09T07:57:52.34Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/7e/4f120ecc54ba26ddf3dc348eeb9eb063f421de65c05fc961941798feea18/numpy-2.4.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:679f2a834bae9020f81534671c56fd0cc76dd7e5182f57131478e23d0dc59e24", size = 16613492, upload-time = "2026-03-09T07:57:54.91Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/86/1b6020db73be330c4b45d5c6ee4295d59cfeef0e3ea323959d053e5a6909/numpy-2.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d84f0f881cb2225c2dfd7f78a10a5645d487a496c6668d6cc39f0f114164f3d0", size = 17031789, upload-time = "2026-03-09T07:57:57.641Z" },
+ { url = "https://files.pythonhosted.org/packages/07/3a/3b90463bf41ebc21d1b7e06079f03070334374208c0f9a1f05e4ae8455e7/numpy-2.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d213c7e6e8d211888cc359bab7199670a00f5b82c0978b9d1c75baf1eddbeac0", size = 18339941, upload-time = "2026-03-09T07:58:00.577Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/74/6d736c4cd962259fd8bae9be27363eb4883a2f9069763747347544c2a487/numpy-2.4.3-cp314-cp314-win32.whl", hash = "sha256:52077feedeff7c76ed7c9f1a0428558e50825347b7545bbb8523da2cd55c547a", size = 6007503, upload-time = "2026-03-09T07:58:03.331Z" },
+ { url = "https://files.pythonhosted.org/packages/48/39/c56ef87af669364356bb011922ef0734fc49dad51964568634c72a009488/numpy-2.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:0448e7f9caefb34b4b7dd2b77f21e8906e5d6f0365ad525f9f4f530b13df2afc", size = 12444915, upload-time = "2026-03-09T07:58:06.353Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/1f/ab8528e38d295fd349310807496fabb7cf9fe2e1f70b97bc20a483ea9d4a/numpy-2.4.3-cp314-cp314-win_arm64.whl", hash = "sha256:b44fd60341c4d9783039598efadd03617fa28d041fc37d22b62d08f2027fa0e7", size = 10494875, upload-time = "2026-03-09T07:58:08.734Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/ef/b7c35e4d5ef141b836658ab21a66d1a573e15b335b1d111d31f26c8ef80f/numpy-2.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0a195f4216be9305a73c0e91c9b026a35f2161237cf1c6de9b681637772ea657", size = 14822225, upload-time = "2026-03-09T07:58:11.034Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/8d/7730fa9278cf6648639946cc816e7cc89f0d891602584697923375f801ed/numpy-2.4.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:cd32fbacb9fd1bf041bf8e89e4576b6f00b895f06d00914820ae06a616bdfef7", size = 5328769, upload-time = "2026-03-09T07:58:13.67Z" },
+ { url = "https://files.pythonhosted.org/packages/47/01/d2a137317c958b074d338807c1b6a383406cdf8b8e53b075d804cc3d211d/numpy-2.4.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:2e03c05abaee1f672e9d67bc858f300b5ccba1c21397211e8d77d98350972093", size = 6649461, upload-time = "2026-03-09T07:58:15.912Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/34/812ce12bc0f00272a4b0ec0d713cd237cb390666eb6206323d1cc9cedbb2/numpy-2.4.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d1ce23cce91fcea443320a9d0ece9b9305d4368875bab09538f7a5b4131938a", size = 15725809, upload-time = "2026-03-09T07:58:17.787Z" },
+ { url = "https://files.pythonhosted.org/packages/25/c0/2aed473a4823e905e765fee3dc2cbf504bd3e68ccb1150fbdabd5c39f527/numpy-2.4.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c59020932feb24ed49ffd03704fbab89f22aa9c0d4b180ff45542fe8918f5611", size = 16655242, upload-time = "2026-03-09T07:58:20.476Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/c8/7e052b2fc87aa0e86de23f20e2c42bd261c624748aa8efd2c78f7bb8d8c6/numpy-2.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9684823a78a6cd6ad7511fc5e25b07947d1d5b5e2812c93fe99d7d4195130720", size = 17080660, upload-time = "2026-03-09T07:58:23.067Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/3d/0876746044db2adcb11549f214d104f2e1be00f07a67edbb4e2812094847/numpy-2.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0200b25c687033316fb39f0ff4e3e690e8957a2c3c8d22499891ec58c37a3eb5", size = 18380384, upload-time = "2026-03-09T07:58:25.839Z" },
+ { url = "https://files.pythonhosted.org/packages/07/12/8160bea39da3335737b10308df4f484235fd297f556745f13092aa039d3b/numpy-2.4.3-cp314-cp314t-win32.whl", hash = "sha256:5e10da9e93247e554bb1d22f8edc51847ddd7dde52d85ce31024c1b4312bfba0", size = 6154547, upload-time = "2026-03-09T07:58:28.289Z" },
+ { url = "https://files.pythonhosted.org/packages/42/f3/76534f61f80d74cc9cdf2e570d3d4eeb92c2280a27c39b0aaf471eda7b48/numpy-2.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:45f003dbdffb997a03da2d1d0cb41fbd24a87507fb41605c0420a3db5bd4667b", size = 12633645, upload-time = "2026-03-09T07:58:30.384Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/b6/7c0d4334c15983cec7f92a69e8ce9b1e6f31857e5ee3a413ac424e6bd63d/numpy-2.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:4d382735cecd7bcf090172489a525cd7d4087bc331f7df9f60ddc9a296cf208e", size = 10565454, upload-time = "2026-03-09T07:58:33.031Z" },
{ url = "https://files.pythonhosted.org/packages/64/e4/4dab9fb43c83719c29241c535d9e07be73bea4bc0c6686c5816d8e1b6689/numpy-2.4.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c6b124bfcafb9e8d3ed09130dbee44848c20b3e758b6bbf006e641778927c028", size = 16834892, upload-time = "2026-03-09T07:58:35.334Z" },
{ url = "https://files.pythonhosted.org/packages/c9/29/f8b6d4af90fed3dfda84ebc0df06c9833d38880c79ce954e5b661758aa31/numpy-2.4.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:76dbb9d4e43c16cf9aa711fcd8de1e2eeb27539dcefb60a1d5e9f12fae1d1ed8", size = 14893070, upload-time = "2026-03-09T07:58:37.7Z" },
{ url = "https://files.pythonhosted.org/packages/9a/04/a19b3c91dbec0a49269407f15d5753673a09832daed40c45e8150e6fa558/numpy-2.4.3-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:29363fbfa6f8ee855d7569c96ce524845e3d726d6c19b29eceec7dd555dab152", size = 5399609, upload-time = "2026-03-09T07:58:39.853Z" },
@@ -1707,6 +1967,22 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a2/61/772b2e2757855e232b7ccf7cb8079a5711becb3a97f291c953def15a833f/pandas-3.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6bf0603c2e30e2cafac32807b06435f28741135cb8697eae8b28c7d492fc7d76", size = 11334191, upload-time = "2026-02-17T22:19:29.411Z" },
{ url = "https://files.pythonhosted.org/packages/1b/08/b16c6df3ef555d8495d1d265a7963b65be166785d28f06a350913a4fac78/pandas-3.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6c426422973973cae1f4a23e51d4ae85974f44871b24844e4f7de752dd877098", size = 11782256, upload-time = "2026-02-17T22:19:32.34Z" },
{ url = "https://files.pythonhosted.org/packages/55/80/178af0594890dee17e239fca96d3d8670ba0f5ff59b7d0439850924a9c09/pandas-3.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b03f91ae8c10a85c1613102c7bef5229b5379f343030a3ccefeca8a33414cf35", size = 10485047, upload-time = "2026-02-17T22:19:34.605Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/8b/4bb774a998b97e6c2fd62a9e6cfdaae133b636fd1c468f92afb4ae9a447a/pandas-3.0.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:99d0f92ed92d3083d140bf6b97774f9f13863924cf3f52a70711f4e7588f9d0a", size = 10322465, upload-time = "2026-02-17T22:19:36.803Z" },
+ { url = "https://files.pythonhosted.org/packages/72/3a/5b39b51c64159f470f1ca3b1c2a87da290657ca022f7cd11442606f607d1/pandas-3.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3b66857e983208654294bb6477b8a63dee26b37bdd0eb34d010556e91261784f", size = 9910632, upload-time = "2026-02-17T22:19:39.001Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/f7/b449ffb3f68c11da12fc06fbf6d2fa3a41c41e17d0284d23a79e1c13a7e4/pandas-3.0.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56cf59638bf24dc9bdf2154c81e248b3289f9a09a6d04e63608c159022352749", size = 10440535, upload-time = "2026-02-17T22:19:41.157Z" },
+ { url = "https://files.pythonhosted.org/packages/55/77/6ea82043db22cb0f2bbfe7198da3544000ddaadb12d26be36e19b03a2dc5/pandas-3.0.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1a9f55e0f46951874b863d1f3906dcb57df2d9be5c5847ba4dfb55b2c815249", size = 10893940, upload-time = "2026-02-17T22:19:43.493Z" },
+ { url = "https://files.pythonhosted.org/packages/03/30/f1b502a72468c89412c1b882a08f6eed8a4ee9dc033f35f65d0663df6081/pandas-3.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1849f0bba9c8a2fb0f691d492b834cc8dadf617e29015c66e989448d58d011ee", size = 11442711, upload-time = "2026-02-17T22:19:46.074Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/f0/ebb6ddd8fc049e98cabac5c2924d14d1dda26a20adb70d41ea2e428d3ec4/pandas-3.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3d288439e11b5325b02ae6e9cc83e6805a62c40c5a6220bea9beb899c073b1c", size = 11963918, upload-time = "2026-02-17T22:19:48.838Z" },
+ { url = "https://files.pythonhosted.org/packages/09/f8/8ce132104074f977f907442790eaae24e27bce3b3b454e82faa3237ff098/pandas-3.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:93325b0fe372d192965f4cca88d97667f49557398bbf94abdda3bf1b591dbe66", size = 9862099, upload-time = "2026-02-17T22:19:51.081Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/b7/6af9aac41ef2456b768ef0ae60acf8abcebb450a52043d030a65b4b7c9bd/pandas-3.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:97ca08674e3287c7148f4858b01136f8bdfe7202ad25ad04fec602dd1d29d132", size = 9185333, upload-time = "2026-02-17T22:19:53.266Z" },
+ { url = "https://files.pythonhosted.org/packages/66/fc/848bb6710bc6061cb0c5badd65b92ff75c81302e0e31e496d00029fe4953/pandas-3.0.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:58eeb1b2e0fb322befcf2bbc9ba0af41e616abadb3d3414a6bc7167f6cbfce32", size = 10772664, upload-time = "2026-02-17T22:19:55.806Z" },
+ { url = "https://files.pythonhosted.org/packages/69/5c/866a9bbd0f79263b4b0db6ec1a341be13a1473323f05c122388e0f15b21d/pandas-3.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cd9af1276b5ca9e298bd79a26bda32fa9cc87ed095b2a9a60978d2ca058eaf87", size = 10421286, upload-time = "2026-02-17T22:19:58.091Z" },
+ { url = "https://files.pythonhosted.org/packages/51/a4/2058fb84fb1cfbfb2d4a6d485e1940bb4ad5716e539d779852494479c580/pandas-3.0.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f87a04984d6b63788327cd9f79dda62b7f9043909d2440ceccf709249ca988", size = 10342050, upload-time = "2026-02-17T22:20:01.376Z" },
+ { url = "https://files.pythonhosted.org/packages/22/1b/674e89996cc4be74db3c4eb09240c4bb549865c9c3f5d9b086ff8fcfbf00/pandas-3.0.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85fe4c4df62e1e20f9db6ebfb88c844b092c22cd5324bdcf94bfa2fc1b391221", size = 10740055, upload-time = "2026-02-17T22:20:04.328Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/f8/e954b750764298c22fa4614376531fe63c521ef517e7059a51f062b87dca/pandas-3.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:331ca75a2f8672c365ae25c0b29e46f5ac0c6551fdace8eec4cd65e4fac271ff", size = 11357632, upload-time = "2026-02-17T22:20:06.647Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/02/c6e04b694ffd68568297abd03588b6d30295265176a5c01b7459d3bc35a3/pandas-3.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:15860b1fdb1973fffade772fdb931ccf9b2f400a3f5665aef94a00445d7d8dd5", size = 11810974, upload-time = "2026-02-17T22:20:08.946Z" },
+ { url = "https://files.pythonhosted.org/packages/89/41/d7dfb63d2407f12055215070c42fc6ac41b66e90a2946cdc5e759058398b/pandas-3.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:44f1364411d5670efa692b146c748f4ed013df91ee91e9bec5677fb1fd58b937", size = 10884622, upload-time = "2026-02-17T22:20:11.711Z" },
+ { url = "https://files.pythonhosted.org/packages/68/b0/34937815889fa982613775e4b97fddd13250f11012d769949c5465af2150/pandas-3.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:108dd1790337a494aa80e38def654ca3f0968cf4f362c85f44c15e471667102d", size = 9452085, upload-time = "2026-02-17T22:20:14.331Z" },
]
[[package]]
@@ -1784,6 +2060,27 @@ wheels = [
{ 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/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" },
@@ -1872,6 +2169,22 @@ wheels = [
{ 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]]
@@ -2006,6 +2319,12 @@ wheels = [
{ 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" },
@@ -2109,6 +2428,34 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" },
{ url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" },
{ url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" },
+ { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" },
+ { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" },
+ { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" },
+ { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" },
+ { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" },
+ { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" },
+ { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" },
+ { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" },
+ { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" },
+ { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" },
+ { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
{ url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" },
{ url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" },
{ url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" },
@@ -2242,6 +2589,10 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fd/50/724ed5c38c504d4e58a88a072776a1e880d970789deaeb2b9f7bd9a5141a/pywinpty-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:fe1f7911805127c94cf51f89ab14096c6f91ffdcacf993d2da6082b2142a2523", size = 234591, upload-time = "2026-02-04T21:52:29.821Z" },
{ url = "https://files.pythonhosted.org/packages/f7/ad/90a110538696b12b39fd8758a06d70ded899308198ad2305ac68e361126e/pywinpty-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:3f07a6cf1c1d470d284e614733c3d0f726d2c85e78508ea10a403140c3c0c18a", size = 2112360, upload-time = "2026-02-04T21:55:33.397Z" },
{ url = "https://files.pythonhosted.org/packages/44/0f/7ffa221757a220402bc79fda44044c3f2cc57338d878ab7d622add6f4581/pywinpty-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:15c7c0b6f8e9d87aabbaff76468dabf6e6121332c40fc1d83548d02a9d6a3759", size = 233107, upload-time = "2026-02-04T21:51:45.455Z" },
+ { url = "https://files.pythonhosted.org/packages/28/88/2ff917caff61e55f38bcdb27de06ee30597881b2cae44fbba7627be015c4/pywinpty-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:d4b6b7b0fe0cdcd02e956bd57cfe9f4e5a06514eecf3b5ae174da4f951b58be9", size = 2113282, upload-time = "2026-02-04T21:52:08.188Z" },
+ { url = "https://files.pythonhosted.org/packages/63/32/40a775343ace542cc43ece3f1d1fce454021521ecac41c4c4573081c2336/pywinpty-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:34789d685fc0d547ce0c8a65e5a70e56f77d732fa6e03c8f74fefb8cbb252019", size = 234207, upload-time = "2026-02-04T21:51:58.687Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/54/5d5e52f4cb75028104ca6faf36c10f9692389b1986d34471663b4ebebd6d/pywinpty-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:0c37e224a47a971d1a6e08649a1714dac4f63c11920780977829ed5c8cadead1", size = 2112910, upload-time = "2026-02-04T21:52:30.976Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/44/dcd184824e21d4620b06c7db9fbb15c3ad0a0f1fa2e6de79969fb82647ec/pywinpty-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:c4e9c3dff7d86ba81937438d5819f19f385a39d8f592d4e8af67148ceb4f6ab5", size = 233425, upload-time = "2026-02-04T21:51:56.754Z" },
]
[[package]]
@@ -2279,6 +2630,24 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
{ url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
{ url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
+ { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
+ { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
+ { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
+ { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
+ { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
+ { 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]]
@@ -2322,6 +2691,16 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4f/6f/55c10e2e49ad52d080dc24e37adb215e5b0d64990b57598abc2e3f01725b/pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c", size = 574964, upload-time = "2025-09-08T23:08:37.178Z" },
{ url = "https://files.pythonhosted.org/packages/87/4d/2534970ba63dd7c522d8ca80fb92777f362c0f321900667c615e2067cb29/pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2", size = 641029, upload-time = "2025-09-08T23:08:40.595Z" },
{ url = "https://files.pythonhosted.org/packages/f6/fa/f8aea7a28b0641f31d40dea42d7ef003fded31e184ef47db696bc74cd610/pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e", size = 561541, upload-time = "2025-09-08T23:08:42.668Z" },
+ { url = "https://files.pythonhosted.org/packages/87/45/19efbb3000956e82d0331bafca5d9ac19ea2857722fa2caacefb6042f39d/pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a", size = 1341197, upload-time = "2025-09-08T23:08:44.973Z" },
+ { url = "https://files.pythonhosted.org/packages/48/43/d72ccdbf0d73d1343936296665826350cb1e825f92f2db9db3e61c2162a2/pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea", size = 897175, upload-time = "2025-09-08T23:08:46.601Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/2e/a483f73a10b65a9ef0161e817321d39a770b2acf8bcf3004a28d90d14a94/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96", size = 660427, upload-time = "2025-09-08T23:08:48.187Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/d2/5f36552c2d3e5685abe60dfa56f91169f7a2d99bbaf67c5271022ab40863/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d", size = 847929, upload-time = "2025-09-08T23:08:49.76Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/2a/404b331f2b7bf3198e9945f75c4c521f0c6a3a23b51f7a4a401b94a13833/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146", size = 1650193, upload-time = "2025-09-08T23:08:51.7Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/0b/f4107e33f62a5acf60e3ded67ed33d79b4ce18de432625ce2fc5093d6388/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd", size = 2024388, upload-time = "2025-09-08T23:08:53.393Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/01/add31fe76512642fd6e40e3a3bd21f4b47e242c8ba33efb6809e37076d9b/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a", size = 1885316, upload-time = "2025-09-08T23:08:55.702Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/59/a5f38970f9bf07cee96128de79590bb354917914a9be11272cfc7ff26af0/pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92", size = 587472, upload-time = "2025-09-08T23:08:58.18Z" },
+ { url = "https://files.pythonhosted.org/packages/70/d8/78b1bad170f93fcf5e3536e70e8fadac55030002275c9a29e8f5719185de/pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0", size = 661401, upload-time = "2025-09-08T23:08:59.802Z" },
+ { url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170, upload-time = "2025-09-08T23:09:01.418Z" },
{ url = "https://files.pythonhosted.org/packages/4c/c6/c4dcdecdbaa70969ee1fdced6d7b8f60cfabe64d25361f27ac4665a70620/pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066", size = 836265, upload-time = "2025-09-08T23:09:49.376Z" },
{ url = "https://files.pythonhosted.org/packages/3e/79/f38c92eeaeb03a2ccc2ba9866f0439593bb08c5e3b714ac1d553e5c96e25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604", size = 800208, upload-time = "2025-09-08T23:09:51.073Z" },
{ url = "https://files.pythonhosted.org/packages/49/0e/3f0d0d335c6b3abb9b7b723776d0b21fa7f3a6c819a0db6097059aada160/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c", size = 567747, upload-time = "2025-09-08T23:09:52.698Z" },
@@ -2413,6 +2792,38 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/69/50/0c7290987f97e7e6830b0d853f69dc4dc5852c934aae63e7fdcd76b4c383/regex-2026.2.28-cp313-cp313t-win32.whl", hash = "sha256:ef77bdde9c9eba3f7fa5b58084b29bbcc74bcf55fdbeaa67c102a35b5bd7e7cc", size = 269137, upload-time = "2026-02-28T02:18:25.375Z" },
{ url = "https://files.pythonhosted.org/packages/68/80/ef26ff90e74ceb4051ad6efcbbb8a4be965184a57e879ebcbdef327d18fa/regex-2026.2.28-cp313-cp313t-win_amd64.whl", hash = "sha256:98adf340100cbe6fbaf8e6dc75e28f2c191b1be50ffefe292fb0e6f6eefdb0d8", size = 280682, upload-time = "2026-02-28T02:18:27.205Z" },
{ url = "https://files.pythonhosted.org/packages/69/8b/fbad9c52e83ffe8f97e3ed1aa0516e6dff6bb633a41da9e64645bc7efdc5/regex-2026.2.28-cp313-cp313t-win_arm64.whl", hash = "sha256:2fb950ac1d88e6b6a9414381f403797b236f9fa17e1eee07683af72b1634207b", size = 271735, upload-time = "2026-02-28T02:18:29.015Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/03/691015f7a7cb1ed6dacb2ea5de5682e4858e05a4c5506b2839cd533bbcd6/regex-2026.2.28-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:78454178c7df31372ea737996fb7f36b3c2c92cccc641d251e072478afb4babc", size = 489497, upload-time = "2026-02-28T02:18:30.889Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/ba/8db8fd19afcbfa0e1036eaa70c05f20ca8405817d4ad7a38a6b4c2f031ac/regex-2026.2.28-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:5d10303dd18cedfd4d095543998404df656088240bcfd3cd20a8f95b861f74bd", size = 291295, upload-time = "2026-02-28T02:18:33.426Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/79/9aa0caf089e8defef9b857b52fc53801f62ff868e19e5c83d4a96612eba1/regex-2026.2.28-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:19a9c9e0a8f24f39d575a6a854d516b48ffe4cbdcb9de55cb0570a032556ecff", size = 289275, upload-time = "2026-02-28T02:18:35.247Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/26/ee53117066a30ef9c883bf1127eece08308ccf8ccd45c45a966e7a665385/regex-2026.2.28-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09500be324f49b470d907b3ef8af9afe857f5cca486f853853f7945ddbf75911", size = 797176, upload-time = "2026-02-28T02:18:37.15Z" },
+ { url = "https://files.pythonhosted.org/packages/05/1b/67fb0495a97259925f343ae78b5d24d4a6624356ae138b57f18bd43006e4/regex-2026.2.28-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fb1c4ff62277d87a7335f2c1ea4e0387b8f2b3ad88a64efd9943906aafad4f33", size = 863813, upload-time = "2026-02-28T02:18:39.478Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/1d/93ac9bbafc53618091c685c7ed40239a90bf9f2a82c983f0baa97cb7ae07/regex-2026.2.28-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b8b3f1be1738feadc69f62daa250c933e85c6f34fa378f54a7ff43807c1b9117", size = 908678, upload-time = "2026-02-28T02:18:41.619Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/7a/a8f5e0561702b25239846a16349feece59712ae20598ebb205580332a471/regex-2026.2.28-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc8ed8c3f41c27acb83f7b6a9eb727a73fc6663441890c5cb3426a5f6a91ce7d", size = 801528, upload-time = "2026-02-28T02:18:43.624Z" },
+ { url = "https://files.pythonhosted.org/packages/96/5d/ed6d4cbde80309854b1b9f42d9062fee38ade15f7eb4909f6ef2440403b5/regex-2026.2.28-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa539be029844c0ce1114762d2952ab6cfdd7c7c9bd72e0db26b94c3c36dcc5a", size = 775373, upload-time = "2026-02-28T02:18:46.102Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/e9/6e53c34e8068b9deec3e87210086ecb5b9efebdefca6b0d3fa43d66dcecb/regex-2026.2.28-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7900157786428a79615a8264dac1f12c9b02957c473c8110c6b1f972dcecaddf", size = 784859, upload-time = "2026-02-28T02:18:48.269Z" },
+ { url = "https://files.pythonhosted.org/packages/48/3c/736e1c7ca7f0dcd2ae33819888fdc69058a349b7e5e84bc3e2f296bbf794/regex-2026.2.28-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0b1d2b07614d95fa2bf8a63fd1e98bd8fa2b4848dc91b1efbc8ba219fdd73952", size = 857813, upload-time = "2026-02-28T02:18:50.576Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/7c/48c4659ad9da61f58e79dbe8c05223e0006696b603c16eb6b5cbfbb52c27/regex-2026.2.28-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b389c61aa28a79c2e0527ac36da579869c2e235a5b208a12c5b5318cda2501d8", size = 763705, upload-time = "2026-02-28T02:18:52.59Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/a1/bc1c261789283128165f71b71b4b221dd1b79c77023752a6074c102f18d8/regex-2026.2.28-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f467cb602f03fbd1ab1908f68b53c649ce393fde056628dc8c7e634dab6bfc07", size = 848734, upload-time = "2026-02-28T02:18:54.595Z" },
+ { url = "https://files.pythonhosted.org/packages/10/d8/979407faf1397036e25a5ae778157366a911c0f382c62501009f4957cf86/regex-2026.2.28-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8c8cb2deba42f5ec1ede46374e990f8adc5e6456a57ac1a261b19be6f28e4e6", size = 789871, upload-time = "2026-02-28T02:18:57.34Z" },
+ { url = "https://files.pythonhosted.org/packages/03/23/da716821277115fcb1f4e3de1e5dc5023a1e6533598c486abf5448612579/regex-2026.2.28-cp314-cp314-win32.whl", hash = "sha256:9036b400b20e4858d56d117108d7813ed07bb7803e3eed766675862131135ca6", size = 271825, upload-time = "2026-02-28T02:18:59.202Z" },
+ { url = "https://files.pythonhosted.org/packages/91/ff/90696f535d978d5f16a52a419be2770a8d8a0e7e0cfecdbfc31313df7fab/regex-2026.2.28-cp314-cp314-win_amd64.whl", hash = "sha256:1d367257cd86c1cbb97ea94e77b373a0bbc2224976e247f173d19e8f18b4afa7", size = 280548, upload-time = "2026-02-28T02:19:01.049Z" },
+ { url = "https://files.pythonhosted.org/packages/69/f9/5e1b5652fc0af3fcdf7677e7df3ad2a0d47d669b34ac29a63bb177bb731b/regex-2026.2.28-cp314-cp314-win_arm64.whl", hash = "sha256:5e68192bb3a1d6fb2836da24aa494e413ea65853a21505e142e5b1064a595f3d", size = 273444, upload-time = "2026-02-28T02:19:03.255Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/eb/8389f9e940ac89bcf58d185e230a677b4fd07c5f9b917603ad5c0f8fa8fe/regex-2026.2.28-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:a5dac14d0872eeb35260a8e30bac07ddf22adc1e3a0635b52b02e180d17c9c7e", size = 492546, upload-time = "2026-02-28T02:19:05.378Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/c7/09441d27ce2a6fa6a61ea3150ea4639c1dcda9b31b2ea07b80d6937b24dd/regex-2026.2.28-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ec0c608b7a7465ffadb344ed7c987ff2f11ee03f6a130b569aa74d8a70e8333c", size = 292986, upload-time = "2026-02-28T02:19:07.24Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/69/4144b60ed7760a6bd235e4087041f487aa4aa62b45618ce018b0c14833ea/regex-2026.2.28-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7815afb0ca45456613fdaf60ea9c993715511c8d53a83bc468305cbc0ee23c7", size = 291518, upload-time = "2026-02-28T02:19:09.698Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/be/77e5426cf5948c82f98c53582009ca9e94938c71f73a8918474f2e2990bb/regex-2026.2.28-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b059e71ec363968671693a78c5053bd9cb2fe410f9b8e4657e88377ebd603a2e", size = 809464, upload-time = "2026-02-28T02:19:12.494Z" },
+ { url = "https://files.pythonhosted.org/packages/45/99/2c8c5ac90dc7d05c6e7d8e72c6a3599dc08cd577ac476898e91ca787d7f1/regex-2026.2.28-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8cf76f1a29f0e99dcfd7aef1551a9827588aae5a737fe31442021165f1920dc", size = 869553, upload-time = "2026-02-28T02:19:15.151Z" },
+ { url = "https://files.pythonhosted.org/packages/53/34/daa66a342f0271e7737003abf6c3097aa0498d58c668dbd88362ef94eb5d/regex-2026.2.28-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:180e08a435a0319e6a4821c3468da18dc7001987e1c17ae1335488dfe7518dd8", size = 915289, upload-time = "2026-02-28T02:19:17.331Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/c7/e22c2aaf0a12e7e22ab19b004bb78d32ca1ecc7ef245949935463c5567de/regex-2026.2.28-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e496956106fd59ba6322a8ea17141a27c5040e5ee8f9433ae92d4e5204462a0", size = 812156, upload-time = "2026-02-28T02:19:20.011Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/bb/2dc18c1efd9051cf389cd0d7a3a4d90f6804b9fff3a51b5dc3c85b935f71/regex-2026.2.28-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bba2b18d70eeb7b79950f12f633beeecd923f7c9ad6f6bae28e59b4cb3ab046b", size = 782215, upload-time = "2026-02-28T02:19:22.047Z" },
+ { url = "https://files.pythonhosted.org/packages/17/1e/9e4ec9b9013931faa32226ec4aa3c71fe664a6d8a2b91ac56442128b332f/regex-2026.2.28-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6db7bfae0f8a2793ff1f7021468ea55e2699d0790eb58ee6ab36ae43aa00bc5b", size = 798925, upload-time = "2026-02-28T02:19:24.173Z" },
+ { url = "https://files.pythonhosted.org/packages/71/57/a505927e449a9ccb41e2cc8d735e2abe3444b0213d1cf9cb364a8c1f2524/regex-2026.2.28-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d0b02e8b7e5874b48ae0f077ecca61c1a6a9f9895e9c6dfb191b55b242862033", size = 864701, upload-time = "2026-02-28T02:19:26.376Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/ad/c62cb60cdd93e13eac5b3d9d6bd5d284225ed0e3329426f94d2552dd7cca/regex-2026.2.28-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:25b6eb660c5cf4b8c3407a1ed462abba26a926cc9965e164268a3267bcc06a43", size = 770899, upload-time = "2026-02-28T02:19:29.38Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/5a/874f861f5c3d5ab99633e8030dee1bc113db8e0be299d1f4b07f5b5ec349/regex-2026.2.28-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:5a932ea8ad5d0430351ff9c76c8db34db0d9f53c1d78f06022a21f4e290c5c18", size = 854727, upload-time = "2026-02-28T02:19:31.494Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/ca/d2c03b0efde47e13db895b975b2be6a73ed90b8ba963677927283d43bf74/regex-2026.2.28-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1c2c95e1a2b0f89d01e821ff4de1be4b5d73d1f4b0bf679fa27c1ad8d2327f1a", size = 800366, upload-time = "2026-02-28T02:19:34.248Z" },
+ { url = "https://files.pythonhosted.org/packages/14/bd/ee13b20b763b8989f7c75d592bfd5de37dc1181814a2a2747fedcf97e3ba/regex-2026.2.28-cp314-cp314t-win32.whl", hash = "sha256:bbb882061f742eb5d46f2f1bd5304055be0a66b783576de3d7eef1bed4778a6e", size = 274936, upload-time = "2026-02-28T02:19:36.313Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/e7/d8020e39414c93af7f0d8688eabcecece44abfd5ce314b21dfda0eebd3d8/regex-2026.2.28-cp314-cp314t-win_amd64.whl", hash = "sha256:6591f281cb44dc13de9585b552cec6fc6cf47fb2fe7a48892295ee9bc4a612f9", size = 284779, upload-time = "2026-02-28T02:19:38.625Z" },
+ { url = "https://files.pythonhosted.org/packages/13/c0/ad225f4a405827486f1955283407cf758b6d2fb966712644c5f5aef33d1b/regex-2026.2.28-cp314-cp314t-win_arm64.whl", hash = "sha256:dee50f1be42222f89767b64b283283ef963189da0dda4a515aa54a5563c62dec", size = 275010, upload-time = "2026-02-28T02:19:40.65Z" },
]
[[package]]
@@ -2553,6 +2964,35 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" },
{ url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" },
{ url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" },
+ { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" },
+ { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" },
+ { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" },
+ { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" },
+ { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" },
+ { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" },
+ { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" },
+ { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" },
+ { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" },
+ { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" },
+ { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" },
+ { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" },
{ url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" },
{ url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" },
{ url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" },
@@ -2650,6 +3090,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fe/56/a85473cd75f200c9759e3a5f0bcab2d116c92a8a02ee08ccd73b870f8bb4/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80832434a6cc114f5219211eec13dcbc16c2bac0e31ef64c6d346cde3cf054cb", size = 8925045, upload-time = "2025-12-10T07:08:22.11Z" },
{ url = "https://files.pythonhosted.org/packages/cc/b7/64d8cfa896c64435ae57f4917a548d7ac7a44762ff9802f75a79b77cb633/scikit_learn-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ee787491dbfe082d9c3013f01f5991658b0f38aa8177e4cd4bf434c58f551702", size = 8507994, upload-time = "2025-12-10T07:08:23.943Z" },
{ url = "https://files.pythonhosted.org/packages/5e/37/e192ea709551799379958b4c4771ec507347027bb7c942662c7fbeba31cb/scikit_learn-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf97c10a3f5a7543f9b88cbf488d33d175e9146115a451ae34568597ba33dcde", size = 7869518, upload-time = "2025-12-10T07:08:25.71Z" },
+ { url = "https://files.pythonhosted.org/packages/24/05/1af2c186174cc92dcab2233f327336058c077d38f6fe2aceb08e6ab4d509/scikit_learn-1.8.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c22a2da7a198c28dd1a6e1136f19c830beab7fdca5b3e5c8bba8394f8a5c45b3", size = 8528667, upload-time = "2025-12-10T07:08:27.541Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/25/01c0af38fe969473fb292bba9dc2b8f9b451f3112ff242c647fee3d0dfe7/scikit_learn-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:6b595b07a03069a2b1740dc08c2299993850ea81cce4fe19b2421e0c970de6b7", size = 8066524, upload-time = "2025-12-10T07:08:29.822Z" },
+ { url = "https://files.pythonhosted.org/packages/be/ce/a0623350aa0b68647333940ee46fe45086c6060ec604874e38e9ab7d8e6c/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29ffc74089f3d5e87dfca4c2c8450f88bdc61b0fc6ed5d267f3988f19a1309f6", size = 8657133, upload-time = "2025-12-10T07:08:31.865Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/cb/861b41341d6f1245e6ca80b1c1a8c4dfce43255b03df034429089ca2a2c5/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb65db5d7531bccf3a4f6bec3462223bea71384e2cda41da0f10b7c292b9e7c4", size = 8923223, upload-time = "2025-12-10T07:08:34.166Z" },
+ { url = "https://files.pythonhosted.org/packages/76/18/a8def8f91b18cd1ba6e05dbe02540168cb24d47e8dcf69e8d00b7da42a08/scikit_learn-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:56079a99c20d230e873ea40753102102734c5953366972a71d5cb39a32bc40c6", size = 8096518, upload-time = "2025-12-10T07:08:36.339Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/77/482076a678458307f0deb44e29891d6022617b2a64c840c725495bee343f/scikit_learn-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:3bad7565bc9cf37ce19a7c0d107742b320c1285df7aab1a6e2d28780df167242", size = 7754546, upload-time = "2025-12-10T07:08:38.128Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/d1/ef294ca754826daa043b2a104e59960abfab4cf653891037d19dd5b6f3cf/scikit_learn-1.8.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:4511be56637e46c25721e83d1a9cea9614e7badc7040c4d573d75fbe257d6fd7", size = 8848305, upload-time = "2025-12-10T07:08:41.013Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/e2/b1f8b05138ee813b8e1a4149f2f0d289547e60851fd1bb268886915adbda/scikit_learn-1.8.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:a69525355a641bf8ef136a7fa447672fb54fe8d60cab5538d9eb7c6438543fb9", size = 8432257, upload-time = "2025-12-10T07:08:42.873Z" },
+ { url = "https://files.pythonhosted.org/packages/26/11/c32b2138a85dcb0c99f6afd13a70a951bfdff8a6ab42d8160522542fb647/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2656924ec73e5939c76ac4c8b026fc203b83d8900362eb2599d8aee80e4880f", size = 8678673, upload-time = "2025-12-10T07:08:45.362Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/57/51f2384575bdec454f4fe4e7a919d696c9ebce914590abf3e52d47607ab8/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15fc3b5d19cc2be65404786857f2e13c70c83dd4782676dd6814e3b89dc8f5b9", size = 8922467, upload-time = "2025-12-10T07:08:47.408Z" },
+ { url = "https://files.pythonhosted.org/packages/35/4d/748c9e2872637a57981a04adc038dacaa16ba8ca887b23e34953f0b3f742/scikit_learn-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:00d6f1d66fbcf4eba6e356e1420d33cc06c70a45bb1363cd6f6a8e4ebbbdece2", size = 8774395, upload-time = "2025-12-10T07:08:49.337Z" },
+ { url = "https://files.pythonhosted.org/packages/60/22/d7b2ebe4704a5e50790ba089d5c2ae308ab6bb852719e6c3bd4f04c3a363/scikit_learn-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f28dd15c6bb0b66ba09728cf09fd8736c304be29409bd8445a080c1280619e8c", size = 8002647, upload-time = "2025-12-10T07:08:51.601Z" },
]
[[package]]
@@ -2701,6 +3153,26 @@ wheels = [
{ 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/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" },
{ url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" },
+ { 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/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" },
+ { 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" },
+ { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" },
+ { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" },
]
[[package]]
@@ -2762,10 +3234,11 @@ wheels = [
[[package]]
name = "spacy"
-version = "3.8.13"
+version = "3.8.16"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "catalogue" },
+ { name = "click" },
{ name = "confection" },
{ name = "cymem" },
{ name = "jinja2" },
@@ -2785,32 +3258,40 @@ dependencies = [
{ name = "wasabi" },
{ name = "weasel" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/63/d7/1924f32272f50d2f13275f16d6f869fcec47b2b676b64f91fa206bd30ef4/spacy-3.8.13.tar.gz", hash = "sha256:eb7c03c2bb16593c34d4c91974118f0931c6e6969dbfe895b1a026c6714176cf", size = 1328016, upload-time = "2026-03-23T17:44:32.042Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/4c/a8/21f5b9c3998c7a9cdd28715248e489ebd9300b1d349d5220166b951c3df4/spacy-3.8.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3fb3ec2a78a72bb87ca1cc22ad9fea2d37a3d88ec68ee6b4a02c967f35c8b0cb", size = 6617511, upload-time = "2026-03-23T17:42:17.689Z" },
- { url = "https://files.pythonhosted.org/packages/0c/24/0da6c637b3d25a37db605f836e53226cae65f035e865d2909ddf66b89185/spacy-3.8.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1c7d719ff171aad00a9aac31e7c7f6e1871be08b0be50c362885c632826d41ce", size = 6441498, upload-time = "2026-03-23T17:42:20.516Z" },
- { url = "https://files.pythonhosted.org/packages/17/64/1eeb854dc194dde91582eedf523d38c8caa209fce45c79b175e3e7a00b9f/spacy-3.8.13-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:95c2bf7d0afbf699df89cca5431527dff4950a823fd093680618057aa2affff8", size = 32050555, upload-time = "2026-03-23T17:42:25.13Z" },
- { url = "https://files.pythonhosted.org/packages/05/5b/7de5aae98f0a4547b9e44fd41e3f5820c199d67c78148782a08df83d19a8/spacy-3.8.13-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b118c9ca4250cba8a39c7fc73641820086d476dafb54b5d1f306a3f60098d3cd", size = 32296475, upload-time = "2026-03-23T17:42:30.151Z" },
- { url = "https://files.pythonhosted.org/packages/96/33/624d0025097b6b26a2c6d1b3dd7e24dec5da495e8c2a0f379bcc3ec26d15/spacy-3.8.13-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:53a6ff574a4505339375e31ba3280ff7235e82e120377614210750fa66cb63df", size = 32288426, upload-time = "2026-03-23T17:42:35.418Z" },
- { url = "https://files.pythonhosted.org/packages/c2/be/93eec7f8266a6d96fb5229d4588d64bb6dfa037604b5121a703cad35debb/spacy-3.8.13-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cf07faad25a301efbd90da1343c1f62e35185d064c5e087bd0d11a5a6b3358fe", size = 33113494, upload-time = "2026-03-23T17:42:41.053Z" },
- { url = "https://files.pythonhosted.org/packages/26/3d/0bd7d780642635e662b32fcd530ed5bed1844dbf977bc5eed43efefa034c/spacy-3.8.13-cp311-cp311-win_amd64.whl", hash = "sha256:740cafd3cb2331c48057f6689ed6be5652d35db92aa2980f32f3d78a9aba6300", size = 15359637, upload-time = "2026-03-23T17:42:45.168Z" },
- { url = "https://files.pythonhosted.org/packages/be/78/11896d5b5987cd37ae498ef1ce795e0dd551f984fffa9cf6a7887655617e/spacy-3.8.13-cp311-cp311-win_arm64.whl", hash = "sha256:189bdd05cfd66fd3d1f79449d38ba4e2d0270743f762db1304617ccad9dd321c", size = 14717010, upload-time = "2026-03-23T17:42:48.915Z" },
- { url = "https://files.pythonhosted.org/packages/d8/e1/e8f6dc7fc00582b8dcbf40fac5bce6c0ff366cf6db212decdacfaf13e584/spacy-3.8.13-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dd8ab0deefe9d2c7dccce1be1e65660a32cfc3cc114d0aa222ff52ae11be58ba", size = 6218311, upload-time = "2026-03-23T17:42:51.938Z" },
- { url = "https://files.pythonhosted.org/packages/31/fe/517de9e25a6ec469738c9e886c15cd96649b338c6ae475b9b3e4c4f5e03d/spacy-3.8.13-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:eab059bc668336d0034f3f69aed6b661b626c174e97cc1b52296d0d923c24405", size = 6033843, upload-time = "2026-03-23T17:42:54.133Z" },
- { url = "https://files.pythonhosted.org/packages/2c/a8/9a33c69f772f5c32a57c9ef7e692293b558e7bad202264d5586ab087fd29/spacy-3.8.13-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:96a8e141fd7ff749fa6693c9c72187f802d334bcadf48b6b9d6ca9bff315ca73", size = 32725183, upload-time = "2026-03-23T17:42:58.847Z" },
- { url = "https://files.pythonhosted.org/packages/ba/cf/dcc0ea61270cb23a46d3efc437fe760e7a9c3cabcfaa770591329d1430f2/spacy-3.8.13-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:32c16f5be8c8006197554da3647d83311fb89bb31bb417d38f0d1da585877b0d", size = 33205816, upload-time = "2026-03-23T17:43:03.717Z" },
- { url = "https://files.pythonhosted.org/packages/90/6b/ce94ba2a166add3376aa2c976cb1a34d6387e9dc61103cfbe9192675d0b6/spacy-3.8.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7402154eae9d2c34d097f3f7f2bf3ed4ce6ef9d44e3035af15ccc7f357d122cb", size = 32090348, upload-time = "2026-03-23T17:43:08.809Z" },
- { url = "https://files.pythonhosted.org/packages/4d/20/df5076a162bda36b04cdfa9cd544ac2be4b47aed957b3922e4c8f75dc25a/spacy-3.8.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:879f467578569db73b5811aa0f0b9a3b3fb81a125c2f7a433dd5626bcaca53f6", size = 32991911, upload-time = "2026-03-23T17:43:14.543Z" },
- { url = "https://files.pythonhosted.org/packages/41/17/316dfdf5f8d1dd33a205e4f5e7190b67db73802180f7d08c8b306eb44375/spacy-3.8.13-cp312-cp312-win_amd64.whl", hash = "sha256:a0c849b5c9cee5a930a5e8a63d0b07e095d4e3d371af6a70c496efa1d0851257", size = 14226861, upload-time = "2026-03-23T17:43:18.122Z" },
- { url = "https://files.pythonhosted.org/packages/be/a8/a794d5bbf7bc2df6770df2b14cd6811420d0e10fe81830920bf0e891c19a/spacy-3.8.13-cp312-cp312-win_arm64.whl", hash = "sha256:5d7f660f64c7d09778600b27b881a367075fe792cb5cc6932737aeda71a9aa09", size = 13628855, upload-time = "2026-03-23T17:43:22.162Z" },
- { url = "https://files.pythonhosted.org/packages/a7/2d/dc15440fa58c12bab25cbd368bbf3b754eb11d1d3cc37f3aee47caa63695/spacy-3.8.13-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6db8903cf3fd8b40db9dab669c1e823a8884100f223dc8ec63c3744ff505d5fd", size = 6202080, upload-time = "2026-03-23T17:43:25.044Z" },
- { url = "https://files.pythonhosted.org/packages/1b/27/6d723fa0c3c9872d7b8c3fb1c76edef65c1db2de441ee87d119128cb82f7/spacy-3.8.13-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c26258bfed2f3bf5563c5994ee752ea8662bbac70924d28fbea4d1c10df48fd0", size = 6015435, upload-time = "2026-03-23T17:43:28.281Z" },
- { url = "https://files.pythonhosted.org/packages/c0/36/d13f36204290e7753ce849106fb732a1a1ff6c1af0a200f2c2f493a56e60/spacy-3.8.13-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a5f02eab8e6e8e9e790b1e8da05b70b3b1d4b1fb10dc4f44203da96939fdb977", size = 32510675, upload-time = "2026-03-23T17:43:32.657Z" },
- { url = "https://files.pythonhosted.org/packages/19/37/f2a0c986bc2d59b18547d5ffaabf57fd9d88e129ad7df5ebc9ccdc91e643/spacy-3.8.13-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:17b14f28d83fe85390f420f89760ae36d515f3e0a236f8266c46335549806e3a", size = 32841103, upload-time = "2026-03-23T17:43:38.139Z" },
- { url = "https://files.pythonhosted.org/packages/37/31/b4a86cc013e4ac3c3c290ecade748c4623709a78db8a368ee0e152931c0b/spacy-3.8.13-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:10fffb387e6afeba582e01efd9b5e0c5431ac323af134f4a71a749255182f4c8", size = 31763223, upload-time = "2026-03-23T17:43:42.823Z" },
- { url = "https://files.pythonhosted.org/packages/4f/fa/b3a406bdc2636aaa315b8539b88f262e78ca391afc4083e3e9806953b24a/spacy-3.8.13-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:500f6eb9e4711e75fc07d517962dbfb553ba144ccf1b1e8ffc71b014c9ad91b2", size = 32717863, upload-time = "2026-03-23T17:43:47.939Z" },
- { url = "https://files.pythonhosted.org/packages/d9/97/11ed2a980e7225b0a1a4041cb1cba44f40bd83682a08e450dd6171f054e4/spacy-3.8.13-cp313-cp313-win_amd64.whl", hash = "sha256:1e679a8c5dd86c564a1185d894b1b8e50b52fa81bee518120fc2f86349d1879c", size = 14220413, upload-time = "2026-03-23T17:43:51.922Z" },
- { url = "https://files.pythonhosted.org/packages/ca/e2/06633e779486f62b3ec7ddeeb4accc10fb9a356b59f5bed3771dfb89931b/spacy-3.8.13-cp313-cp313-win_arm64.whl", hash = "sha256:c086bff909d73be892b4eb6883070dd3e328592b8933f0059a6569c8c7904928", size = 13619060, upload-time = "2026-03-23T17:43:55.449Z" },
+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/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]]
@@ -2864,6 +3345,22 @@ wheels = [
{ 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]]
@@ -2938,6 +3435,14 @@ wheels = [
{ 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]]
@@ -3004,19 +3509,19 @@ wheels = [
[[package]]
name = "tornado"
-version = "6.5.7"
+version = "6.5.8"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/64/24/95ec527ad67b76d59299e5465b3935d05e4294b7e0290a3924b7487df30b/tornado-6.5.7.tar.gz", hash = "sha256:66c513a76cda70d53907bc27cf1447557699c2e95aa48ba27a442ff61c3ddfc2", size = 519252, upload-time = "2026-06-08T17:34:51.232Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/10/d3/343e5bb989d6515b1646cf3d40135d73f3d5e45339bded401b56cdac24dd/tornado-6.5.8.tar.gz", hash = "sha256:9452e1b208a8bd771e2cb1f2ff564985b9b214bdebbe622793e1799e0a6bd23f", size = 520493, upload-time = "2026-08-07T02:12:42.971Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/02/dc/c7043cab6fed8ae159fc1923ce829ada35c4dbd797d408a43858ffaf9639/tornado-6.5.7-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:148b2eb15c2c765a50796172c1e499649b35f30d2e3c3d3e15913cfa56bfb163", size = 448543, upload-time = "2026-06-08T17:34:38.052Z" },
- { url = "https://files.pythonhosted.org/packages/92/4f/090b1431e5a43df696feceffc268c5383cc079ecb5f08ce58f917109aafe/tornado-6.5.7-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9da38de27f1da3b78a966f0dae12b5a1ea9afe72ca805d84ff06508272ddf100", size = 446707, upload-time = "2026-06-08T17:34:39.594Z" },
- { url = "https://files.pythonhosted.org/packages/37/d8/ef374952fd5da67d4463122c2b8e5a96536ec10b4b339254c6dcde81d01c/tornado-6.5.7-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8d759e71906ee783f8867b93bf26a265743da4c1e2f4a018464c1ba019862972", size = 449774, upload-time = "2026-06-08T17:34:41.204Z" },
- { url = "https://files.pythonhosted.org/packages/35/37/d434c73f4c6e014b745b9b37085f34f40c022f007efff3d7fe65991899f3/tornado-6.5.7-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a46347a18f23fb92b396beebe0fb78f61dda0cc302445202c16203d8a18848b", size = 450745, upload-time = "2026-06-08T17:34:42.531Z" },
- { url = "https://files.pythonhosted.org/packages/b6/2b/56b9aff361d7f1ab728a805ec7d7ea835f8807afa9f5cc690ea0e630efb9/tornado-6.5.7-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7778b30bef919231265e91c69963ce0f49a1e9c07ac900bbe75b19ce2575ba92", size = 450578, upload-time = "2026-06-08T17:34:43.787Z" },
- { url = "https://files.pythonhosted.org/packages/02/30/a7444fb23aa76860a14198fab96ac79f1866b0a6e19e26c4381b0938e50f/tornado-6.5.7-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e726f0c75da7726eec023aa62751ff8878bd2737e34fbdd33b1ae5897d2200f5", size = 449985, upload-time = "2026-06-08T17:34:45.326Z" },
- { url = "https://files.pythonhosted.org/packages/5c/42/5f0e56c01e8d9d36f4e23f367b85ae6cae0c1ecddd5e6977d8388ad27488/tornado-6.5.7-cp39-abi3-win32.whl", hash = "sha256:f8de3bf12d3efdd0cbe7c8887868198f8a91415e3f29fcf258d9b8eb7b1d9ae4", size = 451047, upload-time = "2026-06-08T17:34:46.784Z" },
- { url = "https://files.pythonhosted.org/packages/c9/a4/b393076ffb21b469eec5b328a0534cf03a3b90bfc6b1f09507cdd075d938/tornado-6.5.7-cp39-abi3-win_amd64.whl", hash = "sha256:de942f843533a039ef9fa3d9c88c7cd8a7c94553fb5ad0154270989b3d99a2c4", size = 451485, upload-time = "2026-06-08T17:34:48.248Z" },
- { url = "https://files.pythonhosted.org/packages/71/2e/7b1c769803121b809112cf9a00681c472eae1d80e32d7ec0e0bd61d0d0e1/tornado-6.5.7-cp39-abi3-win_arm64.whl", hash = "sha256:ff934fce95643af5f11efdae618eaa73d469dc588641e5c8d19295a0c65c4796", size = 450506, upload-time = "2026-06-08T17:34:49.702Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/d5/007086fd8df5489338e204f65adce33fd4f21a4999dbb2b9cff2f897b5f4/tornado-6.5.8-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:cc6aa787d7cfab7c3d35189dc7a56fbd2399a569624c730c6b55b3d6531d0403", size = 449487, upload-time = "2026-08-07T02:12:28.682Z" },
+ { url = "https://files.pythonhosted.org/packages/70/c8/5a24a99495903f594f6a199dd7beead1cbc0a13e2cb9102727bcaaf2a997/tornado-6.5.8-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9715b5eb79735b2bcd454ce216a9275b7c0470e64ea1bf5742f78b2f72b26eeb", size = 447649, upload-time = "2026-08-07T02:12:30.306Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/de/f2e733f386b85962d1b1dc82cd63d169b5b4580062b35397eac9244a41fe/tornado-6.5.8-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:547d63f450d570c14fe0e8db2cfb14c9bbd1c2503b4a6612586267955aa47b58", size = 450707, upload-time = "2026-08-07T02:12:31.95Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/94/20efeee9a01c141e9ac47c397f81679dfda24b32768fc4fff24e76d36c2c/tornado-6.5.8-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e2360a0ffbe145eca8af0b19cb7203d79b1a98dd4cccdd6b368f6f49c2e3808", size = 451677, upload-time = "2026-08-07T02:12:33.512Z" },
+ { url = "https://files.pythonhosted.org/packages/42/ec/a96ccb8ccf0de2b7bc2c5fa1608a4803735018242e90c4882365a9fd418f/tornado-6.5.8-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5d242290bdf7ab3151bc1065fdd75c0dcc21cbc7b49f22a4c56329c2d6566d22", size = 451510, upload-time = "2026-08-07T02:12:35.346Z" },
+ { url = "https://files.pythonhosted.org/packages/29/b5/93185859245ad3f00e62175f29607346788b696369347f0146e0421286bb/tornado-6.5.8-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7b94ff0e128fe0542f3bd331fb44d06260fc4ac16881545159f34ef08aad4195", size = 450917, upload-time = "2026-08-07T02:12:36.963Z" },
+ { url = "https://files.pythonhosted.org/packages/97/cf/fe33cf062834487d34d1559746a4a12521033c22645b6d74d4bca702e018/tornado-6.5.8-cp39-abi3-win32.whl", hash = "sha256:67832909c4779c64942380cb5f044a5c6163d00831472d80e25e115de9917836", size = 451952, upload-time = "2026-08-07T02:12:38.512Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/e1/468ad54333e92ccb62627e62cb88e5fc14a2171daa67ed47b1b8542d5b86/tornado-6.5.8-cp39-abi3-win_amd64.whl", hash = "sha256:11881db6b7c168494be2c2d12e65931451bdf7ee718535418ae1d8855dd5a0ee", size = 452391, upload-time = "2026-08-07T02:12:39.971Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/3e/cd5e4f06e34cde33b8ef66cf36aa2b5ad46354cc1af7d2136bbe365fee1d/tornado-6.5.8-cp39-abi3-win_arm64.whl", hash = "sha256:68a7468c7e289f8514d7d664101753903217eff1bb6822c6b5994a0b5f5bcb26", size = 451411, upload-time = "2026-08-07T02:12:41.469Z" },
]
[[package]]
@@ -3265,6 +3770,28 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6e/6b/2fabe8ebf148f4ee3c782aae86a795cc68ffe7d432ef550f234025ce0cfa/wrapt-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:f01277d9a5fc1862f26f7626da9cf443bebc0abd2f303f41c5e995b15887dabd", size = 59894, upload-time = "2026-03-06T02:54:15.391Z" },
{ url = "https://files.pythonhosted.org/packages/ca/fb/9ba66fc2dedc936de5f8073c0217b5d4484e966d87723415cc8262c5d9c2/wrapt-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:84ce8f1c2104d2f6daa912b1b5b039f331febfeee74f8042ad4e04992bd95c8f", size = 63197, upload-time = "2026-03-06T02:54:41.943Z" },
{ url = "https://files.pythonhosted.org/packages/c0/1c/012d7423c95d0e337117723eb8ecf73c622ce15a97847e84cf3f8f26cd7e/wrapt-2.1.2-cp313-cp313t-win_arm64.whl", hash = "sha256:a93cd767e37faeddbe07d8fc4212d5cba660af59bdb0f6372c93faaa13e6e679", size = 60363, upload-time = "2026-03-06T02:54:48.093Z" },
+ { url = "https://files.pythonhosted.org/packages/39/25/e7ea0b417db02bb796182a5316398a75792cd9a22528783d868755e1f669/wrapt-2.1.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1370e516598854e5b4366e09ce81e08bfe94d42b0fd569b88ec46cc56d9164a9", size = 61418, upload-time = "2026-03-06T02:53:55.706Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/0f/fa539e2f6a770249907757eaeb9a5ff4deb41c026f8466c1c6d799088a9b/wrapt-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6de1a3851c27e0bd6a04ca993ea6f80fc53e6c742ee1601f486c08e9f9b900a9", size = 61914, upload-time = "2026-03-06T02:52:53.37Z" },
+ { url = "https://files.pythonhosted.org/packages/53/37/02af1867f5b1441aaeda9c82deed061b7cd1372572ddcd717f6df90b5e93/wrapt-2.1.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:de9f1a2bbc5ac7f6012ec24525bdd444765a2ff64b5985ac6e0692144838542e", size = 120417, upload-time = "2026-03-06T02:54:30.74Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/b7/0138a6238c8ba7476c77cf786a807f871672b37f37a422970342308276e7/wrapt-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:970d57ed83fa040d8b20c52fe74a6ae7e3775ae8cff5efd6a81e06b19078484c", size = 122797, upload-time = "2026-03-06T02:54:51.539Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/ad/819ae558036d6a15b7ed290d5b14e209ca795dd4da9c58e50c067d5927b0/wrapt-2.1.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3969c56e4563c375861c8df14fa55146e81ac11c8db49ea6fb7f2ba58bc1ff9a", size = 117350, upload-time = "2026-03-06T02:54:37.651Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/2d/afc18dc57a4600a6e594f77a9ae09db54f55ba455440a54886694a84c71b/wrapt-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:57d7c0c980abdc5f1d98b11a2aa3bb159790add80258c717fa49a99921456d90", size = 121223, upload-time = "2026-03-06T02:54:35.221Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/5b/5ec189b22205697bc56eb3b62aed87a1e0423e9c8285d0781c7a83170d15/wrapt-2.1.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:776867878e83130c7a04237010463372e877c1c994d449ca6aaafeab6aab2586", size = 116287, upload-time = "2026-03-06T02:54:19.654Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/2d/f84939a7c9b5e6cdd8a8d0f6a26cabf36a0f7e468b967720e8b0cd2bdf69/wrapt-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fab036efe5464ec3291411fabb80a7a39e2dd80bae9bcbeeca5087fdfa891e19", size = 119593, upload-time = "2026-03-06T02:54:16.697Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/fe/ccd22a1263159c4ac811ab9374c061bcb4a702773f6e06e38de5f81a1bdc/wrapt-2.1.2-cp314-cp314-win32.whl", hash = "sha256:e6ed62c82ddf58d001096ae84ce7f833db97ae2263bff31c9b336ba8cfe3f508", size = 58631, upload-time = "2026-03-06T02:53:06.498Z" },
+ { url = "https://files.pythonhosted.org/packages/65/0a/6bd83be7bff2e7efaac7b4ac9748da9d75a34634bbbbc8ad077d527146df/wrapt-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:467e7c76315390331c67073073d00662015bb730c566820c9ca9b54e4d67fd04", size = 60875, upload-time = "2026-03-06T02:53:50.252Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/c0/0b3056397fe02ff80e5a5d72d627c11eb885d1ca78e71b1a5c1e8c7d45de/wrapt-2.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:da1f00a557c66225d53b095a97eace0fc5349e3bfda28fa34ffae238978ee575", size = 59164, upload-time = "2026-03-06T02:53:59.128Z" },
+ { url = "https://files.pythonhosted.org/packages/71/ed/5d89c798741993b2371396eb9d4634f009ff1ad8a6c78d366fe2883ea7a6/wrapt-2.1.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:62503ffbc2d3a69891cf29beeaccdb4d5e0a126e2b6a851688d4777e01428dbb", size = 63163, upload-time = "2026-03-06T02:52:54.873Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/8c/05d277d182bf36b0a13d6bd393ed1dec3468a25b59d01fba2dd70fe4d6ae/wrapt-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7e6cd120ef837d5b6f860a6ea3745f8763805c418bb2f12eeb1fa6e25f22d22", size = 63723, upload-time = "2026-03-06T02:52:56.374Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/27/6c51ec1eff4413c57e72d6106bb8dec6f0c7cdba6503d78f0fa98767bcc9/wrapt-2.1.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3769a77df8e756d65fbc050333f423c01ae012b4f6731aaf70cf2bef61b34596", size = 152652, upload-time = "2026-03-06T02:53:23.79Z" },
+ { url = "https://files.pythonhosted.org/packages/db/4c/d7dd662d6963fc7335bfe29d512b02b71cdfa23eeca7ab3ac74a67505deb/wrapt-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a76d61a2e851996150ba0f80582dd92a870643fa481f3b3846f229de88caf044", size = 158807, upload-time = "2026-03-06T02:53:35.742Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/4d/1e5eea1a78d539d346765727422976676615814029522c76b87a95f6bcdd/wrapt-2.1.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6f97edc9842cf215312b75fe737ee7c8adda75a89979f8e11558dfff6343cc4b", size = 146061, upload-time = "2026-03-06T02:52:57.574Z" },
+ { url = "https://files.pythonhosted.org/packages/89/bc/62cabea7695cd12a288023251eeefdcb8465056ddaab6227cb78a2de005b/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4006c351de6d5007aa33a551f600404ba44228a89e833d2fadc5caa5de8edfbf", size = 155667, upload-time = "2026-03-06T02:53:39.422Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/99/6f2888cd68588f24df3a76572c69c2de28287acb9e1972bf0c83ce97dbc1/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a9372fc3639a878c8e7d87e1556fa209091b0a66e912c611e3f833e2c4202be2", size = 144392, upload-time = "2026-03-06T02:54:22.41Z" },
+ { url = "https://files.pythonhosted.org/packages/40/51/1dfc783a6c57971614c48e361a82ca3b6da9055879952587bc99fe1a7171/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3144b027ff30cbd2fca07c0a87e67011adb717eb5f5bd8496325c17e454257a3", size = 150296, upload-time = "2026-03-06T02:54:07.848Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/38/cbb8b933a0201076c1f64fc42883b0023002bdc14a4964219154e6ff3350/wrapt-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:3b8d15e52e195813efe5db8cec156eebe339aaf84222f4f4f051a6c01f237ed7", size = 60539, upload-time = "2026-03-06T02:54:00.594Z" },
+ { url = "https://files.pythonhosted.org/packages/82/dd/e5176e4b241c9f528402cebb238a36785a628179d7d8b71091154b3e4c9e/wrapt-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:08ffa54146a7559f5b8df4b289b46d963a8e74ed16ba3687f99896101a3990c5", size = 63969, upload-time = "2026-03-06T02:54:39Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/99/79f17046cf67e4a95b9987ea129632ba8bcec0bc81f3fb3d19bdb0bd60cd/wrapt-2.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:72aaa9d0d8e4ed0e2e98019cea47a21f823c9dd4b43c7b77bba6679ffcca6a00", size = 60554, upload-time = "2026-03-06T02:53:14.132Z" },
{ url = "https://files.pythonhosted.org/packages/1a/c7/8528ac2dfa2c1e6708f647df7ae144ead13f0a31146f43c7264b4942bf12/wrapt-2.1.2-py3-none-any.whl", hash = "sha256:b8fd6fa2b2c4e7621808f8c62e8317f4aae56e59721ad933bac5239d913cf0e8", size = 43993, upload-time = "2026-03-06T02:53:12.905Z" },
]