From 66c6d9dde89ffe7299b0bae32b5c250d09413b27 Mon Sep 17 00:00:00 2001 From: Omri Mendels Date: Tue, 4 Aug 2026 17:18:28 +0300 Subject: [PATCH 01/13] Create CNAME --- docs/CNAME | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/CNAME 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 From 06d2030688ece62f58047e567aecd70e4bb5f104 Mon Sep 17 00:00:00 2001 From: Omri Mendels Date: Tue, 4 Aug 2026 17:22:10 +0300 Subject: [PATCH 02/13] docs: add Zensical documentation site (#187) --- .github/workflows/release-docs.yml | 94 +++++ .gitignore | 8 + docs/assets/dps-icon.svg | 65 ++++ docs/requirements-docs.txt | 20 + docs/stylesheets/extra.css | 604 +++++++++++++++++++++++++++++ mkdocs.yml | 98 +++++ scripts/zensical_build.py | 362 +++++++++++++++++ 7 files changed, 1251 insertions(+) create mode 100644 .github/workflows/release-docs.yml create mode 100644 docs/assets/dps-icon.svg create mode 100644 docs/requirements-docs.txt create mode 100644 docs/stylesheets/extra.css create mode 100644 mkdocs.yml create mode 100644 scripts/zensical_build.py 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/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/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/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000..df67c519 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,98 @@ +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 + - Tutorials (notebooks): + - 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 + - Concepts: + - Evaluation: evaluation.md + - Token evaluation: token_evaluation.md + - Span evaluation: span_evaluation.md + - Span matching strategies: span_matching_strategies.md + - Entity hierarchy: entity_hierarchy.md + - Why canonical entity mapping: why_canonical_entity_mapping.md + - Mapping scenarios: mapping_scenarios.md + - Migration guide: migration-guide.md + +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/scripts/zensical_build.py b/scripts/zensical_build.py new file mode 100644 index 00000000..becdd3b2 --- /dev/null +++ b/scripts/zensical_build.py @@ -0,0 +1,362 @@ +#!/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("#") + # Notebook links become on-site pages; leave them for the .ipynb pass. + if path.lstrip("./") in notebook_rels: + return match.group(0) + 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:])) From 00fc2ac7930d2572982b582f72acb2372e95c9bd Mon Sep 17 00:00:00 2001 From: Omri Mendels Date: Tue, 4 Aug 2026 22:36:32 +0300 Subject: [PATCH 03/13] docs: fix notebook list, move data-generation doc, link to Presidio (#189) --- README.md | 7 +- docs/data_generation.md | 84 ++++++++++++++++++++ mkdocs.yml | 23 +++--- presidio_evaluator/data_generator/README.md | 85 ++------------------- scripts/zensical_build.py | 8 +- 5 files changed, 113 insertions(+), 94 deletions(-) create mode 100644 docs/data_generation.md diff --git a/README.md b/README.md index cb60ac6b..30c20aea 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). @@ -67,13 +68,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). 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/mkdocs.yml b/mkdocs.yml index df67c519..2a13f6fe 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -13,22 +13,25 @@ edit_uri: "" # the build script stages and converts them before Zensical runs. nav: - Home: index.md - - Tutorials (notebooks): + - 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 - - Concepts: - - Evaluation: evaluation.md - - Token evaluation: token_evaluation.md - - Span evaluation: span_evaluation.md - - Span matching strategies: span_matching_strategies.md - - Entity hierarchy: entity_hierarchy.md - - Why canonical entity mapping: why_canonical_entity_mapping.md - - Mapping scenarios: mapping_scenarios.md - - Migration guide: migration-guide.md + - Presidio ↗: https://presidio.dataprivacystack.org theme: name: material 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/scripts/zensical_build.py b/scripts/zensical_build.py index becdd3b2..11a188eb 100644 --- a/scripts/zensical_build.py +++ b/scripts/zensical_build.py @@ -155,9 +155,15 @@ def repl(match: re.Match) -> str: 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 path.lstrip("./") in notebook_rels: + 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("./") From 1663688c99fa5c8c6d8cf0412e1d71861de4f621 Mon Sep 17 00:00:00 2001 From: Omri Mendels Date: Wed, 5 Aug 2026 16:57:23 +0300 Subject: [PATCH 04/13] docs: fix list rendering and filename link text for Zensical (#190) --- README.md | 2 ++ docs/adr/ADR-002-entity-mapping.md | 2 ++ docs/adr/ADR-003-hierarchical-evaluation.md | 2 ++ docs/entity_hierarchy.md | 4 +++- docs/mapping_scenarios.md | 2 ++ docs/why_canonical_entity_mapping.md | 2 +- 6 files changed, 12 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 30c20aea..27d64f4b 100644 --- a/README.md +++ b/README.md @@ -37,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: @@ -90,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/adr/ADR-002-entity-mapping.md b/docs/adr/ADR-002-entity-mapping.md index 4b9d35f2..8afc37c4 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. @@ -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..2a629ce0 100644 --- a/docs/adr/ADR-003-hierarchical-evaluation.md +++ b/docs/adr/ADR-003-hierarchical-evaluation.md @@ -72,6 +72,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 +80,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) diff --git a/docs/entity_hierarchy.md b/docs/entity_hierarchy.md index 68d65ccf..16a88b09 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,6 +19,7 @@ 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` @@ -51,7 +53,7 @@ The **evaluation depth is data-driven**: `CanonicalMapper` computes a weighted m 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`. -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 diff --git a/docs/mapping_scenarios.md b/docs/mapping_scenarios.md index 6b020374..89185134 100644 --- a/docs/mapping_scenarios.md +++ b/docs/mapping_scenarios.md @@ -111,6 +111,7 @@ 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. @@ -131,6 +132,7 @@ 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: + 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 diff --git a/docs/why_canonical_entity_mapping.md b/docs/why_canonical_entity_mapping.md index f41a2779..dc657ea8 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 From 2c618907a7520963f8a6e6a74588e03acf954d34 Mon Sep 17 00:00:00 2001 From: Ron Shakutai <58519179+RonShakutai@users.noreply.github.com> Date: Tue, 18 Aug 2026 14:54:02 +0300 Subject: [PATCH 05/13] feat: branch-level aliases (LOC->LOCATION, ORG->ORGANIZATION) (#186) * feat: branch-level aliases (LOC->LOCATION, ORG->ORGANIZATION) Non-leaf hierarchy nodes can now declare raw aliases via a reserved `_aliases` key, mirroring the alias lists leaf nodes already have. The key is skipped by every tree-walk so it never becomes a canonical entity, and `add_alias()` on a branch records the alias instead of creating a spurious child leaf. Moves the coarse labels LOC/ORG onto the LOCATION/ORGANIZATION branches, so they canonicalize to the readable branch name and match a model's LOCATION/ORGANIZATION at the exact (leaf) level, not only at branch level. This is what lets coarse corpora such as TAB be scored at the exact level against models that emit the long-form labels. Because LOC/ORG stop being canonical entities, several lookups keyed on canonical names needed to learn about aliases: - `to_branch()` previously returned raw aliases unchanged, so `to_branch("LOC")` would have silently degraded from "LOCATION" to "LOC" -- projecting into the wrong branch rather than failing. It now resolves aliases first. Same for `get_depth()`, which would otherwise raise. - `add_alias()` accepts an alias as its subject, so add_alias("LOC", ...) works. - `_find_node()` no longer matches the reserved `_aliases` key, which would otherwise let `add_alias("_aliases", x)` attach x to whichever branch the tree walk happened to reach first. - `add_alias()` raises ValueError instead of silently persisting an alias that a descendant already claims (branch aliases are applied before the descent into their own subtree, so a descendant wins). The hierarchy is rolled back. Updates two docs examples that used map({'LOCATION': 'LOC'}) as a resolution target; LOC is no longer a valid target, and the mapping is no longer needed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5ceeaa56-7275-46b5-9f57-12e9bb3c20e3 * Address PR #186 review comments - add_alias: rollback no longer deletes an alias the target already owned. Appending is now tracked with an `appended` flag, so a failed call leaves the hierarchy exactly as it found it (negruber1). - add_alias: remove the duplicated, unreachable rollback+raise block (negruber1, Copilot). - definitions: PER moves from an empty leaf under PERSON > NAME to a branch alias of PERSON, matching LOC/ORG (omri374). - hierarchy: warn at construction time when a branch alias declared in the hierarchy definition is shadowed by one of its own descendants. The add_alias() guard only covered the runtime path (omri374). - get_depth: error message said "Canonical entity" even for raw aliases. - docs/entity_hierarchy.md: new "Aliases" section covering leaf vs branch aliases, add_alias(), the collision ValueError, and alias resolution in canonicalize/to_branch/get_depth (omri374). - docs: replace the stale COLLISION_AMBIGUOUS / COLLISION_TRIVIAL issue types with the actual COLLISION_CROSS_BRANCH / COLLISION_SAME_BRANCH (Copilot). ADR-003 left as-is; it records a decision at a point in time. - CHANGELOG: add PER to the breaking-change list. 728 passed, 2 skipped; ruff clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5ceeaa56-7275-46b5-9f57-12e9bb3c20e3 --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5ceeaa56-7275-46b5-9f57-12e9bb3c20e3 --- CHANGELOG.md | 20 +++ docs/entity_hierarchy.md | 70 +++++++- docs/mapping_scenarios.md | 8 +- .../entity_mapping/definitions.py | 6 +- .../entity_mapping/hierarchy.py | 125 ++++++++++++- tests/entity_mapping/test_entity_hierarchy.py | 167 +++++++++++++++++- 6 files changed, 373 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a0f2471..9483fcc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,26 @@ ## 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. diff --git a/docs/entity_hierarchy.md b/docs/entity_hierarchy.md index 16a88b09..9525b9d5 100644 --- a/docs/entity_hierarchy.md +++ b/docs/entity_hierarchy.md @@ -26,12 +26,71 @@ Examples: - `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`. +depth computed by majority vote from the **dataset annotation labels**. A label that sits on the same +branch as its co-occurring annotations but at a different depth (e.g. prediction `PERSON` vs annotation +`NAME`) is reported as `COLLISION_SAME_BRANCH` (INFO) and handled automatically by hierarchical +evaluation. A label on a *different* branch is reported as `COLLISION_CROSS_BRANCH` (WARNING) and +must be resolved with `map()`. 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: @@ -114,10 +173,9 @@ 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_SAME_BRANCH` | INFO | Label and annotation share a branch at different depths — handled by hierarchical evaluation | | `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. @@ -208,5 +266,5 @@ would have changed the canonical depth if analyzed alone. It's projected onto th **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 +as `COLLISION_CROSS_BRANCH` or `PREDICTION_ONLY` issues — resolve them with `map()` before extracting results. diff --git a/docs/mapping_scenarios.md b/docs/mapping_scenarios.md index 89185134..25cd80d1 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. 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 label and its co-occurring annotations sit on the same branch at different depths. Hierarchical evaluation handles this automatically by scoring at the branch level; no `map()` call is required. Use `map({'LOCATION': 'GEO'})` only if you want to force a specific depth-3 target. -**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 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` — also reported as `COLLISION_SAME_BRANCH` (INFO, non-blocking). --- @@ -113,7 +113,7 @@ The same string alias appears under multiple canonical entities in the hierarchy **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_SAME_BRANCH` (INFO) — non-blocking. Raised when a label co-occurs with annotations on the same branch at a different depth (e.g. prediction `PERSON` vs annotation `NAME`). Handled automatically by hierarchical evaluation; use `map({'LABEL': 'CANONICAL'})` only to force a specific target. --- @@ -168,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 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 are reported as `COLLISION_SAME_BRANCH` (INFO) and scored at the branch level by hierarchical evaluation. Use `map({'LOCATION': 'GEO'})` only to force a specific depth-3 target. --- diff --git a/presidio_evaluator/entity_mapping/definitions.py b/presidio_evaluator/entity_mapping/definitions.py index 3fa6a69a..9d47c6d6 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", @@ -37,7 +38,6 @@ "NAME_MEDICAL_PROFESSIONAL", ], "MAIDEN_NAME": [], - "PER": [], }, "PREFIX": [], "SUFFIX": [], @@ -87,6 +87,7 @@ "SOCIAL_HANDLE": ["QQ"], # QQ: Chinese messaging platform ID }, "LOCATION": { + "_aliases": ["LOC"], "ADDRESS": { "STREET_ADDRESS": [ "STREET", @@ -129,10 +130,10 @@ ], "LOCATION_OTHER": ["LOCATION-OTHER", "ORDINALDIRECTION"], "GPE": ["GLOBAL_POLITICAL_ENTITY"], - "LOC": [], "GEO": [], }, "ORGANIZATION": { + "_aliases": ["ORG"], "COMPANY": [ "COMPANYNAME", "COMPANY_ID", @@ -148,7 +149,6 @@ "HOSPITAL_NAME", ], "OTHER_ORG": [], - "ORG": [], }, "EMPLOYMENT": { "JOB_TITLE": [ diff --git a/presidio_evaluator/entity_mapping/hierarchy.py b/presidio_evaluator/entity_mapping/hierarchy.py index c72f8355..74d9feae 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,78 @@ 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: + # 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"Canonical entity {entity!r} has no branch in hierarchy" + 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 +191,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 +208,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 +220,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 +237,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 +265,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 +298,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 +415,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 +435,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/tests/entity_mapping/test_entity_hierarchy.py b/tests/entity_mapping/test_entity_hierarchy.py index c30bd6cc..92d7a879 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,153 @@ 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] + From 78c45e587cedb5683cb299c9ae7c1c2d4840cdd9 Mon Sep 17 00:00:00 2001 From: Ron Shakutai <58519179+RonShakutai@users.noreply.github.com> Date: Mon, 31 Aug 2026 15:18:23 +0300 Subject: [PATCH 06/13] fix: i2b2 PATIENT is a person name, not a medical record number (#195) "PATIENT" was an alias of the PATIENT_ID leaf, i.e. the same leaf as MEDICALRECORD. In the i2b2/n2c2 2014 de-identification schema, PATIENT and DOCTOR are both subtypes of the NAME category and MEDICALRECORD is the ID subtype, so the PATIENT tag marks a person's name ("Yosef Villegas"), not an identifier. The hierarchy already treated the neighbouring labels correctly - DOCTOR, DOCTOR_NAME, PATIENT_NAME and HCW all resolve to NAME - which made bare PATIENT the odd one out: PATIENT -> PATIENT_ID (branch PHI) <- wrong PATIENT_NAME -> NAME (branch PERSON) DOCTOR -> NAME (branch PERSON) Because the two disagree at branch level as well as leaf level, a model that correctly labels a patient name as a NAME was scored wrong at every level, and a model that reproduced the mislabel was scored right. On a 20-record n2c2 sample this affected 32 of 328 gold spans (~10%). Moves "PATIENT" to FULL_NAME alongside DOCTOR and PATIENT_NAME. The record-number aliases (MEDICALRECORD, MEDICAL_RECORD) stay on PATIENT_ID. Noticed while writing the test, not fixed here: MEDICAL_RECORD_NUMBER is listed as an alias of both PATIENT_ID and MRN, and MRN currently wins. The new test asserts that label at branch level only, so it does not silently encode which leaf shadows the other. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5ceeaa56-7275-46b5-9f57-12e9bb3c20e3 --- .../entity_mapping/definitions.py | 2 +- tests/entity_mapping/test_entity_hierarchy.py | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/presidio_evaluator/entity_mapping/definitions.py b/presidio_evaluator/entity_mapping/definitions.py index 9d47c6d6..2f2b0ce1 100644 --- a/presidio_evaluator/entity_mapping/definitions.py +++ b/presidio_evaluator/entity_mapping/definitions.py @@ -32,6 +32,7 @@ "FULL_NAME": [ "FULLNAME", "DOCTOR", + "PATIENT", "PATIENT_NAME", "DOCTOR_NAME", "HCW", @@ -361,7 +362,6 @@ }, "PHI": { "PATIENT_ID": [ - "PATIENT", "MEDICALRECORD", "MEDICAL_RECORD_NUMBER", "MEDICAL_RECORD", diff --git a/tests/entity_mapping/test_entity_hierarchy.py b/tests/entity_mapping/test_entity_hierarchy.py index 92d7a879..29e953ec 100644 --- a/tests/entity_mapping/test_entity_hierarchy.py +++ b/tests/entity_mapping/test_entity_hierarchy.py @@ -656,3 +656,23 @@ def test_clean_hierarchy_emits_no_shadow_warning(self, caplog): 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" From d962ab567e97c096a3a569272cb925a2c80f379e Mon Sep 17 00:00:00 2001 From: Joseph <265838760+jbisaccia-9@users.noreply.github.com> Date: Tue, 1 Sep 2026 06:31:14 -0700 Subject: [PATCH 07/13] Fix HospitalProvider WikiData 403 by sending a User-Agent header (#194) WikiData's API now rejects unauthenticated requests with a 403 and a message asking clients to set a User-Agent per its robot policy. This made HospitalProvider silently fall back to its default hospital list whenever load_wiki_hospitals() ran. Also corrects the log message on the fallback path: it previously claimed to be "returning an empty list" when it actually returns self.default_list. Fixes #173 --- .../faker_extensions/providers.py | 18 ++++++++++-- tests/data_generator/test_providers.py | 28 +++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/presidio_evaluator/data_generator/faker_extensions/providers.py b/presidio_evaluator/data_generator/faker_extensions/providers.py index bc6ecf48..b774d591 100644 --- a/presidio_evaluator/data_generator/faker_extensions/providers.py +++ b/presidio_evaluator/data_generator/faker_extensions/providers.py @@ -326,10 +326,24 @@ 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", []) diff --git a/tests/data_generator/test_providers.py b/tests/data_generator/test_providers.py index cc389fe2..cb92d57f 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,29 @@ 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) + + _, 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 From 62ac9e40c469d619116c47b8144bbef3814b8b44 Mon Sep 17 00:00:00 2001 From: Omri Mendels Date: Tue, 1 Sep 2026 16:36:29 +0300 Subject: [PATCH 08/13] fix: harden HospitalProvider fallback paths from code review (#197) --- .../data_generator/faker_extensions/providers.py | 6 +++--- tests/data_generator/test_providers.py | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/presidio_evaluator/data_generator/faker_extensions/providers.py b/presidio_evaluator/data_generator/faker_extensions/providers.py index b774d591..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'", @@ -346,10 +346,10 @@ def load_wiki_hospitals( ) 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/tests/data_generator/test_providers.py b/tests/data_generator/test_providers.py index cb92d57f..bed5eba1 100644 --- a/tests/data_generator/test_providers.py +++ b/tests/data_generator/test_providers.py @@ -40,6 +40,7 @@ def test_hospital_provider_sends_user_agent_header(mock_get): faker = Faker() faker.add_provider(HospitalProvider) + mock_get.assert_called_once() _, call_kwargs = mock_get.call_args assert "User-Agent" in call_kwargs["headers"] From b40c996f106b11dc0389601c5aa523ed7300e842 Mon Sep 17 00:00:00 2001 From: Ron Shakutai <58519179+RonShakutai@users.noreply.github.com> Date: Mon, 7 Sep 2026 08:30:23 +0300 Subject: [PATCH 09/13] Credit more-specific hierarchy predictions (#196) --- AGENTS.md | 4 +- CHANGELOG.md | 8 + docs/adr/ADR-002-entity-mapping.md | 2 +- docs/adr/ADR-003-hierarchical-evaluation.md | 33 ++- docs/entity_hierarchy.md | 70 +++--- docs/mapping_scenarios.md | 12 +- docs/why_canonical_entity_mapping.md | 20 +- .../5_Evaluate_Custom_Presidio_Analyzer.ipynb | 12 +- notebooks/6_Interactive_Entity_Mapping.ipynb | 22 +- .../entity_mapping/data_objects.py | 9 +- .../entity_mapping/hierarchy.py | 4 +- presidio_evaluator/entity_mapping/mapper.py | 150 ++++++++++-- .../entity_mapping/rendering.py | 10 + .../evaluation/base_evaluator.py | 36 ++- .../evaluation/span_evaluator.py | 6 + .../evaluation/token_evaluator.py | 20 +- tests/entity_mapping/test_canonical_mapper.py | 224 +++++++++++++++--- tests/entity_mapping/test_entity_hierarchy.py | 1 - .../test_hierarchical_evaluation.py | 178 ++++++++++---- tests/integration/test_notebook.py | 5 +- 20 files changed, 628 insertions(+), 198 deletions(-) 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 9483fcc9..0a75a854 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## Unreleased +### 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. + +### 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 diff --git a/docs/adr/ADR-002-entity-mapping.md b/docs/adr/ADR-002-entity-mapping.md index 8afc37c4..eef1c790 100644 --- a/docs/adr/ADR-002-entity-mapping.md +++ b/docs/adr/ADR-002-entity-mapping.md @@ -61,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). diff --git a/docs/adr/ADR-003-hierarchical-evaluation.md b/docs/adr/ADR-003-hierarchical-evaluation.md index 2a629ce0..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 @@ -153,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/entity_hierarchy.md b/docs/entity_hierarchy.md index 9525b9d5..e1afb820 100644 --- a/docs/entity_hierarchy.md +++ b/docs/entity_hierarchy.md @@ -25,12 +25,13 @@ Examples: - `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**. A label that sits on the same -branch as its co-occurring annotations but at a different depth (e.g. prediction `PERSON` vs annotation -`NAME`) is reported as `COLLISION_SAME_BRANCH` (INFO) and handled automatically by hierarchical -evaluation. A label on a *different* branch is reported as `COLLISION_CROSS_BRANCH` (WARNING) and -must be resolved with `map()`. +**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. @@ -108,9 +109,11 @@ 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](why_canonical_entity_mapping.md). @@ -130,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: @@ -154,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() ``` --- @@ -173,12 +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_CROSS_BRANCH` | WARNING | Label maps across hierarchy branches — blocks extraction | -| `PREDICTION_ONLY` | WARNING | Label only in predictions, not dataset — blocks extraction | -| `COLLISION_SAME_BRANCH` | INFO | Label and annotation share a branch at different depths — handled by hierarchical evaluation | +| `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 @@ -217,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 ``` @@ -256,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_CROSS_BRANCH` 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 25cd80d1..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_SAME_BRANCH` (INFO) — non-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 label and its co-occurring annotations sit on the same branch at different depths. Hierarchical evaluation handles this automatically by scoring at the branch level; no `map()` call is required. Use `map({'LOCATION': 'GEO'})` only if you want to force a specific depth-3 target. +**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` — also reported as `COLLISION_SAME_BRANCH` (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). --- @@ -112,8 +112,8 @@ The same string alias appears under multiple canonical entities in the hierarchy **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_SAME_BRANCH` (INFO) — non-blocking. Raised when a label co-occurs with annotations on the same branch at a different depth (e.g. prediction `PERSON` vs annotation `NAME`). Handled automatically by hierarchical evaluation; use `map({'LABEL': 'CANONICAL'})` only to force a specific target. +- `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. --- @@ -131,7 +131,7 @@ 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 @@ -168,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 are reported as `COLLISION_SAME_BRANCH` (INFO) and scored at the branch level by hierarchical evaluation. Use `map({'LOCATION': 'GEO'})` only to force a specific 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/why_canonical_entity_mapping.md b/docs/why_canonical_entity_mapping.md index dc657ea8..05639523 100644 --- a/docs/why_canonical_entity_mapping.md +++ b/docs/why_canonical_entity_mapping.md @@ -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`. - -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. +**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 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/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/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/hierarchy.py b/presidio_evaluator/entity_mapping/hierarchy.py index 74d9feae..6a4f9557 100644 --- a/presidio_evaluator/entity_mapping/hierarchy.py +++ b/presidio_evaluator/entity_mapping/hierarchy.py @@ -117,9 +117,7 @@ def get_depth(self, entity: str) -> int: 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" - ) + raise EntityNotMappedError(f"Entity {entity!r} has no branch in hierarchy") return len(branch) def add_alias(self, entity_name: str, alias: str) -> None: 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 d82834f9..3288bcd4 100644 --- a/presidio_evaluator/evaluation/span_evaluator.py +++ b/presidio_evaluator/evaluation/span_evaluator.py @@ -487,6 +487,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: """ @@ -504,6 +505,11 @@ def calculate_score_on_df( ``EvaluationResult`` contains per-type **and** global PII metrics. :param beta: F-beta parameter (default 2). :param evaluation_result: Optional existing EvaluationResult to accumulate into. + :param allow_generic_entities: 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 with the requested metrics populated. """ if level in ("entity", "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/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 29e953ec..2a908686 100644 --- a/tests/entity_mapping/test_entity_hierarchy.py +++ b/tests/entity_mapping/test_entity_hierarchy.py @@ -656,7 +656,6 @@ def test_clean_hierarchy_emits_no_shadow_warning(self, caplog): 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. diff --git a/tests/entity_mapping/test_hierarchical_evaluation.py b/tests/entity_mapping/test_hierarchical_evaluation.py index 0b1a07a4..feec5fa7 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,85 @@ 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"] + assert detailed.per_type["PERSON"].false_negatives == 1 + assert detailed.per_type["PERSON"].false_positives == 1 + assert detailed.results[("O", "PERSON")] == 1 + 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 +263,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 +320,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 +329,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 +345,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 +375,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 +492,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 From 6a53d948e71fbc61ff15ae902d8eef305aa0c78f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Sep 2026 17:25:45 +0300 Subject: [PATCH 10/13] Bump tornado from 6.5.7 to 6.5.8 in the uv group across 1 directory (#200) --- uv.lock | 74 ++++++++++++++++++++++++++++----------------------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/uv.lock b/uv.lock index dea969a4..91f85c76 100644 --- a/uv.lock +++ b/uv.lock @@ -3,10 +3,10 @@ revision = 3 requires-python = ">=3.11, <3.14" resolution-markers = [ "python_full_version >= '3.12' 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 sys_platform == 'emscripten'", "python_full_version >= '3.12' and sys_platform != 'emscripten' 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 sys_platform != 'emscripten' and sys_platform != 'win32'", ] @@ -834,17 +834,17 @@ resolution-markers = [ "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "colorama", marker = "python_full_version < '3.12' and sys_platform == 'win32'" }, - { name = "decorator", marker = "python_full_version < '3.12'" }, - { name = "ipython-pygments-lexers", marker = "python_full_version < '3.12'" }, - { name = "jedi", marker = "python_full_version < '3.12'" }, - { name = "matplotlib-inline", marker = "python_full_version < '3.12'" }, - { name = "pexpect", marker = "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "prompt-toolkit", marker = "python_full_version < '3.12'" }, - { name = "pygments", marker = "python_full_version < '3.12'" }, - { name = "stack-data", marker = "python_full_version < '3.12'" }, - { name = "traitlets", marker = "python_full_version < '3.12'" }, - { name = "typing-extensions", marker = "python_full_version < '3.12'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "decorator" }, + { name = "ipython-pygments-lexers" }, + { name = "jedi" }, + { name = "matplotlib-inline" }, + { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit" }, + { name = "pygments" }, + { name = "stack-data" }, + { name = "traitlets" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a6/60/2111715ea11f39b1535bed6024b7dec7918b71e5e5d30855a5b503056b50/ipython-9.10.0.tar.gz", hash = "sha256:cd9e656be97618a0676d058134cd44e6dc7012c0e5cb36a9ce96a8c904adaf77", size = 4426526, upload-time = "2026-02-02T10:00:33.594Z" } wheels = [ @@ -861,16 +861,16 @@ resolution-markers = [ "python_full_version >= '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, - { name = "decorator", marker = "python_full_version >= '3.12'" }, - { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.12'" }, - { name = "jedi", marker = "python_full_version >= '3.12'" }, - { name = "matplotlib-inline", marker = "python_full_version >= '3.12'" }, - { name = "pexpect", marker = "python_full_version >= '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "prompt-toolkit", marker = "python_full_version >= '3.12'" }, - { name = "pygments", marker = "python_full_version >= '3.12'" }, - { name = "stack-data", marker = "python_full_version >= '3.12'" }, - { name = "traitlets", marker = "python_full_version >= '3.12'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "decorator" }, + { name = "ipython-pygments-lexers" }, + { name = "jedi" }, + { name = "matplotlib-inline" }, + { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit" }, + { name = "pygments" }, + { name = "stack-data" }, + { name = "traitlets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/86/28/a4698eda5a8928a45d6b693578b135b753e14fa1c2b36ee9441e69a45576/ipython-9.11.0.tar.gz", hash = "sha256:2a94bc4406b22ecc7e4cb95b98450f3ea493a76bec8896cda11b78d7752a6667", size = 4427354, upload-time = "2026-03-05T08:57:30.549Z" } wheels = [ @@ -1732,7 +1732,7 @@ name = "pexpect" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ptyprocess", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "ptyprocess" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } wheels = [ @@ -3004,19 +3004,19 @@ wheels = [ [[package]] name = "tornado" -version = "6.5.7" -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" } -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" }, +version = "6.5.8" +source = { registry = "https://pypi.org/simple" } +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/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]] From 0120cae20d533b2228164bc3010078d512925fcb Mon Sep 17 00:00:00 2001 From: Omri Mendels Date: Wed, 9 Sep 2026 19:20:24 +0300 Subject: [PATCH 11/13] Change default branch from 'master' to 'main' (#203) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c3c5e0a6..21963e69 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: From ddb69960a0cb5e81c0f68bd2b6270fafba2ed189 Mon Sep 17 00:00:00 2001 From: Omri Mendels Date: Wed, 9 Sep 2026 21:10:44 +0300 Subject: [PATCH 12/13] Update CI workflow to remove Python 3.10 (#204) Removed Python version 3.10 from CI workflow matrix. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 21963e69..bf508796 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 From 67bbae7cef0fb9feb20302369dd13f81c6c75c82 Mon Sep 17 00:00:00 2001 From: Omri Mendels Date: Wed, 9 Sep 2026 21:30:56 +0300 Subject: [PATCH 13/13] Support Python 3.14 (#205) --- CHANGELOG.md | 4 + pyproject.toml | 2 +- uv.lock | 643 ++++++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 590 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a75a854..f95d18ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### 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. 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/uv.lock b/uv.lock index 91f85c76..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.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 sys_platform == 'win32'", + "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 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" }, @@ -834,17 +988,17 @@ resolution-markers = [ "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "decorator" }, - { name = "ipython-pygments-lexers" }, - { name = "jedi" }, - { name = "matplotlib-inline" }, - { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "prompt-toolkit" }, - { name = "pygments" }, - { name = "stack-data" }, - { name = "traitlets" }, - { name = "typing-extensions" }, + { name = "colorama", marker = "python_full_version < '3.12' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version < '3.12'" }, + { name = "ipython-pygments-lexers", marker = "python_full_version < '3.12'" }, + { name = "jedi", marker = "python_full_version < '3.12'" }, + { name = "matplotlib-inline", marker = "python_full_version < '3.12'" }, + { name = "pexpect", marker = "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version < '3.12'" }, + { name = "pygments", marker = "python_full_version < '3.12'" }, + { name = "stack-data", marker = "python_full_version < '3.12'" }, + { name = "traitlets", marker = "python_full_version < '3.12'" }, + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a6/60/2111715ea11f39b1535bed6024b7dec7918b71e5e5d30855a5b503056b50/ipython-9.10.0.tar.gz", hash = "sha256:cd9e656be97618a0676d058134cd44e6dc7012c0e5cb36a9ce96a8c904adaf77", size = 4426526, upload-time = "2026-02-02T10:00:33.594Z" } wheels = [ @@ -856,21 +1010,24 @@ 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'", -] -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "decorator" }, - { name = "ipython-pygments-lexers" }, - { name = "jedi" }, - { name = "matplotlib-inline" }, - { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "prompt-toolkit" }, - { name = "pygments" }, - { name = "stack-data" }, - { name = "traitlets" }, + "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'" }, + { name = "decorator", marker = "python_full_version >= '3.12'" }, + { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.12'" }, + { name = "jedi", marker = "python_full_version >= '3.12'" }, + { name = "matplotlib-inline", marker = "python_full_version >= '3.12'" }, + { name = "pexpect", marker = "python_full_version >= '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version >= '3.12'" }, + { name = "pygments", marker = "python_full_version >= '3.12'" }, + { name = "stack-data", marker = "python_full_version >= '3.12'" }, + { name = "traitlets", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/86/28/a4698eda5a8928a45d6b693578b135b753e14fa1c2b36ee9441e69a45576/ipython-9.11.0.tar.gz", hash = "sha256:2a94bc4406b22ecc7e4cb95b98450f3ea493a76bec8896cda11b78d7752a6667", size = 4427354, upload-time = "2026-03-05T08:57:30.549Z" } wheels = [ @@ -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]] @@ -1732,7 +2008,7 @@ name = "pexpect" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ptyprocess" }, + { name = "ptyprocess", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } wheels = [ @@ -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]] @@ -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" }, ]