floorplan-api 0.5.0__tar.gz → 0.6.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.
- {floorplan_api-0.5.0 → floorplan_api-0.6.0}/CHANGELOG.md +41 -0
- {floorplan_api-0.5.0 → floorplan_api-0.6.0}/PKG-INFO +125 -59
- {floorplan_api-0.5.0 → floorplan_api-0.6.0}/README.md +123 -57
- {floorplan_api-0.5.0 → floorplan_api-0.6.0}/examples/async_client.py +7 -3
- {floorplan_api-0.5.0 → floorplan_api-0.6.0}/examples/async_job.py +6 -5
- {floorplan_api-0.5.0 → floorplan_api-0.6.0}/examples/local_dev.py +4 -3
- {floorplan_api-0.5.0 → floorplan_api-0.6.0}/examples/quickstart.py +16 -4
- {floorplan_api-0.5.0 → floorplan_api-0.6.0}/examples/sync_timeout_recovery.py +6 -5
- {floorplan_api-0.5.0 → floorplan_api-0.6.0}/pyproject.toml +1 -1
- {floorplan_api-0.5.0 → floorplan_api-0.6.0}/src/floorplan_api/__init__.py +9 -5
- {floorplan_api-0.5.0 → floorplan_api-0.6.0}/src/floorplan_api/_input.py +2 -2
- {floorplan_api-0.5.0 → floorplan_api-0.6.0}/src/floorplan_api/_transport.py +26 -6
- {floorplan_api-0.5.0 → floorplan_api-0.6.0}/src/floorplan_api/_version.py +1 -1
- {floorplan_api-0.5.0 → floorplan_api-0.6.0}/src/floorplan_api/async_client.py +41 -23
- {floorplan_api-0.5.0 → floorplan_api-0.6.0}/src/floorplan_api/client.py +80 -43
- {floorplan_api-0.5.0 → floorplan_api-0.6.0}/src/floorplan_api/exceptions.py +3 -3
- floorplan_api-0.6.0/src/floorplan_api/models.py +517 -0
- {floorplan_api-0.5.0 → floorplan_api-0.6.0}/tests/conftest.py +64 -4
- {floorplan_api-0.5.0 → floorplan_api-0.6.0}/tests/test_async_client.py +116 -26
- {floorplan_api-0.5.0 → floorplan_api-0.6.0}/tests/test_client.py +228 -63
- floorplan_api-0.6.0/tests/test_models.py +310 -0
- floorplan_api-0.5.0/src/floorplan_api/models.py +0 -241
- floorplan_api-0.5.0/tests/test_models.py +0 -143
- {floorplan_api-0.5.0 → floorplan_api-0.6.0}/.gitignore +0 -0
- {floorplan_api-0.5.0 → floorplan_api-0.6.0}/LICENSE +0 -0
- {floorplan_api-0.5.0 → floorplan_api-0.6.0}/src/floorplan_api/py.typed +0 -0
- {floorplan_api-0.5.0 → floorplan_api-0.6.0}/tests/__init__.py +0 -0
- {floorplan_api-0.5.0 → floorplan_api-0.6.0}/tests/test_input.py +0 -0
|
@@ -5,6 +5,47 @@ All notable changes to `floorplan-api` will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.6.0] - 2026-10-02
|
|
9
|
+
|
|
10
|
+
### Changed (breaking)
|
|
11
|
+
- **`extract`, `analyze` and `upload_then_extract` return a `Result`**
|
|
12
|
+
instead of `MaskBytes` (sync and async clients). One call now gives the
|
|
13
|
+
wall mask and the structured result:
|
|
14
|
+
- `result.wall_mask`: the PNG wall mask, still a `MaskBytes` (`bytes`
|
|
15
|
+
with `width`, `height`, `job_id`, `request_id`, `mode`, `page_size_pt`,
|
|
16
|
+
`pdf_scale`, `pdf_dpi`). Code that wrote the return value to a file now
|
|
17
|
+
writes `result.wall_mask`.
|
|
18
|
+
- `result.room_mask`: PNG bytes where each pixel holds a room `id`
|
|
19
|
+
(0 = no room), or `None`.
|
|
20
|
+
- `result.detections`: doors and windows as `Detection` (`label`,
|
|
21
|
+
`confidence`, `polygon`, `center`, `size`); `result.doors` and
|
|
22
|
+
`result.windows` filter by label. `label` is the JSON `class` key.
|
|
23
|
+
- `result.rooms`: `Room` objects (`id`, `score`, `area`, `polygon`, plus
|
|
24
|
+
`outline` and `holes`).
|
|
25
|
+
- `result.counts`, `width`, `height`, `page`, `dpi`, `min_room_score`,
|
|
26
|
+
`job_id`, `request_id`, `mode`, `page_size_pt`, and `raw` (the JSON
|
|
27
|
+
response without the masks).
|
|
28
|
+
|
|
29
|
+
All coordinates are pixels on the wall mask's grid.
|
|
30
|
+
- **`download_mask(job_id)` is replaced by `get_result(job_id)`**, which
|
|
31
|
+
returns the same `Result` for a completed job. There is no alias. After a
|
|
32
|
+
`TimeoutError` with `job_id`, recover with `wait_for_job` + `get_result`.
|
|
33
|
+
- `Job.result.result_url` now points at the job's result
|
|
34
|
+
(`/v1/jobs/{job_id}/result`).
|
|
35
|
+
- `MaskBytes.from_response` is replaced by `MaskBytes.from_result`: mask
|
|
36
|
+
`width` / `height` now come from the response body; the `X-Mask-Width` /
|
|
37
|
+
`X-Mask-Height` headers are no longer sent.
|
|
38
|
+
- A successful response without a usable wall mask raises `ServerError`.
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
- `Result`, `Detection` and `Room` are exported from `floorplan_api`.
|
|
42
|
+
- **`min_room_score=`** on `extract`, `analyze` and `analyze_async` (sync and
|
|
43
|
+
async clients): the room-score floor, a number from 0 to 0.9 (server
|
|
44
|
+
default 0.9). Every room carries a `score`: 1.0 for a fully enclosed room;
|
|
45
|
+
lower values for rooms the service had to close itself. Rooms scoring
|
|
46
|
+
below `min_room_score` are not returned. Values outside 0–0.9 raise
|
|
47
|
+
`InvalidRequestError` before any request.
|
|
48
|
+
|
|
8
49
|
## [0.5.0] - 2026-09-25
|
|
9
50
|
|
|
10
51
|
### Added
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: floorplan-api
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Official Python client for the Floor Plan API —
|
|
3
|
+
Version: 0.6.0
|
|
4
|
+
Summary: Official Python client for the Floor Plan API — walls, doors, windows and rooms from floor plan images and PDFs.
|
|
5
5
|
Project-URL: Homepage, https://floorplanapi.com
|
|
6
6
|
Project-URL: Documentation, https://floorplanapi.com/docs
|
|
7
7
|
Project-URL: Changelog, https://floorplanapi.com/docs#python-changelog
|
|
@@ -44,11 +44,12 @@ Description-Content-Type: text/markdown
|
|
|
44
44
|
# floorplan-api
|
|
45
45
|
|
|
46
46
|
Official Python client for the [Floor Plan API](https://floorplanapi.com):
|
|
47
|
-
upload a floor plan, get back
|
|
47
|
+
upload a floor plan, get back its walls, doors, windows and rooms.
|
|
48
48
|
|
|
49
|
-
* **
|
|
50
|
-
|
|
51
|
-
everything else
|
|
49
|
+
* **One call, everything.** PNG, JPEG, WEBP, or one page of a PDF in; a
|
|
50
|
+
`Result` out: the wall mask as PNG bytes at the input's resolution
|
|
51
|
+
(`255` = wall, `0` = everything else), the doors and windows as oriented
|
|
52
|
+
boxes, and the rooms as polygons plus a room mask.
|
|
52
53
|
* **Two clients, one surface.** `Client` (synchronous, on `requests`) and
|
|
53
54
|
`AsyncClient` (asyncio, on `httpx`).
|
|
54
55
|
* **Retries that don't double-bill.** Transient failures are retried with
|
|
@@ -72,20 +73,83 @@ Python 3.9+.
|
|
|
72
73
|
from floorplan_api import Client
|
|
73
74
|
|
|
74
75
|
client = Client(api_key="fp_test_...") # or set FLOORPLAN_API_KEY
|
|
75
|
-
|
|
76
|
-
|
|
76
|
+
result = client.extract("plans/floor1.png") # PNG, JPEG or WEBP
|
|
77
|
+
result = client.extract("plans/set.pdf", page=3) # PDF: pick the page
|
|
77
78
|
|
|
78
79
|
with open("walls.png", "wb") as fh:
|
|
79
|
-
fh.write(mask
|
|
80
|
+
fh.write(result.wall_mask) # the PNG wall mask
|
|
80
81
|
|
|
81
|
-
print(
|
|
82
|
+
print(result.width, result.height, result.job_id)
|
|
83
|
+
print(len(result.rooms), "rooms,", len(result.doors), "doors,", len(result.windows), "windows")
|
|
82
84
|
```
|
|
83
85
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
`
|
|
87
|
-
|
|
88
|
-
|
|
86
|
+
## The result
|
|
87
|
+
|
|
88
|
+
`extract()`, `analyze()`, `upload_then_extract()` and `get_result()` return a
|
|
89
|
+
`Result`. Everything in it shares one pixel grid: the decoded input image,
|
|
90
|
+
or the rendered PDF page. The origin is the top-left corner.
|
|
91
|
+
|
|
92
|
+
| Attribute | What it is |
|
|
93
|
+
| --- | --- |
|
|
94
|
+
| `wall_mask` | The wall mask: PNG bytes, 8-bit single-channel, `255` = wall, `0` = everything else. A `MaskBytes` (see below). |
|
|
95
|
+
| `room_mask` | PNG bytes where each pixel holds the `id` of its room (`0` = no room); 8-bit, or 16-bit when there are 255 or more rooms. `None` when the service produced no rooms output. |
|
|
96
|
+
| `detections` | Doors and windows, a list of `Detection`. `result.doors` and `result.windows` are the same list filtered by label. |
|
|
97
|
+
| `rooms` | A list of `Room`. |
|
|
98
|
+
| `counts` | Items per kind, e.g. `{"door": 12, "window": 11, "room": 11}`. May be empty. |
|
|
99
|
+
| `width`, `height`, `size` | The pixel grid. |
|
|
100
|
+
| `page`, `dpi` | The PDF page that was processed and the resolution it was rendered at. `dpi` is `None` for raster input. |
|
|
101
|
+
| `min_room_score` | The room-score floor that was applied (or `None`). |
|
|
102
|
+
| `job_id`, `request_id`, `mode` | The job, the request ID for support tickets, and `live`/`test`. |
|
|
103
|
+
| `page_size_pt` | For a PDF you passed as a file: the page size in PDF points (see below). |
|
|
104
|
+
| `raw` | The decoded JSON response without the two base64 masks, ready for `json.dump`. |
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
for door in result.doors:
|
|
108
|
+
door.label # "door" or "window" (the JSON key is `class`)
|
|
109
|
+
door.confidence # 0 to 1
|
|
110
|
+
door.polygon # the 4 corners of the oriented box: [(x, y), ...]
|
|
111
|
+
door.center # (x, y)
|
|
112
|
+
door.size # (width, height) along the box's own axes
|
|
113
|
+
|
|
114
|
+
for room in result.rooms:
|
|
115
|
+
room.id # 1, 2, ...: the pixel value of this room in result.room_mask
|
|
116
|
+
room.score # 1.0 for a fully enclosed room; lower values for rooms
|
|
117
|
+
# the service had to close itself
|
|
118
|
+
room.area # in square pixels
|
|
119
|
+
room.outline # the outer ring: [(x, y), ...], no closing point
|
|
120
|
+
room.holes # rings cut out of the room
|
|
121
|
+
room.polygon # [outline, *holes]
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`result.wall_mask` is a `MaskBytes`, a `bytes` subclass. Write it, hash it,
|
|
125
|
+
hand it to any image library as usual; it also carries `width`, `height`,
|
|
126
|
+
`job_id`, `request_id`, `mode` and, for a PDF, `page_size_pt` and
|
|
127
|
+
`pdf_scale`. The client never post-processes either mask.
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
import io, json
|
|
131
|
+
import numpy as np
|
|
132
|
+
from PIL import Image
|
|
133
|
+
|
|
134
|
+
walls = np.array(Image.open(io.BytesIO(result.wall_mask))) # 0 / 255
|
|
135
|
+
if result.room_mask is not None:
|
|
136
|
+
room_ids = np.array(Image.open(io.BytesIO(result.room_mask))) # 0 = no room
|
|
137
|
+
kitchen = room_ids == result.rooms[0].id
|
|
138
|
+
|
|
139
|
+
json.dump(result.raw, open("plan.json", "w")) # everything but the masks
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Choosing which rooms you get
|
|
143
|
+
|
|
144
|
+
Every room carries a `score`: 1.0 for a fully enclosed room; lower values
|
|
145
|
+
for rooms the service had to close itself. `min_room_score=` (0 to 0.9,
|
|
146
|
+
server default 0.9) sets the floor: rooms scoring below it are not returned.
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
result = client.extract("plan.png", min_room_score=0.5) # a lower floor returns more rooms
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Inputs
|
|
89
153
|
|
|
90
154
|
Any of these inputs work:
|
|
91
155
|
|
|
@@ -120,35 +184,38 @@ does not slice, then call `extract(upload_key=..., page=N)`.
|
|
|
120
184
|
A PDF is processed one page at a time. Pass `page=` (1-based; default 1) to
|
|
121
185
|
say which. Page errors (out of range, password-protected, unreadable) are
|
|
122
186
|
raised as `InvalidRequestError` before anything is sent. The API rasterises
|
|
123
|
-
the page at 200 DPI (longest edge capped at 8192 px) and returns the
|
|
124
|
-
|
|
187
|
+
the page at 200 DPI (longest edge capped at 8192 px) and returns the result
|
|
188
|
+
on that grid; read it from `result.width`, `result.height` and `result.dpi`.
|
|
125
189
|
|
|
126
190
|
```python
|
|
127
|
-
|
|
191
|
+
result = client.extract("set.pdf", page=3)
|
|
128
192
|
key = client.upload("set.pdf", page=3) # the stored object is page 3 only
|
|
129
|
-
|
|
193
|
+
result = client.extract(upload_key=key) # ... so no page is needed here
|
|
130
194
|
```
|
|
131
195
|
|
|
132
196
|
`page` on a raster input is rejected unless it is 1. When you submit by
|
|
133
197
|
`upload_key` for an object you stored yourself (raw REST), `page=` is sent
|
|
134
198
|
to the server, which renders that page of the stored file.
|
|
135
199
|
|
|
136
|
-
**Mapping the
|
|
137
|
-
page's pixel grid, not in PDF points. The client
|
|
138
|
-
box (honouring `/Rotate`) before upload and
|
|
200
|
+
**Mapping the result back to PDF coordinates.** The masks, detections and
|
|
201
|
+
rooms are on the rendered page's pixel grid, not in PDF points. The client
|
|
202
|
+
measures the page's crop box (honouring `/Rotate`) before upload and
|
|
203
|
+
attaches it, so:
|
|
139
204
|
|
|
140
205
|
```python
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
mask
|
|
206
|
+
result = client.extract("set.pdf", page=3)
|
|
207
|
+
result.page_size_pt # (1728.0, 2592.0) -> a 24 x 36 in sheet
|
|
208
|
+
mask = result.wall_mask
|
|
209
|
+
mask.pdf_scale # pixels per PDF point: mask.width / page width
|
|
144
210
|
mask.pdf_dpi # the DPI actually used: 200, or less if the page hit the 8192 px cap
|
|
145
211
|
|
|
146
|
-
x_px = x_pt * mask.pdf_scale # PDF point ->
|
|
212
|
+
x_px = x_pt * mask.pdf_scale # PDF point -> pixel (origin: top-left of the render)
|
|
213
|
+
outline_pt = [(x / mask.pdf_scale, y / mask.pdf_scale) for x, y in result.rooms[0].outline]
|
|
147
214
|
```
|
|
148
215
|
|
|
149
216
|
`floorplan_api.pdf_page_size(data, page)` gives the same `(width, height)`
|
|
150
|
-
in points for any PDF, for example to compute the scale for a
|
|
151
|
-
fetched later with `
|
|
217
|
+
in points for any PDF, for example to compute the scale for a result you
|
|
218
|
+
fetched later with `get_result()`, which has no `page_size_pt`.
|
|
152
219
|
|
|
153
220
|
### Async
|
|
154
221
|
|
|
@@ -158,12 +225,12 @@ from floorplan_api import AsyncClient
|
|
|
158
225
|
|
|
159
226
|
async def main() -> None:
|
|
160
227
|
async with AsyncClient() as client:
|
|
161
|
-
|
|
228
|
+
results = await asyncio.gather(
|
|
162
229
|
client.extract("a.pdf"),
|
|
163
230
|
client.extract("b.png"),
|
|
164
231
|
)
|
|
165
|
-
for
|
|
166
|
-
print(
|
|
232
|
+
for r in results:
|
|
233
|
+
print(r.size, len(r.rooms))
|
|
167
234
|
|
|
168
235
|
asyncio.run(main())
|
|
169
236
|
```
|
|
@@ -179,11 +246,11 @@ presigned upload for anything bigger, so the API server never holds the
|
|
|
179
246
|
bytes:
|
|
180
247
|
|
|
181
248
|
```python
|
|
182
|
-
|
|
249
|
+
result = client.upload_then_extract("big_floor_plan.pdf", page=2)
|
|
183
250
|
|
|
184
251
|
# or step by step:
|
|
185
252
|
key = client.upload("big_floor_plan.pdf", page=2) # PUT straight to object storage
|
|
186
|
-
|
|
253
|
+
result = client.extract(upload_key=key) # submit by storage key
|
|
187
254
|
```
|
|
188
255
|
|
|
189
256
|
The size check happens after the page is cut out, so a large multi-page PDF
|
|
@@ -193,26 +260,24 @@ whose selected page is small still takes the inline path.
|
|
|
193
260
|
|
|
194
261
|
## What the API does with your file
|
|
195
262
|
|
|
196
|
-
|
|
197
|
-
|
|
263
|
+
The client never touches pixels. This is how the service reads your file
|
|
264
|
+
and what the result is aligned to:
|
|
198
265
|
|
|
199
|
-
* **Rasters
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
266
|
+
* **Rasters.** Alpha is dropped without compositing, so flatten transparent
|
|
267
|
+
PNGs onto white first; grayscale is expanded to three channels; 16-bit
|
|
268
|
+
depth becomes 8-bit; JPEG EXIF orientation is applied, so the result
|
|
269
|
+
aligns with the *displayed* orientation; ICC profiles are ignored. A
|
|
270
|
+
raster whose longer edge exceeds 8192 px is processed downscaled to that
|
|
271
|
+
bound and the result is mapped back to the input size, so it stays
|
|
272
|
+
pixel-aligned but carries less detail.
|
|
206
273
|
* **PDF pages** are rendered at 200 DPI onto white (transparent regions
|
|
207
274
|
composite onto white), reduced so the longer edge is at most 8192 px. All
|
|
208
|
-
content is rendered: linework, hatching, text, dimensions. The
|
|
209
|
-
the rendered
|
|
210
|
-
* **
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
*carved* convention: door and window openings are not wall, and walls are
|
|
215
|
-
as thick as the source linework.
|
|
275
|
+
content is rendered: linework, hatching, text, dimensions. The result is
|
|
276
|
+
on the rendered grid, reported in `width`/`height`/`dpi`.
|
|
277
|
+
* **Wall mask.** An 8-bit single-channel PNG with values exactly 0 and 255.
|
|
278
|
+
`255` is wall in the *carved* convention: door and window openings are
|
|
279
|
+
not wall, and walls are as thick as the source linework.
|
|
280
|
+
* **Doors, windows and rooms** are on the same pixel grid as the wall mask.
|
|
216
281
|
|
|
217
282
|
Limits you will meet: inline uploads 10 MB; presigned URLs valid 15 min;
|
|
218
283
|
beta and Free keys 10 requests per minute; the sync endpoints wait 30 s for
|
|
@@ -236,7 +301,7 @@ client = Client()
|
|
|
236
301
|
|
|
237
302
|
## Background jobs
|
|
238
303
|
|
|
239
|
-
For batches, submit a job and collect the
|
|
304
|
+
For batches, submit a job and collect the result later:
|
|
240
305
|
|
|
241
306
|
```python
|
|
242
307
|
job = client.analyze_async("plan.png")
|
|
@@ -244,8 +309,8 @@ print(f"Submitted {job.id}, status={job.status}")
|
|
|
244
309
|
|
|
245
310
|
final = client.wait_for_job(job.id, poll_interval=2.0, timeout=300.0)
|
|
246
311
|
if final.status == "completed":
|
|
247
|
-
|
|
248
|
-
print(f"got {
|
|
312
|
+
result = client.get_result(final.id)
|
|
313
|
+
print(f"got {result.width}x{result.height}, {len(result.rooms)} rooms")
|
|
249
314
|
|
|
250
315
|
# Or poll yourself:
|
|
251
316
|
job = client.get_job(job.id)
|
|
@@ -255,7 +320,7 @@ if job.is_terminal:
|
|
|
255
320
|
|
|
256
321
|
`analyze` and `extract` currently produce identical output; the two
|
|
257
322
|
endpoints are kept distinct so future tiers can attach to `analyze`
|
|
258
|
-
without
|
|
323
|
+
without changing `extract`.
|
|
259
324
|
|
|
260
325
|
## Timeouts on a busy queue
|
|
261
326
|
|
|
@@ -269,12 +334,12 @@ resubmitting:
|
|
|
269
334
|
from floorplan_api import TimeoutError
|
|
270
335
|
|
|
271
336
|
try:
|
|
272
|
-
|
|
337
|
+
result = client.extract("plan.pdf")
|
|
273
338
|
except TimeoutError as exc:
|
|
274
339
|
if exc.job_id is None:
|
|
275
340
|
raise # client-side timeout
|
|
276
341
|
job = client.wait_for_job(exc.job_id)
|
|
277
|
-
|
|
342
|
+
result = client.get_result(job.id)
|
|
278
343
|
```
|
|
279
344
|
|
|
280
345
|
## Errors
|
|
@@ -290,7 +355,7 @@ from floorplan_api import (
|
|
|
290
355
|
)
|
|
291
356
|
|
|
292
357
|
try:
|
|
293
|
-
|
|
358
|
+
result = client.extract("plan.png")
|
|
294
359
|
except RateLimitError as exc:
|
|
295
360
|
time.sleep(exc.retry_after or 5.0)
|
|
296
361
|
except ProcessingError as exc:
|
|
@@ -304,12 +369,13 @@ except FloorPlanError as exc:
|
|
|
304
369
|
| Exception | Status | When | Retried |
|
|
305
370
|
| --- | --- | --- | --- |
|
|
306
371
|
| `AuthenticationError` | 401, 403 | Missing/invalid/expired/revoked key; job belongs to another account | no |
|
|
307
|
-
| `InvalidRequestError` | 400, 409, 413, 415 | Malformed body, bad `page`,
|
|
372
|
+
| `InvalidRequestError` | 400, 409, 413, 415 | Malformed body, bad `page`, result requested before completion, file too large, unsupported type. Also raised locally for unsupported input or a PDF page that does not exist | no |
|
|
308
373
|
| `NotFoundError` | 404 | Job/resource missing | no |
|
|
309
374
|
| `RateLimitError` | 429 | Per-minute rate limit exceeded | yes, honouring `Retry-After` |
|
|
310
375
|
| `TimeoutError` | 504 | Worker did not finish in the sync window; `job_id` set | no |
|
|
311
376
|
| `ProcessingError` | 500 | Worker failed the job (undecodable file, ...); `job_id` set | no |
|
|
312
377
|
| `ServerError` | other 5xx | Outage, queue at capacity (503 honours `Retry-After`) | yes |
|
|
378
|
+
| `ServerError` | — | A successful response that does not carry a usable result (no wall mask) | no |
|
|
313
379
|
| `TimeoutError` | — | Client-side `timeout` exceeded, or `wait_for_job` gave up | connection timeouts yes |
|
|
314
380
|
| `ConnectionError` | — | DNS / TCP / TLS failure | yes |
|
|
315
381
|
|
|
@@ -359,7 +425,7 @@ See [`examples/`](./examples/):
|
|
|
359
425
|
|
|
360
426
|
* [`quickstart.py`](examples/quickstart.py) — extract a single image or PDF
|
|
361
427
|
* [`async_client.py`](examples/async_client.py) — extract several files concurrently
|
|
362
|
-
* [`async_job.py`](examples/async_job.py) — submit + poll +
|
|
428
|
+
* [`async_job.py`](examples/async_job.py) — submit + poll + fetch the result
|
|
363
429
|
* [`sync_timeout_recovery.py`](examples/sync_timeout_recovery.py) — finish a job after a 504
|
|
364
430
|
* [`local_dev.py`](examples/local_dev.py) — talk to a local dev server
|
|
365
431
|
|