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.
@@ -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
+ ]