driftstack-sdk 0.1.4__tar.gz → 0.2.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.
Files changed (51) hide show
  1. driftstack_sdk-0.2.0/.gitignore +17 -0
  2. driftstack_sdk-0.2.0/CHANGELOG.md +366 -0
  3. driftstack_sdk-0.2.0/LICENSE +21 -0
  4. driftstack_sdk-0.2.0/PKG-INFO +350 -0
  5. driftstack_sdk-0.2.0/README.md +314 -0
  6. {driftstack_sdk-0.1.4 → driftstack_sdk-0.2.0}/pyproject.toml +47 -3
  7. driftstack_sdk-0.2.0/src/driftstack/__init__.py +137 -0
  8. driftstack_sdk-0.2.0/src/driftstack/_generated/models.py +2686 -0
  9. {driftstack_sdk-0.1.4 → driftstack_sdk-0.2.0}/src/driftstack/_version.py +1 -1
  10. driftstack_sdk-0.2.0/src/driftstack/client.py +206 -0
  11. driftstack_sdk-0.2.0/src/driftstack/errors.py +671 -0
  12. driftstack_sdk-0.2.0/src/driftstack/http.py +1248 -0
  13. driftstack_sdk-0.2.0/src/driftstack/pagination.py +103 -0
  14. driftstack_sdk-0.2.0/src/driftstack/resources/account.py +171 -0
  15. driftstack_sdk-0.2.0/src/driftstack/resources/agent_sessions.py +993 -0
  16. driftstack_sdk-0.2.0/src/driftstack/resources/api_keys.py +119 -0
  17. driftstack_sdk-0.2.0/src/driftstack/resources/archetypes.py +47 -0
  18. driftstack_sdk-0.2.0/src/driftstack/resources/audit_log.py +98 -0
  19. driftstack_sdk-0.2.0/src/driftstack/resources/auth.py +187 -0
  20. driftstack_sdk-0.2.0/src/driftstack/resources/billing.py +54 -0
  21. driftstack_sdk-0.2.0/src/driftstack/resources/crypto_orders.py +271 -0
  22. driftstack_sdk-0.2.0/src/driftstack/resources/egress.py +119 -0
  23. driftstack_sdk-0.2.0/src/driftstack/resources/email_preferences.py +68 -0
  24. driftstack_sdk-0.2.0/src/driftstack/resources/legal.py +52 -0
  25. driftstack_sdk-0.2.0/src/driftstack/resources/mfa.py +74 -0
  26. driftstack_sdk-0.2.0/src/driftstack/resources/profile_snapshots.py +141 -0
  27. driftstack_sdk-0.2.0/src/driftstack/resources/profiles.py +290 -0
  28. driftstack_sdk-0.2.0/src/driftstack/resources/recipes.py +149 -0
  29. driftstack_sdk-0.2.0/src/driftstack/resources/sessions.py +353 -0
  30. driftstack_sdk-0.2.0/src/driftstack/resources/team.py +197 -0
  31. driftstack_sdk-0.2.0/src/driftstack/resources/usage.py +47 -0
  32. driftstack_sdk-0.2.0/src/driftstack/resources/webhooks.py +231 -0
  33. {driftstack_sdk-0.1.4 → driftstack_sdk-0.2.0}/src/driftstack/retry.py +36 -12
  34. driftstack_sdk-0.2.0/src/driftstack/webhook_signature.py +165 -0
  35. driftstack_sdk-0.1.4/.gitignore +0 -45
  36. driftstack_sdk-0.1.4/PKG-INFO +0 -226
  37. driftstack_sdk-0.1.4/README.md +0 -191
  38. driftstack_sdk-0.1.4/src/driftstack/__init__.py +0 -62
  39. driftstack_sdk-0.1.4/src/driftstack/_generated/models.py +0 -490
  40. driftstack_sdk-0.1.4/src/driftstack/client.py +0 -120
  41. driftstack_sdk-0.1.4/src/driftstack/errors.py +0 -217
  42. driftstack_sdk-0.1.4/src/driftstack/http.py +0 -300
  43. driftstack_sdk-0.1.4/src/driftstack/resources/api_keys.py +0 -61
  44. driftstack_sdk-0.1.4/src/driftstack/resources/sessions.py +0 -156
  45. driftstack_sdk-0.1.4/src/driftstack/resources/usage.py +0 -29
  46. driftstack_sdk-0.1.4/src/driftstack/resources/webhooks.py +0 -110
  47. driftstack_sdk-0.1.4/src/driftstack/webhook_signature.py +0 -106
  48. {driftstack_sdk-0.1.4 → driftstack_sdk-0.2.0}/src/driftstack/_generated/__init__.py +0 -0
  49. {driftstack_sdk-0.1.4 → driftstack_sdk-0.2.0}/src/driftstack/py.typed +0 -0
  50. {driftstack_sdk-0.1.4 → driftstack_sdk-0.2.0}/src/driftstack/resources/__init__.py +0 -0
  51. {driftstack_sdk-0.1.4 → driftstack_sdk-0.2.0}/src/driftstack/resources/_common.py +0 -0
