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