structocr 1.5.0__tar.gz → 1.6.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.
- {structocr-1.5.0/structocr.egg-info → structocr-1.6.0}/PKG-INFO +37 -2
- {structocr-1.5.0 → structocr-1.6.0}/README.md +36 -1
- {structocr-1.5.0 → structocr-1.6.0}/setup.py +3 -1
- structocr-1.6.0/structocr/__init__.py +4 -0
- structocr-1.6.0/structocr/client.py +233 -0
- structocr-1.6.0/structocr/version.py +1 -0
- {structocr-1.5.0 → structocr-1.6.0/structocr.egg-info}/PKG-INFO +37 -2
- {structocr-1.5.0 → structocr-1.6.0}/structocr.egg-info/SOURCES.txt +1 -0
- structocr-1.5.0/structocr/__init__.py +0 -5
- structocr-1.5.0/structocr/client.py +0 -125
- {structocr-1.5.0 → structocr-1.6.0}/LICENSE +0 -0
- {structocr-1.5.0 → structocr-1.6.0}/MANIFEST.in +0 -0
- {structocr-1.5.0 → structocr-1.6.0}/pyproject.toml +0 -0
- {structocr-1.5.0 → structocr-1.6.0}/setup.cfg +0 -0
- {structocr-1.5.0 → structocr-1.6.0}/structocr.egg-info/dependency_links.txt +0 -0
- {structocr-1.5.0 → structocr-1.6.0}/structocr.egg-info/requires.txt +0 -0
- {structocr-1.5.0 → structocr-1.6.0}/structocr.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: structocr
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.0
|
|
4
4
|
Summary: Official Python SDK for StructOCR Base64 document APIs, including images, PDFs, and account balance.
|
|
5
5
|
Home-page: https://structocr.com
|
|
6
6
|
Author: StructOCR Team
|
|
@@ -35,7 +35,7 @@ Dynamic: summary
|
|
|
35
35
|
|
|
36
36
|
Official Python client for the [StructOCR API](https://structocr.com/developers).
|
|
37
37
|
|
|
38
|
-
The SDK accepts a local JPG, PNG, WebP, or PDF path, plus in-memory `bytes`. It validates the decoded file locally, converts it to Base64, and sends
|
|
38
|
+
The SDK accepts a local JPG, PNG, WebP, or PDF path, plus in-memory `bytes`. It validates the decoded file locally, converts it to Base64, and sends JSON as `{"img": "..."}`. The REST API also supports multipart uploads; this SDK release keeps Base64 JSON as its default transport for backward compatibility.
|
|
39
39
|
|
|
40
40
|
## Install
|
|
41
41
|
|
|
@@ -90,11 +90,31 @@ scan_container(file)
|
|
|
90
90
|
scan_license_plate(file)
|
|
91
91
|
scan_vehicle_registration(file)
|
|
92
92
|
scan_atm_cassette(file)
|
|
93
|
+
scan_weighbridge_ticket(file)
|
|
93
94
|
get_account_balance()
|
|
94
95
|
```
|
|
95
96
|
|
|
96
97
|
All document methods accept a local path or bytes. Supported decoded formats are JPG, PNG, WebP, and PDF, up to 4.5MB.
|
|
97
98
|
|
|
99
|
+
Receipt v2 provides a richer response. Enhanced accuracy requires v2 and costs 2 credits instead of the standard 1 credit:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
receipt = client.scan_receipt(
|
|
103
|
+
"./receipt.jpg",
|
|
104
|
+
response_version=2,
|
|
105
|
+
accuracy="enhanced",
|
|
106
|
+
)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Weighbridge ticket example:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
result = client.scan_weighbridge_ticket("./weighbridge-ticket.jpg")
|
|
113
|
+
if result.get("success"):
|
|
114
|
+
print(result["data"]["weights"])
|
|
115
|
+
print(result["data"]["validation"])
|
|
116
|
+
```
|
|
117
|
+
|
|
98
118
|
## Configuration
|
|
99
119
|
|
|
100
120
|
```python
|
|
@@ -107,6 +127,21 @@ client = StructOCR(
|
|
|
107
127
|
|
|
108
128
|
See the [API documentation](https://structocr.com/developers) for endpoint-specific response schemas and error codes.
|
|
109
129
|
|
|
130
|
+
## Errors
|
|
131
|
+
|
|
132
|
+
API and network failures raise `StructOCRError`. Existing `except RuntimeError` code continues to work because `StructOCRError` extends `RuntimeError`.
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from structocr import StructOCRError
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
client.scan_passport("./passport.jpg")
|
|
139
|
+
except StructOCRError as error:
|
|
140
|
+
print(error.status_code, error.code, error.retryable)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`retryable` is advisory only. The SDK does not automatically retry OCR requests because doing so without an idempotency key could charge a request twice.
|
|
144
|
+
|
|
110
145
|
## License
|
|
111
146
|
|
|
112
147
|
MIT
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Official Python client for the [StructOCR API](https://structocr.com/developers).
|
|
4
4
|
|
|
5
|
-
The SDK accepts a local JPG, PNG, WebP, or PDF path, plus in-memory `bytes`. It validates the decoded file locally, converts it to Base64, and sends
|
|
5
|
+
The SDK accepts a local JPG, PNG, WebP, or PDF path, plus in-memory `bytes`. It validates the decoded file locally, converts it to Base64, and sends JSON as `{"img": "..."}`. The REST API also supports multipart uploads; this SDK release keeps Base64 JSON as its default transport for backward compatibility.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -57,11 +57,31 @@ scan_container(file)
|
|
|
57
57
|
scan_license_plate(file)
|
|
58
58
|
scan_vehicle_registration(file)
|
|
59
59
|
scan_atm_cassette(file)
|
|
60
|
+
scan_weighbridge_ticket(file)
|
|
60
61
|
get_account_balance()
|
|
61
62
|
```
|
|
62
63
|
|
|
63
64
|
All document methods accept a local path or bytes. Supported decoded formats are JPG, PNG, WebP, and PDF, up to 4.5MB.
|
|
64
65
|
|
|
66
|
+
Receipt v2 provides a richer response. Enhanced accuracy requires v2 and costs 2 credits instead of the standard 1 credit:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
receipt = client.scan_receipt(
|
|
70
|
+
"./receipt.jpg",
|
|
71
|
+
response_version=2,
|
|
72
|
+
accuracy="enhanced",
|
|
73
|
+
)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Weighbridge ticket example:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
result = client.scan_weighbridge_ticket("./weighbridge-ticket.jpg")
|
|
80
|
+
if result.get("success"):
|
|
81
|
+
print(result["data"]["weights"])
|
|
82
|
+
print(result["data"]["validation"])
|
|
83
|
+
```
|
|
84
|
+
|
|
65
85
|
## Configuration
|
|
66
86
|
|
|
67
87
|
```python
|
|
@@ -74,6 +94,21 @@ client = StructOCR(
|
|
|
74
94
|
|
|
75
95
|
See the [API documentation](https://structocr.com/developers) for endpoint-specific response schemas and error codes.
|
|
76
96
|
|
|
97
|
+
## Errors
|
|
98
|
+
|
|
99
|
+
API and network failures raise `StructOCRError`. Existing `except RuntimeError` code continues to work because `StructOCRError` extends `RuntimeError`.
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from structocr import StructOCRError
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
client.scan_passport("./passport.jpg")
|
|
106
|
+
except StructOCRError as error:
|
|
107
|
+
print(error.status_code, error.code, error.retryable)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`retryable` is advisory only. The SDK does not automatically retry OCR requests because doing so without an idempotency key could charge a request twice.
|
|
111
|
+
|
|
77
112
|
## License
|
|
78
113
|
|
|
79
114
|
MIT
|
|
@@ -3,10 +3,12 @@ from pathlib import Path
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
ROOT = Path(__file__).parent
|
|
6
|
+
VERSION = {}
|
|
7
|
+
exec((ROOT / "structocr" / "version.py").read_text(encoding="utf-8"), VERSION)
|
|
6
8
|
|
|
7
9
|
setup(
|
|
8
10
|
name="structocr",
|
|
9
|
-
version="
|
|
11
|
+
version=VERSION["__version__"],
|
|
10
12
|
description="Official Python SDK for StructOCR Base64 document APIs, including images, PDFs, and account balance.",
|
|
11
13
|
long_description=(ROOT / "README.md").read_text(encoding="utf-8"),
|
|
12
14
|
long_description_content_type="text/markdown",
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Dict, Optional, Union
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
from .version import __version__
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
FileInput = Union[str, os.PathLike, bytes, bytearray, memoryview]
|
|
12
|
+
MAX_FILE_SIZE = int(4.5 * 1024 * 1024)
|
|
13
|
+
SUPPORTED_FORMATS = "JPG, PNG, WebP, and PDF"
|
|
14
|
+
RETRYABLE_STATUS_CODES = {429, 502, 503, 504}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class StructOCRError(RuntimeError):
|
|
18
|
+
"""Structured SDK error for API, network, and client failures."""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
message: str,
|
|
23
|
+
*,
|
|
24
|
+
status_code: Optional[int] = None,
|
|
25
|
+
code: Optional[str] = None,
|
|
26
|
+
details: Any = None,
|
|
27
|
+
retryable: bool = False,
|
|
28
|
+
) -> None:
|
|
29
|
+
super().__init__(message)
|
|
30
|
+
self.status_code = status_code
|
|
31
|
+
self.code = code
|
|
32
|
+
self.details = details
|
|
33
|
+
self.retryable = retryable
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class StructOCR:
|
|
37
|
+
"""Official Python client for the StructOCR Base64 JSON API."""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
api_key: Optional[str] = None,
|
|
42
|
+
base_url: str = "https://api.structocr.com/v1",
|
|
43
|
+
timeout: float = 30.0,
|
|
44
|
+
) -> None:
|
|
45
|
+
self.api_key = api_key or os.environ.get("STRUCTOCR_API_KEY")
|
|
46
|
+
if not self.api_key:
|
|
47
|
+
raise ValueError("API Key is required. Get one at https://structocr.com")
|
|
48
|
+
|
|
49
|
+
self.base_url = base_url.rstrip("/")
|
|
50
|
+
self.timeout = timeout
|
|
51
|
+
self.session = requests.Session()
|
|
52
|
+
self.session.headers.update({
|
|
53
|
+
"x-api-key": self.api_key,
|
|
54
|
+
"Content-Type": "application/json",
|
|
55
|
+
"User-Agent": f"StructOCR-Python/{__version__}",
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
def _read_file(file: FileInput) -> bytes:
|
|
60
|
+
if isinstance(file, (bytes, bytearray, memoryview)):
|
|
61
|
+
content = bytes(file)
|
|
62
|
+
else:
|
|
63
|
+
path = Path(file)
|
|
64
|
+
if not path.is_file():
|
|
65
|
+
raise FileNotFoundError(f"File not found: {path}")
|
|
66
|
+
content = path.read_bytes()
|
|
67
|
+
|
|
68
|
+
if not content:
|
|
69
|
+
raise ValueError("File is empty")
|
|
70
|
+
if len(content) > MAX_FILE_SIZE:
|
|
71
|
+
raise ValueError("File exceeds the maximum allowed size of 4.5MB")
|
|
72
|
+
if StructOCR._detect_mime(content) is None:
|
|
73
|
+
raise ValueError(f"Unsupported file format. Supported formats: {SUPPORTED_FORMATS}")
|
|
74
|
+
return content
|
|
75
|
+
|
|
76
|
+
@staticmethod
|
|
77
|
+
def _detect_mime(content: bytes) -> Optional[str]:
|
|
78
|
+
if content.startswith(b"%PDF"):
|
|
79
|
+
return "application/pdf"
|
|
80
|
+
if content.startswith(b"\xff\xd8\xff"):
|
|
81
|
+
return "image/jpeg"
|
|
82
|
+
if content.startswith(b"\x89PNG\r\n\x1a\n"):
|
|
83
|
+
return "image/png"
|
|
84
|
+
if len(content) >= 12 and content[:4] == b"RIFF" and content[8:12] == b"WEBP":
|
|
85
|
+
return "image/webp"
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def _response_data(response: requests.Response) -> Dict[str, Any]:
|
|
90
|
+
try:
|
|
91
|
+
data = response.json()
|
|
92
|
+
except ValueError as error:
|
|
93
|
+
raise StructOCRError(
|
|
94
|
+
"StructOCR API returned an invalid JSON response",
|
|
95
|
+
status_code=getattr(response, "status_code", None),
|
|
96
|
+
code="INVALID_RESPONSE",
|
|
97
|
+
) from error
|
|
98
|
+
if not isinstance(data, dict):
|
|
99
|
+
raise StructOCRError(
|
|
100
|
+
"StructOCR API returned an invalid response object",
|
|
101
|
+
status_code=getattr(response, "status_code", None),
|
|
102
|
+
code="INVALID_RESPONSE",
|
|
103
|
+
details=data,
|
|
104
|
+
)
|
|
105
|
+
return data
|
|
106
|
+
|
|
107
|
+
@staticmethod
|
|
108
|
+
def _raise_api_error(response: requests.Response, error: Exception) -> None:
|
|
109
|
+
try:
|
|
110
|
+
details = response.json()
|
|
111
|
+
except ValueError:
|
|
112
|
+
details = None
|
|
113
|
+
status = getattr(response, "status_code", None)
|
|
114
|
+
status = status if isinstance(status, int) else None
|
|
115
|
+
code = None
|
|
116
|
+
message = None
|
|
117
|
+
if isinstance(details, dict):
|
|
118
|
+
code = details.get("code") or details.get("error")
|
|
119
|
+
message = details.get("message")
|
|
120
|
+
if not message:
|
|
121
|
+
message = f"StructOCR API request failed with HTTP {status or 'unknown'}"
|
|
122
|
+
raise StructOCRError(
|
|
123
|
+
message,
|
|
124
|
+
status_code=status,
|
|
125
|
+
code=code,
|
|
126
|
+
details=details,
|
|
127
|
+
retryable=status in RETRYABLE_STATUS_CODES,
|
|
128
|
+
) from error
|
|
129
|
+
|
|
130
|
+
def _post_image(
|
|
131
|
+
self,
|
|
132
|
+
endpoint: str,
|
|
133
|
+
file: FileInput,
|
|
134
|
+
params: Optional[Dict[str, Any]] = None,
|
|
135
|
+
) -> Dict[str, Any]:
|
|
136
|
+
"""Read a local file or bytes and send it as Base64 JSON in ``img``."""
|
|
137
|
+
content = self._read_file(file)
|
|
138
|
+
payload = {"img": base64.b64encode(content).decode("ascii")}
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
request_kwargs: Dict[str, Any] = {"json": payload, "timeout": self.timeout}
|
|
142
|
+
if params is not None:
|
|
143
|
+
request_kwargs["params"] = params
|
|
144
|
+
response = self.session.post(f"{self.base_url}/{endpoint}", **request_kwargs)
|
|
145
|
+
except requests.exceptions.RequestException as error:
|
|
146
|
+
raise StructOCRError(
|
|
147
|
+
f"Network error while calling StructOCR API: {error}",
|
|
148
|
+
code="NETWORK_ERROR",
|
|
149
|
+
retryable=True,
|
|
150
|
+
) from error
|
|
151
|
+
try:
|
|
152
|
+
response.raise_for_status()
|
|
153
|
+
except requests.exceptions.RequestException as error:
|
|
154
|
+
self._raise_api_error(response, error)
|
|
155
|
+
return self._response_data(response)
|
|
156
|
+
|
|
157
|
+
def get_account_balance(self) -> Dict[str, Any]:
|
|
158
|
+
"""Return account-level and current-key usage from ``/account/balance``."""
|
|
159
|
+
try:
|
|
160
|
+
response = self.session.get(
|
|
161
|
+
f"{self.base_url}/account/balance",
|
|
162
|
+
timeout=self.timeout,
|
|
163
|
+
)
|
|
164
|
+
except requests.exceptions.RequestException as error:
|
|
165
|
+
raise StructOCRError(
|
|
166
|
+
f"Network error while calling StructOCR API: {error}",
|
|
167
|
+
code="NETWORK_ERROR",
|
|
168
|
+
retryable=True,
|
|
169
|
+
) from error
|
|
170
|
+
try:
|
|
171
|
+
response.raise_for_status()
|
|
172
|
+
except requests.exceptions.RequestException as error:
|
|
173
|
+
self._raise_api_error(response, error)
|
|
174
|
+
return self._response_data(response)
|
|
175
|
+
|
|
176
|
+
def scan_passport(self, file: FileInput) -> Dict[str, Any]:
|
|
177
|
+
return self._post_image("passport", file)
|
|
178
|
+
|
|
179
|
+
def scan_national_id(self, file: FileInput) -> Dict[str, Any]:
|
|
180
|
+
return self._post_image("national-id", file)
|
|
181
|
+
|
|
182
|
+
def scan_driver_license(self, file: FileInput) -> Dict[str, Any]:
|
|
183
|
+
return self._post_image("driver-license", file)
|
|
184
|
+
|
|
185
|
+
def scan_invoice(self, file: FileInput) -> Dict[str, Any]:
|
|
186
|
+
return self._post_image("invoice", file)
|
|
187
|
+
|
|
188
|
+
def scan_vin(self, file: FileInput) -> Dict[str, Any]:
|
|
189
|
+
return self._post_image("vin", file)
|
|
190
|
+
|
|
191
|
+
def scan_container(self, file: FileInput) -> Dict[str, Any]:
|
|
192
|
+
return self._post_image("container", file)
|
|
193
|
+
|
|
194
|
+
def scan_hin(self, file: FileInput) -> Dict[str, Any]:
|
|
195
|
+
return self._post_image("hin", file)
|
|
196
|
+
|
|
197
|
+
def scan_receipt(
|
|
198
|
+
self,
|
|
199
|
+
file: FileInput,
|
|
200
|
+
response_version: int = 1,
|
|
201
|
+
accuracy: str = "standard",
|
|
202
|
+
) -> Dict[str, Any]:
|
|
203
|
+
if response_version not in (1, 2):
|
|
204
|
+
raise StructOCRError("response_version must be 1 or 2", code="INVALID_OPTIONS")
|
|
205
|
+
if accuracy not in ("standard", "enhanced"):
|
|
206
|
+
raise StructOCRError(
|
|
207
|
+
'accuracy must be "standard" or "enhanced"',
|
|
208
|
+
code="INVALID_OPTIONS",
|
|
209
|
+
)
|
|
210
|
+
if accuracy == "enhanced" and response_version != 2:
|
|
211
|
+
raise StructOCRError(
|
|
212
|
+
"Enhanced accuracy requires response_version=2",
|
|
213
|
+
code="INVALID_OPTIONS",
|
|
214
|
+
)
|
|
215
|
+
if response_version == 1 and accuracy == "standard":
|
|
216
|
+
return self._post_image("receipt", file)
|
|
217
|
+
return self._post_image(
|
|
218
|
+
"receipt",
|
|
219
|
+
file,
|
|
220
|
+
params={"response_version": response_version, "accuracy": accuracy},
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
def scan_license_plate(self, file: FileInput) -> Dict[str, Any]:
|
|
224
|
+
return self._post_image("license-plate", file)
|
|
225
|
+
|
|
226
|
+
def scan_vehicle_registration(self, file: FileInput) -> Dict[str, Any]:
|
|
227
|
+
return self._post_image("vehicle-registration", file)
|
|
228
|
+
|
|
229
|
+
def scan_atm_cassette(self, file: FileInput) -> Dict[str, Any]:
|
|
230
|
+
return self._post_image("atm-cassette", file)
|
|
231
|
+
|
|
232
|
+
def scan_weighbridge_ticket(self, file: FileInput) -> Dict[str, Any]:
|
|
233
|
+
return self._post_image("weighbridge-ticket", file)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.6.0"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: structocr
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.0
|
|
4
4
|
Summary: Official Python SDK for StructOCR Base64 document APIs, including images, PDFs, and account balance.
|
|
5
5
|
Home-page: https://structocr.com
|
|
6
6
|
Author: StructOCR Team
|
|
@@ -35,7 +35,7 @@ Dynamic: summary
|
|
|
35
35
|
|
|
36
36
|
Official Python client for the [StructOCR API](https://structocr.com/developers).
|
|
37
37
|
|
|
38
|
-
The SDK accepts a local JPG, PNG, WebP, or PDF path, plus in-memory `bytes`. It validates the decoded file locally, converts it to Base64, and sends
|
|
38
|
+
The SDK accepts a local JPG, PNG, WebP, or PDF path, plus in-memory `bytes`. It validates the decoded file locally, converts it to Base64, and sends JSON as `{"img": "..."}`. The REST API also supports multipart uploads; this SDK release keeps Base64 JSON as its default transport for backward compatibility.
|
|
39
39
|
|
|
40
40
|
## Install
|
|
41
41
|
|
|
@@ -90,11 +90,31 @@ scan_container(file)
|
|
|
90
90
|
scan_license_plate(file)
|
|
91
91
|
scan_vehicle_registration(file)
|
|
92
92
|
scan_atm_cassette(file)
|
|
93
|
+
scan_weighbridge_ticket(file)
|
|
93
94
|
get_account_balance()
|
|
94
95
|
```
|
|
95
96
|
|
|
96
97
|
All document methods accept a local path or bytes. Supported decoded formats are JPG, PNG, WebP, and PDF, up to 4.5MB.
|
|
97
98
|
|
|
99
|
+
Receipt v2 provides a richer response. Enhanced accuracy requires v2 and costs 2 credits instead of the standard 1 credit:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
receipt = client.scan_receipt(
|
|
103
|
+
"./receipt.jpg",
|
|
104
|
+
response_version=2,
|
|
105
|
+
accuracy="enhanced",
|
|
106
|
+
)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Weighbridge ticket example:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
result = client.scan_weighbridge_ticket("./weighbridge-ticket.jpg")
|
|
113
|
+
if result.get("success"):
|
|
114
|
+
print(result["data"]["weights"])
|
|
115
|
+
print(result["data"]["validation"])
|
|
116
|
+
```
|
|
117
|
+
|
|
98
118
|
## Configuration
|
|
99
119
|
|
|
100
120
|
```python
|
|
@@ -107,6 +127,21 @@ client = StructOCR(
|
|
|
107
127
|
|
|
108
128
|
See the [API documentation](https://structocr.com/developers) for endpoint-specific response schemas and error codes.
|
|
109
129
|
|
|
130
|
+
## Errors
|
|
131
|
+
|
|
132
|
+
API and network failures raise `StructOCRError`. Existing `except RuntimeError` code continues to work because `StructOCRError` extends `RuntimeError`.
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from structocr import StructOCRError
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
client.scan_passport("./passport.jpg")
|
|
139
|
+
except StructOCRError as error:
|
|
140
|
+
print(error.status_code, error.code, error.retryable)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`retryable` is advisory only. The SDK does not automatically retry OCR requests because doing so without an idempotency key could charge a request twice.
|
|
144
|
+
|
|
110
145
|
## License
|
|
111
146
|
|
|
112
147
|
MIT
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import base64
|
|
2
|
-
import os
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
from typing import Any, Dict, Optional, Union
|
|
5
|
-
|
|
6
|
-
import requests
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
FileInput = Union[str, os.PathLike, bytes, bytearray, memoryview]
|
|
10
|
-
MAX_FILE_SIZE = int(4.5 * 1024 * 1024)
|
|
11
|
-
SUPPORTED_FORMATS = "JPG, PNG, WebP, and PDF"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class StructOCR:
|
|
15
|
-
"""Official Python client for the StructOCR Base64 JSON API."""
|
|
16
|
-
|
|
17
|
-
def __init__(
|
|
18
|
-
self,
|
|
19
|
-
api_key: Optional[str] = None,
|
|
20
|
-
base_url: str = "https://api.structocr.com/v1",
|
|
21
|
-
timeout: float = 30.0,
|
|
22
|
-
) -> None:
|
|
23
|
-
self.api_key = api_key or os.environ.get("STRUCTOCR_API_KEY")
|
|
24
|
-
if not self.api_key:
|
|
25
|
-
raise ValueError("API Key is required. Get one at https://structocr.com")
|
|
26
|
-
|
|
27
|
-
self.base_url = base_url.rstrip("/")
|
|
28
|
-
self.timeout = timeout
|
|
29
|
-
self.session = requests.Session()
|
|
30
|
-
self.session.headers.update({
|
|
31
|
-
"x-api-key": self.api_key,
|
|
32
|
-
"Content-Type": "application/json",
|
|
33
|
-
"User-Agent": "StructOCR-Python/1.5.0",
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
@staticmethod
|
|
37
|
-
def _read_file(file: FileInput) -> bytes:
|
|
38
|
-
if isinstance(file, (bytes, bytearray, memoryview)):
|
|
39
|
-
content = bytes(file)
|
|
40
|
-
else:
|
|
41
|
-
path = Path(file)
|
|
42
|
-
if not path.is_file():
|
|
43
|
-
raise FileNotFoundError(f"File not found: {path}")
|
|
44
|
-
content = path.read_bytes()
|
|
45
|
-
|
|
46
|
-
if not content:
|
|
47
|
-
raise ValueError("File is empty")
|
|
48
|
-
if len(content) > MAX_FILE_SIZE:
|
|
49
|
-
raise ValueError("File exceeds the maximum allowed size of 4.5MB")
|
|
50
|
-
if StructOCR._detect_mime(content) is None:
|
|
51
|
-
raise ValueError(f"Unsupported file format. Supported formats: {SUPPORTED_FORMATS}")
|
|
52
|
-
return content
|
|
53
|
-
|
|
54
|
-
@staticmethod
|
|
55
|
-
def _detect_mime(content: bytes) -> Optional[str]:
|
|
56
|
-
if content.startswith(b"%PDF"):
|
|
57
|
-
return "application/pdf"
|
|
58
|
-
if content.startswith(b"\xff\xd8\xff"):
|
|
59
|
-
return "image/jpeg"
|
|
60
|
-
if content.startswith(b"\x89PNG\r\n\x1a\n"):
|
|
61
|
-
return "image/png"
|
|
62
|
-
if len(content) >= 12 and content[:4] == b"RIFF" and content[8:12] == b"WEBP":
|
|
63
|
-
return "image/webp"
|
|
64
|
-
return None
|
|
65
|
-
|
|
66
|
-
def _post_image(self, endpoint: str, file: FileInput) -> Dict[str, Any]:
|
|
67
|
-
"""Read a local file or bytes and send it as Base64 JSON in ``img``."""
|
|
68
|
-
content = self._read_file(file)
|
|
69
|
-
payload = {"img": base64.b64encode(content).decode("ascii")}
|
|
70
|
-
|
|
71
|
-
try:
|
|
72
|
-
response = self.session.post(
|
|
73
|
-
f"{self.base_url}/{endpoint}",
|
|
74
|
-
json=payload,
|
|
75
|
-
timeout=self.timeout,
|
|
76
|
-
)
|
|
77
|
-
response.raise_for_status()
|
|
78
|
-
return response.json()
|
|
79
|
-
except requests.exceptions.RequestException as error:
|
|
80
|
-
raise RuntimeError(f"API request failed: {error}") from error
|
|
81
|
-
|
|
82
|
-
def get_account_balance(self) -> Dict[str, Any]:
|
|
83
|
-
"""Return account-level and current-key usage from ``/account/balance``."""
|
|
84
|
-
try:
|
|
85
|
-
response = self.session.get(
|
|
86
|
-
f"{self.base_url}/account/balance",
|
|
87
|
-
timeout=self.timeout,
|
|
88
|
-
)
|
|
89
|
-
response.raise_for_status()
|
|
90
|
-
return response.json()
|
|
91
|
-
except requests.exceptions.RequestException as error:
|
|
92
|
-
raise RuntimeError(f"API request failed: {error}") from error
|
|
93
|
-
|
|
94
|
-
def scan_passport(self, file: FileInput) -> Dict[str, Any]:
|
|
95
|
-
return self._post_image("passport", file)
|
|
96
|
-
|
|
97
|
-
def scan_national_id(self, file: FileInput) -> Dict[str, Any]:
|
|
98
|
-
return self._post_image("national-id", file)
|
|
99
|
-
|
|
100
|
-
def scan_driver_license(self, file: FileInput) -> Dict[str, Any]:
|
|
101
|
-
return self._post_image("driver-license", file)
|
|
102
|
-
|
|
103
|
-
def scan_invoice(self, file: FileInput) -> Dict[str, Any]:
|
|
104
|
-
return self._post_image("invoice", file)
|
|
105
|
-
|
|
106
|
-
def scan_vin(self, file: FileInput) -> Dict[str, Any]:
|
|
107
|
-
return self._post_image("vin", file)
|
|
108
|
-
|
|
109
|
-
def scan_container(self, file: FileInput) -> Dict[str, Any]:
|
|
110
|
-
return self._post_image("container", file)
|
|
111
|
-
|
|
112
|
-
def scan_hin(self, file: FileInput) -> Dict[str, Any]:
|
|
113
|
-
return self._post_image("hin", file)
|
|
114
|
-
|
|
115
|
-
def scan_receipt(self, file: FileInput) -> Dict[str, Any]:
|
|
116
|
-
return self._post_image("receipt", file)
|
|
117
|
-
|
|
118
|
-
def scan_license_plate(self, file: FileInput) -> Dict[str, Any]:
|
|
119
|
-
return self._post_image("license-plate", file)
|
|
120
|
-
|
|
121
|
-
def scan_vehicle_registration(self, file: FileInput) -> Dict[str, Any]:
|
|
122
|
-
return self._post_image("vehicle-registration", file)
|
|
123
|
-
|
|
124
|
-
def scan_atm_cassette(self, file: FileInput) -> Dict[str, Any]:
|
|
125
|
-
return self._post_image("atm-cassette", file)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|