morphiaone 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.
- morphiaone-0.1.0/LICENSE +21 -0
- morphiaone-0.1.0/PKG-INFO +292 -0
- morphiaone-0.1.0/README.md +267 -0
- morphiaone-0.1.0/morphiaone/__init__.py +91 -0
- morphiaone-0.1.0/morphiaone/client.py +499 -0
- morphiaone-0.1.0/morphiaone/errors.py +168 -0
- morphiaone-0.1.0/morphiaone/models.py +22 -0
- morphiaone-0.1.0/morphiaone/py.typed +1 -0
- morphiaone-0.1.0/morphiaone/v1.py +1323 -0
- morphiaone-0.1.0/morphiaone.egg-info/PKG-INFO +292 -0
- morphiaone-0.1.0/morphiaone.egg-info/SOURCES.txt +14 -0
- morphiaone-0.1.0/morphiaone.egg-info/dependency_links.txt +1 -0
- morphiaone-0.1.0/morphiaone.egg-info/requires.txt +1 -0
- morphiaone-0.1.0/morphiaone.egg-info/top_level.txt +1 -0
- morphiaone-0.1.0/pyproject.toml +38 -0
- morphiaone-0.1.0/setup.cfg +4 -0
morphiaone-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MorphiaOne
|
|
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,292 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: morphiaone
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the MorphiaOne V1 API (Developer Preview): Skin Analysis, Makeup Try-On and Hair Color Try-On.
|
|
5
|
+
Author: MorphiaOne
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/MorphiaOne/vision-api
|
|
8
|
+
Keywords: morphiaone,sdk,api,skin-analysis,virtual-try-on
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: <3.15,>=3.11
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: httpx<1,>=0.27
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# MorphiaOne Python SDK
|
|
27
|
+
|
|
28
|
+
`morphiaone` is the Python client for the MorphiaOne V1 API: Skin Analysis, Makeup
|
|
29
|
+
Try-On and Hair Color Try-On, plus the Assets, Jobs and Usage calls they rest on.
|
|
30
|
+
|
|
31
|
+
> **Developer Preview.** The V1 API contract is still a draft, and this SDK may change
|
|
32
|
+
> before 1.0, including in ways that break code written against it. Pin an exact
|
|
33
|
+
> version. This package is not published yet: until the first release, the install
|
|
34
|
+
> command below resolves nothing.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install morphiaone
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Requires Python 3.11 or newer. One runtime dependency: `httpx`.
|
|
43
|
+
|
|
44
|
+
## Authenticate
|
|
45
|
+
|
|
46
|
+
Keep your API key in the environment of a server process, and read it from there:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
export MORPHIAONE_API_KEY="<your API key>"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
import os
|
|
54
|
+
|
|
55
|
+
from morphiaone import MorphiaOne
|
|
56
|
+
|
|
57
|
+
client = MorphiaOne(
|
|
58
|
+
api_key=os.environ["MORPHIAONE_API_KEY"],
|
|
59
|
+
base_url="<your API base URL>",
|
|
60
|
+
)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Use the client as a context manager (`with MorphiaOne(...) as client:`) or call
|
|
64
|
+
`client.close()` when you are done. `AsyncMorphiaOne` has the same namespaces, with
|
|
65
|
+
every call a coroutine.
|
|
66
|
+
|
|
67
|
+
**Server-side only.** Your API key authorizes spending on your account. Never ship it in
|
|
68
|
+
a browser bundle, a mobile app or a chatbot client; call MorphiaOne from your own
|
|
69
|
+
backend and hand your front end only the result.
|
|
70
|
+
|
|
71
|
+
## How a request works
|
|
72
|
+
|
|
73
|
+
Every product call follows the same three steps:
|
|
74
|
+
|
|
75
|
+
1. Upload each input image once as an **Asset**.
|
|
76
|
+
2. Submit a product request that references the Asset ids. The API answers with a
|
|
77
|
+
**Job**; the work runs asynchronously.
|
|
78
|
+
3. Wait for the Job, or poll it, and read its result Assets.
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
source = client.assets.upload("face.jpg") # a str or Path, or raw bytes
|
|
82
|
+
job = client.skin.analyze(asset_id=source.id, concerns=["acne", "redness"])
|
|
83
|
+
done = client.jobs.wait(job.id) # raises ProcessingError if it failed
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`assets.upload(image, *, media_type=None, timeout=None)` infers the media type
|
|
87
|
+
(`image/jpeg`, `image/png` or `image/webp`) from the file suffix or the leading bytes
|
|
88
|
+
when omitted.
|
|
89
|
+
|
|
90
|
+
Every submit also accepts the keyword-only `idempotency_key`, `timeout` and `wait`.
|
|
91
|
+
With `wait=True` the call polls to the succeeded job before returning, exactly as
|
|
92
|
+
`jobs.wait` would:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
done = client.skin.analyze(asset_id=source.id, concerns=["acne"], wait=True)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Skin Analysis
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from morphiaone import SkinAnalysisOutput
|
|
102
|
+
|
|
103
|
+
job = client.skin.analyze(
|
|
104
|
+
asset_id=source.id,
|
|
105
|
+
concerns=["acne", "redness", "wrinkles"],
|
|
106
|
+
visualizations=[{"concern": "acne", "kind": "mask"}],
|
|
107
|
+
)
|
|
108
|
+
done = client.jobs.wait(job.id)
|
|
109
|
+
if isinstance(done.output, SkinAnalysisOutput):
|
|
110
|
+
for item in done.output.concerns:
|
|
111
|
+
print(item.concern, item.status, item.score)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Skin Analysis is a cosmetic reading, not a medical one.
|
|
115
|
+
|
|
116
|
+
## Makeup Try-On
|
|
117
|
+
|
|
118
|
+
`products` is ordered, and the order is the layer order:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
from morphiaone import MakeupTryOnOutput
|
|
122
|
+
|
|
123
|
+
job = client.makeup.try_on(
|
|
124
|
+
asset_id=source.id,
|
|
125
|
+
products=[{"category": "lipstick", "color": "#C2185B", "opacity": 0.8}],
|
|
126
|
+
)
|
|
127
|
+
done = client.jobs.wait(job.id)
|
|
128
|
+
if isinstance(done.output, MakeupTryOnOutput):
|
|
129
|
+
image = client.assets.get_content(done.output.image)
|
|
130
|
+
with open("makeup.png", "wb") as out:
|
|
131
|
+
out.write(image.content)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Hair Color Try-On
|
|
135
|
+
|
|
136
|
+
Hair Color Try-On takes two Assets: the source image and a grayscale **hair mask** with
|
|
137
|
+
exactly the same dimensions. You produce the mask; the API does not segment hair, and it
|
|
138
|
+
changes color only, never style or length. The color is a two-stop gradient, `dark` and
|
|
139
|
+
`light`:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from morphiaone import HairTryOnOutput
|
|
143
|
+
|
|
144
|
+
hair_mask = client.assets.upload("hair-mask.png")
|
|
145
|
+
job = client.hair.try_on(
|
|
146
|
+
source_asset_id=source.id,
|
|
147
|
+
hair_mask_asset_id=hair_mask.id,
|
|
148
|
+
color_gradient={
|
|
149
|
+
"dark": {"color": "#3B1F16", "opacity": 0.9},
|
|
150
|
+
"light": {"color": "#C87943", "opacity": 0.8},
|
|
151
|
+
},
|
|
152
|
+
)
|
|
153
|
+
done = client.jobs.wait(job.id)
|
|
154
|
+
if isinstance(done.output, HairTryOnOutput):
|
|
155
|
+
image = client.assets.get_content(done.output.image)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Jobs
|
|
159
|
+
|
|
160
|
+
`jobs.wait` polls for you and honours the server's `Retry-After`. It is a convenience
|
|
161
|
+
over the job API, not a replacement for it:
|
|
162
|
+
|
|
163
|
+
- Persist `job.id` before you wait. It is how you resume after a restart, from another
|
|
164
|
+
process or from a queue consumer: `client.jobs.get(job_id)` or
|
|
165
|
+
`client.jobs.wait(job_id)`.
|
|
166
|
+
- A wait timeout stops *local* waiting only. It does not cancel the job on the server.
|
|
167
|
+
- `done.output` is `SkinAnalysisOutput`, `MakeupTryOnOutput`, `HairTryOnOutput`, or
|
|
168
|
+
`UnknownJobOutput` for an output type this SDK version does not know yet. An unknown
|
|
169
|
+
type never raises, so a newer API does not break an older SDK.
|
|
170
|
+
|
|
171
|
+
## Result Assets
|
|
172
|
+
|
|
173
|
+
A result names Assets, not bytes. Read them with `client.assets.get_content(...)`,
|
|
174
|
+
which authenticates with the client's own key. The read is built from the Asset id,
|
|
175
|
+
never from a URL in the response, so a response can never redirect your credential to
|
|
176
|
+
another host. Redirects are never followed.
|
|
177
|
+
|
|
178
|
+
## Usage
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
report = client.usage.get(start_date="2026-09-01", end_date="2026-10-01")
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
The window is half-open, in UTC. The report is counts, not an invoice.
|
|
185
|
+
|
|
186
|
+
## Errors
|
|
187
|
+
|
|
188
|
+
Every error the SDK raises derives from `MorphiaOneError`, and the tree is the
|
|
189
|
+
TypeScript SDK's, class for class. An HTTP error response is an `ApiError`, carrying the
|
|
190
|
+
HTTP `status`, the public error `code`, the human sentence as `message`, `details`, the
|
|
191
|
+
`request_id` and, when the server sent one, `retry_after` (seconds). On a local fault
|
|
192
|
+
(transport, protocol, deadline) those fields are `None`. Branch on `code`, never on the
|
|
193
|
+
message.
|
|
194
|
+
|
|
195
|
+
| Class | Derives from | When |
|
|
196
|
+
| --- | --- | --- |
|
|
197
|
+
| `MorphiaOneError` | `Exception` | every SDK error |
|
|
198
|
+
| `ApiError` | `MorphiaOneError` | any HTTP error response; the class for a non-5xx status with no narrower one |
|
|
199
|
+
| `AuthenticationError` | `ApiError` | 401: the key is missing, malformed or not accepted |
|
|
200
|
+
| `PermissionDeniedError` | `ApiError` | 403: the key is valid but refused; `code` says why. Not an `AuthenticationError`: reloading the key does not help |
|
|
201
|
+
| `InsufficientScopeError` | `PermissionDeniedError` | 403 `INSUFFICIENT_SCOPE`: the key's grant is too narrow |
|
|
202
|
+
| `ValidationError` | `ApiError` | 400, 415 or 422: the request is invalid |
|
|
203
|
+
| `PayloadTooLargeError` | `ValidationError` | 413: the image is too large; downscale, do not retry |
|
|
204
|
+
| `UnsupportedFeatureError` | `ValidationError` | 501: this deployment does not perform the requested feature |
|
|
205
|
+
| `InsufficientCreditsError` | `ApiError` | 402: no credit left; never retried |
|
|
206
|
+
| `NotFoundError` | `ApiError` | 404, or 410 for an expired input Asset: upload again |
|
|
207
|
+
| `ConflictError` | `ApiError` | 409: such as an idempotency key reused with a different body |
|
|
208
|
+
| `RateLimitError` | `ApiError` | 429: rate or quota limits, after the SDK's own retries |
|
|
209
|
+
| `ServerError` | `ApiError` | any other 5xx, after the SDK's own retries |
|
|
210
|
+
| `ServiceUnavailableError` | `ServerError` | 503: the service could not take the request, after retries |
|
|
211
|
+
| `TransportError` | `MorphiaOneError` | no response at all, after retries |
|
|
212
|
+
| `ProtocolError` | `MorphiaOneError` | a response the contract does not allow |
|
|
213
|
+
| `DeadlineExceeded` | `MorphiaOneError` | the call's own `timeout` elapsed |
|
|
214
|
+
| `ProcessingError` | `MorphiaOneError` | the job ran and failed; `job_id`, `code`, `details` and `error` are on it |
|
|
215
|
+
|
|
216
|
+
An upload over the client's `max_image_bytes`, an empty image or a malformed argument
|
|
217
|
+
is a `ValueError` raised before any request is sent (TypeScript throws `RangeError` for
|
|
218
|
+
the oversize upload and `TypeError` for the rest).
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
from morphiaone import MorphiaOneError, RateLimitError
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
done = client.jobs.wait(job.id)
|
|
225
|
+
except RateLimitError as error:
|
|
226
|
+
schedule_retry(after=error.retry_after)
|
|
227
|
+
except MorphiaOneError as error:
|
|
228
|
+
log.warning("MorphiaOne request failed: %s %s", error.code, error.request_id)
|
|
229
|
+
raise
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Log `request_id` on every failure. It is the one value support needs to find a request.
|
|
233
|
+
An error code this SDK version does not know still raises the class for its HTTP
|
|
234
|
+
status; it never crashes decoding.
|
|
235
|
+
|
|
236
|
+
## Timeouts and retries
|
|
237
|
+
|
|
238
|
+
- Timeouts are in seconds. `request_timeout` (default 10) bounds one call, its retries
|
|
239
|
+
included. `operation_timeout` (default 120, at least `request_timeout`) bounds
|
|
240
|
+
`jobs.wait` and a submit with `wait=True`, submit and polling together. Both are
|
|
241
|
+
client options, and every call accepts its own `timeout`, which replaces the relevant
|
|
242
|
+
default for that call: on a submit with `wait=True` it bounds the whole call.
|
|
243
|
+
- Polling never runs forever: the interval is the server's `Retry-After`, else about one
|
|
244
|
+
second, never less than 0.25 s and at most 60 s, plus a little positive jitter.
|
|
245
|
+
- Transport failures, `408`, `429`, `500`, `502`, `503` and `504` are retried (4
|
|
246
|
+
attempts in all by default). A server `Retry-After` is waited as named, never
|
|
247
|
+
shortened; when it is over 60 s, or longer than the call's remaining deadline, the
|
|
248
|
+
error is raised (with `retry_after` set) instead of retrying early. Without one, the
|
|
249
|
+
SDK backs off exponentially, each delay capped at `RetryPolicy.max_delay` (5 s).
|
|
250
|
+
A submit's retries resend the same `Idempotency-Key`, and a `409
|
|
251
|
+
IDEMPOTENCY_IN_PROGRESS` is retried too. A quota or credit `429` is not retried,
|
|
252
|
+
because waiting does not clear it. The TypeScript SDK retries exactly the same set.
|
|
253
|
+
|
|
254
|
+
## Idempotency
|
|
255
|
+
|
|
256
|
+
Every submit carries an `Idempotency-Key`. When you do not pass one, the SDK generates
|
|
257
|
+
one per call and reuses it on every retry of that call, so a retry never runs the work
|
|
258
|
+
twice.
|
|
259
|
+
|
|
260
|
+
That protects one call, not your operation. To make a logical request retry-safe across
|
|
261
|
+
process restarts, pass a key derived from your own order or request id:
|
|
262
|
+
|
|
263
|
+
```python
|
|
264
|
+
job = client.skin.analyze(
|
|
265
|
+
asset_id=source.id,
|
|
266
|
+
concerns=["acne"],
|
|
267
|
+
idempotency_key=f"order-{order_id}-skin",
|
|
268
|
+
)
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Reusing a key with a different request is a `ConflictError`. Asset uploads carry no
|
|
272
|
+
key: a retried upload can leave an unused Asset behind, which expires on its own.
|
|
273
|
+
|
|
274
|
+
## Privacy
|
|
275
|
+
|
|
276
|
+
The SDK sends no telemetry of its own. It never logs your API key, the images you upload
|
|
277
|
+
or their content.
|
|
278
|
+
|
|
279
|
+
## Versioning
|
|
280
|
+
|
|
281
|
+
`/v1` is the HTTP API major. The SDK has its own SemVer: while it is 0.x, a minor release
|
|
282
|
+
may break. The SDK will reach 1.0 only once the V1 contract is released.
|
|
283
|
+
|
|
284
|
+
## Support and security
|
|
285
|
+
|
|
286
|
+
Full documentation is coming with the public release. For support, include the
|
|
287
|
+
`request_id` from the error. Report security issues via your MorphiaOne account
|
|
288
|
+
contact; do not put API keys or images in a report.
|
|
289
|
+
|
|
290
|
+
## License
|
|
291
|
+
|
|
292
|
+
MIT.
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# MorphiaOne Python SDK
|
|
2
|
+
|
|
3
|
+
`morphiaone` is the Python client for the MorphiaOne V1 API: Skin Analysis, Makeup
|
|
4
|
+
Try-On and Hair Color Try-On, plus the Assets, Jobs and Usage calls they rest on.
|
|
5
|
+
|
|
6
|
+
> **Developer Preview.** The V1 API contract is still a draft, and this SDK may change
|
|
7
|
+
> before 1.0, including in ways that break code written against it. Pin an exact
|
|
8
|
+
> version. This package is not published yet: until the first release, the install
|
|
9
|
+
> command below resolves nothing.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install morphiaone
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Requires Python 3.11 or newer. One runtime dependency: `httpx`.
|
|
18
|
+
|
|
19
|
+
## Authenticate
|
|
20
|
+
|
|
21
|
+
Keep your API key in the environment of a server process, and read it from there:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
export MORPHIAONE_API_KEY="<your API key>"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
import os
|
|
29
|
+
|
|
30
|
+
from morphiaone import MorphiaOne
|
|
31
|
+
|
|
32
|
+
client = MorphiaOne(
|
|
33
|
+
api_key=os.environ["MORPHIAONE_API_KEY"],
|
|
34
|
+
base_url="<your API base URL>",
|
|
35
|
+
)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Use the client as a context manager (`with MorphiaOne(...) as client:`) or call
|
|
39
|
+
`client.close()` when you are done. `AsyncMorphiaOne` has the same namespaces, with
|
|
40
|
+
every call a coroutine.
|
|
41
|
+
|
|
42
|
+
**Server-side only.** Your API key authorizes spending on your account. Never ship it in
|
|
43
|
+
a browser bundle, a mobile app or a chatbot client; call MorphiaOne from your own
|
|
44
|
+
backend and hand your front end only the result.
|
|
45
|
+
|
|
46
|
+
## How a request works
|
|
47
|
+
|
|
48
|
+
Every product call follows the same three steps:
|
|
49
|
+
|
|
50
|
+
1. Upload each input image once as an **Asset**.
|
|
51
|
+
2. Submit a product request that references the Asset ids. The API answers with a
|
|
52
|
+
**Job**; the work runs asynchronously.
|
|
53
|
+
3. Wait for the Job, or poll it, and read its result Assets.
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
source = client.assets.upload("face.jpg") # a str or Path, or raw bytes
|
|
57
|
+
job = client.skin.analyze(asset_id=source.id, concerns=["acne", "redness"])
|
|
58
|
+
done = client.jobs.wait(job.id) # raises ProcessingError if it failed
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`assets.upload(image, *, media_type=None, timeout=None)` infers the media type
|
|
62
|
+
(`image/jpeg`, `image/png` or `image/webp`) from the file suffix or the leading bytes
|
|
63
|
+
when omitted.
|
|
64
|
+
|
|
65
|
+
Every submit also accepts the keyword-only `idempotency_key`, `timeout` and `wait`.
|
|
66
|
+
With `wait=True` the call polls to the succeeded job before returning, exactly as
|
|
67
|
+
`jobs.wait` would:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
done = client.skin.analyze(asset_id=source.id, concerns=["acne"], wait=True)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Skin Analysis
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from morphiaone import SkinAnalysisOutput
|
|
77
|
+
|
|
78
|
+
job = client.skin.analyze(
|
|
79
|
+
asset_id=source.id,
|
|
80
|
+
concerns=["acne", "redness", "wrinkles"],
|
|
81
|
+
visualizations=[{"concern": "acne", "kind": "mask"}],
|
|
82
|
+
)
|
|
83
|
+
done = client.jobs.wait(job.id)
|
|
84
|
+
if isinstance(done.output, SkinAnalysisOutput):
|
|
85
|
+
for item in done.output.concerns:
|
|
86
|
+
print(item.concern, item.status, item.score)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Skin Analysis is a cosmetic reading, not a medical one.
|
|
90
|
+
|
|
91
|
+
## Makeup Try-On
|
|
92
|
+
|
|
93
|
+
`products` is ordered, and the order is the layer order:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from morphiaone import MakeupTryOnOutput
|
|
97
|
+
|
|
98
|
+
job = client.makeup.try_on(
|
|
99
|
+
asset_id=source.id,
|
|
100
|
+
products=[{"category": "lipstick", "color": "#C2185B", "opacity": 0.8}],
|
|
101
|
+
)
|
|
102
|
+
done = client.jobs.wait(job.id)
|
|
103
|
+
if isinstance(done.output, MakeupTryOnOutput):
|
|
104
|
+
image = client.assets.get_content(done.output.image)
|
|
105
|
+
with open("makeup.png", "wb") as out:
|
|
106
|
+
out.write(image.content)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Hair Color Try-On
|
|
110
|
+
|
|
111
|
+
Hair Color Try-On takes two Assets: the source image and a grayscale **hair mask** with
|
|
112
|
+
exactly the same dimensions. You produce the mask; the API does not segment hair, and it
|
|
113
|
+
changes color only, never style or length. The color is a two-stop gradient, `dark` and
|
|
114
|
+
`light`:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
from morphiaone import HairTryOnOutput
|
|
118
|
+
|
|
119
|
+
hair_mask = client.assets.upload("hair-mask.png")
|
|
120
|
+
job = client.hair.try_on(
|
|
121
|
+
source_asset_id=source.id,
|
|
122
|
+
hair_mask_asset_id=hair_mask.id,
|
|
123
|
+
color_gradient={
|
|
124
|
+
"dark": {"color": "#3B1F16", "opacity": 0.9},
|
|
125
|
+
"light": {"color": "#C87943", "opacity": 0.8},
|
|
126
|
+
},
|
|
127
|
+
)
|
|
128
|
+
done = client.jobs.wait(job.id)
|
|
129
|
+
if isinstance(done.output, HairTryOnOutput):
|
|
130
|
+
image = client.assets.get_content(done.output.image)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Jobs
|
|
134
|
+
|
|
135
|
+
`jobs.wait` polls for you and honours the server's `Retry-After`. It is a convenience
|
|
136
|
+
over the job API, not a replacement for it:
|
|
137
|
+
|
|
138
|
+
- Persist `job.id` before you wait. It is how you resume after a restart, from another
|
|
139
|
+
process or from a queue consumer: `client.jobs.get(job_id)` or
|
|
140
|
+
`client.jobs.wait(job_id)`.
|
|
141
|
+
- A wait timeout stops *local* waiting only. It does not cancel the job on the server.
|
|
142
|
+
- `done.output` is `SkinAnalysisOutput`, `MakeupTryOnOutput`, `HairTryOnOutput`, or
|
|
143
|
+
`UnknownJobOutput` for an output type this SDK version does not know yet. An unknown
|
|
144
|
+
type never raises, so a newer API does not break an older SDK.
|
|
145
|
+
|
|
146
|
+
## Result Assets
|
|
147
|
+
|
|
148
|
+
A result names Assets, not bytes. Read them with `client.assets.get_content(...)`,
|
|
149
|
+
which authenticates with the client's own key. The read is built from the Asset id,
|
|
150
|
+
never from a URL in the response, so a response can never redirect your credential to
|
|
151
|
+
another host. Redirects are never followed.
|
|
152
|
+
|
|
153
|
+
## Usage
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
report = client.usage.get(start_date="2026-09-01", end_date="2026-10-01")
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The window is half-open, in UTC. The report is counts, not an invoice.
|
|
160
|
+
|
|
161
|
+
## Errors
|
|
162
|
+
|
|
163
|
+
Every error the SDK raises derives from `MorphiaOneError`, and the tree is the
|
|
164
|
+
TypeScript SDK's, class for class. An HTTP error response is an `ApiError`, carrying the
|
|
165
|
+
HTTP `status`, the public error `code`, the human sentence as `message`, `details`, the
|
|
166
|
+
`request_id` and, when the server sent one, `retry_after` (seconds). On a local fault
|
|
167
|
+
(transport, protocol, deadline) those fields are `None`. Branch on `code`, never on the
|
|
168
|
+
message.
|
|
169
|
+
|
|
170
|
+
| Class | Derives from | When |
|
|
171
|
+
| --- | --- | --- |
|
|
172
|
+
| `MorphiaOneError` | `Exception` | every SDK error |
|
|
173
|
+
| `ApiError` | `MorphiaOneError` | any HTTP error response; the class for a non-5xx status with no narrower one |
|
|
174
|
+
| `AuthenticationError` | `ApiError` | 401: the key is missing, malformed or not accepted |
|
|
175
|
+
| `PermissionDeniedError` | `ApiError` | 403: the key is valid but refused; `code` says why. Not an `AuthenticationError`: reloading the key does not help |
|
|
176
|
+
| `InsufficientScopeError` | `PermissionDeniedError` | 403 `INSUFFICIENT_SCOPE`: the key's grant is too narrow |
|
|
177
|
+
| `ValidationError` | `ApiError` | 400, 415 or 422: the request is invalid |
|
|
178
|
+
| `PayloadTooLargeError` | `ValidationError` | 413: the image is too large; downscale, do not retry |
|
|
179
|
+
| `UnsupportedFeatureError` | `ValidationError` | 501: this deployment does not perform the requested feature |
|
|
180
|
+
| `InsufficientCreditsError` | `ApiError` | 402: no credit left; never retried |
|
|
181
|
+
| `NotFoundError` | `ApiError` | 404, or 410 for an expired input Asset: upload again |
|
|
182
|
+
| `ConflictError` | `ApiError` | 409: such as an idempotency key reused with a different body |
|
|
183
|
+
| `RateLimitError` | `ApiError` | 429: rate or quota limits, after the SDK's own retries |
|
|
184
|
+
| `ServerError` | `ApiError` | any other 5xx, after the SDK's own retries |
|
|
185
|
+
| `ServiceUnavailableError` | `ServerError` | 503: the service could not take the request, after retries |
|
|
186
|
+
| `TransportError` | `MorphiaOneError` | no response at all, after retries |
|
|
187
|
+
| `ProtocolError` | `MorphiaOneError` | a response the contract does not allow |
|
|
188
|
+
| `DeadlineExceeded` | `MorphiaOneError` | the call's own `timeout` elapsed |
|
|
189
|
+
| `ProcessingError` | `MorphiaOneError` | the job ran and failed; `job_id`, `code`, `details` and `error` are on it |
|
|
190
|
+
|
|
191
|
+
An upload over the client's `max_image_bytes`, an empty image or a malformed argument
|
|
192
|
+
is a `ValueError` raised before any request is sent (TypeScript throws `RangeError` for
|
|
193
|
+
the oversize upload and `TypeError` for the rest).
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
from morphiaone import MorphiaOneError, RateLimitError
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
done = client.jobs.wait(job.id)
|
|
200
|
+
except RateLimitError as error:
|
|
201
|
+
schedule_retry(after=error.retry_after)
|
|
202
|
+
except MorphiaOneError as error:
|
|
203
|
+
log.warning("MorphiaOne request failed: %s %s", error.code, error.request_id)
|
|
204
|
+
raise
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Log `request_id` on every failure. It is the one value support needs to find a request.
|
|
208
|
+
An error code this SDK version does not know still raises the class for its HTTP
|
|
209
|
+
status; it never crashes decoding.
|
|
210
|
+
|
|
211
|
+
## Timeouts and retries
|
|
212
|
+
|
|
213
|
+
- Timeouts are in seconds. `request_timeout` (default 10) bounds one call, its retries
|
|
214
|
+
included. `operation_timeout` (default 120, at least `request_timeout`) bounds
|
|
215
|
+
`jobs.wait` and a submit with `wait=True`, submit and polling together. Both are
|
|
216
|
+
client options, and every call accepts its own `timeout`, which replaces the relevant
|
|
217
|
+
default for that call: on a submit with `wait=True` it bounds the whole call.
|
|
218
|
+
- Polling never runs forever: the interval is the server's `Retry-After`, else about one
|
|
219
|
+
second, never less than 0.25 s and at most 60 s, plus a little positive jitter.
|
|
220
|
+
- Transport failures, `408`, `429`, `500`, `502`, `503` and `504` are retried (4
|
|
221
|
+
attempts in all by default). A server `Retry-After` is waited as named, never
|
|
222
|
+
shortened; when it is over 60 s, or longer than the call's remaining deadline, the
|
|
223
|
+
error is raised (with `retry_after` set) instead of retrying early. Without one, the
|
|
224
|
+
SDK backs off exponentially, each delay capped at `RetryPolicy.max_delay` (5 s).
|
|
225
|
+
A submit's retries resend the same `Idempotency-Key`, and a `409
|
|
226
|
+
IDEMPOTENCY_IN_PROGRESS` is retried too. A quota or credit `429` is not retried,
|
|
227
|
+
because waiting does not clear it. The TypeScript SDK retries exactly the same set.
|
|
228
|
+
|
|
229
|
+
## Idempotency
|
|
230
|
+
|
|
231
|
+
Every submit carries an `Idempotency-Key`. When you do not pass one, the SDK generates
|
|
232
|
+
one per call and reuses it on every retry of that call, so a retry never runs the work
|
|
233
|
+
twice.
|
|
234
|
+
|
|
235
|
+
That protects one call, not your operation. To make a logical request retry-safe across
|
|
236
|
+
process restarts, pass a key derived from your own order or request id:
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
job = client.skin.analyze(
|
|
240
|
+
asset_id=source.id,
|
|
241
|
+
concerns=["acne"],
|
|
242
|
+
idempotency_key=f"order-{order_id}-skin",
|
|
243
|
+
)
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Reusing a key with a different request is a `ConflictError`. Asset uploads carry no
|
|
247
|
+
key: a retried upload can leave an unused Asset behind, which expires on its own.
|
|
248
|
+
|
|
249
|
+
## Privacy
|
|
250
|
+
|
|
251
|
+
The SDK sends no telemetry of its own. It never logs your API key, the images you upload
|
|
252
|
+
or their content.
|
|
253
|
+
|
|
254
|
+
## Versioning
|
|
255
|
+
|
|
256
|
+
`/v1` is the HTTP API major. The SDK has its own SemVer: while it is 0.x, a minor release
|
|
257
|
+
may break. The SDK will reach 1.0 only once the V1 contract is released.
|
|
258
|
+
|
|
259
|
+
## Support and security
|
|
260
|
+
|
|
261
|
+
Full documentation is coming with the public release. For support, include the
|
|
262
|
+
`request_id` from the error. Report security issues via your MorphiaOne account
|
|
263
|
+
contact; do not put API keys or images in a report.
|
|
264
|
+
|
|
265
|
+
## License
|
|
266
|
+
|
|
267
|
+
MIT.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""MorphiaOne SDK (Developer Preview): the V1 API client, its errors and retry policy.
|
|
2
|
+
|
|
3
|
+
from morphiaone import MorphiaOne
|
|
4
|
+
|
|
5
|
+
client = MorphiaOne(api_key=..., base_url=...)
|
|
6
|
+
asset = client.assets.upload("face.jpg")
|
|
7
|
+
job = client.skin.analyze(asset.id, ["acne"], wait=True)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .client import RetryPolicy
|
|
11
|
+
from .errors import (
|
|
12
|
+
ApiError,
|
|
13
|
+
AuthenticationError,
|
|
14
|
+
ConflictError,
|
|
15
|
+
DeadlineExceeded,
|
|
16
|
+
InsufficientCreditsError,
|
|
17
|
+
InsufficientScopeError,
|
|
18
|
+
MorphiaOneError,
|
|
19
|
+
NotFoundError,
|
|
20
|
+
PayloadTooLargeError,
|
|
21
|
+
PermissionDeniedError,
|
|
22
|
+
ProcessingError,
|
|
23
|
+
ProtocolError,
|
|
24
|
+
RateLimitError,
|
|
25
|
+
ServerError,
|
|
26
|
+
ServiceUnavailableError,
|
|
27
|
+
TransportError,
|
|
28
|
+
UnsupportedFeatureError,
|
|
29
|
+
ValidationError,
|
|
30
|
+
)
|
|
31
|
+
from .models import JobError, JobStatus
|
|
32
|
+
from .v1 import (
|
|
33
|
+
Asset,
|
|
34
|
+
AssetContent,
|
|
35
|
+
AsyncMorphiaOne,
|
|
36
|
+
HairAppliedColorGradient,
|
|
37
|
+
HairAppliedColorStop,
|
|
38
|
+
HairTryOnApplied,
|
|
39
|
+
HairTryOnOutput,
|
|
40
|
+
Job,
|
|
41
|
+
JobInputRef,
|
|
42
|
+
JobOutput,
|
|
43
|
+
MakeupTryOnOutput,
|
|
44
|
+
MorphiaOne,
|
|
45
|
+
ResultAssetRef,
|
|
46
|
+
SkinAnalysisOutput,
|
|
47
|
+
SkinConcernResult,
|
|
48
|
+
UnknownJobOutput,
|
|
49
|
+
UsageReport,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
__all__ = [
|
|
53
|
+
"ApiError",
|
|
54
|
+
"Asset",
|
|
55
|
+
"AssetContent",
|
|
56
|
+
"AsyncMorphiaOne",
|
|
57
|
+
"AuthenticationError",
|
|
58
|
+
"ConflictError",
|
|
59
|
+
"DeadlineExceeded",
|
|
60
|
+
"HairAppliedColorGradient",
|
|
61
|
+
"HairAppliedColorStop",
|
|
62
|
+
"HairTryOnApplied",
|
|
63
|
+
"HairTryOnOutput",
|
|
64
|
+
"InsufficientCreditsError",
|
|
65
|
+
"InsufficientScopeError",
|
|
66
|
+
"Job",
|
|
67
|
+
"JobError",
|
|
68
|
+
"JobInputRef",
|
|
69
|
+
"JobOutput",
|
|
70
|
+
"JobStatus",
|
|
71
|
+
"MakeupTryOnOutput",
|
|
72
|
+
"MorphiaOne",
|
|
73
|
+
"MorphiaOneError",
|
|
74
|
+
"NotFoundError",
|
|
75
|
+
"PayloadTooLargeError",
|
|
76
|
+
"PermissionDeniedError",
|
|
77
|
+
"ProcessingError",
|
|
78
|
+
"ProtocolError",
|
|
79
|
+
"RateLimitError",
|
|
80
|
+
"ResultAssetRef",
|
|
81
|
+
"RetryPolicy",
|
|
82
|
+
"ServerError",
|
|
83
|
+
"ServiceUnavailableError",
|
|
84
|
+
"SkinAnalysisOutput",
|
|
85
|
+
"SkinConcernResult",
|
|
86
|
+
"TransportError",
|
|
87
|
+
"UnknownJobOutput",
|
|
88
|
+
"UnsupportedFeatureError",
|
|
89
|
+
"UsageReport",
|
|
90
|
+
"ValidationError",
|
|
91
|
+
]
|