Compare commits

..
12 Commits
Author SHA1 Message Date
mathias2784 0d6272b4c3 E2e workflow (#21)
Tests / test_and_lint (push) Successful in 49s
Reviewed-on: #21
Co-authored-by: Mathias Scheider <mathias2784@web.de>
2026-10-18 12:02:18 +00:00
mathias2784 ad7dbfdd11 E2e workflow (#20)
Tests / test_and_lint (push) Successful in 34s
Reviewed-on: #20
Co-authored-by: Mathias Scheider <mathias2784@web.de>
2026-10-18 07:24:49 +00:00
mathias2784 ff588f899e Llm session refactor (#19)
Tests / test_and_lint (push) Successful in 34s
Reviewed-on: #19
Co-authored-by: Mathias <mathias2784@web.de>
2026-10-18 06:29:37 +00:00
mathias2784 b6be043426 Mypy integration (#18)
Tests / test_and_lint (push) Successful in 32s
Reviewed-on: #18
Co-authored-by: Mathias <mathias2784@web.de>
2026-10-10 18:37:09 +00:00
mathias2784 b9632d46b2 Metadata writer (#17)
Tests / test_and_lint (push) Successful in 27s
Reviewed-on: #17
Co-authored-by: Mathias <mathias2784@web.de>
2026-10-10 12:22:15 +00:00
mathias2784 6a12144e39 Logging (#16)
Tests / test_and_lint (push) Successful in 12s
Reviewed-on: #16
Co-authored-by: Mathias <mathias2784@web.de>
2026-10-03 08:24:17 +00:00
mathias2784 401488fa7e Llm client quality (#14)
Tests / test_and_lint (push) Successful in 14s
Reviewed-on: #14
Co-authored-by: Mathias Scheider <mathias2784@web.de>
2026-10-02 21:31:21 +00:00
mathias2784 419fb46c3d refactor: simplify CLI command logic by extracting helper functions (#13)
Tests / test_and_lint (push) Successful in 14s
Reviewed-on: #13
Co-authored-by: Mathias <mathias2784@web.de>
2026-10-02 12:03:20 +00:00
mathias2784 ce2da9d953 Add tests for LlmSession and LlmSessionCaptionsAndKeywords in both integration and mocked scenarios (#12)
Tests / test_and_lint (push) Successful in 13s
Reviewed-on: #12
Co-authored-by: Mathias Scheider <mathias2784@web.de>
2026-10-02 09:21:38 +00:00
mathias2784 906707de55 first successful run of photo llm (#11)
Tests / test_and_lint (push) Successful in 11s
Reviewed-on: #11
Co-authored-by: Mathias Scheider <mathias2784@web.de>
2026-10-02 08:16:20 +00:00
mathias2784 0c33153c98 Photo basics (#10)
Tests / test_and_lint (push) Successful in 10s
Reviewed-on: #10
Co-authored-by: Mathias Scheider <mathias2784@web.de>
2026-09-30 21:01:20 +00:00
mathias2784 d9758c598d add ci yaml (#9)
Tests / test_and_lint (push) Successful in 7s
Reviewed-on: #9
Co-authored-by: Mathias Scheider <mathias2784@web.de>
2026-09-30 18:49:48 +00:00
33 changed files with 3982 additions and 29 deletions
+10
View File
@@ -0,0 +1,10 @@
LLM_API_BASE_URL = "http://localhost:1234/v1"
LLM_API_KEY = "not-relevant"
LLM_DEFAULT_TEMPERATURE = 1.0
METADATA_PREFIX = "photools|"
LOG_LEVEL=INFO
LLM_REQUEST_TIMEOUT_SECONDS=10
LLM_MAX_COMPLETION_TOKENS=1024
LLM_MAX_RETRIES=5
LLM_MAX_KEYWORDS=15
+37
View File
@@ -0,0 +1,37 @@
---
name: Tests
on: [push]
jobs:
test_and_lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.14"
- name: Install uv
run: pip install uv
- name: Install dependencies
run: uv sync --frozen
- name: copy env
run: cp .env.template .env
- name: Install exiftool (system dependency)
run: |
if command -v exiftool >/dev/null 2>&1; then
echo "exiftool already installed"
elif command -v sudo >/dev/null 2>&1; then
sudo apt-get update && sudo apt-get install -y --no-install-recommends libimage-exiftool-perl
else
apt-get update && apt-get install -y --no-install-recommends libimage-exiftool-perl
fi
- name: Run pytest
run: uv run pytest .
- name: Lint (ruff)
run: uv run ruff check .
- name: Typing (mypy)
run: uv run mypy .
- name: Check formatting (black)
run: uv run black --check . --extend-exclude .venv
+1
View File
@@ -10,3 +10,4 @@ wheels/
.venv
.env
logs/
+42
View File
@@ -0,0 +1,42 @@
# Agent Instructions: PhotoTools3
## Environment Setup
Requires a `.env` file with the following variables:
- `llm_api_key`: API key for the LLM provider.
- `llm_api_base_url`: Base URL for the LLM API.
- `llm_default_temperature`: Float value for temperature.
## System Dependencies
- `exiftool` binary: required by `pyexiftool` at runtime, not installable via pip/uv.
- Arch: `perl-image-exiftool`
- Debian/Ubuntu: `libimage-exiftool-perl`
## Development Toolchain
- **Dependency Management**: Use `uv`.
- **Python Version**: Requires Python `>=3.14`.
- **Testing**: Run `pytest` to execute the test suite in `test/`.
- **Linting & Formatting**: Use `ruff` and `black`.
## CLI Usage
The primary entry point is the `photools3` command:
- `photools3 keywords <image_path>`: Extracts keywords and captions from a single image.
## Architecture & Logic
- **`src/photools3/photo.py`**: Core image processing logic, including resizing, RGB conversion, and generating base64 JPEG encodings specifically for LLM vision compatibility.
- **`src/photools3/llm_session.py`**: Manages the workflow of sending processed images to the LLM and parsing results.
- **Configuration**: The list of keywords to extract is maintained in `config/keywords.txt`.
## Code style
- Avoid overly verbose docstrings or comments. Be precise!
## Comunication
- When done with code modifications, give a one-liner for the currently unstaged changes that can be used as a commit message.
+101
View File
@@ -0,0 +1,101 @@
# PhotoTools3
PhotoTools3 uses an OpenAI-compatible vision API to generate photo captions and
keywords, then writes the results to photo metadata. It can also assess photo
quality.
## Setup
Install the Python dependencies with `uv` and make sure ExifTool is installed and
available on `PATH` (it is a system dependency, not a Python package):
```bash
uv sync
```
Create a `.env` file in the project root with the LLM connection details and the
prefix used to identify metadata written by PhotoTools3:
```dotenv
llm_api_base_url=http://localhost:1234/v1
llm_api_key=your-api-key
llm_default_temperature=0.2
metadata_prefix=photools3:
LOG_LEVEL=DEBUG
LLM_REQUEST_TIMEOUT_SECONDS=10
LLM_MAX_COMPLETION_TOKENS=1024
LLM_MAX_RETRIES=5
LLM_MAX_KEYWORDS=15
```
Keyword suggestions are restricted to the vocabulary in `config/keywords.txt`.
`LOG_LEVEL=DEBUG` enables detailed per-request and per-photo timings.
`LLM_MAX_RETRIES` controls retries for malformed or structurally invalid
responses; `5` means up to 6 requests for a photo. A valid response with
unsupported keywords gets at most one targeted correction retry, then remaining
invalid keywords are discarded.
Each request is limited by `LLM_REQUEST_TIMEOUT_SECONDS`; a timeout reloads the
LM Studio model before retrying. `LLM_MAX_COMPLETION_TOKENS` caps the generated
response length. When all retries are exhausted, the model is reloaded before
the failed photo is reported. `LLM_MAX_KEYWORDS` sets the maximum number of
distinct keywords requested and stored per photo; excess keywords are discarded.
## Generate metadata
The `metadata-magic` command accepts one or more image paths and/or directories.
The default mode is `both`, which generates and writes a caption and keywords:
```bash
uv run photools3 metadata-magic photo.dng
```
Choose a mode with `--mode`:
```bash
# Generate only keywords
uv run photools3 metadata-magic photo.dng --mode keywords
# Generate only a caption
uv run photools3 metadata-magic photo.dng --mode captions
# Explicitly request both (the default)
uv run photools3 metadata-magic photo.dng --mode both
```
Pass multiple files to process them sequentially as a batch:
```bash
uv run photools3 metadata-magic photo1.dng photo2.jpg --mode both
```
Directories are scanned at the top level by default. Add `--recursive` to
include supported images in subdirectories. You can combine directories and
individual file paths:
```bash
uv run photools3 metadata-magic ./photos --recursive --mode keywords
uv run photools3 metadata-magic ./photos photo-to-include.dng --mode captions
```
The command reports each processed file and exits with a non-zero status if any
file fails. In a batch, it continues processing the remaining files after an
individual failure. Generated metadata is written using ExifTool and XMP
sidecars.
## Assess photo quality
The `quality` command reports an assessment without changing photo metadata:
```bash
uv run photools3 quality photo.dng
```
## Help
Show the available commands and options with:
```bash
uv run photools3 --help
uv run photools3 metadata-magic --help
uv run photools3 quality --help
```
+371
View File
@@ -0,0 +1,371 @@
architektur
architektur|gebäude
architektur|bauwerke
architektur|architecture
architektur|brücke
architektur|brücke|bridge
architektur|burg
architektur|burg|castle
architektur|dach
architektur|dach|roof
architektur|dächer
architektur|garten
architektur|garten|garden
architektur|gleis
architektur|gleis|rail
architektur|industrie
architektur|kirche
architektur|kirche|church
architektur|leuchtturm
architektur|leuchtturm|lighhouse
architektur|levada
architektur|modell
architektur|park
architektur|ruinen
architektur|ruinen|ruins
architektur|siedlung
architektur|siedlung|dorf ortschaft
architektur|siedlung|village
architektur|skyline
architektur|stadt
architektur|stadt|city
architektur|strasse
architektur|strasse|street
architektur|fenster
architektur|fenster|window
architektur|tür
architektur|tür|door
architektur|treppe
architektur|treppe|stairs
architektur|turm
architektur|turm|tower
architektur|hafen
architektur|hafen|harbor
architektur|hafen|port
architektur|bahnhof
architektur|bahnhof|train station
architektur|museum
essen&trinken
essen&trinken|Lebensmittel
essen&trinken|food
essen&trinken|braten
essen&trinken|buffett
essen&trinken|burger
essen&trinken|kaffee
essen&trinken|kaffee|coffee
essen&trinken|shrimps
essen&trinken|tapas
essen&trinken|bier
essen&trinken|bier|beer
essen&trinken|wein
essen&trinken|wein|wine
essen&trinken|obst
essen&trinken|obst|fruit
essen&trinken|gemüse
essen&trinken|gemüse|vegetables
essen&trinken|kuchen
essen&trinken|kuchen|cake
essen&trinken|dessert
himmel/wetter
himmel/wetter|weather
himmel/wetter|sky
himmel/wetter|abendrot
himmel/wetter|abendrot|dusk
himmel/wetter|blauer himmel
himmel/wetter|blauer himmel|blue sky
himmel/wetter|diesig
himmel/wetter|diesig|hazy
himmel/wetter|diesig|misty
himmel/wetter|eis
himmel/wetter|eis|ice
himmel/wetter|grau
himmel/wetter|grau|grey
himmel/wetter|mond
himmel/wetter|mond|moon
himmel/wetter|morgengrauen
himmel/wetter|morgengrauen|dawn
himmel/wetter|morgenrot
himmel/wetter|morgenrot|dawn
himmel/wetter|nebel
himmel/wetter|nebel|fog
himmel/wetter|regen
himmel/wetter|regen|rain
himmel/wetter|regenbogen
himmel/wetter|regenbogen|rainbow
himmel/wetter|schnee
himmel/wetter|schnee|snow
himmel/wetter|sonne
himmel/wetter|sonne|sun
himmel/wetter|sonne|sonnenaufgang
himmel/wetter|sonne|sonnenaufgang|sunrise
himmel/wetter|sonne|sonnenstrahlen
himmel/wetter|sonne|sonnenstrahlen|sun beams
himmel/wetter|sonne|sonnenstrahlen|light rays
himmel/wetter|sonne|sonnenuntergang
himmel/wetter|sonne|sonnenuntergang|sunset
himmel/wetter|sterne
himmel/wetter|sterne|stars
himmel/wetter|windig
himmel/wetter|windig|windy
himmel/wetter|wolken
himmel/wetter|wolken|clouds
himmel/wetter|gewitter
himmel/wetter|gewitter|thunderstorm
himmel/wetter|blitz
himmel/wetter|blitz|lightning
himmel/wetter|frost
jahreszeit
jahreszeit|season
jahreszeit|frühling
jahreszeit|frühling|spring
jahreszeit|sommer
jahreszeit|sommer|summer
jahreszeit|herbst
jahreszeit|herbst|autumn
jahreszeit|herbst|fall
jahreszeit|winter
landschaft
landschaft|landscape
landschaft|berge
landschaft|berge|mountains
landschaft|dünen
landschaft|dünen|dunes
landschaft|dünen|sand
landschaft|feld
landschaft|feld|field
landschaft|feld|meadow
landschaft|felsen
landschaft|felsen|rock
landschaft|höhle
landschaft|höhle|cave
landschaft|insel
landschaft|insel|island
landschaft|klippen
landschaft|klippen|cliffs
landschaft|küste
landschaft|küste|coast
landschaft|steinbruch
landschaft|steinbruch|quarry
landschaft|strand
landschaft|strand|beach
landschaft|vulkan
landschaft|vulkan|vulcano
landschaft|wald
landschaft|wald|forest
landschaft|wald|wood
landschaft|wasser
landschaft|wasser|water
landschaft|wasser|bach
landschaft|wasser|bach|creek
landschaft|wasser|brandung
landschaft|wasser|brandung|breakers
landschaft|wasser|brandung|surf
landschaft|wasser|fluss
landschaft|wasser|fluss|river
landschaft|wasser|hochwasser
landschaft|wasser|hochwasser|flood
landschaft|wasser|hochwasser|high water
landschaft|wasser|meer
landschaft|wasser|meer|sea
landschaft|wasser|meer|ocean
landschaft|wasser|see
landschaft|wasser|see|lake
landschaft|wasser|wasserfall
landschaft|wasser|wasserfall|waterfall
landschaft|weinberge
landschaft|weinberge|vineyard
landschaft|wüste
landschaft|wüste|desert
landschaft|hügel
landschaft|hügel|hills
landschaft|tal
landschaft|tal|valley
landschaft|schlucht
landschaft|schlucht|canyon
landschaft|schlucht|gorge
landschaft|gipfel
landschaft|gipfel|summit
landschaft|gipfel|peak
nacht
nacht|night
nacht|langzeitbelichtung
nacht|langzeitbelichtung|long exposure
nacht|lichtspuren
nacht|lichtspuren|light trails
nacht|nachtaufnahme
nacht|nachtaufnahme|night shot
nacht|stadtlichter
nacht|stadtlichter|city lights
personen
personen|auge
personen|auge|eye
personen|geste
personen|geste|gesture
personen|gruppenbild
personen|gruppenbild|group
personen|menschenmenge
personen|menschenmenge|crowd
personen|portrait
personen|pose
personen|quatsch
personen|quatsch|silly
personen|verkleidung
personen|kind
personen|kind|kinder
personen|kind|child
personen|kind|children
personen|paar
personen|paar|couple
personen|silhouette
personen|schatten
personen|schatten|shadow
pflanze
pflanze|plant
pflanze|bäume
pflanze|bäume|trees
pflanze|blumen
pflanze|blumen|flower
pflanze|blüten
pflanze|blüten|flower
pflanze|blüten|bloomy
pflanze|grass
pflanze|grass|gras
pflanze|kaktus
pflanze|kaktus|cactus
pflanze|laub
pflanze|laub|foliage
pflanze|moos
pflanze|moos|moss
pflanze|palmen
pflanze|palmen|palms
pflanze|pilze
pflanze|pilze|mushrooms
pflanze|schilf
pflanze|schilf|reed
pflanze|sonnenblumen
pflanze|sonnenblumen|sunflower
pflanze|sonnentau
pflanze|sonnentau|sundew
pflanze|venusfliegenfalle
pflanze|venusfliegenfalle|venus trap
pflanze|farn
pflanze|farn|fern
pflanze|strauch
pflanze|strauch|bush
pflanze|strauch|shrub
pflanze|efeu
pflanze|efeu|ivy
pflanze|hecke
pflanze|hecke|hedge
sport
sport|fussball
sport|fussball|football
sport|fussball|soccer
sport|schwimmen
sport|schwimmen|swimming
sport|klettern
sport|klettern|climbing
sport|wandern
sport|wandern|hiking
sport|radfahren
sport|radfahren|cycling
sport|ski
sport|ski|skiing
stil
stil|style
stil|schwarzweiss
stil|schwarzweiss|black and white
stil|schwarzweiss|bw
stil|vintage
stil|hdr
stil|panorama
technik
technik|technology
technik|auto
technik|auto|car
technik|eisenbahn
technik|eisenbahn|train
technik|fahrrad
technik|fahrrad|bike
technik|fahrrad|bicycle
technik|flugzeug
technik|flugzeug|plane
technik|handy
technik|handy|smartphone
technik|handy|cell phone
technik|kamera
technik|kamera|camera
technik|musikinstrument
technik|musikinstrument|musical instrument
technik|musikinstrument|Gitarre
technik|musikinstrument|Gitarre|guitar
technik|musikinstrument|Gitarren Effektgerät
technik|musikinstrument|Gitarrenverstärker
technik|musikinstrument|Gitarrenverstärker|guitar amp
technik|objektiv
technik|objektiv|lens
technik|schiff
technik|schiff|ship
technik|seilbahn
technik|seilbahn|teleferico
technik|seilbahn|cable car
technik|motorrad
technik|motorrad|motorcycle
technik|lastwagen
technik|lastwagen|truck
technik|bus
technik|helikopter
technik|helikopter|helicopter
technik|drohne
technik|drohne|drone
technik|windrad
technik|windrad|wind turbine
tier
tier|animal
tier|elefant
tier|elefant|elephant
tier|fisch
tier|fisch|fish
tier|insekten
tier|insekten|insects
tier|insekten|biene
tier|insekten|biene|bee
tier|katze
tier|katze|cat
tier|nager
tier|nager|gnawer
tier|reptil
tier|reptil|reptile
tier|rind
tier|rind|cattle
tier|Rot$DamWild
tier|Rot$DamWild|deer
tier|spinne
tier|spinne|spider
tier|tiger
tier|vogel
tier|vogel|bird
tier|waschbär
tier|waschbär|raccoon
tier|Wildschwein
tier|Wildschwein|boar
tier|ZiegeBockSchaf
tier|ZiegeBockSchaf|goat
tier|hund
tier|hund|dog
tier|pferd
tier|pferd|horse
tier|eichhörnchen
tier|eichhörnchen|squirrel
tier|fuchs
tier|fuchs|fox
tier|seevogel
tier|seevogel|seabird
tier|eidechse
tier|eidechse|lizard
tier|frosch
tier|frosch|frog
tier|schmetterling
tier|schmetterling|butterfly
tier|libelle
tier|libelle|dragonfly
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+22
View File
@@ -8,10 +8,17 @@ authors = [
]
requires-python = ">=3.14"
dependencies = [
"black>=26.5.1",
"mypy>=2.4.0",
"openai>=3.22.0",
"pillow>=12.3.0",
"pydantic>=2.13.5",
"pydantic-settings>=2.15.0",
"pyexiftool>=0.5.6",
"pytest>=9.1.1",
"ruff>=0.16.9",
"typer>=0.12.0",
"utils>=0.1.3",
]
[project.scripts]
@@ -20,3 +27,18 @@ photools3 = "photools3:main"
[build-system]
requires = ["uv_build>=0.12.19,<0.13.0"]
build-backend = "uv_build"
[tool.uv.sources]
utils = { index = "gitea" }
[tool.black]
target-version = [ "py314" ]
[[tool.mypy.overrides]]
module = ["exiftool", "exiftool.*", "utils", "utils.*"]
ignore_missing_imports = true
[[tool.uv.index]]
name = "gitea"
url = "https://gitea.schneiderserver.site/api/packages/mathias2784/pypi/simple"
explicit = true
+5 -2
View File
@@ -1,2 +1,5 @@
def main() -> None:
print("Hello from photools3!")
from .cli import app
def main():
app()
+4
View File
@@ -0,0 +1,4 @@
from .cli import app
if __name__ == "__main__":
app()
+163
View File
@@ -0,0 +1,163 @@
import logging
from pathlib import Path
from typing import Annotated
import typer
from PIL import Image
from utils.logging_config import LoggingSettings
from photools3.llm_session import LlmSessionQuality
from photools3.photo import Photo
from photools3.processing import (
ProcessingMode,
process_batch,
process_photo,
)
from photools3.settings import settings
app = typer.Typer(help="PhotoTools3: A CLI for image processing.")
LoggingSettings(LOG_LEVEL=settings.log_level)
for name in (
"uvicorn",
"uvicorn.error",
"uvicorn.access",
"PIL",
"openai",
"httpx2",
"httpcore2",
):
logging.getLogger(name).setLevel(logging.WARNING)
logger = logging.getLogger(__name__)
@app.callback(invoke_without_command=True)
def main(
ctx: typer.Context,
version: bool | None = typer.Option(None, "--version", is_eager=True),
):
"""Main entry point for PhotoTools3 CLI."""
if version:
typer.echo("photools3 version 0.1.0")
raise typer.Exit()
if ctx.invoked_subcommand is None:
typer.echo(ctx.get_help())
raise typer.Exit()
@app.command("metadata-magic")
def metadata_magic(
paths: Annotated[
list[Path], typer.Argument(..., help="Photo file(s) and/or directory paths")
],
mode: Annotated[
ProcessingMode,
typer.Option(
"--mode",
case_sensitive=False,
help="Metadata to generate and write",
),
] = ProcessingMode.BOTH,
recursive: Annotated[
bool,
typer.Option(
"--recursive/--no-recursive",
help="Recursively discover photos inside input directories",
),
] = False,
):
"""Generate and write keywords, captions, or both for photos."""
_run_processing(paths, mode, recursive)
@app.command("quality")
def quality(
file_path: Annotated[Path, typer.Argument(..., help="Path to the image file")],
):
"""Assess a photo's quality without changing its metadata."""
try:
with LlmSessionQuality() as session, Photo(file_path) as photo:
result = session.call([photo])
logger.info("Quality assessment for %s: %s", file_path, result)
except FileNotFoundError:
logger.error("Photo not found: %s", file_path)
raise typer.Exit(code=1) from None
except Exception as exc:
logger.error("Error assessing photo %s: %s", file_path, exc)
raise typer.Exit(code=1) from exc
def _run_processing(inputs: list[Path], mode: ProcessingMode, recursive: bool) -> None:
photo_paths = _expand_photo_paths(inputs, recursive)
if not photo_paths:
logger.error("No supported photo files found in the given input paths.")
raise typer.Exit(code=1)
logger.info("Processing %d photo(s) in %s mode", len(photo_paths), mode.value)
if len(photo_paths) == 1:
path = photo_paths[0]
try:
process_photo(path, mode)
except FileNotFoundError:
logger.error("Photo not found: %s", path)
raise typer.Exit(code=1) from None
except Exception as exc:
logger.error("Failed to process %s: %s", path, exc)
raise typer.Exit(code=1) from exc
logger.info("Processed: %s", path)
return
try:
outcomes = process_batch(photo_paths, mode)
except Exception as exc:
logger.error("Batch processing failed: %s", exc)
raise typer.Exit(code=1) from exc
failures = 0
for outcome in outcomes:
if outcome.succeeded:
logger.info("Processed: %s", outcome.path)
else:
failures += 1
logger.error("Failed to process %s: %s", outcome.path, outcome.error)
logger.info(
"Batch complete: %d succeeded, %d failed", len(outcomes) - failures, failures
)
if failures:
raise typer.Exit(code=1)
def _expand_photo_paths(inputs: list[Path], recursive: bool) -> list[Path]:
"""Expand directories to supported image files, preserving input order."""
supported_extensions = {
".dng",
*(extension.lower() for extension in Image.registered_extensions()),
}
discovered: list[Path] = []
for path in inputs:
if not path.is_dir():
discovered.append(path)
continue
try:
candidates = path.rglob("*") if recursive else path.iterdir()
discovered.extend(
sorted(
candidate
for candidate in candidates
if candidate.is_file()
and candidate.suffix.lower() in supported_extensions
)
)
except OSError as exc:
logger.error("Could not read directory %s: %s", path, exc)
raise typer.Exit(code=1) from exc
# Avoid processing the same exact path more than once while retaining order.
return list(dict.fromkeys(discovered))
if __name__ == "__main__":
app()
+332 -7
View File
@@ -1,25 +1,108 @@
import json
import logging
import re
import time
import openai
from openai import OpenAI
from openai.types.chat import ChatCompletion
from pydantic import BaseModel, ValidationError
from photools3.models import LlmRequest
from photools3.settings import Settings
from photools3.lm_studio import LMStudioModelManager
from photools3.models import (
LlmMessageContent,
LlmMessageRole,
LlmMessageType,
LlmRequest,
LlmRequestMessage,
LlmResponseCaption,
LlmResponseCaptionsAndKeywords,
LlmResponseGeneral,
LlmResponseKeywords,
LlmResponseQuality,
load_valid_keywords,
)
from photools3.photo import Photo
from photools3.settings import settings
settings = Settings()
logger = logging.getLogger(__name__)
def _clean_llm_response(txt: str):
# remove ```json ... ``` wrap
txt = re.sub(r"^```json(.*)```$", r"\1", txt, flags=re.DOTALL).strip()
return txt
def _correction_prompt(instruction: str) -> LlmRequestMessage:
return LlmRequestMessage(
role=LlmMessageRole.user,
content=instruction,
)
def _request_with_correction(base_request: LlmRequest, instruction: str) -> LlmRequest:
return base_request.model_copy(
update={"messages": [*base_request.messages, _correction_prompt(instruction)]}
)
def _validation_summary(exception: ValidationError) -> str:
return "; ".join(
f"{'.'.join(map(str, error['loc']))}: {error['msg']}"
for error in exception.errors(include_input=False)
)
class LlmSession:
base_url: str = str(settings.llm_api_base_url)
api_key: str = settings.llm_api_key
response_model: type[BaseModel] = LlmResponseGeneral
prompt: str = ""
def __init__(self, system_prompt: str):
def __init__(
self, system_prompt: str, temperature: float = settings.llm_default_temperature
):
self._models: list = []
self._model_id: str | None = None
self.system_prompt = system_prompt
self.temperature = temperature
self.client = OpenAI(
base_url=self.base_url,
api_key=self.api_key,
max_retries=0,
)
self._model_manager = LMStudioModelManager(self.base_url, self.api_key)
def close(self):
self.client.close()
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
self.close()
def flush_model(self):
"""Unload and reload this session's model through LM Studio."""
if self._model_id is None:
self._model_id = self.models[0].id
self._model_manager.reload_model(self._model_id)
def client_chat_completion_create(self, request: LlmRequest, choice_no: int = 0):
response = self.client.chat.completions.create(
**request.model_dump(),
timeout=settings.llm_request_timeout_seconds,
)
return response.choices[choice_no].message.content
def is_api_reachable(self) -> bool:
try:
_ = self.client.models.list().data
return True
except openai.APIConnectionError:
return False
@property
def models(self):
@@ -27,5 +110,247 @@ class LlmSession:
self._models = self.client.models.list().data
return self._models
def call_raw(self, request: LlmRequest) -> ChatCompletion:
return self.client.chat.completions.create(**request.model_dump())
def call(
self,
photos: list[Photo],
model_id=None,
max_retries: int | None = None,
prompt: str | None = None,
):
retry_limit = settings.llm_max_retries if max_retries is None else max_retries
if retry_limit < 0:
raise ValueError("max_retries must be greater than or equal to zero")
if model_id is None:
if self._model_id is None:
self._model_id = self.models[0].id
model_id = self._model_id
else:
self._model_id = model_id
base_request = self._build_request(prompt or self.prompt, photos, model_id)
request = base_request
tag_correction_used = False
valid_keywords = load_valid_keywords()
max_keywords = settings.llm_max_keywords
for attempt in range(retry_limit + 1):
request_started = time.perf_counter()
try:
result_raw = self.client_chat_completion_create(request)
except openai.APITimeoutError as exc:
logger.warning(
"LLM request timed out after %.2f seconds; reloading model",
time.perf_counter() - request_started,
)
self.flush_model()
if attempt < retry_limit:
continue
raise ValueError(
f"LLM request timed out after {retry_limit + 1} attempts"
) from exc
except Exception:
logger.debug(
"LLM request attempt %d/%d failed after %.2f seconds",
attempt + 1,
retry_limit + 1,
time.perf_counter() - request_started,
)
raise
logger.debug(
"LLM request attempt %d/%d completed in %.2f seconds",
attempt + 1,
retry_limit + 1,
time.perf_counter() - request_started,
)
try:
response_data = json.loads(_clean_llm_response(result_raw))
except (json.JSONDecodeError, TypeError) as exc:
if attempt < retry_limit:
logger.info("Chatbot response was not valid JSON; retrying")
request = _request_with_correction(
base_request,
"Return one complete JSON object matching the schema. "
f"The previous response was invalid: {exc}",
)
continue
logger.warning(
"LLM retry limit reached; reloading model before failing"
)
self.flush_model()
raise ValueError(
f"Failed to parse LLM response after {retry_limit} retries: {exc}"
) from exc
if (
isinstance(response_data, dict)
and "tags" in self.response_model.model_fields
and isinstance(response_data.get("tags"), list)
):
tags = response_data["tags"]
considered_tags = tags[:max_keywords]
invalid_tags = [
tag
for tag in considered_tags
if not isinstance(tag, str) or tag not in valid_keywords
]
if invalid_tags and not tag_correction_used and attempt < retry_limit:
tag_correction_used = True
logger.info(
"LLM returned unsupported keywords; requesting one correction"
)
request = _request_with_correction(
base_request,
f"These keywords are not allowed: {invalid_tags!r}. "
"Replace them with exact allowed keywords when sensible; "
f"otherwise omit them. Return at most {max_keywords} "
"distinct keywords and do not list the vocabulary.",
)
continue
if len(tags) > max_keywords:
logger.info(
"Discarding keywords beyond configured limit of %d",
max_keywords,
)
if invalid_tags:
logger.warning("Discarding unsupported keywords: %s", invalid_tags)
response_data = dict(response_data)
valid_tags = [
tag
for tag in tags
if isinstance(tag, str) and tag in valid_keywords
]
response_data["tags"] = list(dict.fromkeys(valid_tags))[:max_keywords]
try:
return self.response_model.model_validate(response_data)
except ValidationError as exc:
if attempt < retry_limit:
logger.info("LLM response did not match the schema; retrying")
request = _request_with_correction(
base_request,
"Return one complete JSON object matching the schema. "
f"Validation issues: {_validation_summary(exc)}",
)
continue
logger.warning(
"LLM retry limit reached; reloading model before failing"
)
self.flush_model()
raise ValueError(
f"Failed to validate LLM response after {retry_limit} retries: "
f"{_validation_summary(exc)}"
) from exc
def _build_request(
self,
prompt: str,
photos: list[Photo],
model_id: str,
) -> LlmRequest:
"""Build a valid request that complies with openai API"""
return LlmRequest(
model=model_id,
temperature=self.temperature,
max_tokens=settings.llm_max_completion_tokens,
messages=[
# System prompt
LlmRequestMessage(
role=LlmMessageRole.system,
content=self.system_prompt,
),
# User prompt
LlmRequestMessage(
role=LlmMessageRole.user,
content=[
# Text prompt
LlmMessageContent(
type=LlmMessageType.text,
text=prompt,
),
# Image contents
*[
LlmMessageContent(
type=LlmMessageType.image_url,
image_url=photo.to_llm_image_url(),
)
for photo in photos
],
],
),
],
)
class LlmSessionCaptionsAndKeywords(LlmSession):
response_model = LlmResponseCaptionsAndKeywords
prompt = "Give me a caption and keywords for the following photo"
def __init__(self, temperature: float = settings.llm_default_temperature):
super().__init__(
system_prompt=(
"You are a helpful photographer's assistant that helps to write **captions** and **keywords** to photos:\n"
"Answer in German.\n"
f"Choose at most {settings.llm_max_keywords} distinct, relevant keywords. "
"Do not repeat or enumerate the allowed-keyword list.\n"
"Output must be a JSON of the following schema:\n"
f"{self.response_model.model_json_schema()}"
),
temperature=temperature,
)
class LlmSessionCaption(LlmSession):
response_model = LlmResponseCaption
prompt = "Write a caption for the following photo"
def __init__(self, temperature: float = settings.llm_default_temperature):
super().__init__(
system_prompt=(
"You are a helpful photographer's assistant that writes captions "
"for photos. Answer in German.\n"
"Output must be a JSON of the following schema:\n"
f"{self.response_model.model_json_schema()}"
),
temperature=temperature,
)
class LlmSessionKeywords(LlmSession):
response_model = LlmResponseKeywords
prompt = "Choose appropriate keywords for the following photo"
def __init__(self, temperature: float = settings.llm_default_temperature):
super().__init__(
system_prompt=(
"You are a helpful photographer's assistant that chooses keywords "
"for photos. Answer in German.\n"
f"Choose at most {settings.llm_max_keywords} distinct, relevant keywords. "
"Do not repeat or enumerate the allowed-keyword list.\n"
"Output must be a JSON of the following schema:\n"
f"{self.response_model.model_json_schema()}"
),
temperature=temperature,
)
class LlmSessionQuality(LlmSession):
response_model = LlmResponseQuality
prompt = "Assess the quality of the following photos to help me decide which ones to sort out"
def __init__(self, temperature: float = settings.llm_default_temperature):
super().__init__(
system_prompt=(
"You are a helpful photographer's assistant that helps to identify "
"photos that can be sorted out due to qulity issues:\n"
"Output must be a JSON of the following schema:\n"
f"{self.response_model.model_json_schema()}"
),
temperature=temperature,
)
+135
View File
@@ -0,0 +1,135 @@
import json
import logging
import time
from urllib.error import HTTPError, URLError
from urllib.parse import urlsplit, urlunsplit
from urllib.request import Request, urlopen
logger = logging.getLogger(__name__)
_LOAD_CONFIG_KEYS = (
"context_length",
"eval_batch_size",
"flash_attention",
"num_experts",
"offload_kv_cache_to_gpu",
)
class LMStudioError(RuntimeError):
"""Raised when LM Studio model management fails."""
class LMStudioModelManager:
"""Unload and reload a model through LM Studio's native REST API."""
def __init__(self, openai_base_url: str, api_key: str, timeout: float = 600):
parsed_url = urlsplit(openai_base_url)
path = parsed_url.path.rstrip("/")
path = path.removesuffix("/v1")
root_url = urlunsplit(
(parsed_url.scheme, parsed_url.netloc, path, "", "")
).rstrip("/")
self.api_base_url = f"{root_url}/api/v1"
self.api_key = api_key
self.timeout = timeout
def reload_model(self, model_id: str) -> None:
"""Unload the matching loaded instance and reload it with its config."""
started = time.perf_counter()
models_response = self._request("GET", "/models")
model, loaded_instance = self._find_model(models_response, model_id)
model_key = model["key"]
if loaded_instance is not None:
self._request(
"POST",
"/models/unload",
{"instance_id": loaded_instance["id"]},
)
load_config = loaded_instance.get("config", {})
else:
load_config = {}
load_body = {"model": model_key}
load_body.update(
{
key: load_config[key]
for key in _LOAD_CONFIG_KEYS
if key in load_config and load_config[key] is not None
}
)
self._request("POST", "/models/load", load_body)
logger.info(
"Reloaded LM Studio model %s in %.1f seconds",
model_key,
time.perf_counter() - started,
)
@staticmethod
def _find_model(models_response: dict, model_id: str) -> tuple[dict, dict | None]:
models = models_response.get("models")
if not isinstance(models, list):
raise LMStudioError("LM Studio model list response has no models array")
for model in models:
loaded_instances = model.get("loaded_instances", [])
for instance in loaded_instances:
if instance.get("id") == model_id:
return model, instance
matching_models = [model for model in models if model.get("key") == model_id]
if len(matching_models) != 1:
raise LMStudioError(
f"Could not uniquely identify LM Studio model {model_id!r}"
)
model = matching_models[0]
loaded_instances = model.get("loaded_instances", [])
if len(loaded_instances) > 1:
raise LMStudioError(
f"Multiple loaded instances found for LM Studio model {model_id!r}"
)
loaded_instance = loaded_instances[0] if loaded_instances else None
return model, loaded_instance
def _request(self, method: str, path: str, body: dict | None = None) -> dict:
data = None if body is None else json.dumps(body).encode("utf-8")
headers = {"Accept": "application/json"}
if data is not None:
headers["Content-Type"] = "application/json"
if self.api_key:
headers["Authorization"] = f"Bearer {self.api_key}"
request = Request(
f"{self.api_base_url}{path}",
data=data,
headers=headers,
method=method,
)
try:
with urlopen(request, timeout=self.timeout) as response:
response_body = response.read()
except HTTPError as exc:
detail = exc.read().decode("utf-8", errors="replace")
raise LMStudioError(
f"LM Studio {method} {path} returned HTTP {exc.code}: {detail}"
) from exc
except (URLError, TimeoutError) as exc:
raise LMStudioError(
f"Could not reach LM Studio at {self.api_base_url}: {exc}"
) from exc
if not response_body:
return {}
try:
result = json.loads(response_body)
except json.JSONDecodeError as exc:
raise LMStudioError(
f"LM Studio {method} {path} returned invalid JSON"
) from exc
if not isinstance(result, dict):
raise LMStudioError(
f"LM Studio {method} {path} returned an unexpected response"
)
return result
+180
View File
@@ -0,0 +1,180 @@
import logging
import shutil
from pathlib import Path
from typing import Annotated, Any
import exiftool
from pydantic import StringConstraints
from photools3.settings import settings
log = logging.getLogger(__name__)
_EXIFTOOL_MISSING_MSG = (
"exiftool binary not found in PATH. "
"Install the system package: 'perl-image-exiftool' (Arch) or 'libimage-exiftool-perl' (Debian/Ubuntu)."
)
if shutil.which("exiftool") is None:
raise FileNotFoundError(_EXIFTOOL_MISSING_MSG)
XmpKey = Annotated[str, StringConstraints(pattern=r"^\w+\:\w+")]
type Caption = str
type Keywords = list[str]
type Color = int
type XmpMetadata = dict[str, Any]
XMP_KEYS_CAPTION: list[XmpKey] = [
"XMP:Description",
"EXIF:ImageDescription",
"IPTC:Caption-Abstract",
]
XMP_KEYS_KEYWORDS: list[XmpKey] = [
"XMP:HierarchicalSubject",
"XMP:Subject",
"IPTC:Keywords",
]
def _prefixed(txt: str) -> str:
return f"{settings.metadata_prefix}{txt}"
def _is_prefixed(txt: str) -> bool:
return txt.startswith(settings.metadata_prefix)
def _separate_prefixed_caption(txt: str) -> tuple[str, str | None]:
separated = [x.strip() for x in txt.split(settings.metadata_prefix)]
return (separated[0], separated[1] if len(separated) > 1 else None)
class MetadataWriter:
"""
Write metadata (captions and keywords) to image files.
For DNG files, creates/updates XMP sidecar files.
"""
def __init__(
self,
path: Path,
force_sidecar: bool = False,
append: bool = True,
):
self.path: Path = path
self.force_sidecar: bool = force_sidecar
self.append: bool = append
self._metadata: XmpMetadata | None = None
@property
def sidecar_path(self) -> Path:
return Path(str(self.path) + ".xmp")
@property
def metadata(self) -> Any:
path = self.sidecar_path if self.sidecar_path.exists() else self.path
if not path.exists():
log.warning(f"File not found: {path}")
return None
try:
with exiftool.ExifToolHelper() as exif:
metadata = exif.get_metadata([path])[0]
except Exception as exc: # noqa: BLE001
log.error(f"Could not read metadata from {path}: {exc}")
return None
return metadata
def get_caption(self) -> str:
result = (self.metadata or {}).get(XMP_KEYS_CAPTION[0], "")
assert isinstance(result, str)
return result
def get_keywords(self) -> list[str]:
result = (self.metadata or {}).get(XMP_KEYS_KEYWORDS[0], []) or []
if isinstance(result, str):
result = [result]
assert isinstance(result, list)
return result
def set_metadata(self, metadata_tags: dict[str, Any]):
try:
with exiftool.ExifToolHelper() as exif:
if not self.sidecar_path.exists():
exif.execute("-o", str(self.sidecar_path), str(self.path))
exif.set_tags(
[str(self.sidecar_path)],
tags=metadata_tags,
params=["-overwrite_original"],
)
except Exception as exc:
log.error(f"Could not write metadata to file {self.sidecar_path}: {exc}")
raise RuntimeError(
f"Could not write metadata to file {self.sidecar_path}"
) from exc
def write_caption(
self, caption: Caption, include_prefix: bool = True, override: bool = True
):
existing_no_prefix, existing_prefix = _separate_prefixed_caption(
self.get_caption()
)
if existing_no_prefix:
existing_no_prefix = (
f"{existing_no_prefix}."
if not existing_no_prefix.endswith(".")
else existing_no_prefix
)
existing_no_prefix = f"{existing_no_prefix} "
if include_prefix and existing_prefix and not override:
return
if not include_prefix and existing_no_prefix and not override:
return
if include_prefix:
caption = f"{existing_no_prefix}{_prefixed(caption)}"
metadata_tags = {key: caption for key in XMP_KEYS_CAPTION}
self.set_metadata(metadata_tags)
def write_keywords(self, keywords: Keywords, include_prefix: bool = True):
if include_prefix:
keywords = list(map(_prefixed, keywords))
existing = self.get_keywords()
keywords = list(set(existing + keywords))
metadata_tags = {tag: keywords for tag in XMP_KEYS_KEYWORDS}
self.set_metadata(metadata_tags)
def write_color(self, color: Color):
self.set_metadata({"XMP-darktable:Colorlabels": color})
def write(
self,
caption: Caption | None = None,
keywords: Keywords | None = None,
color: Color | None = None,
):
metadata_tags: XmpMetadata = {}
if caption is not None:
existing_no_prefix, _ = _separate_prefixed_caption(self.get_caption())
if existing_no_prefix:
existing_no_prefix = (
f"{existing_no_prefix}."
if not existing_no_prefix.endswith(".")
else existing_no_prefix
)
existing_no_prefix = f"{existing_no_prefix} "
caption = f"{existing_no_prefix}{_prefixed(caption)}"
metadata_tags.update({key: caption for key in XMP_KEYS_CAPTION})
if keywords is not None:
keywords = list(map(_prefixed, keywords))
existing = self.get_keywords()
keywords = list(set(existing + keywords))
metadata_tags.update({tag: keywords for tag in XMP_KEYS_KEYWORDS})
if color is not None:
metadata_tags["XMP-darktable:Colorlabels"] = color
if metadata_tags:
self.set_metadata(metadata_tags)
+160 -7
View File
@@ -1,23 +1,79 @@
from typing import Literal
import json
from enum import StrEnum
from functools import lru_cache
from typing import Annotated, Any, Literal
from pydantic import BaseModel, model_validator
from pydantic import (
BaseModel,
BeforeValidator,
Field,
field_validator,
model_validator,
)
from .settings import KEYWORDS_LIST_PATH, settings
@lru_cache
def load_valid_keywords() -> set[str]:
"""Loads all valid keywords from the configuration file."""
if not KEYWORDS_LIST_PATH.exists():
return set()
keywords = set()
with open(KEYWORDS_LIST_PATH, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
# Each line can contain hierarchical tags separated by '|'
# e.g., 'architektur|gebäude' means both 'architektur' and 'gebäude' are valid.
parts = line.split("|")
current_path = ""
for part in parts:
if current_path:
current_path += "|" + part
else:
current_path = part
keywords.add(current_path)
return keywords
class LlmRequest(BaseModel):
model_id: int
model: str = "gpt-4o"
messages: list[LlmRequestMessage]
temperature: float | None = None
max_tokens: int
class LlmMessageRole(StrEnum):
system = "system"
user = "user"
class LlmRequestMessage(BaseModel):
role: Literal["system", "user"]
content: list[LlmMessageContent]
role: LlmMessageRole
content: list[LlmMessageContent] | str
def transform_to_image_dict(v: Any) -> Any:
if isinstance(v, str):
return {"url": f"data:image/png;base64,{v}"}
return v
Base64ImageUrl = Annotated[dict[str, str], BeforeValidator(transform_to_image_dict)]
class LlmMessageType(StrEnum):
text = "text"
image_url = "image_url"
class LlmMessageContent(BaseModel):
type: Literal["text", "image_url"]
type: LlmMessageType
text: str | None = None
image_url: str | None = None
image_url: Base64ImageUrl | None = None
@model_validator(mode="after")
def _type_matches_content(self):
@@ -26,3 +82,100 @@ class LlmMessageContent(BaseModel):
if self.type == "image_url" and self.image_url is None:
raise ValueError("image_url must be set when type is 'image_url'")
return self
class LlmResponseGeneral(BaseModel):
text: str
class LlmResponseCaption(BaseModel):
caption: Annotated[
str,
Field(
description=(
"A brief but precise description of what the picture shows. "
"No interpretation; just what is visible"
)
),
]
def _keyword_tags_field():
allowed_keywords = json.dumps(
sorted(load_valid_keywords()), ensure_ascii=False, separators=(",", ":")
)
return Field(
max_length=settings.llm_max_keywords,
description=(
f"Choose at most {settings.llm_max_keywords} distinct, relevant keywords. "
"Do not enumerate the vocabulary. Allowed keywords: "
f"{allowed_keywords}"
),
)
class _LlmResponseWithTags(BaseModel):
tags: list[str] = _keyword_tags_field()
@field_validator("tags")
@classmethod
def validate_tags(cls, v: list[str]) -> list[str]:
valid_keywords = load_valid_keywords()
for tag in v:
if tag not in valid_keywords:
raise ValueError(
f"'{tag}' is not a valid keyword. Must be one from the configuration."
)
return v
class LlmResponseKeywords(_LlmResponseWithTags):
pass
class LlmResponseCaptionsAndKeywords(_LlmResponseWithTags):
caption: Annotated[
str,
Field(
description=(
"A brief but precise description of what the picture shows. "
"No interpretation; just what is visible"
)
),
]
class LlmResponseQuality(BaseModel):
rating: Annotated[
Literal["ok", "review", "not_ok"],
Field(
description=(
"General quality assessment of the photo. "
'Helping to decide whether to keep ("ok"), '
'to consider sorting out ("review"), '
'or to almost certainly sort out ("not_ok")'
)
),
]
reason: (
None
| Annotated[
Literal["blur", "exposure", "style"],
Field(
description=(
'Possible reasons for a lower-than-"ok" assessment. '
'"blur": Obviously mis-focussed or motion-blurred. '
'"exposure": severly under- or over-exposed. '
'"style": photo has severe style issues (beyond personal taste).'
)
),
]
)
@model_validator(mode="after")
def _not_ok_needs_reason(self):
if self.rating != "ok" and self.reason is None:
raise ValueError("lower-than-ok rating needs a reason")
elif self.rating == "ok" and self.reason is not None:
raise ValueError("ok rating needs no reason")
return self
+40 -9
View File
@@ -1,18 +1,49 @@
import base64
from io import BytesIO
from pathlib import Path
from typing import Self
from PIL import Image
from photools3.models import transform_to_image_dict
def _exist_guard(path: Path):
pass
class Photo:
def __init__(self):
...
def __init__(self, path: Path):
self.path = path
self.image = Image.open(path)
@classmethod
def from_file(cls, path: Path) -> Photo:
# TODO
return cls()
def close(self):
self.image.close()
def resize(self) -> Photo:
# TODO
def __enter__(self) -> Self:
return self
def __exit__(self, exc_type, exc_value, traceback):
self.close()
def resize(self, image_max_size=1024) -> Photo:
"""Limiting max dimensions while maintaining aspect ratio"""
self.image.thumbnail((image_max_size, image_max_size), Image.Resampling.LANCZOS)
return self
def to_rgb(self):
if self.image.mode != "RGB":
original = self.image
self.image = original.convert("RGB")
original.close()
return self
def get_encoded(self):
buffer = BytesIO()
self.image.save(buffer, format="JPEG", quality=85)
image_data = base64.b64encode(buffer.getvalue()).decode("utf-8")
return image_data
def to_llm_image_url(self):
return transform_to_image_dict(self.resize().to_rgb().get_encoded())
+138
View File
@@ -0,0 +1,138 @@
import logging
import time
from collections.abc import Iterable
from dataclasses import dataclass
from enum import StrEnum
from pathlib import Path
from photools3.llm_session import (
LlmSession,
LlmSessionCaption,
LlmSessionCaptionsAndKeywords,
LlmSessionKeywords,
)
from photools3.metadata_writer import MetadataWriter
from photools3.models import (
LlmResponseCaption,
LlmResponseCaptionsAndKeywords,
LlmResponseKeywords,
)
from photools3.photo import Photo
logger = logging.getLogger(__name__)
type ProcessingResponse = (
LlmResponseCaption | LlmResponseKeywords | LlmResponseCaptionsAndKeywords
)
class ProcessingMode(StrEnum):
KEYWORDS = "keywords"
CAPTIONS = "captions"
BOTH = "both"
@dataclass(frozen=True)
class ProcessingOutcome:
path: Path
result: ProcessingResponse | None = None
error: str | None = None
@property
def succeeded(self) -> bool:
return self.error is None and self.result is not None
def _create_session(mode: ProcessingMode) -> LlmSession:
if mode == ProcessingMode.KEYWORDS:
return LlmSessionKeywords()
if mode == ProcessingMode.CAPTIONS:
return LlmSessionCaption()
return LlmSessionCaptionsAndKeywords()
def process_photo(
path: Path | str,
mode: ProcessingMode | str = ProcessingMode.BOTH,
*,
session: LlmSession | None = None,
) -> ProcessingResponse:
"""Process one photo and write the requested LLM result to its metadata.
If a session is supplied, its lifecycle remains the caller's responsibility.
"""
path = Path(path)
mode = ProcessingMode(mode)
owns_session = session is None
active_session = session if session is not None else _create_session(mode)
logger.info(f"Processing photo: {path!s}")
try:
with Photo(path) as photo:
result = active_session.call([photo])
caption: str | None = None
keywords: list[str] | None = None
if mode == ProcessingMode.CAPTIONS:
if not isinstance(result, LlmResponseCaption):
raise TypeError("Caption mode returned an unexpected response type")
caption = result.caption
elif mode == ProcessingMode.KEYWORDS:
if not isinstance(result, LlmResponseKeywords):
raise TypeError("Keywords mode returned an unexpected response type")
keywords = result.tags
else:
if not isinstance(result, LlmResponseCaptionsAndKeywords):
raise TypeError("Both mode returned an unexpected response type")
caption = result.caption
keywords = result.tags
MetadataWriter(path).write(caption=caption, keywords=keywords)
return result
finally:
if owns_session:
active_session.close()
def process_batch(
paths: Iterable[Path | str],
mode: ProcessingMode | str = ProcessingMode.BOTH,
) -> list[ProcessingOutcome]:
"""Process photos sequentially, reusing one LLM client for the whole batch.
A failed photo is recorded in its outcome and does not stop later photos.
Only one decoded image is held at a time.
"""
mode = ProcessingMode(mode)
session = _create_session(mode)
outcomes: list[ProcessingOutcome] = []
try:
for item in paths:
path = Path(item)
photo_started = time.perf_counter()
try:
result = process_photo(path, mode, session=session)
except Exception as exc:
logger.exception("Failed to process %s", path)
# Keep only a string, not the exception/traceback, which can
# retain references to the just-processed image and stack.
outcomes.append(
ProcessingOutcome(
path=path,
error=f"{type(exc).__name__}: {exc}",
)
)
else:
outcomes.append(ProcessingOutcome(path=path, result=result))
finally:
logger.debug(
"Photo processing for %s took %.2f seconds",
path,
time.perf_counter() - photo_started,
)
finally:
session.close()
return outcomes
+23 -1
View File
@@ -1,9 +1,31 @@
from pathlib import Path
from pydantic import Field
from pydantic_core import Url
from pydantic_settings import BaseSettings, SettingsConfigDict
PHOTOOLS3_DIR = Path(__file__).parent.resolve()
SRC_DIR = PHOTOOLS3_DIR.parent
ROOT_DIR = SRC_DIR.parent
EXAMPLES_DIR = ROOT_DIR / "examples"
DNG_EXAMPLES_DIR = EXAMPLES_DIR / "dng"
CONFIG_DIR = ROOT_DIR / "config"
KEYWORDS_LIST_PATH = CONFIG_DIR / "keywords.txt"
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env")
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
llm_api_base_url: Url
llm_api_key: str
llm_default_temperature: float
llm_request_timeout_seconds: float = Field(default=10, gt=0)
llm_max_completion_tokens: int = Field(default=1024, gt=0)
llm_max_retries: int = Field(default=5, ge=0)
llm_max_keywords: int = Field(default=15, ge=1)
log_level: str = "INFO"
metadata_prefix: str
settings = Settings() # type: ignore[call-arg]
View File
+22
View File
@@ -0,0 +1,22 @@
import pytest
from photools3.llm_session import LlmSession
from photools3.photo import Photo
from test.test_photo import PATHS_DNG_EXAMPLES
@pytest.fixture
def test_photo():
return Photo(PATHS_DNG_EXAMPLES[0])
@pytest.fixture
def llm_session():
return LlmSession(system_prompt="You are a helpful assistant.")
@pytest.fixture
def llm_session_captions_and_keywords():
from photools3.llm_session import LlmSessionCaptionsAndKeywords
return LlmSessionCaptionsAndKeywords()
+198
View File
@@ -0,0 +1,198 @@
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
from typer.testing import CliRunner
from photools3.cli import app
from photools3.processing import ProcessingMode
runner = CliRunner()
def _success_outcome(path: Path):
return SimpleNamespace(path=path, succeeded=True, error=None)
def test_metadata_magic_command_defaults_to_both_for_single_photo(tmp_path):
image_path = tmp_path / "photo.jpg"
image_path.write_bytes(b"test image")
with (
patch("photools3.cli.process_photo") as process_photo,
patch("photools3.cli.process_batch") as process_batch,
):
result = runner.invoke(app, ["metadata-magic", str(image_path)])
assert result.exit_code == 0
process_photo.assert_called_once_with(image_path, ProcessingMode.BOTH)
process_batch.assert_not_called()
def test_metadata_magic_command_selects_caption_mode(tmp_path):
image_path = tmp_path / "photo.jpg"
image_path.write_bytes(b"test image")
with patch("photools3.cli.process_photo") as process_photo:
result = runner.invoke(
app, ["metadata-magic", str(image_path), "--mode", "captions"]
)
assert result.exit_code == 0
process_photo.assert_called_once_with(image_path, ProcessingMode.CAPTIONS)
def test_metadata_magic_command_batches_multiple_files(tmp_path):
paths = [tmp_path / "one.jpg", tmp_path / "two.jpg"]
for path in paths:
path.write_bytes(b"test image")
with (
patch("photools3.cli.process_photo") as process_photo,
patch("photools3.cli.process_batch") as process_batch,
):
process_batch.return_value = [_success_outcome(path) for path in paths]
result = runner.invoke(
app,
["metadata-magic", *(str(path) for path in paths), "--mode", "keywords"],
)
assert result.exit_code == 0
process_photo.assert_not_called()
process_batch.assert_called_once_with(paths, ProcessingMode.KEYWORDS)
def test_metadata_magic_command_expands_directories_recursively(tmp_path):
root = tmp_path / "photos"
nested = root / "nested"
nested.mkdir(parents=True)
top_level_photo = root / "one.jpg"
nested_photo = nested / "two.png"
sidecar = root / "one.jpg.xmp"
for path in (top_level_photo, nested_photo, sidecar):
path.write_bytes(b"placeholder")
with (
patch("photools3.cli.process_photo") as process_photo,
patch("photools3.cli.process_batch") as process_batch,
):
process_batch.return_value = [
_success_outcome(nested_photo),
_success_outcome(top_level_photo),
]
result = runner.invoke(
app, ["metadata-magic", str(root), "--recursive", "--mode", "both"]
)
assert result.exit_code == 0
process_photo.assert_not_called()
process_batch.assert_called_once_with(
[nested_photo, top_level_photo], ProcessingMode.BOTH
)
def test_metadata_magic_command_does_not_recurse_by_default(tmp_path):
root = tmp_path / "photos"
nested = root / "nested"
nested.mkdir(parents=True)
top_level_photo = root / "one.jpg"
nested_photo = nested / "two.jpg"
top_level_photo.write_bytes(b"placeholder")
nested_photo.write_bytes(b"placeholder")
with (
patch("photools3.cli.process_photo") as process_photo,
patch("photools3.cli.process_batch") as process_batch,
):
result = runner.invoke(app, ["metadata-magic", str(root)])
assert result.exit_code == 0
process_photo.assert_called_once_with(top_level_photo, ProcessingMode.BOTH)
process_batch.assert_not_called()
def test_metadata_magic_command_returns_failure_if_any_batch_item_fails(tmp_path):
paths = [tmp_path / "one.jpg", tmp_path / "two.jpg"]
for path in paths:
path.write_bytes(b"test image")
with patch("photools3.cli.process_batch") as process_batch:
process_batch.return_value = [
_success_outcome(paths[0]),
SimpleNamespace(
path=paths[1], succeeded=False, error="RuntimeError: write failed"
),
]
result = runner.invoke(app, ["metadata-magic", *(str(path) for path in paths)])
assert result.exit_code == 1
def test_metadata_magic_command_returns_failure_for_single_photo_error(tmp_path):
image_path = tmp_path / "photo.jpg"
image_path.write_bytes(b"test image")
with patch("photools3.cli.process_photo", side_effect=RuntimeError("LLM error")):
result = runner.invoke(app, ["metadata-magic", str(image_path)])
assert result.exit_code == 1
def test_keywords_alias_is_not_registered():
result = runner.invoke(app, ["keywords", "photo.jpg"])
assert result.exit_code == 2
assert "No such command 'keywords'" in result.output
def test_old_process_command_name_is_not_registered():
result = runner.invoke(app, ["process", "photo.jpg"])
assert result.exit_code == 2
assert "No such command 'process'" in result.output
def test_metadata_magic_command_rejects_empty_directory(tmp_path):
with (
patch("photools3.cli.process_photo") as process_photo,
patch("photools3.cli.process_batch") as process_batch,
):
result = runner.invoke(app, ["metadata-magic", str(tmp_path)])
assert result.exit_code == 1
process_photo.assert_not_called()
process_batch.assert_not_called()
def test_quality_command_closes_photo_and_session(tmp_path):
image_path = tmp_path / "photo.jpg"
image_path.write_bytes(b"test image")
with (
patch("photools3.cli.LlmSessionQuality") as session_class,
patch("photools3.cli.Photo") as photo_class,
):
result = runner.invoke(app, ["quality", str(image_path)])
assert result.exit_code == 0
session_class.return_value.__enter__.return_value.call.assert_called_once_with(
[photo_class.return_value.__enter__.return_value]
)
session_class.return_value.__exit__.assert_called_once()
photo_class.return_value.__exit__.assert_called_once()
def test_quality_command_reports_error_and_exits(tmp_path):
image_path = tmp_path / "photo.jpg"
with patch("photools3.cli.Photo", side_effect=FileNotFoundError(image_path)):
result = runner.invoke(app, ["quality", str(image_path)])
assert result.exit_code == 1
def test_version_command():
result = runner.invoke(app, ["--version"])
assert result.exit_code == 0
assert "photools3 version 0.1.0" in result.stdout
+27
View File
@@ -0,0 +1,27 @@
import pytest
from photools3.llm_session import LlmSession
@pytest.mark.skipif(
not LlmSession(system_prompt="").is_api_reachable(), reason="API not available"
)
def test_llm_session(llm_session, test_photo):
llm_session.system_prompt = "Return only JSON format: {'text': '...'}"
result = llm_session.call(
prompt="keywords for the attached picture", photos=[test_photo]
)
assert result is not None
@pytest.mark.skipif(
not LlmSession(system_prompt="").is_api_reachable(), reason="API not available"
)
def test_llm_session_captions_and_keywords(
llm_session_captions_and_keywords, test_photo
):
result = llm_session_captions_and_keywords.call(photos=[test_photo])
assert result.caption
assert result.tags
assert isinstance(result.caption, str)
assert isinstance(result.tags, list)
+303
View File
@@ -0,0 +1,303 @@
from unittest.mock import MagicMock, patch
import openai
import pytest
from photools3.llm_session import LlmSession, LlmSessionCaptionsAndKeywords
from photools3.models import LlmResponseCaptionsAndKeywords
def test_llm_session_mocked(test_photo):
# We want to mock OpenAI client's chat.completions.create
with patch("photools3.llm_session.OpenAI") as mocked_openai:
# Setup the mock response structure
mock_client = MagicMock()
mocked_openai.return_value = mock_client
# Mock the models list to return a proper object with an ID string
# This prevents Pydantic ValidationError when LlmSession tries to use model_id
mock_model = MagicMock()
mock_model.id = "gpt-4o"
mock_client.models.list.return_value.data = [mock_model]
# Create a response object: response.choices[0].message.content
mock_response = MagicMock()
mock_choice = MagicMock()
mock_message = MagicMock()
mock_message.content = '{"text": "some text"}'
mock_choice.message = mock_message
mock_response.choices = [mock_choice]
# Configure the client to return our mock response
mock_client.chat.completions.create.return_value = mock_response
# Create a new LlmSession instance that will use the mocked OpenAI client
mocked_session = LlmSession(system_prompt="You are a helper.")
# Run the test
result = mocked_session.call(
prompt="keywords for the attached picture", photos=[test_photo]
)
# Verify the results
assert result.text == "some text"
# Verify that the mock was enough
assert mock_client.chat.completions.create.called
def test_llm_session_captions_and_keywords_mocked(test_photo):
# We want to mock OpenAI client's chat.completions.create
with patch("photools3.llm_session.OpenAI") as mocked_openai:
# Setup the mock response structure
mock_client = MagicMock()
mocked_openai.return_value = mock_client
# Mock the models list to return a proper object with an ID string
# This prevents Pydantic ValidationError when LlmSession tries to use model_id
mock_model = MagicMock()
mock_model.id = "gpt-4o"
mock_client.models.list.return_value.data = [mock_model]
# Create a response object: response.choices[0].message.content
mock_response = MagicMock()
mock_choice = MagicMock()
mock_message = MagicMock()
mock_message.content = (
'{"caption": "A mocked caption", "tags": ["architektur"]}'
)
mock_choice.message = mock_message
mock_response.choices = [mock_choice]
# Configure the client to return our mock response
mock_client.chat.completions.create.return_value = mock_response
# Create a new LlmSessionCaptionsAndKeywords instance that will use the mocked OpenAI client
from photools3.llm_session import LlmSessionCaptionsAndKeywords
mocked_session = LlmSessionCaptionsAndKeywords()
# Run the test
result = mocked_session.call(photos=[test_photo])
# Verify the results
assert result.caption == "A mocked caption"
assert "architektur" in result.tags
# Verify that the mock was called
assert mock_client.chat.completions.create.called
def test_llm_session_retries_from_original_request_without_accumulating_messages():
with patch("photools3.llm_session.OpenAI") as mocked_openai:
mock_client = MagicMock()
mocked_openai.return_value = mock_client
mock_model = MagicMock()
mock_model.id = "gpt-4o"
mock_client.models.list.return_value.data = [mock_model]
def response(content):
message = MagicMock()
message.content = content
choice = MagicMock()
choice.message = message
result = MagicMock()
result.choices = [choice]
return result
mock_client.chat.completions.create.side_effect = [
response('{"wrong": "first"}'),
response('{"wrong": "second"}'),
response('{"text": "correct"}'),
]
session = LlmSession(system_prompt="You are a helper.")
result = session.call(photos=[], prompt="Return JSON")
assert result.text == "correct"
requests = mock_client.chat.completions.create.call_args_list
messages_per_attempt = [call.kwargs["messages"] for call in requests]
assert [len(messages) for messages in messages_per_attempt] == [2, 3, 3]
final_correction = messages_per_attempt[2][-1]["content"]
assert "Field required" in final_correction
assert '"wrong"' not in final_correction
def test_llm_session_uses_configured_retry_count(monkeypatch):
monkeypatch.setattr("photools3.llm_session.settings.llm_max_retries", 1)
with patch("photools3.llm_session.OpenAI") as mocked_openai:
mock_client = MagicMock()
mocked_openai.return_value = mock_client
mock_model = MagicMock()
mock_model.id = "gpt-4o"
mock_client.models.list.return_value.data = [mock_model]
def response(content):
message = MagicMock()
message.content = content
choice = MagicMock()
choice.message = message
result = MagicMock()
result.choices = [choice]
return result
mock_client.chat.completions.create.side_effect = [
response('{"wrong": "first"}'),
response('{"wrong": "second"}'),
response('{"text": "would be a third call"}'),
]
session = LlmSession(system_prompt="You are a helper.")
with (
patch.object(session, "flush_model") as flush_model,
pytest.raises(ValueError, match="after 1 retries"),
):
session.call(photos=[])
assert mock_client.chat.completions.create.call_count == 2
flush_model.assert_called_once()
def test_llm_session_reloads_after_timeout_and_retries(monkeypatch):
monkeypatch.setattr(
"photools3.llm_session.settings.llm_request_timeout_seconds", 10
)
monkeypatch.setattr("photools3.llm_session.settings.llm_max_completion_tokens", 768)
monkeypatch.setattr("photools3.llm_session.settings.llm_max_retries", 1)
with patch("photools3.llm_session.OpenAI") as mocked_openai:
mock_client = MagicMock()
mocked_openai.return_value = mock_client
mock_model = MagicMock()
mock_model.id = "gpt-4o"
mock_client.models.list.return_value.data = [mock_model]
response_message = MagicMock(content='{"text": "recovered"}')
response_choice = MagicMock(message=response_message)
success_response = MagicMock(choices=[response_choice])
mock_client.chat.completions.create.side_effect = [
openai.APITimeoutError(request=MagicMock()),
success_response,
]
session = LlmSession(system_prompt="You are a helper.")
with patch.object(session, "flush_model") as flush_model:
result = session.call(photos=[])
assert result.text == "recovered"
flush_model.assert_called_once()
calls = mock_client.chat.completions.create.call_args_list
assert len(calls) == 2
assert all(call.kwargs["timeout"] == 10 for call in calls)
assert all(call.kwargs["max_tokens"] == 768 for call in calls)
mocked_openai.assert_called_once_with(
base_url=session.base_url,
api_key=session.api_key,
max_retries=0,
)
@pytest.mark.parametrize("failure_kind", ["invalid_response", "timeout"])
def test_llm_session_reloads_when_retry_limit_is_exhausted(monkeypatch, failure_kind):
monkeypatch.setattr("photools3.llm_session.settings.llm_max_retries", 0)
with patch("photools3.llm_session.OpenAI") as mocked_openai:
mock_client = MagicMock()
mocked_openai.return_value = mock_client
mock_model = MagicMock()
mock_model.id = "gpt-4o"
mock_client.models.list.return_value.data = [mock_model]
if failure_kind == "timeout":
mock_client.chat.completions.create.side_effect = openai.APITimeoutError(
request=MagicMock()
)
else:
response_message = MagicMock(content='{"wrong": "shape"}')
response_choice = MagicMock(message=response_message)
mock_client.chat.completions.create.return_value = MagicMock(
choices=[response_choice]
)
session = LlmSession(system_prompt="You are a helper.")
with (
patch.object(session, "flush_model") as flush_model,
pytest.raises(ValueError),
):
session.call(photos=[])
flush_model.assert_called_once()
assert mock_client.chat.completions.create.call_count == 1
def test_invalid_keywords_get_one_correction_then_invalid_tags_are_dropped(monkeypatch):
monkeypatch.setattr("photools3.llm_session.settings.llm_max_keywords", 3)
monkeypatch.setattr("photools3.llm_session.settings.llm_max_retries", 5)
with patch("photools3.llm_session.OpenAI") as mocked_openai:
mock_client = MagicMock()
mocked_openai.return_value = mock_client
mock_model = MagicMock()
mock_model.id = "gpt-4o"
mock_client.models.list.return_value.data = [mock_model]
def response(content):
message = MagicMock(content=content)
choice = MagicMock(message=message)
return MagicMock(choices=[choice])
mock_client.chat.completions.create.side_effect = [
response(
'{"caption": "A caption", "tags": ["not_allowed", "architektur"]}'
),
response(
'{"caption": "A caption", '
'"tags": ["still_invalid", "architektur", "personen"]}'
),
]
session = LlmSessionCaptionsAndKeywords()
with patch.object(session, "flush_model") as flush_model:
result = session.call(photos=[])
assert result.tags == ["architektur", "personen"]
assert mock_client.chat.completions.create.call_count == 2
flush_model.assert_not_called()
correction = mock_client.chat.completions.create.call_args_list[1].kwargs[
"messages"
][-1]["content"]
assert "not_allowed" in correction
assert '"caption"' not in correction
def test_excess_keywords_are_truncated_to_configured_limit(monkeypatch):
monkeypatch.setattr("photools3.llm_session.settings.llm_max_keywords", 2)
with patch("photools3.llm_session.OpenAI") as mocked_openai:
mock_client = MagicMock()
mocked_openai.return_value = mock_client
mock_model = MagicMock()
mock_model.id = "gpt-4o"
mock_client.models.list.return_value.data = [mock_model]
message = MagicMock(
content=(
'{"caption": "A caption", '
'"tags": ["architektur", "personen", "landschaft"]}'
)
)
mock_client.chat.completions.create.return_value = MagicMock(
choices=[MagicMock(message=message)]
)
result = LlmSessionCaptionsAndKeywords().call(photos=[])
assert result.tags == ["architektur", "personen"]
assert mock_client.chat.completions.create.call_count == 1
def test_keyword_response_schema_includes_configured_max_items():
schema = LlmResponseCaptionsAndKeywords.model_json_schema()
assert schema["properties"]["tags"]["maxItems"] == 15
+102
View File
@@ -0,0 +1,102 @@
import json
from unittest.mock import MagicMock, patch
import pytest
from photools3.lm_studio import LMStudioError, LMStudioModelManager
def _http_response(payload: dict):
response = MagicMock()
response.__enter__.return_value = response
response.read.return_value = json.dumps(payload).encode("utf-8")
return response
def test_reload_model_unloads_and_reloads_preserving_context_config():
model_response = {
"models": [
{
"key": "qwen/qwen3-vl-4b",
"loaded_instances": [
{
"id": "qwen/qwen3-vl-4b-instance",
"config": {
"context_length": 16384,
"eval_batch_size": 256,
"flash_attention": True,
"offload_kv_cache_to_gpu": False,
"parallel": 2,
},
}
],
}
]
}
with patch(
"photools3.lm_studio.urlopen",
side_effect=[
_http_response(model_response),
_http_response({"instance_id": "qwen/qwen3-vl-4b-instance"}),
_http_response({"instance_id": "qwen/qwen3-vl-4b-instance"}),
],
) as mocked_urlopen:
manager = LMStudioModelManager(
"http://localhost:1234/v1", "test-token", timeout=12
)
manager.reload_model("qwen/qwen3-vl-4b")
requests = [call.args[0] for call in mocked_urlopen.call_args_list]
assert requests[0].full_url == "http://localhost:1234/api/v1/models"
assert requests[0].get_method() == "GET"
assert requests[0].get_header("Authorization") == "Bearer test-token"
assert requests[1].full_url.endswith("/models/unload")
assert json.loads(requests[1].data) == {"instance_id": "qwen/qwen3-vl-4b-instance"}
assert requests[2].full_url.endswith("/models/load")
assert json.loads(requests[2].data) == {
"model": "qwen/qwen3-vl-4b",
"context_length": 16384,
"eval_batch_size": 256,
"flash_attention": True,
"offload_kv_cache_to_gpu": False,
}
def test_reload_model_loads_by_key_if_no_instance_is_loaded():
model_response = {"models": [{"key": "qwen/model", "loaded_instances": []}]}
with patch(
"photools3.lm_studio.urlopen",
side_effect=[
_http_response(model_response),
_http_response({"status": "loaded"}),
],
) as mocked_urlopen:
manager = LMStudioModelManager("http://localhost:1234/v1", "token")
manager.reload_model("qwen/model")
assert mocked_urlopen.call_count == 2
load_request = mocked_urlopen.call_args.args[0]
assert load_request.full_url.endswith("/models/load")
assert json.loads(load_request.data) == {"model": "qwen/model"}
def test_reload_model_rejects_ambiguous_model_selection():
model_response = {
"models": [
{
"key": "qwen/model",
"loaded_instances": [
{"id": "instance-1", "config": {}},
{"id": "instance-2", "config": {}},
],
}
]
}
with patch(
"photools3.lm_studio.urlopen", return_value=_http_response(model_response)
):
manager = LMStudioModelManager("http://localhost:1234/v1", "token")
with pytest.raises(LMStudioError, match="Multiple loaded instances"):
manager.reload_model("qwen/model")
+243
View File
@@ -0,0 +1,243 @@
import shutil
from pathlib import Path
from unittest.mock import patch
import pytest
from photools3.metadata_writer import (
MetadataWriter,
_is_prefixed,
_prefixed,
)
from photools3.settings import settings
from test.test_photo import PATHS_DNG_EXAMPLES
PREFIX = settings.metadata_prefix
@pytest.fixture
def make_dng_file(tmp_path):
def _make(src: Path) -> Path:
dst = tmp_path / f"copy_{src.name}"
shutil.copy(src, dst)
return dst
return _make
@pytest.fixture
def metadata_writer_on_existing(make_dng_file):
metadata_writer = MetadataWriter(make_dng_file(PATHS_DNG_EXAMPLES[0]))
metadata_writer.write_caption(
"Let's pretend this is the caption of the original image",
include_prefix=False,
)
return metadata_writer
@pytest.mark.parametrize("pic", PATHS_DNG_EXAMPLES)
def test_read_metadata(make_dng_file, pic):
"""primitive test for `MetadataWriter`"""
file_path = make_dng_file(pic)
result = MetadataWriter(file_path)
assert result
assert result.metadata
assert isinstance(result.metadata, dict)
@pytest.mark.parametrize(
"pic, expected_keywords",
[
(PATHS_DNG_EXAMPLES[0], ["architektur", "zeit|abend"]),
(PATHS_DNG_EXAMPLES[1], ["architektur"]),
(PATHS_DNG_EXAMPLES[2], ["morgen", "tiere|vögel"]),
(
PATHS_DNG_EXAMPLES[3],
[
"abend",
"architektur",
"bewegungsunschärfe",
"lichter",
"mainz",
"spiegelung",
"wasser",
],
),
(
PATHS_DNG_EXAMPLES[4],
["grau", "landschaft", "landschaft|weinberge", "personen", "tiefe", "weg"],
),
],
)
def test_get_keywords(make_dng_file, pic, expected_keywords):
path = make_dng_file(pic)
result = MetadataWriter(path).get_keywords()
assert sorted(result) == sorted(expected_keywords)
def test_prefixed():
txt = "bla_bla"
txt_prefixed = _prefixed(txt)
assert txt_prefixed == f"{PREFIX}bla_bla"
@pytest.mark.parametrize(
"txt, expected_result",
[
(f"{PREFIX}some text", True),
("prefix is missing here", False),
("some_perfix|but not the right one", False),
],
)
def test_is_prefixed(txt, expected_result):
assert _is_prefixed(txt) == expected_result
@pytest.mark.parametrize("pic", PATHS_DNG_EXAMPLES)
def test_read_metadata_sidecar_path(make_dng_file, pic):
file_path = make_dng_file(pic)
metadata_writer = MetadataWriter(file_path)
assert isinstance(metadata_writer.sidecar_path, Path)
assert str(metadata_writer.sidecar_path).lower().endswith(".dng.xmp")
# file does not exits yet, only the name
assert not metadata_writer.sidecar_path.exists()
@pytest.mark.parametrize("pic", PATHS_DNG_EXAMPLES)
def test_set_caption(make_dng_file, pic):
metadata_writer = MetadataWriter(make_dng_file(pic))
assert not metadata_writer.sidecar_path.exists() # sidecar does not exist yet ...
metadata_writer.write_caption("This is a test")
assert metadata_writer.sidecar_path.exists() # ... but now!
result = metadata_writer.get_caption()
assert result
assert result == f"{PREFIX}This is a test"
def test_set_caption_on_existing(metadata_writer_on_existing):
assert (
metadata_writer_on_existing.get_caption()
== "Let's pretend this is the caption of the original image"
)
metadata_writer_on_existing.write_caption("Another caption")
assert (
metadata_writer_on_existing.get_caption()
== f"Let's pretend this is the caption of the original image. {PREFIX}Another caption"
)
metadata_writer_on_existing.write_caption("Updated caption")
assert (
metadata_writer_on_existing.get_caption()
== f"Let's pretend this is the caption of the original image. {PREFIX}Updated caption"
)
metadata_writer_on_existing.write_caption("Don't do this!", override=False)
assert (
metadata_writer_on_existing.get_caption()
== f"Let's pretend this is the caption of the original image. {PREFIX}Updated caption"
)
@pytest.mark.parametrize(
"pic, expected_keywords",
[
(
PATHS_DNG_EXAMPLES[0],
[
"architektur",
"zeit|abend",
f"{PREFIX}hund",
f"{PREFIX}katze",
f"{PREFIX}maus",
],
),
(
PATHS_DNG_EXAMPLES[1],
["architektur", f"{PREFIX}hund", f"{PREFIX}katze", f"{PREFIX}maus"],
),
(
PATHS_DNG_EXAMPLES[2],
[
"morgen",
"tiere|vögel",
f"{PREFIX}hund",
f"{PREFIX}katze",
f"{PREFIX}maus",
],
),
(
PATHS_DNG_EXAMPLES[3],
[
"abend",
"architektur",
"bewegungsunschärfe",
"lichter",
"mainz",
"spiegelung",
"wasser",
f"{PREFIX}hund",
f"{PREFIX}katze",
f"{PREFIX}maus",
],
),
(
PATHS_DNG_EXAMPLES[4],
[
"grau",
"landschaft",
"landschaft|weinberge",
"personen",
"tiefe",
"weg",
f"{PREFIX}hund",
f"{PREFIX}katze",
f"{PREFIX}maus",
],
),
],
)
def test_set_keywords(make_dng_file, pic, expected_keywords):
metadata_writer = MetadataWriter(make_dng_file(pic))
metadata_writer.write_keywords(["hund", "katze", "maus"])
result = metadata_writer.get_keywords()
assert result
assert isinstance(result, list)
assert f"{PREFIX}hund" in result
assert f"{PREFIX}katze" in result
assert f"{PREFIX}maus" in result
def test_set_keywords_on_existing():
# TODO
...
def test_write_caption_and_keywords_in_one_metadata_update(tmp_path):
writer = MetadataWriter(tmp_path / "photo.jpg")
with (
patch.object(writer, "get_caption", return_value="Original caption"),
patch.object(writer, "get_keywords", return_value=["existing"]),
patch.object(writer, "set_metadata") as set_metadata,
):
writer.write(caption="Generated caption", keywords=["architektur"])
set_metadata.assert_called_once()
metadata_tags = set_metadata.call_args.args[0]
assert metadata_tags["XMP:Description"] == (
f"Original caption. {PREFIX}Generated caption"
)
assert set(metadata_tags["XMP:Subject"]) == {
"existing",
f"{PREFIX}architektur",
}
def test_set_metadata_surfaces_write_failures(tmp_path):
writer = MetadataWriter(tmp_path / "photo.jpg")
with patch("photools3.metadata_writer.exiftool.ExifToolHelper") as exif:
exif_instance = exif.return_value.__enter__.return_value
exif_instance.execute.side_effect = OSError("ExifTool failed")
with pytest.raises(RuntimeError, match="Could not write metadata"):
writer.set_metadata({"XMP:Description": "caption"})
+285
View File
@@ -0,0 +1,285 @@
import base64
from io import BytesIO
import pytest
from PIL import Image
from photools3.photo import Photo
from photools3.settings import DNG_EXAMPLES_DIR
PATHS_DNG_EXAMPLES = [
DNG_EXAMPLES_DIR / "example_dng_01.dng",
DNG_EXAMPLES_DIR / "example_dng_02.dng",
DNG_EXAMPLES_DIR / "example_dng_03.dng",
DNG_EXAMPLES_DIR / "example_dng_04.dng",
DNG_EXAMPLES_DIR / "example_dng_05.dng",
]
# (PIL mode) -> file format used to persist the test image
MODE_TO_FORMAT = {
"RGB": "PNG",
"L": "PNG",
"P": "PNG",
"RGBA": "PNG",
"LA": "PNG",
"CMYK": "TIFF",
"1": "PNG",
"I": "TIFF",
"F": "TIFF",
}
ALL_MODES = list(MODE_TO_FORMAT)
JPEG_COMPATIBLE_MODES = ["RGB", "L", "CMYK", "1"]
JPEG_INCOMPATIBLE_MODES = ["P", "RGBA", "LA", "I", "F"]
def _make_image_file(tmp_path, mode: str, size=(64, 48)):
"""Create an image in the given mode, save it to disk and return its path."""
fmt = MODE_TO_FORMAT[mode]
img = Image.new(mode, size)
path = tmp_path / f"test_{mode}.{fmt.lower()}"
img.save(path, format=fmt)
return path
def test_load_photo():
photo = Photo(PATHS_DNG_EXAMPLES[0])
assert photo
assert photo.path == PATHS_DNG_EXAMPLES[0]
assert isinstance(photo.image, Image.Image)
def test_photo_context_manager_closes_image(tmp_path):
path = _make_image_file(tmp_path, "RGB")
with Photo(path) as photo:
image = photo.image
assert image.fp is not None
assert image.fp is None
def test_resize_method_exists():
"""Test that resize method exists and returns a Photo instance"""
photo = Photo(PATHS_DNG_EXAMPLES[0])
resized_photo = photo.resize()
assert isinstance(resized_photo, Photo)
assert resized_photo is photo # Should return self for chaining
def test_resize_with_custom_max_size():
"""Test resize with a custom maximum size"""
photo = Photo(PATHS_DNG_EXAMPLES[0])
original_width, original_height = photo.image.size
assert original_width > 100 or original_height > 100 # Assume it's larger
resized_photo = photo.resize(image_max_size=50)
new_width, new_height = resized_photo.image.size
max_dim = max(new_width, new_height)
assert max_dim <= 50, f"Max dimension {max_dim} exceeds limit of 50"
def test_resize_maintains_aspect_ratio():
"""Test that resize maintains the original aspect ratio"""
photo = Photo(PATHS_DNG_EXAMPLES[0])
original_width, original_height = photo.image.size
resized_photo = photo.resize(image_max_size=100)
new_width, new_height = resized_photo.image.size
original_ratio = original_width / original_height
resized_ratio = new_width / new_height
assert (
abs(original_ratio - resized_ratio) < 0.01
), f"Aspect ratio changed: {original_ratio} -> {resized_ratio}"
def test_resize_image_thumbnail_called():
"""Test that the image was actually resized using thumbnail"""
photo = Photo(PATHS_DNG_EXAMPLES[0])
original_size = photo.image.size
# Resize to a smaller dimension
resized_photo = photo.resize(image_max_size=10)
new_size = resized_photo.image.size
assert (
new_size != original_size
), "Image should be resized when max_size is smaller than original"
def test_resize_chaining():
"""Test that resize returns self for method chaining"""
photo = Photo(PATHS_DNG_EXAMPLES[0])
# Should be able to chain multiple operations
result = photo.resize().resize(image_max_size=50)
assert result is photo
def test_resize_default_parameter():
"""Test resize with default parameter (no arguments)"""
photo = Photo(PATHS_DNG_EXAMPLES[0])
resized_photo = photo.resize()
# Should return a valid Photo instance
assert isinstance(resized_photo, Photo)
assert resized_photo.path == photo.path
# --- to_rgb() ---
@pytest.mark.parametrize("mode", ALL_MODES)
def test_to_rgb_converts_to_rgb_mode(tmp_path, mode):
"""to_rgb() must leave the image in RGB mode regardless of the initial mode"""
path = _make_image_file(tmp_path, mode)
photo = Photo(path)
result = photo.to_rgb()
assert result.image.mode == "RGB"
@pytest.mark.parametrize("mode", ALL_MODES)
def test_to_rgb_returns_self_for_chaining(tmp_path, mode):
"""to_rgb() returns self so it can be chained"""
path = _make_image_file(tmp_path, mode)
photo = Photo(path)
assert photo.to_rgb() is photo
def test_to_rgb_keeps_rgb_image_unmodified(tmp_path):
"""RGB images are not re-converted: the same image object is kept"""
path = _make_image_file(tmp_path, "RGB")
photo = Photo(path)
original_image = photo.image
photo.to_rgb()
assert photo.image is original_image
@pytest.mark.parametrize("mode", ["L", "P", "RGBA", "LA", "CMYK", "1", "I", "F"])
def test_to_rgb_replaces_non_rgb_image(tmp_path, mode):
"""Non-RGB images are converted: a new image object is assigned"""
path = _make_image_file(tmp_path, mode)
photo = Photo(path)
original_image = photo.image
photo.to_rgb()
assert photo.image is not original_image
assert photo.image.mode == "RGB"
@pytest.mark.parametrize("mode", ["RGBA", "LA"])
def test_to_rgb_drops_alpha_channel(tmp_path, mode):
"""Converting alpha-bearing modes to RGB drops the alpha band"""
path = _make_image_file(tmp_path, mode)
photo = Photo(path)
assert "A" in photo.image.getbands()
photo.to_rgb()
assert photo.image.getbands() == ("R", "G", "B")
def test_to_rgb_on_dng_examples(tmp_path):
"""DNG examples are already RGB; to_rgb() is a safe no-op on every file"""
for path in PATHS_DNG_EXAMPLES:
photo = Photo(path)
original_image = photo.image
assert photo.to_rgb() is photo
assert photo.image is original_image
assert photo.image.mode == "RGB"
# --- get_encoded() ---
@pytest.mark.parametrize("mode", JPEG_COMPATIBLE_MODES)
def test_get_encoded_returns_valid_base64(tmp_path, mode):
"""get_encoded() returns a non-empty, decodable base64 string"""
path = _make_image_file(tmp_path, mode)
photo = Photo(path)
encoded = photo.get_encoded()
assert isinstance(encoded, str)
assert len(encoded) > 0
raw = base64.b64decode(encoded, validate=True)
assert len(raw) > 0
@pytest.mark.parametrize("mode", JPEG_COMPATIBLE_MODES)
def test_get_encoded_produces_openable_jpeg(tmp_path, mode):
"""The decoded payload must be a valid JPEG with the original dimensions"""
path = _make_image_file(tmp_path, mode)
photo = Photo(path)
original_size = photo.image.size
encoded = photo.get_encoded()
img = Image.open(BytesIO(base64.b64decode(encoded)))
assert img.format == "JPEG"
assert img.size == original_size
def test_get_encoded_starts_with_jpeg_magic_bytes(tmp_path):
"""JPEG files start with the FFD8FF magic bytes"""
path = _make_image_file(tmp_path, "RGB")
photo = Photo(path)
raw = base64.b64decode(photo.get_encoded())
assert raw[:3] == b"\xff\xd8\xff"
def test_get_encoded_reencodes_instead_of_passing_raw_file_bytes(tmp_path):
"""The output must be a fresh JPEG encoding, not the source file bytes"""
path = _make_image_file(tmp_path, "RGB")
photo = Photo(path)
raw = base64.b64decode(photo.get_encoded())
source_bytes = path.read_bytes()
assert raw != source_bytes
assert source_bytes[:4] == b"\x89PNG" # source is a PNG...
assert raw[:3] == b"\xff\xd8\xff" # ...but the encoding is a JPEG
def test_get_encoded_is_deterministic(tmp_path):
"""Encoding the same image twice yields identical base64"""
path = _make_image_file(tmp_path, "RGB")
assert Photo(path).get_encoded() == Photo(path).get_encoded()
@pytest.mark.parametrize("mode", JPEG_INCOMPATIBLE_MODES)
def test_get_encoded_raises_for_non_jpeg_modes(tmp_path, mode):
"""Modes JPEG cannot store (palette, alpha, integer/float) must raise OSError"""
path = _make_image_file(tmp_path, mode)
photo = Photo(path)
with pytest.raises(OSError):
photo.get_encoded()
@pytest.mark.parametrize("mode", JPEG_INCOMPATIBLE_MODES)
def test_to_rgb_then_get_encoded_works_around_unsupported_modes(tmp_path, mode):
"""Chaining to_rgb().get_encoded() fixes modes that JPEG cannot store"""
path = _make_image_file(tmp_path, mode)
photo = Photo(path)
with pytest.raises(OSError):
photo.get_encoded()
encoded = photo.to_rgb().get_encoded()
assert isinstance(encoded, str)
img = Image.open(BytesIO(base64.b64decode(encoded)))
assert img.format == "JPEG"
@pytest.mark.parametrize("path", PATHS_DNG_EXAMPLES)
def test_get_encoded_works_on_all_dng_examples(path):
"""Every DNG example (all different files) encodes to a valid JPEG"""
photo = Photo(path)
encoded = photo.get_encoded()
assert isinstance(encoded, str)
raw = base64.b64decode(encoded, validate=True)
assert raw[:3] == b"\xff\xd8\xff"
img = Image.open(BytesIO(raw))
assert img.format == "JPEG"
assert img.size == photo.image.size
def test_full_chain_resize_to_rgb_get_encoded(tmp_path):
"""The intended workflow: resize -> to_rgb -> get_encoded"""
path = _make_image_file(tmp_path, "RGBA")
photo = Photo(path)
photo.resize(32).to_rgb()
assert photo.image.mode == "RGB"
img = Image.open(BytesIO(base64.b64decode(photo.get_encoded())))
assert img.format == "JPEG"
assert max(img.size) <= 32
+129
View File
@@ -0,0 +1,129 @@
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from PIL import Image
from photools3.models import (
LlmResponseCaption,
LlmResponseCaptionsAndKeywords,
LlmResponseKeywords,
)
from photools3.processing import (
ProcessingMode,
_create_session,
process_batch,
process_photo,
)
@pytest.fixture
def image_path(tmp_path):
path = tmp_path / "photo.png"
Image.new("RGB", (20, 20)).save(path)
return path
@pytest.mark.parametrize(
("mode", "response", "expected_metadata"),
[
(
ProcessingMode.KEYWORDS,
LlmResponseKeywords(tags=["architektur"]),
{"caption": None, "keywords": ["architektur"]},
),
(
ProcessingMode.CAPTIONS,
LlmResponseCaption(caption="Ein Haus"),
{"caption": "Ein Haus", "keywords": None},
),
(
ProcessingMode.BOTH,
LlmResponseCaptionsAndKeywords(caption="Ein Haus", tags=["architektur"]),
{"caption": "Ein Haus", "keywords": ["architektur"]},
),
],
)
def test_process_photo_writes_only_requested_metadata(
image_path, mode, response, expected_metadata
):
session = MagicMock()
session.call.return_value = response
with (
patch("photools3.processing._create_session", return_value=session),
patch("photools3.processing.MetadataWriter") as writer_class,
):
result = process_photo(image_path, mode)
assert result == response
session.call.assert_called_once()
session.close.assert_called_once()
writer_class.assert_called_once_with(image_path)
writer_class.return_value.write.assert_called_once_with(**expected_metadata)
def test_process_photo_does_not_close_a_caller_owned_session(image_path):
session = MagicMock()
session.call.return_value = LlmResponseCaption(caption="Ein Haus")
with patch("photools3.processing.MetadataWriter"):
process_photo(image_path, ProcessingMode.CAPTIONS, session=session)
session.close.assert_not_called()
@pytest.mark.parametrize(
("mode", "constructor"),
[
(ProcessingMode.KEYWORDS, "LlmSessionKeywords"),
(ProcessingMode.CAPTIONS, "LlmSessionCaption"),
(ProcessingMode.BOTH, "LlmSessionCaptionsAndKeywords"),
],
)
def test_create_session_selects_mode_specific_response_model(mode, constructor):
with patch(f"photools3.processing.{constructor}") as session_class:
assert _create_session(mode) is session_class.return_value
session_class.assert_called_once_with()
def test_process_batch_is_sequential_reuses_session_and_continues_after_error():
active_photos = 0
max_active_photos = 0
class TrackingPhoto:
def __init__(self, path):
self.path = path
def __enter__(self):
nonlocal active_photos, max_active_photos
active_photos += 1
max_active_photos = max(max_active_photos, active_photos)
return self
def __exit__(self, exc_type, exc_value, traceback):
nonlocal active_photos
active_photos -= 1
session = MagicMock()
response = LlmResponseCaptionsAndKeywords(caption="Ein Haus", tags=["architektur"])
session.call.side_effect = [response, RuntimeError("temporary error"), response]
paths = [Path("one.jpg"), Path("two.jpg"), Path("three.jpg")]
with (
patch("photools3.processing._create_session", return_value=session),
patch("photools3.processing.Photo", TrackingPhoto),
patch("photools3.processing.MetadataWriter") as writer_class,
):
outcomes = process_batch(paths, ProcessingMode.BOTH)
assert [outcome.succeeded for outcome in outcomes] == [True, False, True]
assert [outcome.path for outcome in outcomes] == paths
assert outcomes[1].error == "RuntimeError: temporary error"
assert max_active_photos == 1
assert active_photos == 0
assert session.call.call_count == 3
assert all(len(call.args[0]) == 1 for call in session.call.call_args_list)
assert writer_class.return_value.write.call_count == 2
session.close.assert_called_once()
+31 -2
View File
@@ -1,6 +1,35 @@
from photools3.settings import Settings
from pathlib import Path
settings = Settings()
from photools3.settings import (
DNG_EXAMPLES_DIR,
EXAMPLES_DIR,
PHOTOOLS3_DIR,
ROOT_DIR,
SRC_DIR,
settings,
)
def test_photools3_dir():
assert PHOTOOLS3_DIR == Path(__file__).parent.parent.resolve() / "src" / "photools3"
def test_src_dir():
assert SRC_DIR == Path(__file__).parent.parent.resolve() / "src"
def test_root_dir():
assert ROOT_DIR == Path(__file__).parent.parent.resolve()
def test_examples_dir():
assert EXAMPLES_DIR == Path(__file__).parent.parent.resolve() / "examples"
def test_dng_examples_dir():
assert (
DNG_EXAMPLES_DIR == Path(__file__).parent.parent.resolve() / "examples" / "dng"
)
def test_settings_base():
Generated
+878 -1
View File
@@ -1,6 +1,19 @@
version = 1
revision = 3
revision = 5
requires-python = ">=3.14"
resolution-markers = [
"python_full_version >= '3.15'",
"python_full_version < '3.15'",
]
[[package]]
name = "annotated-doc"
version = "0.0.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5a/8e/38aa427ed5402449e226975b649c5dc73ccadfefeb95e6aecb8f8ea4b6b6/annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb", size = 10758, upload-time = "2026-07-28T13:50:58.129Z" }
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" },
]
[[package]]
name = "annotated-types"
@@ -24,6 +37,235 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" },
]
[[package]]
name = "ast-serialize"
version = "0.12.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c2/1c/7257e6ec9382843915ce475558ce4492ccb5ed39122c256bb369c27e2ebf/ast_serialize-0.12.1.tar.gz", hash = "sha256:5285a390caf1c44368ae270f037f797b91427d138b7d43cad0f1fda4c83518d9", size = 954408, upload-time = "2026-10-03T12:25:00.221Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4a/f7/e976169da322c009bb083a52d21e88fbfe5f071e1806e8c8361ab4ac477a/ast_serialize-0.12.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:e73255c9227fd74eac8a9b55c4049e8ad7b66d1f690bf827c98a86b2e594def7", size = 897232, upload-time = "2026-10-03T12:23:21.945Z" },
{ url = "https://files.pythonhosted.org/packages/e1/89/5545f6f4d38dd41b4e2a20050967ccd722508bc90fab0dfba463d8c8b994/ast_serialize-0.12.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:4655ef993e69e01bb47d2d99647de9bbb74af03938438656832cd010d95de348", size = 1235397, upload-time = "2026-10-03T12:23:23.835Z" },
{ url = "https://files.pythonhosted.org/packages/22/19/e9b839ef9b57626e15e20dd7cf764a9a6b50f9750f86d0a49bc3a971fb72/ast_serialize-0.12.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bac5a99a2c91dd823be9b8c44645694fccbb0750773cc5b27889a9f6f22fce89", size = 1216331, upload-time = "2026-10-03T12:23:25.557Z" },
{ url = "https://files.pythonhosted.org/packages/26/2a/d054d4ff8ba42472a22e3da6eb6dee0e69a32c477b5077eefdbada99f554/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6485e681625ed7a094221f16a7ff2ef154946112266a05cf83bde50c959ef345", size = 1281194, upload-time = "2026-10-03T12:23:27.349Z" },
{ url = "https://files.pythonhosted.org/packages/7e/0c/c73eddfa180a7a4c1613c0f3d3ef020b05dca9b922ac08212463c33ad11f/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a513bc6f60980d01767f7cbe39b17ce0373e722824a74ce28d6cea49ee3c8460", size = 1287170, upload-time = "2026-10-03T12:23:29.333Z" },
{ url = "https://files.pythonhosted.org/packages/94/77/39dc75d8b718844859b64a9067c9df0cfce218ca45ea215fb24a1fda3cf7/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:82866f3523d53ffca8d2a69a750bec52908b69f728012e40959bebce2620453c", size = 1531238, upload-time = "2026-10-03T12:23:30.954Z" },
{ url = "https://files.pythonhosted.org/packages/b9/c0/6a6a6f94f45a288c4bac2eb8379a3d9654574a0f9249380ce3b07f6d64bb/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:243054a05a5190f5d087b5c8b16423e7f1cefa4991bac26e9c8ace18074b75b6", size = 1307154, upload-time = "2026-10-03T12:23:32.645Z" },
{ url = "https://files.pythonhosted.org/packages/f9/3d/80f843892bd0f7c0d95ec5422ba3dc315c1ce011e6f08b06d5f71bd82c25/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d9fbe5a3e8acddfc2fddff3dbbc7ea0e9798b3df3428f851b8abc52a3806f31", size = 1301506, upload-time = "2026-10-03T12:23:34.63Z" },
{ url = "https://files.pythonhosted.org/packages/df/cc/49a5fe852706f545e3e005584c5be89456bc637a8c9179aeaa8b9f26e8e4/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:d3d516da3463071d27e64caf54d88cba25cf4ad4afcc807e0bcf67743719f03e", size = 1297616, upload-time = "2026-10-03T12:23:36.377Z" },
{ url = "https://files.pythonhosted.org/packages/49/5c/1208c91d6e00cc43cc276bd6233c40c9b4ec3ef8537c83281dd5372cbdb8/ast_serialize-0.12.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d6711adf11136c77e3a35517de9488a5081d1012874fae99c2876b64f4daace", size = 1354884, upload-time = "2026-10-03T12:23:38.035Z" },
{ url = "https://files.pythonhosted.org/packages/a9/80/2b5fc912ff0be64d8d61ff5dc7dc405c6311297a0e2039b848b7d14333f2/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:cbe239bee4bd609186daf60b95b7b0f47146c7f7f55f6da83807d747d6fe753f", size = 1457911, upload-time = "2026-10-03T12:23:39.679Z" },
{ url = "https://files.pythonhosted.org/packages/b2/f8/d720429bf8933efbd0cc2038c0a50b6267a585d503500845c44bc6c8ff66/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:5bbf582286c9dc6b4c544ef645dc99e4b3aa09db28892bc60344141f6926641f", size = 1563023, upload-time = "2026-10-03T12:23:41.585Z" },
{ url = "https://files.pythonhosted.org/packages/7e/0a/99e6cc92bdbae5db60f84a14a0fb1ae77b6087e451d77808d87558162c9a/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:99e33c93efb5254a70c525b46038212371dfe5693d48eb2d0d5f17d936a263d7", size = 1556601, upload-time = "2026-10-03T12:23:43.361Z" },
{ url = "https://files.pythonhosted.org/packages/7a/05/59de9e16a2e333da534f30776d0f5e426034b64c67c17843425e3cc827d1/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:aa6c17a2b7f07e81fa8cfcc4aa7c832b3e57733853aebea113ab502f9b0963db", size = 1665039, upload-time = "2026-10-03T12:23:45.257Z" },
{ url = "https://files.pythonhosted.org/packages/33/83/35ed67a127167b484b42a071df440f84b14c0d20ea8f69dbed5cc96bfd98/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f896fa38e0af38821e1ab1425c5dee89e359623e165765bdeae7d0eb6909e76d", size = 1472263, upload-time = "2026-10-03T12:23:46.811Z" },
{ url = "https://files.pythonhosted.org/packages/c4/b0/3ab8613bbb690297f1bb687d780a248c486df0f4131b6a82044fcb49e438/ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:af699e81fd7ce80b8b03945826d8ea23dd36d072f10d4613402da597ba4ee9c6", size = 1499242, upload-time = "2026-10-03T12:23:48.531Z" },
{ url = "https://files.pythonhosted.org/packages/9d/a0/a28894d3b06f8775cea8989f371bd9f72ce562c32bc807770e7fb920ce17/ast_serialize-0.12.1-cp314-cp314t-win32.whl", hash = "sha256:10b59afc108eb285146acb23d1b5ec0fc58bb3c09cb2ab8876402df06c373c3b", size = 1119488, upload-time = "2026-10-03T12:23:50.419Z" },
{ url = "https://files.pythonhosted.org/packages/c1/b2/0c44952f4ba4e14bb7f60a5858e2960dfefb9884e6ff007aaf64337dba5c/ast_serialize-0.12.1-cp314-cp314t-win_amd64.whl", hash = "sha256:72e871f6995a066c1b19104f8a6b5832b1163adb9a8267c2aa4711fbb0f4d1f3", size = 1157434, upload-time = "2026-10-03T12:23:52.383Z" },
{ url = "https://files.pythonhosted.org/packages/1f/1e/cb594c63f46a01d53629af1c4f9e42cd02afcea1c2fe483e12f22743ebac/ast_serialize-0.12.1-cp314-cp314t-win_arm64.whl", hash = "sha256:3398e458047d21c9bc1b323fe5aab77c608dc9ddb65b2d44deebaff503a1f1eb", size = 1129094, upload-time = "2026-10-03T12:23:54.133Z" },
{ url = "https://files.pythonhosted.org/packages/16/05/ca16884f9498386f3646bb18be59f0e31d44e992d252d7d6f5e4f8ae1ee2/ast_serialize-0.12.1-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:410233de149ab8414cb27c6fc73e9d2baa35d6f971672d540d752060d980ffb4", size = 1235686, upload-time = "2026-10-03T12:23:55.863Z" },
{ url = "https://files.pythonhosted.org/packages/29/f2/34e87ed30e292cf365523712c4bcfef1967d9c3c2749de21b1f93b1fe0f3/ast_serialize-0.12.1-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:b9a2310845302f1a6bd45ae8a67d5760211103a8d66410b854bfa440d107e093", size = 1215598, upload-time = "2026-10-03T12:23:57.48Z" },
{ url = "https://files.pythonhosted.org/packages/f8/dc/c498f41c957b6ff31b97ed8ceccf3a84f85af7debca1125183cab95bb58b/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ff65f40f49d5e1a1a043ba366081d59a4e26a9f5c1b07eb1170e172115da7ca", size = 1281911, upload-time = "2026-10-03T12:23:58.954Z" },
{ url = "https://files.pythonhosted.org/packages/dc/60/70ccefae9d88058c4c234bf0aed93f54aca36eb74087736e76e9515aee96/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:536d783c4d91331f094e0a892221e619be5ffbe6fb6640885f14d7f226ec90ca", size = 1287296, upload-time = "2026-10-03T12:24:00.429Z" },
{ url = "https://files.pythonhosted.org/packages/08/e9/4fc697879c7128e29f9dab2ed19a9b586a56b621e5ea4aee2ae28c18e116/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c42d2d65f388d1960c5796231eb9bf5a988c46228633eb489605c4549ad16c52", size = 1532339, upload-time = "2026-10-03T12:24:02.053Z" },
{ url = "https://files.pythonhosted.org/packages/8a/9e/9e2bd489731602a94dbd0c576ebe1cc487a2d0f6127be743f44711166f0d/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5628a12acc875fe7a167910f18d101dd101c2a7b1e6c2b6f7289ffaff25805c", size = 1308988, upload-time = "2026-10-03T12:24:03.61Z" },
{ url = "https://files.pythonhosted.org/packages/3f/69/e9cae837bd766a66db6953ffb5fc7f04b1945e02b0a9e4c6a0b6acb08f17/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e855adfa5bb982b2e6fe09056b2d584f6dd4fce085d91a07d1155683751b6b5", size = 1303412, upload-time = "2026-10-03T12:24:05.62Z" },
{ url = "https://files.pythonhosted.org/packages/b2/1e/5ef8c62d5031d93187ed0d8dade5d942de9920c3fbd678c7652362b9a7a2/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:fafe1471e8aca6c87b4913b7b54ff97197adf702fbe28692284b929dfa62ff96", size = 1298466, upload-time = "2026-10-03T12:24:07.242Z" },
{ url = "https://files.pythonhosted.org/packages/c2/f3/25ded60844a1a437edc840e597b6f81daf91dc4a26035416e14298d3a091/ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a5cac246474d2a703147d1605a6ac5ba0fa9e0a443cf1cf42513adf4df02686f", size = 1355617, upload-time = "2026-10-03T12:24:09.125Z" },
{ url = "https://files.pythonhosted.org/packages/05/68/a0d3cc8d8042208a2cbef7b26483f4941b44dd5dd717bb19f20e4a4c0d66/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:6e25cd319fb0d7b39fcac666784ec86708ccbc78d07a698b1400cf5ed40c045b", size = 1458949, upload-time = "2026-10-03T12:24:10.772Z" },
{ url = "https://files.pythonhosted.org/packages/df/a0/5e4d355c48a9f125b8bec7b1b98d4d2dcd8324ff1d4dfcb03678a03c1414/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:657a7354ea16ed4d29f8127ed477c6fee3915c111d135f020ca835a991438e90", size = 1562328, upload-time = "2026-10-03T12:24:12.404Z" },
{ url = "https://files.pythonhosted.org/packages/8a/5f/3d40f6a7908200f2f0ed9ce1bad130d00e06d0405baa7918d4a4299b25dd/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:9eb9de7e59621acdb3e66984f374272b33d56d15a04763e2fd0604211e1c8303", size = 1557751, upload-time = "2026-10-03T12:24:14.455Z" },
{ url = "https://files.pythonhosted.org/packages/3f/13/d53e5a7e299d6dbaeab23a424eea2c98c821b46ed7b05abbe14743beeb63/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:09cc4d3103c1fc97f6845ba307af1db9cde5226bef47f8843220dde83f2276ba", size = 1666238, upload-time = "2026-10-03T12:24:16.171Z" },
{ url = "https://files.pythonhosted.org/packages/37/5b/7638ee3ae35a64e4467160a37dc7565cddfe2a87f06cef2fd07c93cfd503/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:c9e2a592706fd791c2271ce9c8f4e38c98d3ea0b4a86b511e09a4fe3ac44ab37", size = 1472620, upload-time = "2026-10-03T12:24:18.053Z" },
{ url = "https://files.pythonhosted.org/packages/fc/e7/6e9e621e0e4a3be5a9a5f8b6961982964d2367013dbe64feb5adaa43e56d/ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:f4ac042e95a575432c1730ca4cb9183066a2074886a599f46c1f1b0955fb8198", size = 1500141, upload-time = "2026-10-03T12:24:20.036Z" },
{ url = "https://files.pythonhosted.org/packages/4a/4f/3217da5b671711c09cc6be580095839cad539983662a6599405bd75c1a19/ast_serialize-0.12.1-cp315-abi3.abi3t-win32.whl", hash = "sha256:b3cd105995942cc6163a229674a86161ba1305f646493efd59bd6723a357ee14", size = 1118382, upload-time = "2026-10-03T12:24:21.599Z" },
{ url = "https://files.pythonhosted.org/packages/80/1e/6074cf29dca8ceff27d50e845c88e7a2eaaa7f0b6f3972909e878d844737/ast_serialize-0.12.1-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:a9cd24a26126088693ca054547ea0a391398a29cf1a3a2bec1009b4b6acc8b82", size = 1158034, upload-time = "2026-10-03T12:24:23.316Z" },
{ url = "https://files.pythonhosted.org/packages/92/a0/81ce428f9f3f1ca45f8b62c9711c30452bf8190476e8685cea0f72d8d008/ast_serialize-0.12.1-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:9649cd903db0dc047906c6dd740784a2ba665d54f7e43ba31457edbce76c9493", size = 1129437, upload-time = "2026-10-03T12:24:25.044Z" },
{ url = "https://files.pythonhosted.org/packages/3a/d9/1c08adb90728607d0d07d188df4558ae863d688b4458087efe9fafeca458/ast_serialize-0.12.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:5ef62601db3ce5c23445132262a193075e211fb2fc87b46b7550dd351fac0976", size = 897419, upload-time = "2026-10-03T12:24:26.699Z" },
{ url = "https://files.pythonhosted.org/packages/80/fb/1eabd2c0673283054468b1c6cb539aeb877636d6c84b280279f2d7a177a9/ast_serialize-0.12.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:98d91cd3a6cb76a39512ee090a539d1e3206b732ad8150eb38918cffa1ddf515", size = 1241515, upload-time = "2026-10-03T12:24:28.493Z" },
{ url = "https://files.pythonhosted.org/packages/1b/d7/c56955934a431a0fa3e4e9aa7af4a53ceab2a61241005427545208945eb4/ast_serialize-0.12.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:8a32f184ce3e4b1d0b06d642a1243281cf55b99e0323680b1b8f904029fd7700", size = 1228048, upload-time = "2026-10-03T12:24:30.556Z" },
{ url = "https://files.pythonhosted.org/packages/1e/4e/2b2ca4602baf92f842316ea617423402089df4fbd2ea42571ba28725ba46/ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e62126ac2be2d9340ac1b3ee7a0466a883ecbed634cff0929c88ca0b671483b7", size = 1292087, upload-time = "2026-10-03T12:24:32.45Z" },
{ url = "https://files.pythonhosted.org/packages/d9/49/9ebd05218a87ca31f4f855d5e3df14239bba3c58f2aed9d02c7cba5d94f5/ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0f93a70fa9826c04ea9f2c3a880f87f4cca09a828144ed5084682abd28110980", size = 1295355, upload-time = "2026-10-03T12:24:34.423Z" },
{ url = "https://files.pythonhosted.org/packages/dc/09/6db7c4327e7a56aba805f7190d377a159fc0bf6bdefb410dc7860624dfa3/ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1858887be56a64a2aea899423dfe43787c34c75c18b0d7497de8e618d54b2790", size = 1541231, upload-time = "2026-10-03T12:24:36.05Z" },
{ url = "https://files.pythonhosted.org/packages/ed/85/7ab6097e5fe23cd4657b0e5a2fabb4f789f91e441a3ee40b3ca8b79be238/ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:89a2bc39a820bc7785b60c53a5742b4e8dd4c1a599294e2dd68fae545883d44a", size = 1317324, upload-time = "2026-10-03T12:24:37.737Z" },
{ url = "https://files.pythonhosted.org/packages/c0/60/58961e7fd129e226ce36788fe328d20034f3105f5d3380df690050517737/ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50a9eaedf1db4857dad7cc47dd757ed70bfcc40b89d44d516c4a2f0d5033bd76", size = 1311809, upload-time = "2026-10-03T12:24:39.306Z" },
{ url = "https://files.pythonhosted.org/packages/15/c7/09d973db87d4575cba470fd80a3fa489322f7d6882546e43a4b21012468e/ast_serialize-0.12.1-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:c30b609e8fea426b310543126de876592236a25aa8ebd59f1e2b323dd52a4085", size = 1306678, upload-time = "2026-10-03T12:24:40.891Z" },
{ url = "https://files.pythonhosted.org/packages/84/27/84f69c22bcdaa5256b4fe43ff972fc117668fff8e68495807d5792eadcce/ast_serialize-0.12.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b1ad06513022cfa1337744959255af0ef16119d2beb1e547b67f37ad9433d4a", size = 1362009, upload-time = "2026-10-03T12:24:42.809Z" },
{ url = "https://files.pythonhosted.org/packages/43/46/76ee342ef22cd6d82ccd6089d5e2f7163246de73d1816ccb4b6ec0550db6/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6add54b495e37ae3cf3a1f0d5eaba364814eb72e93026adc41b7791e4b0d45d3", size = 1467959, upload-time = "2026-10-03T12:24:44.496Z" },
{ url = "https://files.pythonhosted.org/packages/31/4d/18e48154bbf6058eed8d9b54fcebb2e130f8a380db1a2a202b4faac48626/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:5fc136cd08001b817ad0b3e7426f50a7d2b8982dc7c6491f0af78af4c3dd8672", size = 1571157, upload-time = "2026-10-03T12:24:46.156Z" },
{ url = "https://files.pythonhosted.org/packages/34/76/6b16ddf0510e713613a5f5441b13407c1bde6158df04946b9f1fdc65add3/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:45a9e6b700bbd973d49942668a5cdbeffa693f2e250b8a5409abd1fa9d351854", size = 1566873, upload-time = "2026-10-03T12:24:48.542Z" },
{ url = "https://files.pythonhosted.org/packages/75/33/9f6169ae7f60c2da4baec03450073d3f1edb39e95ab538be0d25a7d2f72e/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:a1d8267f83c613ea0a31f2518df074bd62e98a4b3a4892f6a529d74e08e02dba", size = 1673030, upload-time = "2026-10-03T12:24:50.566Z" },
{ url = "https://files.pythonhosted.org/packages/11/51/0d78755bd61d6cf8980f0cfdc7fa8ede38df46a5423c9f7a3da0cff587ec/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:9a0cbab9796e6ce841197feeba4008faa96b4cc7741129542fd81c882d7a4f01", size = 1480052, upload-time = "2026-10-03T12:24:52.277Z" },
{ url = "https://files.pythonhosted.org/packages/01/ae/ad4c0e5129991f2761f388420c5ded37cb134ec5882e3e59043d33c1ad87/ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b4282695f1d51a3c6ef76560351bad5af880eff7d755aefea325bffb9bf68c25", size = 1508940, upload-time = "2026-10-03T12:24:53.846Z" },
{ url = "https://files.pythonhosted.org/packages/38/6b/3299182794d38815ae6e9c7ede9bb8f2e4aa93c3578bed201c1ea746643a/ast_serialize-0.12.1-cp39-abi3-win32.whl", hash = "sha256:119d1b0cadaba4a6e475f9bbe79eecc79351e373142eabe7c353f0250d70aebb", size = 1125618, upload-time = "2026-10-03T12:24:55.405Z" },
{ url = "https://files.pythonhosted.org/packages/86/14/5d4fb733c18a1d69e237c067b183842f3a7ea1c999a51ddc87093a281c88/ast_serialize-0.12.1-cp39-abi3-win_amd64.whl", hash = "sha256:3d6ed63d4fc1ec867b8cb522d58c36df0e8f05e487bea0ffd102043a37636d72", size = 1165798, upload-time = "2026-10-03T12:24:57.052Z" },
{ url = "https://files.pythonhosted.org/packages/f1/f4/b54123680025c0b7253117418f023d1b2487f1102552acbdd9d8ee96b622/ast_serialize-0.12.1-cp39-abi3-win_arm64.whl", hash = "sha256:610a41351de68199de9a1434499083b4256c0df7658ec1cfc0a0a7b20b08d317", size = 1136560, upload-time = "2026-10-03T12:24:58.689Z" },
]
[[package]]
name = "black"
version = "26.5.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "mypy-extensions" },
{ name = "packaging" },
{ name = "pathspec" },
{ name = "platformdirs" },
{ name = "pytokens" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c0/37/5628dd55bf2b34257fc7603f0fe97c40e3aaf24265f416a9c85c95ca1436/black-26.5.1.tar.gz", hash = "sha256:dd321f668053961824bcc1be1cc1df748b2d7e4fa28086b08331e577b0100a73", size = 679439, upload-time = "2026-05-18T16:53:36.107Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a6/16/a8da8eb208c51c7f4ce74609a45d0dcc6d8a2141e45e81ee5289d1bb0d59/black-26.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e88976690a64b0af98312ca958415849cb42423423c5f2ee74af4b49a97a2168", size = 2004800, upload-time = "2026-05-18T17:05:38.182Z" },
{ url = "https://files.pythonhosted.org/packages/11/8a/a479296a19e383b70a725882a6cf3d786540601ff03cabbaaf1cce864c5a/black-26.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32d5ea7f6c8bdfa6e648326ebca1f02b0764e2a029edc6f8dce2627e19d468c3", size = 1815576, upload-time = "2026-05-18T17:05:40.309Z" },
{ url = "https://files.pythonhosted.org/packages/81/6b/cfaf3d39f25132c156a068f6b805576c9103a84086019507c70e1911ee7d/black-26.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea8d16dc41655aa113cd64665e7219446cd7e4ff2248d7178eaa905190c86b18", size = 1877927, upload-time = "2026-05-18T17:05:42.463Z" },
{ url = "https://files.pythonhosted.org/packages/66/76/302e313964bcff7e28df329d39f84f5270095730d85ff0acc260610a0d82/black-26.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:577f21094ea469ef92ec1adaf2c9441a226d2144d01a5be2fa823cecf6543e50", size = 1511860, upload-time = "2026-05-18T17:05:43.943Z" },
{ url = "https://files.pythonhosted.org/packages/27/4e/a3827e35e0e567f9f9ee59e2a0ab979267dca98718f25547ca8c6733afd4/black-26.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:ed1a20af114c301a0269bf01163d51dbef72737fd65f850001e7cbe7f3c7abae", size = 1316632, upload-time = "2026-05-18T17:05:45.521Z" },
{ url = "https://files.pythonhosted.org/packages/94/51/f975cae76d44274cc2868dc9040ac5d58d464784610234455b4e7b19c6ef/black-26.5.1-py3-none-any.whl", hash = "sha256:4ed7f7da04046d2e488437170797d3b4a4ad83906683bcb7dfc68b673bbce5e2", size = 213693, upload-time = "2026-05-18T16:53:33.964Z" },
]
[[package]]
name = "certifi"
version = "2026.7.22"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" },
]
[[package]]
name = "cffi"
version = "2.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064, upload-time = "2026-08-03T21:20:17.148Z" },
{ url = "https://files.pythonhosted.org/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720, upload-time = "2026-08-03T21:20:18.268Z" },
{ url = "https://files.pythonhosted.org/packages/67/b8/b42132ca113dc567d37684437b46ca1dafc885902b02a110a02d5b511857/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", size = 222328, upload-time = "2026-08-03T21:20:22.118Z" },
{ url = "https://files.pythonhosted.org/packages/e9/02/4e7d553a7ac4b4238b38b3c1b80d486e9d4436f8d2acbf87a0997fe3f402/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", size = 221525, upload-time = "2026-08-03T21:20:25.758Z" },
{ url = "https://files.pythonhosted.org/packages/82/1d/a4aaf9babd75acb4d5f223bff71533bee748dd770a382619a798960ee9ba/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", size = 225053, upload-time = "2026-08-03T21:20:26.985Z" },
{ url = "https://files.pythonhosted.org/packages/81/10/5dc0e7bdd18e22107054288283380fc97a06ae3f1656a106908d666a3c88/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", size = 223213, upload-time = "2026-08-03T21:20:28.277Z" },
{ url = "https://files.pythonhosted.org/packages/2a/9c/92934c3bea9f785b23eba304538c0b4d37a2a96d2431eb3a1bc87a11aa19/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", size = 223864, upload-time = "2026-08-03T21:20:32.571Z" },
{ url = "https://files.pythonhosted.org/packages/9a/95/eff5f0cee78d2eabc7eebffec40d3fc1876b5f3c95582e018bb4b99601f2/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", size = 223803, upload-time = "2026-08-03T21:20:36.564Z" },
{ url = "https://files.pythonhosted.org/packages/fa/01/579d39fb8bef00a335a23d83757b44feb24cd6345a2c451b64cb67b9c362/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", size = 226763, upload-time = "2026-08-03T21:20:37.816Z" },
{ url = "https://files.pythonhosted.org/packages/8d/b0/0b44f47c60b01b57b6e2bbd92343f13a85a1d93bc46ccf6e47e244acd99c/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", size = 225688, upload-time = "2026-08-03T21:20:38.959Z" },
{ url = "https://files.pythonhosted.org/packages/c1/8f/9ebe220eab48a093d1a5a5e339ab0dc7316eef3bb04d63c42f0251b61f50/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d", size = 194043, upload-time = "2026-08-03T21:20:48.179Z" },
{ url = "https://files.pythonhosted.org/packages/ff/69/844bad3ece306c4782c2ecb93597035b6690d48704b803914c199da1e8b3/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b", size = 196737, upload-time = "2026-08-03T21:20:49.457Z" },
{ url = "https://files.pythonhosted.org/packages/e2/31/9e1313b0a6e30e91b3b3d3fff51ae99c857c07738e3afcce1f7334e1b7ab/cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6", size = 222271, upload-time = "2026-08-03T21:20:53.462Z" },
{ url = "https://files.pythonhosted.org/packages/44/16/29e6d01b388bef055ecd6ca8244b3f4d336bd09e92d5d892187b9601084e/cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399", size = 221630, upload-time = "2026-08-03T21:20:57.336Z" },
{ url = "https://files.pythonhosted.org/packages/a4/18/fa7f1f6857d5eb88a4ca99ffcbfb7c387a287ccc154c64a73e86314745d7/cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688", size = 225134, upload-time = "2026-08-03T21:20:58.675Z" },
{ url = "https://files.pythonhosted.org/packages/e0/9f/e8e3dfa04a1b4c241f8c91faacad872b4d4efd051d49764ad4e2fd4b9fea/cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7", size = 223197, upload-time = "2026-08-03T21:20:59.968Z" },
{ url = "https://files.pythonhosted.org/packages/53/b2/6187f46f2912276a3ae284076109cc5c8680482f11f766ccf26db4a86427/cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e", size = 223779, upload-time = "2026-08-03T21:21:03.553Z" },
{ url = "https://files.pythonhosted.org/packages/52/86/2976131c639aead931c5bee5aba67e4b09fbeb8018b6f282f70803f923a7/cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6", size = 223835, upload-time = "2026-08-03T21:21:07.539Z" },
{ url = "https://files.pythonhosted.org/packages/ac/0c/33a7aeab2f9c76918c52e084beb39c570db3588133412929e8ec06fab90b/cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94", size = 226705, upload-time = "2026-08-03T21:21:08.774Z" },
{ url = "https://files.pythonhosted.org/packages/e3/26/2cde30fdde421130bfc18f70395731a6e6b2053c6a1978a5258ff04e72fa/cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5", size = 225539, upload-time = "2026-08-03T21:21:09.911Z" },
]
[[package]]
name = "charset-normalizer"
version = "3.5.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/33/1c/f41d4e74c28ab327ff3acd36053f7ea506c55872d7a90b0fa71aa3ab0c89/charset_normalizer-3.5.2.tar.gz", hash = "sha256:39de2a259fc954455c57274dc94c79d5842774e1247a016aff30bc0efed0f4ef", size = 172659, upload-time = "2026-09-30T04:39:23.398Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/58/ca/7aa91362a2f77ac8e9e28a9b902a74f7d0e11a851ef0d27a74308da8cd90/charset_normalizer-3.5.2-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:1afb975bd5d68d5ce9f6b6d44fdf2f7e34b895a35e95708a7a91b20a3b51d187", size = 226010, upload-time = "2026-09-30T04:36:27.669Z" },
{ url = "https://files.pythonhosted.org/packages/a8/cf/ac8878d0322cf88a1aad4c7b147db32ca0bd806eb0060957b2e31486dbe6/charset_normalizer-3.5.2-cp314-cp314-android_24_x86_64.whl", hash = "sha256:bbbfc8e28816f19d7c0f1816664980c0a9875d01b27cdf8eedddb639d9e108ad", size = 239266, upload-time = "2026-09-30T04:36:29.434Z" },
{ url = "https://files.pythonhosted.org/packages/c9/6d/9a08d7e0b29b7208e2c6c01dc56c8e0520e7c7beadbbfb024b58fd69c8a5/charset_normalizer-3.5.2-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7967d08cf06dee78443b874f98c98036f624f3a4e73e11f9f64f5be4d25393cf", size = 206783, upload-time = "2026-09-30T04:36:30.872Z" },
{ url = "https://files.pythonhosted.org/packages/82/44/b0aa350280e6ff5a5492d17cf10460dd39d5ee848f872f7ba2df10607f60/charset_normalizer-3.5.2-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c2b5031f63e331e3839b40aed2dd6f191e9c07edbde303e7876846ea1946995", size = 210449, upload-time = "2026-09-30T04:36:32.625Z" },
{ url = "https://files.pythonhosted.org/packages/7c/8a/40db9aa9f5907bb0e6f8b6d64064bf8852fb33d4b813ff9414911df7647c/charset_normalizer-3.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fcff63213e8e6e47770541a4607175404f47cbb3ebea7b6058cc82d524a0e424", size = 367815, upload-time = "2026-09-30T04:36:34.197Z" },
{ url = "https://files.pythonhosted.org/packages/7f/72/9c5e7707b57c8ddfa9ddf7b0b1d009d7fbab9e9e887d5b721060f37e307d/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8d86d6fc60743dc916eb79e2eb1ec4818e21e427731543af40a3021851174a13", size = 247095, upload-time = "2026-09-30T04:36:35.803Z" },
{ url = "https://files.pythonhosted.org/packages/83/09/71e453691e927de4ddf792770cfaab3f49d494e222f66ea5e404bbd5e39c/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a881931aa470808df94a8c380eed2bbbc76cd9dc622310f99665658c821eb6d", size = 233942, upload-time = "2026-09-30T04:36:37.407Z" },
{ url = "https://files.pythonhosted.org/packages/9f/86/85c84e4da8b27dd409577d9437926ff581c5f9d3c66038dc68c1a526de51/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8024d00c3faf3fc0c16e07a69f4405e8eac7cc0ab15f65fe6cf43827c4cf72b4", size = 273762, upload-time = "2026-09-30T04:36:38.904Z" },
{ url = "https://files.pythonhosted.org/packages/92/08/564955a4b5f2ccb410ab480bbe8c6a18063ff27f2d35458731c4a5335df9/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4d48f2d08b9de5864e2c8744d4461b862fb149a18274abc8b698c45975573438", size = 269514, upload-time = "2026-09-30T04:36:40.469Z" },
{ url = "https://files.pythonhosted.org/packages/18/24/bad3ac4271589df29cf5ce2f5ae490518a5739358052bd0d61209e6fea54/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34276fd796040bf0993ab33a369aa572e6979c7aab225a88893667ad8eac8f7a", size = 255056, upload-time = "2026-09-30T04:36:42.02Z" },
{ url = "https://files.pythonhosted.org/packages/d6/3e/350d89ad49916b86554d6f5f2d03ec1152148f87e5ff735106c6a03b1a36/charset_normalizer-3.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0521c5665880b33d603717defa76c094048900010897909952397feb3039da56", size = 251311, upload-time = "2026-09-30T04:36:43.577Z" },
{ url = "https://files.pythonhosted.org/packages/56/5b/4970a2d154df502e133402906dd04e3ae7cada7b3011283c88d0479a2585/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:eff0ac9dbe711a4aee69bf04a83896aa9b85f19641264053a9f6d48573abb7dd", size = 250190, upload-time = "2026-09-30T04:36:45.185Z" },
{ url = "https://files.pythonhosted.org/packages/88/8c/f1a91bddc8fb47c2889e29ea7ea49a194eb0d9868675d786806519c00d76/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1503bccbeb36d5527790c3930327704c39af22de3112f1b1666a9f3ce15ee204", size = 237098, upload-time = "2026-09-30T04:36:46.689Z" },
{ url = "https://files.pythonhosted.org/packages/24/0e/bb5dace3cc7e79068425386a6589c19b5a2ab5fefc2a46abea6919683332/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:52aa6992700996af31f375de0c6bacd402b0097fe40b53c426b9f51a90ebabc7", size = 274228, upload-time = "2026-09-30T04:36:48.31Z" },
{ url = "https://files.pythonhosted.org/packages/9d/79/b849ad523017ea9f5a45581bbebed91439e0cf42fd2860a6f64e358eb5a6/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e09a3942ecbdee5cce73ea9d42da82b81b72ac1bf031ce069b93b5adf4eac8cd", size = 252401, upload-time = "2026-09-30T04:36:50.091Z" },
{ url = "https://files.pythonhosted.org/packages/89/8c/75469d690cf47200bce8f6cad7655724fc23148e147abfc5ce78b5f65863/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:c7c9ab723cde841fefb34efbad91e87f00a674b1fe1cd0784fde742bf2c154dc", size = 271846, upload-time = "2026-09-30T04:36:51.719Z" },
{ url = "https://files.pythonhosted.org/packages/26/cd/6d52d3c7437cdcf2e310ce9f28f282e733d4ef60ed19105d1819c356255f/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ddc7dacc8ece3a182e7f15cb862d1fd616b46d076cb1ae9dd232b2c38b655874", size = 258151, upload-time = "2026-09-30T04:36:53.234Z" },
{ url = "https://files.pythonhosted.org/packages/f7/4c/070b38bdb5f49a70199fce923ec0726a49536a63ab262abbfcaaf351110b/charset_normalizer-3.5.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:ee43c17b173d46a3212baa6ead3ae258eeabdae48c263a01ccf0218c366dd655", size = 143570, upload-time = "2026-09-30T04:36:54.816Z" },
{ url = "https://files.pythonhosted.org/packages/81/84/9ebfc8ed6c8c4fcd8e726ff6bf220cc8deb3966e31dce9be8dd8aa017e64/charset_normalizer-3.5.2-cp314-cp314-win32.whl", hash = "sha256:4f87960d57feabfb618e4e0af6e7371645fa26a277860739d6e5d6e0012c92f0", size = 186412, upload-time = "2026-09-30T04:36:56.643Z" },
{ url = "https://files.pythonhosted.org/packages/d1/78/5ed86f743d4bc350db307e7636419a0a5ee1d91806d30c7f667bd5c80dae/charset_normalizer-3.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:e4e81e09c1578b8df602e3db08b0b3ea0a6947ad612f52bf8dc5ea8d47691f0c", size = 210965, upload-time = "2026-09-30T04:36:58.205Z" },
{ url = "https://files.pythonhosted.org/packages/53/94/a3a7698e9b1a395e1eb99ccd9a324be9347973bff4e72db2a06496d7cd27/charset_normalizer-3.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:80d02b6f04e92601a081dd97b23d3128033098bff5d35d392ddcc0476ea11253", size = 200406, upload-time = "2026-09-30T04:36:59.764Z" },
{ url = "https://files.pythonhosted.org/packages/c1/48/c5dd00d5ef7791f02666de250a5bb6071e29b7e133cf4b835800b6d3bc27/charset_normalizer-3.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:dca9ab98072a5a54ebacebdc45f53e645336b320c667410b061be1ca588ae709", size = 396871, upload-time = "2026-09-30T04:37:01.543Z" },
{ url = "https://files.pythonhosted.org/packages/12/c8/8379554b42e8368161d898476686947a0fdbd3e8865170d7909dcabfdee8/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0aa869112ef88429ae17820d99c3dd9504c9e9c671d3c246f3d7442cb051084", size = 249711, upload-time = "2026-09-30T04:37:03.111Z" },
{ url = "https://files.pythonhosted.org/packages/4a/eb/2ddb1035d17320caa9f41682935123a9a250277b261c3efc86b2d2a21343/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c0afc6800ba57ccc350374c5bd6150419915d95ce93cdbab2d783d75eaf30ecb", size = 231146, upload-time = "2026-09-30T04:37:04.721Z" },
{ url = "https://files.pythonhosted.org/packages/4a/24/2ecb4bde104322cd7859d6594fcfa74649f8d90b3221c9feecbef149875b/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7dcd882da75ef9adf94903b1e3b9419e8aa8fb4c7396822b834b9ef7fb96954f", size = 271552, upload-time = "2026-09-30T04:37:06.295Z" },
{ url = "https://files.pythonhosted.org/packages/3f/98/9d5f6ebc3aee9fef5d30b4aff11fb2ab7a1222b4064f8ef2c7c87cde217a/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2e06a3a98f916dd41d27f3105e02e7a40181c98c94b9158733d03a6f80506c09", size = 269810, upload-time = "2026-09-30T04:37:07.905Z" },
{ url = "https://files.pythonhosted.org/packages/09/e1/a3b06a10461b1b7628853c934c644e03bc28e42767116afb52f19a56519b/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6bd128f206a7752ae1f2ab6c61bf8a24ba28913a10df8b14c2637b973ff97a80", size = 259845, upload-time = "2026-09-30T04:37:09.554Z" },
{ url = "https://files.pythonhosted.org/packages/fd/d3/6f561f74a296cf27d61775a1dc665ad13f3bff6a798810ca05907f37a7c4/charset_normalizer-3.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c8f3d67aeaf55f017982b73683f0e7342ba2f6635a78f69ce89ebb26aa411e5c", size = 252825, upload-time = "2026-09-30T04:37:11.274Z" },
{ url = "https://files.pythonhosted.org/packages/26/9f/69e13ca3b18f43e0eafcd34c04a45b732ae22a43b54a5fc9e119103356eb/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fe9753dfee015c570d73df76f899f18444d41388bffcde097deba51c4fadbb9f", size = 252659, upload-time = "2026-09-30T04:37:12.941Z" },
{ url = "https://files.pythonhosted.org/packages/73/a9/ace29806a0dae18939919c76ba526472d83214afa101105fabff2cf30625/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:92888bb3187c5ba50500b00b3b310c9f2c651709d28036077680cb5255450a03", size = 238819, upload-time = "2026-09-30T04:37:14.659Z" },
{ url = "https://files.pythonhosted.org/packages/f8/c1/6116d52a2e3311ec80f21f5fb5e17b27405f10b9608af8f6e69516841a1b/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d008d90a7f2471519aef0c90dfbe73b3e6e4d5e66ac48e19154c17e89e98b604", size = 272973, upload-time = "2026-09-30T04:37:16.346Z" },
{ url = "https://files.pythonhosted.org/packages/19/aa/9955c7e93bba10a9c7e8f7a5031b7ced66f3a1883a55c00712b8d5850ff3/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31f3930700408d211f13378ccbe1c40845d8da54bd0681fac3a9b5aae81c7aa8", size = 253692, upload-time = "2026-09-30T04:37:18.212Z" },
{ url = "https://files.pythonhosted.org/packages/bb/33/2a6ae7fdc1b10cb581cef91addd8cdfc5f40d50abb5702309369d5834579/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:2a925889534b3748302dae5dead07cc13480de1dac3aea80a941b729b471ef93", size = 268757, upload-time = "2026-09-30T04:37:19.877Z" },
{ url = "https://files.pythonhosted.org/packages/a2/22/80992720a0282cd39bba1db35868e6b9c22f41281160143a836544bc1d8a/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f5ec61164adcec446f8969a3358ec3f9b26bbda3b9213e5586d219afa8df2915", size = 262220, upload-time = "2026-09-30T04:37:21.583Z" },
{ url = "https://files.pythonhosted.org/packages/92/9f/181fd07e1bffea1d95cd80c84ac537354f50699c22cfc4d3c02b6fc16208/charset_normalizer-3.5.2-cp314-cp314t-win32.whl", hash = "sha256:598a11a2c7ebaa5334bf698bf29568c9c390abac6a154d8170fedecd1cea38c5", size = 197797, upload-time = "2026-09-30T04:37:23.235Z" },
{ url = "https://files.pythonhosted.org/packages/49/1c/25d8415ec1c4f2f41f1680435e4c87cfb378ff2f677d950946f2a45d0632/charset_normalizer-3.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:7fdde2c9fd9e3eca40631e024664cf2584272cc8f96308cbe5fdfc930f51d8bc", size = 222765, upload-time = "2026-09-30T04:37:24.891Z" },
{ url = "https://files.pythonhosted.org/packages/3e/b4/46b48f013dadfc0d0d33b375438e31bdf5a989dc68389c6bf627054d4df9/charset_normalizer-3.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d1befeed746d247c81127bb14de9dc3d30edb6e5976d34f83f86ed262b1d9105", size = 210985, upload-time = "2026-09-30T04:37:26.634Z" },
{ url = "https://files.pythonhosted.org/packages/ca/e9/34e597dee616d0b8ee4b34d29399e85c2204ade174157a48505d42baa4ff/charset_normalizer-3.5.2-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:87475fabc8d9996fd9c27debb395e642e8c838d78a00b6e932227a0e06b81e26", size = 367676, upload-time = "2026-09-30T04:37:28.329Z" },
{ url = "https://files.pythonhosted.org/packages/60/9f/a5d1c91c0263745e2cd344c5a4415d787c575501ab1d449f1148ac6b495d/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9409a8bf35cf78353942504b24a57de3d75b708997a1e4bd8db71ac8633ce364", size = 248414, upload-time = "2026-09-30T04:37:30.167Z" },
{ url = "https://files.pythonhosted.org/packages/26/79/e697f77464748a3ee3cf490c83d592459400d4898380d66c38366b03080c/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:498dc3188ca05a68231ac3fdbfc7f57eb67e1343c30e0fea17f8218c1599b253", size = 244775, upload-time = "2026-09-30T04:37:31.964Z" },
{ url = "https://files.pythonhosted.org/packages/ca/87/3d42a42e18ea066e2513936fd678a00696e77878b5ae04528976abdbcb83/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e242bb1c5e76e97dfa9e7f209a71e93a01d7f19ffdd5cfbb2e2d55b4f08f8ab0", size = 273877, upload-time = "2026-09-30T04:37:33.661Z" },
{ url = "https://files.pythonhosted.org/packages/c3/76/8a28136f3938ba9836f84280ce0c4d61ed1cf15a036b2034900c62634162/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:def79fa35ef0cef8d2accec024f4fdc7ead3012ff02f5215c783f39f03ef8cfc", size = 269455, upload-time = "2026-09-30T04:37:35.573Z" },
{ url = "https://files.pythonhosted.org/packages/a0/a1/4fbf5d0f0f1b2a080474c1cf9a2f12c4c6531bb0e8ba591055e846d2b4e9/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df041de8887954562c9b261cba85ca0e9ded74048daf125f45edcfaa4832229", size = 257336, upload-time = "2026-09-30T04:37:37.397Z" },
{ url = "https://files.pythonhosted.org/packages/ba/a2/8b50aa320adb880ad579518e6f718f24944804b42a88b83d267d5d444125/charset_normalizer-3.5.2-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04851f73ae72b8413dddadb16a49dfee95263553741fd42d546f7d66907e6be5", size = 257584, upload-time = "2026-09-30T04:37:39.522Z" },
{ url = "https://files.pythonhosted.org/packages/a5/57/50e3fed84e175f40349bd0da7a4fce94c87f0378f52d74f511d89e0bdc20/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:183b88127acdb4fabe59d951ab424faf1af7b63cdbb5f776186c1ea2ffcaed98", size = 251854, upload-time = "2026-09-30T04:37:41.23Z" },
{ url = "https://files.pythonhosted.org/packages/d6/54/f7fbb3493c9f49091213b9c2d6dd65800696f1ce1a3f196a4205f50417b1/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:16fa0eccf81304b79c5cd87f9271c3b85dd9dd99245e4422ae9c0dd45e0f99d3", size = 248767, upload-time = "2026-09-30T04:37:42.883Z" },
{ url = "https://files.pythonhosted.org/packages/d9/37/b3a6385acc5a1e45b39ae9c90bfb9cf838a09b9dd37ef2740ab4c6b4a2eb/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:7441d755b7ab94f8d4eb3e43ec05482d760842fd263d003a99102d742cd835e2", size = 274479, upload-time = "2026-09-30T04:37:44.658Z" },
{ url = "https://files.pythonhosted.org/packages/89/44/809913e2cfd279e635a9294fdbbfb1b1dc62a8189d473d561f649fce98d8/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:ca403d7e4798f525fdfc78e258820419cbbd0f0ecbab9de7840e3c017cf6b8cf", size = 258310, upload-time = "2026-09-30T04:37:46.529Z" },
{ url = "https://files.pythonhosted.org/packages/af/a2/f28400ab13359d91bd39179df8e149376b9bf36588e739a3a4f9de2b84b2/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:df29a0a7107f7011e77f4eebdddec4c7331e24d787a0b21a46d63bdf7445da95", size = 271100, upload-time = "2026-09-30T04:37:48.399Z" },
{ url = "https://files.pythonhosted.org/packages/e9/89/9bab37955edf0adb3b66f8a3a6617d9f2f487e0d56f295a6a286cb640aa6/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f3c96f633825733f735c5a9cf21d21a257d8e1edf0b1cee0a064b9c424ca0f7d", size = 259936, upload-time = "2026-09-30T04:37:50.023Z" },
{ url = "https://files.pythonhosted.org/packages/23/b5/4459e08d45a679f903d50fea08bc52cfa728cca4d7bd02c757b5e5abda2e/charset_normalizer-3.5.2-cp315-cp315-win32.whl", hash = "sha256:281cb91036248400f4cc957495cccd44c275c2e0c5854f7e45ac5cf7dc193847", size = 186301, upload-time = "2026-09-30T04:37:51.722Z" },
{ url = "https://files.pythonhosted.org/packages/98/e8/55d5fd3935b4bce6da4fe0df61898e8c82653e317e677bd58aceb9c60f13/charset_normalizer-3.5.2-cp315-cp315-win_amd64.whl", hash = "sha256:89b53f3cda69831909888e0494f4fa0bcd3537e3e138dabeb620bd6ad946bae8", size = 210858, upload-time = "2026-09-30T04:37:53.427Z" },
{ url = "https://files.pythonhosted.org/packages/a9/5b/974423c2fd8e524c7a7f64318c1e02240ef954912fa2b4d70344107b9c68/charset_normalizer-3.5.2-cp315-cp315-win_arm64.whl", hash = "sha256:6be488a102b8cf28d0391d8c4ba7748938ae28b78ad901f8585520fca33ead1a", size = 200309, upload-time = "2026-09-30T04:37:55.015Z" },
{ url = "https://files.pythonhosted.org/packages/ee/f9/00ee0195db1013d8f7c416fd770fbeb560bb46eb2e36b054d05cb56f6cfa/charset_normalizer-3.5.2-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:915563965d418f986e7e145accc592eae9e1a1be3566ff98a05d7a9ec42a76e1", size = 395343, upload-time = "2026-09-30T04:37:56.743Z" },
{ url = "https://files.pythonhosted.org/packages/04/3a/c00b50e94c964cf934c7899cd47c97952fc11dad71cc5884b3c61795b09b/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65cd72beeeca9d3aaea1201e5923859f308f952f9c71de93f06063c79f0f7a3b", size = 250114, upload-time = "2026-09-30T04:37:58.607Z" },
{ url = "https://files.pythonhosted.org/packages/50/27/d102dc880bbcffd0479ab64dfc1fb96777a854355a55e2bda72a71efadcb/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b7fd005a73d9e657273b7a10dc71a9e03c8fb9ee6999798d6918ce095b81ac7f", size = 239997, upload-time = "2026-09-30T04:38:00.511Z" },
{ url = "https://files.pythonhosted.org/packages/a5/4a/bf7ef45794dd293fab5f98a9309817977fbb845b9998f171b8cc5d8437a3/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e54da4baf05720032d527874d40b65fa4d7e5c6c6a43d0c3adbeffcaf275a2b3", size = 272313, upload-time = "2026-09-30T04:38:02.509Z" },
{ url = "https://files.pythonhosted.org/packages/e8/ee/008a2837737991474c5754bb3191010007663860979701990982a502cbaf/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:124fbf1a8ff966d87ae05bb8bd45a71f966055ed8bba320d0c7cf450bc5f4d0e", size = 269986, upload-time = "2026-09-30T04:38:04.435Z" },
{ url = "https://files.pythonhosted.org/packages/93/ad/bd74a283940dc910c5b14f8e4f80a248082bc9c0fcbe1f54530cb6d9cc5e/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28b4f0d66fb834ff90f28209ac7bce77868c45d8c93e26f906709d9b7c2e1af9", size = 260202, upload-time = "2026-09-30T04:38:06.549Z" },
{ url = "https://files.pythonhosted.org/packages/8a/7b/ed341c66f69f688723501fac752be3d63c7159ca0d0d4174fc611e5710bb/charset_normalizer-3.5.2-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:58ca3755ee7ff7f59b57789ec9833c9de9ea275405cdd240eda1f193112e398a", size = 259083, upload-time = "2026-09-30T04:38:08.311Z" },
{ url = "https://files.pythonhosted.org/packages/cc/9d/e41588b777965e5031a43128a1e96173ebb35ac75fc53ec3b517e7c21cd4/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:443eae2bf318abeaf6f15d785138f71fd6de770e99a92158b8b814265e079115", size = 253081, upload-time = "2026-09-30T04:38:10.402Z" },
{ url = "https://files.pythonhosted.org/packages/81/35/b761eb6d8c1eb218b9b42b9b4d5ac902afdc399fb6dac6f9a9aac7bda589/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:58f361dcbab699cf8f42db3f47c8e7fd1036f138c23a5d08de9fde5f425a730c", size = 248712, upload-time = "2026-09-30T04:38:12.317Z" },
{ url = "https://files.pythonhosted.org/packages/4d/2c/147169a041b747759f37405c0a97157e8e92de967968373101ff14915cba/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:1b4cbc7c3491ccb4aa17fcd8165649d01cf39f76de1696da8631b5f71b85401d", size = 274176, upload-time = "2026-09-30T04:38:14.138Z" },
{ url = "https://files.pythonhosted.org/packages/f0/2d/0ff8db0d373ba8538db686db11cd7e8912031490b9e4f383b41912e8d594/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:ba0b1d2620edf869789c3879223f52bf2afc5d31b3cb47cc57b3a12c05e2aa9d", size = 259413, upload-time = "2026-09-30T04:38:15.841Z" },
{ url = "https://files.pythonhosted.org/packages/8a/8e/b4a085fb47c9d3a7e43576a4784fdd8fe23f907514a972de8086edaf7a48/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:5e2b6b57e9733d39f0c9fd3185efa6b8e29652c4cd8fe94180272cf6ed9a78c4", size = 269434, upload-time = "2026-09-30T04:38:17.626Z" },
{ url = "https://files.pythonhosted.org/packages/83/1c/d8d8d7322a7c3eecdf3237a4a419cf41d2eaad8e006ce7dfdd9d4c8fa2eb/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:51cf45226a9b588d0d2b4880c62d686934b63ab0bd79ca23ab0e9762eb27441b", size = 262716, upload-time = "2026-09-30T04:38:19.214Z" },
{ url = "https://files.pythonhosted.org/packages/a0/16/0e4c6ba9b44e97a2da150e52d331e8f9c968b21b358fbffa6c856cebcd89/charset_normalizer-3.5.2-cp315-cp315t-win32.whl", hash = "sha256:5fb29fb8cd1a46c27a1bf9613ad5ec2599310d46b4025d9556404a6b6a292800", size = 197159, upload-time = "2026-09-30T04:38:21.037Z" },
{ url = "https://files.pythonhosted.org/packages/be/33/e90bc2b1374f7f36ef106f56620de5a783907e19ca857efe2277e31cac3e/charset_normalizer-3.5.2-cp315-cp315t-win_amd64.whl", hash = "sha256:a192e2c40070d92c3ccf777e3a5c4ff515573cd2bb7ed0c537fdadbbec5bbf21", size = 221322, upload-time = "2026-09-30T04:38:22.886Z" },
{ url = "https://files.pythonhosted.org/packages/66/89/dfa6dcb08c200b7830ab56439e8c1890f2971d51aafbb3937894a2e7fcfc/charset_normalizer-3.5.2-cp315-cp315t-win_arm64.whl", hash = "sha256:749e97e1b32313717a565abbe321bc2190bc8b35f1a67e4cdbc7c56c8d8ffe58", size = 209192, upload-time = "2026-09-30T04:38:24.648Z" },
{ url = "https://files.pythonhosted.org/packages/8c/ab/176fbfd5b64939c55d652366aa5b9ef1d767af207a3aa6ebeb0d226c484d/charset_normalizer-3.5.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:4275811936e2f06feff5e598fb42a1b7ae852da8e39605211892b56b81a34efd", size = 331815, upload-time = "2026-09-30T04:38:26.216Z" },
{ url = "https://files.pythonhosted.org/packages/7e/84/371eac6b30bdbcbf2d632a1a01809103459216fcaae61b8b8d922c1bfb8a/charset_normalizer-3.5.2-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1c50fe28bbc2ced33386f298650d91218076c05420e6cbd790b913adc41659e7", size = 253276, upload-time = "2026-09-30T04:38:28.032Z" },
{ url = "https://files.pythonhosted.org/packages/43/6f/c4fbae58febff71709c51bc7e18fdfa55341dc382704740f9f0cbf03817b/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d19fbd981a488e22cd04883659ca6b08f50b5974f9fd7c95655ef6a043e5893f", size = 241239, upload-time = "2026-09-30T04:38:29.732Z" },
{ url = "https://files.pythonhosted.org/packages/61/71/458c3f42164a07d0c5210798e9e704b39e540a6793b05aba67f3a35243a9/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0fed1d06615f022ee3b13caf5e8b180cfea32bb2c5aded8a9d44277afc040f93", size = 231121, upload-time = "2026-09-30T04:38:31.462Z" },
{ url = "https://files.pythonhosted.org/packages/09/54/ab9e89367076f6331bb6c65c4bf14a5361fa5191cb6561bf534f18504e1b/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:838dcc90063569a0448120554591a1d6c4a4ffe11babf048908793154ab86ade", size = 260350, upload-time = "2026-09-30T04:38:33.239Z" },
{ url = "https://files.pythonhosted.org/packages/7c/c1/061431ecc688d9d76602502cb57cc01e691e682c18f1beb45f9673b5bbd2/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2ce45c6627b22c47e390bc91a41c3d13032192e699fa0bea96e9671b373d69b0", size = 255430, upload-time = "2026-09-30T04:38:34.865Z" },
{ url = "https://files.pythonhosted.org/packages/8d/1f/20c8949f0676f7ab811abdeb7f4d7f1cbc6e61ff20bef08b44edeb092bc8/charset_normalizer-3.5.2-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0774bf9bf620249fee3e0b8b9fd3065de213be30f3aa94ce2494b3b638949e26", size = 250612, upload-time = "2026-09-30T04:38:36.649Z" },
{ url = "https://files.pythonhosted.org/packages/2b/9e/46f2fa4c431fc98c4ae76a8cb5bdca54e0341e3cfc3fcfd8e82740250818/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1db38f4c5496827c1a501846d64d14c3b80c7e6714e406cd7dc36a9899fa1011", size = 242083, upload-time = "2026-09-30T04:38:38.26Z" },
{ url = "https://files.pythonhosted.org/packages/bd/39/559be29a0c0f086e0bba6922babd38916cc5e0b58ced4de13ee01ea05508/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:304d8e4d493af723536393eee0c689eb7813f4a474c8b479dee63f1fdd98f621", size = 232738, upload-time = "2026-09-30T04:38:39.81Z" },
{ url = "https://files.pythonhosted.org/packages/ff/6c/387b0e4f756a282831c1d9fc6aeb6c51ca4507ca202767c8de15ce9b12e2/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:9b7f416ff0978e2f2249330527f0ad6fa02f4932e6199692d3b52da2048c19e4", size = 260703, upload-time = "2026-09-30T04:38:41.346Z" },
{ url = "https://files.pythonhosted.org/packages/96/92/1fdf015f09ef449f50d3ac4b67c90887c9c318b727daa95cc4f866e6521d/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:01077390b03f7988f11d700a2194e69b119741a86b1a638b1db88891e3eced8e", size = 247622, upload-time = "2026-09-30T04:38:42.937Z" },
{ url = "https://files.pythonhosted.org/packages/dc/3c/8e7b8a5671ad5d433669fb2a76f1a0164df2d9b1718b0206bc2a16d840cc/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:7e841fb9010836c992c9f12fcbd43a831de93a5f726fc1ccd8ca1d0268c5014c", size = 257500, upload-time = "2026-09-30T04:38:44.604Z" },
{ url = "https://files.pythonhosted.org/packages/b4/f0/45b579df5cabc1d5d53ea1cc35e8437d3ca768c0acccc7041517cb6fbb32/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9cae88599c7219005d879f98e5ed53341e9a122af585e1091200358a3003d2a0", size = 255100, upload-time = "2026-09-30T04:38:46.289Z" },
{ url = "https://files.pythonhosted.org/packages/31/68/fdec18a343f5fb3f310588dd478b09ac4799e0b187dbade3a8cd776f03ef/charset_normalizer-3.5.2-cp37-abi3-win32.whl", hash = "sha256:01b0c0d2262a9e28e8484a278c7e1b5d650e3ac8cf2683d2967e25899f208bdf", size = 174499, upload-time = "2026-09-30T04:38:47.999Z" },
{ url = "https://files.pythonhosted.org/packages/9d/8a/b618149cc5207943a0242068d7a27897f56a62947b5a039085f2a22029f8/charset_normalizer-3.5.2-cp37-abi3-win_amd64.whl", hash = "sha256:9f56f72050826f63dcee7a7f55b0a77168cb3bfc553fd405e7f8f9ece75a4036", size = 200092, upload-time = "2026-09-30T04:38:49.707Z" },
{ url = "https://files.pythonhosted.org/packages/03/cf/4c66866fa9e2b1c78e3c911516d1de497a677b7ac60f1eceda74ce777ca3/charset_normalizer-3.5.2-cp37-abi3-win_arm64.whl", hash = "sha256:40ab6bffa02ae10a0581e6c198be7d2d8ca5c2a0c64e4ed3465d766df457573e", size = 294363, upload-time = "2026-09-30T04:38:51.312Z" },
{ url = "https://files.pythonhosted.org/packages/fc/ad/d07d7862a62ffa6d79d68074d14823243dd235a77c45262acbf6adeb28bf/charset_normalizer-3.5.2-py3-none-any.whl", hash = "sha256:b6b751274acb69d77b3323d6b7dbaa3c7fdfc1eb829b7eb61d262f32e1af9685", size = 68872, upload-time = "2026-09-30T04:39:21.828Z" },
]
[[package]]
name = "click"
version = "8.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34", size = 382235, upload-time = "2026-08-26T13:33:14.56Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
@@ -33,6 +275,62 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "cryptography"
version = "50.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9d/af/182eb91b0df3fe75c4d9f26fe70684569566745f6ba7e5c9c73a862c5252/cryptography-50.0.2.tar.gz", hash = "sha256:7b46165bb56eb4704e2eaaf86f3c940d19154535d9b0ca7d6d590b04060e00d5", size = 880623, upload-time = "2026-09-30T15:30:04.884Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d9/69/c9bd862c3bf43d6399c433caf002df16e2dffd4be49bdf515cda38038711/cryptography-50.0.2-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:79def8d059362e7831389ed3be0ecdf58a89386e1271e35dd9f5af84e81bffd0", size = 4731146, upload-time = "2026-09-30T14:43:47.113Z" },
{ url = "https://files.pythonhosted.org/packages/21/69/64cef1f702bf6657e0cc186ed1a2891d50d29fb41586b254e1c07adea261/cryptography-50.0.2-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:630ebfea3bf689d075f82316324ff7433dc447fe6bc1bfc76524b74b4a9567d2", size = 4719841, upload-time = "2026-09-30T14:43:49.01Z" },
{ url = "https://files.pythonhosted.org/packages/38/6b/61a3f8d8c5e1e49a6cddccafc4015cc1c0021360ab0acb4080e7a423644a/cryptography-50.0.2-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f9f6143a8c75945eb960d9eb98905a441394abfa24afaae239d514ffb2586480", size = 4738340, upload-time = "2026-09-30T14:43:50.932Z" },
{ url = "https://files.pythonhosted.org/packages/1a/f1/b474e930c4d910328780e3940da76f5aa5cbc48ce1fc14e44d239d9ea9db/cryptography-50.0.2-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4061c0079120205fb760c58acab6443e217307dcf05e3702cf970e0689972856", size = 4753050, upload-time = "2026-09-30T14:43:55.272Z" },
{ url = "https://files.pythonhosted.org/packages/7c/52/9af10e80ac16b0fcc2123f9cbd5e7afbd0fd5075bb7a607c592258a39cda/cryptography-50.0.2-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:ac9ed99d81760c62fe89d5f0815cdfa1ba9a35141cf30f1c2d044f04b4803d2e", size = 4376724, upload-time = "2026-09-30T14:43:57.24Z" },
{ url = "https://files.pythonhosted.org/packages/71/37/6202e488cc1eb625ea110c292c6bda92823176e023f427d8d5660ce8d632/cryptography-50.0.2-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:87e9ce85beb6b328ba370cc6e6aea483c92617b4c95b1d33a49297eb662bfb04", size = 4737859, upload-time = "2026-09-30T14:43:59.541Z" },
{ url = "https://files.pythonhosted.org/packages/d3/69/2c833a049475e0a3444e94c7d0aca0aa51d166374a449b09e92ac98138de/cryptography-50.0.2-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9dab55f57c74c3cad24c323bacbbd04be4705ba6eb0d92e920b1fc4837ed5079", size = 4752576, upload-time = "2026-09-30T14:44:04.545Z" },
{ url = "https://files.pythonhosted.org/packages/6c/5d/906970b83bbfc1f5bbfb677a143c181f2801f23b6a7204a3b47c42c97e65/cryptography-50.0.2-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25784ce8b9621c90c643efb9e1e2162ab3b0224cae446ad5e70e7fcb1ce18b51", size = 4870819, upload-time = "2026-09-30T14:44:06.884Z" },
{ url = "https://files.pythonhosted.org/packages/68/e3/f2298d3bb55e0c4a91841ec4d01b3f020ba8c5fbf15ccdcc6dcf03f97025/cryptography-50.0.2-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:85d0d9a31b9098e98534226d5686b47264b95e62ce459dc2e62fdfc809f9fe93", size = 5030152, upload-time = "2026-09-30T14:44:09.443Z" },
{ url = "https://files.pythonhosted.org/packages/19/8e/aa1fc533d4546b127b45de8aa024eb5933d23eff9debfe25931e56861095/cryptography-50.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0ecbc5652bdb6fc9eaf89a7d196e20941adfe812f43bc4ca05d9150496821047", size = 4710431, upload-time = "2026-09-30T14:44:15.427Z" },
{ url = "https://files.pythonhosted.org/packages/6a/64/72bc3f75176e7e406b748a3e3830432b8c51297b38368713df04dc04898a/cryptography-50.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ab50ee449bf968271e820086f10a33d101dd060370abc10bcd22279be2656539", size = 4694824, upload-time = "2026-09-30T14:44:17.69Z" },
{ url = "https://files.pythonhosted.org/packages/4e/c6/62c77550edfa5ca3f14bf44a1e6739b9fa09d6e998a11d97ed8213bccc98/cryptography-50.0.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a9f7355e6fab51f6c369b86fb7571cffa05edee2c2121e0380a37fb9ac1cd5c1", size = 4716967, upload-time = "2026-09-30T14:44:19.661Z" },
{ url = "https://files.pythonhosted.org/packages/aa/9a/6f2f0304d634ceafdeaf23e84537336664ac419b5d07611675c2ad3f6b7a/cryptography-50.0.2-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:241449bf940a5d27309bd317e6f9a2af6932113818bb2b8f5c59ddc7ef16da18", size = 4727698, upload-time = "2026-09-30T14:44:24.178Z" },
{ url = "https://files.pythonhosted.org/packages/1d/de/66bcf9244d118663b2e1aaded8990f4640e3d7b7411870a5765f252074d2/cryptography-50.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d8947001be83df1394050758ce0e745dd74fb134eef0a4b5124208dfc3a68c37", size = 4354821, upload-time = "2026-09-30T14:44:26.263Z" },
{ url = "https://files.pythonhosted.org/packages/bd/e6/db28a28c7b6c676addce89136de3d8db49ea825a8c863472e36e42ead4ad/cryptography-50.0.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4a20ce1e5cb4284a86692fdcba7cb8754185c6b2e5c56fcef3751cf451d3cdc2", size = 4716748, upload-time = "2026-09-30T14:44:28.447Z" },
{ url = "https://files.pythonhosted.org/packages/6c/01/03263395f74d50b071e9e66daace3f8bef80493e5d410726f2ba8554736b/cryptography-50.0.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:828d49b0ff5a0e3975865571c5d91dbbdd0d38d8289b249a163e9425413a5e05", size = 4727268, upload-time = "2026-09-30T14:44:32.92Z" },
{ url = "https://files.pythonhosted.org/packages/eb/94/2bfe8f29ec0cc9c0d99359c4161adf32858e4934b72c6d100d2ac0bbe962/cryptography-50.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:deb9fde5c60e437ee4821bc9bc39ff31b42135c27e1dc61ef0a629389c1de62e", size = 4849503, upload-time = "2026-09-30T14:44:34.969Z" },
{ url = "https://files.pythonhosted.org/packages/54/44/e80651ecbf0e42b62e2bb5f5768916e07eea72e1297338956a61df361f88/cryptography-50.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8c71ba2cd31fc93748c38e1b613200ff1c2665cbfd5341fe3a61cfde35a1430e", size = 5004057, upload-time = "2026-09-30T14:44:37.064Z" },
{ url = "https://files.pythonhosted.org/packages/8c/75/32ac2a56243d778805c16ca6a32b8f74fb757df7e28d7ecb560afafb59cf/cryptography-50.0.2-cp315-abi3.abi3t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d370b8d1dfcdf7130178137f6fbee6140774a1acc6cacefc4b42643ec11d0a3a", size = 4956267, upload-time = "2026-09-30T14:44:43.693Z" },
{ url = "https://files.pythonhosted.org/packages/aa/a4/2c8d734e43d97f0842ee9f1b7b4bfb3d0cf5e19edebf43c2afe6675c2320/cryptography-50.0.2-cp315-abi3.abi3t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f2f9bd7f90c64fe89253f0a2c05e3c4856072660429ce8831b4235bf29403a67", size = 4966465, upload-time = "2026-09-30T14:44:45.769Z" },
{ url = "https://files.pythonhosted.org/packages/c2/58/ee288c829a6f41f6235ae9dd33d82fd19b45442b65b4c8a3da36963d9f7a/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_aarch64.whl", hash = "sha256:e275096ea1e60cc595cda2836fd4a6c725d1125108b868be17f53684d164e2cc", size = 4959356, upload-time = "2026-09-30T14:44:48.211Z" },
{ url = "https://files.pythonhosted.org/packages/02/a8/8df951850d6b31d2a00218f19e2b3f999523437ed7a819df7fa427942fca/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_x86_64.whl", hash = "sha256:58a0c478eeca76fe5e07993c5a0703def34a6dc6a0cda4f5564639b33112ffe7", size = 5001199, upload-time = "2026-09-30T14:44:53.379Z" },
{ url = "https://files.pythonhosted.org/packages/8b/f9/36b3022218ce75b7cdf068fb95f809f9bd0d820e4955ef43b90c255cc7ac/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_31_armv7l.whl", hash = "sha256:d38cdff612d06fa6a32840d5e1b1f7a27cee4a349aa9085d94a67789d6bfd408", size = 4629333, upload-time = "2026-09-30T14:44:55.635Z" },
{ url = "https://files.pythonhosted.org/packages/8c/72/20f99a219f6af47cdd1cbd978c243b92d71496e168a746138af44ded4f29/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_aarch64.whl", hash = "sha256:fdd28f912fccfec1846a94e2e1e8f9b0012f557f0c46fe4f3eb0d7a87afcf90b", size = 4958822, upload-time = "2026-09-30T14:44:59.639Z" },
{ url = "https://files.pythonhosted.org/packages/24/95/83378121ef3eaaaf71d4b781577ff794acb39b9e1b87a3f156898c8497ed/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_x86_64.whl", hash = "sha256:e105ab60406787da31fccc883fc0f733af1efd78f0136a4599692c4083a73d0c", size = 5000859, upload-time = "2026-09-30T14:45:05.009Z" },
{ url = "https://files.pythonhosted.org/packages/22/f7/70fd7ae4d1dbfa7ba29b02e1b9068771519a86027756510b700ce81086a8/cryptography-50.0.2-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:6f8700550aa1474a91e5dc07049c46f98b423b5b1ddd0483e0b51362eeeaf5be", size = 5092151, upload-time = "2026-09-30T15:29:15.932Z" },
{ url = "https://files.pythonhosted.org/packages/d4/be/688367b74de86984bd58d8efacfc7c9e68b89a6a22ced0fb4f38db50254a/cryptography-50.0.2-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:c71be1cbfa5cd9a41ee452acf1eccd82b2c05950358b106ec8ceb83411d1a020", size = 5286120, upload-time = "2026-09-30T15:29:18.309Z" },
{ url = "https://files.pythonhosted.org/packages/d5/8d/6d585339bedf85d45044c85d8412dac53f2bb6f918e8b7777efba1787844/cryptography-50.0.2-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c5e67125c7dca78d199ec4e116aa93dbb83494808ecbb8211a2cb09b1bf41dbd", size = 4756166, upload-time = "2026-09-30T15:29:24.58Z" },
{ url = "https://files.pythonhosted.org/packages/bf/f1/1c1f6874e8550cfddd4b688ceb38cefb6ed15ceed224d56f133f3d88c214/cryptography-50.0.2-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ee247f5c245c9a2fe7c8e2214e295918838e44e00a45a6718451e4004219e767", size = 4749145, upload-time = "2026-09-30T15:29:26.807Z" },
{ url = "https://files.pythonhosted.org/packages/c1/63/61b15dc1a8de03fe0adbe3fd7608b3ad5c73bf50993bbcb1faaa930afe33/cryptography-50.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dfe9763530994147d9af1def057a5b9658b00e8f8fe8743d144d1e0911c2e454", size = 4763638, upload-time = "2026-09-30T15:29:28.588Z" },
{ url = "https://files.pythonhosted.org/packages/4f/87/ef344a9e616871f2519c22d6afcda79ddd5d35e9592d95eb6e677608d055/cryptography-50.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f21e8a22c8605750c7af886bab299a363721264061b4ac0a30efb73cfd58efc5", size = 4781387, upload-time = "2026-09-30T15:29:32.605Z" },
{ url = "https://files.pythonhosted.org/packages/90/5b/f2fdb13cd0b96f6f932c8627bb292a45f11c64d21620a8e120aee9a3b848/cryptography-50.0.2-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:9c8402a82ea0dc4ceeab793db05f0fafa8ca139ca34fcde5df0f596103c74107", size = 4403790, upload-time = "2026-09-30T15:29:34.374Z" },
{ url = "https://files.pythonhosted.org/packages/bc/ce/7e4f662b1e3c393513569e402cfc85ac7da0bd3d5435e122a3140219eb2d/cryptography-50.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:0ddc924c04591c2811ca024d62ecad4f7f6f08af8939c211438f48a16bd23602", size = 4764319, upload-time = "2026-09-30T15:29:36.149Z" },
{ url = "https://files.pythonhosted.org/packages/40/cf/6b5c8e2fd9202d98988ab7cb5cc5c991704c4ad55f492ff408e4969f83f1/cryptography-50.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:1981f1db4630889b9ef7803fadef12b056f428cb6b85c27ba57b774793b6093c", size = 4780973, upload-time = "2026-09-30T15:29:41.251Z" },
{ url = "https://files.pythonhosted.org/packages/10/bf/8d6ebc7dded797bd0f0160d52188021211f011a2b164ef0ae1dac4587465/cryptography-50.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7a8701d6b584d76e909e3d305b7d126b41439876a5aaf76cddc67fc230eafa2e", size = 4897738, upload-time = "2026-09-30T15:29:43.106Z" },
{ url = "https://files.pythonhosted.org/packages/d4/aa/f3f6e0de7e6253b8baa8b2d8fb9d50924fa75cee3d4624bd4bc1208ee923/cryptography-50.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ce47f66801c20ec6c6632453bb5960fe38939e9306970b48b3a5a26de7745d94", size = 5058280, upload-time = "2026-09-30T15:29:44.827Z" },
]
[[package]]
name = "docutils"
version = "0.23"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/39/a4/5180d9afc57e8fca05601dd652bdff19604c218814037fe90ffc7625a50a/docutils-0.23.tar.gz", hash = "sha256:746f5060322511280a1e50eb76846ed6bf2342984b2ac04dc42caa1a8d78799e", size = 2303823, upload-time = "2026-05-27T17:41:06.934Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/32/91/30151a39f7570f448ed84529390628a651d7f27c87d73c9b887f8189695e/docutils-0.23-py3-none-any.whl", hash = "sha256:25d013af9bf23bc1c7b2b093dff4208166c53a94786c9e447808335ef1185fea", size = 634701, upload-time = "2026-05-27T17:40:58.442Z" },
]
[[package]]
name = "h11"
version = "0.16.0"
@@ -80,6 +378,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9b/43/832f631d32e4f1211caa2ba368317739fe71f0b8530e4c9d15dc454bac2a/httpx2_jsfetch-1.0-py3-none-any.whl", hash = "sha256:cb916b707601e69a07721aabc8f3f6659be3a6893bc1ff5c6f9e02241df2da32", size = 6382, upload-time = "2026-08-07T00:13:06.567Z" },
]
[[package]]
name = "id"
version = "1.6.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6d/04/c2156091427636080787aac190019dc64096e56a23b7364d3c1764ee3a06/id-1.6.1.tar.gz", hash = "sha256:d0732d624fb46fd4e7bc4e5152f00214450953b9e772c182c1c22964def1a069", size = 18088, upload-time = "2026-02-04T16:19:41.26Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/42/77/de194443bf38daed9452139e960c632b0ef9f9a5dd9ce605fdf18ca9f1b1/id-1.6.1-py3-none-any.whl", hash = "sha256:f5ec41ed2629a508f5d0988eda142e190c9c6da971100612c4de9ad9f9b237ca", size = 14689, upload-time = "2026-02-04T16:19:40.051Z" },
]
[[package]]
name = "idna"
version = "3.20"
@@ -98,6 +408,48 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
]
[[package]]
name = "jaraco-classes"
version = "3.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "more-itertools" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" },
]
[[package]]
name = "jaraco-context"
version = "6.1.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/af/50/4763cd07e722bb6285316d390a164bc7e479db9d90daa769f22578f698b4/jaraco_context-6.1.2.tar.gz", hash = "sha256:f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3", size = 16801, upload-time = "2026-03-20T22:13:33.922Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl", hash = "sha256:bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535", size = 7871, upload-time = "2026-03-20T22:13:32.808Z" },
]
[[package]]
name = "jaraco-functools"
version = "4.6.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "more-itertools" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6c/1f/c23395957d41ccf27c4e535c3d334c4051e5395b3752057ba4cbaec35c56/jaraco_functools-4.6.0.tar.gz", hash = "sha256:880c577ec9720b3a052d5bc611fb9f2269b3d87902ef42440df443b88e443280", size = 20837, upload-time = "2026-07-14T01:28:02.544Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl", hash = "sha256:99e3dc0060c5cbe8fcd1cdb36258e2a65ca40f1566b2033b12abb1bb44dd3c30", size = 11677, upload-time = "2026-07-14T01:28:01.59Z" },
]
[[package]]
name = "jeepney"
version = "0.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" },
]
[[package]]
name = "jiter"
version = "0.17.0"
@@ -161,6 +513,208 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a4/b4/923a1215daba959aed8355973315cb3f81f53e0d01c5b211870a27b41f45/jiter-0.17.0-cp315-cp315t-win_arm64.whl", hash = "sha256:d47687806f9c54c84ea38733507081337922beca90ce819c7d852dd485bc0f23", size = 182977, upload-time = "2026-09-12T15:14:01.799Z" },
]
[[package]]
name = "keyring"
version = "25.7.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jaraco-classes" },
{ name = "jaraco-context" },
{ name = "jaraco-functools" },
{ name = "jeepney", marker = "sys_platform == 'linux'" },
{ name = "pywin32-ctypes", marker = "sys_platform == 'win32'" },
{ name = "secretstorage", marker = "sys_platform == 'linux'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" },
]
[[package]]
name = "librt"
version = "0.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/04/f5/9dc696772d241814bacac7880bac32f2930b5a6ebc1f85317b83161a011c/librt-0.16.0.tar.gz", hash = "sha256:ac38d6d8d66bf3d744148dbbc0b8e193e195a51e364ed55e224631f5721891fc", size = 219839, upload-time = "2026-09-29T00:55:32.891Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/93/9e0cf7da129a93c3dc7f45bc3cd4a660f2aaa995aa8a6f95c2583ef41239/librt-0.16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:931a0bb0fcac88f263e269e46eb30ba8e21402cd3c62ca40cb97034c0693fab1", size = 149844, upload-time = "2026-09-29T00:53:31.391Z" },
{ url = "https://files.pythonhosted.org/packages/8f/26/8a90d2a8f2b2e471bb486b7aec117b8ae622715ed6c39853aec48ea20073/librt-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1bc17e54e5305f8d40b7ca203671ff5a9e59c1d0f8ea0f625dcca53a3984de11", size = 154111, upload-time = "2026-09-29T00:53:32.718Z" },
{ url = "https://files.pythonhosted.org/packages/35/ce/67abb46258da4d3e42ff5b141db6f38c59357bef84c7979f183b22f924f1/librt-0.16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:877698bf6bca5721d8be345f2fe09778e40ecadea8b58c73075f2b1a53666bf2", size = 494234, upload-time = "2026-09-29T00:53:34.466Z" },
{ url = "https://files.pythonhosted.org/packages/12/f9/ea7162414a16f8f1bbd3b493ad6d22b926c5471916e078350bdbca8c4e5f/librt-0.16.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5981c011b306781ce561e18e14230a14524a3d8109b97553666c942c18f31a96", size = 491173, upload-time = "2026-09-29T00:53:36.124Z" },
{ url = "https://files.pythonhosted.org/packages/b1/09/9b3e869060dd33f9989b80ba4fea306f6db8cecefcdfe6d7346ef0603f65/librt-0.16.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:afced3dfc17cd805ecf7a3d77996a71cf5f2c75aa66eb0c21a9930f4fc992f86", size = 505547, upload-time = "2026-09-29T00:53:37.686Z" },
{ url = "https://files.pythonhosted.org/packages/50/07/79007d2165f649ea93e08c0962d1d62c70af9cde77965255095bf9d96f9a/librt-0.16.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ca8052401c55d7511dda6760719fda7618067e83535d7d0010096d216c34b667", size = 523057, upload-time = "2026-09-29T00:53:39.301Z" },
{ url = "https://files.pythonhosted.org/packages/61/0c/8fbaff66d0ba376d8864653f5acce1569bae27e89648671f26f7eca67ab8/librt-0.16.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1e511762a074005bb0aa569166779834e75e438370226930d0ce1866d4b6a33b", size = 515158, upload-time = "2026-09-29T00:53:41.012Z" },
{ url = "https://files.pythonhosted.org/packages/df/2e/23ff0dece76f07a4124413a57682efa0bbeb5765ac0122bc0955513f82ca/librt-0.16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1e8591bd8a5a628cd7f07954c6a1592359a878bf032957a8e9057a41d644311", size = 534183, upload-time = "2026-09-29T00:53:42.451Z" },
{ url = "https://files.pythonhosted.org/packages/26/c4/e11dea21d9a29486eba78887380374189d472734fa32cc50cb37ca44d3d0/librt-0.16.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4aaefb4ba6c07e1aeebb2795c8958148f1d6f9af3b555b53d23d766edb6d67a", size = 540663, upload-time = "2026-09-29T00:53:44.272Z" },
{ url = "https://files.pythonhosted.org/packages/44/75/e873ae158a8b7f5359be33e7fd6c1fbe02d9a78a3e89a77de6b0e837f476/librt-0.16.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d92db7a0f6aee44f1baee94750457e8d2d1c6ccea41842de6268d34e8dc7eddd", size = 545908, upload-time = "2026-09-29T00:53:45.812Z" },
{ url = "https://files.pythonhosted.org/packages/ba/36/8939d3f6a93e11bd9592e6fe28d2b44f1c2dc4bed6e22e72359a91e18ffb/librt-0.16.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:378dfaffb38e59c24a87cde5713cd865d51ff7383fa12947f3907f306ea1ca55", size = 523302, upload-time = "2026-09-29T00:53:47.596Z" },
{ url = "https://files.pythonhosted.org/packages/71/14/35309f44a077f0f42ade0e2e7cd88c0cea760c661af205c01ea90d3c0e1f/librt-0.16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3e0c39bdc85370422e8b637be76eb1fd07d30967551b03e62267dd156f553152", size = 565911, upload-time = "2026-09-29T00:53:49.278Z" },
{ url = "https://files.pythonhosted.org/packages/78/0c/df6255b94967f3159ebc46f08d8e783c12da6ee269ddb74b2efed63c640c/librt-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:1b384b90ab79a7bc30b566895809a636e0666f21f3cf12b54823d025b7e83839", size = 79119, upload-time = "2026-09-29T00:53:50.938Z" },
{ url = "https://files.pythonhosted.org/packages/6b/44/d30d5a5461378c9d33f36736c6791c3b4c4ba4b1ffe0da7350aedcb2c9a0/librt-0.16.0-cp314-cp314-win32.whl", hash = "sha256:52327da75a94012e7f932f913d20d3876bed3c102be00e6c3e8600ff7bdd58a7", size = 100140, upload-time = "2026-09-29T00:53:52.205Z" },
{ url = "https://files.pythonhosted.org/packages/c2/98/769712f356a1e897df3581bb0c3100375d054a000de26099360ea65b5111/librt-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:3f0b8114c44b2ac06ff5dacd08e07e8e807ff4f46083f2a1602685122559be41", size = 120464, upload-time = "2026-09-29T00:53:53.495Z" },
{ url = "https://files.pythonhosted.org/packages/bf/d3/ae2abccc8bdc8b063c1613a77686e17b74e9d3d60cfe6f12c63fa2821b9f/librt-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:8caf96a4ef8fb27d0ac0d1ad8337d26a240acd4a02fe4345d0a8f264753e8f99", size = 110889, upload-time = "2026-09-29T00:53:54.817Z" },
{ url = "https://files.pythonhosted.org/packages/e4/26/0737d4be058dd6376eade7dd8b380d4b869b6394cee929393a5a431c45bb/librt-0.16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:953107e2f68d0f3512c48f898b0dbf0ce5cc52bba0f318d847c985dc555ee4cc", size = 159838, upload-time = "2026-09-29T00:53:56.22Z" },
{ url = "https://files.pythonhosted.org/packages/01/96/9bc96531d7c620e9949af904470f02e3fa8f35129ab8e8f281c51eaa3788/librt-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ad37d5b9abd49c9a655dcda7ea52a8a752884062ef1ee71ae17c2f2a0f81fe6a", size = 162018, upload-time = "2026-09-29T00:53:57.532Z" },
{ url = "https://files.pythonhosted.org/packages/0e/fa/b0289dcb186eb3f97221ba00da5f4bd3ba7fa5e99752d48aa8d336615334/librt-0.16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c4aa329c17bd1aaea4f6e89335d8ccd494b3a5830b6654462273e50e11023f0", size = 703983, upload-time = "2026-09-29T00:53:59.024Z" },
{ url = "https://files.pythonhosted.org/packages/d9/ab/05ebbbde7530fc5eeb58fbd1581522a9f64f132fa703c4c595eed6e14760/librt-0.16.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0ead24d2562a49473dddd9efef8581f020007eb0054389c3ee3ffad38b1ca4c9", size = 683847, upload-time = "2026-09-29T00:54:00.671Z" },
{ url = "https://files.pythonhosted.org/packages/a3/75/f52aeecd4dbadbddf80725ba7de126d8bd5d0eb66247eae17a81ed90dd4d/librt-0.16.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02118f56a9c36ddd07dfd9b919d9ecc117ba20a90987d56aa4c429fa34509188", size = 698045, upload-time = "2026-09-29T00:54:02.247Z" },
{ url = "https://files.pythonhosted.org/packages/e6/55/fa277a835cd6eb42380591ceb85f48c5b4d2b2d7e2cb9869e1e17d24d237/librt-0.16.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4e29522c62e28595ff7e324c6834ade51127707f0e255b18d1c1cf03d39c1048", size = 724640, upload-time = "2026-09-29T00:54:04.078Z" },
{ url = "https://files.pythonhosted.org/packages/25/e4/2cf64354f3fde8ebd591b3b48f96510bee24ac5f7d1e567adbbe210abdbd/librt-0.16.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ff4b2367926b69c6215635902cccb04048e73094e9862900d27cb2c6bbff143", size = 731748, upload-time = "2026-09-29T00:54:05.741Z" },
{ url = "https://files.pythonhosted.org/packages/f8/c9/c180af3e94e01aa529fa93d7733fec2abc47f222345400cf21d5481d5f8a/librt-0.16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c6f1b27bf1632a7e016af9f145f82be95e1edd7721a646505c21059257cb5a04", size = 754960, upload-time = "2026-09-29T00:54:07.38Z" },
{ url = "https://files.pythonhosted.org/packages/95/d6/01073aa78c58f356b10d9c57b3fe9abb143df338142bcd316df417b89db0/librt-0.16.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5696d7f52e7b37217cb3a8f92c744fe835942602fdd4c1a8bc4741d3bfdce15e", size = 747000, upload-time = "2026-09-29T00:54:09.06Z" },
{ url = "https://files.pythonhosted.org/packages/f7/d8/1de3783908658d697a8cfc00582f61299ffba7796d7260c112e4700b1109/librt-0.16.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:6072e92dd876ff6ceeb6cf371e35e51f479349837391341f479b08df4564242b", size = 749569, upload-time = "2026-09-29T00:54:10.702Z" },
{ url = "https://files.pythonhosted.org/packages/b7/32/e817f66c96d6caa8bb8435ff93c4c220624d59efc506be59ab98fcd01d0c/librt-0.16.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:39ca4f2f2fe05de8e63493da592d84311adabe5bef52b193851981da9816b302", size = 729483, upload-time = "2026-09-29T00:54:12.25Z" },
{ url = "https://files.pythonhosted.org/packages/7c/c9/23992ccd2b9d22798fdd0f61353183a47414e651da782ad83eb680f50833/librt-0.16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9807485a908f00355820f18e91e045ffdcdc5adb68aaec40a1e2b88c5f7bba1", size = 776584, upload-time = "2026-09-29T00:54:13.837Z" },
{ url = "https://files.pythonhosted.org/packages/67/3b/e8af957f08e6e2e8e566b099e43d2748418aa565df6ee1d9d3331209fdfe/librt-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:94be5cb7bca4df6201f4183e9e4fa2086c655283d20b38cd84500a69057575a7", size = 104402, upload-time = "2026-09-29T00:54:15.568Z" },
{ url = "https://files.pythonhosted.org/packages/ca/1c/946e6443d7cd32347a086043395e421e52fd603c9163d2ec970ceab8eed6/librt-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d46ca272b251d033dd4527b0dec5f261a28a52bd5fa0f99c117b0a1f8588cc2d", size = 126227, upload-time = "2026-09-29T00:54:17.165Z" },
{ url = "https://files.pythonhosted.org/packages/c2/a3/bc4f9959d3c62bcbf9e5fd3470a8bbd8bf33224ed4c25d7fba173201b8ac/librt-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b9d6d4b14e92d876f8026b54c20c445f36425214c1081dc76f74e40db386b82b", size = 116493, upload-time = "2026-09-29T00:54:18.567Z" },
{ url = "https://files.pythonhosted.org/packages/b6/4b/10fdb42dfab4c1533e1570e686b18e86ff4328b406361b39fb3016667638/librt-0.16.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:6fe436af2eaf630474f491af5d032cbe45f93fcff5c3b9fe4ab194a7255b20ff", size = 149829, upload-time = "2026-09-29T00:54:19.933Z" },
{ url = "https://files.pythonhosted.org/packages/8e/30/a90ca13f1d3d91af1680000a4038536907018fbd51764767287a05d28b8b/librt-0.16.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:8ff5d26c529336be9bd7ae04483235d77778ee7d6444a95353102b542601ce81", size = 154522, upload-time = "2026-09-29T00:54:21.306Z" },
{ url = "https://files.pythonhosted.org/packages/5c/dd/bcf364eacfa070bb1fc88d503111ae7177914197ba1fa717c748926e7930/librt-0.16.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:909d8e3c1faee44cb762b1c519ff8613dcc5ceae5c99987a00917b5a31fd1d6a", size = 497593, upload-time = "2026-09-29T00:54:22.798Z" },
{ url = "https://files.pythonhosted.org/packages/18/c1/2c4e81e347bdabfe8346bfc6dc37ae5e154d61c88e30848ec0606025a5e0/librt-0.16.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:6d4a64283ee61824b5790de882bc68e2d9d7a5143537cb7a966f7354f71646d4", size = 480459, upload-time = "2026-09-29T00:54:24.364Z" },
{ url = "https://files.pythonhosted.org/packages/ab/d7/fef2a3cb8400701be496f6e459876f650b3451f407e30cb243de571ae615/librt-0.16.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5810ba811297fdf37a1531a57667cb8ace0842013ca8606bf9eb7c24cf4be154", size = 507814, upload-time = "2026-09-29T00:54:25.997Z" },
{ url = "https://files.pythonhosted.org/packages/e1/6f/53762927a32e9dc9eb1d1c1f3528da281290c86d96929a8af652f671266e/librt-0.16.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e56aaf8c167548dc8e5d6f3bd0f48dcdd299a23c73be3f744aab79d99e9c7f5d", size = 525114, upload-time = "2026-09-29T00:54:27.747Z" },
{ url = "https://files.pythonhosted.org/packages/6c/67/0b9d031f303c4e8c691a9a8ef9d272f13b530c11cc563b819df621b6a348/librt-0.16.0-cp315-cp315-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f36c58e33b304b525c6c9c5076399c6ebf1109e17b9051a05a407b091b9215b", size = 520465, upload-time = "2026-09-29T00:54:29.331Z" },
{ url = "https://files.pythonhosted.org/packages/ae/d5/2056a3a85864e882eb17a203a10ddb26fa748bc9718ec67e79059ab46cae/librt-0.16.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:242e00b3d4fa37c3d3c1ca5f5c9adb7d909ddb1eac9c41f2787320d00caa0af2", size = 537257, upload-time = "2026-09-29T00:54:30.915Z" },
{ url = "https://files.pythonhosted.org/packages/54/57/e0d79790c163cbc0909e209a6f62e30bb713b647f905a176cdc64848fd4d/librt-0.16.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:0253721561787b8df8443eb347b7a6461015354e5bdd37ee38a41fef220d2bb0", size = 527442, upload-time = "2026-09-29T00:54:32.478Z" },
{ url = "https://files.pythonhosted.org/packages/aa/50/1c0c95aba7af51f4752ea34fbf2eb79b36e7cae3a72536248e8c735de735/librt-0.16.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:3e483a8d69ede8067db70c0e83007423b6925de6fd53afed01d66160f2e9398c", size = 548139, upload-time = "2026-09-29T00:54:34.183Z" },
{ url = "https://files.pythonhosted.org/packages/98/91/a8a43dd5138d4f55f88846b8f0c85454a4fad69952cebfcff9948f831290/librt-0.16.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:69ba927445cfaaffb4081003ef5224c55a5c2ab67ef956f416ef744916e44121", size = 529708, upload-time = "2026-09-29T00:54:35.761Z" },
{ url = "https://files.pythonhosted.org/packages/2d/41/d5226881ab2b7c20d9d587b37bdd4a0ec8775a96d00ca87ac9f385587db4/librt-0.16.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:d6a365f2ab45a984d0e00eee0dd17f599ceab8cadab6ea07b6111c8132fc0e42", size = 567781, upload-time = "2026-09-29T00:54:37.409Z" },
{ url = "https://files.pythonhosted.org/packages/bb/bf/2345ba57a626e8c78c4ddcc724636a8df5a593fe46c1ee76bbf477e32b0d/librt-0.16.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:f01f3805f2dae4781c0c34b440e31740d082950bdaf89a6f601ad589a28af57a", size = 79177, upload-time = "2026-09-29T00:54:38.792Z" },
{ url = "https://files.pythonhosted.org/packages/1e/40/99e77936cc9207f629b077bf7cbc5e2f0827cddd7c29792f04ef881bd2c3/librt-0.16.0-cp315-cp315-win32.whl", hash = "sha256:b0e3e721c75d2e79a76d4422c79d7ba705fe1bbafec907037fe7a657a480a0e3", size = 100127, upload-time = "2026-09-29T00:54:40.251Z" },
{ url = "https://files.pythonhosted.org/packages/56/1e/801fe26bc622061b9dfd010e166d94142cb774b733217e6d98d1c0cf2638/librt-0.16.0-cp315-cp315-win_amd64.whl", hash = "sha256:bc02954b1295de798bbdb0b4e2d8a28c2117de8b5c73dcbeb27dc32572dfb971", size = 120480, upload-time = "2026-09-29T00:54:41.722Z" },
{ url = "https://files.pythonhosted.org/packages/bb/a9/d533983055bd36e112627384c2c038845d1df882540b8bcefb566475640b/librt-0.16.0-cp315-cp315-win_arm64.whl", hash = "sha256:c5db585d43449a5f54303d4b2774e45e1babd975cfe1630a3d708c0b80c3e560", size = 110915, upload-time = "2026-09-29T00:54:43.052Z" },
{ url = "https://files.pythonhosted.org/packages/55/fe/d62238fa9c653b0e0613290349467cb65711b5800e8e474f45e942a0ad95/librt-0.16.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f06c689cb14afd9b612727553a5ec5a40febf113ca41c4413a2b0b334285884b", size = 159714, upload-time = "2026-09-29T00:54:44.497Z" },
{ url = "https://files.pythonhosted.org/packages/f3/ac/f31efe7818700be72ba4f9af8a80fa67c39808c8d26dacc55dc1f6f35172/librt-0.16.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:13b4e8aba90b0b1c82474e9844aa9ffe7ad3faa484350e1da64cb8188d903134", size = 162086, upload-time = "2026-09-29T00:54:45.968Z" },
{ url = "https://files.pythonhosted.org/packages/d9/16/4d7487bf86a9d7e8e18f37ba5538789233ea693637379b75701bd35ec9de/librt-0.16.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a269c46ae327d8e6f8c1f85f7516cb52c0fa48127565a1105a4f4a05ff2a0b4", size = 712727, upload-time = "2026-09-29T00:54:47.486Z" },
{ url = "https://files.pythonhosted.org/packages/77/74/50cd550ccc1a517b9ed62347625c01ddf8c4afad66490312677c011458f6/librt-0.16.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a33e0dae1f8592146a4764d54ce842b278732d21a84e17c3bbe6b1bc158a2248", size = 681106, upload-time = "2026-09-29T00:54:49.126Z" },
{ url = "https://files.pythonhosted.org/packages/df/5d/7293f712975ee6fdd2251411fd9ef1c62bc99c7b83ecbe62dbc999b1fab7/librt-0.16.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:47ada6ea32636492c61aa8ad27ae3b9404bfe7a97e3ba946d1984236cc741da0", size = 705921, upload-time = "2026-09-29T00:54:50.905Z" },
{ url = "https://files.pythonhosted.org/packages/67/f7/8aab946f11d59d1bece9ffc65d994b200c789a8ca5a95c17e17e609f912a/librt-0.16.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43bd6e642d8a248c114327f98dd25ac5a7cb5aa168ef02f0559b91874df16b8", size = 731786, upload-time = "2026-09-29T00:54:52.584Z" },
{ url = "https://files.pythonhosted.org/packages/43/76/1c42ab31e7cb8384ebf6d3af607213c495222474f4940443ae7639ab7685/librt-0.16.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c3d1bb7841a816ace6449bb26d3f9560dbfa20e71c568d23f0f62bf1e68f50b1", size = 744964, upload-time = "2026-09-29T00:54:54.217Z" },
{ url = "https://files.pythonhosted.org/packages/6f/2e/4b19982d933d2dfced671e840b219e4c1cd3f507df6e6dabb51bbd4e3850/librt-0.16.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:3931f7a3db322e7f44e02a280e3949326ce9579ad388ee8d691dc7c76da9fb70", size = 765305, upload-time = "2026-09-29T00:54:55.815Z" },
{ url = "https://files.pythonhosted.org/packages/b8/1b/e872583de2dcb3ac7746e7a2321aeeb168274f3952a87dc66e05ddb29faf/librt-0.16.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:f4462528b6000afe8f16907b5c7c2553abf1df005ba5140e6eb394541c3624c3", size = 744959, upload-time = "2026-09-29T00:54:57.532Z" },
{ url = "https://files.pythonhosted.org/packages/10/de/a18c6bcfb297af2674233e90b3c3661c3a0af0f1434a0c966d2ef708a835/librt-0.16.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:80039ba9b6a7d5f1a0175a4cca6bbefead87bd854c80abad1cb30afe47a830db", size = 758138, upload-time = "2026-09-29T00:54:59.484Z" },
{ url = "https://files.pythonhosted.org/packages/22/1c/0df1d732539c297bb1a093e1fe3204d2faf76a9022cf4e1d1c2fce059790/librt-0.16.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:7a1d272724b581bb6bc769dfdafed6da2ecc9886ba2450311de55a4ac2e1e9cd", size = 744268, upload-time = "2026-09-29T00:55:01.179Z" },
{ url = "https://files.pythonhosted.org/packages/a2/f7/ccaf31331f20c91a5bd9bd48ffc3f743f9c81bb5720cc7b2a720ca204f0a/librt-0.16.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:fbe4fb8c5445f7496d7f7f6bb0807875d09d47e6771ffa175fb2df2895fb86ba", size = 785783, upload-time = "2026-09-29T00:55:02.938Z" },
{ url = "https://files.pythonhosted.org/packages/c1/ea/7421d9e6db894cd6cd788b604b3394a163ded6b942052b6f96c23ccf08e1/librt-0.16.0-cp315-cp315t-win32.whl", hash = "sha256:375bfe6b572a8f6cfc398709356046173bf27e64c4c5edaf5f7062f051fb4bf9", size = 104355, upload-time = "2026-09-29T00:55:04.533Z" },
{ url = "https://files.pythonhosted.org/packages/85/6d/7c31a506eb847bc58aeb2402d58e17605e821f68ab5df5e66fe274a6df41/librt-0.16.0-cp315-cp315t-win_amd64.whl", hash = "sha256:bd3150023d3dc2bc70f3784e59ffa1140d56ddba3d8125b3d6f9f85221279bfc", size = 126134, upload-time = "2026-09-29T00:55:06.017Z" },
{ url = "https://files.pythonhosted.org/packages/36/69/7a5d10ac409c4da0355e054a14371871da9b5557fcc42772cd00181c6cce/librt-0.16.0-cp315-cp315t-win_arm64.whl", hash = "sha256:8ceafb70f2a4f0826f11031942e59c0728fd98da112dc346d4352bde1e486866", size = 116391, upload-time = "2026-09-29T00:55:07.484Z" },
]
[[package]]
name = "markdown-it-py"
version = "4.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mdurl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" },
]
[[package]]
name = "mdurl"
version = "0.1.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
]
[[package]]
name = "more-itertools"
version = "11.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/de/1d/f4da6f02cdffe04d6362210b807146a26044c88d839208aec273bb0d9184/more_itertools-11.1.0.tar.gz", hash = "sha256:48e8f4d9e7e5878571ecf6f2b4e57634f93cd474cc8cfbd2376f2d11b396e30d", size = 145772, upload-time = "2026-05-22T14:14:29.909Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl", hash = "sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192", size = 72226, upload-time = "2026-05-22T14:14:28.824Z" },
]
[[package]]
name = "mypy"
version = "2.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "ast-serialize" },
{ name = "librt", marker = "platform_python_implementation != 'PyPy'" },
{ name = "mypy-extensions" },
{ name = "pathspec" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/34/4e/64300736cf0a0373a27b94a91b664ee7382e36f77b0621bae6381da3e180/mypy-2.4.0.tar.gz", hash = "sha256:77bdaebd452f43fcfc4cc3ba94352a3ea537cd01e3f2d0879f48673d2ec00d6e", size = 4064425, upload-time = "2026-10-01T20:40:39.229Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e0/66/924be0b653372ed31ad5c48e26044cc00840e591943a56e61621cf05b60e/mypy-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:058165f564ccf559c68c70fec2091fca5891110480210c22594635e3f6683437", size = 14234230, upload-time = "2026-10-01T20:39:44.535Z" },
{ url = "https://files.pythonhosted.org/packages/56/39/c4f176880a4177123576de6cec6309feea8f42fca2bf2f6584e88054f656/mypy-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fa247e02b505a45a2775f69df38d360d197e3790bc60f717595db9eda358b6e", size = 14392944, upload-time = "2026-10-01T20:40:05.78Z" },
{ url = "https://files.pythonhosted.org/packages/bc/1c/26e16977e25ef2494a74f8ffc872a76ec2c3aa43c3156057cbdf88f352c5/mypy-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20e9a5cd875837520c43db98dea0b6d0c2197833d95c30127d8f570fb9b1f00b", size = 15441463, upload-time = "2026-10-01T20:40:32.412Z" },
{ url = "https://files.pythonhosted.org/packages/31/9c/9e4b049f0ecefbfd6817ca2a16eea55dd74a07950268edc6cffd29ccfe98/mypy-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9f03a7828cca2b0adcd6662aee8f2711ff8830e1027641fdea3ab0b787483566", size = 15705406, upload-time = "2026-10-01T20:40:34.844Z" },
{ url = "https://files.pythonhosted.org/packages/4a/5e/e861b5f6c5ef9ee6cd24683aed1edecf82a26dbd536049f9b7850b586267/mypy-2.4.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:9279488933040b638c0ab739084c0ca100efeea6db581bf5d7628d8e89de53fe", size = 7897523, upload-time = "2026-10-01T20:39:39.861Z" },
{ url = "https://files.pythonhosted.org/packages/3a/87/61ffee58b25a956532a6006fae84918a2de849ed25f397492b2e815fe7b3/mypy-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:2106b55105ba5ea9be4f53a24517fc5fa927ff1585edc9bc1a975abb72caef89", size = 11576345, upload-time = "2026-10-01T20:39:41.553Z" },
{ url = "https://files.pythonhosted.org/packages/a1/88/a331c20698971c2ce8d1c30f317fd61b5be13a85b1f053e12dfa22ac6568/mypy-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:528c8744b8b5e3ecb8774f86af38d2376216816e9908317ad055f3c9c2d74799", size = 11172062, upload-time = "2026-10-01T20:38:59.534Z" },
{ url = "https://files.pythonhosted.org/packages/47/c0/4f7daa73270dced8e86c6f4a911c68082d03a84e6c1ec9bd916028d66131/mypy-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0bb95cf34899e4619c61ab0a8667804e139e580b30d5df12af2102dfe44d0c97", size = 15711208, upload-time = "2026-10-01T20:39:30.597Z" },
{ url = "https://files.pythonhosted.org/packages/2f/05/f1afa303c678be24cf7a266d38fb24b3de4599a024c2f9ba0d5905a3efa3/mypy-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86d616fe84c6eab8026f8c50ab5bcb90db780d2ccd233d971e34e92bede9b359", size = 15935442, upload-time = "2026-10-01T20:39:22.781Z" },
{ url = "https://files.pythonhosted.org/packages/9d/d6/6a1a45459b63716e0d035f4892a926d3054f0a8cbfa02551d228a9946083/mypy-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3f86fd1313dd69d013e265f1fdcd12ea7a9d606f9875b2a3db946cd334555f3", size = 17240044, upload-time = "2026-10-01T20:39:32.95Z" },
{ url = "https://files.pythonhosted.org/packages/87/85/ae33bee66c13f98d421964d87bf0888be941063bc75c1204a4cf142cf1cc/mypy-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:720434d48542ecfe84d32d287b727569d3fc8f5769acd39051130e490a5c295c", size = 17545856, upload-time = "2026-10-01T20:39:08.827Z" },
{ url = "https://files.pythonhosted.org/packages/df/de/eeff209b65c3e267818d79333bb9f058da6e4e73761461bd94748a2eb628/mypy-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:a6e851b82c0661f69f1630fc16172c68787a6a9cf0991e7c6437d60976cdcd76", size = 12369342, upload-time = "2026-10-01T20:40:10.516Z" },
{ url = "https://files.pythonhosted.org/packages/2b/43/e62d8d5c1dd737aa248968302ff7d6eabf997c3301773ed8bcb64932ca77/mypy-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:3bd0e340f0ebe65c548210f53be3fd8192e83964760caf0c28bef368e68b0d37", size = 11849099, upload-time = "2026-10-01T20:39:51.834Z" },
{ url = "https://files.pythonhosted.org/packages/2e/5f/335b8980055118dc131355155883fb676bb2161a0d97e08658e479c776fb/mypy-2.4.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:afa89837d9be67e0cadfa33bca3bb7efdda98c3b07e74dc3b635ebfb1c8a926a", size = 14241302, upload-time = "2026-10-01T20:39:13.227Z" },
{ url = "https://files.pythonhosted.org/packages/18/37/1482fdc49332b145828912b15f16eee0c4ca70a8bce0f6514ece79b14680/mypy-2.4.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb443e81057896132d3642d6be219e6efd158691ac7883e3ba8fcb469865f05d", size = 14408200, upload-time = "2026-10-01T20:38:52.576Z" },
{ url = "https://files.pythonhosted.org/packages/04/09/dce2e8f6c1b31053c430ef6963f6f7a38ccb49b90c5e01e37ed0129b7d5a/mypy-2.4.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7f38f57d344f8b6accb40e01c3d83cfc590498231724d16c07ffb7940f157818", size = 15444409, upload-time = "2026-10-01T20:39:18.1Z" },
{ url = "https://files.pythonhosted.org/packages/43/c5/91b68306da4cd280cb15be35dbb5ffc343cabd67c4b121b6625bf2d13177/mypy-2.4.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e76172710bd4e5eeae061abfd68347e5264632e02778be61784671ae3a2132f5", size = 15731186, upload-time = "2026-10-01T20:40:23.426Z" },
{ url = "https://files.pythonhosted.org/packages/64/71/2d0340182a8f27352fb1e25531355951108b506097c433937e9eec452ffd/mypy-2.4.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:f83353e47ab520bf6fd4df8f5897d9fe081211f2fbc4b7d37736a3e3c166cbcf", size = 7897838, upload-time = "2026-10-01T20:38:50.779Z" },
{ url = "https://files.pythonhosted.org/packages/3e/08/32703c117e134c02efa2a82705bb40cee91eaedea10b6530e20eba651b1f/mypy-2.4.0-cp315-cp315-win_amd64.whl", hash = "sha256:970b221ed5842213d98e3c480c08f795ace4b1f81fb21e1b126bd0476bce1c34", size = 11575700, upload-time = "2026-10-01T20:39:53.982Z" },
{ url = "https://files.pythonhosted.org/packages/ab/08/08bb269feafdaad031046ee2d771528a64467e6a180f962afc28c4a3ccd8/mypy-2.4.0-cp315-cp315-win_arm64.whl", hash = "sha256:502b94b0b331f7dafe32fd6b151797ddbb4f32385b362e722c783a025e5954a3", size = 11172908, upload-time = "2026-10-01T20:40:12.91Z" },
{ url = "https://files.pythonhosted.org/packages/fb/3f/c5c92626006ca92c7686adfbece47fc6a0daa0e54753952a9ad13ce0561c/mypy-2.4.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:c9de622fd397495695d0598ddc789222bfcfec9d7c9ec3a1e385c855e3bc5e01", size = 15705970, upload-time = "2026-10-01T20:40:17.79Z" },
{ url = "https://files.pythonhosted.org/packages/10/f3/863365f7997a76a5a1dd42d7902ab05afa4124e8419089cbca1ce2554db1/mypy-2.4.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f459f0b4f0596d9d51fe7716b404b35287b99e77da98a7af90a65dd5fd61141", size = 15942234, upload-time = "2026-10-01T20:40:37.243Z" },
{ url = "https://files.pythonhosted.org/packages/f6/30/2f45b1f425a2c95dbe1a3f4d076bfd42b76e9615dba5906230703b14e4eb/mypy-2.4.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9b028548b3af480e2b1ed8df14ccaac86f99c9f600d1580770ab7ba3dcd40f0", size = 17218364, upload-time = "2026-10-01T20:39:06.497Z" },
{ url = "https://files.pythonhosted.org/packages/43/8b/5b2bbfc69e84800b78fa2dba16d995b003f93b573558e412c5490d6e6c37/mypy-2.4.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:cb734b2668c1f40d07ce093bbeb4407e9527c67901627b0e1679825be3f09975", size = 17557934, upload-time = "2026-10-01T20:38:46.077Z" },
{ url = "https://files.pythonhosted.org/packages/df/c0/1a5dc601c22041a7bbfe1ff71cf097fbbd4fb49930867c6789baf5102106/mypy-2.4.0-cp315-cp315t-win_amd64.whl", hash = "sha256:172e30b8fea631fe310f0c665477f52d9ea40bb4e99e0c81dc30118563b13710", size = 12350955, upload-time = "2026-10-01T20:40:01.52Z" },
{ url = "https://files.pythonhosted.org/packages/1c/bc/697e9e26fc2a86c094ad67ee1e419971b7f01ded66ee66231b09e9f3e12e/mypy-2.4.0-cp315-cp315t-win_arm64.whl", hash = "sha256:5786ef987b3767e51aaa53f20aec104c0252b42ecda7aef8e8b4cbae279b05c5", size = 11842738, upload-time = "2026-10-01T20:40:30.026Z" },
{ url = "https://files.pythonhosted.org/packages/81/12/46ae8670c98a3cd0286ca5645c2f918f8f6be65edfed81b916010619f668/mypy-2.4.0-py3-none-any.whl", hash = "sha256:d01c5d26a352acc6d5cf3128225477e1e8465e8d3029d4c345807fbf7f3cf093", size = 2800734, upload-time = "2026-10-01T20:39:26.837Z" },
]
[[package]]
name = "mypy-extensions"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
]
[[package]]
name = "nh3"
version = "0.3.7"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/18/2f/022b27146d52d24b1b353b003359134788ecbcd6fcdf6283adbd57c0fbc8/nh3-0.3.7.tar.gz", hash = "sha256:71860d01c16f4d8c72e334e0674beb2b0899dbd0bf760de18932ef4390303848", size = 25662, upload-time = "2026-08-23T14:26:30.728Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ce/88/b594f0e86856b37e182fb663283da419eea6424972506e640e890885467f/nh3-0.3.7-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:91a4dab4e94d9fc54b9f67b1adfb23e81fab7ab43f33c3b8c97be9aa38f789ba", size = 1471147, upload-time = "2026-08-23T14:25:55.259Z" },
{ url = "https://files.pythonhosted.org/packages/1e/60/847a21339f095c4d4c655af31fa2d18b174585bcc210709facacc7ce205c/nh3-0.3.7-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eae64328e46a25785535afcb6885b6f182ecaf5ee8c88f8c075422db8aacc65b", size = 820463, upload-time = "2026-08-23T14:25:56.803Z" },
{ url = "https://files.pythonhosted.org/packages/7b/7f/1a103e00aaf5e59f2dee4c2709aac609bb2d4bb74fddaf0dcfade11ed87b/nh3-0.3.7-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4968fe8d2db97c6f047659bf46a449fd8ec377f44ebf3e0a1b96c0d3a333ae32", size = 861456, upload-time = "2026-08-23T14:25:58.087Z" },
{ url = "https://files.pythonhosted.org/packages/d8/4a/e9c436089a0c80b928011ead0efd156aa7639a19b6064ef58dcedcab8369/nh3-0.3.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:be53a4825585f701955cb9baf49f478f56eb81e20294329fe4bc689dd5dd81fa", size = 1023930, upload-time = "2026-08-23T14:25:59.465Z" },
{ url = "https://files.pythonhosted.org/packages/04/5c/aa1468e3e281e78d2b3b7d762ccba59f681af355e971dbd255d5903f7b86/nh3-0.3.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:94fd6e59553fbb9ffd8ba71bbd5a54e3126ba01799a097ae30d5341d750bc6ac", size = 1102614, upload-time = "2026-08-23T14:26:00.869Z" },
{ url = "https://files.pythonhosted.org/packages/6a/9f/57d186d9d3dd38905dc12dddb3484406cdf6aa0b1ce33639a2d277d4ee1c/nh3-0.3.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:18f4278ecd157d43cb35acd5aae9f35cfa79f546b4922bd86536adc0f6312102", size = 1059915, upload-time = "2026-08-23T14:26:02.388Z" },
{ url = "https://files.pythonhosted.org/packages/6b/53/097a5ad0b34b15d67a472ef849165a54209fa5fbd3e639801c6fe439ba28/nh3-0.3.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:808def0c8c07843e6e50dc84f532457bfa2cfd17417b219a5d9e7c773709331a", size = 1047402, upload-time = "2026-08-23T14:26:03.897Z" },
{ url = "https://files.pythonhosted.org/packages/9a/a7/c57a2c70534418310889a65ccfac3525e62f0bc0a8613225903403755ce7/nh3-0.3.7-cp314-cp314t-win32.whl", hash = "sha256:874b7d67a067bd29a59223f6270fc30da4edd8e6d87fd219fc93bcbaa662c946", size = 619895, upload-time = "2026-08-23T14:26:05.105Z" },
{ url = "https://files.pythonhosted.org/packages/e6/b7/efda1d0a611d940bdfde6893bde1ea6b7b7d48c31273aea48e35b822fd58/nh3-0.3.7-cp314-cp314t-win_amd64.whl", hash = "sha256:614dac4a4c36ad084e78447d16fe898dedd762e354a7ab9cda2984e82f67883d", size = 633456, upload-time = "2026-08-23T14:26:06.661Z" },
{ url = "https://files.pythonhosted.org/packages/1d/18/3ab564595cb88196f50d26e163ed0fd2acc731ab26ac615df91981885887/nh3-0.3.7-cp314-cp314t-win_arm64.whl", hash = "sha256:157ec1eb7a62f3d9a7badb8d82d89aa810e3e24e097eedfa481a25d0c8a99877", size = 611003, upload-time = "2026-08-23T14:26:07.813Z" },
{ url = "https://files.pythonhosted.org/packages/94/0d/c257754bf57f829f307aa226bbe136d3a1356b5a0d08324c7b6bd2a8aacd/nh3-0.3.7-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:6c3aa50eb26e9228238271db9f983cbc3b006dfbfeca2d4dc34c33ddc6ac5ea5", size = 1493959, upload-time = "2026-08-23T14:26:09.025Z" },
{ url = "https://files.pythonhosted.org/packages/07/42/a687e7091928806e514f89fa2666f25ec9bfe0a902fc4402b25e51ce408b/nh3-0.3.7-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f266d3f1b3647449923a8e406524632220dd5d8b647078dfe45b885d33d10479", size = 859615, upload-time = "2026-08-23T14:26:10.606Z" },
{ url = "https://files.pythonhosted.org/packages/85/05/b0e6bef633549a23347d5462aa288fcc42381e7918482062ca3cb456242a/nh3-0.3.7-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e8fd1ab205258b29254f72db377d99e2c96aa7653ef3b015ccab0420b094b506", size = 839872, upload-time = "2026-08-23T14:26:12.037Z" },
{ url = "https://files.pythonhosted.org/packages/17/40/2a0921d45b20828708bcb56887e47dcf8cae13818de5bf9a01308d348712/nh3-0.3.7-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:19f288c938ec6eef1f5d2c6cab47838e71fef8097e1c1233802be5a6230ba086", size = 1091325, upload-time = "2026-08-23T14:26:13.34Z" },
{ url = "https://files.pythonhosted.org/packages/e4/d1/9d70e0e418a48280ec0ddc6c1b08b4b1136ebcc31a1625e57ff5c665fa51/nh3-0.3.7-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de2b2aab32ea303405debefdcfc58043d3e635fa3f67b9eb140d2b0e0c0d2563", size = 1042482, upload-time = "2026-08-23T14:26:14.667Z" },
{ url = "https://files.pythonhosted.org/packages/93/a7/02dd159d4e71f98607d8d4249cddb7561e77be1a8e4dec77d76e1b68fc99/nh3-0.3.7-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b7279d43323a25225df23576af6594a16693f61431170848b8b2ac21ad4f174", size = 946868, upload-time = "2026-08-23T14:26:16.094Z" },
{ url = "https://files.pythonhosted.org/packages/a6/ed/c5510c615dce55b6fcc364aa1838142f938beed64f5e4927490dfcaf4405/nh3-0.3.7-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70f5ac8626e899a4bab0ef74ca2f5bd602f49c7b739e6e5026b4afc6d63dac42", size = 832161, upload-time = "2026-08-23T14:26:17.272Z" },
{ url = "https://files.pythonhosted.org/packages/7b/e3/3212c1a5b5745245d7f18885207bbddb34c56075f34dd682bd539aad55cc/nh3-0.3.7-cp38-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:5ffdfcb9a686ffb12765376bcfb6b5b55728516d3c0ee317d29982381ded3df8", size = 849791, upload-time = "2026-08-23T14:26:18.498Z" },
{ url = "https://files.pythonhosted.org/packages/20/64/9e36594efad6c290de4240d02cb2bd80c339a4ab1c4de66e599ffa6d9d81/nh3-0.3.7-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bc42bb1193c1e28a1e74c2cabaca178e118a7103e8832699fef8a2b3e2496493", size = 875473, upload-time = "2026-08-23T14:26:19.908Z" },
{ url = "https://files.pythonhosted.org/packages/00/0c/1a8985fd43fea5530c0ac890b6f0b423770ee72f111b70b7a77f2dec243a/nh3-0.3.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d56e76bd3cadb09b6b0cef364850811663734b348a25f5f587a2819c495367bd", size = 1036463, upload-time = "2026-08-23T14:26:21.536Z" },
{ url = "https://files.pythonhosted.org/packages/b2/5d/891e533b716cf00df76ad0ba6485dcfd14d59a6430a3cc99057c4c04004e/nh3-0.3.7-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:fd4a70efb45d5372174f718878eb7a35c12677626a63b2f103b23b833457dcac", size = 1116029, upload-time = "2026-08-23T14:26:22.907Z" },
{ url = "https://files.pythonhosted.org/packages/42/e5/ae8c0782fce74fb6fcf7234bb3d4017f37ce181b4f9d29369eab21c50a04/nh3-0.3.7-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:15f5fbf090f5c88d61c820e1fc1fceecb6520cca9fe85649c06b57ef9dc9ff62", size = 1076589, upload-time = "2026-08-23T14:26:24.302Z" },
{ url = "https://files.pythonhosted.org/packages/26/a4/c3423351e8d864ad756e85e15f0c01433361f14d34e4ed156482c0518f2a/nh3-0.3.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6698a822132beedab80f131c08d8d0ac5a178ddeb488d02ca4b67716ecfac7af", size = 1058871, upload-time = "2026-08-23T14:26:25.674Z" },
{ url = "https://files.pythonhosted.org/packages/4b/6a/478f153f1d7c0baaa3d1e8bb5fdcee3a6235f90fe44ea969a9d4e2b8c47a/nh3-0.3.7-cp38-abi3-win32.whl", hash = "sha256:6e4280115d44c3b278eef712a86748c1a723105cd79feec46952383117ab4e59", size = 630729, upload-time = "2026-08-23T14:26:26.932Z" },
{ url = "https://files.pythonhosted.org/packages/b4/b9/34433ccb1f0fe6968dabbb7d4bf5721c6221878ef07832748c06655a6a80/nh3-0.3.7-cp38-abi3-win_amd64.whl", hash = "sha256:618e3059caf41ccdf5dcccb3fa9df4cf6e4efe23d1382a8bbfca272a8a4f8bfc", size = 644462, upload-time = "2026-08-23T14:26:28.294Z" },
{ url = "https://files.pythonhosted.org/packages/f9/70/e140dffff6e808dc6343598df76e7e2407fd0f581de3524c75fba2e0cf24/nh3-0.3.7-cp38-abi3-win_arm64.whl", hash = "sha256:f04b7d333b27f13ca439da3cf1c75c2fba34f104969f6ce4ac8e7079699c2f4a", size = 621867, upload-time = "2026-08-23T14:26:29.547Z" },
]
[[package]]
name = "openai"
version = "3.22.0"
@@ -187,23 +741,105 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
]
[[package]]
name = "pathspec"
version = "1.1.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" },
]
[[package]]
name = "photools3"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "black" },
{ name = "mypy" },
{ name = "openai" },
{ name = "pillow" },
{ name = "pydantic" },
{ name = "pydantic-settings" },
{ name = "pyexiftool" },
{ name = "pytest" },
{ name = "ruff" },
{ name = "typer" },
{ name = "utils" },
]
[package.metadata]
requires-dist = [
{ name = "black", specifier = ">=26.5.1" },
{ name = "mypy", specifier = ">=2.4.0" },
{ name = "openai", specifier = ">=3.22.0" },
{ name = "pillow", specifier = ">=12.3.0" },
{ name = "pydantic", specifier = ">=2.13.5" },
{ name = "pydantic-settings", specifier = ">=2.15.0" },
{ name = "pyexiftool", specifier = ">=0.5.6" },
{ name = "pytest", specifier = ">=9.1.1" },
{ name = "ruff", specifier = ">=0.16.9" },
{ name = "typer", specifier = ">=0.12.0" },
{ name = "utils", specifier = ">=0.1.3", index = "https://gitea.schneiderserver.site/api/packages/mathias2784/pypi/simple" },
]
[[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/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 = "platformdirs"
version = "4.12.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/17/c8/721b3855fe457da514fe249247d404b9b39c5d16532278f70ebaa6acf18b/platformdirs-4.12.2.tar.gz", hash = "sha256:eab5f70271a490ef74618bb314fbb86e3c7e82fa3b9c922c2ea0e0a1a155d329", size = 59353, upload-time = "2026-09-29T18:27:56.009Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d0/89/446044f33aba0348d35e433f56d12206d010a5281a1df54054d4cfb82388/platformdirs-4.12.2-py3-none-any.whl", hash = "sha256:29dbf06d96c500bc6bdbce75fb0a14d63279c93b1842f97e72a135b33e856983", size = 32457, upload-time = "2026-09-29T18:27:54.43Z" },
]
[[package]]
@@ -215,6 +851,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
[[package]]
name = "pycparser"
version = "3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
]
[[package]]
name = "pydantic"
version = "2.13.5"
@@ -285,6 +930,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/30/a4/2bffa9f8e804325a09867f0e9d30795c80ea9f8d62560bd1b6ad6220eb2f/pydantic_settings-2.15.0-py3-none-any.whl", hash = "sha256:0ba092c291c94baceb5eff768aa0d56400a457585bc0175925a5a5510303da42", size = 69413, upload-time = "2026-08-07T09:24:55.839Z" },
]
[[package]]
name = "pyexiftool"
version = "0.5.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5e/48/406da6691d15abf3c8d399bce8bc588709a5b54e857fd7c22dad2f90c33c/PyExifTool-0.5.6.tar.gz", hash = "sha256:22a972c1c212d1ad5f61916fded5057333dcc48fb8e42eed12d2ff9665b367ae", size = 56365, upload-time = "2023-10-22T23:18:18.819Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fc/b9/175e9a1f8f3f94b22f622f0fcac853ae2c43cb4ac6034f849269c6086dac/PyExifTool-0.5.6-py3-none-any.whl", hash = "sha256:ac7d7836d2bf373f20aa558528f6b2222c4c0d896ed28c951a3ff8e6cec05a87", size = 51243, upload-time = "2023-10-22T23:18:16.614Z" },
]
[[package]]
name = "pygments"
version = "2.21.0"
@@ -319,6 +973,144 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0d/17/c5c6b53ddc18f297992099b3d9ec16c855c0ccc83263a21fe4d1c625ec6c/python_dotenv-1.2.3-py3-none-any.whl", hash = "sha256:904552145e8bfed22162c09dab1c2b9b54fefa7b23ba780f4f26ca0316b0f0d9", size = 22780, upload-time = "2026-08-16T16:54:52.473Z" },
]
[[package]]
name = "pytokens"
version = "0.4.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" },
{ url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" },
{ url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" },
{ url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" },
{ url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" },
{ url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" },
{ url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" },
{ url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" },
{ url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" },
{ url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" },
{ url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" },
]
[[package]]
name = "pywin32-ctypes"
version = "0.2.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" },
]
[[package]]
name = "readme-renderer"
version = "46.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "docutils" },
{ name = "nh3" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/25/d7/9309494fad74ee831d4546f69325b5519f37c6dfb2d9ba495db8c6d4f4ca/readme_renderer-46.0.tar.gz", hash = "sha256:af3e964914f6310a33ff67b72a4bdd940bed8d7c3bdecd2d14f40edf284bfe90", size = 38382, upload-time = "2026-08-28T15:18:32.49Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4b/72/ac5ca81fe9121fcaa9d828d21017cba00a16a98e4ea5fb60c878f93dda4f/readme_renderer-46.0-py3-none-any.whl", hash = "sha256:d0dae1f74bb273b534770cb4cccb6bb78735540afdb03c2146f4e19dcd412560", size = 14239, upload-time = "2026-08-28T15:18:31.132Z" },
]
[[package]]
name = "requests"
version = "2.34.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" },
]
[[package]]
name = "requests-toolbelt"
version = "1.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "requests" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" },
]
[[package]]
name = "rfc3986"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", size = 49026, upload-time = "2022-01-10T00:52:30.832Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", size = 31326, upload-time = "2022-01-10T00:52:29.594Z" },
]
[[package]]
name = "rich"
version = "15.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markdown-it-py" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
]
[[package]]
name = "ruff"
version = "0.16.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c4/49/23802c45f093eb14bde54b141d2b2f058edfa63a06db7beed047308cc08f/ruff-0.16.10.tar.gz", hash = "sha256:eff4728c4eaae93f0955cd264d24b2ab348e74bf59986ccf282ba6dc16b3b017", size = 4958724, upload-time = "2026-10-01T18:03:21.697Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2f/21/ebce22e1d90cdb2cd691026b9c6e9bec6499f0b396089481755b5d49efff/ruff-0.16.10-py3-none-linux_armv6l.whl", hash = "sha256:488b0fe3f3574210e5cf80d9f59b9e3ab17a127a8155de3f307b392589cfb511", size = 10095557, upload-time = "2026-10-01T18:02:36.072Z" },
{ url = "https://files.pythonhosted.org/packages/cb/98/a54de85876a8b2612bfa0d84c7b9abfb39c6a3354aee7800b09c1649b9e3/ruff-0.16.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e748ff95c934c4e978783b8e687bc174e7bd84e8ad24e3243e1ecfcda5e0282d", size = 10340577, upload-time = "2026-10-01T18:02:39.258Z" },
{ url = "https://files.pythonhosted.org/packages/9f/16/1a5a4a2657effe4806110f2b907313802f1367fcdbb29e8122766e407fab/ruff-0.16.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3031a4a2e8e7b8a46f70be45f198c35a11ece509a94b80334d8d397a33c67550", size = 9774282, upload-time = "2026-10-01T18:02:41.79Z" },
{ url = "https://files.pythonhosted.org/packages/6e/fb/470085af734da396e68cd80fb0a3e7c109a459716ae59588c0f6fab8a17d/ruff-0.16.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:494401c86df4c4c25f69b9419605d944467ee98c42fb6ad405ef4fa40b8fb67d", size = 9920895, upload-time = "2026-10-01T18:02:44.458Z" },
{ url = "https://files.pythonhosted.org/packages/57/de/f10cffe4f88a37ea6615ec460f01bf76f0bc1477737a35d9ee61a630a78b/ruff-0.16.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d203abc0ff2b773ee33d00ab8df0bb67046fbc7c07b119332f08b9b345cf8221", size = 9902707, upload-time = "2026-10-01T18:02:47.041Z" },
{ url = "https://files.pythonhosted.org/packages/ff/44/3fdcedf83ae60ef837dd239170e480dce606a9142cfa2db911afdf855fd9/ruff-0.16.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd83d1235a5258d318477bc5b576303974cbdef5df0c01a1bff14efcc23bd12a", size = 10619287, upload-time = "2026-10-01T18:02:49.485Z" },
{ url = "https://files.pythonhosted.org/packages/c1/62/02e76a5574002153618eafbb70e159468addc72a5d2c488e65cbb4639d2d/ruff-0.16.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc2610fb269fa56dd8a68669ae470fa6272902668c0fc2ebc3aa112b2633d5b8", size = 11339942, upload-time = "2026-10-01T18:02:52.008Z" },
{ url = "https://files.pythonhosted.org/packages/6b/c4/cde27d47ad8d4126c587e608c67c46e7feac11763f606d261a1bc8a489e6/ruff-0.16.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e70175e29cc94c26ea296c80e470180b744b7419026898e58f520c6ab32578e", size = 10934316, upload-time = "2026-10-01T18:02:54.811Z" },
{ url = "https://files.pythonhosted.org/packages/e4/03/17234145f302a645a123e8c3bb2411ecf4669fbc350de3b0d803230a1729/ruff-0.16.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f33f43a864a8483eebd160e713336c8bab02c934feaff0a33cf5ccb41546d09a", size = 10387968, upload-time = "2026-10-01T18:02:57.497Z" },
{ url = "https://files.pythonhosted.org/packages/71/29/2493af60240ee7644b38a4b821f5f9c3b5a4fa3178770fe1d0c685217395/ruff-0.16.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1dfc6f0088149fb6a362c1c446bcbb3fd2157b3852fe2fa68409276eab9ad9b3", size = 10537906, upload-time = "2026-10-01T18:03:00.006Z" },
{ url = "https://files.pythonhosted.org/packages/2e/9a/f56b28f3b143bb9e518c34fb89ab191b626bca8b70dbf8af0d2e2d473572/ruff-0.16.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6553498afc35f580f036030795810b9e6bcea31604b0fd9e8d352795473042e3", size = 10012938, upload-time = "2026-10-01T18:03:03.006Z" },
{ url = "https://files.pythonhosted.org/packages/c2/c4/fca37362848ea4d4d80712df13632e645e7c7cfb1bdedf140699ac7a090b/ruff-0.16.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a3b8471dea115d37f123882be852bed13403746d3a76c11de4a19ec5f9ff5a03", size = 9897945, upload-time = "2026-10-01T18:03:05.818Z" },
{ url = "https://files.pythonhosted.org/packages/7d/c7/e0bc57664d6e0c61fd22f620af260fe9662d7e1ddb320ea7f160e76ef165/ruff-0.16.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:92e59a70bcbd9d3a5483656da906ec28edfdacfce00afd99edb8b4e9d15644be", size = 10333134, upload-time = "2026-10-01T18:03:08.25Z" },
{ url = "https://files.pythonhosted.org/packages/21/aa/5c9f3b68737c0e4a33a1db5dfab44f7b786d96ca91a7233112ddab1e6dc2/ruff-0.16.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7ae7375f803b5520dc9f546bed7e3a0acb70b91812e9bb4b19927de22f25b77d", size = 10741702, upload-time = "2026-10-01T18:03:10.775Z" },
{ url = "https://files.pythonhosted.org/packages/78/fa/0f9c2020dc316c53d983be011720b8157cc052b97e3f4dfa7db6d0f880a6/ruff-0.16.10-py3-none-win32.whl", hash = "sha256:2a12e01cb9156c10c466f63b46eaae5ecea28dfbd21b5836353ae498e7d1349a", size = 10139176, upload-time = "2026-10-01T18:03:13.267Z" },
{ url = "https://files.pythonhosted.org/packages/99/29/cfb0df9448d4d4ad48c2de029ada9ebd71baa6da983a6c77ee6c6cd0fe82/ruff-0.16.10-py3-none-win_amd64.whl", hash = "sha256:97f2015c92aa97105b0eab19eb5d224884399281cfc5da86a92db4ab5e7fb2ca", size = 10584734, upload-time = "2026-10-01T18:03:16.006Z" },
{ url = "https://files.pythonhosted.org/packages/fc/05/c16957eb287c3fc062e032619a25868d93d408a844b725bcf514f7a378ff/ruff-0.16.10-py3-none-win_arm64.whl", hash = "sha256:25a65fe998c4e6861ec079ada5826a2fc605e6cbccbe9dcd7fac1f54e791621b", size = 10366440, upload-time = "2026-10-01T18:03:19.04Z" },
]
[[package]]
name = "secretstorage"
version = "3.5.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
{ name = "jeepney" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" },
]
[[package]]
name = "shellingham"
version = "1.5.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
]
[[package]]
name = "sniffio"
version = "1.3.1"
@@ -328,6 +1120,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
]
[[package]]
name = "tk"
version = "0.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a0/81/742b342fd642e672fbedecde725ba44db44e800dc4c936216c3c6729885a/tk-0.1.0.tar.gz", hash = "sha256:60bc8923d5d35f67f5c6bd93d4f0c49d2048114ec077768f959aef36d4ed97f8", size = 2247, upload-time = "2019-07-08T06:51:58.051Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/0b/029cbdb868bb555fed99bf6540fff072d500b3f895873709f25084e85e33/tk-0.1.0-py3-none-any.whl", hash = "sha256:703a69ff0d5ba2bd2f7440582ad10160e4a6561595d33457dc6caa79b9bf4930", size = 3879, upload-time = "2019-07-08T06:51:55.175Z" },
]
[[package]]
name = "truststore"
version = "0.10.4"
@@ -337,6 +1138,41 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" },
]
[[package]]
name = "twine"
version = "7.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "id" },
{ name = "keyring", marker = "platform_machine != 'ppc64le' and platform_machine != 's390x'" },
{ name = "packaging" },
{ name = "readme-renderer" },
{ name = "requests" },
{ name = "requests-toolbelt" },
{ name = "rfc3986" },
{ name = "rich" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/92/3c/58f808a359700f39a967dffede33efeac809262c03303fa3eec6afff8f49/twine-7.0.0.tar.gz", hash = "sha256:85cdb29c518efef867360ae4acd4b0dfd61c8654a22fca08e6f8539f05022177", size = 215032, upload-time = "2026-07-27T15:59:00.825Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/96/08/ddcdc06225eaad6de0e48e1002b06d919dbde20582d0662c7af51308e5d6/twine-7.0.0-py3-none-any.whl", hash = "sha256:b854164df26db268af05f49aa5c0344b10e27a494343ff05b1e0bad3b135f5a7", size = 43204, upload-time = "2026-07-27T15:58:59.26Z" },
]
[[package]]
name = "typer"
version = "0.27.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-doc" },
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "rich" },
{ name = "shellingham" },
]
sdist = { url = "https://files.pythonhosted.org/packages/16/f7/57713ba479fd405eb76de31404b2c744c289e336b2d999511ebf51e496f7/typer-0.27.2.tar.gz", hash = "sha256:269b7eb9d3c202ca84b4bc9618cb04ebb43d3d4d1e567e4c768607232c05f945", size = 204045, upload-time = "2026-08-28T10:26:55.046Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/bf/205d0004930ede8f542fb58f601526fccf4ae7626075ca1e6c4de5d3d652/typer-0.27.2-py3-none-any.whl", hash = "sha256:b3a5fc4342d5fc8fda8fc3010b1cf117e9249aab7fae800c2eff62fd3842d97d", size = 123130, upload-time = "2026-08-28T10:26:53.752Z" },
]
[[package]]
name = "typing-extensions"
version = "4.16.0"
@@ -357,3 +1193,44 @@ sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c
wheels = [
{ url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" },
]
[[package]]
name = "urllib3"
version = "2.8.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e3/05/b17359e1cefb4f909b5e40b1b90a496d987258916dbbf88e842c729f510e/urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63", size = 458972, upload-time = "2026-09-15T19:29:36.253Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", size = 135717, upload-time = "2026-09-15T19:29:34.577Z" },
]
[[package]]
name = "utils"
version = "0.1.3"
source = { registry = "https://gitea.schneiderserver.site/api/packages/mathias2784/pypi/simple" }
dependencies = [
{ name = "black" },
{ name = "pydantic" },
{ name = "pydantic-settings" },
{ name = "pytest" },
{ name = "ruff" },
{ name = "tk" },
{ name = "twine" },
{ name = "uvicorn" },
]
sdist = { url = "https://gitea.schneiderserver.site/api/packages/mathias2784/pypi/files/utils/0.1.3/utils-0.1.3.tar.gz", hash = "sha256:8970a179573a63ab4c546cfd1da8ceca0d915bc4357b49ccffaba9b3d4a0195e" }
wheels = [
{ url = "https://gitea.schneiderserver.site/api/packages/mathias2784/pypi/files/utils/0.1.3/utils-0.1.3-py3-none-any.whl", hash = "sha256:c24078ba05f9f5b41fc3909598084b93c6df59f4ca60001e1e0b4fa55797e642" },
]
[[package]]
name = "uvicorn"
version = "0.54.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/da/34/30e9280707135d2cfc589dfff3cb796bd07a3aeb1a3e415ba09dd89d7bb4/uvicorn-0.54.0.tar.gz", hash = "sha256:a2e33cbfaa0306f8e6b0c13e0cb89d7d7a2da3e62b90c66e18c33d9807b28620", size = 112283, upload-time = "2026-09-25T06:52:37.601Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/38/0c/b54a4fdd7f90a3af8b02ebc9ce6712c2c208b7926a2f7bad95c33ebbe943/uvicorn-0.54.0-py3-none-any.whl", hash = "sha256:505bdb0f318731d45f1f712071fc781a8981f6847a31c902c9f5e652d4f67faf", size = 87427, upload-time = "2026-09-25T06:52:35.829Z" },
]