Compare commits
3
Commits
12fc0522b2
...
9897da7f82
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9897da7f82 | ||
|
|
b7a1ef8549 | ||
|
|
96a1e09956 |
@@ -17,6 +17,10 @@ put it in config.toml, code, or logs. Requests must go to
|
|||||||
Exception: downloading your own collection while logged in on the website
|
Exception: downloading your own collection while logged in on the website
|
||||||
needs no registration — relevant to the Playwright stages, not the API
|
needs no registration — relevant to the Playwright stages, not the API
|
||||||
client. Usage is monitored per-application at `/applications` → "Usage".
|
client. Usage is monitored per-application at `/applications` → "Usage".
|
||||||
|
Open-source note: each user of this tool registers their OWN application
|
||||||
|
and supplies their own token — never ship or share a token in the repo.
|
||||||
|
Future frontend note: public-facing apps must display the "Powered by
|
||||||
|
BGG" logo linking back to boardgamegeek.com.
|
||||||
|
|
||||||
## Endpoints (XML API2 — the only sanctioned read API)
|
## Endpoints (XML API2 — the only sanctioned read API)
|
||||||
|
|
||||||
@@ -55,11 +59,11 @@ The first `/collection` call typically returns **HTTP 202** with a "please retry
|
|||||||
## Website automation (upload stage)
|
## Website automation (upload stage)
|
||||||
|
|
||||||
- Login with `BGG_USERNAME` / `BGG_PASSWORD` env vars; persist Playwright storage state locally (gitignored) so repeat runs skip login. Never write credentials to disk, logs, or error messages.
|
- Login with `BGG_USERNAME` / `BGG_PASSWORD` env vars; persist Playwright storage state locally (gitignored) so repeat runs skip login. Never write credentials to disk, logs, or error messages.
|
||||||
- Per game: navigate to the game page → "Add to Collection" flow → status **Owned** → if a version_id is known, set it in the collection item's version picker → save. Never guess a version — omit it when unknown. The version picker is the most fragile part of the UI: walk it manually once and document the selectors before automating.
|
- Per game: navigate to the game page → "Add to Collection" flow → status **Owned** → if a version_id is known, set it in the collection item's version picker → save. Never guess a version — omit it when unknown. The flow has been walked and documented: see `docs/bgg-upload-flow.md` for the dialog structure, version-picker behavior (paginated, no search — match by canonical version NAME from the XML API), and observed automation gotchas (stale elements, hidden-not-removed dialogs, hydration races).
|
||||||
- A second copy of an owned game must be a NEW collection entry (new collid), not an edit of the existing item.
|
- A second copy of an owned game must be a NEW collection entry (new collid), not an edit of the existing item.
|
||||||
- Expect UI fragility: wrap each game in its own try/except, log the failure to `upload_log.csv`, and continue. `--retry-failed` re-attempts failures; `--dry-run` logs without touching the site.
|
- Expect UI fragility: wrap each game in its own try/except, log the failure to `upload_log.csv`, and continue. `--retry-failed` re-attempts failures; `--dry-run` logs without touching the site.
|
||||||
- Idempotency: skip IDs already logged `added`; `--verify` re-fetches the collection to confirm.
|
- Idempotency: skip IDs already logged `added`; `--verify` re-fetches the collection to confirm.
|
||||||
|
|
||||||
## Policy note
|
## Policy note
|
||||||
|
|
||||||
BGG changed API access policies in 2025 and broke older community tools. Do not use undocumented endpoints, scraped JSON blobs, or third-party mirrors — only XML API2 and the public website.
|
BGG's 2025 policy (version 2025-07-02, boardgamegeek.com/using_the_xml_api) locked the API behind registered applications and Bearer tokens, breaking older community tools. Do not use undocumented endpoints, scraped JSON blobs, or third-party mirrors — only the authenticated XML API2 and the public website. Third-party services that proxy BGG data to other applications are explicitly prohibited. The policy asks for server-side requests, aggressive caching, and minimal request counts — the cache-first client design is mandatory, not optional. Policies can change at any time; changes are announced in BGG's Geek Tools News forum.
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ storage_state.json
|
|||||||
# Local inputs & cache (CSV/JSON artifacts in data/ ARE committed)
|
# Local inputs & cache (CSV/JSON artifacts in data/ ARE committed)
|
||||||
photos/
|
photos/
|
||||||
data/bgg_cache/
|
data/bgg_cache/
|
||||||
|
data/extract_raw/
|
||||||
|
|
||||||
# Python
|
# Python
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
|||||||
@@ -30,14 +30,14 @@ Every stage is idempotent and resumable: kill it mid-run, restart, lose nothing.
|
|||||||
|
|
||||||
- macOS or Linux, Python 3.12+, [uv](https://docs.astral.sh/uv/)
|
- macOS or Linux, Python 3.12+, [uv](https://docs.astral.sh/uv/)
|
||||||
- An [Anthropic API key](https://console.anthropic.com/) (vision extraction)
|
- An [Anthropic API key](https://console.anthropic.com/) (vision extraction)
|
||||||
- A BoardGameGeek account
|
- A BoardGameGeek account **and a registered BGG application** — as of BGG's [2025 API policy](https://boardgamegeek.com/using_the_xml_api), the XML API requires a Bearer token from a registered app. Register a free non-commercial application at [boardgamegeek.com/applications](https://boardgamegeek.com/applications) (approval can take a week or more — apply early), then create a token. Each user needs their own — tokens must not be shared.
|
||||||
- Playwright Chromium: `uv run playwright install chromium`
|
- Playwright Chromium: `uv run playwright install chromium`
|
||||||
|
|
||||||
Secrets come from environment variables only — `ANTHROPIC_API_KEY`, `BGG_USERNAME`, `BGG_PASSWORD` — and are never written to disk or logs.
|
Secrets come from environment variables only — `ANTHROPIC_API_KEY`, `BGG_USERNAME`, `BGG_PASSWORD`, `BGG_API_TOKEN` — and are never written to disk or logs.
|
||||||
|
|
||||||
## A note on being a good BGG citizen
|
## A note on being a good BGG citizen
|
||||||
|
|
||||||
This tool is **not affiliated with or supported by BoardGameGeek**. It uses only the sanctioned XML API2 for reads and drives the regular website for writes, deliberately slowly (one request every couple of seconds, slower for uploads). Please keep it that way: BGG is a community resource running on community goodwill. You are responsible for your own account — review the dry-run output before a real upload.
|
This tool is **not affiliated with or supported by BoardGameGeek**. It uses only the sanctioned XML API2 for reads (with your own registered application token, per BGG's current policy) and drives the regular website for writes, deliberately slowly (one request every couple of seconds, slower for uploads). Please keep it that way: BGG is a community resource running on community goodwill. You are responsible for your own account — review the dry-run output before a real upload.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ Beyond the bare game, capture **which edition/version I own** wherever the photo
|
|||||||
## Constraints & Context
|
## Constraints & Context
|
||||||
|
|
||||||
- BGG has **no write API**. Reads go through the XML API2 (`https://boardgamegeek.com/xmlapi2/`); writes must automate the website itself with a real login session.
|
- BGG has **no write API**. Reads go through the XML API2 (`https://boardgamegeek.com/xmlapi2/`); writes must automate the website itself with a real login session.
|
||||||
|
- The XML API **requires a registered application** (boardgamegeek.com/using_the_xml_api, policy 2025-07-02): every request carries `Authorization: Bearer <token>` from the `BGG_API_TOKEN` env var, sent to `boardgamegeek.com` without a leading `www`. Register a free non-commercial application at boardgamegeek.com/applications — approval can take a week+, so development runs on recorded/stub XML fixtures until then.
|
||||||
- BGG's XML API queues collection requests: a first call may return HTTP 202 ("try again"). Retry with backoff.
|
- BGG's XML API queues collection requests: a first call may return HTTP 202 ("try again"). Retry with backoff.
|
||||||
- BGG will throttle aggressive clients. Target ≤1 request every 2 seconds to any BGG endpoint, with jittered backoff on 429/503.
|
- BGG will throttle aggressive clients. Target ≤1 request every 2 seconds to any BGG endpoint, with jittered backoff on 429/503.
|
||||||
- BGG changed API access policies in 2025; some older community tools broke. Don't depend on undocumented endpoints beyond XML API2 and the public website.
|
- BGG changed API access policies in 2025; some older community tools broke. Don't depend on undocumented endpoints beyond XML API2 and the public website.
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# BGG "Add to Collection" flow — recon notes for the upload stage
|
||||||
|
|
||||||
|
Recorded 2026-08-01 by walking the flow manually on boardgamegeek.com (Wingspan,
|
||||||
|
id 266192) while logged in. Dialog opened, inspected, and **cancelled without
|
||||||
|
saving**. These notes are the selector documentation the spec requires before
|
||||||
|
automating Stage 5.
|
||||||
|
|
||||||
|
## Entry point
|
||||||
|
|
||||||
|
- Game page has **two** "Add To" buttons (one in the game header module, one
|
||||||
|
lower on the page) — target the first, but match by accessible name, not
|
||||||
|
position. Button's accessible name is "Add To" with adjacent text
|
||||||
|
"Collection".
|
||||||
|
- Clicking opens a `role="dialog"` containing a `<form>`. Dialog heading shows
|
||||||
|
a "Loading..." span before content settles — **wait for the game-name
|
||||||
|
heading** (e.g. `getByRole('heading', {name: gameName})`) before
|
||||||
|
interacting.
|
||||||
|
|
||||||
|
## Main dialog structure
|
||||||
|
|
||||||
|
- **Status checkboxes**, each wrapped in a `<label>`: Own, Prev. Owned,
|
||||||
|
For Trade, Want to Play, Want in Trade, Want to Buy, Pre-ordered, Wishlist.
|
||||||
|
→ `dialog.getByLabel('Own')` and check it. Nothing is pre-checked.
|
||||||
|
- Rating: a 1–10 slider plus a "Rating" text input. We do not set ratings.
|
||||||
|
- Comment (public) textbox — unused by us.
|
||||||
|
- "Advanced (private info, parts exchange)" expander: Price Paid, Current
|
||||||
|
Price, Quantity, Acquisition Date, Acquired From, Inventory Date/Location,
|
||||||
|
Private Comment, Want/Has Parts. All unused (we don't track provenance).
|
||||||
|
- "Customize Item Info (title, image)" expander: Custom Title, Custom Image
|
||||||
|
Id, plus **manual version-override fields** (Publisher Id, Language select,
|
||||||
|
Year, Other, Barcode). These are for defining a custom version — do NOT use
|
||||||
|
them; always pick a cataloged version instead (or none).
|
||||||
|
- Footer: `Save` (`type="submit"`) and `Cancel` buttons.
|
||||||
|
|
||||||
|
## Version picker ("Set version/edition")
|
||||||
|
|
||||||
|
- Button labeled **"Set version/edition"** near the top of the dialog swaps
|
||||||
|
the dialog content to a "Versions" sub-view (same `role="dialog"`).
|
||||||
|
- The sub-view is a **paginated list with NO search/filter box**. Each
|
||||||
|
listitem's text is the full canonical version name + year, e.g.
|
||||||
|
"Flügelschlag (German fifth edition) (2024)" — these names match the
|
||||||
|
version names returned by `/thing?id=X&versions=1`.
|
||||||
|
- Selection strategy: resolve the target version NAME from the XML API,
|
||||||
|
then page through the list matching listitem text
|
||||||
|
(`getByRole('listitem').filter({hasText: versionName})`). Newest years
|
||||||
|
appear first.
|
||||||
|
- The sub-view has its own **Cancel** that returns to the main dialog — it
|
||||||
|
is a different button from the main dialog's Cancel. Two-level dismissal.
|
||||||
|
|
||||||
|
## Automation gotchas observed
|
||||||
|
|
||||||
|
1. **Element references go stale constantly.** The page re-renders after
|
||||||
|
load and after every dialog transition; clicks on cached handles silently
|
||||||
|
miss. Playwright's auto-waiting role/label locators handle this — never
|
||||||
|
cache element handles across a dialog state change.
|
||||||
|
2. **Dialog persists in the DOM after cancel**, just hidden
|
||||||
|
(`offsetParent === null`). "Is the dialog gone" checks must test
|
||||||
|
visibility, not existence. Same applies when verifying a save completed.
|
||||||
|
3. First click on "Add To" right after page load can no-op (hydration race).
|
||||||
|
Wait for network-idle or the button's stable state before clicking.
|
||||||
|
4. Verify saves via the collection API (`--verify`), not by UI state.
|
||||||
|
|
||||||
|
## Playwright locator sketch
|
||||||
|
|
||||||
|
```python
|
||||||
|
page.get_by_role("button", name="Add To").first.click()
|
||||||
|
dialog = page.get_by_role("dialog")
|
||||||
|
dialog.get_by_role("heading", name=game_name).wait_for()
|
||||||
|
dialog.get_by_label("Own").check()
|
||||||
|
if version_name:
|
||||||
|
dialog.get_by_role("button", name="Set version/edition").click()
|
||||||
|
# page through listitems until version_name matches, then click it
|
||||||
|
dialog.get_by_role("button", name="Save").click()
|
||||||
|
```
|
||||||
|
|
||||||
|
Unverified so far (needs a real, sacrificial save on one game before batch
|
||||||
|
runs): pagination controls in the version sub-view, exact post-save behavior
|
||||||
|
(toast? dialog close? redirect?), and how the dialog differs when the game is
|
||||||
|
ALREADY in the collection (second-copy flow must create a new entry, not edit
|
||||||
|
the existing one).
|
||||||
@@ -8,6 +8,10 @@ dependencies = [
|
|||||||
"httpx>=0.27",
|
"httpx>=0.27",
|
||||||
"rapidfuzz>=3.9",
|
"rapidfuzz>=3.9",
|
||||||
"defusedxml>=0.7.1",
|
"defusedxml>=0.7.1",
|
||||||
|
"anthropic>=0.120.2",
|
||||||
|
"pillow>=12.3.0",
|
||||||
|
"pillow-heif>=1.5.0",
|
||||||
|
"rich>=15.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|||||||
+8
-2
@@ -35,7 +35,10 @@ def extract(
|
|||||||
config: ConfigOpt = None,
|
config: ConfigOpt = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Stage 1: extract game titles + edition cues from shelf photos."""
|
"""Stage 1: extract game titles + edition cues from shelf photos."""
|
||||||
_not_implemented("extract", 3)
|
from bggpipe.extract import run_extract
|
||||||
|
|
||||||
|
cfg = load_config(config)
|
||||||
|
run_extract(cfg, only=only)
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
@@ -55,7 +58,10 @@ def resolve(
|
|||||||
@app.command()
|
@app.command()
|
||||||
def review(config: ConfigOpt = None) -> None:
|
def review(config: ConfigOpt = None) -> None:
|
||||||
"""Stage 3: human review of ambiguous/unmatched items."""
|
"""Stage 3: human review of ambiguous/unmatched items."""
|
||||||
_not_implemented("review", 4)
|
from bggpipe.review import run_review
|
||||||
|
|
||||||
|
cfg = load_config(config)
|
||||||
|
run_review(cfg)
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
|
|||||||
@@ -0,0 +1,240 @@
|
|||||||
|
"""Stage 1 — extract game titles + edition cues from shelf photos.
|
||||||
|
|
||||||
|
Each photo is sent to Claude vision once and the raw result is cached under
|
||||||
|
data/extract_raw/<photo>.json (making re-runs free and --only targeted).
|
||||||
|
titles.json is rebuilt from all raw files on every run, deduping identical
|
||||||
|
titles across photos unless their edition cues conflict — conflicting cues
|
||||||
|
mean two different editions on the shelf, which stay separate entries.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
from collections.abc import Callable
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import typer
|
||||||
|
|
||||||
|
from bggpipe.config import Config
|
||||||
|
from bggpipe.normalize import normalize_title
|
||||||
|
|
||||||
|
IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".heic"}
|
||||||
|
MAX_LONG_EDGE = 1568 # Anthropic vision sweet spot (spec)
|
||||||
|
JPEG_QUALITY = 85
|
||||||
|
_CONFIDENCE_ORDER = {"low": 0, "medium": 1, "high": 2}
|
||||||
|
_CODE_FENCE = re.compile(r"^```(?:json)?\s*|\s*```$", re.MULTILINE)
|
||||||
|
|
||||||
|
# vision(image_b64, media_type) -> model's raw text response
|
||||||
|
VisionFn = Callable[[str, str], str]
|
||||||
|
|
||||||
|
VISION_PROMPT = """\
|
||||||
|
You are cataloging a photo of board game shelves.
|
||||||
|
|
||||||
|
List every board game or card game title visible (spines and face-out boxes),
|
||||||
|
transcribed exactly as printed. Exclude books, card sleeves, storage boxes,
|
||||||
|
and anything that is not a board/card game.
|
||||||
|
|
||||||
|
If the SAME title appears more than once with visibly different boxes, report
|
||||||
|
each as a separate entry — the owner has multiple editions of some games.
|
||||||
|
|
||||||
|
Respond with ONLY a JSON array, no prose, where each element is:
|
||||||
|
{
|
||||||
|
"title_raw": "title as printed on the box",
|
||||||
|
"confidence": "high" | "medium" | "low",
|
||||||
|
"publisher_hint": "publisher name or logo if legible, else null",
|
||||||
|
"edition_hint": "edition wording if visible ('2nd Edition', 'Deluxe',
|
||||||
|
'Big Box', anniversary marks), else null",
|
||||||
|
"year_hint": copyright/print year as an integer if legible, else null,
|
||||||
|
"language_hint": "language of the box text if determinable, else null",
|
||||||
|
"art_notes": "distinctive box-art notes (colorway, artwork style) that
|
||||||
|
could identify the edition, else null"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_image(path: Path) -> tuple[str, str]:
|
||||||
|
"""Load a photo (JPEG/PNG/HEIC), downscale to <=1568px long edge, return
|
||||||
|
(base64 JPEG, media_type). HEIC converts transparently via pillow-heif."""
|
||||||
|
from PIL import Image
|
||||||
|
from pillow_heif import register_heif_opener
|
||||||
|
|
||||||
|
register_heif_opener()
|
||||||
|
with Image.open(path) as img:
|
||||||
|
img = img.convert("RGB")
|
||||||
|
long_edge = max(img.size)
|
||||||
|
if long_edge > MAX_LONG_EDGE:
|
||||||
|
scale = MAX_LONG_EDGE / long_edge
|
||||||
|
img = img.resize(
|
||||||
|
(round(img.width * scale), round(img.height * scale)),
|
||||||
|
Image.LANCZOS,
|
||||||
|
)
|
||||||
|
buffer = io.BytesIO()
|
||||||
|
img.save(buffer, format="JPEG", quality=JPEG_QUALITY)
|
||||||
|
return base64.standard_b64encode(buffer.getvalue()).decode(), "image/jpeg"
|
||||||
|
|
||||||
|
|
||||||
|
def parse_vision_json(text: str) -> list[dict]:
|
||||||
|
"""Parse the model's JSON defensively: strip code fences, find the array."""
|
||||||
|
cleaned = _CODE_FENCE.sub("", text).strip()
|
||||||
|
if not cleaned.startswith("["):
|
||||||
|
start, end = cleaned.find("["), cleaned.rfind("]")
|
||||||
|
if start == -1 or end == -1:
|
||||||
|
raise ValueError(f"no JSON array in vision response: {text[:200]!r}")
|
||||||
|
cleaned = cleaned[start : end + 1]
|
||||||
|
entries = json.loads(cleaned)
|
||||||
|
if not isinstance(entries, list):
|
||||||
|
raise ValueError("vision response is not a JSON array")
|
||||||
|
return [e for e in entries if isinstance(e, dict) and e.get("title_raw")]
|
||||||
|
|
||||||
|
|
||||||
|
def default_vision(model: str) -> VisionFn:
|
||||||
|
"""The real Anthropic-API-backed vision callable (needs ANTHROPIC_API_KEY)."""
|
||||||
|
import anthropic
|
||||||
|
|
||||||
|
client = anthropic.Anthropic()
|
||||||
|
|
||||||
|
def vision(image_b64: str, media_type: str) -> str:
|
||||||
|
response = client.messages.create(
|
||||||
|
model=model,
|
||||||
|
max_tokens=4000,
|
||||||
|
messages=[
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "image",
|
||||||
|
"source": {
|
||||||
|
"type": "base64",
|
||||||
|
"media_type": media_type,
|
||||||
|
"data": image_b64,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{"type": "text", "text": VISION_PROMPT},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
return next(b.text for b in response.content if b.type == "text")
|
||||||
|
|
||||||
|
return vision
|
||||||
|
|
||||||
|
|
||||||
|
def extract_photo(photo: Path, vision: VisionFn) -> list[dict]:
|
||||||
|
image_b64, media_type = prepare_image(photo)
|
||||||
|
raw_entries = parse_vision_json(vision(image_b64, media_type))
|
||||||
|
entries = []
|
||||||
|
for raw in raw_entries:
|
||||||
|
year = raw.get("year_hint")
|
||||||
|
entries.append(
|
||||||
|
{
|
||||||
|
"title_raw": str(raw["title_raw"]).strip(),
|
||||||
|
"confidence": raw.get("confidence") or "medium",
|
||||||
|
"publisher_hint": raw.get("publisher_hint") or "",
|
||||||
|
"edition_hint": raw.get("edition_hint") or "",
|
||||||
|
"year_hint": int(year)
|
||||||
|
if isinstance(year, int | str) and str(year).isdigit()
|
||||||
|
else None,
|
||||||
|
"language_hint": raw.get("language_hint") or "",
|
||||||
|
"art_notes": raw.get("art_notes") or "",
|
||||||
|
"source_photos": [photo.name],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return entries
|
||||||
|
|
||||||
|
|
||||||
|
def _cues_conflict(a: dict, b: dict) -> bool:
|
||||||
|
"""Two sightings conflict if any edition cue is set on both and differs —
|
||||||
|
that means visibly different boxes, i.e. separate editions (spec)."""
|
||||||
|
for key in ("publisher_hint", "edition_hint", "language_hint"):
|
||||||
|
va, vb = a.get(key) or "", b.get(key) or ""
|
||||||
|
if va and vb and normalize_title(va) != normalize_title(vb):
|
||||||
|
return True
|
||||||
|
ya, yb = a.get("year_hint"), b.get("year_hint")
|
||||||
|
return bool(ya and yb and ya != yb)
|
||||||
|
|
||||||
|
|
||||||
|
def _merge(a: dict, b: dict) -> dict:
|
||||||
|
merged = dict(a)
|
||||||
|
merged["source_photos"] = sorted(set(a["source_photos"]) | set(b["source_photos"]))
|
||||||
|
if _CONFIDENCE_ORDER.get(b["confidence"], 1) > _CONFIDENCE_ORDER.get(
|
||||||
|
a["confidence"], 1
|
||||||
|
):
|
||||||
|
merged["confidence"] = b["confidence"]
|
||||||
|
for key in (
|
||||||
|
"publisher_hint",
|
||||||
|
"edition_hint",
|
||||||
|
"year_hint",
|
||||||
|
"language_hint",
|
||||||
|
"art_notes",
|
||||||
|
):
|
||||||
|
merged[key] = merged.get(key) or b.get(key)
|
||||||
|
return merged
|
||||||
|
|
||||||
|
|
||||||
|
def dedupe_entries(entries: list[dict]) -> list[dict]:
|
||||||
|
"""Collapse same-normalized-title sightings unless their cues conflict."""
|
||||||
|
result: list[dict] = []
|
||||||
|
for entry in entries:
|
||||||
|
entry = {**entry, "title_normalized": normalize_title(entry["title_raw"])}
|
||||||
|
for existing in result:
|
||||||
|
if existing["title_normalized"] == entry[
|
||||||
|
"title_normalized"
|
||||||
|
] and not _cues_conflict(existing, entry):
|
||||||
|
existing.update(_merge(existing, entry))
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
result.append(entry)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def rebuild_titles(raw_dir: Path, titles_path: Path) -> list[dict]:
|
||||||
|
entries: list[dict] = []
|
||||||
|
for raw_file in sorted(raw_dir.glob("*.json")):
|
||||||
|
entries.extend(json.loads(raw_file.read_text()))
|
||||||
|
deduped = dedupe_entries(entries)
|
||||||
|
titles_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
titles_path.write_text(json.dumps(deduped, indent=2, ensure_ascii=False) + "\n")
|
||||||
|
return deduped
|
||||||
|
|
||||||
|
|
||||||
|
def run_extract(
|
||||||
|
cfg: Config, *, only: str | None = None, vision: VisionFn | None = None
|
||||||
|
) -> list[dict]:
|
||||||
|
photos = (
|
||||||
|
sorted(
|
||||||
|
p for p in cfg.photos_dir.iterdir() if p.suffix.lower() in IMAGE_EXTENSIONS
|
||||||
|
)
|
||||||
|
if cfg.photos_dir.is_dir()
|
||||||
|
else []
|
||||||
|
)
|
||||||
|
if only:
|
||||||
|
photos = [p for p in photos if p.name == only]
|
||||||
|
if not photos:
|
||||||
|
raise FileNotFoundError(
|
||||||
|
f"--only {only!r}: no such photo in {cfg.photos_dir}"
|
||||||
|
)
|
||||||
|
if not photos:
|
||||||
|
typer.echo(
|
||||||
|
f"No photos found in {cfg.photos_dir}/ — add JPEG/PNG/HEIC files first."
|
||||||
|
)
|
||||||
|
raise typer.Exit(code=1)
|
||||||
|
|
||||||
|
raw_dir = cfg.data_dir / "extract_raw"
|
||||||
|
raw_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
vision = vision or default_vision(cfg.model)
|
||||||
|
|
||||||
|
for photo in photos:
|
||||||
|
raw_path = raw_dir / f"{photo.name}.json"
|
||||||
|
if raw_path.exists() and not only:
|
||||||
|
typer.echo(f" {photo.name}: already extracted, skipping")
|
||||||
|
continue
|
||||||
|
entries = extract_photo(photo, vision)
|
||||||
|
raw_path.write_text(json.dumps(entries, indent=2, ensure_ascii=False) + "\n")
|
||||||
|
typer.echo(f" {photo.name}: {len(entries)} title(s)")
|
||||||
|
|
||||||
|
deduped = rebuild_titles(raw_dir, cfg.titles_path)
|
||||||
|
typer.echo(f"Wrote {len(deduped)} unique title(s) to {cfg.titles_path}.")
|
||||||
|
return deduped
|
||||||
@@ -10,6 +10,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import csv
|
import csv
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -301,6 +302,24 @@ def _row_key(title_raw: str, source_photos: str) -> tuple[str, str]:
|
|||||||
return (title_raw, source_photos)
|
return (title_raw, source_photos)
|
||||||
|
|
||||||
|
|
||||||
|
def read_matches(path: Path) -> list[dict[str, str]]:
|
||||||
|
if not path.exists():
|
||||||
|
return []
|
||||||
|
with path.open(newline="") as f:
|
||||||
|
return list(csv.DictReader(f))
|
||||||
|
|
||||||
|
|
||||||
|
def write_matches(path: Path, rows: list[dict[str, str]]) -> None:
|
||||||
|
"""Atomic full rewrite — review updates rows in place decision by decision."""
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
tmp = path.with_name(path.name + ".tmp")
|
||||||
|
with tmp.open("w", newline="") as f:
|
||||||
|
writer = csv.DictWriter(f, fieldnames=MATCH_COLUMNS, extrasaction="ignore")
|
||||||
|
writer.writeheader()
|
||||||
|
writer.writerows(rows)
|
||||||
|
os.replace(tmp, path)
|
||||||
|
|
||||||
|
|
||||||
def read_existing_keys(path: Path) -> set[tuple[str, str]]:
|
def read_existing_keys(path: Path) -> set[tuple[str, str]]:
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
return set()
|
return set()
|
||||||
|
|||||||
@@ -0,0 +1,277 @@
|
|||||||
|
"""Stage 3 — human review of ambiguous/unmatched matches, then versions.
|
||||||
|
|
||||||
|
A plain rich prompt loop (deliberately dependency-light, per spec). Every
|
||||||
|
decision rewrites matches.csv atomically, so quitting mid-review — q,
|
||||||
|
Ctrl-C, or end of input — loses nothing; the next run resumes with
|
||||||
|
whatever is still ambiguous/unmatched. The version pass is optional and
|
||||||
|
skippable: version review must never block getting games uploaded.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.panel import Panel
|
||||||
|
from rich.table import Table
|
||||||
|
|
||||||
|
from bggpipe.bgg_client import BGGAuthError, BGGClient, BGGQueueTimeout
|
||||||
|
from bggpipe.config import Config
|
||||||
|
from bggpipe.models import BGGResponseError
|
||||||
|
from bggpipe.resolve import (
|
||||||
|
MatchRow,
|
||||||
|
_resolve_version,
|
||||||
|
load_titles,
|
||||||
|
read_matches,
|
||||||
|
write_matches,
|
||||||
|
)
|
||||||
|
|
||||||
|
# input_fn(prompt) -> user's response; tests inject a scripted one
|
||||||
|
InputFn = Callable[[str], str]
|
||||||
|
|
||||||
|
_BGG_ERRORS = (
|
||||||
|
BGGAuthError,
|
||||||
|
BGGQueueTimeout,
|
||||||
|
BGGResponseError,
|
||||||
|
httpx.HTTPError,
|
||||||
|
OSError,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class _Quit(Exception):
|
||||||
|
"""User asked to leave the review (q / Ctrl-C / end of input)."""
|
||||||
|
|
||||||
|
|
||||||
|
class ReviewSession:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
cfg: Config,
|
||||||
|
*,
|
||||||
|
console: Console | None = None,
|
||||||
|
input_fn: InputFn | None = None,
|
||||||
|
client: BGGClient | None = None,
|
||||||
|
) -> None:
|
||||||
|
self.cfg = cfg
|
||||||
|
self.console = console or Console()
|
||||||
|
self.input_fn = input_fn or (lambda prompt: self.console.input(prompt))
|
||||||
|
self.client = client or BGGClient(cfg.cache_dir, cfg.rate_limit_seconds)
|
||||||
|
self.rows = read_matches(cfg.matches_path)
|
||||||
|
self.decisions = 0
|
||||||
|
try:
|
||||||
|
self._titles = {e.title_raw: e for e in load_titles(cfg.titles_path)}
|
||||||
|
except FileNotFoundError:
|
||||||
|
self._titles = {}
|
||||||
|
|
||||||
|
# -- plumbing -------------------------------------------------------
|
||||||
|
|
||||||
|
def _ask(self, prompt: str) -> str:
|
||||||
|
try:
|
||||||
|
answer = self.input_fn(prompt).strip()
|
||||||
|
except (EOFError, KeyboardInterrupt) as err:
|
||||||
|
raise _Quit from err
|
||||||
|
if answer.lower() == "q":
|
||||||
|
raise _Quit
|
||||||
|
return answer
|
||||||
|
|
||||||
|
def _save(self) -> None:
|
||||||
|
write_matches(self.cfg.matches_path, self.rows)
|
||||||
|
self.decisions += 1
|
||||||
|
|
||||||
|
def _apply_choice(self, row: dict, candidate: dict) -> None:
|
||||||
|
row["match_status"] = "approved"
|
||||||
|
row["bgg_id"] = str(candidate.get("bgg_id") or "")
|
||||||
|
row["bgg_name"] = candidate.get("name") or ""
|
||||||
|
row["year"] = str(candidate.get("year") or "")
|
||||||
|
row["type"] = candidate.get("type") or "boardgame"
|
||||||
|
self._fill_version(row)
|
||||||
|
self._save()
|
||||||
|
|
||||||
|
def _fill_version(self, row: dict) -> None:
|
||||||
|
"""Try version resolution for a just-approved row. Degrades gracefully:
|
||||||
|
no cues, no token, or API trouble all leave version_unknown."""
|
||||||
|
entry = self._titles.get(row["title_raw"])
|
||||||
|
if entry is None or not row["bgg_id"]:
|
||||||
|
row["version_status"] = row["version_status"] or "version_unknown"
|
||||||
|
return
|
||||||
|
shim = MatchRow(title_raw=row["title_raw"], bgg_id=int(row["bgg_id"]))
|
||||||
|
try:
|
||||||
|
_resolve_version(self.client, entry, shim)
|
||||||
|
except _BGG_ERRORS as err:
|
||||||
|
self.console.print(f"[yellow]version lookup unavailable: {err}[/yellow]")
|
||||||
|
row["version_status"] = "version_unknown"
|
||||||
|
return
|
||||||
|
row["version_status"] = shim.version_status
|
||||||
|
row["version_id"] = str(shim.version_id or "")
|
||||||
|
row["version_name"] = shim.version_name
|
||||||
|
row["version_candidates_json"] = json.dumps(
|
||||||
|
shim.version_candidates, ensure_ascii=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# -- displays -------------------------------------------------------
|
||||||
|
|
||||||
|
def _show_item(self, row: dict, candidates: list[dict]) -> None:
|
||||||
|
self.console.print(
|
||||||
|
Panel(
|
||||||
|
f"[bold]{row['title_raw']}[/bold]\n"
|
||||||
|
f"photos: {row['source_photos'] or '-'} "
|
||||||
|
f"status: {row['match_status']}",
|
||||||
|
expand=False,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if candidates:
|
||||||
|
table = Table()
|
||||||
|
for col in ("#", "name", "year", "type", "owned", "rank"):
|
||||||
|
table.add_column(col)
|
||||||
|
for i, c in enumerate(candidates, start=1):
|
||||||
|
table.add_row(
|
||||||
|
str(i),
|
||||||
|
str(c.get("name", "")),
|
||||||
|
str(c.get("year") or "-"),
|
||||||
|
str(c.get("type") or "-"),
|
||||||
|
str(c.get("owned") if c.get("owned") is not None else "-"),
|
||||||
|
str(c.get("rank") if c.get("rank") is not None else "-"),
|
||||||
|
)
|
||||||
|
self.console.print(table)
|
||||||
|
|
||||||
|
# -- match pass -----------------------------------------------------
|
||||||
|
|
||||||
|
def _review_match_row(self, row: dict) -> None:
|
||||||
|
candidates = json.loads(row["candidates_json"] or "[]")
|
||||||
|
while True:
|
||||||
|
self._show_item(row, candidates)
|
||||||
|
prompt = (
|
||||||
|
"[1-N] pick (s)kip (r)eject (m <id>) manual BGG id "
|
||||||
|
"(f <text>) re-search (q)uit > "
|
||||||
|
if row["match_status"] == "unmatched"
|
||||||
|
else "[1-N] pick (s)kip (r)eject (q)uit > "
|
||||||
|
)
|
||||||
|
answer = self._ask(prompt)
|
||||||
|
lowered = answer.lower()
|
||||||
|
if lowered == "s":
|
||||||
|
return
|
||||||
|
if lowered == "r":
|
||||||
|
row["match_status"] = "rejected"
|
||||||
|
self._save()
|
||||||
|
return
|
||||||
|
if answer.isdigit() and 1 <= int(answer) <= len(candidates):
|
||||||
|
self._apply_choice(row, candidates[int(answer) - 1])
|
||||||
|
return
|
||||||
|
if lowered.startswith("m ") and answer[2:].strip().isdigit():
|
||||||
|
self._manual_id(row, int(answer[2:].strip()))
|
||||||
|
return
|
||||||
|
if lowered.startswith("f ") and answer[2:].strip():
|
||||||
|
candidates = self._research(answer[2:].strip()) or candidates
|
||||||
|
continue
|
||||||
|
self.console.print("[yellow]didn't understand that — try again[/yellow]")
|
||||||
|
|
||||||
|
def _manual_id(self, row: dict, bgg_id: int) -> None:
|
||||||
|
candidate: dict = {"bgg_id": bgg_id}
|
||||||
|
try:
|
||||||
|
(thing,) = self.client.things([bgg_id])
|
||||||
|
candidate.update(name=thing.name, year=thing.year, type=thing.type)
|
||||||
|
except _BGG_ERRORS as err:
|
||||||
|
self.console.print(
|
||||||
|
f"[yellow]couldn't look up id {bgg_id} ({err}) — "
|
||||||
|
"recording the id with no name[/yellow]"
|
||||||
|
)
|
||||||
|
self._apply_choice(row, candidate)
|
||||||
|
|
||||||
|
def _research(self, query: str) -> list[dict]:
|
||||||
|
try:
|
||||||
|
results = self.client.search(query)
|
||||||
|
except _BGG_ERRORS as err:
|
||||||
|
self.console.print(f"[yellow]search unavailable: {err}[/yellow]")
|
||||||
|
return []
|
||||||
|
if not results:
|
||||||
|
self.console.print("[yellow]no results[/yellow]")
|
||||||
|
return [
|
||||||
|
{"bgg_id": r.bgg_id, "name": r.name, "year": r.year, "type": r.type}
|
||||||
|
for r in results
|
||||||
|
]
|
||||||
|
|
||||||
|
# -- version pass ---------------------------------------------------
|
||||||
|
|
||||||
|
def _review_version_row(self, row: dict) -> None:
|
||||||
|
candidates = json.loads(row["version_candidates_json"] or "[]")
|
||||||
|
table = Table(title=f"{row['title_raw']} — which edition?")
|
||||||
|
for col in ("#", "version", "year", "publishers", "languages", "score"):
|
||||||
|
table.add_column(col)
|
||||||
|
for i, v in enumerate(candidates, start=1):
|
||||||
|
table.add_row(
|
||||||
|
str(i),
|
||||||
|
str(v.get("name", "")),
|
||||||
|
str(v.get("year") or "-"),
|
||||||
|
", ".join(v.get("publishers") or []),
|
||||||
|
", ".join(v.get("languages") or []),
|
||||||
|
str(v.get("score", "-")),
|
||||||
|
)
|
||||||
|
self.console.print(table)
|
||||||
|
while True:
|
||||||
|
answer = self._ask("[1-N] pick (u)nknown (s)kip (q)uit > ")
|
||||||
|
lowered = answer.lower()
|
||||||
|
if lowered == "s":
|
||||||
|
return
|
||||||
|
if lowered == "u":
|
||||||
|
row["version_status"] = "version_unknown"
|
||||||
|
row["version_id"] = ""
|
||||||
|
row["version_name"] = ""
|
||||||
|
self._save()
|
||||||
|
return
|
||||||
|
if answer.isdigit() and 1 <= int(answer) <= len(candidates):
|
||||||
|
chosen = candidates[int(answer) - 1]
|
||||||
|
row["version_status"] = "version_approved"
|
||||||
|
row["version_id"] = str(chosen.get("version_id") or "")
|
||||||
|
row["version_name"] = chosen.get("name") or ""
|
||||||
|
self._save()
|
||||||
|
return
|
||||||
|
self.console.print("[yellow]didn't understand that — try again[/yellow]")
|
||||||
|
|
||||||
|
# -- entry point ----------------------------------------------------
|
||||||
|
|
||||||
|
def run(self) -> None:
|
||||||
|
pending = [
|
||||||
|
r for r in self.rows if r["match_status"] in ("ambiguous", "unmatched")
|
||||||
|
]
|
||||||
|
try:
|
||||||
|
for row in pending:
|
||||||
|
self._review_match_row(row)
|
||||||
|
versions = [
|
||||||
|
r for r in self.rows if r["version_status"] == "version_ambiguous"
|
||||||
|
]
|
||||||
|
if versions:
|
||||||
|
answer = self._ask(
|
||||||
|
f"{len(versions)} game(s) have ambiguous editions. "
|
||||||
|
"Review them now? [y/N] > "
|
||||||
|
)
|
||||||
|
if answer.lower() == "y":
|
||||||
|
for row in versions:
|
||||||
|
self._review_version_row(row)
|
||||||
|
except _Quit:
|
||||||
|
self.console.print("[dim]stopping — progress is saved[/dim]")
|
||||||
|
|
||||||
|
remaining = sum(
|
||||||
|
1 for r in self.rows if r["match_status"] in ("ambiguous", "unmatched")
|
||||||
|
)
|
||||||
|
self.console.print(
|
||||||
|
f"Recorded {self.decisions} decision(s); "
|
||||||
|
f"{remaining} item(s) still need review."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_review(
|
||||||
|
cfg: Config,
|
||||||
|
*,
|
||||||
|
console: Console | None = None,
|
||||||
|
input_fn: InputFn | None = None,
|
||||||
|
client: BGGClient | None = None,
|
||||||
|
) -> ReviewSession:
|
||||||
|
session = ReviewSession(cfg, console=console, input_fn=input_fn, client=client)
|
||||||
|
if not session.rows:
|
||||||
|
session.console.print(
|
||||||
|
f"{cfg.matches_path} is empty — run `bggpipe resolve` first."
|
||||||
|
)
|
||||||
|
return session
|
||||||
|
session.run()
|
||||||
|
return session
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
"""Extract-stage tests. The vision callable is always a local fake — the
|
||||||
|
Anthropic API is never touched here."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import io
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import typer
|
||||||
|
|
||||||
|
from bggpipe.config import Config
|
||||||
|
from bggpipe.extract import (
|
||||||
|
dedupe_entries,
|
||||||
|
parse_vision_json,
|
||||||
|
prepare_image,
|
||||||
|
run_extract,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _write_image(path, size=(400, 300), fmt="JPEG", color=(200, 30, 30)):
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
img = Image.new("RGB", size, color)
|
||||||
|
if fmt == "HEIC":
|
||||||
|
from pillow_heif import register_heif_opener
|
||||||
|
|
||||||
|
register_heif_opener()
|
||||||
|
img.save(path, format="HEIF")
|
||||||
|
else:
|
||||||
|
img.save(path, format=fmt)
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def _vision_stub(payload):
|
||||||
|
"""A VisionFn returning a canned response, recording call count."""
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def vision(image_b64, media_type):
|
||||||
|
calls.append(media_type)
|
||||||
|
return payload
|
||||||
|
|
||||||
|
return vision, calls
|
||||||
|
|
||||||
|
|
||||||
|
# -- image preparation --------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_prepare_image_downscales_long_edge(tmp_path):
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
photo = _write_image(tmp_path / "big.jpg", size=(4000, 3000))
|
||||||
|
b64, media_type = prepare_image(photo)
|
||||||
|
assert media_type == "image/jpeg"
|
||||||
|
out = Image.open(io.BytesIO(base64.standard_b64decode(b64)))
|
||||||
|
assert max(out.size) == 1568
|
||||||
|
assert out.size == (1568, 1176) # aspect ratio preserved
|
||||||
|
|
||||||
|
|
||||||
|
def test_prepare_image_keeps_small_images(tmp_path):
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
photo = _write_image(tmp_path / "small.png", size=(640, 480), fmt="PNG")
|
||||||
|
b64, _ = prepare_image(photo)
|
||||||
|
out = Image.open(io.BytesIO(base64.standard_b64decode(b64)))
|
||||||
|
assert out.size == (640, 480)
|
||||||
|
assert out.format == "JPEG" # PNG converted to JPEG
|
||||||
|
|
||||||
|
|
||||||
|
def test_prepare_image_converts_heic(tmp_path):
|
||||||
|
photo = _write_image(tmp_path / "iphone.heic", size=(2000, 1500), fmt="HEIC")
|
||||||
|
b64, media_type = prepare_image(photo)
|
||||||
|
assert media_type == "image/jpeg"
|
||||||
|
assert len(b64) > 0
|
||||||
|
|
||||||
|
|
||||||
|
# -- defensive JSON parsing ---------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_strips_code_fences():
|
||||||
|
text = '```json\n[{"title_raw": "Catan", "confidence": "high"}]\n```'
|
||||||
|
assert parse_vision_json(text)[0]["title_raw"] == "Catan"
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_tolerates_prose_around_array():
|
||||||
|
text = 'Here are the games:\n[{"title_raw": "Wingspan"}]\nLet me know!'
|
||||||
|
assert parse_vision_json(text)[0]["title_raw"] == "Wingspan"
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_drops_malformed_entries():
|
||||||
|
text = '[{"title_raw": "Catan"}, {"no_title": true}, "just a string"]'
|
||||||
|
entries = parse_vision_json(text)
|
||||||
|
assert len(entries) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_raises_on_garbage():
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
parse_vision_json("I couldn't see any games clearly.")
|
||||||
|
|
||||||
|
|
||||||
|
# -- dedupe with edition cues -------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _entry(title, photo, **cues):
|
||||||
|
return {
|
||||||
|
"title_raw": title,
|
||||||
|
"confidence": cues.pop("confidence", "high"),
|
||||||
|
"publisher_hint": cues.pop("publisher_hint", ""),
|
||||||
|
"edition_hint": cues.pop("edition_hint", ""),
|
||||||
|
"year_hint": cues.pop("year_hint", None),
|
||||||
|
"language_hint": cues.pop("language_hint", ""),
|
||||||
|
"art_notes": cues.pop("art_notes", ""),
|
||||||
|
"source_photos": [photo],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_dedupe_merges_same_title_compatible_cues():
|
||||||
|
deduped = dedupe_entries(
|
||||||
|
[
|
||||||
|
_entry("Wingspan", "a.jpg", publisher_hint="Stonemaier Games"),
|
||||||
|
_entry("WINGSPAN", "b.jpg"), # no cues -> compatible
|
||||||
|
]
|
||||||
|
)
|
||||||
|
assert len(deduped) == 1
|
||||||
|
assert deduped[0]["source_photos"] == ["a.jpg", "b.jpg"]
|
||||||
|
assert deduped[0]["publisher_hint"] == "Stonemaier Games" # provenance kept
|
||||||
|
|
||||||
|
|
||||||
|
def test_dedupe_keeps_conflicting_editions_separate():
|
||||||
|
deduped = dedupe_entries(
|
||||||
|
[
|
||||||
|
_entry(
|
||||||
|
"Cosmic Encounter", "a.jpg", edition_hint="42nd Anniversary Edition"
|
||||||
|
),
|
||||||
|
_entry("Cosmic Encounter", "b.jpg", edition_hint="Eon 1977 edition"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
assert len(deduped) == 2 # different editions stay separate entries
|
||||||
|
|
||||||
|
|
||||||
|
def test_dedupe_conflicting_years_stay_separate():
|
||||||
|
deduped = dedupe_entries(
|
||||||
|
[
|
||||||
|
_entry("Catan", "a.jpg", year_hint=1995),
|
||||||
|
_entry("Catan", "b.jpg", year_hint=2015),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
assert len(deduped) == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_dedupe_upgrades_confidence():
|
||||||
|
deduped = dedupe_entries(
|
||||||
|
[
|
||||||
|
_entry("Azul", "a.jpg", confidence="low"),
|
||||||
|
_entry("Azul", "b.jpg", confidence="high"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
assert deduped[0]["confidence"] == "high"
|
||||||
|
|
||||||
|
|
||||||
|
# -- run_extract orchestration ------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _cfg(tmp_path):
|
||||||
|
return Config(photos_dir=tmp_path / "photos", data_dir=tmp_path / "data")
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_extract_is_idempotent_and_resumable(tmp_path):
|
||||||
|
cfg = _cfg(tmp_path)
|
||||||
|
cfg.photos_dir.mkdir()
|
||||||
|
_write_image(cfg.photos_dir / "shelf1.jpg")
|
||||||
|
_write_image(cfg.photos_dir / "shelf2.jpg")
|
||||||
|
vision, calls = _vision_stub('[{"title_raw": "Catan", "confidence": "high"}]')
|
||||||
|
|
||||||
|
result = run_extract(cfg, vision=vision)
|
||||||
|
assert len(calls) == 2 # one vision call per photo
|
||||||
|
assert len(result) == 1 # deduped across photos
|
||||||
|
assert result[0]["source_photos"] == ["shelf1.jpg", "shelf2.jpg"]
|
||||||
|
assert cfg.titles_path.exists()
|
||||||
|
|
||||||
|
run_extract(cfg, vision=vision)
|
||||||
|
assert len(calls) == 2 # second run: everything cached, no vision calls
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_extract_only_reprocesses_one_photo(tmp_path):
|
||||||
|
cfg = _cfg(tmp_path)
|
||||||
|
cfg.photos_dir.mkdir()
|
||||||
|
_write_image(cfg.photos_dir / "shelf1.jpg")
|
||||||
|
_write_image(cfg.photos_dir / "blurry.jpg")
|
||||||
|
vision, calls = _vision_stub('[{"title_raw": "Azul"}]')
|
||||||
|
run_extract(cfg, vision=vision)
|
||||||
|
|
||||||
|
retake_vision, retake_calls = _vision_stub(
|
||||||
|
'[{"title_raw": "Azul: Summer Pavilion"}]'
|
||||||
|
)
|
||||||
|
result = run_extract(cfg, only="blurry.jpg", vision=retake_vision)
|
||||||
|
assert retake_calls == ["image/jpeg"] # exactly one re-extraction
|
||||||
|
titles = {e["title_raw"] for e in result}
|
||||||
|
assert titles == {"Azul", "Azul: Summer Pavilion"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_extract_empty_photos_dir_exits(tmp_path):
|
||||||
|
cfg = _cfg(tmp_path)
|
||||||
|
cfg.photos_dir.mkdir()
|
||||||
|
vision, _ = _vision_stub("[]")
|
||||||
|
with pytest.raises(typer.Exit):
|
||||||
|
run_extract(cfg, vision=vision)
|
||||||
@@ -0,0 +1,290 @@
|
|||||||
|
"""Review-TUI tests against a synthetic matches.csv, driven by scripted
|
||||||
|
input. BGG responses replay from the fixture cache; nothing hits the
|
||||||
|
network (the transport raises if it would)."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from rich.console import Console
|
||||||
|
|
||||||
|
from bggpipe.bgg_client import BGGClient
|
||||||
|
from bggpipe.config import Config
|
||||||
|
from bggpipe.resolve import read_matches, write_matches
|
||||||
|
from bggpipe.review import run_review
|
||||||
|
|
||||||
|
FIXTURES = Path(__file__).parent / "fixtures" / "bgg_cache"
|
||||||
|
|
||||||
|
|
||||||
|
def _no_network(request: httpx.Request) -> httpx.Response:
|
||||||
|
raise AssertionError(f"test hit the network: {request.url}")
|
||||||
|
|
||||||
|
|
||||||
|
def fixture_client() -> BGGClient:
|
||||||
|
return BGGClient(cache_dir=FIXTURES, transport=httpx.MockTransport(_no_network))
|
||||||
|
|
||||||
|
|
||||||
|
def unauthorized_client(tmp_path) -> BGGClient:
|
||||||
|
"""Client whose every request 401s — the no-token-yet world."""
|
||||||
|
transport = httpx.MockTransport(
|
||||||
|
lambda req: httpx.Response(401, text="Unauthorized")
|
||||||
|
)
|
||||||
|
return BGGClient(cache_dir=tmp_path / "empty_cache", transport=transport)
|
||||||
|
|
||||||
|
|
||||||
|
def scripted(*answers):
|
||||||
|
it = iter(answers)
|
||||||
|
|
||||||
|
def input_fn(prompt: str) -> str:
|
||||||
|
try:
|
||||||
|
return next(it)
|
||||||
|
except StopIteration:
|
||||||
|
raise EOFError from None
|
||||||
|
|
||||||
|
return input_fn
|
||||||
|
|
||||||
|
|
||||||
|
def quiet_console() -> Console:
|
||||||
|
return Console(file=io.StringIO(), width=100)
|
||||||
|
|
||||||
|
|
||||||
|
def _row(**overrides) -> dict:
|
||||||
|
row = {
|
||||||
|
"title_raw": "",
|
||||||
|
"bgg_id": "",
|
||||||
|
"bgg_name": "",
|
||||||
|
"year": "",
|
||||||
|
"type": "",
|
||||||
|
"match_status": "auto",
|
||||||
|
"version_id": "",
|
||||||
|
"version_name": "",
|
||||||
|
"version_status": "version_unknown",
|
||||||
|
"candidates_json": "[]",
|
||||||
|
"version_candidates_json": "[]",
|
||||||
|
"source_photos": "hand-typed-test-list",
|
||||||
|
}
|
||||||
|
row.update(overrides)
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
CITADELS_CANDIDATES = json.dumps(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"bgg_id": 478,
|
||||||
|
"name": "Citadels",
|
||||||
|
"year": 2000,
|
||||||
|
"type": "boardgame",
|
||||||
|
"owned": 85000,
|
||||||
|
"rank": 250,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bgg_id": 205398,
|
||||||
|
"name": "Citadels",
|
||||||
|
"year": 2016,
|
||||||
|
"type": "boardgame",
|
||||||
|
"owned": 24000,
|
||||||
|
"rank": 400,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
WINGSPAN_VERSION_CANDIDATES = json.dumps(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"version_id": 465063,
|
||||||
|
"name": "English first edition",
|
||||||
|
"year": 2019,
|
||||||
|
"publishers": ["Stonemaier Games"],
|
||||||
|
"languages": ["English"],
|
||||||
|
"score": 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version_id": 521212,
|
||||||
|
"name": "English fourth printing",
|
||||||
|
"year": 2020,
|
||||||
|
"publishers": ["Stonemaier Games"],
|
||||||
|
"languages": ["English"],
|
||||||
|
"score": 5,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _cfg(tmp_path) -> Config:
|
||||||
|
return Config(data_dir=tmp_path / "data")
|
||||||
|
|
||||||
|
|
||||||
|
def _setup(tmp_path, rows) -> Config:
|
||||||
|
cfg = _cfg(tmp_path)
|
||||||
|
write_matches(cfg.matches_path, rows)
|
||||||
|
return cfg
|
||||||
|
|
||||||
|
|
||||||
|
def test_ambiguous_pick_approves_candidate(tmp_path):
|
||||||
|
cfg = _setup(
|
||||||
|
tmp_path,
|
||||||
|
[
|
||||||
|
_row(
|
||||||
|
title_raw="Citadels",
|
||||||
|
match_status="ambiguous",
|
||||||
|
candidates_json=CITADELS_CANDIDATES,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
run_review(
|
||||||
|
cfg, console=quiet_console(), input_fn=scripted("2"), client=fixture_client()
|
||||||
|
)
|
||||||
|
(row,) = read_matches(cfg.matches_path)
|
||||||
|
assert row["match_status"] == "approved"
|
||||||
|
assert row["bgg_id"] == "205398"
|
||||||
|
assert row["year"] == "2016"
|
||||||
|
|
||||||
|
|
||||||
|
def test_reject_and_skip(tmp_path):
|
||||||
|
cfg = _setup(
|
||||||
|
tmp_path,
|
||||||
|
[
|
||||||
|
_row(title_raw="Blorvath", match_status="unmatched"),
|
||||||
|
_row(
|
||||||
|
title_raw="Citadels",
|
||||||
|
match_status="ambiguous",
|
||||||
|
candidates_json=CITADELS_CANDIDATES,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
run_review(
|
||||||
|
cfg,
|
||||||
|
console=quiet_console(),
|
||||||
|
input_fn=scripted("r", "s"),
|
||||||
|
client=fixture_client(),
|
||||||
|
)
|
||||||
|
rows = {r["title_raw"]: r for r in read_matches(cfg.matches_path)}
|
||||||
|
assert rows["Blorvath"]["match_status"] == "rejected"
|
||||||
|
assert rows["Citadels"]["match_status"] == "ambiguous" # skipped, still pending
|
||||||
|
|
||||||
|
|
||||||
|
def test_unmatched_manual_id_degrades_without_token(tmp_path):
|
||||||
|
cfg = _setup(tmp_path, [_row(title_raw="Some Rare Game", match_status="unmatched")])
|
||||||
|
run_review(
|
||||||
|
cfg,
|
||||||
|
console=quiet_console(),
|
||||||
|
input_fn=scripted("m 99999"),
|
||||||
|
client=unauthorized_client(tmp_path),
|
||||||
|
)
|
||||||
|
(row,) = read_matches(cfg.matches_path)
|
||||||
|
assert row["match_status"] == "approved"
|
||||||
|
assert row["bgg_id"] == "99999"
|
||||||
|
assert row["bgg_name"] == "" # lookup blocked; id recorded anyway
|
||||||
|
|
||||||
|
|
||||||
|
def test_unmatched_research_from_fixture_cache_with_version(tmp_path):
|
||||||
|
cfg = _setup(tmp_path, [_row(title_raw="Wingspan", match_status="unmatched")])
|
||||||
|
cfg.titles_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
cfg.titles_path.write_text(
|
||||||
|
json.dumps(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"title_raw": "Wingspan",
|
||||||
|
"publisher_hint": "Stonemaier Games",
|
||||||
|
"year_hint": 2019,
|
||||||
|
"language_hint": "English",
|
||||||
|
"source_photos": ["x.jpg"],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
run_review(
|
||||||
|
cfg,
|
||||||
|
console=quiet_console(),
|
||||||
|
input_fn=scripted("f Wingspan", "1"),
|
||||||
|
client=fixture_client(),
|
||||||
|
)
|
||||||
|
(row,) = read_matches(cfg.matches_path)
|
||||||
|
assert row["match_status"] == "approved"
|
||||||
|
assert row["bgg_id"] == "266192"
|
||||||
|
# cues + fixture versions -> resolved to the 2019 Stonemaier English edition
|
||||||
|
assert row["version_status"] == "version_auto"
|
||||||
|
assert row["version_id"] == "465063"
|
||||||
|
|
||||||
|
|
||||||
|
def test_resumable_quit_midway_then_continue(tmp_path):
|
||||||
|
rows = [
|
||||||
|
_row(
|
||||||
|
title_raw="Citadels",
|
||||||
|
match_status="ambiguous",
|
||||||
|
candidates_json=CITADELS_CANDIDATES,
|
||||||
|
),
|
||||||
|
_row(title_raw="Blorvath", match_status="unmatched"),
|
||||||
|
]
|
||||||
|
cfg = _setup(tmp_path, rows)
|
||||||
|
|
||||||
|
# first sitting: one decision, then input runs out (EOF == quit)
|
||||||
|
run_review(
|
||||||
|
cfg, console=quiet_console(), input_fn=scripted("1"), client=fixture_client()
|
||||||
|
)
|
||||||
|
by_title = {r["title_raw"]: r for r in read_matches(cfg.matches_path)}
|
||||||
|
assert by_title["Citadels"]["match_status"] == "approved"
|
||||||
|
assert by_title["Blorvath"]["match_status"] == "unmatched" # untouched
|
||||||
|
|
||||||
|
# second sitting resumes exactly where we left off
|
||||||
|
run_review(
|
||||||
|
cfg, console=quiet_console(), input_fn=scripted("r"), client=fixture_client()
|
||||||
|
)
|
||||||
|
by_title = {r["title_raw"]: r for r in read_matches(cfg.matches_path)}
|
||||||
|
assert by_title["Blorvath"]["match_status"] == "rejected"
|
||||||
|
|
||||||
|
|
||||||
|
def test_version_pass_pick_and_unknown(tmp_path):
|
||||||
|
cfg = _setup(
|
||||||
|
tmp_path,
|
||||||
|
[
|
||||||
|
_row(
|
||||||
|
title_raw="Wingspan",
|
||||||
|
match_status="auto",
|
||||||
|
bgg_id="266192",
|
||||||
|
version_status="version_ambiguous",
|
||||||
|
version_candidates_json=WINGSPAN_VERSION_CANDIDATES,
|
||||||
|
),
|
||||||
|
_row(
|
||||||
|
title_raw="Catan",
|
||||||
|
match_status="auto",
|
||||||
|
bgg_id="13",
|
||||||
|
version_status="version_ambiguous",
|
||||||
|
version_candidates_json=WINGSPAN_VERSION_CANDIDATES,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
run_review(
|
||||||
|
cfg,
|
||||||
|
console=quiet_console(),
|
||||||
|
input_fn=scripted("y", "1", "u"),
|
||||||
|
client=fixture_client(),
|
||||||
|
)
|
||||||
|
by_title = {r["title_raw"]: r for r in read_matches(cfg.matches_path)}
|
||||||
|
assert by_title["Wingspan"]["version_status"] == "version_approved"
|
||||||
|
assert by_title["Wingspan"]["version_id"] == "465063"
|
||||||
|
assert by_title["Catan"]["version_status"] == "version_unknown"
|
||||||
|
assert by_title["Catan"]["version_id"] == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_version_pass_is_skippable(tmp_path):
|
||||||
|
cfg = _setup(
|
||||||
|
tmp_path,
|
||||||
|
[
|
||||||
|
_row(
|
||||||
|
title_raw="Wingspan",
|
||||||
|
match_status="auto",
|
||||||
|
bgg_id="266192",
|
||||||
|
version_status="version_ambiguous",
|
||||||
|
version_candidates_json=WINGSPAN_VERSION_CANDIDATES,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
run_review(
|
||||||
|
cfg, console=quiet_console(), input_fn=scripted("n"), client=fixture_client()
|
||||||
|
)
|
||||||
|
(row,) = read_matches(cfg.matches_path)
|
||||||
|
assert row["version_status"] == "version_ambiguous" # untouched, review later
|
||||||
@@ -11,6 +11,34 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/3e/30/e900b21425a860e195f32e37657aa1f7c7f2b1bfb26f03ca209b90933c06/annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101", size = 5302, upload-time = "2026-07-28T13:50:57.239Z" },
|
{ url = "https://files.pythonhosted.org/packages/3e/30/e900b21425a860e195f32e37657aa1f7c7f2b1bfb26f03ca209b90933c06/annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101", size = 5302, upload-time = "2026-07-28T13:50:57.239Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "annotated-types"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anthropic"
|
||||||
|
version = "0.120.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "anyio" },
|
||||||
|
{ name = "distro" },
|
||||||
|
{ name = "docstring-parser" },
|
||||||
|
{ name = "httpx" },
|
||||||
|
{ name = "jiter" },
|
||||||
|
{ name = "pydantic" },
|
||||||
|
{ name = "sniffio" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d7/10/4ca013cb166f226bd89e0aeb0fcaff94f45ddf716d4925ce89475d3c587b/anthropic-0.120.2.tar.gz", hash = "sha256:9722efc10c27a30a69f5338ddacdb35bc6a64297a4e4ba729bf83af873d5fb3a", size = 1008421, upload-time = "2026-07-28T17:38:26.986Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/af/0f5db57b9397a0f3b7fc204cbef143401a7cadaf982330f97f1ce3d39f34/anthropic-0.120.2-py3-none-any.whl", hash = "sha256:0f0bc2b381dc0eb41c8d886b815d79c2041cd2374f83aed36f574b6dc9c579c1", size = 1022851, upload-time = "2026-07-28T17:38:25.466Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyio"
|
name = "anyio"
|
||||||
version = "4.14.2"
|
version = "4.14.2"
|
||||||
@@ -29,9 +57,13 @@ name = "bggpipe"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
{ name = "anthropic" },
|
||||||
{ name = "defusedxml" },
|
{ name = "defusedxml" },
|
||||||
{ name = "httpx" },
|
{ name = "httpx" },
|
||||||
|
{ name = "pillow" },
|
||||||
|
{ name = "pillow-heif" },
|
||||||
{ name = "rapidfuzz" },
|
{ name = "rapidfuzz" },
|
||||||
|
{ name = "rich" },
|
||||||
{ name = "typer" },
|
{ name = "typer" },
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -43,9 +75,13 @@ dev = [
|
|||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
|
{ name = "anthropic", specifier = ">=0.120.2" },
|
||||||
{ name = "defusedxml", specifier = ">=0.7.1" },
|
{ name = "defusedxml", specifier = ">=0.7.1" },
|
||||||
{ name = "httpx", specifier = ">=0.27" },
|
{ name = "httpx", specifier = ">=0.27" },
|
||||||
|
{ name = "pillow", specifier = ">=12.3.0" },
|
||||||
|
{ name = "pillow-heif", specifier = ">=1.5.0" },
|
||||||
{ name = "rapidfuzz", specifier = ">=3.9" },
|
{ name = "rapidfuzz", specifier = ">=3.9" },
|
||||||
|
{ name = "rich", specifier = ">=15.0.0" },
|
||||||
{ name = "typer", specifier = ">=0.12" },
|
{ name = "typer", specifier = ">=0.12" },
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -82,6 +118,24 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" },
|
{ url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "distro"
|
||||||
|
version = "1.9.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "docstring-parser"
|
||||||
|
version = "0.18.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341, upload-time = "2026-04-14T04:09:19.867Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h11"
|
name = "h11"
|
||||||
version = "0.16.0"
|
version = "0.16.0"
|
||||||
@@ -137,6 +191,74 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jiter"
|
||||||
|
version = "0.16.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/1d/1f/10936e16d8860c70698a1aa939a46aa0224813b782bce4e000e637da0b2d/jiter-0.16.0.tar.gz", hash = "sha256:7b24c3492c5f4f84a37946ad9cf504910cf6a782d6a4e0689b6673c5894b4a1c", size = 176431, upload-time = "2026-06-29T13:05:13.657Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/83/2b/52ace16ed031354f0539749a49e4bf33797d82bea5137910835fa4b09793/jiter-0.16.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:67c3bc1760f8c99d805dcab4e644027142a53b1d5d861f18780ebdbd5d40b72a", size = 306943, upload-time = "2026-06-29T13:03:14.035Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/2e/34957c2c1b661c252ba9bcc60ae0bddc27e0f7202c6073326a13c5390eec/jiter-0.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5af7780e4a26bd7d0d989592bf9ef12ebf806b74ab709223ecca37c749872ea9", size = 307779, upload-time = "2026-06-29T13:03:15.418Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/6c/59bd309cab4460c54cf1079f3eb7fe7af6a4c895c5c957a53378693bad2b/jiter-0.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5bf78d0e05e45cfdd66558893938d59afe3d1b1a824a202039b20e607d25a72", size = 335826, upload-time = "2026-06-29T13:03:17.11Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3b/8c/f5ef7b65f0df47afa16596969defb281ebb86e96df346d62be6fd853d620/jiter-0.16.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4444a83f946605990c98f625cdd3d2725bfb818158760c5748c653170a20e0e", size = 362573, upload-time = "2026-06-29T13:03:18.781Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/0b/ace4354da061ee38844a0c27dc2c21eecd27aea119e8da324bea987522d0/jiter-0.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a23f0e4f957e1be65752d2dfac9a5a06b1917af8dc85deb639c3b9d02e31290", size = 457979, upload-time = "2026-06-29T13:03:20.293Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/55/40/c0253d3772eb9dcd8e6606ee9b2d53ec8e5b814589c47f140aa585f21eaa/jiter-0.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c22a488f7b9218e245a0025a9ba6b100e2e54700831cf4cf16833a27fba3ad01", size = 372302, upload-time = "2026-06-29T13:03:21.739Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a8/d2/4839422241aa12860ce597b20068727094ba0bc480723c74924ca5bad483/jiter-0.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46add52f4ad47a08bfb1219f3e673da972191489a33016edefdb5ea55bfa8c48", size = 343805, upload-time = "2026-06-29T13:03:23.384Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e2/59/e196888a05befdda7dbe299b722d56f2f6eec65402bc34c0a3306d595feb/jiter-0.16.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:9c8a956fd72c2cf1e730d01ea080341f13aa0a97a4a33b51abebe725b7ae9ca9", size = 351107, upload-time = "2026-06-29T13:03:24.815Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/74/4cd9e0fca65232136400354b630fbfcd2de634e22ccbb96567725981b548/jiter-0.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:561926e0573ffe4a32498420a76d64b16c513e1ab413b9d28158a8764ac701e5", size = 388441, upload-time = "2026-06-29T13:03:26.266Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d9/8c/554691e48bc711299c0a293dd8a6179e24b2d66a54dc295421fcf64569c0/jiter-0.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:44d019fa8cdaf89bf29c71b39e3712143fdd0ac76725c6ef954f9957a5ea8730", size = 516354, upload-time = "2026-06-29T13:03:28.02Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a4/cb/01e9d69dc2cc6759d4f91e230b34489c4fdb2518992650633f9e20bece89/jiter-0.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0df91907609837f33341b8e6fe73b95991fdaa57caf1a0fbd343dffe826f386f", size = 547880, upload-time = "2026-06-29T13:03:29.534Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/79/70/2953195f1c6ad00f49fa67e13df7e60acb3dd4f387101bc15abccddd905e/jiter-0.16.0-cp312-cp312-win32.whl", hash = "sha256:51d7b836acb0108d7c77df1742332cac2a1fa04a74d6dacec46e7091f0e91274", size = 203473, upload-time = "2026-06-29T13:03:31.025Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2d/05/2909a8b10699a4d560f8c502b6b2c5f3991b682b1922c1eedda242b225bd/jiter-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:1878349266f8ee36ecb1375cc5ba2f115f35fd9f0a1a4119e725e379126647f7", size = 196905, upload-time = "2026-06-29T13:03:32.472Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e9/a9/6b82bb1c8d7790d602489b967b982a909e5d092875a6c2ade96444c8dfc5/jiter-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:2ed5738ae4af18271a51a528b8811b0cbfa4a1858de9d83359e4169855d6a331", size = 190618, upload-time = "2026-06-29T13:03:34.672Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/91/c0/555fc60473d30d66894ba825e63615e3be7524fac23858356afa7a38906c/jiter-0.16.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:41977aa5654023948c2dae2a81cbf9c43343954bef1cd59a154dd15a4d84c195", size = 306203, upload-time = "2026-06-29T13:03:36.243Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d0/2b/c3eaf16f5d7c9bad66ea32f40a95bd169b29a91217fcc7f081375157e99c/jiter-0.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d28bb3c26762358dadf3e5bf0bccd29ae987d65e6988d2e6f49829c76b003c09", size = 306489, upload-time = "2026-06-29T13:03:37.846Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/96/3f/02fdfc6705cad96127d883af5c34e4867f554f29ec7705ec1a46156400a9/jiter-0.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0542a7189c26920778658fc8fcf2af8bae05bae9924577f71804acef37996536", size = 335453, upload-time = "2026-06-29T13:03:39.221Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b2/a6/e4bda5920d4b0d7c5dfb7174ce4a6b2e4d3e11c9162c452ef0eab4cdbdbd/jiter-0.16.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8fb8de1e23a0cb2a7f53c335049c7b72b6db41aa6227cdcc0972a1de5cb39450", size = 361625, upload-time = "2026-06-29T13:03:40.597Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/97/4e6b59b2c6e55cbb3e183595f81ad65dcfb21c915fee5e19e335df21bc55/jiter-0.16.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b72d0b2990ca754a9102779ac98d8597b7cb31678958562214a007f909eab78e", size = 456958, upload-time = "2026-06-29T13:03:42.074Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/e0/97e9557686d2f94f4b93786eccb7eed28e9228ad132ea8237f44727314a7/jiter-0.16.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f91b1c27fc22a57993d5a5cb8a627cb8ed4b10502716fac1ffbfe1d19d84e8", size = 372017, upload-time = "2026-06-29T13:03:43.658Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0f/94/db768b6938e0df35c86beeba3dfbbb025c9ee5c19e1aa271f2396e50864d/jiter-0.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c682bea068a90b764577bdb78a60a4c1d1606daf9cd4c893832a37c7cc9d9026", size = 343320, upload-time = "2026-06-29T13:03:45.226Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c1/d6/5a59d938244a30735fe62d9433fd325f9021ea29d89780ea4596ea93bc89/jiter-0.16.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:8d031aabecc4f1b6276adfb42e3aabb77c89d468bf616600e8d3a11328929053", size = 350520, upload-time = "2026-06-29T13:03:46.671Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/f8/c4a857f49c9af125f6bbcac7e3eee7f7978ed89682833062e2dbf62576b1/jiter-0.16.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eab2cd170150e70153de16896a1774e3a1dca80154c56b54d7a812c479a7165e", size = 387550, upload-time = "2026-06-29T13:03:48.361Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/d6/5fbc2f7d6b67b754caa61a993a2e626e815dec47ffc2f9e35f01adfebec7/jiter-0.16.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:6edb63a46e65a82c26800a868e49b2cac30dd5a4218b88d74bc2c848c8ad60bb", size = 515424, upload-time = "2026-06-29T13:03:49.881Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/54/284f0164b64a5fed915fea6ba7e9ba9b3d8d37c67d59cf2e3bb99d45cdfe/jiter-0.16.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:659039cc50b5addcc35fcc87ae2c1833b7c0a8e5326ef631a75e4478447bcf84", size = 546981, upload-time = "2026-06-29T13:03:51.363Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/13/c5/2a467585a576594384e1d2c43e1224deaafc085f24e243529cf98beef8e1/jiter-0.16.0-cp313-cp313-win32.whl", hash = "sha256:c9c53be232c2e206ef9cdbad81a48bfa74c3d3f08bcf8124630a8a748aad993e", size = 202853, upload-time = "2026-06-29T13:03:53.015Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/6a/de61d04b9eec69c71719968d2f716532a3bc121170c44a39e14979c6be81/jiter-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:baad945ed47f163ad833314f8e3288c396118934f94e7bbb9e243ce4b341a4fd", size = 196160, upload-time = "2026-06-29T13:03:54.447Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/19/4b/b390ed59bafb3f31d008d1218578f10327714484b334439947f7e5b11e7f/jiter-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:3c1fd2dbe1b0af19e987f03fe66c5f5bd105a2229c1aff4ab14890b24f41d21a", size = 189862, upload-time = "2026-06-29T13:03:55.754Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a7/89/bc4f1b57d5da938fd344a466396541e586d161320d70bffd929aaafcd8f4/jiter-0.16.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b2c61484666ad42726029af0c00ef4541f0f3b5cdc550221f56c2343208018ee", size = 308239, upload-time = "2026-06-29T13:03:57.205Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/65/7a/c415453e5213001bf3b411ff65dec3d303b0e76a4a2cfea9768cd4960994/jiter-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:63efadc657488f45db1c676d81e704cac2abf3fdb892def1faea61db053127e2", size = 308928, upload-time = "2026-06-29T13:03:58.643Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/11/fc/1f4fb7ebf9a724c7741994f4aae18fba1e2f3133df14521a79194952c34a/jiter-0.16.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf0d73f50e7b6935677854f6e8e31d499ca7064dd24734f703e060f5b237d883", size = 336998, upload-time = "2026-06-29T13:04:00.071Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/8d/72cadaac05ccfa7cc3a0a2232862e6c72443ca40cf300ba8b57f9f18b69b/jiter-0.16.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3ea07d9bc8e7d03a9fbc051295462e6dbc295b894fd72457c3136e3e43d898", size = 362112, upload-time = "2026-06-29T13:04:01.52Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/58/4a/c4b0d5f651fda90a24ffce9f8d56cde462a2e09d31ae3de3c68cef34c04e/jiter-0.16.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26798522707abb47d767db536e4148ceac1b14446bf028ee85e579a2e043cfe5", size = 459807, upload-time = "2026-06-29T13:04:03.214Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/80/58/ef77879ea9aa56b50824edc5a445e226422c7a8d211f3fd2a56bcb9493cf/jiter-0.16.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bc837c1b9631be10abfe0191537fe8009838204cec7e44827401ace390ddb567", size = 373181, upload-time = "2026-06-29T13:04:04.629Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/2e/ffbc3f254e4d8a66da3062c624a7df4b7c2b2cf9e1fe43cf394b3e104041/jiter-0.16.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49060fd70737fad59d33ba9dcc0d83247dc9e77187de26053a19c16c9f32bd69", size = 344927, upload-time = "2026-06-29T13:04:06.067Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9a/f6/0be5dc6d64a89f80aa8fec984f94dedb2973e251edcae55841d60786d578/jiter-0.16.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:adbb8edeadd431bc4477879d5d371ece7cb1334486584e0f252656dd7ffada29", size = 352754, upload-time = "2026-06-29T13:04:07.477Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/6e/7d31243b3b91cd261dd19e9d3557fc3251a80883d3d8049c86174e7ab7af/jiter-0.16.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:31aaee5b80f672c1dc21272bcfb9cbdcfc1ea04ff50f00ed5af500b80c44fa93", size = 390553, upload-time = "2026-06-29T13:04:08.92Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/25/33/51ae371fde3c88897520f62b4d5f8b27ad7103e2bb10812ff52195609853/jiter-0.16.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:6722bcef4ffc86c835574b1b2fac6b33b9fb4a889c781e67950e891591f3c55a", size = 516900, upload-time = "2026-06-29T13:04:10.407Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/45/6449b3d123ea439ba79507c657288f461d55049e7bcbdc2cf8eb8210f491/jiter-0.16.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:5ab4f50ff971b611d656554ea10b75f80097392c827bc32923c6eeb6386c8b00", size = 548754, upload-time = "2026-06-29T13:04:12.046Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9b/e7/fd2fb11ae3e2649333da3aa170d04d7b3000bbdc3b270f6513382fdf4e04/jiter-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:710cc51d4ebdcd3c1f70b232c1db1ea1344a075770422bbd4bede5708335acbe", size = 122381, upload-time = "2026-06-29T13:04:13.413Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/26/80/f0b147a62c315a164ed2168908286ca302310824c218d3aae52b06c0c9a9/jiter-0.16.0-cp314-cp314-win32.whl", hash = "sha256:57b37fc887a32d44798e4d8ebfa7c9683ff3da1d5bf38f08d1bb3573ccb39106", size = 204578, upload-time = "2026-06-29T13:04:14.813Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5e/e6/4758a14304b4523a6f5adb2419340086aa3593bd4327c2b25b5948a90548/jiter-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:cbd18dd5e2df96b580487b5745adf57ef64ad89ba2d9662fc3c19386acce7db8", size = 198154, upload-time = "2026-06-29T13:04:16.272Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/26/be/41fa54a2e7ea41d6c99f1dc5b1f0fd4cb474680304b5d268dd518e81da3a/jiter-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:a32d2027a9fa67f109ff245a3252ece3ccc32cc56703e1deab6cc846a59e0585", size = 191458, upload-time = "2026-06-29T13:04:17.707Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/81/6b/59127338b86d9fe4d99418f5a15118bea778103ee0fe9d9dd7e0af174e95/jiter-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2577196f4474ef3fc4779a088a23b0897bbf86f9ea3679c372d45b8383b43207", size = 316739, upload-time = "2026-06-29T13:04:19.663Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2d/95/49461034d5388196d3dabf98748935f017b7785d8f3f5349f834bcc4ed0d/jiter-0.16.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e89e008a93c01104161c75b4988e58716b01d62307ebfe161e52a56d2a818", size = 340911, upload-time = "2026-06-29T13:04:21.257Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cd/97/a4369f2fb82cb3dda13b98622f31249b2e014b223fe64ee534413ad72294/jiter-0.16.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e2e9efbe042210df657bade597f66d6d75723e3d8f45a12ea6d8167ff8bbce3", size = 361747, upload-time = "2026-06-29T13:04:22.677Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/28/51/49b6ed456261646e1906016a6760367a28aacd3c24805e4e5fe64116c1db/jiter-0.16.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f4d9e473a5ce7d27fef8b848df4dc16e283893d3f53b4a585e72c9595f3c284", size = 460225, upload-time = "2026-06-29T13:04:24.441Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/33/b5/5689aff4f66c5b60be63106e591dbfcba2190df97d2c9c7cf052361ddb98/jiter-0.16.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d30a4a1c87713060c8d1cc59a7b6c8fb6b8ef0a6900368014c76c87922a2929", size = 373169, upload-time = "2026-06-29T13:04:25.884Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a2/96/3ae1b85ee0d6d6cab254fb7f8da018272b932bbf2d69b07e98aa2a96c746/jiter-0.16.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae96332410f866e5900d809298b1ed82735932986c672495f9701daacd80620", size = 350332, upload-time = "2026-06-29T13:04:27.302Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/32/c99d7bafd78986556c95bf60ce84c6cc98786eac56066c12d7f828bb6747/jiter-0.16.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:da3d7ec75dc83bb18bca888b5edfae0656a26849056c59e05a7728badd17e7af", size = 353377, upload-time = "2026-06-29T13:04:28.731Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0e/4b/f99a8e571287c3dec766bcc18528bbe8e8fb5365522ab5e6d64c93e87066/jiter-0.16.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ee6162b77d49a9939229df666dfa8af3e656b6701b54c4c84966d740e189264e", size = 387746, upload-time = "2026-06-29T13:04:30.319Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/75/69/c78a5b3f71040e34eb5917df26fb7ae9a2174cad1ccbf277512507c53a6e/jiter-0.16.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:63ffdbdae7d4499f4cda14eadc12ddcabef0fc0c081191bdc2247489cb698077", size = 517292, upload-time = "2026-06-29T13:04:31.709Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c2/f7/095b38eda4c70d03651c403f29a5590f16d12ddc5d544aac9f9cddf72277/jiter-0.16.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a111256a7193bea0759267b10385e5870949c239ed7b6ddbaaf57573edb38734", size = 549259, upload-time = "2026-06-29T13:04:33.721Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2e/c5/6a0207d90e5f656d95af98ebd0934f382d37674416f215aeda2ff8063e51/jiter-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:de5ba8763e56b793561f43bed197c9ea55776daa5e9a6b91eed68a909bc9cdbf", size = 206523, upload-time = "2026-06-29T13:04:35.068Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a5/31/c757d5f30a8980fd945ce7b98be10be9e4ff59c7c42f5fd86804c2e87db8/jiter-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b8a3f9a6008048fe9def7bf465180564a6e458047d2ce499149cfbe73c3ae9db", size = 200366, upload-time = "2026-06-29T13:04:36.61Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7c/a2/d88de6d313d734a544a7901353ad5db67cb38dcfcd91713b7979dafc345d/jiter-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0fa25b09b13075c46f5bc174f2690525a925a4fc2f7c82969a2bbabff22386ce", size = 190516, upload-time = "2026-06-29T13:04:38.004Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/98/ab/664fd8c4be028b2bedd3d2ff08769c4ede23d0dbc87a77c62384a0515b5d/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:f17d61a28b4b3e0e3e2ba98490c70501403b4d196f78732439160e7fd3678127", size = 303106, upload-time = "2026-06-29T13:05:07.118Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/07/421f1d5b65493a76e16027b848aba6a7d28073ae75944fa4289cc914d39f/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:96e38eea538c8ddf853a35727c7be0741c76c13f04148ac5c116222f50ece3b3", size = 304658, upload-time = "2026-06-29T13:05:08.708Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0a/db/bba1155f01a01c3c37a89425d571da751bbedf5c54247b831a04cb971798/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d284fb8d94d5855d60c44fefcab4bf966f1da6fada73992b01f6f0c9bc0c6702", size = 339719, upload-time = "2026-06-29T13:05:10.41Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/f7/18a1afcd64f35314b68c1f23afcd9994d0bc13e65cc77517afff4e83986d/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d613743df53199b1aa256a7d328340da6d7078aac7705a7db9d7a791e9cfd2", size = 343885, upload-time = "2026-06-29T13:05:12.087Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "markdown-it-py"
|
name = "markdown-it-py"
|
||||||
version = "4.2.0"
|
version = "4.2.0"
|
||||||
@@ -167,6 +289,120 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
|
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pillow"
|
||||||
|
version = "12.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684, upload-time = "2026-07-01T11:54:25.934Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487, upload-time = "2026-07-01T11:54:27.935Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433, upload-time = "2026-07-01T11:54:29.813Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889, upload-time = "2026-07-01T11:54:31.97Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109, upload-time = "2026-07-01T11:54:34.026Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736, upload-time = "2026-07-01T11:54:36.131Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129, upload-time = "2026-07-01T11:54:38.216Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562, upload-time = "2026-07-01T11:54:40.354Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439, upload-time = "2026-07-01T11:54:42.489Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287, upload-time = "2026-07-01T11:54:44.9Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691, upload-time = "2026-07-01T11:54:47.141Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185, upload-time = "2026-07-01T11:54:49.137Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736, upload-time = "2026-07-01T11:54:51.156Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435, upload-time = "2026-07-01T11:54:53.414Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262, upload-time = "2026-07-01T11:54:55.739Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344, upload-time = "2026-07-01T11:54:57.657Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131, upload-time = "2026-07-01T11:54:59.713Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757, upload-time = "2026-07-01T11:55:01.778Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962, upload-time = "2026-07-01T11:55:03.93Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171, upload-time = "2026-07-01T11:55:05.989Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116, upload-time = "2026-07-01T11:55:08.131Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209, upload-time = "2026-07-01T11:55:10.408Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707, upload-time = "2026-07-01T11:55:12.745Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995, upload-time = "2026-07-01T11:55:14.736Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503, upload-time = "2026-07-01T11:55:17.076Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956, upload-time = "2026-07-01T11:55:19.448Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855, upload-time = "2026-07-01T11:55:21.613Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642, upload-time = "2026-07-01T11:55:24.006Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281, upload-time = "2026-07-01T11:55:26.252Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716, upload-time = "2026-07-01T11:55:28.318Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063, upload-time = "2026-07-01T11:55:37.941Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549, upload-time = "2026-07-01T11:55:40.022Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331, upload-time = "2026-07-01T11:55:41.98Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370, upload-time = "2026-07-01T11:55:44.028Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147, upload-time = "2026-07-01T11:55:46.073Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659, upload-time = "2026-07-01T11:55:48.264Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439, upload-time = "2026-07-01T11:55:50.503Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577, upload-time = "2026-07-01T11:55:52.697Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394, upload-time = "2026-07-01T11:55:55.149Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375, upload-time = "2026-07-01T11:55:57.769Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048, upload-time = "2026-07-01T11:55:59.975Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006, upload-time = "2026-07-01T11:56:02.143Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509, upload-time = "2026-07-01T11:56:04.2Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167, upload-time = "2026-07-01T11:56:06.631Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237, upload-time = "2026-07-01T11:56:08.868Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047, upload-time = "2026-07-01T11:56:11.379Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440, upload-time = "2026-07-01T11:56:13.908Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895, upload-time = "2026-07-01T11:56:16.575Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384, upload-time = "2026-07-01T11:56:18.855Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537, upload-time = "2026-07-01T11:56:21.214Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491, upload-time = "2026-07-01T11:56:23.506Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pillow-heif"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pillow" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/08/dc/6ab7a469ced899fbdbbc27b00067e4da02c21ff1ce7e9c6110f14e8fea6c/pillow_heif-1.5.0.tar.gz", hash = "sha256:16b11a37b762ff42da2d36527bb5cb14bd9194c24c389ee911155d6e23c53065", size = 17114105, upload-time = "2026-07-22T14:27:50.571Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/2c/6135e0f6d76d5c5e31dbeb8ea50a1c4f2f872122055bf5c2db714a772c1b/pillow_heif-1.5.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:115d538c6b4ebbf13dcbadfa5215ffe56365a0de10849ae3a75329d36bc04ee6", size = 4743136, upload-time = "2026-07-22T14:26:49.693Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/23/3f/96c6143c006fa6188898c19cde2d131ca7f32f7f2d720514912d56cd29df/pillow_heif-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f14cde17b9288a9cbabc59c98a5928c54150ab98e3577ef2e4fd9f77f524b61", size = 4264318, upload-time = "2026-07-22T14:26:51.163Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/f9/05177861cec40bf8645e5194d5bac8a88b409f4c63a447298df7ecc2ef67/pillow_heif-1.5.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a0e2b2966418cd2fb9a2b8427f60516980e3c9146ddf0a00e443f254d7ae291", size = 6346272, upload-time = "2026-07-22T14:26:52.746Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a8/70/5f9c81a022339914ea8b8370c284e3a4327d21dcf4242b9461baa44047dc/pillow_heif-1.5.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c8032c6c2bddde009e703e045a0e17f7724925403982821ddd1a8d78559f28d", size = 5601930, upload-time = "2026-07-22T14:26:54.412Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/68/91/ce4fbd148e76daeb147a50ed761dc43d7f7d4b95c4796182b6f36e408afa/pillow_heif-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:36742b4f3f5bee57a90d9492b17ad0336b95ab7fc358172cfae144505433ac94", size = 7374394, upload-time = "2026-07-22T14:26:56.061Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/5c/32af6c25df15783a068b524648a83f854abddcd681155b6320737875c144/pillow_heif-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c5533c061cfef1ed508137bb7303093ef7b3b1ea045c70a778554f6d551ed540", size = 6633463, upload-time = "2026-07-22T14:26:57.722Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/14/0a73f6d2605a20d32bbf7d20ee033d103b49215957d0330f09c64b3d0e19/pillow_heif-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:9aa2cdc94d8d0b26dd3c16c3421273eb394c24e708899d8740dc314904ea0453", size = 6520223, upload-time = "2026-07-22T14:26:59.31Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/2c/102cfc58546ce2c00c312174ebc14d89efae8670f5c4f3c6bbe2e4f23197/pillow_heif-1.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:901121ed12b15bd1451e70ba7fca0233990ba958ec9c6f8751838402e69e1078", size = 3811009, upload-time = "2026-07-22T14:27:00.853Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/33/92/502ae6bebaf1fc48850f690858040932fb6fe0bd0de25b5e715df2517d14/pillow_heif-1.5.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:b736040d566271143c09bf87967549daf3d3090e9b4283ca2076c3383ab141d8", size = 4743134, upload-time = "2026-07-22T14:27:02.444Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e3/4f/a9f2e1525655c93e9b72cec3792a41f7d732222dad1d329fa04c227cbe28/pillow_heif-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d7c9dd6f490c433267ab9b66b63961e59a4c04c9d0e38ba66e4b6664d5e7d8", size = 4264310, upload-time = "2026-07-22T14:27:04.13Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7f/27/5f901b1d69d641b3b4e99aae6b3cbebe0cfa53188517ca7c79f86f86d4eb/pillow_heif-1.5.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cdada8d4cd5886c9ec5454b907290c4cbd420809afbf2c251120f73ba2ee1d12", size = 6346291, upload-time = "2026-07-22T14:27:05.547Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/7e/72c9fd99a74c82b08df3690077141bd16f1c9c7a3b3cd1c30e6fec4eb4a5/pillow_heif-1.5.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e87c9a51ee973b5c1896beba74ddf9b58417f5b8a52d021b493f3ef6257c5f90", size = 5601975, upload-time = "2026-07-22T14:27:07.252Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/82/ff/be2b42f3aa4ec040fca5a2bab8bac6b611d39b7a3c69c185c85ae8537a6d/pillow_heif-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ff580e1453bcf3d4965dbbfa00bfabef7784a51214413a344ce2eb652a81a987", size = 7374392, upload-time = "2026-07-22T14:27:08.804Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/34/047aba6e860dbe8e32dc948d5ed707c921168769d34379b24956322fb4c3/pillow_heif-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2cfa3761cf4fe43ee4b909963f87f05bac925d7bcff7016802f02a26574fb11e", size = 6633487, upload-time = "2026-07-22T14:27:10.815Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/0e/4b463d7183ee45624fc822f22501feb4b772e5667377d2f981a31c08310a/pillow_heif-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:139f95d1052954804b349089deb91c55ecfe344a7a21ef46c961778a45a17589", size = 6520219, upload-time = "2026-07-22T14:27:12.642Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/ce/e69cd5b9aea6e75e361d3d3a98201bb7eab31455b07a5b1fc8ec570ab2c1/pillow_heif-1.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:c6156bee6f69797a782d8ca373536dac4a7e0d4f793c9842f6bd09669fe4da0f", size = 3811013, upload-time = "2026-07-22T14:27:14.211Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/51/9b5a9352b86b7d82ad62969a84e3b4f998f6b5dc0320cec3e2d54d74d754/pillow_heif-1.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:8d96783ca1648ba8384e66b888128ca3a48d769c0e3593acf84c40ce0c791629", size = 4743156, upload-time = "2026-07-22T14:27:15.721Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/10/dc89bec0edf37df5d7609d76ce125a2f8d1e901b2d34944d64d05f497bef/pillow_heif-1.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5c50626e77510f38cb11aacb3b4bba56eff17ce39a9d85b7ee210bd81387a083", size = 4264368, upload-time = "2026-07-22T14:27:17.198Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cf/54/837e9077784df4a0ba69d3e76e9f29a4e52929c84f0e1b0d29a13437a5cc/pillow_heif-1.5.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4fb15135733626ee3fd4d7104bde1e54d4370f9f8b697a5d239b8b552b73fc7b", size = 6346432, upload-time = "2026-07-22T14:27:18.55Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/90/42f392c0513bf351ba2bca1350c457b0d7bd94f12d0e5fa74d7a9deae336/pillow_heif-1.5.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:646ddad98c8f9c5e637dbb84b6b7db905e565994e9fd6a496a36adff36ae28b3", size = 5602088, upload-time = "2026-07-22T14:27:20.112Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/4d/8070974a2e695f83fb10883b7d83c3eda635561f99993623bf111a55a984/pillow_heif-1.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b9803c1291c7c598134f16016a071ec8d9dce268ab1806c8541450c4e3972893", size = 7374543, upload-time = "2026-07-22T14:27:22.08Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4a/78/9a5abfb163b0fb44a4f624146de142ba4887e5011f965442af2c997db6bc/pillow_heif-1.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1ad925299422d4592e4b5344c03e25b27dc7a5130447603a89404d7d138b746c", size = 6633558, upload-time = "2026-07-22T14:27:23.97Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7d/bc/7bae1c1efcec25dfdca950fe1a10cd723c544269db73203019e965394015/pillow_heif-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:cfd5f8a8a7b65f2ded217c5bde4dd9365b6b67187657a121b05224841a1b92c0", size = 6703549, upload-time = "2026-07-22T14:27:25.504Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/8d/fff074683f96eb004d7159228d2fc9e2e9fe8afd899d53aa695c622487d8/pillow_heif-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:6759febf63ea31fd00756e4a828cd643d0b9f98c93f5814b0ce5baa036da50e4", size = 4017234, upload-time = "2026-07-22T14:27:27.443Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/57/04/fa5727a62b2fc37b611c7a172fc10c6719530d27bc9126de8456fac69346/pillow_heif-1.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:e2c3f47eac33d8368ea5779eb7bd8c736e7048b5897474d7e1799c073c9505e3", size = 4744030, upload-time = "2026-07-22T14:27:29.047Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4c/6f/afaccf55774c1084e5beba83f0ac201d315096f6a95d003696376184a7a3/pillow_heif-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d0881c82752d676fbe054cb0258100790ead94946fbb075ffa041bb9ae579b11", size = 4265061, upload-time = "2026-07-22T14:27:30.703Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f7/a6/c90f9fffd5cb56e213753f22a9a955525d5ad99816481ae2392ab6c7ccd8/pillow_heif-1.5.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d5810606f70d3432b55987a5e44aa26fb7598fea565d8645b017f7ed935f5e99", size = 6351671, upload-time = "2026-07-22T14:27:32.231Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/b3/e15933817e71b9fcd8865e3b64171995ed26f5ca329bf4d03f3571213c52/pillow_heif-1.5.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e737d326410e733868566c045f651b5371086a4ec294c40d39e129481d024bef", size = 5606028, upload-time = "2026-07-22T14:27:34.213Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f5/55/2665f6c781b90bb5a25595c58ede1955125f0ad2c7d81bf3c153c9e9c9d8/pillow_heif-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:522719e5dd56eae6f9f48828c08eefe0e5c02dd92f5a07780e6f3c04bdbcf880", size = 7379667, upload-time = "2026-07-22T14:27:35.883Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/77/0f0487b9f6cb1f5dff629b42ad08a7ae1997886048c4c2671e17079ad5d6/pillow_heif-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f1d281b2efee954ef78c0a473a41498d0548a32290d7250b4879ddd2480dffb8", size = 6637581, upload-time = "2026-07-22T14:27:37.67Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/b7/c65adbcab1b1c99c886c55b97b144b3092c432e7b0fedb91501c479cebe4/pillow_heif-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:65b3d619cea2d1f758cd039cddb331cb44d63768c355606ea9100cfee4421eb4", size = 6704174, upload-time = "2026-07-22T14:27:39.32Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/17/53/2a52f64f399717adae560068e9b0ae12ae3e43be2c64991e246af5aedc3f/pillow_heif-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:508fc9dd8fb4df933b666c30b60b0930270d9e072fcd90b75990166050e44656", size = 4017728, upload-time = "2026-07-22T14:27:41.102Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pluggy"
|
name = "pluggy"
|
||||||
version = "1.6.0"
|
version = "1.6.0"
|
||||||
@@ -176,6 +412,96 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pydantic"
|
||||||
|
version = "2.13.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "annotated-types" },
|
||||||
|
{ name = "pydantic-core" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
{ name = "typing-inspection" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pydantic-core"
|
||||||
|
version = "2.46.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pygments"
|
name = "pygments"
|
||||||
version = "2.20.0"
|
version = "2.20.0"
|
||||||
@@ -311,6 +637,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
|
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sniffio"
|
||||||
|
version = "1.3.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typer"
|
name = "typer"
|
||||||
version = "0.27.0"
|
version = "0.27.0"
|
||||||
@@ -334,3 +669,15 @@ sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3
|
|||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
|
{ url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-inspection"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
|
||||||
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user