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.
- wbaila-0.1.0/.gitignore +16 -0
- wbaila-0.1.0/AGENTS.md +106 -0
- wbaila-0.1.0/PKG-INFO +100 -0
- wbaila-0.1.0/README.md +78 -0
- wbaila-0.1.0/pyproject.toml +50 -0
- wbaila-0.1.0/pyrightconfig.json +7 -0
- wbaila-0.1.0/ruff.toml +8 -0
- wbaila-0.1.0/src/wbaila/__init__.py +7 -0
- wbaila-0.1.0/src/wbaila/__main__.py +38 -0
- wbaila-0.1.0/src/wbaila/dongniao.py +349 -0
- wbaila-0.1.0/uv.lock +146 -0
wbaila-0.1.0/.gitignore
ADDED
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"]
|
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" }]
|