exportflow 1.0.0__py3-none-any.whl
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.
- exportflow/__init__.py +91 -0
- exportflow/_client.py +614 -0
- exportflow/_errors.py +140 -0
- exportflow/_models.py +305 -0
- exportflow/_version.py +11 -0
- exportflow/py.typed +0 -0
- exportflow/webhooks.py +87 -0
- exportflow-1.0.0.dist-info/METADATA +222 -0
- exportflow-1.0.0.dist-info/RECORD +10 -0
- exportflow-1.0.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: exportflow
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: The official Python SDK for the ExportFlow document processing and verification API
|
|
5
|
+
Project-URL: Documentation, https://exportflow.app/developers
|
|
6
|
+
Project-URL: Changelog, https://exportflow.app/developers/changelog
|
|
7
|
+
Author: MeerTech Ltd
|
|
8
|
+
License: Proprietary
|
|
9
|
+
Keywords: documents,erp,exportflow,ocr,trade,verification
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Typing :: Typed
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Requires-Dist: httpx<1,>=0.25
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# ExportFlow Python SDK
|
|
18
|
+
|
|
19
|
+
The official Python client for the ExportFlow API: upload trade documents from
|
|
20
|
+
your own software, have them classified and read, confirm what they say, and
|
|
21
|
+
verify them against each other.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install exportflow
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Python 3.9 or later. The only dependency is `httpx`.
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from exportflow import ExportFlow
|
|
33
|
+
|
|
34
|
+
client = ExportFlow(api_key="exfk_test_...") # or set EXPORTFLOW_API_KEY
|
|
35
|
+
|
|
36
|
+
# 1. Upload. The document is scanned, stored and queued for processing.
|
|
37
|
+
document = client.documents.upload(file="bill_of_lading.pdf")
|
|
38
|
+
|
|
39
|
+
# 2. Wait for processing (or use a webhook; see below).
|
|
40
|
+
document = client.documents.wait(document.id)
|
|
41
|
+
print(document.status, document.document_type) # processed bill_of_lading
|
|
42
|
+
|
|
43
|
+
# 3. Read what was extracted.
|
|
44
|
+
extraction = client.documents.extraction(document.id)
|
|
45
|
+
print(extraction.as_dict()) # {"bl_number": "MEDU7741203", ...}
|
|
46
|
+
|
|
47
|
+
# 4. Confirm what the documents say, then verify the shipment.
|
|
48
|
+
client.records.confirm(document.shipment_id, {"port_of_discharge": "NHAVA SHEVA"})
|
|
49
|
+
verification = client.verifications.create(document.shipment_id)
|
|
50
|
+
verification = client.verifications.wait(verification.id)
|
|
51
|
+
for finding in verification.findings:
|
|
52
|
+
print(finding.severity, finding.message)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
A key that begins `exfk_test_` works in the **sandbox**: the same pipeline,
|
|
56
|
+
separate data, never charged. A key that begins `exfk_live_` is **production**.
|
|
57
|
+
Create both in the developer console.
|
|
58
|
+
|
|
59
|
+
## How ExportFlow models documents
|
|
60
|
+
|
|
61
|
+
A **shipment** is a set of documents that travel together: a bill of lading, a
|
|
62
|
+
commercial invoice, a packing list, certificates. Documents are uploaded to a
|
|
63
|
+
shipment, each is **processed** (classified, then its fields extracted), and
|
|
64
|
+
the shipment is **verified** by checking the documents against each other and
|
|
65
|
+
against the published rule set.
|
|
66
|
+
|
|
67
|
+
`client.documents.upload(file=...)` with no `shipment_id` creates a shipment
|
|
68
|
+
for the document. To group several documents, create the shipment first:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
shipment = client.shipments.create(reference="PO-7741", origin_country="NG",
|
|
72
|
+
destination_country="IN")
|
|
73
|
+
for path in ("bl.pdf", "invoice.pdf", "packing_list.pdf"):
|
|
74
|
+
client.documents.upload(file=path, shipment_id=shipment.id)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Why you confirm before you verify
|
|
78
|
+
|
|
79
|
+
Verification refuses to start (`ConflictError`, code `not_confirmed`) until the
|
|
80
|
+
shipment's record has been confirmed. This is deliberate. If a field was
|
|
81
|
+
misread, every finding computed from it would be wrong, in both directions.
|
|
82
|
+
Show your user what was read (`client.records.get(shipment_id)`), let them
|
|
83
|
+
confirm or correct it, and send that with `client.records.confirm(...)`.
|
|
84
|
+
Confirm what the document *says*, not what it should say: verification is what
|
|
85
|
+
finds the error.
|
|
86
|
+
|
|
87
|
+
## Webhooks
|
|
88
|
+
|
|
89
|
+
Polling works; webhooks are better. Register an endpoint once:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
endpoint = client.webhook_endpoints.create(
|
|
93
|
+
"https://erp.example.com/hooks/exportflow",
|
|
94
|
+
event_types=["document.processing.completed", "verification.completed"],
|
|
95
|
+
)
|
|
96
|
+
print(endpoint.signing_secret) # whsec_... store this
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Verify every delivery before trusting it:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from exportflow import webhooks, SignatureVerificationError
|
|
103
|
+
|
|
104
|
+
def handle(request):
|
|
105
|
+
try:
|
|
106
|
+
event = webhooks.construct_event(
|
|
107
|
+
payload=request.body, # raw bytes
|
|
108
|
+
signature=request.headers["ExportFlow-Signature"],
|
|
109
|
+
secret=WEBHOOK_SECRET,
|
|
110
|
+
)
|
|
111
|
+
except SignatureVerificationError:
|
|
112
|
+
return 400
|
|
113
|
+
if already_handled(event.id): # deliveries are at-least-once
|
|
114
|
+
return 200
|
|
115
|
+
if event.type == "document.processing.completed":
|
|
116
|
+
document_id = event.data["document"]["id"]
|
|
117
|
+
...
|
|
118
|
+
return 200
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Answer with any `2xx` within 10 seconds. Anything else is retried with
|
|
122
|
+
increasing delays for about two and a half hours. An event carries identifiers
|
|
123
|
+
and statuses, never document content: fetch the resource to read it.
|
|
124
|
+
|
|
125
|
+
| Event | When |
|
|
126
|
+
|---|---|
|
|
127
|
+
| `document.uploaded` | A document was accepted, scanned and stored |
|
|
128
|
+
| `document.processing.started` | Classification began |
|
|
129
|
+
| `document.processing.completed` | The document was classified and read |
|
|
130
|
+
| `document.processing.failed` | Processing stopped; `GET` the document for why |
|
|
131
|
+
| `verification.completed` | A verification finished; fetch it for the findings |
|
|
132
|
+
| `verification.failed` | A verification could not run |
|
|
133
|
+
| `billing.invoice.generated` | An invoice was issued to your organisation |
|
|
134
|
+
|
|
135
|
+
## Errors
|
|
136
|
+
|
|
137
|
+
Every error is an `exportflow.ExportFlowError`. One returned by the API is an
|
|
138
|
+
`APIError` with a stable `code`, the HTTP `status_code` and a `request_id` to
|
|
139
|
+
quote to support. Branch on the type or the code, never on the message.
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
import exportflow
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
client.verifications.create(shipment_id)
|
|
146
|
+
except exportflow.ConflictError as error:
|
|
147
|
+
if error.code == "not_confirmed":
|
|
148
|
+
... # ask the user to confirm the record
|
|
149
|
+
except exportflow.QuotaExceededError:
|
|
150
|
+
... # the month's allowance is used up
|
|
151
|
+
except exportflow.RateLimitError as error:
|
|
152
|
+
time.sleep(error.retry_after or 5)
|
|
153
|
+
except exportflow.APIConnectionError:
|
|
154
|
+
... # safe to retry later
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
| Exception | HTTP | Meaning |
|
|
158
|
+
|---|---|---|
|
|
159
|
+
| `AuthenticationError` | 401 | Key missing, wrong, revoked or expired |
|
|
160
|
+
| `PermissionDeniedError` | 403 | Missing scope, suspended account, network not allowed |
|
|
161
|
+
| `NotFoundError` | 404 | No such resource in this organisation and environment |
|
|
162
|
+
| `ConflictError` | 409 | Not possible in the current state (e.g. `not_confirmed`) |
|
|
163
|
+
| `InvalidRequestError` | 400, 422 | The request is wrong; do not retry |
|
|
164
|
+
| `RateLimitError` | 429 | Slow down; see `retry_after` |
|
|
165
|
+
| `QuotaExceededError` | 429 | Allowance or spending limit reached for the period |
|
|
166
|
+
| `SDKVersionUnsupportedError` | 426 | Upgrade this SDK |
|
|
167
|
+
| `ServerError` | 5xx | ExportFlow's fault; retried automatically |
|
|
168
|
+
| `APIConnectionError`, `APITimeoutError` | none | The API could not be reached |
|
|
169
|
+
|
|
170
|
+
## Retries and idempotency
|
|
171
|
+
|
|
172
|
+
The client retries connection failures, timeouts, `429 rate_limited` and `5xx`
|
|
173
|
+
up to `max_retries` times (default 2), with backoff, honouring `Retry-After`.
|
|
174
|
+
|
|
175
|
+
Every request that changes something carries an `Idempotency-Key`, and a retry
|
|
176
|
+
sends the *same* key and the *same* body, so the work is done once however many
|
|
177
|
+
times it is sent. To make your own retries safe across process restarts, pass
|
|
178
|
+
your own key:
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
client.documents.upload(file="bl.pdf", shipment_id=sid, idempotency_key=f"upload-{erp_document_id}")
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Uploading the same file to the same shipment twice returns the first document
|
|
185
|
+
(`duplicate=True`) and is not charged again.
|
|
186
|
+
|
|
187
|
+
## Usage and charges
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
usage = client.usage.get() # this month; or client.usage.get("2026-10")
|
|
191
|
+
for meter in usage.meters:
|
|
192
|
+
print(meter.name, meter.succeeded, "of", meter.quota or "unlimited")
|
|
193
|
+
print(usage.estimated_charge, usage.currency)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Requests are not charged. Usage is recorded when work completes: a document
|
|
197
|
+
accepted, a document processed, a verification completed. Sandbox usage is
|
|
198
|
+
measured and never charged. Charges are estimates until an invoice is issued.
|
|
199
|
+
|
|
200
|
+
## Configuration
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
ExportFlow(
|
|
204
|
+
api_key=None, # default: EXPORTFLOW_API_KEY
|
|
205
|
+
base_url=None, # default: EXPORTFLOW_BASE_URL or https://api.exportflow.app
|
|
206
|
+
timeout=60.0, # seconds per response
|
|
207
|
+
max_retries=2,
|
|
208
|
+
http_client=None, # your own httpx.Client: proxies, TLS, tests
|
|
209
|
+
)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
The client is safe to share between threads. Use it as a context manager, or
|
|
213
|
+
call `close()`, to release its connections.
|
|
214
|
+
|
|
215
|
+
## Versioning
|
|
216
|
+
|
|
217
|
+
This SDK follows semantic versioning and speaks version 1 of the API. Within
|
|
218
|
+
version 1 the API only adds; nothing is removed or changed in meaning, so this
|
|
219
|
+
SDK keeps working as the API grows, and unknown fields are available on every
|
|
220
|
+
model's `.raw`. If the server ever reports a different major version the
|
|
221
|
+
client raises `IncompatibleAPIVersionError` rather than misread a response.
|
|
222
|
+
See `CHANGELOG.md`.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
exportflow/__init__.py,sha256=_j4jIjZpnLzTqkRSaQtK8WcPQNwA1pQaqiyIUyn65Uk,2009
|
|
2
|
+
exportflow/_client.py,sha256=tHm4BedY4hvowBdfNMyWFscRYbkyKFh07x6s0wL_XUE,23490
|
|
3
|
+
exportflow/_errors.py,sha256=GMeaOE3dkKZWZw0bBLV-q0bj6FjdB6zpVG1s30MJ3SA,4139
|
|
4
|
+
exportflow/_models.py,sha256=cN0a-dSgaz2iMFPiEVLnxqbJq85y_D-YGS2I63ZOzTM,9432
|
|
5
|
+
exportflow/_version.py,sha256=2PZkUmAuO6RtbMAGbuvx-dbkFjjEE4gFF_JrZB8wruY,384
|
|
6
|
+
exportflow/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
exportflow/webhooks.py,sha256=zEfp_fdqrruHaTW-ZcNQS3n9d__DECEEiE-CJDuTgfo,3028
|
|
8
|
+
exportflow-1.0.0.dist-info/METADATA,sha256=Yh5NmGtOaOwFKwJnJpssG_ATvt0BHzDQo_eBAZ_bbME,8766
|
|
9
|
+
exportflow-1.0.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
10
|
+
exportflow-1.0.0.dist-info/RECORD,,
|