@@ -0,0 +1,17 @@
1
+ # Build and tool caches for the Python SDK. Every pattern here is already
2
+ # covered by the repository root .gitignore, so this file changes nothing about
3
+ # what git tracks.
4
+ #
5
+ # It exists for the SDIST. Hatchling force-includes the nearest .gitignore above
6
+ # the package root into `driftstack_sdk-<version>.tar.gz`, and `exclude` does
7
+ # not override a force-include. Without this file the nearest one is the
8
+ # repository root's, which PyPI would then serve to anyone who downloads the
9
+ # source distribution. Pinned by the packaging guard in apps/server/tests/unit.
10
+ .venv/
11
+ __pycache__/
12
+ *.egg-info/
13
+ .pytest_cache/
14
+ .mypy_cache/
15
+ .ruff_cache/
16
+ dist/
17
+ build/
@@ -0,0 +1,366 @@
1
+ # Changelog
2
+
3
+ All notable changes to the `driftstack` Python SDK. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning
5
+ follows [SemVer](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.2.0] - 2026-09-20
10
+
11
+ The release the guide [Run AI tasks from your
12
+ code](https://docs.driftstack.io/guides/run-ai-tasks-from-code/) is written
13
+ against — 0.1.5 cannot run any of its examples, because the AI agent was not
14
+ reachable from it at all.
15
+
16
+ **Nothing was removed.** Every name 0.1.5 exported, every method, every
17
+ keyword argument and every error class is still here and still means the same
18
+ thing, so upgrading takes no code change. What grew: both clients went from 4
19
+ resources to 19, and `driftstack.__all__` from 22 names to 56. Every addition
20
+ below exists on **both** `Driftstack` and `AsyncDriftstack`. Read **Changed**
21
+ before you upgrade anyway — a few behaviours differ, and one of them changes
22
+ which exception an `except` clause sees.
23
+
24
+ ### Added
25
+
26
+ #### Run an AI task end to end
27
+
28
+ `client.agent_sessions` is new, and is the whole AI surface.
29
+
30
+ - **Start it, send the task, close it** — `create(body, ...)` opens a
31
+ session, on a saved profile if you pass one; `message(id, text, ...)` sends
32
+ the task in plain words and returns what happened; `get(id)`,
33
+ `list(...)` and `iterate(...)` read sessions back; `close(id)` ends one and
34
+ saves the profile's sign-in. A new session is `provisioning` until its
35
+ browser is ready, then `active`.
36
+ - **Every way a turn can end is a named result kind** — `plan-executed` (the
37
+ steps ran, with `answer` when you asked a question), `clarify` (the agent
38
+ is asking you something), `refuse` (it will not do this), `stopped`, and a
39
+ step held for your approval. `answer_unavailable` says why there is no
40
+ `answer` when you asked for one.
41
+ - **Live progress while it runs** — `message(..., on_step=..., on_event=...)`
42
+ parses the turn's stream as it arrives: `on_step(step)` gets each step
43
+ (`{"index", "result"}`) and `on_event(name, data)` every other progress
44
+ event (`phase`, `plan`, `step_start`, `answer`, `notice`, and any added
45
+ later). On the async client either callback may be `async def`.
46
+ - **Approve a step, or don't** — a step with real-world consequences (a
47
+ payment, a message sent, something deleted) pauses the turn and comes back
48
+ as `confirmation_required`. Send the same task again with
49
+ `approve_consequential_actions=` to release it; it accepts the step result
50
+ exactly as it was returned, as well as `{"category", "matched_text"}`
51
+ dicts, and raises `ValueError` before sending anything if an entry is
52
+ neither. An unattended job simply never approves.
53
+ - **Stop a task that runs too long** — `stop(id)` asks the running turn to
54
+ stop; the waiting `message()` then returns kind `stopped` rather than
55
+ raising.
56
+ - **Screenshots** — `get_capture(id, capture_id)` returns the image behind a
57
+ `capture` step's `captureId` as `{"content_type", "bytes"}` (`image/png` or
58
+ `image/jpeg`). Screenshots are kept only briefly, so fetch one as soon as
59
+ its turn ends; one that is no longer kept raises `NotFoundError`.
60
+ - **Transcripts** — `transcript(id, last_event_id=..., timeout_s=...)`
61
+ iterates the session's conversation — an iterator on the sync client, an
62
+ async iterator on the async one: every entry so far, then each new one as
63
+ it is written. `last_event_id` resumes after the last `index` you saw, and
64
+ closing the iterator closes the connection.
65
+ - **Why a turn handed back, in one word** — a `plan-executed` result carries
66
+ `notice_reason` beside the `notice` sentence: `"step_limit"`,
67
+ `"time_limit"`, `"budget_low"`, `"no_progress"`, `"repeated_step"`,
68
+ `"ai_unavailable"`, `"question"` or `"declined"`, exported as the open
69
+ union `AgentNoticeReason` (`Literal[...] | str`), so an ending this SDK has
70
+ never heard of still type-checks and still parses — match the ones you know
71
+ and show `notice` for the rest.
72
+ - **When it is safe to retry a message** — a refusal that did no work leaves
73
+ its `Idempotency-Key` free: after a 409 whose `turn_in_progress` is set, a
74
+ 429, a 402, a 403 about the plan's AI or the model, or a 502 whose
75
+ `key_rejected` is false, send the same request again with the **same**
76
+ `idempotency_key=`. Any other failure gets a new one. One message is one
77
+ key, always.
78
+ - **Typed refusals you can act on** — `ForbiddenError.requires_own_key` /
79
+ `.model` (this model needs your own Anthropic key);
80
+ `ConflictError.turn_in_progress`, `.session_status`, `.idempotency_status`,
81
+ `.ai_control_unavailable`, `.phase`, `.tokens_consumed`, `.usage`,
82
+ `.partial_results` and `.closed_reason` (why a closed session ended,
83
+ without a second call); `FeatureUnavailableError.stop_unconfirmed` (the one
84
+ `stop()` 503 worth calling again); and
85
+ `ByokAnthropicRequiredError.key_rejected` / `.key_source` /
86
+ `.key_rejected_reason` (your own key was refused, which key, and why).
87
+ - **Send your own Anthropic key** — `byok_api_key=` on `create()`, so a
88
+ session can run on a model that requires one without storing anything.
89
+ - **Bound the whole call** — `timeout_s=` on `message()` (default 50
90
+ minutes).
91
+ - **Why a step failed** — a failed step's `Diagnosis.category` explains it,
92
+ including `"target_unverified"` (the tap was not made because its target
93
+ could not be checked first — not retryable as the same step; the agent
94
+ re-plans).
95
+ - **`examples/agent_chat.py`** is the complete flow end to end: create, wait
96
+ until ready, send a task with a fresh idempotency key and live progress,
97
+ handle every result kind, close in `finally`.
98
+
99
+ #### Watch one live, or take the wheel
100
+
101
+ - **`livekit_token(id)`** — a token for the live video view of a running
102
+ session, so a person can watch it work. Returns `LiveKitInfo`.
103
+ - **`set_mode(id, body)`**, **`takeover(id, client_id)`** and
104
+ **`handback(id)`** — hand control of a running session between the agent
105
+ and a person, and hand it back. **`send_input_event(id, body)`** drives it
106
+ while a person holds it, and **`resume(id)`** picks a session back up.
107
+ - **`set_egress(id, body)`** changes which of your proxies a running session
108
+ goes out through.
109
+
110
+ #### The rest of the API
111
+
112
+ `client.sessions`, `client.api_keys`, `client.usage` and `client.webhooks`
113
+ were the whole client in 0.1.5, and each of the four gained methods:
114
+ `sessions` gained `get` / `iterate` / `extract` (pull structured data off the
115
+ page) / `search` / `login`; `usage` gained `series()` for usage over time;
116
+ `api_keys` gained
117
+ `rotate()` (issue the replacement and keep the old key working for a grace
118
+ window); `webhooks` gained `update()` (partial update — it does not rotate
119
+ the signing secret), `rotate_secret()` (fresh secret shown once, previous one
120
+ valid for 24h, both signatures sent during the window), `send_test()` (a
121
+ synthetic `test.ping` delivery so you can check your handler before depending
122
+ on it), `replay_delivery()` and `iterate_deliveries()`.
123
+
124
+ And fourteen resources are new:
125
+
126
+ - **`client.profiles`** — create, list, iterate, get, update, delete, plus
127
+ `clone(profile_id, body=None)` (pass `None` to let the server name it
128
+ "(copy)", "(copy 2)", …) and `trim()`.
129
+ - **`client.profile_snapshots`** — immutable point-in-time copies of a
130
+ profile: `capture`, `list_for_profile`, `list`, `iterate`, `get`,
131
+ `restore`, `delete`. `restore` creates a NEW profile; the original is never
132
+ modified.
133
+ - **`client.account`** — `me()` (the full account profile: timezone, slug,
134
+ region, avatar, whether MFA is enrolled, team memberships),
135
+ `update_me()`, `upload_avatar()` / `clear_avatar()`,
136
+ `list_web_sessions()` / `revoke_web_session(id)` /
137
+ `revoke_all_other_web_sessions()`, and `rate_limits()` for the limits
138
+ actually in force on your account.
139
+ - **`client.auth`** — sign-up, e-mail verification, log in, magic links,
140
+ password reset, refresh, log out, and the three-call activation flow a CLI
141
+ or desktop app uses instead of asking for a pasted key
142
+ (`cli_authorize_initiate` → open the returned `browser_url` → poll
143
+ `cli_authorize_exchange`, which delivers the key once, then reports
144
+ `expired`).
145
+ - **`client.mfa`** — `status`, `enroll`, `verify`, `disable`,
146
+ `regenerate_recovery_codes`; plus `auth.mfa_challenge()` to exchange a
147
+ login challenge for a session (the response says whether it was satisfied
148
+ by `"totp"` or `"recovery"`) and `auth.mfa_step_up()` to refresh the
149
+ freshness window an operation asked for.
150
+ - **`client.team`** — members, invites, roles, and `list_owners()` for the
151
+ workspaces your account has joined.
152
+ - **`client.audit_log`** — `list` / `iterate`, and `export()`: a single-call
153
+ JSON export of your account's audit log, up to 10,000 rows, with
154
+ `truncated` set when there were more.
155
+ - **`client.billing`** — current state, checkout, and the billing portal.
156
+ - **`client.crypto_orders`** — `quote`, `create_checkout` (takes
157
+ `idempotency_key=` so a retry cannot mint a second order), `list`,
158
+ `iterate` (walks every page for you; narrow it with `status`,
159
+ `created_after`, `created_before`), `get`, `update_note`, `cancel`,
160
+ `receipt`. Crypto payments are not refundable, and cancelling only works
161
+ while an order is pending.
162
+ - **`client.egress`** and account proxies — manage saved proxies and route a
163
+ session's traffic through one with `proxy_id` on create.
164
+ - **`client.archetypes`** — the device archetypes your plan can use.
165
+ - **`client.recipes`** — `create(agent_session_id=, label=, description=)`
166
+ snapshots a finished agent session's steps and transcript into a recipe you
167
+ can replay.
168
+ - **`client.email_preferences`** — `list` / `set` / `opt_in` / `opt_out`.
169
+ - **`client.legal`** — record acceptance of a document version.
170
+
171
+ #### Errors and retries
172
+
173
+ - **`is_retryable(err)`** is exported, so the predicate the built-in retry
174
+ loop uses is one you can call yourself.
175
+ - **New error classes**, all importable from `driftstack`:
176
+ `BadRequestError`, `InternalError`, `FeatureUnavailableError`,
177
+ `MfaStepUpRequiredError`, `EmailAlreadyRegisteredError`,
178
+ `InvalidCredentialsError`, `InvalidAuthTokenError`,
179
+ `EmailNotVerifiedError`, `ByokAnthropicRequiredError`,
180
+ `ProxyValidationFailedError`, `StorageQuotaExceededError`,
181
+ `ProfileInUseError`, `BundledLlmConsentRequiredError`,
182
+ `BundledLlmBudgetExhaustedError`, `PairModeConflictError` and
183
+ `PairModeStateInvalidTransitionError`.
184
+ - **`verify_webhook_signature` accepts `header_prev=`** — an optional second
185
+ signature header. You rarely need it: during a rotation grace window both
186
+ signatures arrive inside the one `x-driftstack-signature` header, which the
187
+ verifier already checks.
188
+
189
+ ### Changed
190
+
191
+ - **Too many AI turns is a `RateLimitError` now.** This refusal answers as
192
+ `rate-limited` with `retry_after_seconds` 5, so `message()` raises
193
+ `RateLimitError` where it raised `ConcurrencyLimitError`.
194
+ `ConcurrencyLimitError` still means exactly what it always meant on
195
+ `create()`: your plan's limit on sessions running at once.
196
+ - **A generic 400 raises `BadRequestError` now**, not `ValidationError`. A
197
+ 400 that carries field-level issues is still a `ValidationError`. Callers
198
+ with `except ValidationError` around a generic 400 should switch to
199
+ `except BadRequestError`; `except DriftstackError` catch-alls and
200
+ `is_retryable` are unaffected.
201
+ - **The agent-session docstrings describe what the API does** — result kinds,
202
+ how an approval resumes paused steps, when a key may be reused, the
203
+ progress event names — and no longer describe how the service is built. The
204
+ README no longer claims every resource returns a Pydantic model
205
+ (`agent_sessions` returns dicts) and now says that `ForbiddenError` is a
206
+ subclass of `AuthError`.
207
+
208
+ ### Fixed
209
+
210
+ - **A failure category newer than the SDK no longer breaks parsing.**
211
+ `Diagnosis.category` is `Literal[...] | str`, so a category the server adds
212
+ after this release is kept as a plain string. ⚠️ **This is the one reason
213
+ to upgrade before you need to.** In 0.1.5 the field is a closed `Literal`,
214
+ so a response carrying a category that SDK has never seen raises a pydantic
215
+ `ValidationError` for the **whole** response — not just that field. Treat a
216
+ value you do not recognise as `"unknown"`; code comparing `category`
217
+ against known strings needs no change.
218
+ - **The account profile matches the full response** — `me()` returns every
219
+ field the API sends, including the ones added since 0.1.5.
220
+
221
+ ### Pre-1.0 stability
222
+
223
+ The SDK is pre-1.0. The surface is stable enough to build against, but a
224
+ MINOR bump may still carry additive changes — new methods, new fields, new
225
+ error subclasses. **Patch** releases (0.2.x) are fixes and additive types
226
+ only. Breaking changes that would stop shipping customer code are deferred to
227
+ 1.0; until then pin `driftstack-sdk~=0.2.0` rather than an exact version, and
228
+ read this file before bumping.
229
+
230
+ ## [0.1.5] - 2026-05-03
231
+
232
+ Written on 2026-09-20 from the published wheel: 0.1.5 went to PyPI without a
233
+ CHANGELOG heading of its own.
234
+
235
+ ### Added
236
+
237
+ - **`LegalAcceptanceRequiredError`** — a 409 that asks you to accept a
238
+ document version is its own exception now, carrying the pending
239
+ acceptances (`document_key` + `current_version` for each) as
240
+ `pending_acceptances`. Exported from the package root.
241
+
242
+ ## [0.1.4] - 2026-05-03
243
+
244
+ ### Added
245
+
246
+ - **`SessionTimeoutError`** — new typed error subclass mapping
247
+ the `https://errors.driftstack.dev/session-timeout` problem type
248
+ (status 504). Distinguished from `DriverError` so callers can
249
+ react specifically to "the operation didn't finish within the
250
+ per-call timeout I supplied" without conflating with downstream
251
+ driver failures. Carries `timeout_ms: int | None` from the
252
+ problem extension. Re-exported at `driftstack.SessionTimeoutError`
253
+ for convenient `isinstance` checks.
254
+
255
+ ```python
256
+ from driftstack import SessionTimeoutError
257
+
258
+ try:
259
+ client.sessions.interact(sid, body)
260
+ except SessionTimeoutError as e:
261
+ # Retry with a longer timeout, or surface to the user.
262
+ print(f"Op timed out after {e.timeout_ms} ms")
263
+ ```
264
+
265
+ - Test coverage at `tests/test_errors.py::test_session_timeout_extracts_timeout_ms`.
266
+
267
+ ## [0.1.3] - 2026-05-03
268
+
269
+ ### Removed
270
+
271
+ - `tap.offset` field stripped from the public `InteractAction.tap`
272
+ shape. Same reason as `tap_at`: a coordinate primitive on the
273
+ customer-facing schema lets the customer bypass the behavioral
274
+ simulation layer. Bounded coordinates are still coordinates.
275
+
276
+ ### Migration
277
+
278
+ If your code passes `offset={"x": ..., "y": ...}` to a `tap`
279
+ action, the value is now silently dropped (Pydantic strips unknown
280
+ keys by default). Re-express the intent through selector
281
+ specificity — better selectors, child-element targeting, ARIA-role
282
+ qualifiers, text-content matching:
283
+
284
+ ```python
285
+ # Before (0.1.x):
286
+ client.sessions.interact(
287
+ session_id,
288
+ InteractRequest(action={"kind": "tap", "selector": "button.cta", "offset": {"x": 0, "y": 50}}),
289
+ )
290
+
291
+ # After (0.1.3+):
292
+ client.sessions.interact(
293
+ session_id,
294
+ InteractRequest(action={"kind": "tap", "selector": "button.cta .icon-arrow"}),
295
+ )
296
+ ```
297
+
298
+ Coordinate-level addressing for screenshot-driven workflows lives
299
+ behind a separate endpoint gated by the `gui_control` API-key
300
+ scope, and is not exposed in this SDK.
301
+
302
+ ## [0.1.2] - 2026-05-03
303
+
304
+ ### Added
305
+
306
+ - Wire-shape regression tests at `tests/test_wire_shape.py` (10
307
+ tests). Locks the canonical JSON shape for `InteractRequest`,
308
+ `WaitRequest`, `NavigateRequest`. Asserts rejection of `tap_at` /
309
+ `type_focused` (these live behind the `gui_control`-scoped
310
+ endpoint).
311
+
312
+ ### Fixed
313
+
314
+ - `tests/test_client.py::test_version_string_matches_pyproject_default`
315
+ was pinning `__version__ == "0.0.1"` (stale from the pre-publish
316
+ era). Fixed to assert SemVer shape, not exact value.
317
+
318
+ ## [0.1.1] - 2026-05-02
319
+
320
+ ### Changed
321
+
322
+ - Re-cut: `tap_at` and `type_focused` removed from
323
+ `InteractAction`. They were briefly added in 0.1.0+ for the
324
+ self-hosted GUI's manual-control input forwarding, and reverted.
325
+ Customer-facing schemas stay intent-only — coordinate primitives
326
+ bypass the behavioral simulation layer. The GUI now uses a
327
+ separate, scope-gated endpoint
328
+ (`/v1/sessions/:id/gui-input`).
329
+
330
+ ## [0.1.0] - 2026-05-02
331
+
332
+ ### Added
333
+
334
+ - Inaugural PyPI publish (under a maintainer account pre-entity;
335
+ will transfer to a company-owned account once the legal entity
336
+ is registered).
337
+ - Pydantic models regenerated from updated OpenAPI spec.
338
+
339
+ ## [0.0.1] - 2026-05-02
340
+
341
+ ### Added
342
+
343
+ - `Driftstack` (sync) and `AsyncDriftstack` (async) clients.
344
+ - Resource accessors mounted on the client: `sessions` (9 methods),
345
+ `api_keys` (3), `usage` (1), `webhooks` (5).
346
+ - Typed Pydantic v2 models generated from the API's OpenAPI 3.1 spec.
347
+ - Error hierarchy: `DriftstackError` base + 14 subclasses covering
348
+ every documented RFC 7807 problem type. `RateLimitError`,
349
+ `ConcurrencyLimitError`, `QuotaExceededError` carry the relevant
350
+ payload fields.
351
+ - Retry policy (`RetryConfig` + `with_retry`) with exponential
352
+ backoff and full jitter; honours `Retry-After`.
353
+ - `verify_webhook_signature` helper (Stripe-style HMAC-SHA256,
354
+ constant-time).
355
+ - 85 tests covering errors, retry, webhook signatures, every
356
+ resource method, and end-to-end customer-journey workflows.
357
+ - Examples: `quickstart`, `error_handling`, `webhook_receiver`,
358
+ `langchain_tool`, `pytest_fixture`.
359
+
360
+ ### Build
361
+
362
+ - Hatchling backend, `py.typed` marker for PEP 561.
363
+ - Runtime deps: `httpx>=0.27,<1.0`, `pydantic[email]>=2.5,<3.0`.
364
+ - Dev deps: `pytest`, `pytest-asyncio`, `respx`, `ruff`, `mypy`,
365
+ `datamodel-code-generator`.
366
+ - CI: lint + format + mypy + pytest on Ubuntu / Python 3.10.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 driftstackdev
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.