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.
Files changed (34) hide show
  1. fastocr_sdk-0.1.0/.gitignore +9 -0
  2. fastocr_sdk-0.1.0/CHANGELOG.md +35 -0
  3. fastocr_sdk-0.1.0/LICENSE +21 -0
  4. fastocr_sdk-0.1.0/PKG-INFO +285 -0
  5. fastocr_sdk-0.1.0/README.md +252 -0
  6. fastocr_sdk-0.1.0/examples/async_batch.py +33 -0
  7. fastocr_sdk-0.1.0/examples/cookbooks/01_langchain_scanned_pdf_rag/rag_pipeline.py +82 -0
  8. fastocr_sdk-0.1.0/examples/cookbooks/02_llamaindex_multilingual_reader/custom_reader.py +43 -0
  9. fastocr_sdk-0.1.0/examples/cookbooks/03_searchable_pdf_compliance_archive/batch_archive.py +47 -0
  10. fastocr_sdk-0.1.0/examples/cookbooks/04_local_curl_recipes/extract_raw_text.sh +70 -0
  11. fastocr_sdk-0.1.0/examples/cookbooks/README.md +30 -0
  12. fastocr_sdk-0.1.0/examples/extract_text.py +10 -0
  13. fastocr_sdk-0.1.0/examples/handle_errors.py +32 -0
  14. fastocr_sdk-0.1.0/examples/searchable_pdf.py +15 -0
  15. fastocr_sdk-0.1.0/pyproject.toml +60 -0
  16. fastocr_sdk-0.1.0/src/fastocr_sdk/__init__.py +53 -0
  17. fastocr_sdk-0.1.0/src/fastocr_sdk/_core.py +318 -0
  18. fastocr_sdk-0.1.0/src/fastocr_sdk/_version.py +1 -0
  19. fastocr_sdk-0.1.0/src/fastocr_sdk/async_client.py +290 -0
  20. fastocr_sdk-0.1.0/src/fastocr_sdk/client.py +289 -0
  21. fastocr_sdk-0.1.0/src/fastocr_sdk/errors.py +145 -0
  22. fastocr_sdk-0.1.0/src/fastocr_sdk/models.py +140 -0
  23. fastocr_sdk-0.1.0/src/fastocr_sdk/py.typed +0 -0
  24. fastocr_sdk-0.1.0/tests/conftest.py +51 -0
  25. fastocr_sdk-0.1.0/tests/fake_server.py +143 -0
  26. fastocr_sdk-0.1.0/tests/test_cookbooks.py +121 -0
  27. fastocr_sdk-0.1.0/tests/test_documents.py +542 -0
  28. fastocr_sdk-0.1.0/tests/test_end_to_end.py +157 -0
  29. fastocr_sdk-0.1.0/tests/test_errors.py +262 -0
  30. fastocr_sdk-0.1.0/tests/test_examples.py +74 -0
  31. fastocr_sdk-0.1.0/tests/test_live_smoke.py +80 -0
  32. fastocr_sdk-0.1.0/tests/test_models.py +35 -0
  33. fastocr_sdk-0.1.0/tests/test_openapi_conformance.py +475 -0
  34. fastocr_sdk-0.1.0/tests/test_retries.py +364 -0
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .pytest_cache/
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .venv/
8
+ venv/
9
+ .env
@@ -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
+ [![PyPI](https://img.shields.io/pypi/v/fastocr-sdk.svg)](https://pypi.org/project/fastocr-sdk/)
37
+ [![Python](https://img.shields.io/pypi/pyversions/fastocr-sdk.svg)](https://pypi.org/project/fastocr-sdk/)
38
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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
+ [![PyPI](https://img.shields.io/pypi/v/fastocr-sdk.svg)](https://pypi.org/project/fastocr-sdk/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/fastocr-sdk.svg)](https://pypi.org/project/fastocr-sdk/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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])))