wbaila 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,16 @@
1
+ # Python
2
+ **/__pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .venv/
6
+ build/
7
+ dist/
8
+ .ruff_cache/
9
+ .pytest_cache/
10
+ .coverage*
11
+
12
+ # Editors / OS
13
+ .idea/
14
+ .vscode/
15
+ .DS_Store
16
+ Thumbs.db
wbaila-0.1.0/AGENTS.md ADDED
@@ -0,0 +1,106 @@
1
+ # AGENTS.md — `wbaila` CLI subproject
2
+
3
+ Coding conventions for the Python CLI package in this directory
4
+ (`src/wbaila/`). The repo root's `AGENTS.md` describes the overall
5
+ repository layout. The checks in the last section must pass before you
6
+ consider a change done.
7
+
8
+ ## Scope & layout
9
+
10
+ - This directory is an independent `uv` project: the `wbaila` package,
11
+ published to PyPI and normally invoked as `uvx wbaila <backend> ...`.
12
+ - Python 3.13+. Prefer the standard library; Pillow is allowed for
13
+ image work (and is the only runtime dependency). numpy is a
14
+ **dev-only** dependency: pillow's annotations define `NumpyArray`
15
+ under `TYPE_CHECKING`, so pyright needs numpy importable in the
16
+ analysis environment (`.venv`); it is never needed at runtime.
17
+ - Each identification backend lives in one module under `src/wbaila/`
18
+ (currently `dongniao.py`) and exposes:
19
+ - `register(subparsers)`: argparse subcommand registration (sets the
20
+ `func` default);
21
+ - `run(args)`: the workflow, returning the process exit code.
22
+ New backends must be registered in `__main__.py:build_parser` under a
23
+ subcommand named after the service (e.g. `inaturalist`).
24
+ - Cross-backend helpers (e.g. `fail()`) currently live in
25
+ `dongniao.py`; factor them into a shared module when the second
26
+ backend actually needs them.
27
+
28
+ ## Language conventions
29
+
30
+ - Comments, docstrings, CLI help text, and error messages: **English**.
31
+ - Product/API proper nouns may stay in their original language
32
+ (e.g. 懂鸟/Dongniao in the module docstring).
33
+
34
+ ## Output contract (CLI scripts)
35
+
36
+ - **stdout carries exactly one JSON document** and nothing else:
37
+ `[code, payload]`, mirroring the backend API's own shape. Local and
38
+ HTTP-level failures use the same shape with code `0`:
39
+ `[0, "<english error message>"]`. Never print progress, banners, or
40
+ logs to stdout; scripts should stay pipe-friendly (`jq` etc.).
41
+ - Prefer writing nothing to stderr at all; if a script ever needs
42
+ progress output, it must go to stderr, never stdout.
43
+ - Exit codes: `0` = success; `1` = API-level business error or timeout;
44
+ `2` = local or network error. `run()` returns the exit code; only
45
+ error paths may `sys.exit()` directly (via `fail()`).
46
+ - argparse usage errors are the one exception: argparse prints its own
47
+ message to stderr and exits 2 before any backend code runs.
48
+
49
+ ## Type checking — pyright, strict mode
50
+
51
+ Configured in `pyrightconfig.json` (`typeCheckingMode: "strict"`).
52
+ Run with the system `pyright` binary; imports resolve from `.venv`,
53
+ so run `uv sync` once after cloning or after dependency changes.
54
+
55
+ - Every function needs full parameter **and** return annotations. Use
56
+ precise types (`tuple[bytes, int, tuple[int, int]]`) rather than bare
57
+ `Any`.
58
+ - Functions that always exit or raise (e.g. `fail()`) must be annotated
59
+ `-> NoReturn` so callers get control-flow narrowing (possibly-unbound
60
+ variables after an `except` branch, unreachable returns, etc.).
61
+ - `Any` is acceptable only at the JSON boundary (values coming from
62
+ `json.loads`) and at the argparse `Namespace` boundary; do not let it
63
+ leak into image/HTTP layer signatures.
64
+ - Gotcha: `isinstance(x, list)` narrows an `Any` value to
65
+ `list[Unknown]` under strict mode, which then fails
66
+ `reportUnknownArgumentType` on `len()`/subscripts. Prefer defensive
67
+ unpacking (`code, data = payload` in a `try/except`) or
68
+ `typing.cast("list[Any]", ...)` over chained isinstance checks.
69
+ - Do not silence diagnostics with `# pyright: ignore` unless there is a
70
+ comment explaining why; restructure the code instead when possible.
71
+
72
+ ## Lint & format — ruff
73
+
74
+ Configured in `ruff.toml` (line length 88, `target-version = "py313"`,
75
+ rules `E4/E7/E9/F + UP + ISC`). ruff is not installed locally; run it
76
+ through `uvx ruff`.
77
+
78
+ - Code must be `ruff format`-clean.
79
+ - Typical fixes the ruleset enforces:
80
+ - `UP012`: `.encode()` instead of `.encode("utf-8")` (UTF-8 is the
81
+ default).
82
+ - `ISC004`: parenthesize implicitly concatenated strings when they
83
+ sit inside a collection literal, or split them across lines inside
84
+ existing parentheses.
85
+ - No unused imports/variables (F401/F841); no bare `except:` (E722).
86
+
87
+ ## Checks before finishing a change
88
+
89
+ From this directory:
90
+
91
+ ```bash
92
+ uv sync # keep .venv / uv.lock in sync
93
+ pyright # 0 errors (strict)
94
+ uvx ruff check . # all checks passed
95
+ uvx ruff format --check . # no diff
96
+ ```
97
+
98
+ Plus a smoke test of the failure paths (does not consume API quota):
99
+
100
+ ```bash
101
+ uv run wbaila dongniao /nonexistent.jpg
102
+ # -> [0, "image file not found: ..."] on stdout, exit code 2
103
+ ```
104
+
105
+ When touching `pyproject.toml`, also run `uv build` to catch packaging
106
+ problems early.
wbaila-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,100 @@
1
+ Metadata-Version: 2.5
2
+ Name: wbaila
3
+ Version: 0.1.0
4
+ Summary: WBAILA — What Bird Am I Looking At? A WAILA-style bird and wildlife ID CLI.
5
+ Project-URL: Homepage, https://github.com/hurui200320/wbaila
6
+ Project-URL: Repository, https://github.com/hurui200320/wbaila
7
+ Project-URL: Issues, https://github.com/hurui200320/wbaila/issues
8
+ Author: hurui200320
9
+ License-Expression: AGPL-3.0-only
10
+ Keywords: bioacoustics,bird,bird-identification,birding,cli,dongniao,ornithology,wildlife
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Scientific/Engineering
18
+ Classifier: Topic :: Utilities
19
+ Requires-Python: >=3.13
20
+ Requires-Dist: pillow>=11
21
+ Description-Content-Type: text/markdown
22
+
23
+ # wbaila
24
+
25
+ **W**hat **B**ird **A**m **I** **L**ooking **A**t? — a WAILA-style
26
+ "what am I looking at" command-line tool for birds and other wildlife.
27
+
28
+ `wbaila` dispatches to pluggable identification backends, one
29
+ subcommand each. The first backend is `dongniao`: an unofficial client
30
+ for the Dongniao (懂鸟) animal recognition API (photo identification of
31
+ birds, mammals, amphibians and reptiles) on the HHOLOVE AI Open
32
+ Platform.
33
+
34
+ ## Install & run
35
+
36
+ With [uv](https://docs.astral.sh/uv/) installed, no project setup is
37
+ needed:
38
+
39
+ ```bash
40
+ uvx wbaila dongniao photo.jpg
41
+ ```
42
+
43
+ Or install it as a persistent tool:
44
+
45
+ ```bash
46
+ uv tool install wbaila
47
+ wbaila dongniao photo.jpg
48
+ ```
49
+
50
+ Python 3.13+ is required; uv fetches a matching interpreter
51
+ automatically if missing. (pipx or a plain `pip install wbaila` work
52
+ too.)
53
+
54
+ ## Authentication
55
+
56
+ ```bash
57
+ # Direct, with your own API key:
58
+ HHODATA_API_KEY=xxxx uvx wbaila dongniao photo.jpg
59
+
60
+ # Or through a relay that injects the key:
61
+ HHODATA_API_BASE=https://my-relay.example/relay/hhodata \
62
+ uvx wbaila dongniao photo.jpg
63
+ ```
64
+
65
+ - `HHODATA_API_KEY` — API key from the HHOLOVE AI Open Platform; the
66
+ header is skipped when unset or empty.
67
+ - `HHODATA_API_BASE` — API base URL; defaults to
68
+ `https://ai.open.hhodata.com/api/v2`.
69
+
70
+ ## Options
71
+
72
+ ```text
73
+ image path of the image to recognize
74
+ --class BM animal classes: B(bird)/M(mammal)/A(amphibian)/
75
+ R(reptile), any combination (default: BM)
76
+ --area CODE area code to filter results (optional)
77
+ --did ID unique device/client ID, alphanumeric (optional)
78
+ --interval SECONDS polling interval (default: 2)
79
+ --max-retries N maximum number of polls (default: 30)
80
+ ```
81
+
82
+ ## Output & exit codes
83
+
84
+ stdout always carries exactly one JSON document in the API's own
85
+ shape, `[code, payload]` (code `1000` = success, `0` = local/HTTP
86
+ failure with an English message) — pipe it into `jq` freely.
87
+
88
+ Exit codes: `0` success · `1` API-level business error or polling
89
+ timeout · `2` local or network error.
90
+
91
+ ## Disclaimer
92
+
93
+ Unofficial project, not affiliated with or endorsed by 懂鸟
94
+ (Aboutbirds), HHOLOVE, or 两氢一氧(杭州)数字科技有限公司; all related
95
+ trademarks belong to their respective owners. Register your own API
96
+ key on the [HHOLOVE AI Open Platform](https://ai.open.hhodata.com/).
97
+
98
+ ## License
99
+
100
+ AGPL-3.0 (see the repository's LICENSE file).
wbaila-0.1.0/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # wbaila
2
+
3
+ **W**hat **B**ird **A**m **I** **L**ooking **A**t? — a WAILA-style
4
+ "what am I looking at" command-line tool for birds and other wildlife.
5
+
6
+ `wbaila` dispatches to pluggable identification backends, one
7
+ subcommand each. The first backend is `dongniao`: an unofficial client
8
+ for the Dongniao (懂鸟) animal recognition API (photo identification of
9
+ birds, mammals, amphibians and reptiles) on the HHOLOVE AI Open
10
+ Platform.
11
+
12
+ ## Install & run
13
+
14
+ With [uv](https://docs.astral.sh/uv/) installed, no project setup is
15
+ needed:
16
+
17
+ ```bash
18
+ uvx wbaila dongniao photo.jpg
19
+ ```
20
+
21
+ Or install it as a persistent tool:
22
+
23
+ ```bash
24
+ uv tool install wbaila
25
+ wbaila dongniao photo.jpg
26
+ ```
27
+
28
+ Python 3.13+ is required; uv fetches a matching interpreter
29
+ automatically if missing. (pipx or a plain `pip install wbaila` work
30
+ too.)
31
+
32
+ ## Authentication
33
+
34
+ ```bash
35
+ # Direct, with your own API key:
36
+ HHODATA_API_KEY=xxxx uvx wbaila dongniao photo.jpg
37
+
38
+ # Or through a relay that injects the key:
39
+ HHODATA_API_BASE=https://my-relay.example/relay/hhodata \
40
+ uvx wbaila dongniao photo.jpg
41
+ ```
42
+
43
+ - `HHODATA_API_KEY` — API key from the HHOLOVE AI Open Platform; the
44
+ header is skipped when unset or empty.
45
+ - `HHODATA_API_BASE` — API base URL; defaults to
46
+ `https://ai.open.hhodata.com/api/v2`.
47
+
48
+ ## Options
49
+
50
+ ```text
51
+ image path of the image to recognize
52
+ --class BM animal classes: B(bird)/M(mammal)/A(amphibian)/
53
+ R(reptile), any combination (default: BM)
54
+ --area CODE area code to filter results (optional)
55
+ --did ID unique device/client ID, alphanumeric (optional)
56
+ --interval SECONDS polling interval (default: 2)
57
+ --max-retries N maximum number of polls (default: 30)
58
+ ```
59
+
60
+ ## Output & exit codes
61
+
62
+ stdout always carries exactly one JSON document in the API's own
63
+ shape, `[code, payload]` (code `1000` = success, `0` = local/HTTP
64
+ failure with an English message) — pipe it into `jq` freely.
65
+
66
+ Exit codes: `0` success · `1` API-level business error or polling
67
+ timeout · `2` local or network error.
68
+
69
+ ## Disclaimer
70
+
71
+ Unofficial project, not affiliated with or endorsed by 懂鸟
72
+ (Aboutbirds), HHOLOVE, or 两氢一氧(杭州)数字科技有限公司; all related
73
+ trademarks belong to their respective owners. Register your own API
74
+ key on the [HHOLOVE AI Open Platform](https://ai.open.hhodata.com/).
75
+
76
+ ## License
77
+
78
+ AGPL-3.0 (see the repository's LICENSE file).
@@ -0,0 +1,50 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "wbaila"
7
+ version = "0.1.0"
8
+ description = "WBAILA — What Bird Am I Looking At? A WAILA-style bird and wildlife ID CLI."
9
+ readme = "README.md"
10
+ requires-python = ">=3.13"
11
+ license = "AGPL-3.0-only"
12
+ authors = [{ name = "hurui200320" }]
13
+ keywords = [
14
+ "bird",
15
+ "birding",
16
+ "bird-identification",
17
+ "ornithology",
18
+ "wildlife",
19
+ "bioacoustics",
20
+ "dongniao",
21
+ "cli",
22
+ ]
23
+ classifiers = [
24
+ "Development Status :: 4 - Beta",
25
+ "Environment :: Console",
26
+ "Intended Audience :: Science/Research",
27
+ "Operating System :: OS Independent",
28
+ "Programming Language :: Python :: 3",
29
+ "Programming Language :: Python :: 3.13",
30
+ "Topic :: Scientific/Engineering",
31
+ "Topic :: Utilities",
32
+ ]
33
+ dependencies = ["pillow>=11"]
34
+
35
+ [dependency-groups]
36
+ # Dev-only: pillow's type annotations reference NumpyArray (defined
37
+ # under TYPE_CHECKING in PIL._typing), so pyright needs numpy to be
38
+ # importable in the analysis environment. Not needed at runtime.
39
+ dev = ["numpy>=2"]
40
+
41
+ [project.urls]
42
+ Homepage = "https://github.com/hurui200320/wbaila"
43
+ Repository = "https://github.com/hurui200320/wbaila"
44
+ Issues = "https://github.com/hurui200320/wbaila/issues"
45
+
46
+ [project.scripts]
47
+ wbaila = "wbaila.__main__:main"
48
+
49
+ [tool.hatch.build.targets.wheel]
50
+ packages = ["src/wbaila"]
@@ -0,0 +1,7 @@
1
+ {
2
+ "pythonVersion": "3.13",
3
+ "typeCheckingMode": "strict",
4
+ "include": ["src"],
5
+ "venvPath": ".",
6
+ "venv": ".venv"
7
+ }
wbaila-0.1.0/ruff.toml ADDED
@@ -0,0 +1,8 @@
1
+ # Ruff lint + format configuration (matches the VS Code setup).
2
+ line-length = 88
3
+ target-version = "py313"
4
+
5
+ [lint]
6
+ # Extend the default rule set (E4/E7/E9/F) with pyupgrade (UP) and
7
+ # implicit string concatenation (ISC) checks.
8
+ select = ["E4", "E7", "E9", "F", "UP", "ISC"]
@@ -0,0 +1,7 @@
1
+ """WBAILA — What Bird Am I Looking At?
2
+
3
+ A WAILA-style "what am I looking at" CLI for birds and other wildlife.
4
+ The `dongniao` backend calls the Dongniao (懂鸟) animal recognition API
5
+ on the HHOLOVE AI Open Platform; further identification backends (e.g.
6
+ reference lookups on iNaturalist) may join.
7
+ """
@@ -0,0 +1,38 @@
1
+ """WBAILA command-line entry point.
2
+
3
+ Dispatches identification backends as subcommands. Every backend prints
4
+ exactly one JSON document to stdout — the backend API's own
5
+ [code, payload] shape — and exits 0/1/2; see cli/AGENTS.md.
6
+ """
7
+
8
+ import argparse
9
+ import json
10
+ import sys
11
+
12
+ from wbaila import dongniao
13
+
14
+
15
+ def build_parser() -> argparse.ArgumentParser:
16
+ """Build the top-level wbaila parser with one subcommand per backend."""
17
+ parser = argparse.ArgumentParser(
18
+ prog="wbaila",
19
+ description="WBAILA — What Bird Am I Looking At? Identify the bird "
20
+ "or other animal you are looking at.",
21
+ )
22
+ subparsers = parser.add_subparsers(dest="backend", required=True, metavar="backend")
23
+ dongniao.register(subparsers)
24
+ return parser
25
+
26
+
27
+ def main() -> int:
28
+ """Console-script entry point; returns the process exit code."""
29
+ try:
30
+ args = build_parser().parse_args()
31
+ return args.func(args)
32
+ except KeyboardInterrupt:
33
+ print(json.dumps([0, "interrupted"], ensure_ascii=False, indent=2))
34
+ return 130
35
+
36
+
37
+ if __name__ == "__main__":
38
+ sys.exit(main())
@@ -0,0 +1,349 @@
1
+ """Dongniao (懂鸟) animal recognition backend for WBAILA.
2
+
3
+ Pipeline: preprocess the image locally (EXIF orientation fix, JPEG
4
+ conversion, optional resizing to meet the API limits of 11~8192 px per
5
+ edge and <= 2MB) -> submit the recognition task -> poll for the result
6
+ -> print exactly one JSON document to stdout.
7
+
8
+ stdout always carries a single JSON array in the API's own shape:
9
+
10
+ [code, payload]
11
+
12
+ where `code` is the API business code (1000 = success), or 0 for local /
13
+ HTTP-level failures with an English error message as the payload. This
14
+ keeps the output safe to pipe into tools like jq.
15
+
16
+ Dependencies: Python 3.13 standard library + Pillow (declared in
17
+ pyproject.toml).
18
+
19
+ Environment variables:
20
+ HHODATA_API_KEY API key; the header is skipped when unset or empty
21
+ (e.g. when going through a relay injecting the key)
22
+ HHODATA_API_BASE API base URL; defaults to
23
+ https://ai.open.hhodata.com/api/v2
24
+
25
+ Exit codes: 0 = got a recognition result (code 1000); 1 = API-level
26
+ business error or polling timeout; 2 = local or network error.
27
+
28
+ Examples:
29
+ # Through a bifrost relay (no api key needed)
30
+ HHODATA_API_BASE=http://10.233.1.8:8002/relay/hhodata \\
31
+ uvx wbaila dongniao ~/Pictures/DSC_3006_DxO.png --class BM
32
+
33
+ # Direct against the official API (api key required)
34
+ HHODATA_API_KEY=xxxx uvx wbaila dongniao photo.jpg
35
+
36
+ Code style: see cli/AGENTS.md (pyright strict, ruff check + ruff format).
37
+ """
38
+
39
+ import argparse
40
+ import io
41
+ import json
42
+ import os
43
+ import sys
44
+ import time
45
+ import urllib.error
46
+ import urllib.parse
47
+ import urllib.request
48
+ import uuid
49
+ from typing import Any, NoReturn
50
+
51
+ DEFAULT_API_BASE = "https://ai.open.hhodata.com/api/v2"
52
+ ENV_API_KEY = "HHODATA_API_KEY"
53
+ ENV_API_BASE = "HHODATA_API_BASE"
54
+
55
+ MIN_EDGE = 11 # API requirement: minimum pixels per edge
56
+ MAX_EDGE = 8192 # API requirement: maximum pixels per edge
57
+ MAX_BYTES = 2_000_000 # API limit is 2MB; keep a small margin
58
+ JPEG_QUALITIES = (90, 85, 80, 75, 70, 60, 50, 40, 30)
59
+ SHRINK_FACTOR = 0.8 # edge scale-down factor when still over 2MB
60
+ HTTP_TIMEOUT = 60 # per-request timeout in seconds
61
+
62
+ CODE_OK = 1000 # task accepted / result ready
63
+ CODE_PENDING = 1001 # upload: image over 2MB; poll: result not ready yet
64
+
65
+
66
+ def fail(message: str, exit_code: int = 2) -> NoReturn:
67
+ """Print the error form of the final JSON to stdout and exit.
68
+
69
+ All local/HTTP-level failures are reported as [0, "<message>"] so
70
+ that stdout stays a single, consistently shaped JSON document.
71
+ """
72
+ print(json.dumps([0, message], ensure_ascii=False, indent=2))
73
+ sys.exit(exit_code)
74
+
75
+
76
+ try:
77
+ from PIL import Image, ImageOps
78
+ except ImportError:
79
+ fail(
80
+ "Pillow is missing; install the distro package python3-pil / "
81
+ "python3-pillow (or: pip install pillow)"
82
+ )
83
+
84
+ # ---------------------------------------------------------------- image prep
85
+
86
+
87
+ def load_rgb_image(path: str) -> Image.Image:
88
+ """Open the image, fix EXIF orientation, convert to RGB.
89
+
90
+ Returns an image object that no longer depends on the file handle.
91
+ """
92
+ try:
93
+ src = Image.open(path)
94
+ except FileNotFoundError:
95
+ fail(f"image file not found: {path}")
96
+ except OSError as exc:
97
+ fail(f"cannot open image {path}: {exc}")
98
+ with src:
99
+ im = ImageOps.exif_transpose(src)
100
+ if im.mode != "RGB":
101
+ im = im.convert("RGB")
102
+ if im is src:
103
+ # exif_transpose may return the original object when nothing
104
+ # needs transposing; copy to detach from the file handle
105
+ im = src.copy()
106
+ return im
107
+
108
+
109
+ def fit_edges(im: Image.Image) -> Image.Image:
110
+ """Clamp every edge into the 11~8192 px range (aspect ratio kept).
111
+
112
+ Oversized images are scaled down so the longest edge is 8192 px;
113
+ undersized ones are scaled up so the shortest edge is 11 px.
114
+ """
115
+ width, height = im.size
116
+ longest, shortest = max(width, height), min(width, height)
117
+ if longest > MAX_EDGE and shortest < MIN_EDGE:
118
+ fail(
119
+ f"image {width}x{height} cannot fit the {MIN_EDGE}~{MAX_EDGE} px "
120
+ "requirement"
121
+ )
122
+ if longest > MAX_EDGE:
123
+ factor = MAX_EDGE / longest
124
+ elif shortest < MIN_EDGE:
125
+ factor = MIN_EDGE / shortest
126
+ else:
127
+ return im
128
+ new_size = (round(width * factor), round(height * factor))
129
+ if min(new_size) < MIN_EDGE or max(new_size) > MAX_EDGE:
130
+ fail(
131
+ f"extreme aspect ratio ({width}x{height}) cannot fit the "
132
+ f"{MIN_EDGE}~{MAX_EDGE} px requirement"
133
+ )
134
+ return im.resize(new_size, Image.Resampling.LANCZOS)
135
+
136
+
137
+ def encode_jpeg(im: Image.Image) -> tuple[bytes, int, tuple[int, int]]:
138
+ """Encode as JPEG under 2MB: step down quality first, then shrink."""
139
+ while True:
140
+ for quality in JPEG_QUALITIES:
141
+ buffer = io.BytesIO()
142
+ im.save(buffer, "JPEG", quality=quality, optimize=True)
143
+ data = buffer.getvalue()
144
+ if len(data) <= MAX_BYTES:
145
+ return data, quality, im.size
146
+ width, height = im.size
147
+ new_size = (round(width * SHRINK_FACTOR), round(height * SHRINK_FACTOR))
148
+ if min(new_size) < MIN_EDGE:
149
+ fail(
150
+ "cannot get the image under 2MB even at the minimum size; "
151
+ "please shrink it manually"
152
+ )
153
+ im = im.resize(new_size, Image.Resampling.LANCZOS)
154
+
155
+
156
+ def prepare_image(path: str) -> tuple[bytes, int, tuple[int, int]]:
157
+ """Return (jpeg_bytes, quality, (width, height)) ready for upload."""
158
+ return encode_jpeg(fit_edges(load_rgb_image(path)))
159
+
160
+
161
+ # ---------------------------------------------------------------- HTTP layer
162
+
163
+
164
+ def build_multipart(
165
+ fields: dict[str, str], file_field: tuple[str, str, bytes, str]
166
+ ) -> tuple[bytes, str]:
167
+ """Build a multipart/form-data body.
168
+
169
+ file_field is a tuple of (field name, filename, data, MIME type).
170
+ """
171
+ boundary = "dongniao-" + uuid.uuid4().hex
172
+ name, filename, data, content_type = file_field
173
+ chunks: list[bytes] = []
174
+ for key, value in fields.items():
175
+ chunks.append(
176
+ (
177
+ f"--{boundary}\r\n"
178
+ f'Content-Disposition: form-data; name="{key}"\r\n\r\n'
179
+ f"{value}\r\n"
180
+ ).encode()
181
+ )
182
+ chunks.append(
183
+ (
184
+ f"--{boundary}\r\n"
185
+ f'Content-Disposition: form-data; name="{name}"; '
186
+ f'filename="{filename}"\r\n'
187
+ f"Content-Type: {content_type}\r\n\r\n"
188
+ ).encode()
189
+ + data
190
+ + b"\r\n"
191
+ )
192
+ chunks.append(f"--{boundary}--\r\n".encode())
193
+ return b"".join(chunks), f"multipart/form-data; boundary={boundary}"
194
+
195
+
196
+ def parse_json_body(raw: bytes, http_status: int) -> Any:
197
+ text = raw.decode("utf-8", errors="replace")
198
+ try:
199
+ return json.loads(text)
200
+ except json.JSONDecodeError:
201
+ fail(f"server returned a non-JSON response (HTTP {http_status}): {text[:300]}")
202
+
203
+
204
+ def post_form(
205
+ url: str, headers: dict[str, str], body: bytes, content_type: str
206
+ ) -> tuple[int, Any]:
207
+ request = urllib.request.Request(url, data=body, method="POST")
208
+ request.add_header("Content-Type", content_type)
209
+ for key, value in headers.items():
210
+ request.add_header(key, value)
211
+ try:
212
+ with urllib.request.urlopen(request, timeout=HTTP_TIMEOUT) as response:
213
+ return response.status, parse_json_body(response.read(), response.status)
214
+ except urllib.error.HTTPError as exc:
215
+ # the API also answers 4xx/5xx with a [code, message] JSON body
216
+ return exc.code, parse_json_body(exc.read(), exc.code)
217
+ except urllib.error.URLError as exc:
218
+ fail(f"cannot connect to {url}: {exc.reason}")
219
+ except TimeoutError:
220
+ fail(f"request to {url} timed out after {HTTP_TIMEOUT}s")
221
+
222
+
223
+ def expect_pair(payload: Any, http_status: int, action: str) -> tuple[int, Any]:
224
+ """Check that the response matches the API's [code, data] shape."""
225
+ # Unpack defensively instead of isinstance() narrowing: payload is Any,
226
+ # and isinstance() would narrow it to list[Unknown] under strict mode.
227
+ # Any 2-element iterable unpacks; lists/dicts/strings that don't match
228
+ # the [int, ...] shape end up in the failure branch below.
229
+ code: Any = None
230
+ data: Any = None
231
+ try:
232
+ code, data = payload
233
+ valid = isinstance(code, int)
234
+ except (TypeError, ValueError):
235
+ valid = False
236
+ if not valid:
237
+ fail(
238
+ f"{action}: unexpected response format (HTTP {http_status}): "
239
+ f"{json.dumps(payload, ensure_ascii=False)[:300]}"
240
+ )
241
+ return code, data
242
+
243
+
244
+ # ---------------------------------------------------------------- workflow
245
+
246
+
247
+ def upload_image(
248
+ url: str, headers: dict[str, str], jpeg_bytes: bytes, args: argparse.Namespace
249
+ ) -> tuple[int, Any]:
250
+ """Submit the recognition task; returns the [code, data] pair."""
251
+ fields: dict[str, str] = {"upload": "1", "class": args.animal_class}
252
+ if args.area:
253
+ fields["area"] = args.area
254
+ if args.did:
255
+ fields["did"] = args.did
256
+ body, content_type = build_multipart(
257
+ fields, ("image", "image.jpg", jpeg_bytes, "image/jpeg")
258
+ )
259
+ http_status, payload = post_form(url, headers, body, content_type)
260
+ return expect_pair(payload, http_status, "task submission")
261
+
262
+
263
+ def poll_result(
264
+ url: str,
265
+ headers: dict[str, str],
266
+ resultid: str,
267
+ interval: float,
268
+ max_retries: int,
269
+ ) -> tuple[int, Any]:
270
+ """Poll for the recognition result; returns the terminal [code, data]."""
271
+ body = urllib.parse.urlencode({"resultid": resultid}).encode()
272
+ for _ in range(max_retries):
273
+ http_status, payload = post_form(
274
+ url, headers, body, "application/x-www-form-urlencoded"
275
+ )
276
+ code, data = expect_pair(payload, http_status, "result polling")
277
+ if code == CODE_PENDING:
278
+ time.sleep(interval)
279
+ continue
280
+ return code, data # 1000 = result ready, anything else = terminal
281
+ fail(f"no result after {max_retries} polls; giving up", exit_code=1)
282
+
283
+
284
+ # ---------------------------------------------------------------- CLI wiring
285
+
286
+
287
+ def register(
288
+ # typeshed exposes no public alias for the subparsers action type
289
+ subparsers: argparse._SubParsersAction[ # pyright: ignore[reportPrivateUsage]
290
+ argparse.ArgumentParser
291
+ ],
292
+ ) -> None:
293
+ """Register the `dongniao` subcommand on the wbaila CLI parser."""
294
+ parser = subparsers.add_parser(
295
+ "dongniao",
296
+ help="identify animals with the Dongniao (懂鸟) recognition API",
297
+ description="Dongniao (懂鸟) animal recognition API client: preprocess "
298
+ "the image, submit it, poll and print the final JSON.",
299
+ )
300
+ parser.add_argument("image", help="path of the image to recognize")
301
+ parser.add_argument(
302
+ "--class",
303
+ dest="animal_class",
304
+ default="BM",
305
+ help="animal classes, a combination of B(bird)/M(mammal)/"
306
+ "A(amphibian)/R(reptile) (default: BM)",
307
+ )
308
+ parser.add_argument(
309
+ "--area", default="", help="area code to filter results (optional)"
310
+ )
311
+ parser.add_argument(
312
+ "--did",
313
+ default="",
314
+ help="unique device/client ID, alphanumeric only (optional)",
315
+ )
316
+ parser.add_argument(
317
+ "--interval",
318
+ type=float,
319
+ default=2.0,
320
+ help="polling interval in seconds (default: 2)",
321
+ )
322
+ parser.add_argument(
323
+ "--max-retries",
324
+ type=int,
325
+ default=30,
326
+ help="maximum number of polls (default: 30)",
327
+ )
328
+ parser.set_defaults(func=run)
329
+
330
+
331
+ def run(args: argparse.Namespace) -> int:
332
+ """Execute the dongniao workflow; returns the process exit code."""
333
+ if set(args.animal_class) - set("BMARF"):
334
+ fail("--class must be a combination of B/M/A/R/F")
335
+ if args.did and not (args.did.isascii() and args.did.isalnum()):
336
+ fail("--did must contain ASCII letters and digits only")
337
+
338
+ base = os.environ.get(ENV_API_BASE, "").strip() or DEFAULT_API_BASE
339
+ url = base.rstrip("/") + "/dongniao"
340
+ api_key = os.environ.get(ENV_API_KEY, "").strip()
341
+ headers: dict[str, str] = {"api_key": api_key} if api_key else {}
342
+
343
+ jpeg_bytes, _, _ = prepare_image(args.image)
344
+ code, data = upload_image(url, headers, jpeg_bytes, args)
345
+ if code == CODE_OK:
346
+ code, data = poll_result(url, headers, data, args.interval, args.max_retries)
347
+
348
+ print(json.dumps([code, data], ensure_ascii=False, indent=2))
349
+ return 0 if code == CODE_OK else 1
wbaila-0.1.0/uv.lock ADDED
@@ -0,0 +1,146 @@
1
+ version = 1
2
+ revision = 5
3
+ requires-python = ">=3.13"
4
+
5
+ [[package]]
6
+ name = "numpy"
7
+ version = "2.5.4"
8
+ source = { registry = "https://pypi.org/simple" }
9
+ sdist = { url = "https://files.pythonhosted.org/packages/95/b0/c7453d0b6e2073c3264468b106ee1563750cecc910965e67357e3698c83e/numpy-2.5.4.tar.gz", hash = "sha256:9a94cf751c9ad8ebaa835bcd3d40dacf8534ad086b88c38029b65123c7999d2a", size = 20866315, upload-time = "2026-10-10T20:05:31.422Z" }
10
+ wheels = [
11
+ { url = "https://files.pythonhosted.org/packages/67/14/1c3ee0118a8fce08565a5d8482631608426a33af10a01077fada5dc7c119/numpy-2.5.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2377da2dd3ba2c1200956acbab2a358c83b8e1f8531191672d1cd6ad83250d53", size = 16997729, upload-time = "2026-10-10T20:03:09.291Z" },
12
+ { url = "https://files.pythonhosted.org/packages/83/8c/b0ea9477fb1f0d4484bbc5cba21678cc9969704d8d7f3f158d1db35f8e14/numpy-2.5.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7415db95818b39ec475a5eea54d9e3b6bc83e3912158e46da3438cdce399804d", size = 12009826, upload-time = "2026-10-10T20:03:11.946Z" },
13
+ { url = "https://files.pythonhosted.org/packages/e2/84/6a3d75b3ba3dfe84ac0053450753d1e6d250a8bf80f66474cc46d1fb643f/numpy-2.5.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:6d6a71b9d9a97c03633aa12565ef2825ffa036cc1d99cfd50dacf0f128af4fe2", size = 5445803, upload-time = "2026-10-10T20:03:14.329Z" },
14
+ { url = "https://files.pythonhosted.org/packages/61/18/bb993f267ca20b376e07092a16793a5b31ed3138751e9ba480011a14d742/numpy-2.5.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d8200f16437b289a5bb927c6e184eccc3e8389bc0070fea4cd5b9e13c1757959", size = 6786220, upload-time = "2026-10-10T20:03:16.602Z" },
15
+ { url = "https://files.pythonhosted.org/packages/db/b6/135bb0953b61dc21c6cafa14b424ae666944e4899cf140e00c2b322a1a45/numpy-2.5.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2e71b04c6cad90026e544501bbe0ab9290fa8a4d845e7e8c0d124fb429c988", size = 15689178, upload-time = "2026-10-10T20:03:18.721Z" },
16
+ { url = "https://files.pythonhosted.org/packages/da/24/3bd070f3269dc609d8f26b2643f62ef91bb415841c0b294805aaf7fe06da/numpy-2.5.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6ffa07666f8da0eef81d149934a626d0d95fbd6838432a33e66245423a9062c0", size = 16718044, upload-time = "2026-10-10T20:03:21.386Z" },
17
+ { url = "https://files.pythonhosted.org/packages/c7/8e/9d15bd356b0a019c965312b1a3c6a727cac4cae5bc40045fbc12ce4cff9c/numpy-2.5.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2fa3328f784fc8277fc48026f6cad516f5c561c5d8e2e39b3c9e0c8f23223b34", size = 17048364, upload-time = "2026-10-10T20:03:24.468Z" },
18
+ { url = "https://files.pythonhosted.org/packages/dc/fe/9d5b560db964f15871885f2250795d15945f8699e17ef90c0c2ff4c875b2/numpy-2.5.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b86966fbe4ad7de710422175572bcdc75fdedadfb54bc6fab7deabccddd7780b", size = 18474904, upload-time = "2026-10-10T20:03:27.895Z" },
19
+ { url = "https://files.pythonhosted.org/packages/e9/98/d27552990f1bd611ef3e7466adadc78312ea2df63b83aad47fdc3d3ca8df/numpy-2.5.4-cp313-cp313-win32.whl", hash = "sha256:5258bc06526964be5face2fc6f756857a3f24f21ec3e72ca131337a75b165d6c", size = 6134537, upload-time = "2026-10-10T20:03:30.511Z" },
20
+ { url = "https://files.pythonhosted.org/packages/90/8c/140a40398a66b4471211be1affdb6ed24c486d581bd28d07b7f2fcb69540/numpy-2.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:8b4d2fd2d34e5f8c9235ee787de5631a37a28402b15cb80814df973d2be54129", size = 12566113, upload-time = "2026-10-10T20:03:32.612Z" },
21
+ { url = "https://files.pythonhosted.org/packages/34/52/01d205e5e8ccb27b2b0b141e801f22b830198c979111b0fa44771438d9a9/numpy-2.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:bc39ac66a7a9a3fbd6134fda43136b60ffde99c8f4501e64e0d2b24da137babf", size = 10519523, upload-time = "2026-10-10T20:03:35.163Z" },
22
+ { url = "https://files.pythonhosted.org/packages/99/ba/005cb5edd580d2f84d7ca3206b92dc17d4388e56e6f87ffe8f2762f83139/numpy-2.5.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c668b2f0d651605b58892644b0e302c7157f7159544227758c896982ef384b18", size = 17005499, upload-time = "2026-10-10T20:03:37.961Z" },
23
+ { url = "https://files.pythonhosted.org/packages/f3/49/fee7587c33ee35f7977f9051d7f2023d4e7246d62710c80f20c2361ea232/numpy-2.5.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ffa6ce09a1c6a08e9667dd9c97aa0b14184e8d18f2a14b78b2a2328c9147f076", size = 12019666, upload-time = "2026-10-10T20:03:40.606Z" },
24
+ { url = "https://files.pythonhosted.org/packages/d5/b2/c6ce165acffceb15a82c07b9cc77d391f86b3f379ba62911908ae5d34b91/numpy-2.5.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:956555e0603a4d38019ae6925711cb9dc43195c076a928accf7ea5d50bddfe53", size = 5455617, upload-time = "2026-10-10T20:03:43.138Z" },
25
+ { url = "https://files.pythonhosted.org/packages/77/7f/dd85ce260a669a89be06842cf355d7353a33e6cfbc590fb8ebb947d88dc9/numpy-2.5.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:2c2c4afffdeb7920e445028dd71eb932cac3e704792e964bc2a232426d4f1255", size = 6791932, upload-time = "2026-10-10T20:03:44.874Z" },
26
+ { url = "https://files.pythonhosted.org/packages/63/d6/34b0a2b0741386a63025a65a2c09caaaaaad6d0ca95b66cd65c30dd7fcb5/numpy-2.5.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4054173604cd8658796053f1f3bc0befb68ec1c0762c57fdad61e199256a8617", size = 15710899, upload-time = "2026-10-10T20:03:46.839Z" },
27
+ { url = "https://files.pythonhosted.org/packages/16/d5/928078d2b28f26829b138b4a6c3980045022fb409f570657a224ae60ef4e/numpy-2.5.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d549420b8858885cea8838a727842249218b9c1da24dd517e25c9c7a948310a3", size = 16721710, upload-time = "2026-10-10T20:03:49.489Z" },
28
+ { url = "https://files.pythonhosted.org/packages/f9/cf/673fd1b8f4cd78eb6320e87ec4c90ac19c095644259e3749853a405c70f4/numpy-2.5.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:823874a507a84af050493b622affde94b6f7c3a0dc22cb2801381bc03b871c00", size = 17066182, upload-time = "2026-10-10T20:03:52.25Z" },
29
+ { url = "https://files.pythonhosted.org/packages/f3/92/a77b5061b1b3e2643928c37976d79ee173e1b171ed158b7a3c61056b41bc/numpy-2.5.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4e263278bfb5ee6409db8aedbc4cc32973b1b82bc1e8d3c668551d04d83a7e37", size = 18480315, upload-time = "2026-10-10T20:03:55.39Z" },
30
+ { url = "https://files.pythonhosted.org/packages/bb/1d/1486ef3d3fb2279fd93c4c43c1bbbf1ca389a19816696684409f71babaab/numpy-2.5.4-cp314-cp314-win32.whl", hash = "sha256:cfd73180400042a7c532d30c5e287bdd03c59ff9ee1b4c0316af0539e29dfe23", size = 6185739, upload-time = "2026-10-10T20:03:58.186Z" },
31
+ { url = "https://files.pythonhosted.org/packages/52/9a/e1e512ebc948d5b9dd33b08736760f0ebbed2848fd4eda1f553088a6dcee/numpy-2.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:2ca144f15135b6212a5c47b1e2aeca6e412f102f95a2d5d88d8aec77eb255de3", size = 12703552, upload-time = "2026-10-10T20:04:00.28Z" },
32
+ { url = "https://files.pythonhosted.org/packages/2c/05/de709a982d7bbcd688a3fad71f002e9ff80c2db39e03ee726609b610f1d1/numpy-2.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:468397ba3c64427474706e5c9123fe266395496714dc684294eac75cd4930d1e", size = 10803901, upload-time = "2026-10-10T20:04:02.659Z" },
33
+ { url = "https://files.pythonhosted.org/packages/13/34/083570ada3bb2a30fbe5d77c8c6fef9141144a15d33e6f793a67e9749ab8/numpy-2.5.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1ef3aa6d7e29bb13677323114280b05acc57607fa2300e66432d665d5418a162", size = 12138695, upload-time = "2026-10-10T20:04:05.012Z" },
34
+ { url = "https://files.pythonhosted.org/packages/94/06/1f9c24db48eef0c2d1207e3b11fffb0478e39dfd8c1e1be7476936885eed/numpy-2.5.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:98b053943e5a0474ec0da309d2cb9d3f18ea57f8a2067c2ab7b5f763d1068380", size = 5574615, upload-time = "2026-10-10T20:04:07.316Z" },
35
+ { url = "https://files.pythonhosted.org/packages/da/0f/593fba2e1560e949123bc7d2fc48b5893d56e58cd4bd5a273d2fbf60b220/numpy-2.5.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:b64a85f40e154983960a4167d4c1d57a50c7f109b3d3264a3a984154e90a8454", size = 6889383, upload-time = "2026-10-10T20:04:09.918Z" },
36
+ { url = "https://files.pythonhosted.org/packages/eb/9f/b799dfdce4e05e80ed4bc815c71ff343a11533b2c0ffc221cae8538cda63/numpy-2.5.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a813ed7719bf45463c51779e6a98d0385fe905e48447526938a4b8337333d551", size = 15753763, upload-time = "2026-10-10T20:04:12.278Z" },
37
+ { url = "https://files.pythonhosted.org/packages/34/88/16c5f12f86f5ad2817c4d103205131fc6c8acb3d1878af05a1a4f23ec859/numpy-2.5.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9b80cdf5cedba0e90d93fa5f9a333c4d65bd545cd669b71bb97ce2b703c9d73", size = 16757212, upload-time = "2026-10-10T20:04:14.799Z" },
38
+ { url = "https://files.pythonhosted.org/packages/ff/4f/a1fe40e18a898e6a5089f4f0d891f0a493eb0574d5b34458f0fbe5aa3e5c/numpy-2.5.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2199ed071f460487c8db2c0e5c0b564494190edb4772fe80f9aad88b2604def5", size = 17116471, upload-time = "2026-10-10T20:04:17.58Z" },
39
+ { url = "https://files.pythonhosted.org/packages/aa/46/e923a11c78e65c1722e7aaad817c06bd591324174b9d28ce5d31eee4d432/numpy-2.5.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:64f9c9878c1938476365e11ccfb6b770f3b9e5f045ccddc514235041e6959365", size = 18524063, upload-time = "2026-10-10T20:04:20.365Z" },
40
+ { url = "https://files.pythonhosted.org/packages/5a/fa/84ab064514440c1f64a1b21088f2c82756defdd05e07c75ab233899565b2/numpy-2.5.4-cp314-cp314t-win32.whl", hash = "sha256:64d1c8ac28a4077cf987e0a71a7a0ef7e2df70722f07f0baa42dbb7eb6938647", size = 6340926, upload-time = "2026-10-10T20:04:22.865Z" },
41
+ { url = "https://files.pythonhosted.org/packages/7e/7e/6cd886876f435b10685db9b9f7eeb70356f99e052116f4e5f11c5792c714/numpy-2.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:067374eb538c34c745436365cf7b0112595c1d326f21ce4ff340f61230239fbb", size = 12901584, upload-time = "2026-10-10T20:04:24.99Z" },
42
+ { url = "https://files.pythonhosted.org/packages/38/1b/3c1684f6a06f7307f2335fca6e486cb162847fb97e91d65f8eb5cabad213/numpy-2.5.4-cp314-cp314t-win_arm64.whl", hash = "sha256:e94aef2c639da4a960ad0db8e06471208d8589974953d78b61d345b4eb99e394", size = 10891152, upload-time = "2026-10-10T20:04:27.52Z" },
43
+ { url = "https://files.pythonhosted.org/packages/08/f4/3224deff3af2bef6bc0b175369698d8cb348f3d91d9bb0286cd5c9eae9e0/numpy-2.5.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:8dddfbee2e68d26d0d7d7d9cb247b1fd4409241cce32d815a11d97ec2cfde179", size = 17003231, upload-time = "2026-10-10T20:04:30.021Z" },
44
+ { url = "https://files.pythonhosted.org/packages/be/75/fee0b8c6d94b44b2fdfae74f6a4ad5a138739589a8aebaec28ce4e713ed5/numpy-2.5.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:81e3420b27048b65eb14c3acf0c174a8cb0e023277716110347d2dcb26026dad", size = 12018300, upload-time = "2026-10-10T20:04:32.519Z" },
45
+ { url = "https://files.pythonhosted.org/packages/47/c0/d0b335a499a04b65f532c3f034346ef390f81299060f928492dabc1e0272/numpy-2.5.4-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:0b4724a19de67bea8cfc4970798efa78bcbbe2ac2613cfac16721a42d44de2a5", size = 5454250, upload-time = "2026-10-10T20:04:34.943Z" },
46
+ { url = "https://files.pythonhosted.org/packages/5a/0e/461b3783c03d668052e6a21b01b673db6ffcb7831fd32d9aa5368c1cd426/numpy-2.5.4-cp315-cp315-macosx_14_0_x86_64.whl", hash = "sha256:2132418bf8dd124a427ca9e6a1daf9ee1a87185344c95119ceae868b99466da1", size = 6789644, upload-time = "2026-10-10T20:04:37.258Z" },
47
+ { url = "https://files.pythonhosted.org/packages/b3/02/5dad269b02166965a7b4ca14adaddd75dbee0de42435bfecf561b84ba5a6/numpy-2.5.4-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:325518d4245b9e331387702aa58c2ce1dc4cdcbb41dfb4ccd5dcbc7e08db1266", size = 15704353, upload-time = "2026-10-10T20:04:39.616Z" },
48
+ { url = "https://files.pythonhosted.org/packages/93/3a/01360c8036822ed9f7aa32189a77d1476567ec1e8e1383522389e4faac45/numpy-2.5.4-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56733449d2544178beaa4545cee357370440cf056c197f9c7bfb19dbfdd0e86d", size = 16718648, upload-time = "2026-10-10T20:04:42.383Z" },
49
+ { url = "https://files.pythonhosted.org/packages/7d/5c/b863a2c093c4d6f21a597fcaf24ead0835c09ab16a8312d5a5a8868af683/numpy-2.5.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:5ec3753760c1a6d8bb91200666e545c3a9728e6269dfb5d6ce02340996698aa3", size = 17059053, upload-time = "2026-10-10T20:04:44.976Z" },
50
+ { url = "https://files.pythonhosted.org/packages/0a/60/ced4f57f9a1258a0af74f17cb0b0c2700b5c67cd6678823c803b263e4df3/numpy-2.5.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:b1185012870173de7ae33d370bd45b1cf5baee747ea4b97036b65f4e93016877", size = 18477406, upload-time = "2026-10-10T20:04:47.863Z" },
51
+ { url = "https://files.pythonhosted.org/packages/f9/bd/0ef22dafaafcc7d4bb3ca26b8d2afbd55dedad8eaba99a8c864e1997456f/numpy-2.5.4-cp315-cp315-win32.whl", hash = "sha256:298eca75243f2cbbfdb460560b9fb2a1792a33cf2ab4286efd43d92e8d3df508", size = 6185133, upload-time = "2026-10-10T20:04:50.467Z" },
52
+ { url = "https://files.pythonhosted.org/packages/50/bc/d2651b155ecc608a77e6f4d15495c11f14f19bb98f8bf0c5b0d38f86dda1/numpy-2.5.4-cp315-cp315-win_amd64.whl", hash = "sha256:332f3378fe077dd850e677ec01bdcc4f22368fb5d50ef10b2c79230b1bf5a592", size = 12703085, upload-time = "2026-10-10T20:04:52.63Z" },
53
+ { url = "https://files.pythonhosted.org/packages/dc/d2/45e404f8abb26fb9eda12b94012936873e827b1be76f2ee7890be128312e/numpy-2.5.4-cp315-cp315-win_arm64.whl", hash = "sha256:d4cccbbc78717966f764cd3af4fb70276fa01fc7a2688af11c78901fa5c04f05", size = 10801451, upload-time = "2026-10-10T20:04:55.677Z" },
54
+ { url = "https://files.pythonhosted.org/packages/c6/c3/2ae14e09cfdb67dc187a342e15308a21c15bf4d2071f8079e6aee5fe56dc/numpy-2.5.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:950ea81d57ef070665581b6e1b5f6a029306423cd1739c5b95fe78aa30db6b9d", size = 17097121, upload-time = "2026-10-10T20:04:58.403Z" },
55
+ { url = "https://files.pythonhosted.org/packages/f5/cf/305ae624ef8a039414317224abe9ec9c2fe7ea3c2e1cf204d43ff6b2ffb9/numpy-2.5.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:c05ede731b03fb1b7591faca9389ade3267d2bddf1ad8882bb3f2cc5e101694f", size = 12135439, upload-time = "2026-10-10T20:05:01.65Z" },
56
+ { url = "https://files.pythonhosted.org/packages/a9/a8/f75c63813aef95827bb2c0d13b12803016853056e8792c280058cdbfe783/numpy-2.5.4-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:5fbf7141bbfd63aea22f435c9062a032b9ea0082fe9845dad7f021d3f1234e71", size = 5571451, upload-time = "2026-10-10T20:05:04.135Z" },
57
+ { url = "https://files.pythonhosted.org/packages/6f/0f/f17763f983868b5c49b4101ebd7e00760bd1769478a6bb6a8de6e085bbac/numpy-2.5.4-cp315-cp315t-macosx_14_0_x86_64.whl", hash = "sha256:3573cd22564692a5b899ec344e5d5b9cc4576f2985b96f22af3564ed54f2710f", size = 6883356, upload-time = "2026-10-10T20:05:06.249Z" },
58
+ { url = "https://files.pythonhosted.org/packages/67/a7/8af04c5a79e047996cfa38854dcfbececdd0343a7c933a46fdd03ef6f5da/numpy-2.5.4-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c109eac9cd439193678f69d70733c1108487546ca8eafc107b510ae10c1aecd", size = 15750991, upload-time = "2026-10-10T20:05:08.376Z" },
59
+ { url = "https://files.pythonhosted.org/packages/57/7a/648254290d0c504faa8f2d07aa206660c728802c781a6f3fc68ab7cb5d71/numpy-2.5.4-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80d6ef6e8620eb2c2b4c4caad50b5935d6db3cde2d51581b55dcc79e14016d1d", size = 16757675, upload-time = "2026-10-10T20:05:11.393Z" },
60
+ { url = "https://files.pythonhosted.org/packages/b8/fe/4a8c3cdb0c70400cfe4c5bec42d3099a5673802a95064614b33e07b82aa1/numpy-2.5.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:77045a4b175bbf5316ec08003880804336c78f92281a1b72222b274ea85ec5ac", size = 17113846, upload-time = "2026-10-10T20:05:14.49Z" },
61
+ { url = "https://files.pythonhosted.org/packages/1b/7e/619692bb67778702c0e9eb2d468568a7573f4e269386ea61aed01ee4e557/numpy-2.5.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:0f02a46e49cfb6c73bdb7aea1c0d3461dbae9aba613542b65f657cd3d17b9fab", size = 18522915, upload-time = "2026-10-10T20:05:17.33Z" },
62
+ { url = "https://files.pythonhosted.org/packages/b7/b5/4da41c328788f575838f97a098fe8ca691ebc6f6fd73ad4a262ee40b184d/numpy-2.5.4-cp315-cp315t-win32.whl", hash = "sha256:ad62a416ddcf863bf44bba76fbf6b53366ab0692e294f51cae4b5fbe0d246788", size = 6335804, upload-time = "2026-10-10T20:05:19.921Z" },
63
+ { url = "https://files.pythonhosted.org/packages/98/94/6482ddfa3d312490cb9358f375bf2ad56427dbea8769187158e94d653753/numpy-2.5.4-cp315-cp315t-win_amd64.whl", hash = "sha256:38f47be9f74ab870d2633b5456ae519c43758a8d1fd05342f0ce4ecc034396ee", size = 12890095, upload-time = "2026-10-10T20:05:21.875Z" },
64
+ { url = "https://files.pythonhosted.org/packages/48/7f/c2d1b436b6e7cfebac140c2579a298344b85f2991a2ce5c3615cefb29400/numpy-2.5.4-cp315-cp315t-win_arm64.whl", hash = "sha256:7a14a461d9340f1b46b8648578aed9cdb8b3b018a8fac6c1dde2c9192a01a87f", size = 10883718, upload-time = "2026-10-10T20:05:28.547Z" },
65
+ ]
66
+
67
+ [[package]]
68
+ name = "pillow"
69
+ version = "12.3.0"
70
+ source = { registry = "https://pypi.org/simple" }
71
+ 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" }
72
+ wheels = [
73
+ { url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684, upload-time = "2026-07-01T11:54:25.934Z" },
74
+ { url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487, upload-time = "2026-07-01T11:54:27.935Z" },
75
+ { url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433, upload-time = "2026-07-01T11:54:29.813Z" },
76
+ { url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889, upload-time = "2026-07-01T11:54:31.97Z" },
77
+ { url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109, upload-time = "2026-07-01T11:54:34.026Z" },
78
+ { url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736, upload-time = "2026-07-01T11:54:36.131Z" },
79
+ { url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129, upload-time = "2026-07-01T11:54:38.216Z" },
80
+ { url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562, upload-time = "2026-07-01T11:54:40.354Z" },
81
+ { url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439, upload-time = "2026-07-01T11:54:42.489Z" },
82
+ { url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287, upload-time = "2026-07-01T11:54:44.9Z" },
83
+ { url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691, upload-time = "2026-07-01T11:54:47.141Z" },
84
+ { url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185, upload-time = "2026-07-01T11:54:49.137Z" },
85
+ { 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" },
86
+ { 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" },
87
+ { 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" },
88
+ { 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" },
89
+ { 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" },
90
+ { 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" },
91
+ { 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" },
92
+ { 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" },
93
+ { 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" },
94
+ { 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" },
95
+ { 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" },
96
+ { 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" },
97
+ { 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" },
98
+ { 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" },
99
+ { 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" },
100
+ { 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" },
101
+ { 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" },
102
+ { 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" },
103
+ { 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" },
104
+ { 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" },
105
+ { 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" },
106
+ { 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" },
107
+ { 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" },
108
+ { 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" },
109
+ { 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" },
110
+ { 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" },
111
+ { 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" },
112
+ { 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" },
113
+ { 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" },
114
+ { 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" },
115
+ { 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" },
116
+ { 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" },
117
+ { 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" },
118
+ { 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" },
119
+ { 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" },
120
+ { 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" },
121
+ { 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" },
122
+ { 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" },
123
+ { 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" },
124
+ { 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" },
125
+ { 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" },
126
+ { 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" },
127
+ ]
128
+
129
+ [[package]]
130
+ name = "wbaila"
131
+ version = "0.1.0"
132
+ source = { editable = "." }
133
+ dependencies = [
134
+ { name = "pillow" },
135
+ ]
136
+
137
+ [package.dev-dependencies]
138
+ dev = [
139
+ { name = "numpy" },
140
+ ]
141
+
142
+ [package.metadata]
143
+ requires-dist = [{ name = "pillow", specifier = ">=11" }]
144
+
145
+ [package.metadata.requires-dev]
146
+ dev = [{ name = "numpy", specifier = ">=2" }]