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.
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any