floorplan-api 0.5.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,44 @@
1
+ # Dependencies
2
+ node_modules
3
+ .pnpm-store
4
+
5
+ # Next.js
6
+ .next
7
+ out
8
+
9
+ # Production
10
+ build
11
+ dist
12
+
13
+ # Environment
14
+ .env
15
+ .env.local
16
+ .env.development.local
17
+ .env.test.local
18
+ .env.production.local
19
+
20
+ # Debug
21
+ npm-debug.log*
22
+ yarn-debug.log*
23
+ yarn-error.log*
24
+
25
+ # IDE
26
+ .vscode
27
+ .idea
28
+ *.swp
29
+ *.swo
30
+
31
+ # OS
32
+ .DS_Store
33
+ Thumbs.db
34
+
35
+ # TypeScript
36
+ *.tsbuildinfo
37
+ next-env.d.ts
38
+
39
+ # Prisma
40
+ prisma/*.db
41
+ prisma/*.db-journal
42
+
43
+ # Misc
44
+ *.log
@@ -0,0 +1,129 @@
1
+ # Changelog
2
+
3
+ All notable changes to `floorplan-api` will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.5.0] - 2026-09-25
9
+
10
+ ### Added
11
+ - **PDF input with page selection.** `extract`, `analyze`, `analyze_async`,
12
+ `upload` and `upload_then_extract` accept PDF files and a `page=` argument
13
+ (1-based, default 1). The chosen page is cut out locally with `pypdf` and
14
+ only that single page is uploaded; out-of-range pages and encrypted PDFs
15
+ raise `InvalidRequestError` before any request. With `upload_key=`, `page`
16
+ is passed to the API, which renders that page of the stored object. The
17
+ API rasterises the page at 200 DPI (longest edge capped at 8192 px) and
18
+ returns the mask at that size. `create_upload(content_type="application/pdf")`
19
+ presigns a PDF slot. New runtime dependency: `pypdf`.
20
+ - **`AsyncClient`** (`pip install "floorplan-api[async]"`): an asyncio
21
+ client built on httpx with the same methods as `Client`, all awaitable.
22
+ `from floorplan_api import AsyncClient`.
23
+ - **`MaskBytes`**: `extract`, `analyze`, `upload_then_extract` and
24
+ `download_mask` now return a `bytes` subclass carrying `width`, `height`,
25
+ `job_id` and `request_id` from the response headers. It is still `bytes`;
26
+ existing code that writes or decodes the result is unaffected.
27
+ - `extract(upload_key=...)`, `analyze(upload_key=...)` and
28
+ `analyze_async(upload_key=...)`: submit by storage key without a second
29
+ HTTP client.
30
+ - `ProcessingError` (subclass of `ServerError`): the worker failed the job.
31
+ Carries `job_id` and the server's error `type`.
32
+ - `FloorPlanError.job_id`, set when the server created a job for the request
33
+ before failing it.
34
+ - `MaskBytes.mode` (`live`/`test`, from `X-Floorplan-Mode`) and, for PDF
35
+ input, `MaskBytes.page_size_pt`, `pdf_scale` and `pdf_dpi`: the client
36
+ measures the page before upload so the mask can be mapped back to PDF
37
+ coordinates and the DPI the server actually rendered at is known.
38
+ `floorplan_api.pdf_page_size(data, page)` exposes the measurement.
39
+ - `SUPPORTED_CONTENT_TYPES` and `ImageInput` are exported.
40
+
41
+ ### Changed
42
+ - **Raw `bytes` and unnamed file-like inputs now work against the real
43
+ API.** They were sent as `application/octet-stream`, which the server
44
+ rejects with 415. The client now sniffs the PNG, JPEG, WEBP and PDF
45
+ signatures and labels the upload accordingly; a path or named stream whose
46
+ extension disagrees with its contents is labelled by its contents.
47
+ Anything else is rejected client-side with `InvalidRequestError` before a
48
+ request is made.
49
+ - **Retries no longer resubmit queued jobs.** A 504 `timeout_error` (worker
50
+ still running) now raises `TimeoutError` with `job_id` set, and a 500 that
51
+ carries a `job_id` raises `ProcessingError`; neither is retried. Previously
52
+ both were retried as generic 5xx, creating a new job on every attempt.
53
+ Other 5xx, 429 and connection errors are still retried, and the upload
54
+ body is rewound before each retry (previously a retried multipart request
55
+ could go out with an empty file).
56
+ - `Retry-After` is honoured for 429 and 503 responses, capped at 30 s per
57
+ attempt.
58
+ - Minimum Python is now 3.9 (3.8 reached end of life in October 2024).
59
+ Python 3.13 is tested.
60
+ - Package version is read from `floorplan_api/_version.py` at build time;
61
+ the wheel and `floorplan_api.__version__` can no longer disagree.
62
+ - Text-mode file objects are rejected with a clear `InvalidRequestError`
63
+ instead of a transport error.
64
+
65
+ ### Removed
66
+ - The `Source` and `Issues` project URLs, which pointed at a private
67
+ repository.
68
+
69
+ ## [0.4.0] - 2026-09-25
70
+
71
+ ### Changed
72
+ - Default `base_url` is now `https://api.floorplanapi.com` (was the never-
73
+ launched `api.floorplan.dev`). Clients that passed `base_url` or set
74
+ `FLOORPLAN_BASE_URL` are unaffected.
75
+ - Package metadata points at floorplanapi.com and the `floorplan-api` repo.
76
+
77
+ ## [0.3.0] - 2026-05-07
78
+
79
+ ### Added
80
+ - `Client.create_upload(content_type=...)` — reserve a presigned upload slot.
81
+ Returns `{upload_url, key, expires_at, content_type}`. The web tier never
82
+ sees the bytes; the client PUTs directly to object storage (Cloudflare R2
83
+ in production, or a dev-mode passthrough on a self-hosted instance).
84
+ - `Client.upload(image, content_type=...)` — combines `create_upload` with
85
+ the PUT and returns the storage key.
86
+ - `Client.upload_then_extract(image, threshold_bytes=10*1024*1024)` —
87
+ one-shot helper. Files smaller than `threshold_bytes` use the inline
88
+ multipart `extract()` path; larger files switch to the presigned flow
89
+ automatically. Pass `threshold_bytes=0` to force the presigned path.
90
+
91
+ ### Changed
92
+ - `/v1/extract` and `/v1/analyze` now accept either the existing
93
+ multipart `image` field or a new `upload_key` form field referencing a
94
+ prior `POST /v1/uploads`.
95
+
96
+ ## [0.2.0] - 2026-05-06
97
+
98
+ ### Changed
99
+ - **Breaking:** `Client.extract` and `Client.analyze` now return `bytes` (raw PNG
100
+ wall-segmentation mask) instead of an `ExtractResult`. The API is now
101
+ image-in / image-out.
102
+ - **Breaking:** `Job.result` is now an optional `MaskResult` (with `result_url`,
103
+ `width`, `height`) rather than an `ExtractResult`.
104
+ - `analyze_async` no longer accepts `include_symbols` / `include_measurements`.
105
+ - `Client.extract` no longer accepts a `format=` keyword.
106
+
107
+ ### Added
108
+ - `Client.download_mask(job_id)` — fetch the PNG mask for a completed job.
109
+ - `MaskResult` model.
110
+
111
+ ### Removed
112
+ - `Client.extract_svg`. SVG rendering is no longer offered server-side.
113
+ - `ExtractResult`, `Room`, `Symbol`, `Boundary`, `Position`, `Measurements`
114
+ models. The wall-segmentation model returns a binary mask, not structured
115
+ rooms.
116
+
117
+ ## [0.1.0] - 2026-04-28
118
+
119
+ ### Added
120
+ - Initial release of the official Python client.
121
+ - Synchronous `Client` with methods: `extract`, `analyze`, `analyze_async`, `wait_for_job`, `get_job`.
122
+ - Typed dataclass models: `ExtractResult`, `Room`, `Symbol`, `Job`, `Measurements`.
123
+ - Typed exception hierarchy: `FloorPlanError`, `AuthenticationError`, `RateLimitError`,
124
+ `InvalidRequestError`, `NotFoundError`, `ServerError`, `TimeoutError`, `ConnectionError`.
125
+ - Automatic retries with exponential backoff for 5xx responses and connection errors.
126
+ - Configurable base URL (works against the hosted API or your own deployment).
127
+ - Test environment support — keys starting with `fp_test_` run the same model
128
+ without being billed or recorded as usage.
129
+ - Example scripts in `examples/`.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Floor Plan API
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,381 @@
1
+ Metadata-Version: 2.5
2
+ Name: floorplan-api
3
+ Version: 0.5.0
4
+ Summary: Official Python client for the Floor Plan API — wall-segmentation masks from floor plan images and PDFs.
5
+ Project-URL: Homepage, https://floorplanapi.com
6
+ Project-URL: Documentation, https://floorplanapi.com/docs
7
+ Project-URL: Changelog, https://floorplanapi.com/docs#python-changelog
8
+ Author-email: Floor Plan API <admin@auctas.ai>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: api,computer vision,extraction,floor plan,floorplan,segmentation
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Multimedia :: Graphics
24
+ Classifier: Topic :: Scientific/Engineering :: Image Recognition
25
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
26
+ Classifier: Typing :: Typed
27
+ Requires-Python: >=3.9
28
+ Requires-Dist: pypdf>=4.0
29
+ Requires-Dist: requests>=2.28
30
+ Provides-Extra: async
31
+ Requires-Dist: httpx>=0.24; extra == 'async'
32
+ Provides-Extra: dev
33
+ Requires-Dist: httpx>=0.24; extra == 'dev'
34
+ Requires-Dist: mypy>=1.8; extra == 'dev'
35
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
36
+ Requires-Dist: pytest-cov>=4.0; extra == 'dev'
37
+ Requires-Dist: pytest>=7.0; extra == 'dev'
38
+ Requires-Dist: responses>=0.23; extra == 'dev'
39
+ Requires-Dist: respx>=0.21; extra == 'dev'
40
+ Requires-Dist: ruff>=0.5; extra == 'dev'
41
+ Requires-Dist: types-requests; extra == 'dev'
42
+ Description-Content-Type: text/markdown
43
+
44
+ # floorplan-api
45
+
46
+ Official Python client for the [Floor Plan API](https://floorplanapi.com):
47
+ upload a floor plan, get back a binary wall-segmentation PNG mask.
48
+
49
+ * **Image in, image out.** PNG, JPEG, WEBP, or one page of a PDF. The mask
50
+ comes back as PNG bytes at the input's resolution: `255` = wall, `0` =
51
+ everything else.
52
+ * **Two clients, one surface.** `Client` (synchronous, on `requests`) and
53
+ `AsyncClient` (asyncio, on `httpx`).
54
+ * **Retries that don't double-bill.** Transient failures are retried with
55
+ backoff; a job the server already queued is never resubmitted.
56
+ * Works with paths, raw bytes, or binary file-like objects. Large files go
57
+ straight to object storage via a presigned URL.
58
+ * Self-hosted friendly: point `base_url` at any Floor Plan API deployment.
59
+
60
+ ## Install
61
+
62
+ ```bash
63
+ pip install floorplan-api # sync client (requests + pypdf)
64
+ pip install "floorplan-api[async]" # adds AsyncClient (httpx)
65
+ ```
66
+
67
+ Python 3.9+.
68
+
69
+ ## Quickstart
70
+
71
+ ```python
72
+ from floorplan_api import Client
73
+
74
+ client = Client(api_key="fp_test_...") # or set FLOORPLAN_API_KEY
75
+ mask = client.extract("plans/floor1.png") # PNG, JPEG or WEBP
76
+ mask = client.extract("plans/set.pdf", page=3) # PDF: pick the page
77
+
78
+ with open("walls.png", "wb") as fh:
79
+ fh.write(mask)
80
+
81
+ print(mask.width, mask.height, mask.job_id)
82
+ ```
83
+
84
+ `extract()` returns `MaskBytes`, a `bytes` subclass. Write it, hash it, hand
85
+ it to Pillow or OpenCV as usual; the extra attributes `width`, `height`,
86
+ `job_id`, `request_id` and `mode` (`live`/`test`) come from the response
87
+ headers. For a PDF, `page_size_pt` and `pdf_scale` are attached too (see
88
+ below). The client never post-processes the mask.
89
+
90
+ Any of these inputs work:
91
+
92
+ ```python
93
+ client.extract("plans/floor1.png") # path string
94
+ client.extract(Path("plans/floor1.pdf")) # pathlib.Path
95
+ client.extract(image_bytes) # raw bytes (format is sniffed)
96
+ client.extract(open("plan.jpg", "rb")) # binary file-like
97
+ ```
98
+
99
+ The format is detected from the file's leading bytes (PNG, JPEG, WEBP, PDF
100
+ signatures), falling back to the extension. Anything else raises
101
+ `InvalidRequestError` before a request is made.
102
+
103
+ ## How your file is sent
104
+
105
+ Be aware that images and PDFs are handled differently on the way out:
106
+
107
+ | Input | What is uploaded |
108
+ | --- | --- |
109
+ | PNG, JPEG, WEBP | **The file, byte for byte.** The client never decodes, resizes or re-encodes an image. |
110
+ | PDF | **A new single-page PDF containing only the requested page.** Built locally with `pypdf`: the page object is copied with its content stream, resources (fonts, embedded images) and annotations; nothing is rasterised client-side. The other pages, document metadata, bookmarks, attachments and form definitions are not sent. |
111
+ | `upload_key` | Nothing; the object is already in storage. For a multi-page PDF you stored yourself, `page=` is sent as a form field and the server picks the page. |
112
+
113
+ So a 40-page drawing set costs one page of bandwidth and storage, and the
114
+ server only ever holds the page you asked about. If you need the whole
115
+ document on the server side, upload it with `upload()` from a tool that
116
+ does not slice, then call `extract(upload_key=..., page=N)`.
117
+
118
+ ### PDFs and `page`
119
+
120
+ A PDF is processed one page at a time. Pass `page=` (1-based; default 1) to
121
+ say which. Page errors (out of range, password-protected, unreadable) are
122
+ 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 mask
124
+ at that size; read it from `mask.width` and `mask.height`.
125
+
126
+ ```python
127
+ mask = client.extract("set.pdf", page=3)
128
+ key = client.upload("set.pdf", page=3) # the stored object is page 3 only
129
+ mask = client.extract(upload_key=key) # ... so no page is needed here
130
+ ```
131
+
132
+ `page` on a raster input is rejected unless it is 1. When you submit by
133
+ `upload_key` for an object you stored yourself (raw REST), `page=` is sent
134
+ to the server, which renders that page of the stored file.
135
+
136
+ **Mapping the mask back to PDF coordinates.** The mask is on the rendered
137
+ page's pixel grid, not in PDF points. The client measures the page's crop
138
+ box (honouring `/Rotate`) before upload and attaches it, so:
139
+
140
+ ```python
141
+ mask = client.extract("set.pdf", page=3)
142
+ mask.page_size_pt # (1728.0, 2592.0) -> a 24 x 36 in sheet
143
+ mask.pdf_scale # mask pixels per PDF point: mask.width / page width
144
+ mask.pdf_dpi # the DPI actually used: 200, or less if the page hit the 8192 px cap
145
+
146
+ x_px = x_pt * mask.pdf_scale # PDF point -> mask pixel (origin: top-left of the render)
147
+ ```
148
+
149
+ `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 mask you
151
+ fetched later with `download_mask()`, which has no `page_size_pt`.
152
+
153
+ ### Async
154
+
155
+ ```python
156
+ import asyncio
157
+ from floorplan_api import AsyncClient
158
+
159
+ async def main() -> None:
160
+ async with AsyncClient() as client:
161
+ masks = await asyncio.gather(
162
+ client.extract("a.pdf"),
163
+ client.extract("b.png"),
164
+ )
165
+ for m in masks:
166
+ print(m.size)
167
+
168
+ asyncio.run(main())
169
+ ```
170
+
171
+ `AsyncClient` has the same methods as `Client`, all awaitable. Pass your own
172
+ `httpx.AsyncClient` as `client=` for proxies or HTTP/2; the wrapper then
173
+ leaves it open.
174
+
175
+ ## Large files
176
+
177
+ Inline uploads are capped at 10 MB. `upload_then_extract()` switches to a
178
+ presigned upload for anything bigger, so the API server never holds the
179
+ bytes:
180
+
181
+ ```python
182
+ mask = client.upload_then_extract("big_floor_plan.pdf", page=2)
183
+
184
+ # or step by step:
185
+ key = client.upload("big_floor_plan.pdf", page=2) # PUT straight to object storage
186
+ mask = client.extract(upload_key=key) # submit by storage key
187
+ ```
188
+
189
+ The size check happens after the page is cut out, so a large multi-page PDF
190
+ whose selected page is small still takes the inline path.
191
+
192
+ `analyze()` and `analyze_async()` accept `upload_key=` the same way.
193
+
194
+ ## What the API does with your file
195
+
196
+ Nothing on the client or the API server touches pixels; the worker does,
197
+ like this (the full trace is in `docs/IMAGE_PIPELINE.md` of the API repo):
198
+
199
+ * **Rasters** are decoded with OpenCV in colour mode. Alpha is dropped
200
+ without compositing, so flatten transparent PNGs onto white first;
201
+ grayscale is expanded to three channels; 16-bit depth becomes 8-bit; JPEG
202
+ EXIF orientation is applied, so the mask aligns with the *displayed*
203
+ orientation; ICC profiles are ignored. A raster whose longer edge exceeds
204
+ 8192 px is processed downscaled to that bound and the mask is resized back
205
+ to the input size, so it stays pixel-aligned but carries less detail.
206
+ * **PDF pages** are rendered at 200 DPI onto white (transparent regions
207
+ composite onto white), reduced so the longer edge is at most 8192 px. All
208
+ content is rendered: linework, hatching, text, dimensions. The mask has
209
+ the rendered size, reported in `width`/`height`.
210
+ * **Inference** is a two-stage U-Net++ (whole sheet at shortest side 1024,
211
+ then a crop refiner at native resolution). No test-time augmentation.
212
+ * **Output** is `prob > 0.5` as an 8-bit single-channel PNG with values
213
+ exactly 0 and 255, no morphology or filtering. `255` is wall in the
214
+ *carved* convention: door and window openings are not wall, and walls are
215
+ as thick as the source linework.
216
+
217
+ Limits you will meet: inline uploads 10 MB; presigned URLs valid 15 min;
218
+ beta and Free keys 10 requests per minute; the sync endpoints wait 30 s for
219
+ the worker before answering 504 with the job id; the queue answers 503 with
220
+ `Retry-After: 30` when 100 jobs are pending. `extract` costs 1 credit,
221
+ `analyze` 2, only on live keys.
222
+
223
+ ## Authentication
224
+
225
+ API keys come from the [Floor Plan API dashboard](https://floorplanapi.com/api-keys).
226
+
227
+ * **Live keys** (`fp_live_...`) — production use, billed against your account.
228
+ * **Test keys** (`fp_test_...`) — same model, never billed.
229
+
230
+ ```python
231
+ client = Client(api_key="fp_live_xxx")
232
+ # or, equivalently:
233
+ import os; os.environ["FLOORPLAN_API_KEY"] = "fp_live_xxx"
234
+ client = Client()
235
+ ```
236
+
237
+ ## Background jobs
238
+
239
+ For batches, submit a job and collect the mask later:
240
+
241
+ ```python
242
+ job = client.analyze_async("plan.png")
243
+ print(f"Submitted {job.id}, status={job.status}")
244
+
245
+ final = client.wait_for_job(job.id, poll_interval=2.0, timeout=300.0)
246
+ if final.status == "completed":
247
+ mask = client.download_mask(final.id)
248
+ print(f"got {final.result.width}x{final.result.height} mask")
249
+
250
+ # Or poll yourself:
251
+ job = client.get_job(job.id)
252
+ if job.is_terminal:
253
+ ...
254
+ ```
255
+
256
+ `analyze` and `extract` currently produce identical output; the two
257
+ endpoints are kept distinct so future tiers can attach to `analyze`
258
+ without breaking `extract`'s simpler contract.
259
+
260
+ ## Timeouts on a busy queue
261
+
262
+ The synchronous endpoints wait about 30 s for the worker. If the queue is
263
+ deep the server answers 504 and includes the job's id; the job keeps
264
+ running. The client raises `TimeoutError` with `job_id` set and does **not**
265
+ retry (a retry would queue a second copy). Finish the job without
266
+ resubmitting:
267
+
268
+ ```python
269
+ from floorplan_api import TimeoutError
270
+
271
+ try:
272
+ mask = client.extract("plan.pdf")
273
+ except TimeoutError as exc:
274
+ if exc.job_id is None:
275
+ raise # client-side timeout
276
+ job = client.wait_for_job(exc.job_id)
277
+ mask = client.download_mask(job.id)
278
+ ```
279
+
280
+ ## Errors
281
+
282
+ All errors derive from `FloorPlanError`. Catch the base class to handle every
283
+ API error, or specific subclasses to take action:
284
+
285
+ ```python
286
+ from floorplan_api import (
287
+ Client, FloorPlanError,
288
+ AuthenticationError, RateLimitError, InvalidRequestError, NotFoundError,
289
+ ServerError, ProcessingError, TimeoutError, ConnectionError,
290
+ )
291
+
292
+ try:
293
+ mask = client.extract("plan.png")
294
+ except RateLimitError as exc:
295
+ time.sleep(exc.retry_after or 5.0)
296
+ except ProcessingError as exc:
297
+ print(f"worker could not process this file: {exc.message} (job {exc.job_id})")
298
+ except AuthenticationError:
299
+ print("Check your API key.")
300
+ except FloorPlanError as exc:
301
+ print(f"{exc.type}: {exc.message} (request_id={exc.request_id})")
302
+ ```
303
+
304
+ | Exception | Status | When | Retried |
305
+ | --- | --- | --- | --- |
306
+ | `AuthenticationError` | 401, 403 | Missing/invalid/expired/revoked key; job belongs to another account | no |
307
+ | `InvalidRequestError` | 400, 409, 413, 415 | Malformed body, bad `page`, mask requested before completion, file too large, unsupported type. Also raised locally for unsupported input or a PDF page that does not exist | no |
308
+ | `NotFoundError` | 404 | Job/resource missing | no |
309
+ | `RateLimitError` | 429 | Per-minute rate limit exceeded | yes, honouring `Retry-After` |
310
+ | `TimeoutError` | 504 | Worker did not finish in the sync window; `job_id` set | no |
311
+ | `ProcessingError` | 500 | Worker failed the job (undecodable file, ...); `job_id` set | no |
312
+ | `ServerError` | other 5xx | Outage, queue at capacity (503 honours `Retry-After`) | yes |
313
+ | `TimeoutError` | — | Client-side `timeout` exceeded, or `wait_for_job` gave up | connection timeouts yes |
314
+ | `ConnectionError` | — | DNS / TCP / TLS failure | yes |
315
+
316
+ Every exception carries `status_code`, `type`, `request_id`, `job_id` and
317
+ the decoded `response` body when available.
318
+
319
+ ## Configuration
320
+
321
+ ```python
322
+ client = Client(
323
+ api_key="fp_live_...",
324
+ base_url="https://api.floorplanapi.com", # default
325
+ timeout=60.0, # seconds per request
326
+ max_retries=3, # connection errors, 429, transient 5xx
327
+ retry_backoff=0.5, # base delay (s) for exp backoff w/ jitter
328
+ session=None, # your own requests.Session
329
+ )
330
+ ```
331
+
332
+ `AsyncClient` takes the same arguments, with `client=` (an
333
+ `httpx.AsyncClient`) in place of `session=`.
334
+
335
+ A server `Retry-After` header overrides the backoff, capped at 30 s per
336
+ attempt. With the defaults, a 503 "queue at capacity" response can hold an
337
+ `extract()` call for up to about 90 s before it raises.
338
+
339
+ Environment variables:
340
+
341
+ * `FLOORPLAN_API_KEY` — used when `api_key=` is omitted.
342
+ * `FLOORPLAN_BASE_URL` — used when `base_url=` is omitted (handy for self-hosted).
343
+
344
+ ## Pointing at a self-hosted instance
345
+
346
+ ```python
347
+ client = Client(
348
+ api_key="fp_test_...",
349
+ base_url="http://localhost:3000",
350
+ )
351
+ ```
352
+
353
+ The Next.js app rewrites `/v1/*` to `/api/v1/*` internally, so the
354
+ client's base URL is the bare host with no `/api` segment.
355
+
356
+ ## Examples
357
+
358
+ See [`examples/`](./examples/):
359
+
360
+ * [`quickstart.py`](examples/quickstart.py) — extract a single image or PDF
361
+ * [`async_client.py`](examples/async_client.py) — extract several files concurrently
362
+ * [`async_job.py`](examples/async_job.py) — submit + poll + download
363
+ * [`sync_timeout_recovery.py`](examples/sync_timeout_recovery.py) — finish a job after a 504
364
+ * [`local_dev.py`](examples/local_dev.py) — talk to a local dev server
365
+
366
+ ## Development
367
+
368
+ ```bash
369
+ pip install -e '.[dev]'
370
+ pytest
371
+ ruff check src tests examples
372
+ mypy src
373
+ ```
374
+
375
+ Releases: bump `src/floorplan_api/_version.py`, add a changelog entry, and
376
+ push a `python-v<version>` tag. CI runs the tests on Python 3.9–3.13,
377
+ builds the sdist and wheel, and publishes to PyPI via trusted publishing.
378
+
379
+ ## License
380
+
381
+ MIT — see [LICENSE](./LICENSE).