finwave-wavefront 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.
- finwave_wavefront-0.1.0/.gitignore +9 -0
- finwave_wavefront-0.1.0/LICENSE +21 -0
- finwave_wavefront-0.1.0/PKG-INFO +109 -0
- finwave_wavefront-0.1.0/PROTOCOL.md +57 -0
- finwave_wavefront-0.1.0/README.md +85 -0
- finwave_wavefront-0.1.0/pyproject.toml +36 -0
- finwave_wavefront-0.1.0/src/wavefront/__init__.py +69 -0
- finwave_wavefront-0.1.0/src/wavefront/__main__.py +85 -0
- finwave_wavefront-0.1.0/src/wavefront/_art.py +107 -0
- finwave_wavefront-0.1.0/src/wavefront/client.py +277 -0
- finwave_wavefront-0.1.0/src/wavefront/exceptions.py +49 -0
- finwave_wavefront-0.1.0/src/wavefront/models.py +111 -0
- finwave_wavefront-0.1.0/tests/test_client.py +102 -0
- finwave_wavefront-0.1.0/uv.lock +309 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alexander Barnhill / Operational Ecology
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: finwave-wavefront
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python client for fetching finwave datasets over the dataset-API handshake.
|
|
5
|
+
Project-URL: Homepage, https://operationalecology.io
|
|
6
|
+
Project-URL: Source, https://github.com/Operational-Ecology/Wavefront
|
|
7
|
+
Project-URL: finwave, https://finwave.io
|
|
8
|
+
Author-email: Alexander Barnhill <alex.c.barnhill@gmail.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: conservation,datasets,finwave,photo-identification,wildlife,yolo
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Image Recognition
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Requires-Dist: httpx>=0.24
|
|
20
|
+
Provides-Extra: test
|
|
21
|
+
Requires-Dist: pytest>=7; extra == 'test'
|
|
22
|
+
Requires-Dist: respx>=0.20; extra == 'test'
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# wavefront
|
|
26
|
+
|
|
27
|
+
The official Python client for **[Finwave](https://finwave.io)** datasets.
|
|
28
|
+
|
|
29
|
+
Finwave serves frozen, versioned wildlife photo-identification and detector
|
|
30
|
+
datasets behind a small handshake API. `wavefront` turns that into one call.
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install finwave-wavefront
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Quick start
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
import wavefront
|
|
40
|
+
|
|
41
|
+
# the API key is read from $FW_API_TOKEN (or passed as api_key=...)
|
|
42
|
+
ds = wavefront.fetch("a7673931-9810-4c52-9654-1c9b1fafb63d", format="yolo")
|
|
43
|
+
|
|
44
|
+
print(ds.path) # extracted, ready to train on
|
|
45
|
+
print(ds.classes) # ['fluke']
|
|
46
|
+
print(ds.num_images) # 497
|
|
47
|
+
print(ds.fingerprint) # content hash — record it next to any model you train
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`ds` is path-like, so it drops straight into a trainer:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from ultralytics import YOLO
|
|
54
|
+
YOLO("yolo11n.pt").train(data=f"{ds.path}/data.yaml")
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Pre-flight without downloading
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
m = wavefront.manifest("a7673931-9810-4c52-9654-1c9b1fafb63d")
|
|
61
|
+
print(m.name, m.sample_count, m.available_formats) # Flukes v1 497 ['Yolo']
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### A reusable client
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from wavefront import Client
|
|
68
|
+
client = Client(api_key="...", base_url="https://finwave.io")
|
|
69
|
+
ds = client.fetch(dataset_id, format="yolo", dest="./data/flukes")
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Command line
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
export FW_API_TOKEN=...
|
|
76
|
+
wavefront manifest a7673931-9810-4c52-9654-1c9b1fafb63d
|
|
77
|
+
wavefront fetch a7673931-9810-4c52-9654-1c9b1fafb63d --format yolo --dest ./data/flukes
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## How it works
|
|
81
|
+
|
|
82
|
+
1. `GET /manifest` — cheap metadata + which export formats are ready.
|
|
83
|
+
2. `GET ?format=…` — a **handshake** that mints a short-lived signed download URL.
|
|
84
|
+
3. Download that URL → a zip → extract → a `Dataset`.
|
|
85
|
+
|
|
86
|
+
Downloads are **cached by content fingerprint**, so re-fetching a frozen
|
|
87
|
+
version is a no-op. The key needs the dataset-download scope.
|
|
88
|
+
|
|
89
|
+
## Authentication
|
|
90
|
+
|
|
91
|
+
Provide the key explicitly (`fetch(..., api_key=...)`) or set **`FW_API_TOKEN`**.
|
|
92
|
+
For compatibility, `WAVEFRONT_API_KEY`, `FINWAVE_DATASET_API_KEY` and
|
|
93
|
+
`DATASET_API_KEY` are also accepted (in that order).
|
|
94
|
+
|
|
95
|
+
## Errors
|
|
96
|
+
|
|
97
|
+
All errors subclass `wavefront.WavefrontError`:
|
|
98
|
+
|
|
99
|
+
| Exception | When |
|
|
100
|
+
|---|---|
|
|
101
|
+
| `AuthError` | key missing / rejected (401/403) |
|
|
102
|
+
| `DatasetNotFoundError` | no such version, or not visible to the key (404) |
|
|
103
|
+
| `FormatNotAvailableError` | the version exists but that export hasn't been generated yet (`.available` lists what is) |
|
|
104
|
+
| `APIError` | any other non-success response |
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT © Alexander Barnhill / [Operational Ecology](https://operationalecology.io).
|
|
109
|
+
A partnership artifact between finwave and Operational Ecology.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# PROTOCOL — wavefront
|
|
2
|
+
|
|
3
|
+
Decision log for the finwave dataset client. Governance: [`../CLAUDE.md`](../CLAUDE.md).
|
|
4
|
+
|
|
5
|
+
## Mission
|
|
6
|
+
|
|
7
|
+
### One-line — the official, public Python client for fetching finwave datasets
|
|
8
|
+
Status: Committed — 2026-06-21
|
|
9
|
+
finwave already exposes a dataset handshake API (`/api/datasets-api/{id}`); every
|
|
10
|
+
consumer was re-implementing the manifest → handshake → SAS-download → unzip
|
|
11
|
+
dance by hand. `wavefront` is the single, supported way to do it. It is
|
|
12
|
+
OpEco's **first public** project (PyPI + public GitHub) and a deliberate
|
|
13
|
+
finwave × Operational Ecology partnership artifact, both authored by AB.
|
|
14
|
+
|
|
15
|
+
## Scope
|
|
16
|
+
|
|
17
|
+
### v1 fetches frozen versions in declared export formats; it does not create them
|
|
18
|
+
Status: Committed — 2026-06-21
|
|
19
|
+
The client is read/download only. Generating an export (e.g. producing the YOLO
|
|
20
|
+
bundle for a frozen version) is an admin action on the Hub and is out of scope —
|
|
21
|
+
hence `FormatNotAvailableError` carries `.available` rather than trying to
|
|
22
|
+
trigger generation. The dataset-API key is download-scoped only.
|
|
23
|
+
|
|
24
|
+
## Design
|
|
25
|
+
|
|
26
|
+
### Caching is keyed by the server's content fingerprint, not by id alone
|
|
27
|
+
Status: Committed — 2026-06-21
|
|
28
|
+
Versions are frozen, so a fingerprint pins exact bytes. A completed extraction
|
|
29
|
+
writes a `.wavefront-complete` marker containing the fingerprint; a later fetch
|
|
30
|
+
with a matching marker is a no-op. This makes `fetch` idempotent and cheap to
|
|
31
|
+
call in a training loop without a manual "already downloaded?" guard.
|
|
32
|
+
|
|
33
|
+
### `Dataset.fingerprint` is surfaced as first-class provenance
|
|
34
|
+
Status: Committed — 2026-06-21
|
|
35
|
+
Every fetched dataset exposes the version fingerprint so a downstream training
|
|
36
|
+
run can record exactly which data produced a model (aligns with OpEco Rule 2,
|
|
37
|
+
source-code/data consistency). The client does not recompute the hash
|
|
38
|
+
client-side (the algorithm is server-owned); it carries the declared one.
|
|
39
|
+
|
|
40
|
+
### API-key precedence: explicit arg → `FW_API_TOKEN` → `WAVEFRONT_API_KEY` → `FINWAVE_DATASET_API_KEY` → `DATASET_API_KEY`
|
|
41
|
+
Status: Committed — 2026-06-21
|
|
42
|
+
`FW_API_TOKEN` is the canonical name; the rest are accepted so existing
|
|
43
|
+
finwave/finprint workspace environments keep working unchanged.
|
|
44
|
+
|
|
45
|
+
## Deferred
|
|
46
|
+
|
|
47
|
+
### Streaming-to-disk integrity check beyond fingerprint consistency
|
|
48
|
+
Status: Open — 2026-06-21
|
|
49
|
+
The handshake and manifest fingerprints are checked for consistency, but the
|
|
50
|
+
downloaded bytes are not independently re-hashed against a per-file digest (the
|
|
51
|
+
export bundle does not yet ship one). Revisit if the Hub starts emitting
|
|
52
|
+
per-artifact `sha256` so `IntegrityError` can be raised on a real mismatch.
|
|
53
|
+
|
|
54
|
+
### Additional export formats (COCO, PascalVoc)
|
|
55
|
+
Status: Parked — 2026-06-21
|
|
56
|
+
Format aliases are mapped, but only YOLO is produced by the Hub in v1. The
|
|
57
|
+
client already accepts any format string and lets the server decide.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# wavefront
|
|
2
|
+
|
|
3
|
+
The official Python client for **[Finwave](https://finwave.io)** datasets.
|
|
4
|
+
|
|
5
|
+
Finwave serves frozen, versioned wildlife photo-identification and detector
|
|
6
|
+
datasets behind a small handshake API. `wavefront` turns that into one call.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install finwave-wavefront
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Quick start
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
import wavefront
|
|
16
|
+
|
|
17
|
+
# the API key is read from $FW_API_TOKEN (or passed as api_key=...)
|
|
18
|
+
ds = wavefront.fetch("a7673931-9810-4c52-9654-1c9b1fafb63d", format="yolo")
|
|
19
|
+
|
|
20
|
+
print(ds.path) # extracted, ready to train on
|
|
21
|
+
print(ds.classes) # ['fluke']
|
|
22
|
+
print(ds.num_images) # 497
|
|
23
|
+
print(ds.fingerprint) # content hash — record it next to any model you train
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`ds` is path-like, so it drops straight into a trainer:
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from ultralytics import YOLO
|
|
30
|
+
YOLO("yolo11n.pt").train(data=f"{ds.path}/data.yaml")
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Pre-flight without downloading
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
m = wavefront.manifest("a7673931-9810-4c52-9654-1c9b1fafb63d")
|
|
37
|
+
print(m.name, m.sample_count, m.available_formats) # Flukes v1 497 ['Yolo']
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### A reusable client
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from wavefront import Client
|
|
44
|
+
client = Client(api_key="...", base_url="https://finwave.io")
|
|
45
|
+
ds = client.fetch(dataset_id, format="yolo", dest="./data/flukes")
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Command line
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
export FW_API_TOKEN=...
|
|
52
|
+
wavefront manifest a7673931-9810-4c52-9654-1c9b1fafb63d
|
|
53
|
+
wavefront fetch a7673931-9810-4c52-9654-1c9b1fafb63d --format yolo --dest ./data/flukes
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## How it works
|
|
57
|
+
|
|
58
|
+
1. `GET /manifest` — cheap metadata + which export formats are ready.
|
|
59
|
+
2. `GET ?format=…` — a **handshake** that mints a short-lived signed download URL.
|
|
60
|
+
3. Download that URL → a zip → extract → a `Dataset`.
|
|
61
|
+
|
|
62
|
+
Downloads are **cached by content fingerprint**, so re-fetching a frozen
|
|
63
|
+
version is a no-op. The key needs the dataset-download scope.
|
|
64
|
+
|
|
65
|
+
## Authentication
|
|
66
|
+
|
|
67
|
+
Provide the key explicitly (`fetch(..., api_key=...)`) or set **`FW_API_TOKEN`**.
|
|
68
|
+
For compatibility, `WAVEFRONT_API_KEY`, `FINWAVE_DATASET_API_KEY` and
|
|
69
|
+
`DATASET_API_KEY` are also accepted (in that order).
|
|
70
|
+
|
|
71
|
+
## Errors
|
|
72
|
+
|
|
73
|
+
All errors subclass `wavefront.WavefrontError`:
|
|
74
|
+
|
|
75
|
+
| Exception | When |
|
|
76
|
+
|---|---|
|
|
77
|
+
| `AuthError` | key missing / rejected (401/403) |
|
|
78
|
+
| `DatasetNotFoundError` | no such version, or not visible to the key (404) |
|
|
79
|
+
| `FormatNotAvailableError` | the version exists but that export hasn't been generated yet (`.available` lists what is) |
|
|
80
|
+
| `APIError` | any other non-success response |
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
MIT © Alexander Barnhill / [Operational Ecology](https://operationalecology.io).
|
|
85
|
+
A partnership artifact between finwave and Operational Ecology.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "finwave-wavefront"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Official Python client for fetching finwave datasets over the dataset-API handshake."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Alexander Barnhill", email = "alex.c.barnhill@gmail.com" }]
|
|
13
|
+
keywords = ["finwave", "wildlife", "photo-identification", "datasets", "conservation", "yolo"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Science/Research",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Topic :: Scientific/Engineering",
|
|
20
|
+
"Topic :: Scientific/Engineering :: Image Recognition",
|
|
21
|
+
]
|
|
22
|
+
dependencies = ["httpx>=0.24"]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://operationalecology.io"
|
|
26
|
+
Source = "https://github.com/Operational-Ecology/Wavefront"
|
|
27
|
+
"finwave" = "https://finwave.io"
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
test = ["pytest>=7", "respx>=0.20"]
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
wavefront = "wavefront.__main__:main"
|
|
34
|
+
|
|
35
|
+
[tool.hatch.build.targets.wheel]
|
|
36
|
+
packages = ["src/wavefront"]
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""wavefront — the official Python client for finwave datasets.
|
|
2
|
+
|
|
3
|
+
finwave (https://finwave.io) serves frozen, versioned wildlife photo-ID and
|
|
4
|
+
detector datasets behind a small handshake API. ``wavefront`` turns that into
|
|
5
|
+
one call:
|
|
6
|
+
|
|
7
|
+
>>> import wavefront
|
|
8
|
+
>>> ds = wavefront.fetch("a7673931-9810-4c52-9654-1c9b1fafb63d", format="yolo")
|
|
9
|
+
>>> ds.path, ds.classes, ds.num_images
|
|
10
|
+
(PosixPath('.../Yolo-81f97dec8667'), ['fluke'], 497)
|
|
11
|
+
|
|
12
|
+
The key is read from the ``FW_API_TOKEN`` environment variable (or passed
|
|
13
|
+
explicitly as ``api_key=``); ``WAVEFRONT_API_KEY``, ``FINWAVE_DATASET_API_KEY``
|
|
14
|
+
and ``DATASET_API_KEY`` are also accepted for compatibility. For repeated or
|
|
15
|
+
configured use, construct a :class:`Client`.
|
|
16
|
+
|
|
17
|
+
Every step logs on the ``wavefront`` logger. The library attaches a
|
|
18
|
+
``NullHandler`` and never configures logging itself — enable output with
|
|
19
|
+
``logging.basicConfig(level=logging.INFO)`` in your application.
|
|
20
|
+
|
|
21
|
+
Built by Operational Ecology (https://operationalecology.io).
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import logging
|
|
26
|
+
from typing import Optional
|
|
27
|
+
|
|
28
|
+
from .client import API_KEY_ENV, DEFAULT_BASE_URL, Client
|
|
29
|
+
|
|
30
|
+
logging.getLogger("wavefront").addHandler(logging.NullHandler())
|
|
31
|
+
from .exceptions import (
|
|
32
|
+
APIError,
|
|
33
|
+
AuthError,
|
|
34
|
+
DatasetNotFoundError,
|
|
35
|
+
FormatNotAvailableError,
|
|
36
|
+
IntegrityError,
|
|
37
|
+
WavefrontError,
|
|
38
|
+
)
|
|
39
|
+
from .models import Dataset, Manifest
|
|
40
|
+
|
|
41
|
+
__version__ = "0.1.0"
|
|
42
|
+
__all__ = [
|
|
43
|
+
"fetch",
|
|
44
|
+
"manifest",
|
|
45
|
+
"Client",
|
|
46
|
+
"Dataset",
|
|
47
|
+
"Manifest",
|
|
48
|
+
"WavefrontError",
|
|
49
|
+
"AuthError",
|
|
50
|
+
"DatasetNotFoundError",
|
|
51
|
+
"FormatNotAvailableError",
|
|
52
|
+
"IntegrityError",
|
|
53
|
+
"APIError",
|
|
54
|
+
"DEFAULT_BASE_URL",
|
|
55
|
+
"API_KEY_ENV",
|
|
56
|
+
"__version__",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def fetch(dataset_version_id: str, *, format: str = "yolo",
|
|
61
|
+
api_key: Optional[str] = None, base_url: str = DEFAULT_BASE_URL, **kwargs) -> Dataset:
|
|
62
|
+
"""Fetch + extract a dataset version with a one-off client. See :meth:`Client.fetch`."""
|
|
63
|
+
return Client(api_key, base_url=base_url).fetch(dataset_version_id, format=format, **kwargs)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def manifest(dataset_version_id: str, *,
|
|
67
|
+
api_key: Optional[str] = None, base_url: str = DEFAULT_BASE_URL) -> Manifest:
|
|
68
|
+
"""Return a dataset version's manifest with a one-off client. See :meth:`Client.manifest`."""
|
|
69
|
+
return Client(api_key, base_url=base_url).manifest(dataset_version_id)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Command-line interface: ``wavefront fetch|manifest <id>``."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import logging
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from . import __version__, _art
|
|
9
|
+
from .client import Client
|
|
10
|
+
from .exceptions import WavefrontError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _fmt_bytes(n: int) -> str:
|
|
14
|
+
for unit in ("B", "KB", "MB", "GB"):
|
|
15
|
+
if n < 1024 or unit == "GB":
|
|
16
|
+
return f"{n:.0f}{unit}" if unit == "B" else f"{n/1:.0f}{unit}"
|
|
17
|
+
n /= 1024
|
|
18
|
+
return f"{n:.0f}B"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def main(argv=None) -> int:
|
|
22
|
+
p = argparse.ArgumentParser(prog="wavefront", description="Fetch finwave datasets.")
|
|
23
|
+
p.add_argument("--version", action="version", version=f"wavefront {__version__}")
|
|
24
|
+
p.add_argument("--api-key", default=None, help="overrides $FW_API_TOKEN")
|
|
25
|
+
p.add_argument("--base-url", default=None, help="finwave base URL")
|
|
26
|
+
p.add_argument("-v", "--verbose", action="store_true", help="debug-level logging")
|
|
27
|
+
p.add_argument("-q", "--quiet", action="store_true", help="warnings and errors only")
|
|
28
|
+
p.add_argument("--no-art", action="store_true", help="disable the wave animation")
|
|
29
|
+
sub = p.add_subparsers(dest="cmd", required=False)
|
|
30
|
+
|
|
31
|
+
m = sub.add_parser("manifest", help="print a version's metadata + formats")
|
|
32
|
+
m.add_argument("dataset_version_id")
|
|
33
|
+
|
|
34
|
+
f = sub.add_parser("fetch", help="download + extract a dataset version")
|
|
35
|
+
f.add_argument("dataset_version_id")
|
|
36
|
+
f.add_argument("--format", default="yolo")
|
|
37
|
+
f.add_argument("--dest", default=None, help="extract dir (default: cache)")
|
|
38
|
+
f.add_argument("--force", action="store_true", help="ignore cache")
|
|
39
|
+
|
|
40
|
+
args = p.parse_args(argv)
|
|
41
|
+
level = logging.WARNING if args.quiet else (logging.DEBUG if args.verbose else logging.INFO)
|
|
42
|
+
logging.basicConfig(level=level, format="%(message)s", stream=sys.stderr)
|
|
43
|
+
show_art = not (args.no_art or args.quiet)
|
|
44
|
+
if args.cmd is None: # bare `wavefront` → wave + wordmark
|
|
45
|
+
if show_art:
|
|
46
|
+
_art.banner()
|
|
47
|
+
return 0
|
|
48
|
+
kw = {}
|
|
49
|
+
if args.base_url:
|
|
50
|
+
kw["base_url"] = args.base_url
|
|
51
|
+
try:
|
|
52
|
+
client = Client(args.api_key, **kw)
|
|
53
|
+
if args.cmd == "manifest":
|
|
54
|
+
mf = client.manifest(args.dataset_version_id)
|
|
55
|
+
print(f"{mf.name} (v{mf.version_number})")
|
|
56
|
+
print(f" samples: {mf.sample_count} annotations: {mf.annotation_count}")
|
|
57
|
+
print(f" formats: {mf.available_formats or '(none generated yet)'}")
|
|
58
|
+
print(f" fingerprint: {mf.fingerprint}")
|
|
59
|
+
return 0
|
|
60
|
+
if args.cmd == "fetch":
|
|
61
|
+
if show_art:
|
|
62
|
+
_art.wave()
|
|
63
|
+
last = [0.0]
|
|
64
|
+
|
|
65
|
+
def prog(got, total):
|
|
66
|
+
pct = f" {100*got/total:.0f}%" if total else ""
|
|
67
|
+
if got - last[0] >= (1 << 23) or got == total: # ~8MB steps
|
|
68
|
+
print(f"\r downloading {_fmt_bytes(got)}{pct}", end="", file=sys.stderr)
|
|
69
|
+
last[0] = got
|
|
70
|
+
|
|
71
|
+
ds = client.fetch(args.dataset_version_id, format=args.format,
|
|
72
|
+
dest=args.dest, force=args.force, progress=prog)
|
|
73
|
+
print("", file=sys.stderr)
|
|
74
|
+
print(ds.path)
|
|
75
|
+
print(f" {ds.num_images} images, {ds.num_labels} labels, classes={ds.classes}",
|
|
76
|
+
file=sys.stderr)
|
|
77
|
+
return 0
|
|
78
|
+
except WavefrontError as e:
|
|
79
|
+
print(f"error: {e}", file=sys.stderr)
|
|
80
|
+
return 1
|
|
81
|
+
return 0
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
if __name__ == "__main__":
|
|
85
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""A small finwave-blue wave flourish for the terminal. Purely cosmetic.
|
|
2
|
+
|
|
3
|
+
Animates a travelling, foam-tipped wave in the finwave palette. No-ops on
|
|
4
|
+
non-TTY streams, under ``NO_COLOR``, or for dumb terminals, so it never
|
|
5
|
+
corrupts piped or logged output.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import math
|
|
10
|
+
import os
|
|
11
|
+
import shutil
|
|
12
|
+
import sys
|
|
13
|
+
import time
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
# finwave palette, deep water → crest (mirrors the logo's blue gradient)
|
|
17
|
+
_GRAD = [
|
|
18
|
+
(14, 63, 133), (21, 88, 184), (31, 111, 230),
|
|
19
|
+
(59, 143, 255), (93, 158, 255), (130, 185, 255),
|
|
20
|
+
]
|
|
21
|
+
_FOAM = (224, 238, 255)
|
|
22
|
+
_BLOCKS = " ▁▂▃▄▅▆▇█"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def supported(stream) -> bool:
|
|
26
|
+
return (
|
|
27
|
+
hasattr(stream, "isatty")
|
|
28
|
+
and stream.isatty()
|
|
29
|
+
and not os.environ.get("NO_COLOR")
|
|
30
|
+
and not os.environ.get("WAVEFRONT_NO_ART")
|
|
31
|
+
and os.environ.get("TERM", "") not in ("", "dumb")
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _rgb(rgb) -> str:
|
|
36
|
+
return f"\x1b[38;2;{rgb[0]};{rgb[1]};{rgb[2]}m"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _surface(width: int, rows: int, phase: float) -> list[float]:
|
|
40
|
+
"""Water height in cells (0..rows) per column — two summed travelling waves."""
|
|
41
|
+
out = []
|
|
42
|
+
for x in range(width):
|
|
43
|
+
h = 0.52 + 0.30 * math.sin(x * 0.26 - phase) + 0.14 * math.sin(x * 0.11 + phase * 0.6)
|
|
44
|
+
out.append(max(0.0, min(1.0, h)) * rows)
|
|
45
|
+
return out
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _render_frame(width: int, rows: int, phase: float) -> str:
|
|
49
|
+
surf = _surface(width, rows, phase)
|
|
50
|
+
lines = []
|
|
51
|
+
for r in range(rows): # r = 0 is the top row
|
|
52
|
+
depth_from_surface = r # 0 near crest → lighter
|
|
53
|
+
line = []
|
|
54
|
+
for x in range(width):
|
|
55
|
+
band = surf[x] - (rows - 1 - r) # cells of water in this row (>1 = full)
|
|
56
|
+
if band <= 0:
|
|
57
|
+
line.append(" ")
|
|
58
|
+
continue
|
|
59
|
+
crest = band < 1.0 # the topmost filled cell
|
|
60
|
+
block = _BLOCKS[min(8, max(1, int(round(band * 8))))] if crest else "█"
|
|
61
|
+
if crest:
|
|
62
|
+
color = _FOAM
|
|
63
|
+
else:
|
|
64
|
+
gi = min(len(_GRAD) - 1, depth_from_surface)
|
|
65
|
+
color = _GRAD[len(_GRAD) - 1 - gi] if False else _GRAD[gi]
|
|
66
|
+
line.append(_rgb(color) + block)
|
|
67
|
+
lines.append("".join(line) + "\x1b[0m")
|
|
68
|
+
return "\n".join(lines)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def wave(stream=None, *, duration: float = 1.3, fps: int = 30,
|
|
72
|
+
width: Optional[int] = None, rows: int = 4) -> None:
|
|
73
|
+
"""Play the wave flourish, then leave the terminal clean."""
|
|
74
|
+
stream = stream or sys.stderr
|
|
75
|
+
if not supported(stream):
|
|
76
|
+
return
|
|
77
|
+
cols = shutil.get_terminal_size((80, 24)).columns
|
|
78
|
+
w = min(width or cols - 2, 60)
|
|
79
|
+
frames = max(1, int(duration * fps))
|
|
80
|
+
stream.write("\x1b[?25l") # hide cursor
|
|
81
|
+
try:
|
|
82
|
+
for f in range(frames):
|
|
83
|
+
stream.write(_render_frame(w, rows, f / fps * 6.5))
|
|
84
|
+
if f < frames - 1:
|
|
85
|
+
stream.write(f"\x1b[{rows - 1}A\r") # back to top of the wave
|
|
86
|
+
stream.flush()
|
|
87
|
+
time.sleep(1.0 / fps)
|
|
88
|
+
stream.write("\n")
|
|
89
|
+
finally:
|
|
90
|
+
stream.write("\x1b[?25h\x1b[0m") # restore cursor + reset
|
|
91
|
+
stream.flush()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def banner(stream=None) -> None:
|
|
95
|
+
"""A one-shot wave + wordmark, used by the bare ``wavefront`` command."""
|
|
96
|
+
from . import __version__
|
|
97
|
+
stream = stream or sys.stderr
|
|
98
|
+
wave(stream, duration=1.1)
|
|
99
|
+
if supported(stream):
|
|
100
|
+
stream.write(_rgb(_GRAD[3]) + " wavefront " + _rgb(_GRAD[5])
|
|
101
|
+
+ f"v{__version__}\x1b[0m " + "\x1b[2mfinwave datasets, one call\x1b[0m\n")
|
|
102
|
+
else:
|
|
103
|
+
stream.write(f"wavefront v{__version__} — finwave datasets, one call\n")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
if __name__ == "__main__": # quick visual check: `python -m wavefront._art`
|
|
107
|
+
wave(sys.stdout, duration=3.0)
|