Photo Tagger
Photo Tagger asks a vision-language model to analyze your photos and writes Lightroom-compatible
metadata. Use it as a command-line tool (photo-tagger) or as an optional desktop app
(photo-tagger gui). Both run the same pipeline and read the same configuration.
By default, Photo Tagger keeps your originals untouched by creating XMP sidecars. You can embed
updates directly into each photo with --embed-in-photo. You can also split sidecar and embedded
output by file type with --sidecar-mode raw.
Full documentation: https://jbsilva.github.io/photo-tagger/
Highlights
- Works with RAW and standard image formats (CR3, CR2, NEF, JPG, PNG, and more)
- Generates a title, a concise description, and hierarchical keywords, in any language you ask for
(
--output-language) - Merges with existing keywords unless you opt-in to overwrite, and decides per field (title, description, keywords) what a photo keeps and what gets replaced.
- Snaps keywords onto your own keyword list, so a run cannot fill your catalog with near-duplicate terms
- Builds that keyword list for you from the photos you already have (
photo-tagger vocabulary) - Harmonizes each shoot: every frame of the same subject gets the same keyword and the same hierarchy, instead of "Osprey" here and "Ospreys" there
- Writes the fields no model can guess: your credit and copyright notice, dated by the year of each
photo (
--artist,--copyright) - Writes any other tag you name too (
--set) - Prints the metadata a photo already carries, tag by tag (
photo-tagger show) - Works with Ollama, LM Studio, llama.cpp, and any hosted OpenAI-compatible API
- Ships a
doctorcommand that pre-flights ExifTool and your model provider - Records what each run writes, so
photo-tagger undoputs a bad batch back - Watches a folder and tags photos as they arrive (
photo-tagger watch) - Optional desktop GUI (
photo-tagger gui) for a point-and-click workflow, with the same vocabulary, shoot-harmonization, watch, and undo features - The GUI is translated into your own language (English and Brazilian Portuguese ship today)
- Converts images to compact JPEG bytes to minimize token usage
- Reports what it did: a JSON run summary, NDJSON on stdout, or a per-photo CSV (
--csv-file) - Generates detailed log files for easy debugging and auditing
- Highly configurable via CLI flags and environment variables
Requirements
- Python 3.14+
- ExifTool available on
PATH, or its path in theexiftool_pathkey or thePHOTO_TAGGER_EXIFTOOLvariable - A vision-language model (for example Qwen-VL). It can run in Ollama, LM Studio, or llama.cpp on your machine, on your own server, or at any OpenAI-compatible service, free or paid.
librawsupport forrawpy. The rawpy wheels for Linux, Windows, and Apple silicon include it. An Intel Mac may needbrew install libraw
Installation
For end-users, the recommended installation method is via uv:
uv tool install photo-tagger
To include the optional desktop GUI, install the gui extra:
uv tool install 'photo-tagger[gui]'
Or install from conda-forge (works with conda, mamba, or pixi). The GUI is a separate package that bundles PySide6:
conda install -c conda-forge photo-tagger # CLI only
conda install -c conda-forge photo-tagger-gui # CLI + desktop GUI
For development (tests, linting):
uv sync --group dev --group test
Configuration
Environment variables provide defaults so you can keep the CLI concise:
OLLAMA_BASE_URL: override the Ollama HTTP endpoint (defaulthttp://localhost:11434/v1)OLLAMA_API_KEY: optional API key passed to Ollama requestsLM_STUDIO_BASE_URL: override the LM Studio endpoint (defaulthttp://localhost:1234/v1)LM_STUDIO_API_KEY: API key for LM Studio's OpenAI-compatible server.OPENAI_API_KEYis never sent to itLLAMA_CPP_BASE_URL: override the llama.cppllama-serverendpoint (defaulthttp://localhost:8080/v1)LLAMA_CPP_API_KEY: API key for llama.cpp. Set this only for allama-serverthat runs with--api-keyOPENAI_BASE_URL: endpoint for theopenaiprovider (defaulthttps://api.openai.com/v1)OPENAI_API_KEY: API key for theopenaiprovider (required for that provider)MODEL_NAME: default model name (defaultqwen/qwen3-vl-30b)JPEG_DIMENSIONS,JPEG_QUALITY,TEMPERATURE,MAX_TOKENS,RETRIES,TIMEOUT_SECONDS,FREQUENCY_PENALTY: fine-tune runtimePHOTO_TAGGER_EXIFTOOL: path to the ExifTool binary, for installs not onPATH(overrides the configuration file'sexiftool_path)PHOTO_TAGGER_LANG: language of the desktop app's own interface (en,pt_BR, orauto). The language of the generated metadata is a separate configuration,--output-language
Any CLI flag takes precedence over the environment.
Config file
You can persist CLI defaults in a TOML file so they apply automatically. Search order:
$PHOTO_TAGGER_CONFIGenvironment variable (explicit path).photo-tagger.tomlin the current working directory (project-local)~/.config/photo-tagger/config.toml(user-wide)
CLI flags override configuration file values, and the configuration file overrides built-in
defaults. If $PHOTO_TAGGER_CONFIG names a file that does not exist, photo-tagger logs a warning
and reads no configuration file at all.
Example .photo-tagger.toml:
extensions = "cr3,jpg,dng"
recursive = true
workers = 2
[provider]
model_name = "qwen/qwen3-vl-30b"
provider_name = "lmstudio"
api_base_url = "http://localhost:1234/v1"
[inference]
temperature = 0.2
max_tokens = 32768
[output]
preserve_keywords = true
max_keywords = 15
[artifacts]
cache_file = ".photo-tagger-cache.db"
[telemetry]
enabled = true
The section names match the internal option groups: provider, inference, output, copyright,
log, display, filter, artifacts, telemetry, and pacing. The gui section holds the
settings of the desktop window. Top-level keys cover extensions, recursive, workers,
exiftool_path, and language (the language of the desktop window). If your ExifTool binary lives
somewhere unusual, set exiftool_path to its path. Unknown keys are silently ignored, so the file
stays forward-compatible.
Usage
The CLI is exposed as photo-tagger once installed, or you can invoke it directly:
photo-tagger -i ./photos --ext cr3,jpg -r
Key options:
-i/--input PATH: repeatable. Mix files and directories--ext: comma-separated extension list for scanning directories (defaultcr3,jpg)-r/--recursive: recurse into subdirectories while scanning inputs-m/--model: model identifier understood by your provider--provider:ollama,lmstudio,llamacpp, oropenai(defaults tolmstudio)--url/--api-key: override provider endpoint and credentials--output-language/--lang: language of the generated title, description, and keywords (any language name the model understands, for exampleGermanor"Brazilian Portuguese")--hint: a note about every photo in the run that the model must trust over its own reading of the image, for example"The animal in these photos is a deer"--overwrite-keywords: replace instead of merge existing keyword metadata--preserve-title/--preserve-description: keep the title or description a photo already has, and write the generated one only where there is none. Both are replaced by default, which is what clears a camera-written placeholder--no-write-title/--no-write-description/--no-write-keywords: skip writing those fields--no-backup-xmp: avoid creating*_originalsnapshot before writing--embed-in-photo: write metadata directly into the image instead of creating an XMP sidecar--sidecar-mode {all,none,raw,both}: where metadata goesall(the default): a sidecar for every photonone(the same as--embed-in-photo): metadata inside every photoraw: a sidecar for RAW files, embedded metadata for JPEG and friendsboth: each photo and a sidecar beside it
--sidecar-override EXT=TARGET: send one file type elsewhere, whatever the mode. For example,dng=photowrites into each DNG, which is where Lightroom Classic keeps the metadata of a DNG--dry-run: run the model and log the proposed metadata without writing any metadata-w/--workers N: process N photos concurrently using a thread pool (default 1)--no-progress: hide the live rich progress bar (auto-disabled when stderr is not a terminal)--max-keywords N: cap how many AI-generated keywords are kept per photo before merging--vocabulary PATH: restrict generated keywords to the terms in a Lightroom keyword-list export. Use either the.txtor the.csvshape of Metadata > Export Keywords, or a plain list of terms andAnimal|Bird|Ospreypaths. Matching ignores case and punctuation, and every match is rewritten to the file's own spelling and hierarchy. It works in any language: write the file in the language you generate in (see--output-language), and use the{synonym}syntax for inflected forms. Add--vocabulary-strictto drop keywords the file does not cover. The run summary then lists every dropped term and how often it came up--session-gap MINUTES: group photos into shoots separated by this much idle time (capture time, with mtime as a fallback). Each shoot's keywords then agree with itself: the spelling and hierarchy most of the session used win for all of it. Nothing in a session is written until photo-tagger analyzes every photo in it--prompt-file PATH: override the default user prompt with the contents ofPATH--summary-file PATH: write a JSON run summary (token usage, success/failure counts) toPATHon completion--csv-file PATH: write a spreadsheet-friendly report with one row per photo. Each row holds what was generated, the keywords already on the file, the camera and location EXIF read as context, and per-photo tokens and timing. Rows stream as photos finish, so a stopped run still leaves a valid file--cache-file PATH: persistent SQLite cache of model outputs, keyed by an image-data hash and by the model, the prompt, and the configuration used. The image-data hash comes from ExifTool'sImageDataHash, which ignores metadata, so it survives--embed-in-photo. If nothing relevant changes, a rerun skips the model call--lock-file PATH: acquire an exclusive file lock onPATHbefore running. If anotherphoto-taggeralready holds it, the run refuses to start (this prevents two runs racing on the same folder). It works on Linux, macOS, and Windows--json: emit one NDJSON line per processed photo to stdout (file, status, title, description, keywords, token usage, cache flag). Logs and progress stay on stderr, so you can pipe straight intojqor your own tools--notify: raise a desktop notification at the end of the batch, so you can start a long run and walk away. It carries the tally (38 of 42 photos tagged, 4 failed.), and it fires whether the run fully succeeded or only partly did. Underwatch, every batch gets one notification. Add--notify-soundfor an audible alert too. It uses whatever notifier your OS already ships (osascript,notify-send, a PowerShell toast). There is nothing to install, and nothing breaks on a system without one--newer-than DATE/--older-than DATE: filter the input batch by file mtime. It accepts ISO 8601 dates like2024-01-01or2024-01-01T14:30. Naive timestamps use local time--no-undo-log: do not record this run's writes (recording is on by default, and is whatphoto-tagger undoputs back)--no-telemetry: turn off anonymous usage telemetry for this run (see Telemetry)--jpeg-dimensions,--jpeg-quality,--temperature,--max-tokens,--retries,--timeout-seconds,--frequency-penalty: control inference behavior
Skipping and resuming
Three flags work together so you can re-run on a folder without redoing finished work:
--skip-from FILE: skip the photos listed inFILE, one per line, with#marking a comment. A line is a bare filename or a full path, and the match ignores case.--append-to-skip-file FILE: append the full path of each successfully tagged photo toFILEas the run progresses. IfFILEis missing, photo-tagger creates it, so you can pass the same path to both flags from the very first run.--skip-tagged: skip files that already have keywords, a description, or a title in either the image or its XMP sidecar. It catches photos tagged in Lightroom or by hand without needing a skip list at all.
Resume-on-failure pattern: pass the same path to both flags so a killed run can be restarted with a single command.
photo-tagger -i ~/Pictures/Shoot -r \
--skip-from processed.txt \
--append-to-skip-file processed.txt
To process a folder mixing already-tagged and untagged photos:
photo-tagger -i ~/Pictures/Mixed --skip-tagged
A successful run creates or updates an .xmp sidecar for every processed image (unless you embed
the metadata). Existing metadata is merged so Lightroom keeps hierarchical keywords such as
Animal|Bird|Osprey intact.
Copyright and other metadata
Title, description, and keywords come from the model. Your name and your copyright notice do not, and the file is open anyway, so photo-tagger writes them in the same pass:
photo-tagger -i ~/Pictures/2025 \
--artist "Ada Lovelace" \
--copyright "Copyright (c) {year} {artist}, All rights reserved" \
--rights "© {year} {artist}, All rights reserved" \
--marked
The credit goes to EXIF:Artist, XMP-dc:Creator, and IPTC:By-line. The notice goes to
EXIF:Copyright and IPTC:CopyrightNotice, and --rights gives XMP-dc:Rights its own UTF-8
wording. --marked sets XMP-xmpRights:Marked. {year} is the year of the photo, read from its
capture date, so a folder of ten years gets ten different notices. A placeholder can also read any
tag and fall back, as in {EXIF:Artist|XMP-dc:Creator|"Unknown"}. A photo that already has a notice
or a creator keeps it, unless you pass --no-preserve-copyright. --set 'TAG=VALUE' writes any
other ExifTool tag.
To stamp photos that are already tagged, photo-tagger set does the same without calling a model:
photo-tagger set -i ~/Pictures -r --embed-in-photo --artist "Ada Lovelace" --marked
Put the values under [copyright] in the configuration file and every run credits your photos, with
no flags to remember. photo-tagger undo puts back a set run, the same as a tagging run.
To see what a photo already carries, tag by tag:
photo-tagger show -i IMG_0001.CR3
Each line names the ExifTool tag behind the value, and marks a value that lives in the XMP sidecar
rather than in the photo. Add --all for every tag ExifTool reports, or --json to pipe the report
into another tool.
Examples
Process a folder of RAW and JPEG files recursively:
photo-tagger -i ~/Pictures/Portfolio --ext cr3,jpg -r
Tag a few explicit files and overwrite existing keywords:
photo-tagger \
-i IMG_0001.CR3 \
-i IMG_0002.CR3 \
--overwrite-keywords
Embed metadata directly into a set of JPEGs:
photo-tagger -i ./exports --ext jpg --embed-in-photo
Tag a folder of RAW + JPEG pairs, sidecars for the RAWs and embedded metadata for the JPEGs:
photo-tagger -i ~/Pictures/shoot --ext cr3,jpg --sidecar-mode raw
Match Lightroom Classic: a sidecar for proprietary RAW files, embedded metadata in DNG and JPEG:
photo-tagger -i ~/Pictures/shoot --ext cr3,dng,jpg --sidecar-mode raw --sidecar-override dng=photo
Build a keyword list from the photos you already have, then tag against it:
photo-tagger vocabulary -i ~/Pictures -r -o vocabulary.txt --report dropped.csv
photo-tagger -i ~/Pictures/Shoot --vocabulary vocabulary.txt --vocabulary-strict
The keywords are read from the photos themselves with ExifTool, so it works whatever wrote them.
That includes Lightroom, digiKam, darktable, Immich, PhotoPrism, Synology Photos, and anything else
that writes XMP or IPTC. Photos and catalog are left untouched. The output is a text file to review
and edit, and --report names every keyword it dropped and why.
Add --organize to let the model fold synonyms together, and file the keywords under categories.
For example, Golden Light becomes a {synonym} of Golden Hour, so it still matches. It never
decides what to keep, and never invents a keyword: everything it returns is matched back to one your
library already uses.
Tag photos as they land in an import folder, until you stop it with Ctrl-C:
photo-tagger watch -i ~/Pictures/Inbox -r --skip-tagged
Put back what the last run wrote (a bad prompt across 500 photos is one command to revert):
photo-tagger undo # or: photo-tagger undo --list / --dry-run
Make sure that your setup is ready before a big run: doctor pre-flights ExifTool and your model
provider:
photo-tagger doctor --provider lmstudio --model qwen/qwen3-vl-30b
Send requests to a remote Ollama host with a custom model:
photo-tagger -i ./shoot --provider ollama --model llava:34b --url http://ollama-box:11434/v1
Use a hosted OpenAI-compatible API (key read from OPENAI_API_KEY):
photo-tagger -i ./shoot --provider openai --model gpt-4o-mini
Preview proposed metadata without writing anything (useful while you iterate on prompts):
photo-tagger -i ./sample --dry-run
Process a large folder concurrently with a live progress bar and a JSON summary:
photo-tagger -i ~/Pictures/Trip -r --workers 4 --summary-file ~/Pictures/Trip/run.json
Keep generated keywords inside the vocabulary your Lightroom catalog already uses:
photo-tagger -i ./shoot --vocabulary ~/lightroom-keywords.txt --vocabulary-strict
Tag a trip so every frame of the same subject agrees, with shoots grouped an hour apart:
photo-tagger -i ~/Pictures/Trip -r --session-gap 60
Use a custom prompt tuned for wildlife photography:
photo-tagger -i ./shoot --prompt-file prompts/wildlife.txt --max-keywords 12
Cache model outputs so reruns on the same folder skip the inference cost:
photo-tagger -i ~/Pictures/Shoot -r --cache-file ~/.cache/photo-tagger.db
Tag only photos from a specific trip and stream NDJSON for downstream tools:
photo-tagger -i ~/Pictures/Camera -r \
--newer-than 2026-04-01 --older-than 2026-05-01 \
--json --no-progress | jq -c 'select(.status == "ok") | {file, title}'
If another run is already in flight on this folder, refuse to start:
photo-tagger -i ~/Pictures/Camera --lock-file /tmp/photo-tagger.lock
Desktop GUI
Prefer a point-and-click workflow? Install the optional gui extra (or the photo-tagger-gui
conda-forge package) and launch the desktop app:
uv tool install 'photo-tagger[gui]' # or: conda install -c conda-forge photo-tagger-gui
photo-tagger gui
The GUI is a review-before-write frontend over the same building blocks as the CLI. Drag in photos
or folders. Pick what to process from a checkbox tree, with columns for file type, status, and
whether a photo already carries tags. Choose a provider and model: Connection... holds the URL,
the API key, and the same pre-flight as doctor. Then click Generate Selected to run the model
on a background thread with a progress bar. Results are cached, so re-runs are free.
Each photo's proposed title, description, and keywords appear next to the existing values in a side-by-side detail pane. You can edit any field there before you Save. For a batch nobody will sit through, use Save After Generating (on either Generate button's arrow). It writes each run's photos as soon as that run finishes.
A photo that fails turns red and shows why. You can retry it from its right-click menu. Help > Open Logs opens the run log folder.
The GUI reads the same configuration file and environment variables as the CLI, and Settings >
Save Settings as Defaults... writes them back. PySide6 is only pulled in by the gui extra, so
the plain CLI install stays lightweight.
Settings > Copyright... holds your credit and copyright notice, written by every save next to
whatever the model produced. {year} in a notice becomes the year of that photo, so the same notice
fits every shoot. A photo that already has a notice keeps it, unless you uncheck the option. The
panel under the fields shows the tags a save writes, before it writes them. With a copyright set, a
save writes it even with Title, Description, and Keywords all unchecked.
The window covers the same keyword workflow as the CLI. Settings > Keyword Rules... picks the controlled vocabulary that generated keywords snap onto (with a strict toggle), and the session gap that harmonizes each shoot. Both apply to the proposals, so you still review them before saving.
Tools holds the jobs that act on a whole library. Build Vocabulary... writes a keyword file out of the keywords your photos already carry (optionally organized by the model). Watch Folder... picks up and generates photos as they land. Undo Writes... puts back what a recorded run wrote, whether that run came from the window or the command line.
Build a standalone macOS app
For a double-clickable Photo Tagger.app that needs no Python install to run:
./packaging/build_macos_app.sh
This bundles Python, PySide6, and everything else into dist/Photo Tagger.app with PyInstaller (its
source and spec live in packaging/). Two things to know:
- It is unsigned, so the first launch needs a right-click > Open (Gatekeeper asks once). It opens normally after that.
- ExifTool is not bundled: install it however you like (
brew install exiftool, Nix, MacPorts, ...). A Finder launch gives apps a minimalPATH. On startup, the app inherits thePATHthat your login shell sets, so it finds ExifTool wherever your package manager put it. If ExifTool is still missing, Test Connection reports a clear error.
Telemetry
photo-tagger sends small anonymous beacons that show how the tool is used. A beacon goes out at the end of each run and each other command, as the GUI window opens and closes, and on a crash. It is opt-out. It is on by default. You see a notice once on the first run, and you can turn it off at any time. docs/telemetry.md lists every field.
What is collected:
- the app version, the interface (
cliorgui), and the command - the provider and model, and where the model server runs:
localhost, your network, or the name of a known public service, never the URL - the configuration and features you use, as switches, counts, and mode names, never the text or paths they hold
- which parts of your configuration differ from the defaults
- per run: batch size, file types, duration, outcome counts, tokens, and model time
- in the GUI: how long the window is busy, in use, or idle
- in the GUI: how many photos you edit before a save (counts only), and your screen size
- the OS, CPU and GPU model names, core count, RAM, power source and battery level, locale, time zone offset, and install method
- on a crash: the exception type and a stack trace of code locations (module, function, line, and library frames), plus the chain of exceptions that caused it. Also the versions of key libraries, but never the message and never a file path.
- a random install id (a UUID made once, not derived from any hardware identifier)
What is never collected: photos, file paths, and filenames. The generated tags, titles, and
descriptions, your prompts and hints, and server URLs are never sent either. Neither are API keys,
IP addresses, error messages, or anything else that identifies you. The dataclasses in
telemetry_events.py, with the payload builders in
telemetry.py, are the closed list of what leaves the machine.
Where it goes: our own Cloudflare Worker at telemetry.tagger.photo. No third-party analytics
service is involved, and the collector's full source (and the queries run against it) lives in
telemetry/. The Analytics Engine copy of each event expires after three months. The
D1 copy has no expiry date, so trends can be compared across releases. Sending happens on a
background thread and is wrapped so it can never crash a run. At exit, the CLI waits at most 2.5
seconds for each beacon.
How to disable it (any one of these):
- pass
--no-telemetrytotagorwatch(the other commands do not take this flag) - set
PHOTO_TAGGER_TELEMETRY=0(or the cross-toolDO_NOT_TRACK=1) in your environment - put
enabled = falseunder[telemetry]in your configuration file - in the desktop app, clear Settings > Send Anonymous Telemetry. The first-run dialog also offers Turn It Off. The app keeps the choice for the next launch.
An opt-out in the environment or on the command line wins over every opt-in, so exporting
PHOTO_TAGGER_TELEMETRY=0 once disables telemetry everywhere, including the GUI. In the desktop
app, a toggle checked by hand wins over enabled = false in the configuration file, because the app
writes that key itself when it saves your settings. The PHOTO_TAGGER_NO_TELEMETRY=1 of version 0.9
still works too.
DO_NOT_TRACK=1 is the one exception, because it speaks for every tool at once. If you set it but
still want to send photo-tagger's stats, set PHOTO_TAGGER_TELEMETRY=1. It works for the CLI and
the GUI. In the desktop app you can also check Settings > Send Anonymous Telemetry by hand. The
toggle shows unchecked while DO_NOT_TRACK stops the beacons, so what you see is what is sent.
Logging
Logs are written to stderr and to a timestamped file (for example 20260101...-photo_tagger.log).
Adjust levels with --console-log-level and --file-log-level, or disable either by setting the
value to OFF.
Testing
Run the unit tests with:
uv run poe test
The GUI tests are skipped automatically unless the gui extra is installed. To run them headless,
use the test-gui task. It installs the extra and uses Qt's offscreen platform:
uv run poe test-gui
If you plan to contribute, also run uv run poe check before opening a PR. uv run poe lists the
other dev tasks.
Metadata
Release files for photo-tagger 0.10.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| photo_tagger-0.10.0.tar.gz | 476.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| photo_tagger-0.10.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 997.5 kB
Release files / photo_tagger-0.10.0.tar.gz
| Download URL | photo_tagger-0.10.0.tar.gz |
|---|---|
| Size | 476.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f89b62c9d0de475174bd123c3c0536a7e8a59c28ee9e73f0040d99535063d42f
|
|
BLAKE2b-256 checksum How to use checksums |
d00bd280182748acaabdee563c71324950f4da395357fe24bdb0eeed3a5ddd4d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 10, 2026.
Transparency logRelease files / photo_tagger-0.10.0-py3-none-any.whl
| Download URL | photo_tagger-0.10.0-py3-none-any.whl |
|---|---|
| Size | 520.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
24f4e2c6f7ed4d00ad9f3a39b13ee7e2757c1b95dcf0725130a0dea5e5f324f6
|
|
BLAKE2b-256 checksum How to use checksums |
d271087dfa15a9d679eefb8e76c7b28e849b384f024983c6b9f2e85b5f7f9f77
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 10, 2026.
Transparency log