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.
- floorplan_api-0.5.0/.gitignore +44 -0
- floorplan_api-0.5.0/CHANGELOG.md +129 -0
- floorplan_api-0.5.0/LICENSE +21 -0
- floorplan_api-0.5.0/PKG-INFO +381 -0
- floorplan_api-0.5.0/README.md +338 -0
- floorplan_api-0.5.0/examples/async_client.py +40 -0
- floorplan_api-0.5.0/examples/async_job.py +53 -0
- floorplan_api-0.5.0/examples/local_dev.py +41 -0
- floorplan_api-0.5.0/examples/quickstart.py +43 -0
- floorplan_api-0.5.0/examples/sync_timeout_recovery.py +50 -0
- floorplan_api-0.5.0/pyproject.toml +95 -0
- floorplan_api-0.5.0/src/floorplan_api/__init__.py +75 -0
- floorplan_api-0.5.0/src/floorplan_api/_input.py +411 -0
- floorplan_api-0.5.0/src/floorplan_api/_transport.py +175 -0
- floorplan_api-0.5.0/src/floorplan_api/_version.py +3 -0
- floorplan_api-0.5.0/src/floorplan_api/async_client.py +400 -0
- floorplan_api-0.5.0/src/floorplan_api/client.py +502 -0
- floorplan_api-0.5.0/src/floorplan_api/exceptions.py +123 -0
- floorplan_api-0.5.0/src/floorplan_api/models.py +241 -0
- floorplan_api-0.5.0/src/floorplan_api/py.typed +0 -0
- floorplan_api-0.5.0/tests/__init__.py +0 -0
- floorplan_api-0.5.0/tests/conftest.py +104 -0
- floorplan_api-0.5.0/tests/test_async_client.py +287 -0
- floorplan_api-0.5.0/tests/test_client.py +627 -0
- floorplan_api-0.5.0/tests/test_input.py +290 -0
- floorplan_api-0.5.0/tests/test_models.py +143 -0
|
@@ -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).
|