voxelion 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ .venv/
2
+ dist/
3
+ *.egg-info/
4
+ __pycache__/
5
+ .pytest_cache/
6
+ .mypy_cache/
7
+ .ruff_cache/
@@ -0,0 +1,14 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 — unreleased
4
+
5
+ First release. Targets `/api/v1`.
6
+
7
+ - `dedup()` — synchronous batch deduplication
8
+ - `submit_job()` / `JobHandle.wait()` — the queued path, with polling backoff
9
+ - `list_reports()` / `get_report()` / `manifest()`
10
+ - `balance()`
11
+ - Typed exceptions per API error code
12
+ - Retries: `Retry-After` on 429, exponential backoff with full jitter on 5xx,
13
+ nothing else retried
14
+ - Streaming uploads — an archive is never read into memory
voxelion-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Voxelion
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,149 @@
1
+ Metadata-Version: 2.4
2
+ Name: voxelion
3
+ Version: 0.1.0
4
+ Summary: Perceptual deduplication for image datasets — the official Python client for the Voxelion API.
5
+ Project-URL: Homepage, https://voxelion.ai
6
+ Project-URL: Documentation, https://voxelion.ai/docs/
7
+ Project-URL: Changelog, https://github.com/Asekun/bioHarmonix/blob/main/sdk/python/CHANGELOG.md
8
+ Author: Voxelion
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: data-quality,dataset,deduplication,dicom,machine-learning,perceptual-hash
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Scientific/Engineering :: Image Processing
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.9
24
+ Requires-Dist: httpx<1,>=0.24
25
+ Requires-Dist: pydantic<3,>=2
26
+ Provides-Extra: dev
27
+ Requires-Dist: mypy>=1.8; extra == 'dev'
28
+ Requires-Dist: pytest-httpx>=0.30; extra == 'dev'
29
+ Requires-Dist: pytest>=7; extra == 'dev'
30
+ Requires-Dist: ruff>=0.4; extra == 'dev'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # voxelion
34
+
35
+ Perceptual deduplication for image datasets — the official Python client for the
36
+ [Voxelion API](https://voxelion.ai/docs/).
37
+
38
+ Voxelion finds the files in a dataset that are the *same image* — after
39
+ re-encoding, resizing, format conversion or a metadata rewrite — and tells you
40
+ which ones are redundant. It never deletes anything.
41
+
42
+ ```bash
43
+ pip install voxelion
44
+ ```
45
+
46
+ ## Quickstart
47
+
48
+ ```python
49
+ from voxelion import Voxelion
50
+
51
+ vx = Voxelion() # reads VOXELION_API_KEY
52
+ report = vx.dedup(["a.png", "b.png", "c.png"], engine="curate")
53
+
54
+ print(f"{report.leakage:.1%} redundant across {report.count} images")
55
+ for cluster in report.clusters:
56
+ print("same image:", ", ".join(cluster))
57
+ ```
58
+
59
+ Create a key in the console under **API Keys**, then:
60
+
61
+ ```bash
62
+ export VOXELION_API_KEY=pk_live_...
63
+ ```
64
+
65
+ ## Large datasets
66
+
67
+ A batch that fits in one upload goes through `dedup()`. Anything larger is
68
+ queued, which survives a disconnect and reports progress:
69
+
70
+ ```python
71
+ job = vx.submit_job("corpus.zip", engine="curate")
72
+ print(job.id, job.status)
73
+
74
+ report = job.wait() # polls with backoff
75
+ plan = vx.manifest(report.id)
76
+ print(f"keep {len(plan.keep)}, drop {len(plan.drop)}")
77
+ ```
78
+
79
+ `plan.drop` is a list of filenames. **Voxelion does not delete them** — what
80
+ you do with the plan is yours to decide.
81
+
82
+ ## Engines
83
+
84
+ | Engine | For |
85
+ |---|---|
86
+ | `health` | Medical imaging. DICOM-aware: a CT or MR series is deduplicated as a scan, not as loose slices. |
87
+ | `curate` | General image archives — product photography, scraped corpora, generated output. |
88
+
89
+ Omit `engine=` to use your account's default. The engine that actually ran is
90
+ on the report either way:
91
+
92
+ ```python
93
+ report = vx.dedup(files)
94
+ print(report.engine) # "curate"
95
+ ```
96
+
97
+ ## Errors
98
+
99
+ Exceptions are typed by what went wrong, not by status code:
100
+
101
+ ```python
102
+ from voxelion import InsufficientCreditError, ConsentRequiredError
103
+
104
+ try:
105
+ report = vx.dedup(files)
106
+ except InsufficientCreditError as e:
107
+ print(f"needs {e.required_cents}c, have {e.available_cents}c")
108
+ except ConsentRequiredError:
109
+ print("Grant data-processing consent in the console first.")
110
+ ```
111
+
112
+ `429` responses honour the server's `Retry-After` automatically, and `5xx`
113
+ retries with exponential backoff. Nothing else is retried: a `402` will not
114
+ become affordable by asking again.
115
+
116
+ ## Sandbox
117
+
118
+ The sandbox is a separate deployment with its own database and accounts. It is
119
+ the same client with a different base URL:
120
+
121
+ ```python
122
+ from voxelion import Voxelion, SANDBOX_BASE_URL
123
+
124
+ vx = Voxelion(base_url=SANDBOX_BASE_URL)
125
+ ```
126
+
127
+ ## Checking cost before spending
128
+
129
+ ```python
130
+ account = vx.balance()
131
+ print(account.spendable_cents) # not balance_cents — see below
132
+ ```
133
+
134
+ `spendable_cents` is the balance minus credit already held against runs in
135
+ flight. It is the number that decides whether the next call succeeds.
136
+
137
+ ## Development
138
+
139
+ Models in `src/voxelion/models.py` are **generated** from the API's OpenAPI
140
+ document. Do not edit them by hand:
141
+
142
+ ```bash
143
+ ./scripts-generate.sh
144
+ ```
145
+
146
+ ```bash
147
+ pytest # unit tests, no network
148
+ pytest -m live # against a real deployment; needs VOXELION_API_KEY
149
+ ```
@@ -0,0 +1,117 @@
1
+ # voxelion
2
+
3
+ Perceptual deduplication for image datasets — the official Python client for the
4
+ [Voxelion API](https://voxelion.ai/docs/).
5
+
6
+ Voxelion finds the files in a dataset that are the *same image* — after
7
+ re-encoding, resizing, format conversion or a metadata rewrite — and tells you
8
+ which ones are redundant. It never deletes anything.
9
+
10
+ ```bash
11
+ pip install voxelion
12
+ ```
13
+
14
+ ## Quickstart
15
+
16
+ ```python
17
+ from voxelion import Voxelion
18
+
19
+ vx = Voxelion() # reads VOXELION_API_KEY
20
+ report = vx.dedup(["a.png", "b.png", "c.png"], engine="curate")
21
+
22
+ print(f"{report.leakage:.1%} redundant across {report.count} images")
23
+ for cluster in report.clusters:
24
+ print("same image:", ", ".join(cluster))
25
+ ```
26
+
27
+ Create a key in the console under **API Keys**, then:
28
+
29
+ ```bash
30
+ export VOXELION_API_KEY=pk_live_...
31
+ ```
32
+
33
+ ## Large datasets
34
+
35
+ A batch that fits in one upload goes through `dedup()`. Anything larger is
36
+ queued, which survives a disconnect and reports progress:
37
+
38
+ ```python
39
+ job = vx.submit_job("corpus.zip", engine="curate")
40
+ print(job.id, job.status)
41
+
42
+ report = job.wait() # polls with backoff
43
+ plan = vx.manifest(report.id)
44
+ print(f"keep {len(plan.keep)}, drop {len(plan.drop)}")
45
+ ```
46
+
47
+ `plan.drop` is a list of filenames. **Voxelion does not delete them** — what
48
+ you do with the plan is yours to decide.
49
+
50
+ ## Engines
51
+
52
+ | Engine | For |
53
+ |---|---|
54
+ | `health` | Medical imaging. DICOM-aware: a CT or MR series is deduplicated as a scan, not as loose slices. |
55
+ | `curate` | General image archives — product photography, scraped corpora, generated output. |
56
+
57
+ Omit `engine=` to use your account's default. The engine that actually ran is
58
+ on the report either way:
59
+
60
+ ```python
61
+ report = vx.dedup(files)
62
+ print(report.engine) # "curate"
63
+ ```
64
+
65
+ ## Errors
66
+
67
+ Exceptions are typed by what went wrong, not by status code:
68
+
69
+ ```python
70
+ from voxelion import InsufficientCreditError, ConsentRequiredError
71
+
72
+ try:
73
+ report = vx.dedup(files)
74
+ except InsufficientCreditError as e:
75
+ print(f"needs {e.required_cents}c, have {e.available_cents}c")
76
+ except ConsentRequiredError:
77
+ print("Grant data-processing consent in the console first.")
78
+ ```
79
+
80
+ `429` responses honour the server's `Retry-After` automatically, and `5xx`
81
+ retries with exponential backoff. Nothing else is retried: a `402` will not
82
+ become affordable by asking again.
83
+
84
+ ## Sandbox
85
+
86
+ The sandbox is a separate deployment with its own database and accounts. It is
87
+ the same client with a different base URL:
88
+
89
+ ```python
90
+ from voxelion import Voxelion, SANDBOX_BASE_URL
91
+
92
+ vx = Voxelion(base_url=SANDBOX_BASE_URL)
93
+ ```
94
+
95
+ ## Checking cost before spending
96
+
97
+ ```python
98
+ account = vx.balance()
99
+ print(account.spendable_cents) # not balance_cents — see below
100
+ ```
101
+
102
+ `spendable_cents` is the balance minus credit already held against runs in
103
+ flight. It is the number that decides whether the next call succeeds.
104
+
105
+ ## Development
106
+
107
+ Models in `src/voxelion/models.py` are **generated** from the API's OpenAPI
108
+ document. Do not edit them by hand:
109
+
110
+ ```bash
111
+ ./scripts-generate.sh
112
+ ```
113
+
114
+ ```bash
115
+ pytest # unit tests, no network
116
+ pytest -m live # against a real deployment; needs VOXELION_API_KEY
117
+ ```
@@ -0,0 +1,88 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "voxelion"
7
+ dynamic = ["version"]
8
+ description = "Perceptual deduplication for image datasets — the official Python client for the Voxelion API."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Voxelion" }]
14
+ keywords = ["deduplication", "dataset", "perceptual-hash", "dicom", "machine-learning", "data-quality"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "Intended Audience :: Science/Research",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.9",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Topic :: Scientific/Engineering :: Image Processing",
26
+ "Typing :: Typed",
27
+ ]
28
+
29
+ # Two runtime dependencies and no more. Every dependency an SDK adds is one its
30
+ # users must resolve against their own pins, and a data-science environment is
31
+ # already the hardest place in Python to add a constraint.
32
+ dependencies = [
33
+ "httpx>=0.24,<1",
34
+ "pydantic>=2,<3",
35
+ ]
36
+
37
+ [project.urls]
38
+ Homepage = "https://voxelion.ai"
39
+ Documentation = "https://voxelion.ai/docs/"
40
+ Changelog = "https://github.com/Asekun/bioHarmonix/blob/main/sdk/python/CHANGELOG.md"
41
+
42
+ [project.optional-dependencies]
43
+ dev = ["pytest>=7", "pytest-httpx>=0.30", "mypy>=1.8", "ruff>=0.4"]
44
+
45
+ [tool.hatch.version]
46
+ path = "src/voxelion/_version.py"
47
+
48
+ [tool.hatch.build.targets.wheel]
49
+ packages = ["src/voxelion"]
50
+
51
+ [tool.pytest.ini_options]
52
+ testpaths = ["tests"]
53
+ # Live tests need a sandbox key and are skipped without one.
54
+ markers = ["live: talks to a real deployment (needs VOXELION_API_KEY)"]
55
+
56
+ [tool.mypy]
57
+ python_version = "3.9"
58
+ strict = true
59
+
60
+ # The TESTS are type-checked too, but not at library strictness.
61
+ #
62
+ # They went unchecked entirely until the TypeScript side turned out to have the
63
+ # same blind spot — its tsconfig excluded `test/`, so every type assertion in
64
+ # every test there was inert. The same was true here.
65
+ #
66
+ # Annotating every pytest function and fixture buys noise, not safety, so the
67
+ # annotation REQUIREMENT is relaxed while real type errors — a wrong argument,
68
+ # a field that does not exist on a model — are still caught.
69
+ [[tool.mypy.overrides]]
70
+ module = "tests.*"
71
+ disallow_untyped_defs = false
72
+ disallow_incomplete_defs = false
73
+ disallow_untyped_calls = false
74
+ # Generated file: its style is the generator's business, not ours.
75
+ [[tool.mypy.overrides]]
76
+ module = "voxelion.models"
77
+ ignore_errors = true
78
+
79
+ [tool.ruff]
80
+ line-length = 100
81
+ target-version = "py39"
82
+ # models.py is generated from the OpenAPI document. Its formatting is the
83
+ # generator's business; linting it would mean either editing a generated file
84
+ # or carrying a permanent list of ignores.
85
+ extend-exclude = ["src/voxelion/models.py"]
86
+
87
+ [tool.ruff.lint]
88
+ select = ["E", "F", "I", "UP", "B", "SIM"]
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env bash
2
+ # Regenerate the models from the API's OpenAPI document.
3
+ #
4
+ # cd sdk/python && ./scripts-generate.sh
5
+ #
6
+ # Run this after any change to backend/src/schemas.js. The generated file is
7
+ # committed so that installing the SDK never requires the generator.
8
+ set -euo pipefail
9
+ cd "$(dirname "$0")"
10
+
11
+ (cd ../../backend && npm run openapi >/dev/null)
12
+
13
+ .venv/bin/datamodel-codegen \
14
+ --input ../../backend/openapi.json --input-file-type openapi \
15
+ --output src/voxelion/models.py \
16
+ --output-model-type pydantic_v2.BaseModel \
17
+ --target-python-version 3.9 \
18
+ --use-schema-description --use-field-description \
19
+ --field-constraints --use-double-quotes --use-standard-collections \
20
+ --snake-case-field --allow-population-by-field-name --use-subclass-enum \
21
+ --custom-file-header "# GENERATED FROM backend/openapi.json — DO NOT EDIT BY HAND.
22
+ #
23
+ # Regenerate with: ./scripts-generate.sh
24
+ #
25
+ # Derived from the API's own OpenAPI document, which is generated from
26
+ # apiDocs(). Hand-editing here would create a third description of the API that
27
+ # drifts from the other two, and that drift surfaces as a wrong type in
28
+ # somebody else's program."
29
+
30
+ echo "models regenerated: $(grep -c '^class' src/voxelion/models.py) classes"
@@ -0,0 +1,64 @@
1
+ """Voxelion — perceptual deduplication for image datasets.
2
+
3
+ from voxelion import Voxelion
4
+
5
+ vx = Voxelion() # reads VOXELION_API_KEY
6
+ report = vx.dedup(["a.png", "b.png"], engine="curate")
7
+ print(report.leakage, len(report.clusters))
8
+
9
+ Nothing here ever deletes a file. A report and its manifest tell you what is
10
+ redundant; acting on that stays your decision.
11
+ """
12
+
13
+ from ._client import (
14
+ DEFAULT_BASE_URL,
15
+ SANDBOX_BASE_URL,
16
+ JobHandle,
17
+ Voxelion,
18
+ )
19
+ from ._transport import ApiKeyProvider, ApiKeySource
20
+ from ._version import __version__
21
+ from .errors import (
22
+ AccountNotActiveError,
23
+ APIConnectionError,
24
+ AuthenticationError,
25
+ ConsentRequiredError,
26
+ InsufficientCreditError,
27
+ InvalidRequestError,
28
+ JobFailedError,
29
+ NotFoundError,
30
+ RateLimitError,
31
+ ServerError,
32
+ VoxelionError,
33
+ )
34
+ from .models import (
35
+ AutoTopUp,
36
+ BillingAccount,
37
+ Card,
38
+ DedupReport,
39
+ Engine,
40
+ Hash,
41
+ Job,
42
+ Manifest,
43
+ ManifestCluster,
44
+ Pair,
45
+ ReportSummary,
46
+ ReportVolume,
47
+ ReportVolumePair,
48
+ )
49
+ from .models import (
50
+ Report as StoredReport,
51
+ )
52
+
53
+ __all__ = [
54
+ "ApiKeyProvider", "ApiKeySource",
55
+ "__version__",
56
+ "Voxelion", "JobHandle", "DEFAULT_BASE_URL", "SANDBOX_BASE_URL",
57
+ "VoxelionError", "AuthenticationError", "ConsentRequiredError",
58
+ "AccountNotActiveError", "InsufficientCreditError", "InvalidRequestError",
59
+ "NotFoundError", "RateLimitError", "ServerError", "APIConnectionError",
60
+ "JobFailedError",
61
+ "DedupReport", "StoredReport", "ReportSummary", "ReportVolume", "ReportVolumePair",
62
+ "Manifest", "ManifestCluster", "Job",
63
+ "BillingAccount", "Card", "AutoTopUp", "Hash", "Pair", "Engine",
64
+ ]