preflight-sdk 1.0.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.
- preflight_sdk-1.0.0/LICENSE +21 -0
- preflight_sdk-1.0.0/PKG-INFO +148 -0
- preflight_sdk-1.0.0/README.md +127 -0
- preflight_sdk-1.0.0/preflight_sdk/__init__.py +45 -0
- preflight_sdk-1.0.0/preflight_sdk/_version.py +4 -0
- preflight_sdk-1.0.0/preflight_sdk/client.py +514 -0
- preflight_sdk-1.0.0/preflight_sdk/errors.py +85 -0
- preflight_sdk-1.0.0/preflight_sdk/py.typed +0 -0
- preflight_sdk-1.0.0/preflight_sdk/types.py +209 -0
- preflight_sdk-1.0.0/preflight_sdk/webhooks.py +78 -0
- preflight_sdk-1.0.0/preflight_sdk.egg-info/PKG-INFO +148 -0
- preflight_sdk-1.0.0/preflight_sdk.egg-info/SOURCES.txt +17 -0
- preflight_sdk-1.0.0/preflight_sdk.egg-info/dependency_links.txt +1 -0
- preflight_sdk-1.0.0/preflight_sdk.egg-info/top_level.txt +1 -0
- preflight_sdk-1.0.0/pyproject.toml +37 -0
- preflight_sdk-1.0.0/setup.cfg +4 -0
- preflight_sdk-1.0.0/tests/test_client.py +641 -0
- preflight_sdk-1.0.0/tests/test_version.py +24 -0
- preflight_sdk-1.0.0/tests/test_webhooks.py +99 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 IBUR Ventures LLC
|
|
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,148 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: preflight-sdk
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Thin fail-closed client for the Preflight pre-dial API. Standard library only.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://www.preflightapi.com/docs/
|
|
7
|
+
Project-URL: Documentation, https://www.preflightapi.com/docs/
|
|
8
|
+
Project-URL: Support, https://www.preflightapi.com/support.html
|
|
9
|
+
Project-URL: Source, https://github.com/palfi-andras/preflight
|
|
10
|
+
Keywords: preflight,tcpa,dnc,opt-out,consent,voice,sms
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Topic :: Communications :: Telephony
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# Preflight Python SDK
|
|
23
|
+
|
|
24
|
+
A thin client for the Preflight pre-dial API. Standard library only (Python 3.9+), no dependencies. Typed (`py.typed`).
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
pip install preflight-sdk
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Preflight applies encoded rules and keeps evidence. This SDK does not make anyone compliant and does not replace
|
|
31
|
+
counsel.
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from preflight_sdk import Preflight
|
|
35
|
+
|
|
36
|
+
pf = Preflight("https://api.preflightapi.com", os.environ["PREFLIGHT_KEY"])
|
|
37
|
+
|
|
38
|
+
d = pf.check({"to": "(702) 555-0142", "channel": "voice", "purpose": "marketing", "method": "ai_voice"})
|
|
39
|
+
if d["decision"] == "allow":
|
|
40
|
+
place_call() # d["obligations"] belong in your opening script
|
|
41
|
+
elif d["decision"] == "defer":
|
|
42
|
+
reschedule(d["next_allowed_at"])
|
|
43
|
+
else:
|
|
44
|
+
drop_lead(d["reasons"]) # includes "preflight_unavailable"
|
|
45
|
+
|
|
46
|
+
pf.record_opt_out({"phone": "+12065550142", "channel": "voice", "source": "in_call", "raw": "stop calling me"})
|
|
47
|
+
pf.record_consent({"phone": "+17025550142", "kind": "express_written", "source": "https://acme.example/quote", "wording": "..."})
|
|
48
|
+
job = pf.batch(items) # polls until done; job["results"] is in item order
|
|
49
|
+
pf.report_outcome(check_id, "placed", "CA123") # after the call or text ends: placed | not_placed | failed
|
|
50
|
+
ev = pf.get_evidence("+17025550142")
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Account and data calls, each one request to the endpoint of the same name in the [API reference](https://www.preflightapi.com/docs/api.html):
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
pf.revoke_consent(consent_id, "asked to stop")
|
|
57
|
+
pf.get_consent_capture(capture_id) # consent widget certificate
|
|
58
|
+
pf.claim_consent_capture(capture_id, "+17025550142") # 409 unless it is the capture's phone
|
|
59
|
+
lst = pf.create_list({"kind": "litigator", "name": "Vendor feed"})
|
|
60
|
+
pf.import_list_entries(lst["id"], file_text, options={"mode": "replace", "edition_date": "2026-10-01"}, timeout=120)
|
|
61
|
+
pf.list_lists()
|
|
62
|
+
pf.get_check_evidence(check_id)
|
|
63
|
+
r = pf.get_check_evidence_pdf(check_id) # {"pdf": bytes, "bundle_sha256": ...}
|
|
64
|
+
pf.get_rule_changes("2026-10-14.3") # or a date, or None for all
|
|
65
|
+
pf.get_rule_impact(limit=1000) # follow next_cursor with cursor=
|
|
66
|
+
hook = pf.create_webhook("https://acme.example/preflight", ["opt_out.created"]) # secret shown once
|
|
67
|
+
pf.list_webhooks()
|
|
68
|
+
pf.delete_webhook(hook["id"])
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Verifying webhooks
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from preflight_sdk import verify_webhook
|
|
75
|
+
|
|
76
|
+
# raw body exactly as received: request.get_data() (Flask), await request.body() (Starlette), never a parsed dict
|
|
77
|
+
if not verify_webhook(request.get_data(), request.headers.get("preflight-signature"), os.environ["PREFLIGHT_WEBHOOK_SECRET"]):
|
|
78
|
+
abort(400)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Returns `True` only when a `v1` value in `preflight-signature` is the HMAC-SHA256 of `<t>.<raw body>` under the
|
|
82
|
+
endpoint secret (the whole `whsec_...` string from `create_webhook`) and `t` is within `tolerance` seconds (default
|
|
83
|
+
300) of now in either direction. Anything else (missing or malformed header, non-numeric, stale or future `t`, wrong
|
|
84
|
+
signature) returns `False`; it never raises for bad input from the network. It raises `TypeError` for programming
|
|
85
|
+
errors: a body that is not bytes or str (for example a parsed dict), an empty secret, a negative tolerance. The
|
|
86
|
+
compare is constant time (`hmac.compare_digest`), and any matching `v1` is accepted (for secret rotation).
|
|
87
|
+
`verify_webhook(raw_body, header, secret, tolerance=300, now=None)`; `now` is Unix seconds, for tests.
|
|
88
|
+
Receiver guide: [Webhooks guide](https://www.preflightapi.com/docs/).
|
|
89
|
+
|
|
90
|
+
## Fail-closed behaviour
|
|
91
|
+
|
|
92
|
+
- `check()` defaults to a **300 ms** timeout per attempt. On a timeout, a network error, a 429, a 5xx (after one
|
|
93
|
+
quick retry) or any 4xx it **returns** `{"decision": "block", "reasons": [{"code": "preflight_unavailable", ...}]}` instead of
|
|
94
|
+
raising. Treat it like any other `block`: do not dial or text. A 4xx also carries its HTTP `status` in the reason.
|
|
95
|
+
- A timeout can leave a check the server already recorded as an allowed attempt that you never dialed; it keeps
|
|
96
|
+
counting toward the frequency and volume caps. The block's reason carries the `idempotency_key` the check was sent
|
|
97
|
+
under. Call `pf.release_unplaced(req, d)` (same request, the block decision) within 5 minutes: it replays the key and,
|
|
98
|
+
if the stored check was an allow, reports `not_placed`. Over-counting is the safe direction, so skipping this only
|
|
99
|
+
costs reachable leads. Never use it for a call you placed.
|
|
100
|
+
- There is no fail-open option. The only alternative is `throw_on_error=True` (on the client or per call), which
|
|
101
|
+
raises `PreflightError` (with `status`, `code`, `body`, `retry_after`) so you can handle the failure yourself. Even then, no
|
|
102
|
+
answer means do not dial.
|
|
103
|
+
- Retries: a network error, timeout, 429 or 5xx is retried; any other status is not. `check()` gets one quick retry
|
|
104
|
+
(`check_max_retries`, default 1) and never sleeps longer than its timeout, so a `Retry-After` longer than that ends
|
|
105
|
+
it at once with the block decision: worst case about two timeouts. Every other call gets `max_retries` (default 2)
|
|
106
|
+
with jittered exponential backoff (`retry_base_delay`, default 0.2 s) or the server's `Retry-After` when it sends
|
|
107
|
+
one, up to `max_retry_delay` (default 10 s); a longer `Retry-After` raises `RateLimitError` (or `ServerError` for
|
|
108
|
+
a 503) with `retry_after` so you can schedule the retry.
|
|
109
|
+
- A retry never records a write twice: every attempt of a POST sends the **same** `Idempotency-Key`, and the server
|
|
110
|
+
replays a key it has already answered instead of running the request again. The key is a UUID generated
|
|
111
|
+
per call; when you retry a failed `record_opt_out` (or any write) yourself, pass `idempotency_key=` (your inbound
|
|
112
|
+
message id, or `new_idempotency_key()` kept for the job) so your retries are replays too. The server keeps keys
|
|
113
|
+
for 24 hours: a retry later than that runs again (for an opt-out, a second opt-out event: it still blocks, but the
|
|
114
|
+
audit trail shows it twice).
|
|
115
|
+
- Every method other than `check()` always raises `PreflightError` on failure (default timeout 10 s): swallowing a
|
|
116
|
+
failed opt-out or revocation would be unsafe, so retry it until it succeeds.
|
|
117
|
+
- `import_list_entries` and `create_list` retry like any write; a replayed import returns the first import's counts
|
|
118
|
+
and does not apply the file again.
|
|
119
|
+
- `create_webhook` retries like any write; a replay returns the same signing secret, so a retry never makes a second
|
|
120
|
+
endpoint.
|
|
121
|
+
- `batch()` is screening only (items must not set `commit`). It raises if the job fails or polling exceeds
|
|
122
|
+
`max_wait`; an item the server could not evaluate comes back as `{"error": {...}}`: treat it as do-not-dial.
|
|
123
|
+
|
|
124
|
+
The per-attempt timeout is a total deadline (not per socket read), covering an error response's body too, so a server that drips bytes cannot outlast it. The SDK never follows redirects (a 30x fails closed without a retry and the API key is never sent elsewhere), and `check()` rejects a response whose `reasons` is not a list.
|
|
125
|
+
|
|
126
|
+
## Errors
|
|
127
|
+
|
|
128
|
+
Every failure is a `PreflightError`; the subclass says why: `InvalidRequestError` (400, 422), `AuthenticationError`
|
|
129
|
+
(401), `PermissionDeniedError` (403), `NotFoundError` (404), `ConflictError` (409), `RateLimitError` (429),
|
|
130
|
+
`ServerError` (5xx), `NetworkError` (no response) and `RequestTimeoutError`. Other statuses (a refused redirect, 413,
|
|
131
|
+
421) are a plain `PreflightError`.
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from preflight_sdk import AuthenticationError, PreflightError
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
pf.record_opt_out(opt_out, idempotency_key=inbound_message_id)
|
|
138
|
+
except AuthenticationError:
|
|
139
|
+
alert_ops("Preflight key rejected")
|
|
140
|
+
except PreflightError as e:
|
|
141
|
+
requeue(opt_out, delay=e.retry_after) # keep the same key on the next try
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Requests send `User-Agent: preflight-sdk-python/<SDK_VERSION>`. `SDK_VERSION` and `API_VERSION` (the `/v1` API this
|
|
145
|
+
SDK speaks) are exported.
|
|
146
|
+
|
|
147
|
+
Request and response types (`TypedDict`) are in `preflight_sdk.types`.
|
|
148
|
+
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Preflight Python SDK
|
|
2
|
+
|
|
3
|
+
A thin client for the Preflight pre-dial API. Standard library only (Python 3.9+), no dependencies. Typed (`py.typed`).
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
pip install preflight-sdk
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Preflight applies encoded rules and keeps evidence. This SDK does not make anyone compliant and does not replace
|
|
10
|
+
counsel.
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
from preflight_sdk import Preflight
|
|
14
|
+
|
|
15
|
+
pf = Preflight("https://api.preflightapi.com", os.environ["PREFLIGHT_KEY"])
|
|
16
|
+
|
|
17
|
+
d = pf.check({"to": "(702) 555-0142", "channel": "voice", "purpose": "marketing", "method": "ai_voice"})
|
|
18
|
+
if d["decision"] == "allow":
|
|
19
|
+
place_call() # d["obligations"] belong in your opening script
|
|
20
|
+
elif d["decision"] == "defer":
|
|
21
|
+
reschedule(d["next_allowed_at"])
|
|
22
|
+
else:
|
|
23
|
+
drop_lead(d["reasons"]) # includes "preflight_unavailable"
|
|
24
|
+
|
|
25
|
+
pf.record_opt_out({"phone": "+12065550142", "channel": "voice", "source": "in_call", "raw": "stop calling me"})
|
|
26
|
+
pf.record_consent({"phone": "+17025550142", "kind": "express_written", "source": "https://acme.example/quote", "wording": "..."})
|
|
27
|
+
job = pf.batch(items) # polls until done; job["results"] is in item order
|
|
28
|
+
pf.report_outcome(check_id, "placed", "CA123") # after the call or text ends: placed | not_placed | failed
|
|
29
|
+
ev = pf.get_evidence("+17025550142")
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Account and data calls, each one request to the endpoint of the same name in the [API reference](https://www.preflightapi.com/docs/api.html):
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
pf.revoke_consent(consent_id, "asked to stop")
|
|
36
|
+
pf.get_consent_capture(capture_id) # consent widget certificate
|
|
37
|
+
pf.claim_consent_capture(capture_id, "+17025550142") # 409 unless it is the capture's phone
|
|
38
|
+
lst = pf.create_list({"kind": "litigator", "name": "Vendor feed"})
|
|
39
|
+
pf.import_list_entries(lst["id"], file_text, options={"mode": "replace", "edition_date": "2026-10-01"}, timeout=120)
|
|
40
|
+
pf.list_lists()
|
|
41
|
+
pf.get_check_evidence(check_id)
|
|
42
|
+
r = pf.get_check_evidence_pdf(check_id) # {"pdf": bytes, "bundle_sha256": ...}
|
|
43
|
+
pf.get_rule_changes("2026-10-14.3") # or a date, or None for all
|
|
44
|
+
pf.get_rule_impact(limit=1000) # follow next_cursor with cursor=
|
|
45
|
+
hook = pf.create_webhook("https://acme.example/preflight", ["opt_out.created"]) # secret shown once
|
|
46
|
+
pf.list_webhooks()
|
|
47
|
+
pf.delete_webhook(hook["id"])
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Verifying webhooks
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from preflight_sdk import verify_webhook
|
|
54
|
+
|
|
55
|
+
# raw body exactly as received: request.get_data() (Flask), await request.body() (Starlette), never a parsed dict
|
|
56
|
+
if not verify_webhook(request.get_data(), request.headers.get("preflight-signature"), os.environ["PREFLIGHT_WEBHOOK_SECRET"]):
|
|
57
|
+
abort(400)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Returns `True` only when a `v1` value in `preflight-signature` is the HMAC-SHA256 of `<t>.<raw body>` under the
|
|
61
|
+
endpoint secret (the whole `whsec_...` string from `create_webhook`) and `t` is within `tolerance` seconds (default
|
|
62
|
+
300) of now in either direction. Anything else (missing or malformed header, non-numeric, stale or future `t`, wrong
|
|
63
|
+
signature) returns `False`; it never raises for bad input from the network. It raises `TypeError` for programming
|
|
64
|
+
errors: a body that is not bytes or str (for example a parsed dict), an empty secret, a negative tolerance. The
|
|
65
|
+
compare is constant time (`hmac.compare_digest`), and any matching `v1` is accepted (for secret rotation).
|
|
66
|
+
`verify_webhook(raw_body, header, secret, tolerance=300, now=None)`; `now` is Unix seconds, for tests.
|
|
67
|
+
Receiver guide: [Webhooks guide](https://www.preflightapi.com/docs/).
|
|
68
|
+
|
|
69
|
+
## Fail-closed behaviour
|
|
70
|
+
|
|
71
|
+
- `check()` defaults to a **300 ms** timeout per attempt. On a timeout, a network error, a 429, a 5xx (after one
|
|
72
|
+
quick retry) or any 4xx it **returns** `{"decision": "block", "reasons": [{"code": "preflight_unavailable", ...}]}` instead of
|
|
73
|
+
raising. Treat it like any other `block`: do not dial or text. A 4xx also carries its HTTP `status` in the reason.
|
|
74
|
+
- A timeout can leave a check the server already recorded as an allowed attempt that you never dialed; it keeps
|
|
75
|
+
counting toward the frequency and volume caps. The block's reason carries the `idempotency_key` the check was sent
|
|
76
|
+
under. Call `pf.release_unplaced(req, d)` (same request, the block decision) within 5 minutes: it replays the key and,
|
|
77
|
+
if the stored check was an allow, reports `not_placed`. Over-counting is the safe direction, so skipping this only
|
|
78
|
+
costs reachable leads. Never use it for a call you placed.
|
|
79
|
+
- There is no fail-open option. The only alternative is `throw_on_error=True` (on the client or per call), which
|
|
80
|
+
raises `PreflightError` (with `status`, `code`, `body`, `retry_after`) so you can handle the failure yourself. Even then, no
|
|
81
|
+
answer means do not dial.
|
|
82
|
+
- Retries: a network error, timeout, 429 or 5xx is retried; any other status is not. `check()` gets one quick retry
|
|
83
|
+
(`check_max_retries`, default 1) and never sleeps longer than its timeout, so a `Retry-After` longer than that ends
|
|
84
|
+
it at once with the block decision: worst case about two timeouts. Every other call gets `max_retries` (default 2)
|
|
85
|
+
with jittered exponential backoff (`retry_base_delay`, default 0.2 s) or the server's `Retry-After` when it sends
|
|
86
|
+
one, up to `max_retry_delay` (default 10 s); a longer `Retry-After` raises `RateLimitError` (or `ServerError` for
|
|
87
|
+
a 503) with `retry_after` so you can schedule the retry.
|
|
88
|
+
- A retry never records a write twice: every attempt of a POST sends the **same** `Idempotency-Key`, and the server
|
|
89
|
+
replays a key it has already answered instead of running the request again. The key is a UUID generated
|
|
90
|
+
per call; when you retry a failed `record_opt_out` (or any write) yourself, pass `idempotency_key=` (your inbound
|
|
91
|
+
message id, or `new_idempotency_key()` kept for the job) so your retries are replays too. The server keeps keys
|
|
92
|
+
for 24 hours: a retry later than that runs again (for an opt-out, a second opt-out event: it still blocks, but the
|
|
93
|
+
audit trail shows it twice).
|
|
94
|
+
- Every method other than `check()` always raises `PreflightError` on failure (default timeout 10 s): swallowing a
|
|
95
|
+
failed opt-out or revocation would be unsafe, so retry it until it succeeds.
|
|
96
|
+
- `import_list_entries` and `create_list` retry like any write; a replayed import returns the first import's counts
|
|
97
|
+
and does not apply the file again.
|
|
98
|
+
- `create_webhook` retries like any write; a replay returns the same signing secret, so a retry never makes a second
|
|
99
|
+
endpoint.
|
|
100
|
+
- `batch()` is screening only (items must not set `commit`). It raises if the job fails or polling exceeds
|
|
101
|
+
`max_wait`; an item the server could not evaluate comes back as `{"error": {...}}`: treat it as do-not-dial.
|
|
102
|
+
|
|
103
|
+
The per-attempt timeout is a total deadline (not per socket read), covering an error response's body too, so a server that drips bytes cannot outlast it. The SDK never follows redirects (a 30x fails closed without a retry and the API key is never sent elsewhere), and `check()` rejects a response whose `reasons` is not a list.
|
|
104
|
+
|
|
105
|
+
## Errors
|
|
106
|
+
|
|
107
|
+
Every failure is a `PreflightError`; the subclass says why: `InvalidRequestError` (400, 422), `AuthenticationError`
|
|
108
|
+
(401), `PermissionDeniedError` (403), `NotFoundError` (404), `ConflictError` (409), `RateLimitError` (429),
|
|
109
|
+
`ServerError` (5xx), `NetworkError` (no response) and `RequestTimeoutError`. Other statuses (a refused redirect, 413,
|
|
110
|
+
421) are a plain `PreflightError`.
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from preflight_sdk import AuthenticationError, PreflightError
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
pf.record_opt_out(opt_out, idempotency_key=inbound_message_id)
|
|
117
|
+
except AuthenticationError:
|
|
118
|
+
alert_ops("Preflight key rejected")
|
|
119
|
+
except PreflightError as e:
|
|
120
|
+
requeue(opt_out, delay=e.retry_after) # keep the same key on the next try
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Requests send `User-Agent: preflight-sdk-python/<SDK_VERSION>`. `SDK_VERSION` and `API_VERSION` (the `/v1` API this
|
|
124
|
+
SDK speaks) are exported.
|
|
125
|
+
|
|
126
|
+
Request and response types (`TypedDict`) are in `preflight_sdk.types`.
|
|
127
|
+
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from ._version import API_VERSION, SDK_VERSION
|
|
2
|
+
from .client import UNAVAILABLE, Preflight, new_idempotency_key, parse_retry_after, unavailable_decision
|
|
3
|
+
from .errors import (
|
|
4
|
+
AuthenticationError,
|
|
5
|
+
ConflictError,
|
|
6
|
+
InvalidRequestError,
|
|
7
|
+
NetworkError,
|
|
8
|
+
NotFoundError,
|
|
9
|
+
PermissionDeniedError,
|
|
10
|
+
PreflightError,
|
|
11
|
+
RateLimitError,
|
|
12
|
+
RequestTimeoutError,
|
|
13
|
+
ServerError,
|
|
14
|
+
)
|
|
15
|
+
from .webhooks import verify_webhook
|
|
16
|
+
from .types import (
|
|
17
|
+
BatchJob,
|
|
18
|
+
BatchResult,
|
|
19
|
+
CheckRequest,
|
|
20
|
+
ConsentRequest,
|
|
21
|
+
Decision,
|
|
22
|
+
Evidence,
|
|
23
|
+
EvidencePdf,
|
|
24
|
+
ImportOptions,
|
|
25
|
+
ListKind,
|
|
26
|
+
NewList,
|
|
27
|
+
Obligation,
|
|
28
|
+
OptOutRequest,
|
|
29
|
+
Outcome,
|
|
30
|
+
OutcomeReceipt,
|
|
31
|
+
Reason,
|
|
32
|
+
ReasonCode,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"UNAVAILABLE", "Preflight", "PreflightError", "unavailable_decision",
|
|
37
|
+
"BatchJob", "BatchResult", "CheckRequest", "ConsentRequest", "Decision", "Evidence", "EvidencePdf", "ImportOptions", "ListKind", "NewList", "Obligation",
|
|
38
|
+
"OptOutRequest", "Outcome", "OutcomeReceipt", "Reason", "ReasonCode",
|
|
39
|
+
]
|
|
40
|
+
__all__ += ["verify_webhook"]
|
|
41
|
+
__all__ += [
|
|
42
|
+
"API_VERSION", "SDK_VERSION", "new_idempotency_key", "parse_retry_after",
|
|
43
|
+
"AuthenticationError", "ConflictError", "InvalidRequestError", "NetworkError", "NotFoundError",
|
|
44
|
+
"PermissionDeniedError", "RateLimitError", "RequestTimeoutError", "ServerError",
|
|
45
|
+
]
|