doc-cheap 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.
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ build/
5
+ dist/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .mypy_cache/
9
+ .ruff_cache/
10
+ .coverage
11
+ htmlcov/
@@ -0,0 +1,7 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ - First release: `Client` with `recognize`, `get_scan`, `list_scans`,
6
+ `delete_scan` and `usage`; typed answers; an exception per API error code;
7
+ the key from an argument or the `DOC_CHEAP_API_KEY` environment variable.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Document Cheap Inc.
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,169 @@
1
+ Metadata-Version: 2.5
2
+ Name: doc-cheap
3
+ Version: 0.1.0
4
+ Summary: Typed Python client for the doc.cheap document-recognition API: read passports, ID cards and driver's licences into JSON.
5
+ Project-URL: Homepage, https://doc.cheap
6
+ Project-URL: Documentation, https://doc.cheap/docs
7
+ Project-URL: Source, https://gitlab.com/doccheap/doc-cheap-python
8
+ Project-URL: Changelog, https://gitlab.com/doccheap/doc-cheap-python/-/blob/main/CHANGELOG.md
9
+ Author-email: Teoh Chin Heng <admin@doc.cheap>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: doc.cheap,document-recognition,id-card,kyc,mrz,ocr,passport
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
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: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.10
25
+ Requires-Dist: httpx<1,>=0.27
26
+ Provides-Extra: dev
27
+ Requires-Dist: mypy>=1.11; extra == 'dev'
28
+ Requires-Dist: pytest-cov>=5; extra == 'dev'
29
+ Requires-Dist: pytest>=8; extra == 'dev'
30
+ Requires-Dist: ruff>=0.6; extra == 'dev'
31
+ Provides-Extra: test
32
+ Requires-Dist: pytest-cov>=5; extra == 'test'
33
+ Requires-Dist: pytest>=8; extra == 'test'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # doc-cheap
37
+
38
+ [![PyPI](https://img.shields.io/pypi/v/doc-cheap.svg)](https://pypi.org/project/doc-cheap/)
39
+
40
+ The Python client for [doc.cheap](https://doc.cheap): it sends a photo or scan
41
+ of a passport, national ID card or driving licence to the doc.cheap API and
42
+ returns the printed fields – name, date of birth, document number, expiry, the
43
+ machine-readable zone – as typed dictionaries.
44
+
45
+ One recognised document costs one credit, $0.01. Registering gives 100 free
46
+ documents every month. An unreadable image, an empty frame or an unsupported
47
+ document type costs nothing, and every answer says whether it was billed.
48
+
49
+ The package needs Python 3.10 or later and depends only on
50
+ [httpx](https://www.python-httpx.org/).
51
+
52
+ ## Installation
53
+
54
+ ```sh
55
+ pip install doc-cheap
56
+ ```
57
+
58
+ ## Quick start
59
+
60
+ ```python
61
+ from doc_cheap import SANDBOX_KEY, Client
62
+
63
+ with Client(SANDBOX_KEY) as client:
64
+ scan = client.recognize(path="passport.jpg")
65
+
66
+ print("status:", scan["meta"]["status"], "billed:", scan["meta"]["billed"])
67
+ if scan["holder"] and scan["holder"]["full_name"]:
68
+ print("holder:", scan["holder"]["full_name"])
69
+ if scan["document"] and scan["document"]["expiry_date"]:
70
+ print("expires:", scan["document"]["expiry_date"])
71
+ ```
72
+
73
+ ## API keys
74
+
75
+ - **Live key** – `sk_live_…`, created in the
76
+ [doc.cheap cabinet](https://doc.cheap/docs/concepts/api-keys-and-sessions).
77
+ - **Sandbox key** – `sk_sandbox_public` (the `doc_cheap.SANDBOX_KEY` constant)
78
+ works without an account. It gives 10 free recognised documents per address
79
+ in all, and at most 10 requests per address an hour, whatever their answer.
80
+
81
+ `Client()` without a key reads it from the `DOC_CHEAP_API_KEY` environment
82
+ variable, which keeps a live key out of source code. The key is sent as a
83
+ Bearer token.
84
+
85
+ ## Methods
86
+
87
+ | Method | Request | What it does |
88
+ | --- | --- | --- |
89
+ | `recognize` | `POST /v1/scans` | Reads one JPEG or PNG image, from a file (`path`), raw bytes (`image`) or base64 (`image_base64`), with an optional `reference` label, `idempotency_key`, `expect_country`, `return_portrait` and `retain_hours`. |
90
+ | `get_scan` | `GET /v1/scans/{id}` | Returns a stored result. |
91
+ | `list_scans` | `GET /v1/scans` | Returns one page of stored results; pass `next_cursor` back as `cursor` for the next page. |
92
+ | `delete_scan` | `DELETE /v1/scans/{id}` | Deletes a stored result. |
93
+ | `usage` | `GET /v1/usage` | Returns this month's free credits, the paid credits, the credits spent and the scan counters. |
94
+
95
+ An image that holds no readable document is not an error: the scan comes back
96
+ with a status such as `unreadable` or `no_document_found` and `billed` set to
97
+ `False`.
98
+
99
+ **Stored results.** A result is stored only when the scan was made with a live
100
+ key under a non-zero retention window, and only until that window ends. Under
101
+ the sandbox key nothing is stored, so `get_scan` and `delete_scan` raise
102
+ `NotFoundError`. None of `get_scan`, `delete_scan` or `usage` charges a credit.
103
+
104
+ ## Errors
105
+
106
+ Every exception the package raises is a `doc_cheap.DocCheapError`. When the API
107
+ answers with an error status, it is an `APIError` carrying the HTTP status, the
108
+ API's error code and its message, narrowed to a subclass you can catch:
109
+
110
+ ```python
111
+ from doc_cheap import APIError, NotFoundError, RateLimitError
112
+
113
+ try:
114
+ scan = client.get_scan(scan_id)
115
+ except NotFoundError:
116
+ scan = None
117
+ except RateLimitError as err:
118
+ print("try again in", err.retry_after, "seconds")
119
+ except APIError as err:
120
+ print(err.status_code, err.code, err.message, err.request_id)
121
+ ```
122
+
123
+ | Exception | When |
124
+ | --- | --- |
125
+ | `InvalidRequestError` | The request was malformed or failed validation (400, 413, 415, 422). |
126
+ | `AuthenticationError` | The key is missing, unknown or revoked (401). |
127
+ | `PermissionDeniedError` | The key may not do this (403). |
128
+ | `InsufficientCreditsError` | The account has no credits left (402). |
129
+ | `NotFoundError` | No such scan (404). |
130
+ | `ConflictError` | An idempotency key is reused or still in progress (409). |
131
+ | `RateLimitError` | Too many requests; `retry_after` says how long to wait (429). |
132
+ | `ServiceUnavailableError` | The service is briefly unavailable (503). |
133
+ | `InternalServerError` | Any other server error (5xx). |
134
+ | `APIConnectionError` | No answer at all: a network failure or a timeout. |
135
+
136
+ An argument the API would refuse anyway – no image, an image over 25 MiB, a
137
+ reference over 128 characters – raises `InvalidArgumentError` (also a
138
+ `ValueError`) before any request is sent.
139
+
140
+ ## Options
141
+
142
+ ```python
143
+ import httpx
144
+ from doc_cheap import Client
145
+
146
+ client = Client(
147
+ timeout=30.0,
148
+ user_agent="my-app/1.0",
149
+ )
150
+
151
+ # Or bring your own httpx.Client for a proxy or a shared connection pool;
152
+ # the doc.cheap client leaves it open when it closes.
153
+ client = Client(http_client=httpx.Client(proxy="http://proxy.internal:3128"))
154
+ ```
155
+
156
+ `base_url` (or the `DOC_CHEAP_API_BASE` environment variable) points the client
157
+ at another address, for example a test server.
158
+
159
+ ## Resources
160
+
161
+ - [doc.cheap documentation](https://doc.cheap/docs)
162
+ - [Your first recognition](https://doc.cheap/docs/start/first-recognition)
163
+ - [From the sandbox to a live key](https://doc.cheap/docs/start/from-sandbox-to-live)
164
+ - [API reference](https://doc.cheap/docs/reference)
165
+ - [Handling errors](https://doc.cheap/docs/guides/handle-errors)
166
+
167
+ ## License
168
+
169
+ [MIT](LICENSE)
@@ -0,0 +1,134 @@
1
+ # doc-cheap
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/doc-cheap.svg)](https://pypi.org/project/doc-cheap/)
4
+
5
+ The Python client for [doc.cheap](https://doc.cheap): it sends a photo or scan
6
+ of a passport, national ID card or driving licence to the doc.cheap API and
7
+ returns the printed fields – name, date of birth, document number, expiry, the
8
+ machine-readable zone – as typed dictionaries.
9
+
10
+ One recognised document costs one credit, $0.01. Registering gives 100 free
11
+ documents every month. An unreadable image, an empty frame or an unsupported
12
+ document type costs nothing, and every answer says whether it was billed.
13
+
14
+ The package needs Python 3.10 or later and depends only on
15
+ [httpx](https://www.python-httpx.org/).
16
+
17
+ ## Installation
18
+
19
+ ```sh
20
+ pip install doc-cheap
21
+ ```
22
+
23
+ ## Quick start
24
+
25
+ ```python
26
+ from doc_cheap import SANDBOX_KEY, Client
27
+
28
+ with Client(SANDBOX_KEY) as client:
29
+ scan = client.recognize(path="passport.jpg")
30
+
31
+ print("status:", scan["meta"]["status"], "billed:", scan["meta"]["billed"])
32
+ if scan["holder"] and scan["holder"]["full_name"]:
33
+ print("holder:", scan["holder"]["full_name"])
34
+ if scan["document"] and scan["document"]["expiry_date"]:
35
+ print("expires:", scan["document"]["expiry_date"])
36
+ ```
37
+
38
+ ## API keys
39
+
40
+ - **Live key** – `sk_live_…`, created in the
41
+ [doc.cheap cabinet](https://doc.cheap/docs/concepts/api-keys-and-sessions).
42
+ - **Sandbox key** – `sk_sandbox_public` (the `doc_cheap.SANDBOX_KEY` constant)
43
+ works without an account. It gives 10 free recognised documents per address
44
+ in all, and at most 10 requests per address an hour, whatever their answer.
45
+
46
+ `Client()` without a key reads it from the `DOC_CHEAP_API_KEY` environment
47
+ variable, which keeps a live key out of source code. The key is sent as a
48
+ Bearer token.
49
+
50
+ ## Methods
51
+
52
+ | Method | Request | What it does |
53
+ | --- | --- | --- |
54
+ | `recognize` | `POST /v1/scans` | Reads one JPEG or PNG image, from a file (`path`), raw bytes (`image`) or base64 (`image_base64`), with an optional `reference` label, `idempotency_key`, `expect_country`, `return_portrait` and `retain_hours`. |
55
+ | `get_scan` | `GET /v1/scans/{id}` | Returns a stored result. |
56
+ | `list_scans` | `GET /v1/scans` | Returns one page of stored results; pass `next_cursor` back as `cursor` for the next page. |
57
+ | `delete_scan` | `DELETE /v1/scans/{id}` | Deletes a stored result. |
58
+ | `usage` | `GET /v1/usage` | Returns this month's free credits, the paid credits, the credits spent and the scan counters. |
59
+
60
+ An image that holds no readable document is not an error: the scan comes back
61
+ with a status such as `unreadable` or `no_document_found` and `billed` set to
62
+ `False`.
63
+
64
+ **Stored results.** A result is stored only when the scan was made with a live
65
+ key under a non-zero retention window, and only until that window ends. Under
66
+ the sandbox key nothing is stored, so `get_scan` and `delete_scan` raise
67
+ `NotFoundError`. None of `get_scan`, `delete_scan` or `usage` charges a credit.
68
+
69
+ ## Errors
70
+
71
+ Every exception the package raises is a `doc_cheap.DocCheapError`. When the API
72
+ answers with an error status, it is an `APIError` carrying the HTTP status, the
73
+ API's error code and its message, narrowed to a subclass you can catch:
74
+
75
+ ```python
76
+ from doc_cheap import APIError, NotFoundError, RateLimitError
77
+
78
+ try:
79
+ scan = client.get_scan(scan_id)
80
+ except NotFoundError:
81
+ scan = None
82
+ except RateLimitError as err:
83
+ print("try again in", err.retry_after, "seconds")
84
+ except APIError as err:
85
+ print(err.status_code, err.code, err.message, err.request_id)
86
+ ```
87
+
88
+ | Exception | When |
89
+ | --- | --- |
90
+ | `InvalidRequestError` | The request was malformed or failed validation (400, 413, 415, 422). |
91
+ | `AuthenticationError` | The key is missing, unknown or revoked (401). |
92
+ | `PermissionDeniedError` | The key may not do this (403). |
93
+ | `InsufficientCreditsError` | The account has no credits left (402). |
94
+ | `NotFoundError` | No such scan (404). |
95
+ | `ConflictError` | An idempotency key is reused or still in progress (409). |
96
+ | `RateLimitError` | Too many requests; `retry_after` says how long to wait (429). |
97
+ | `ServiceUnavailableError` | The service is briefly unavailable (503). |
98
+ | `InternalServerError` | Any other server error (5xx). |
99
+ | `APIConnectionError` | No answer at all: a network failure or a timeout. |
100
+
101
+ An argument the API would refuse anyway – no image, an image over 25 MiB, a
102
+ reference over 128 characters – raises `InvalidArgumentError` (also a
103
+ `ValueError`) before any request is sent.
104
+
105
+ ## Options
106
+
107
+ ```python
108
+ import httpx
109
+ from doc_cheap import Client
110
+
111
+ client = Client(
112
+ timeout=30.0,
113
+ user_agent="my-app/1.0",
114
+ )
115
+
116
+ # Or bring your own httpx.Client for a proxy or a shared connection pool;
117
+ # the doc.cheap client leaves it open when it closes.
118
+ client = Client(http_client=httpx.Client(proxy="http://proxy.internal:3128"))
119
+ ```
120
+
121
+ `base_url` (or the `DOC_CHEAP_API_BASE` environment variable) points the client
122
+ at another address, for example a test server.
123
+
124
+ ## Resources
125
+
126
+ - [doc.cheap documentation](https://doc.cheap/docs)
127
+ - [Your first recognition](https://doc.cheap/docs/start/first-recognition)
128
+ - [From the sandbox to a live key](https://doc.cheap/docs/start/from-sandbox-to-live)
129
+ - [API reference](https://doc.cheap/docs/reference)
130
+ - [Handling errors](https://doc.cheap/docs/guides/handle-errors)
131
+
132
+ ## License
133
+
134
+ [MIT](LICENSE)
@@ -0,0 +1,68 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.25"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "doc-cheap"
7
+ version = "0.1.0"
8
+ description = "Typed Python client for the doc.cheap document-recognition API: read passports, ID cards and driver's licences into JSON."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.10"
13
+ authors = [{ name = "Teoh Chin Heng", email = "admin@doc.cheap" }]
14
+ keywords = ["ocr", "passport", "id-card", "mrz", "document-recognition", "kyc", "doc.cheap"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3 :: Only",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Topic :: Software Development :: Libraries :: Python Modules",
26
+ "Typing :: Typed",
27
+ ]
28
+ dependencies = ["httpx>=0.27,<1"]
29
+
30
+ [project.optional-dependencies]
31
+ test = ["pytest>=8", "pytest-cov>=5"]
32
+ dev = ["pytest>=8", "pytest-cov>=5", "ruff>=0.6", "mypy>=1.11"]
33
+
34
+ [project.urls]
35
+ Homepage = "https://doc.cheap"
36
+ Documentation = "https://doc.cheap/docs"
37
+ Source = "https://gitlab.com/doccheap/doc-cheap-python"
38
+ Changelog = "https://gitlab.com/doccheap/doc-cheap-python/-/blob/main/CHANGELOG.md"
39
+
40
+ [tool.hatch.build.targets.wheel]
41
+ packages = ["src/doc_cheap"]
42
+
43
+ [tool.hatch.build.targets.sdist]
44
+ include = ["src/doc_cheap", "tests", "README.md", "CHANGELOG.md", "LICENSE", "pyproject.toml"]
45
+
46
+ [tool.pytest.ini_options]
47
+ testpaths = ["tests"]
48
+ addopts = "-q"
49
+
50
+ [tool.coverage.run]
51
+ branch = true
52
+ source = ["doc_cheap"]
53
+
54
+ [tool.coverage.report]
55
+ fail_under = 100
56
+ show_missing = true
57
+
58
+ [tool.ruff]
59
+ line-length = 100
60
+ target-version = "py310"
61
+
62
+ [tool.ruff.lint]
63
+ select = ["E", "F", "W", "I", "B", "UP", "SIM", "RUF"]
64
+
65
+ [tool.mypy]
66
+ strict = true
67
+ python_version = "3.10"
68
+ files = ["src", "tests"]
@@ -0,0 +1,99 @@
1
+ """A typed Python client for the doc.cheap document-recognition API.
2
+
3
+ >>> from doc_cheap import Client, SANDBOX_KEY
4
+ >>> with Client(SANDBOX_KEY) as client: # doctest: +SKIP
5
+ ... scan = client.recognize(path="passport.jpg")
6
+ ... print(scan["meta"]["status"], scan["holder"])
7
+ """
8
+
9
+ from ._client import (
10
+ API_KEY_ENV,
11
+ BASE_URL_ENV,
12
+ DEFAULT_BASE_URL,
13
+ MAX_IMAGE_BYTES,
14
+ SANDBOX_KEY,
15
+ Client,
16
+ )
17
+ from ._errors import (
18
+ APIConnectionError,
19
+ APIError,
20
+ AuthenticationError,
21
+ ConflictError,
22
+ DocCheapError,
23
+ InsufficientCreditsError,
24
+ InternalServerError,
25
+ InvalidArgumentError,
26
+ InvalidRequestError,
27
+ NotFoundError,
28
+ PermissionDeniedError,
29
+ RateLimitError,
30
+ ServiceUnavailableError,
31
+ )
32
+ from ._types import (
33
+ Confidence,
34
+ CreditsByKind,
35
+ FreeAllowance,
36
+ Scan,
37
+ ScanAuthenticity,
38
+ ScanCheck,
39
+ ScanDeletion,
40
+ ScanDocument,
41
+ ScanField,
42
+ ScanHolder,
43
+ ScanImages,
44
+ ScanList,
45
+ ScanMeta,
46
+ ScanMRZ,
47
+ ScanQuality,
48
+ ScanSummary,
49
+ ScanTiming,
50
+ Status,
51
+ Usage,
52
+ UsagePeriod,
53
+ UsageScans,
54
+ )
55
+ from ._version import __version__
56
+
57
+ __all__ = [
58
+ "API_KEY_ENV",
59
+ "BASE_URL_ENV",
60
+ "DEFAULT_BASE_URL",
61
+ "MAX_IMAGE_BYTES",
62
+ "SANDBOX_KEY",
63
+ "APIConnectionError",
64
+ "APIError",
65
+ "AuthenticationError",
66
+ "Client",
67
+ "Confidence",
68
+ "ConflictError",
69
+ "CreditsByKind",
70
+ "DocCheapError",
71
+ "FreeAllowance",
72
+ "InsufficientCreditsError",
73
+ "InternalServerError",
74
+ "InvalidArgumentError",
75
+ "InvalidRequestError",
76
+ "NotFoundError",
77
+ "PermissionDeniedError",
78
+ "RateLimitError",
79
+ "Scan",
80
+ "ScanAuthenticity",
81
+ "ScanCheck",
82
+ "ScanDeletion",
83
+ "ScanDocument",
84
+ "ScanField",
85
+ "ScanHolder",
86
+ "ScanImages",
87
+ "ScanList",
88
+ "ScanMRZ",
89
+ "ScanMeta",
90
+ "ScanQuality",
91
+ "ScanSummary",
92
+ "ScanTiming",
93
+ "ServiceUnavailableError",
94
+ "Status",
95
+ "Usage",
96
+ "UsagePeriod",
97
+ "UsageScans",
98
+ "__version__",
99
+ ]