fastocr-sdk 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.
- fastocr_sdk-0.1.0/.gitignore +9 -0
- fastocr_sdk-0.1.0/CHANGELOG.md +35 -0
- fastocr_sdk-0.1.0/LICENSE +21 -0
- fastocr_sdk-0.1.0/PKG-INFO +285 -0
- fastocr_sdk-0.1.0/README.md +252 -0
- fastocr_sdk-0.1.0/examples/async_batch.py +33 -0
- fastocr_sdk-0.1.0/examples/cookbooks/01_langchain_scanned_pdf_rag/rag_pipeline.py +82 -0
- fastocr_sdk-0.1.0/examples/cookbooks/02_llamaindex_multilingual_reader/custom_reader.py +43 -0
- fastocr_sdk-0.1.0/examples/cookbooks/03_searchable_pdf_compliance_archive/batch_archive.py +47 -0
- fastocr_sdk-0.1.0/examples/cookbooks/04_local_curl_recipes/extract_raw_text.sh +70 -0
- fastocr_sdk-0.1.0/examples/cookbooks/README.md +30 -0
- fastocr_sdk-0.1.0/examples/extract_text.py +10 -0
- fastocr_sdk-0.1.0/examples/handle_errors.py +32 -0
- fastocr_sdk-0.1.0/examples/searchable_pdf.py +15 -0
- fastocr_sdk-0.1.0/pyproject.toml +60 -0
- fastocr_sdk-0.1.0/src/fastocr_sdk/__init__.py +53 -0
- fastocr_sdk-0.1.0/src/fastocr_sdk/_core.py +318 -0
- fastocr_sdk-0.1.0/src/fastocr_sdk/_version.py +1 -0
- fastocr_sdk-0.1.0/src/fastocr_sdk/async_client.py +290 -0
- fastocr_sdk-0.1.0/src/fastocr_sdk/client.py +289 -0
- fastocr_sdk-0.1.0/src/fastocr_sdk/errors.py +145 -0
- fastocr_sdk-0.1.0/src/fastocr_sdk/models.py +140 -0
- fastocr_sdk-0.1.0/src/fastocr_sdk/py.typed +0 -0
- fastocr_sdk-0.1.0/tests/conftest.py +51 -0
- fastocr_sdk-0.1.0/tests/fake_server.py +143 -0
- fastocr_sdk-0.1.0/tests/test_cookbooks.py +121 -0
- fastocr_sdk-0.1.0/tests/test_documents.py +542 -0
- fastocr_sdk-0.1.0/tests/test_end_to_end.py +157 -0
- fastocr_sdk-0.1.0/tests/test_errors.py +262 -0
- fastocr_sdk-0.1.0/tests/test_examples.py +74 -0
- fastocr_sdk-0.1.0/tests/test_live_smoke.py +80 -0
- fastocr_sdk-0.1.0/tests/test_models.py +35 -0
- fastocr_sdk-0.1.0/tests/test_openapi_conformance.py +475 -0
- fastocr_sdk-0.1.0/tests/test_retries.py +364 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this SDK are listed here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions follow
|
|
5
|
+
[semantic versioning](https://semver.org).
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - 2026-10-02
|
|
8
|
+
|
|
9
|
+
First public release. Document OCR (`/v1/documents`) only.
|
|
10
|
+
|
|
11
|
+
- `FastOCR` and `AsyncFastOCR` clients: `create`, `upload`, `start`, `get`,
|
|
12
|
+
`list`, `delete`, `get_output_url`, `get_text`, `download_searchable_pdf`,
|
|
13
|
+
`wait_for_completion`, `process`, and `extract_text`.
|
|
14
|
+
- Files are streamed from disk with an explicit `Content-Length`.
|
|
15
|
+
- Retries (default 2) for connection errors, 429 and 502/503/504, with backoff,
|
|
16
|
+
jitter and `Retry-After`. One idempotency key per `process()` call, reused on
|
|
17
|
+
every retry.
|
|
18
|
+
- Errors carry `status_code`, `code`, `type` and `request_id`, and are parsed
|
|
19
|
+
from both the API's error envelope and API Gateway's `{"message": ...}`. HTTP
|
|
20
|
+
401 and 403 raise `AuthenticationError`.
|
|
21
|
+
- `DocumentFailedError` for jobs that end in `failed`, with `code`, `retryable`
|
|
22
|
+
and `document_id`.
|
|
23
|
+
- `extract_text()` raises `PartialResultError` instead of returning truncated text;
|
|
24
|
+
pass `allow_partial=True` to accept it.
|
|
25
|
+
- API Gateway error bodies (`{"message"}` and the newer `error` object) raise the same
|
|
26
|
+
exceptions, with `code`, `type` and `request_id` when present.
|
|
27
|
+
- `process()` with a caller-supplied `idempotency_key` is safe to repeat: it finds the same
|
|
28
|
+
document and waits for it instead of failing with 409 `document_not_startable`.
|
|
29
|
+
- A blank or whitespace-only API key is treated as missing, and whitespace around a key is
|
|
30
|
+
trimmed (a trailing newline used to surface as a connection error).
|
|
31
|
+
- Unknown response fields, statuses and error codes are tolerated.
|
|
32
|
+
- A 2xx response whose body is not a JSON object raises `ServerError`.
|
|
33
|
+
- Every SDK error can be pickled, so worker processes (multiprocessing, Celery) can pass
|
|
34
|
+
them back.
|
|
35
|
+
- Distribution `fastocr-sdk`, import package `fastocr_sdk`.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 FastOCR
|
|
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,285 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: fastocr-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for the FastOCR Document OCR API: extract text and searchable PDFs from scanned PDFs.
|
|
5
|
+
Project-URL: Homepage, https://fastocr.org
|
|
6
|
+
Project-URL: Documentation, https://fastocr.org/docs/documents
|
|
7
|
+
Project-URL: Repository, https://github.com/fastocr-org/fastocr-python
|
|
8
|
+
Project-URL: Changelog, https://github.com/fastocr-org/fastocr-python/blob/main/CHANGELOG.md
|
|
9
|
+
Project-URL: Issues, https://github.com/fastocr-org/fastocr-python/issues
|
|
10
|
+
Author-email: FastOCR <support@fastocr.org>
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: document-ai,fastocr,ocr,pdf,rag,searchable-pdf,text-extraction
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: Image Recognition
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.9
|
|
27
|
+
Requires-Dist: httpx>=0.25.0
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest>=7.4.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: respx>=0.21.0; extra == 'dev'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# FastOCR Python SDK
|
|
35
|
+
|
|
36
|
+
[](https://pypi.org/project/fastocr-sdk/)
|
|
37
|
+
[](https://pypi.org/project/fastocr-sdk/)
|
|
38
|
+
[](LICENSE)
|
|
39
|
+
|
|
40
|
+
Python client for the [FastOCR Document OCR API](https://fastocr.org/docs/documents).
|
|
41
|
+
Send a scanned PDF, get back its text or a searchable PDF.
|
|
42
|
+
|
|
43
|
+
- Document OCR only: PDF input, up to 1 GiB per file.
|
|
44
|
+
- Sync (`FastOCR`) and async (`AsyncFastOCR`) clients with identical behavior.
|
|
45
|
+
- Streams large files from disk, retries transient failures, and never creates a document twice.
|
|
46
|
+
- One runtime dependency: `httpx`. Python 3.9+.
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install fastocr-sdk
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The distribution is `fastocr-sdk` and the import is `fastocr_sdk`. Do not
|
|
55
|
+
install the unrelated `fastocr` package from PyPI; it is a different project.
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from fastocr_sdk import FastOCR
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Get access
|
|
62
|
+
|
|
63
|
+
API access is currently granted per account on request. Email
|
|
64
|
+
[support@fastocr.org](mailto:support@fastocr.org?subject=FastOCR%20API%20Access%20Request)
|
|
65
|
+
from your FastOCR account email. Once approved, create a key at
|
|
66
|
+
<https://fastocr.org/developers/keys>. Approved accounts get 10 free pages per
|
|
67
|
+
month and buy more pages by card on the same page.
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
export FASTOCR_API_KEY="your_api_key"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Quickstart
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from fastocr_sdk import FastOCR
|
|
77
|
+
|
|
78
|
+
with FastOCR() as client:
|
|
79
|
+
text = client.extract_text("scanned_contract.pdf")
|
|
80
|
+
print(text)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`extract_text` uploads the file, starts OCR, waits for it to finish, and
|
|
84
|
+
returns the text. To keep the job and the searchable PDF:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
with FastOCR() as client:
|
|
88
|
+
job = client.documents.process("scanned_archive.pdf")
|
|
89
|
+
client.documents.download_searchable_pdf(job.id, "searchable.pdf")
|
|
90
|
+
print(job.id, job.status, job.pages_billed)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`job.status` is `completed`, or `partial` if the account ran out of pages
|
|
94
|
+
partway (`job.is_partial`); see [Out of pages](#out-of-pages).
|
|
95
|
+
|
|
96
|
+
## Async
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
import asyncio
|
|
100
|
+
from fastocr_sdk import AsyncFastOCR
|
|
101
|
+
|
|
102
|
+
async def main():
|
|
103
|
+
async with AsyncFastOCR() as client:
|
|
104
|
+
text = await client.extract_text("invoice.pdf")
|
|
105
|
+
print(text)
|
|
106
|
+
|
|
107
|
+
asyncio.run(main())
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Accounts can have 2 documents processing at once by default (10 with a prepaid
|
|
111
|
+
page allowance). When fanning out, cap concurrency with a semaphore, as in
|
|
112
|
+
[`examples/async_batch.py`](examples/async_batch.py).
|
|
113
|
+
|
|
114
|
+
## Step by step
|
|
115
|
+
|
|
116
|
+
`process()` runs these five steps; each is also a method on `client.documents`:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
created = client.documents.create("scan.pdf", size_bytes) # POST /v1/documents
|
|
120
|
+
client.documents.upload(created.upload_url, "scan.pdf") # PUT to the presigned URL
|
|
121
|
+
client.documents.start(created.id) # POST /v1/documents/{id}/start
|
|
122
|
+
job = client.documents.wait_for_completion(created.id) # GET until a final status
|
|
123
|
+
text = client.documents.get_text(created.id) # GET /output?format=text
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Also available: `get`, `list`, `delete`, and `get_output_url`.
|
|
127
|
+
|
|
128
|
+
## Large files
|
|
129
|
+
|
|
130
|
+
Files given as a path (or a seekable binary stream) are streamed from disk in
|
|
131
|
+
1 MiB chunks, so a 1 GiB PDF does not need 1 GiB of memory. The upload is sent
|
|
132
|
+
with a real `Content-Length`, because S3 rejects chunked uploads. A stream that
|
|
133
|
+
cannot seek has no known size, so it is read into memory first.
|
|
134
|
+
|
|
135
|
+
## Errors
|
|
136
|
+
|
|
137
|
+
Every error is a `FastOCRError` with `status_code`, `code`, `type`,
|
|
138
|
+
`request_id` and `message`. Branch on `code`, never on `message`.
|
|
139
|
+
|
|
140
|
+
| Exception | When |
|
|
141
|
+
|---|---|
|
|
142
|
+
| `AuthenticationError` | HTTP 401 or 403: missing, unknown, or malformed API key |
|
|
143
|
+
| `BadRequestError` | HTTP 400: invalid request |
|
|
144
|
+
| `NotFoundError` | HTTP 404: no such document |
|
|
145
|
+
| `DocumentConflictError` | HTTP 409: wrong state, or an idempotency key reused with a different body |
|
|
146
|
+
| `RateLimitError` | HTTP 429, after retries; `retry_after` holds the seconds, if sent |
|
|
147
|
+
| `ServerError` | HTTP 5xx, after retries |
|
|
148
|
+
| `APIConnectionError` | No response (DNS, TLS, reset, timeout), after retries |
|
|
149
|
+
| `UploadError`, `DownloadError` | The presigned upload or download failed |
|
|
150
|
+
| `PollingTimeoutError` | The document did not finish in `timeout` seconds |
|
|
151
|
+
| `DocumentFailedError` | The document reached status `failed` |
|
|
152
|
+
| `PartialResultError` | `extract_text()` finished a document that was only partly processed (out of pages) |
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from fastocr_sdk import DocumentFailedError, FastOCR, FastOCRError
|
|
156
|
+
|
|
157
|
+
with FastOCR() as client:
|
|
158
|
+
try:
|
|
159
|
+
text = client.extract_text("scan.pdf")
|
|
160
|
+
except DocumentFailedError as error:
|
|
161
|
+
print(error.code, error.retryable, error.document_id)
|
|
162
|
+
except FastOCRError as error:
|
|
163
|
+
print(error.status_code, error.code, error.request_id)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Requests rejected by API Gateway before they reach the API (bad or missing key,
|
|
167
|
+
throttling) raise the same classes. Their body is either `{"message": ...}` or,
|
|
168
|
+
on newer deployments, also carries an `error` object with `type`, `code` (for
|
|
169
|
+
example `unauthorized`, `access_denied`, `throttled`) and `request_id`. A
|
|
170
|
+
throttled request has no `Retry-After`, so the SDK backs off 0.5 to 1 second,
|
|
171
|
+
then 1 to 2 seconds, between its retries.
|
|
172
|
+
|
|
173
|
+
A failed document is not an HTTP error: the API answers 200 with
|
|
174
|
+
`status: "failed"`, and the SDK raises `DocumentFailedError` with the failure's
|
|
175
|
+
`code` and `retryable` flag. Treat an unknown code as `processing_failed`.
|
|
176
|
+
|
|
177
|
+
### Out of pages
|
|
178
|
+
|
|
179
|
+
Running out of pages ends in one of two ways, and they are handled differently.
|
|
180
|
+
|
|
181
|
+
**The document fails with `buy_pages`.** Nothing was processed. `process()` and
|
|
182
|
+
`extract_text()` raise `DocumentFailedError` with `error.out_of_pages == True`.
|
|
183
|
+
After pages are added to the account, start the same document again:
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
try:
|
|
187
|
+
client.documents.process("scan.pdf")
|
|
188
|
+
except DocumentFailedError as error:
|
|
189
|
+
if error.out_of_pages:
|
|
190
|
+
... # pages added to the account
|
|
191
|
+
client.documents.start(error.document_id)
|
|
192
|
+
job = client.documents.wait_for_completion(error.document_id)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**The document finishes as `partial`.** The pages that fit were processed (the
|
|
196
|
+
first `pages_billed` pages) and the rest were not. `process()` returns the job,
|
|
197
|
+
and `job.is_partial` is true. `extract_text()` never returns truncated text
|
|
198
|
+
silently: it raises `PartialResultError`, which carries `document_id`,
|
|
199
|
+
`pages_processed`, `pages_billed` and `pages_total`.
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
from fastocr_sdk import PartialResultError
|
|
203
|
+
|
|
204
|
+
try:
|
|
205
|
+
text = client.extract_text("scan.pdf")
|
|
206
|
+
except PartialResultError as error:
|
|
207
|
+
print(f"{error.pages_billed} of {error.pages_total} pages were processed")
|
|
208
|
+
partial_text = client.documents.get_text(error.document_id)
|
|
209
|
+
|
|
210
|
+
text = client.extract_text("scan.pdf", allow_partial=True) # accept truncated text
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
A partial document cannot be started again (the API answers `409
|
|
214
|
+
document_not_startable`). After pages are added, submit the file again as a new
|
|
215
|
+
document, which is billed as a new document.
|
|
216
|
+
|
|
217
|
+
## Retries and idempotency
|
|
218
|
+
|
|
219
|
+
`FastOCR(max_retries=2)` retries connection errors, HTTP 429 and 502/503/504
|
|
220
|
+
with exponential backoff and jitter. It waits for `Retry-After` when the server
|
|
221
|
+
sends one (up to 60 seconds; longer waits are raised as `RateLimitError`). It
|
|
222
|
+
does not retry other errors, including 400, 401, 403, 404, 409 and 500. Set
|
|
223
|
+
`max_retries=0` to turn retries off. Uploads and downloads of results retry the
|
|
224
|
+
same way.
|
|
225
|
+
|
|
226
|
+
Creating a document is safe to retry because every `create` call sends an
|
|
227
|
+
`Idempotency-Key`, and the same key is reused across all retries of that call.
|
|
228
|
+
`process()` uses one key for the whole call. Pass your own to make your own
|
|
229
|
+
retries safe too:
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
job = client.documents.process("scan.pdf", idempotency_key="invoice-2026-10-001")
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Calling `process()` again with the same key is safe: it finds the same document and,
|
|
236
|
+
if it was already started, just waits for it instead of uploading again.
|
|
237
|
+
|
|
238
|
+
Reusing a key with a different filename, size, `sha256` or `external_id`
|
|
239
|
+
raises `DocumentConflictError` (`idempotency_key_conflict`).
|
|
240
|
+
|
|
241
|
+
## Examples
|
|
242
|
+
|
|
243
|
+
Runnable scripts are in [`examples/`](examples/), and
|
|
244
|
+
[`examples/cookbooks/`](examples/cookbooks/) has LangChain, LlamaIndex, batch
|
|
245
|
+
archiving and cURL recipes.
|
|
246
|
+
|
|
247
|
+
## Configuration
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
FastOCR(
|
|
251
|
+
api_key=None, # defaults to $FASTOCR_API_KEY
|
|
252
|
+
base_url=None, # defaults to $FASTOCR_BASE_URL, then https://api.fastocr.org
|
|
253
|
+
timeout=30.0, # seconds per API request
|
|
254
|
+
max_retries=2,
|
|
255
|
+
)
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## Versioning and compatibility
|
|
259
|
+
|
|
260
|
+
The API only adds things: new response fields, statuses, and error codes can
|
|
261
|
+
appear without a new version. The SDK ignores unknown fields (they stay in each
|
|
262
|
+
model's `raw` dict) and treats an unknown `status` as still processing and an
|
|
263
|
+
unknown error `code` as an ordinary failure. Write your own code the same way:
|
|
264
|
+
branch on the codes you know and have a default.
|
|
265
|
+
|
|
266
|
+
The SDK follows [semantic versioning](https://semver.org). Releases before 1.0
|
|
267
|
+
may change the SDK's own interface in a minor version, and each change is
|
|
268
|
+
listed in the [changelog](CHANGELOG.md). The HTTP API itself is not versioned
|
|
269
|
+
by the SDK.
|
|
270
|
+
|
|
271
|
+
## Development
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
pip install -e ".[dev]"
|
|
275
|
+
pytest
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
The suite includes an end-to-end test against a local server and a conformance test
|
|
279
|
+
that checks the SDK against `openapi/fastocr-v1.json`. A live smoke test
|
|
280
|
+
is skipped unless `FASTOCR_LIVE_API_KEY` and `FASTOCR_LIVE_BASE_URL` are set; it
|
|
281
|
+
processes one page.
|
|
282
|
+
|
|
283
|
+
## License
|
|
284
|
+
|
|
285
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# FastOCR Python SDK
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/fastocr-sdk/)
|
|
4
|
+
[](https://pypi.org/project/fastocr-sdk/)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
Python client for the [FastOCR Document OCR API](https://fastocr.org/docs/documents).
|
|
8
|
+
Send a scanned PDF, get back its text or a searchable PDF.
|
|
9
|
+
|
|
10
|
+
- Document OCR only: PDF input, up to 1 GiB per file.
|
|
11
|
+
- Sync (`FastOCR`) and async (`AsyncFastOCR`) clients with identical behavior.
|
|
12
|
+
- Streams large files from disk, retries transient failures, and never creates a document twice.
|
|
13
|
+
- One runtime dependency: `httpx`. Python 3.9+.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install fastocr-sdk
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The distribution is `fastocr-sdk` and the import is `fastocr_sdk`. Do not
|
|
22
|
+
install the unrelated `fastocr` package from PyPI; it is a different project.
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from fastocr_sdk import FastOCR
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Get access
|
|
29
|
+
|
|
30
|
+
API access is currently granted per account on request. Email
|
|
31
|
+
[support@fastocr.org](mailto:support@fastocr.org?subject=FastOCR%20API%20Access%20Request)
|
|
32
|
+
from your FastOCR account email. Once approved, create a key at
|
|
33
|
+
<https://fastocr.org/developers/keys>. Approved accounts get 10 free pages per
|
|
34
|
+
month and buy more pages by card on the same page.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
export FASTOCR_API_KEY="your_api_key"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quickstart
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from fastocr_sdk import FastOCR
|
|
44
|
+
|
|
45
|
+
with FastOCR() as client:
|
|
46
|
+
text = client.extract_text("scanned_contract.pdf")
|
|
47
|
+
print(text)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`extract_text` uploads the file, starts OCR, waits for it to finish, and
|
|
51
|
+
returns the text. To keep the job and the searchable PDF:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
with FastOCR() as client:
|
|
55
|
+
job = client.documents.process("scanned_archive.pdf")
|
|
56
|
+
client.documents.download_searchable_pdf(job.id, "searchable.pdf")
|
|
57
|
+
print(job.id, job.status, job.pages_billed)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`job.status` is `completed`, or `partial` if the account ran out of pages
|
|
61
|
+
partway (`job.is_partial`); see [Out of pages](#out-of-pages).
|
|
62
|
+
|
|
63
|
+
## Async
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
import asyncio
|
|
67
|
+
from fastocr_sdk import AsyncFastOCR
|
|
68
|
+
|
|
69
|
+
async def main():
|
|
70
|
+
async with AsyncFastOCR() as client:
|
|
71
|
+
text = await client.extract_text("invoice.pdf")
|
|
72
|
+
print(text)
|
|
73
|
+
|
|
74
|
+
asyncio.run(main())
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Accounts can have 2 documents processing at once by default (10 with a prepaid
|
|
78
|
+
page allowance). When fanning out, cap concurrency with a semaphore, as in
|
|
79
|
+
[`examples/async_batch.py`](examples/async_batch.py).
|
|
80
|
+
|
|
81
|
+
## Step by step
|
|
82
|
+
|
|
83
|
+
`process()` runs these five steps; each is also a method on `client.documents`:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
created = client.documents.create("scan.pdf", size_bytes) # POST /v1/documents
|
|
87
|
+
client.documents.upload(created.upload_url, "scan.pdf") # PUT to the presigned URL
|
|
88
|
+
client.documents.start(created.id) # POST /v1/documents/{id}/start
|
|
89
|
+
job = client.documents.wait_for_completion(created.id) # GET until a final status
|
|
90
|
+
text = client.documents.get_text(created.id) # GET /output?format=text
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Also available: `get`, `list`, `delete`, and `get_output_url`.
|
|
94
|
+
|
|
95
|
+
## Large files
|
|
96
|
+
|
|
97
|
+
Files given as a path (or a seekable binary stream) are streamed from disk in
|
|
98
|
+
1 MiB chunks, so a 1 GiB PDF does not need 1 GiB of memory. The upload is sent
|
|
99
|
+
with a real `Content-Length`, because S3 rejects chunked uploads. A stream that
|
|
100
|
+
cannot seek has no known size, so it is read into memory first.
|
|
101
|
+
|
|
102
|
+
## Errors
|
|
103
|
+
|
|
104
|
+
Every error is a `FastOCRError` with `status_code`, `code`, `type`,
|
|
105
|
+
`request_id` and `message`. Branch on `code`, never on `message`.
|
|
106
|
+
|
|
107
|
+
| Exception | When |
|
|
108
|
+
|---|---|
|
|
109
|
+
| `AuthenticationError` | HTTP 401 or 403: missing, unknown, or malformed API key |
|
|
110
|
+
| `BadRequestError` | HTTP 400: invalid request |
|
|
111
|
+
| `NotFoundError` | HTTP 404: no such document |
|
|
112
|
+
| `DocumentConflictError` | HTTP 409: wrong state, or an idempotency key reused with a different body |
|
|
113
|
+
| `RateLimitError` | HTTP 429, after retries; `retry_after` holds the seconds, if sent |
|
|
114
|
+
| `ServerError` | HTTP 5xx, after retries |
|
|
115
|
+
| `APIConnectionError` | No response (DNS, TLS, reset, timeout), after retries |
|
|
116
|
+
| `UploadError`, `DownloadError` | The presigned upload or download failed |
|
|
117
|
+
| `PollingTimeoutError` | The document did not finish in `timeout` seconds |
|
|
118
|
+
| `DocumentFailedError` | The document reached status `failed` |
|
|
119
|
+
| `PartialResultError` | `extract_text()` finished a document that was only partly processed (out of pages) |
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from fastocr_sdk import DocumentFailedError, FastOCR, FastOCRError
|
|
123
|
+
|
|
124
|
+
with FastOCR() as client:
|
|
125
|
+
try:
|
|
126
|
+
text = client.extract_text("scan.pdf")
|
|
127
|
+
except DocumentFailedError as error:
|
|
128
|
+
print(error.code, error.retryable, error.document_id)
|
|
129
|
+
except FastOCRError as error:
|
|
130
|
+
print(error.status_code, error.code, error.request_id)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Requests rejected by API Gateway before they reach the API (bad or missing key,
|
|
134
|
+
throttling) raise the same classes. Their body is either `{"message": ...}` or,
|
|
135
|
+
on newer deployments, also carries an `error` object with `type`, `code` (for
|
|
136
|
+
example `unauthorized`, `access_denied`, `throttled`) and `request_id`. A
|
|
137
|
+
throttled request has no `Retry-After`, so the SDK backs off 0.5 to 1 second,
|
|
138
|
+
then 1 to 2 seconds, between its retries.
|
|
139
|
+
|
|
140
|
+
A failed document is not an HTTP error: the API answers 200 with
|
|
141
|
+
`status: "failed"`, and the SDK raises `DocumentFailedError` with the failure's
|
|
142
|
+
`code` and `retryable` flag. Treat an unknown code as `processing_failed`.
|
|
143
|
+
|
|
144
|
+
### Out of pages
|
|
145
|
+
|
|
146
|
+
Running out of pages ends in one of two ways, and they are handled differently.
|
|
147
|
+
|
|
148
|
+
**The document fails with `buy_pages`.** Nothing was processed. `process()` and
|
|
149
|
+
`extract_text()` raise `DocumentFailedError` with `error.out_of_pages == True`.
|
|
150
|
+
After pages are added to the account, start the same document again:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
try:
|
|
154
|
+
client.documents.process("scan.pdf")
|
|
155
|
+
except DocumentFailedError as error:
|
|
156
|
+
if error.out_of_pages:
|
|
157
|
+
... # pages added to the account
|
|
158
|
+
client.documents.start(error.document_id)
|
|
159
|
+
job = client.documents.wait_for_completion(error.document_id)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**The document finishes as `partial`.** The pages that fit were processed (the
|
|
163
|
+
first `pages_billed` pages) and the rest were not. `process()` returns the job,
|
|
164
|
+
and `job.is_partial` is true. `extract_text()` never returns truncated text
|
|
165
|
+
silently: it raises `PartialResultError`, which carries `document_id`,
|
|
166
|
+
`pages_processed`, `pages_billed` and `pages_total`.
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
from fastocr_sdk import PartialResultError
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
text = client.extract_text("scan.pdf")
|
|
173
|
+
except PartialResultError as error:
|
|
174
|
+
print(f"{error.pages_billed} of {error.pages_total} pages were processed")
|
|
175
|
+
partial_text = client.documents.get_text(error.document_id)
|
|
176
|
+
|
|
177
|
+
text = client.extract_text("scan.pdf", allow_partial=True) # accept truncated text
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
A partial document cannot be started again (the API answers `409
|
|
181
|
+
document_not_startable`). After pages are added, submit the file again as a new
|
|
182
|
+
document, which is billed as a new document.
|
|
183
|
+
|
|
184
|
+
## Retries and idempotency
|
|
185
|
+
|
|
186
|
+
`FastOCR(max_retries=2)` retries connection errors, HTTP 429 and 502/503/504
|
|
187
|
+
with exponential backoff and jitter. It waits for `Retry-After` when the server
|
|
188
|
+
sends one (up to 60 seconds; longer waits are raised as `RateLimitError`). It
|
|
189
|
+
does not retry other errors, including 400, 401, 403, 404, 409 and 500. Set
|
|
190
|
+
`max_retries=0` to turn retries off. Uploads and downloads of results retry the
|
|
191
|
+
same way.
|
|
192
|
+
|
|
193
|
+
Creating a document is safe to retry because every `create` call sends an
|
|
194
|
+
`Idempotency-Key`, and the same key is reused across all retries of that call.
|
|
195
|
+
`process()` uses one key for the whole call. Pass your own to make your own
|
|
196
|
+
retries safe too:
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
job = client.documents.process("scan.pdf", idempotency_key="invoice-2026-10-001")
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Calling `process()` again with the same key is safe: it finds the same document and,
|
|
203
|
+
if it was already started, just waits for it instead of uploading again.
|
|
204
|
+
|
|
205
|
+
Reusing a key with a different filename, size, `sha256` or `external_id`
|
|
206
|
+
raises `DocumentConflictError` (`idempotency_key_conflict`).
|
|
207
|
+
|
|
208
|
+
## Examples
|
|
209
|
+
|
|
210
|
+
Runnable scripts are in [`examples/`](examples/), and
|
|
211
|
+
[`examples/cookbooks/`](examples/cookbooks/) has LangChain, LlamaIndex, batch
|
|
212
|
+
archiving and cURL recipes.
|
|
213
|
+
|
|
214
|
+
## Configuration
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
FastOCR(
|
|
218
|
+
api_key=None, # defaults to $FASTOCR_API_KEY
|
|
219
|
+
base_url=None, # defaults to $FASTOCR_BASE_URL, then https://api.fastocr.org
|
|
220
|
+
timeout=30.0, # seconds per API request
|
|
221
|
+
max_retries=2,
|
|
222
|
+
)
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Versioning and compatibility
|
|
226
|
+
|
|
227
|
+
The API only adds things: new response fields, statuses, and error codes can
|
|
228
|
+
appear without a new version. The SDK ignores unknown fields (they stay in each
|
|
229
|
+
model's `raw` dict) and treats an unknown `status` as still processing and an
|
|
230
|
+
unknown error `code` as an ordinary failure. Write your own code the same way:
|
|
231
|
+
branch on the codes you know and have a default.
|
|
232
|
+
|
|
233
|
+
The SDK follows [semantic versioning](https://semver.org). Releases before 1.0
|
|
234
|
+
may change the SDK's own interface in a minor version, and each change is
|
|
235
|
+
listed in the [changelog](CHANGELOG.md). The HTTP API itself is not versioned
|
|
236
|
+
by the SDK.
|
|
237
|
+
|
|
238
|
+
## Development
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
pip install -e ".[dev]"
|
|
242
|
+
pytest
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
The suite includes an end-to-end test against a local server and a conformance test
|
|
246
|
+
that checks the SDK against `openapi/fastocr-v1.json`. A live smoke test
|
|
247
|
+
is skipped unless `FASTOCR_LIVE_API_KEY` and `FASTOCR_LIVE_BASE_URL` are set; it
|
|
248
|
+
processes one page.
|
|
249
|
+
|
|
250
|
+
## License
|
|
251
|
+
|
|
252
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""OCR every PDF in a folder, a few at a time, and write a .txt next to each.
|
|
2
|
+
|
|
3
|
+
FASTOCR_API_KEY=... python examples/async_batch.py ./scans
|
|
4
|
+
|
|
5
|
+
New accounts may run 2 documents at once, so keep the limit at 2 unless yours is higher.
|
|
6
|
+
"""
|
|
7
|
+
import asyncio
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from fastocr_sdk import AsyncFastOCR, FastOCRError
|
|
12
|
+
|
|
13
|
+
CONCURRENCY = 2
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def convert(client: AsyncFastOCR, limit: asyncio.Semaphore, pdf: Path) -> None:
|
|
17
|
+
async with limit:
|
|
18
|
+
try:
|
|
19
|
+
text = await client.extract_text(pdf)
|
|
20
|
+
except FastOCRError as error:
|
|
21
|
+
print(f"{pdf.name}: failed ({error})")
|
|
22
|
+
return
|
|
23
|
+
pdf.with_suffix(".txt").write_text(text, encoding="utf-8")
|
|
24
|
+
print(f"{pdf.name}: done")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def main(folder: Path) -> None:
|
|
28
|
+
limit = asyncio.Semaphore(CONCURRENCY)
|
|
29
|
+
async with AsyncFastOCR() as client:
|
|
30
|
+
await asyncio.gather(*(convert(client, limit, pdf) for pdf in sorted(folder.glob("*.pdf"))))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
asyncio.run(main(Path(sys.argv[1])))
|