Compare commits
1
Commits
master
..
68511550a0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68511550a0 |
@@ -1,10 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
---
|
|
||||||
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
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
name: Tests
|
||||||
|
|
||||||
|
on: [push]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
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 default settings
|
||||||
|
run: cp settings.template.py settings.py
|
||||||
|
- name: Run pytest
|
||||||
|
run: uv run pytest
|
||||||
|
|
||||||
|
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 default settings
|
||||||
|
run: cp settings.template.py settings.py
|
||||||
|
- name: Check formatting (black)
|
||||||
|
run: uv run black --check . --extend-exclude .venv
|
||||||
|
- name: Lint (ruff)
|
||||||
|
run: uv run ruff check .
|
||||||
@@ -10,4 +10,3 @@ wheels/
|
|||||||
.venv
|
.venv
|
||||||
|
|
||||||
.env
|
.env
|
||||||
logs/
|
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
# 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
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -1,371 +0,0 @@
|
|||||||
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.
@@ -8,17 +8,10 @@ authors = [
|
|||||||
]
|
]
|
||||||
requires-python = ">=3.14"
|
requires-python = ">=3.14"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"black>=26.5.1",
|
|
||||||
"mypy>=2.4.0",
|
|
||||||
"openai>=3.22.0",
|
"openai>=3.22.0",
|
||||||
"pillow>=12.3.0",
|
|
||||||
"pydantic>=2.13.5",
|
"pydantic>=2.13.5",
|
||||||
"pydantic-settings>=2.15.0",
|
"pydantic-settings>=2.15.0",
|
||||||
"pyexiftool>=0.5.6",
|
|
||||||
"pytest>=9.1.1",
|
"pytest>=9.1.1",
|
||||||
"ruff>=0.16.9",
|
|
||||||
"typer>=0.12.0",
|
|
||||||
"utils>=0.1.3",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
@@ -27,18 +20,3 @@ photools3 = "photools3:main"
|
|||||||
[build-system]
|
[build-system]
|
||||||
requires = ["uv_build>=0.12.19,<0.13.0"]
|
requires = ["uv_build>=0.12.19,<0.13.0"]
|
||||||
build-backend = "uv_build"
|
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
|
|
||||||
|
|||||||
@@ -1,5 +1,2 @@
|
|||||||
from .cli import app
|
def main() -> None:
|
||||||
|
print("Hello from photools3!")
|
||||||
|
|
||||||
def main():
|
|
||||||
app()
|
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
from .cli import app
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
app()
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
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()
|
|
||||||
@@ -1,108 +1,25 @@
|
|||||||
import json
|
|
||||||
import logging
|
|
||||||
import re
|
|
||||||
import time
|
|
||||||
|
|
||||||
import openai
|
|
||||||
from openai import OpenAI
|
from openai import OpenAI
|
||||||
from pydantic import BaseModel, ValidationError
|
from openai.types.chat import ChatCompletion
|
||||||
|
|
||||||
from photools3.lm_studio import LMStudioModelManager
|
from photools3.models import LlmRequest
|
||||||
from photools3.models import (
|
from photools3.settings import Settings
|
||||||
LlmMessageContent,
|
|
||||||
LlmMessageRole,
|
|
||||||
LlmMessageType,
|
|
||||||
LlmRequest,
|
|
||||||
LlmRequestMessage,
|
|
||||||
LlmResponseCaption,
|
|
||||||
LlmResponseCaptionsAndKeywords,
|
|
||||||
LlmResponseGeneral,
|
|
||||||
LlmResponseKeywords,
|
|
||||||
LlmResponseQuality,
|
|
||||||
load_valid_keywords,
|
|
||||||
)
|
|
||||||
from photools3.photo import Photo
|
|
||||||
from photools3.settings import settings
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
settings = Settings()
|
||||||
|
|
||||||
|
|
||||||
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:
|
class LlmSession:
|
||||||
base_url: str = str(settings.llm_api_base_url)
|
base_url: str = str(settings.llm_api_base_url)
|
||||||
api_key: str = settings.llm_api_key
|
api_key: str = settings.llm_api_key
|
||||||
response_model: type[BaseModel] = LlmResponseGeneral
|
|
||||||
prompt: str = ""
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, system_prompt: str):
|
||||||
self, system_prompt: str, temperature: float = settings.llm_default_temperature
|
|
||||||
):
|
|
||||||
self._models: list = []
|
self._models: list = []
|
||||||
self._model_id: str | None = None
|
|
||||||
|
|
||||||
self.system_prompt = system_prompt
|
self.system_prompt = system_prompt
|
||||||
self.temperature = temperature
|
|
||||||
|
|
||||||
self.client = OpenAI(
|
self.client = OpenAI(
|
||||||
base_url=self.base_url,
|
base_url=self.base_url,
|
||||||
api_key=self.api_key,
|
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
|
@property
|
||||||
def models(self):
|
def models(self):
|
||||||
@@ -110,247 +27,5 @@ class LlmSession:
|
|||||||
self._models = self.client.models.list().data
|
self._models = self.client.models.list().data
|
||||||
return self._models
|
return self._models
|
||||||
|
|
||||||
def call(
|
def call_raw(self, request: LlmRequest) -> ChatCompletion:
|
||||||
self,
|
return self.client.chat.completions.create(**request.model_dump())
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,135 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
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)
|
|
||||||
+7
-160
@@ -1,79 +1,23 @@
|
|||||||
import json
|
from typing import Literal
|
||||||
from enum import StrEnum
|
|
||||||
from functools import lru_cache
|
|
||||||
from typing import Annotated, Any, Literal
|
|
||||||
|
|
||||||
from pydantic import (
|
from pydantic import BaseModel, model_validator
|
||||||
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):
|
class LlmRequest(BaseModel):
|
||||||
model: str = "gpt-4o"
|
model_id: int
|
||||||
messages: list[LlmRequestMessage]
|
messages: list[LlmRequestMessage]
|
||||||
temperature: float | None = None
|
temperature: float | None = None
|
||||||
max_tokens: int
|
|
||||||
|
|
||||||
|
|
||||||
class LlmMessageRole(StrEnum):
|
|
||||||
system = "system"
|
|
||||||
user = "user"
|
|
||||||
|
|
||||||
|
|
||||||
class LlmRequestMessage(BaseModel):
|
class LlmRequestMessage(BaseModel):
|
||||||
role: LlmMessageRole
|
role: Literal["system", "user"]
|
||||||
content: list[LlmMessageContent] | str
|
content: list[LlmMessageContent]
|
||||||
|
|
||||||
|
|
||||||
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):
|
class LlmMessageContent(BaseModel):
|
||||||
type: LlmMessageType
|
type: Literal["text", "image_url"]
|
||||||
text: str | None = None
|
text: str | None = None
|
||||||
image_url: Base64ImageUrl | None = None
|
image_url: str | None = None
|
||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
def _type_matches_content(self):
|
def _type_matches_content(self):
|
||||||
@@ -82,100 +26,3 @@ class LlmMessageContent(BaseModel):
|
|||||||
if self.type == "image_url" and self.image_url is None:
|
if self.type == "image_url" and self.image_url is None:
|
||||||
raise ValueError("image_url must be set when type is 'image_url'")
|
raise ValueError("image_url must be set when type is 'image_url'")
|
||||||
return self
|
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
|
|
||||||
|
|||||||
+9
-40
@@ -1,49 +1,18 @@
|
|||||||
import base64
|
|
||||||
from io import BytesIO
|
|
||||||
from pathlib import Path
|
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:
|
class Photo:
|
||||||
|
|
||||||
def __init__(self, path: Path):
|
def __init__(self):
|
||||||
self.path = path
|
...
|
||||||
self.image = Image.open(path)
|
|
||||||
|
|
||||||
def close(self):
|
@classmethod
|
||||||
self.image.close()
|
def from_file(cls, path: Path) -> Photo:
|
||||||
|
# TODO
|
||||||
|
return cls()
|
||||||
|
|
||||||
def __enter__(self) -> Self:
|
def resize(self) -> Photo:
|
||||||
|
# TODO
|
||||||
return 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())
|
|
||||||
|
|||||||
@@ -1,138 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,31 +1,9 @@
|
|||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from pydantic import Field
|
|
||||||
from pydantic_core import Url
|
from pydantic_core import Url
|
||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
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):
|
class Settings(BaseSettings):
|
||||||
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
|
model_config = SettingsConfigDict(env_file=".env")
|
||||||
|
|
||||||
llm_api_base_url: Url
|
llm_api_base_url: Url
|
||||||
llm_api_key: str
|
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]
|
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
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()
|
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,303 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
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")
|
|
||||||
@@ -1,243 +0,0 @@
|
|||||||
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"})
|
|
||||||
@@ -1,285 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
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()
|
|
||||||
+2
-31
@@ -1,35 +1,6 @@
|
|||||||
from pathlib import Path
|
from photools3.settings import Settings
|
||||||
|
|
||||||
from photools3.settings import (
|
settings = Settings()
|
||||||
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():
|
def test_settings_base():
|
||||||
|
|||||||
@@ -1,19 +1,6 @@
|
|||||||
version = 1
|
version = 1
|
||||||
revision = 5
|
revision = 3
|
||||||
requires-python = ">=3.14"
|
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]]
|
[[package]]
|
||||||
name = "annotated-types"
|
name = "annotated-types"
|
||||||
@@ -37,235 +24,6 @@ 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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "colorama"
|
name = "colorama"
|
||||||
version = "0.4.6"
|
version = "0.4.6"
|
||||||
@@ -275,62 +33,6 @@ 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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "h11"
|
name = "h11"
|
||||||
version = "0.16.0"
|
version = "0.16.0"
|
||||||
@@ -378,18 +80,6 @@ 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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "3.20"
|
version = "3.20"
|
||||||
@@ -408,48 +98,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "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]]
|
[[package]]
|
||||||
name = "jiter"
|
name = "jiter"
|
||||||
version = "0.17.0"
|
version = "0.17.0"
|
||||||
@@ -513,208 +161,6 @@ 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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "openai"
|
name = "openai"
|
||||||
version = "3.22.0"
|
version = "3.22.0"
|
||||||
@@ -741,105 +187,23 @@ 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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "photools3"
|
name = "photools3"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "black" },
|
|
||||||
{ name = "mypy" },
|
|
||||||
{ name = "openai" },
|
{ name = "openai" },
|
||||||
{ name = "pillow" },
|
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
{ name = "pydantic-settings" },
|
{ name = "pydantic-settings" },
|
||||||
{ name = "pyexiftool" },
|
|
||||||
{ name = "pytest" },
|
{ name = "pytest" },
|
||||||
{ name = "ruff" },
|
|
||||||
{ name = "typer" },
|
|
||||||
{ name = "utils" },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "black", specifier = ">=26.5.1" },
|
|
||||||
{ name = "mypy", specifier = ">=2.4.0" },
|
|
||||||
{ name = "openai", specifier = ">=3.22.0" },
|
{ name = "openai", specifier = ">=3.22.0" },
|
||||||
{ name = "pillow", specifier = ">=12.3.0" },
|
|
||||||
{ name = "pydantic", specifier = ">=2.13.5" },
|
{ name = "pydantic", specifier = ">=2.13.5" },
|
||||||
{ name = "pydantic-settings", specifier = ">=2.15.0" },
|
{ name = "pydantic-settings", specifier = ">=2.15.0" },
|
||||||
{ name = "pyexiftool", specifier = ">=0.5.6" },
|
|
||||||
{ name = "pytest", specifier = ">=9.1.1" },
|
{ 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]]
|
[[package]]
|
||||||
@@ -851,15 +215,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "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]]
|
[[package]]
|
||||||
name = "pydantic"
|
name = "pydantic"
|
||||||
version = "2.13.5"
|
version = "2.13.5"
|
||||||
@@ -930,15 +285,6 @@ 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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "pygments"
|
name = "pygments"
|
||||||
version = "2.21.0"
|
version = "2.21.0"
|
||||||
@@ -973,144 +319,6 @@ 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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "sniffio"
|
name = "sniffio"
|
||||||
version = "1.3.1"
|
version = "1.3.1"
|
||||||
@@ -1120,15 +328,6 @@ 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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "truststore"
|
name = "truststore"
|
||||||
version = "0.10.4"
|
version = "0.10.4"
|
||||||
@@ -1138,41 +337,6 @@ 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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "typing-extensions"
|
name = "typing-extensions"
|
||||||
version = "4.16.0"
|
version = "4.16.0"
|
||||||
@@ -1193,44 +357,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c
|
|||||||
wheels = [
|
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" },
|
{ 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" },
|
|
||||||
]
|
|
||||||
|
|||||||
Reference in New Issue
Block a user