driftstack-sdk 0.1.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.
driftstack/__init__.py ADDED
@@ -0,0 +1,60 @@
1
+ """Driftstack Python SDK.
2
+
3
+ Customer-facing entry points re-exported here so callers can write::
4
+
5
+ from driftstack import Driftstack, AsyncDriftstack, DriftstackError
6
+
7
+ Resource accessors live on the client instance::
8
+
9
+ client = Driftstack(api_key="ds_live_...")
10
+ session = client.sessions.create()
11
+ client.sessions.navigate(session.id, url="https://example.com")
12
+ client.sessions.destroy(session.id)
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from driftstack._version import __version__
18
+ from driftstack.client import AsyncDriftstack, Driftstack
19
+ from driftstack.errors import (
20
+ AuthError,
21
+ ConcurrencyLimitError,
22
+ ConflictError,
23
+ DriftstackError,
24
+ DriverError,
25
+ ExpiredKeyError,
26
+ ForbiddenError,
27
+ InvalidKeyError,
28
+ NotFoundError,
29
+ QuotaExceededError,
30
+ RateLimitError,
31
+ RevokedKeyError,
32
+ SessionDestroyedError,
33
+ SessionNotFoundError,
34
+ TransportError,
35
+ ValidationError,
36
+ )
37
+ from driftstack.webhook_signature import verify_webhook_signature
38
+
39
+ __all__ = [
40
+ "__version__",
41
+ "Driftstack",
42
+ "AsyncDriftstack",
43
+ "DriftstackError",
44
+ "AuthError",
45
+ "ForbiddenError",
46
+ "InvalidKeyError",
47
+ "ExpiredKeyError",
48
+ "RevokedKeyError",
49
+ "ConflictError",
50
+ "NotFoundError",
51
+ "RateLimitError",
52
+ "QuotaExceededError",
53
+ "ConcurrencyLimitError",
54
+ "SessionNotFoundError",
55
+ "SessionDestroyedError",
56
+ "DriverError",
57
+ "ValidationError",
58
+ "TransportError",
59
+ "verify_webhook_signature",
60
+ ]
@@ -0,0 +1,6 @@
1
+ """Codegen output. Regenerated by ``scripts/generate.sh``.
2
+
3
+ Hand-edit nothing in this package — it gets clobbered every regeneration.
4
+ Customer-facing types are re-exported from :mod:`driftstack` (the package
5
+ root); to add a wrapping layer, edit there.
6
+ """
@@ -0,0 +1,494 @@
1
+ # generated by datamodel-codegen:
2
+ # filename: openapi.json
3
+ # timestamp: 2026-05-02T16:11:24+00:00
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import Any, Literal
8
+ from uuid import UUID
9
+
10
+ from pydantic import AnyUrl, AwareDatetime, BaseModel, EmailStr, Field, conint, constr
11
+
12
+
13
+ class Account(BaseModel):
14
+ id: constr(
15
+ pattern=r"^acc_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
16
+ )
17
+ email: EmailStr
18
+ name: str | None
19
+ tier: Literal["free", "starter", "solo", "builder", "scale", "enterprise"]
20
+ status: Literal["active", "suspended", "deleted"]
21
+ created_at: AwareDatetime
22
+ """
23
+ ISO 8601 timestamp with timezone offset, e.g. 2026-05-02T09:15:00Z
24
+ """
25
+ updated_at: AwareDatetime
26
+ """
27
+ ISO 8601 timestamp with timezone offset, e.g. 2026-05-02T09:15:00Z
28
+ """
29
+
30
+
31
+ class ApiKey(BaseModel):
32
+ id: constr(
33
+ pattern=r"^key_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
34
+ )
35
+ name: str
36
+ key_prefix: str
37
+ scopes: list[Literal["read", "write", "admin"]]
38
+ last_used_at: AwareDatetime | None
39
+ """
40
+ ISO 8601 timestamp with timezone offset, e.g. 2026-05-02T09:15:00Z
41
+ """
42
+ revoked_at: AwareDatetime | None
43
+ """
44
+ ISO 8601 timestamp with timezone offset, e.g. 2026-05-02T09:15:00Z
45
+ """
46
+ expires_at: AwareDatetime | None
47
+ """
48
+ ISO 8601 timestamp with timezone offset, e.g. 2026-05-02T09:15:00Z
49
+ """
50
+ created_at: AwareDatetime
51
+ """
52
+ ISO 8601 timestamp with timezone offset, e.g. 2026-05-02T09:15:00Z
53
+ """
54
+
55
+
56
+ class Session(BaseModel):
57
+ id: constr(
58
+ pattern=r"^ses_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
59
+ )
60
+ account_id: constr(
61
+ pattern=r"^acc_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
62
+ )
63
+ api_key_id: constr(
64
+ pattern=r"^key_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
65
+ )
66
+ status: Literal["creating", "ready", "busy", "destroyed", "errored"]
67
+ archetype: constr(pattern=r"^[a-z0-9_]+$", min_length=3, max_length=60)
68
+ label: str | None
69
+ metadata: dict[str, Any] | None
70
+ created_at: AwareDatetime
71
+ """
72
+ ISO 8601 timestamp with timezone offset, e.g. 2026-05-02T09:15:00Z
73
+ """
74
+ updated_at: AwareDatetime
75
+ """
76
+ ISO 8601 timestamp with timezone offset, e.g. 2026-05-02T09:15:00Z
77
+ """
78
+ last_state_at: AwareDatetime | None
79
+ """
80
+ ISO 8601 timestamp with timezone offset, e.g. 2026-05-02T09:15:00Z
81
+ """
82
+ destroyed_at: AwareDatetime | None
83
+ """
84
+ ISO 8601 timestamp with timezone offset, e.g. 2026-05-02T09:15:00Z
85
+ """
86
+
87
+
88
+ class SessionState(BaseModel):
89
+ url: AnyUrl | None
90
+ title: str | None
91
+ cookies: list[dict[str, Any]]
92
+ local_storage: dict[str, str]
93
+ captured_at: AwareDatetime
94
+ """
95
+ ISO 8601 timestamp with timezone offset, e.g. 2026-05-02T09:15:00Z
96
+ """
97
+
98
+
99
+ class Problem(BaseModel):
100
+ """
101
+ RFC 7807 problem details
102
+ """
103
+
104
+ type: AnyUrl
105
+ """
106
+ Stable URI identifying the problem class. Clients switch on this.
107
+ """
108
+ title: str
109
+ status: conint(ge=100, le=599)
110
+ detail: str | None = None
111
+ instance: str | None = None
112
+
113
+
114
+ class Totals(BaseModel):
115
+ session_minute: conint(ge=0) | None = None
116
+ navigate: conint(ge=0) | None = None
117
+ interact: conint(ge=0) | None = None
118
+ wait: conint(ge=0) | None = None
119
+ state_capture: conint(ge=0) | None = None
120
+ screenshot_capture: conint(ge=0) | None = None
121
+
122
+
123
+ class Quotas(BaseModel):
124
+ session_minute: conint(ge=0) | None = None
125
+ navigate: conint(ge=0) | None = None
126
+ interact: conint(ge=0) | None = None
127
+ wait: conint(ge=0) | None = None
128
+ state_capture: conint(ge=0) | None = None
129
+ screenshot_capture: conint(ge=0) | None = None
130
+
131
+
132
+ class UsagePeriodSummary(BaseModel):
133
+ period_start: AwareDatetime
134
+ """
135
+ ISO 8601 timestamp with timezone offset, e.g. 2026-05-02T09:15:00Z
136
+ """
137
+ period_end: AwareDatetime
138
+ """
139
+ ISO 8601 timestamp with timezone offset, e.g. 2026-05-02T09:15:00Z
140
+ """
141
+ tier: Literal["free", "starter", "solo", "builder", "scale", "enterprise"]
142
+ totals: Totals
143
+ quotas: Quotas
144
+
145
+
146
+ class PaginationQuery(BaseModel):
147
+ limit: conint(ge=1, le=100) | None = 50
148
+ cursor: str | None = None
149
+
150
+
151
+ class CreateSessionRequest(BaseModel):
152
+ archetype: constr(pattern=r"^[a-z0-9_]+$", min_length=3, max_length=60) | None = (
153
+ None
154
+ )
155
+ label: constr(max_length=120) | None = None
156
+ metadata: dict[str, Any] | None = None
157
+
158
+
159
+ class CreateSessionResponse(BaseModel):
160
+ id: constr(
161
+ pattern=r"^ses_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
162
+ )
163
+ account_id: constr(
164
+ pattern=r"^acc_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
165
+ )
166
+ api_key_id: constr(
167
+ pattern=r"^key_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
168
+ )
169
+ status: Literal["creating", "ready", "busy", "destroyed", "errored"]
170
+ archetype: constr(pattern=r"^[a-z0-9_]+$", min_length=3, max_length=60)
171
+ label: str | None
172
+ metadata: dict[str, Any] | None
173
+ created_at: AwareDatetime
174
+ """
175
+ ISO 8601 timestamp with timezone offset, e.g. 2026-05-02T09:15:00Z
176
+ """
177
+ updated_at: AwareDatetime
178
+ """
179
+ ISO 8601 timestamp with timezone offset, e.g. 2026-05-02T09:15:00Z
180
+ """
181
+ last_state_at: AwareDatetime | None
182
+ """
183
+ ISO 8601 timestamp with timezone offset, e.g. 2026-05-02T09:15:00Z
184
+ """
185
+ destroyed_at: AwareDatetime | None
186
+ """
187
+ ISO 8601 timestamp with timezone offset, e.g. 2026-05-02T09:15:00Z
188
+ """
189
+
190
+
191
+ class NavigateRequest(BaseModel):
192
+ url: AnyUrl
193
+ timeout_ms: conint(ge=1000, le=120000) | None = None
194
+ wait_until: Literal["load", "domcontentloaded", "networkidle"] | None = "load"
195
+
196
+
197
+ class NavigateResponse(BaseModel):
198
+ url: AnyUrl
199
+ status: conint(ge=100, le=599)
200
+ final_url: AnyUrl
201
+ duration_ms: conint(ge=0)
202
+
203
+
204
+ class Offset(BaseModel):
205
+ x: int
206
+ y: int
207
+
208
+
209
+ class Action(BaseModel):
210
+ kind: Literal["tap"]
211
+ selector: constr(min_length=1)
212
+ offset: Offset | None = None
213
+
214
+
215
+ class Action1(BaseModel):
216
+ kind: Literal["type"]
217
+ selector: constr(min_length=1)
218
+ text: constr(max_length=10000)
219
+ delay_ms: conint(ge=0, le=500) | None = None
220
+
221
+
222
+ class Action2(BaseModel):
223
+ kind: Literal["scroll"]
224
+ selector: constr(min_length=1) | None = None
225
+ delta_x: int | None = 0
226
+ delta_y: int | None = 0
227
+
228
+
229
+ class Action3(BaseModel):
230
+ kind: Literal["press"]
231
+ key: constr(min_length=1, max_length=20)
232
+
233
+
234
+ class InteractRequest(BaseModel):
235
+ action: Action | Action1 | Action2 | Action3
236
+ timeout_ms: conint(ge=100, le=60000) | None = None
237
+
238
+
239
+ class InteractResponse(BaseModel):
240
+ ok: Literal[True]
241
+ duration_ms: conint(ge=0)
242
+
243
+
244
+ class Condition(BaseModel):
245
+ kind: Literal["selector"]
246
+ selector: constr(min_length=1)
247
+
248
+
249
+ class Condition1(BaseModel):
250
+ kind: Literal["selector_hidden"]
251
+ selector: constr(min_length=1)
252
+
253
+
254
+ class Condition2(BaseModel):
255
+ kind: Literal["url_matches"]
256
+ pattern: constr(min_length=1)
257
+
258
+
259
+ class Condition3(BaseModel):
260
+ kind: Literal["time"]
261
+ ms: conint(ge=0, le=60000)
262
+
263
+
264
+ class WaitRequest(BaseModel):
265
+ condition: Condition | Condition1 | Condition2 | Condition3
266
+ timeout_ms: conint(ge=100, le=120000) | None = None
267
+
268
+
269
+ class WaitResponse(BaseModel):
270
+ satisfied: bool
271
+ duration_ms: conint(ge=0)
272
+
273
+
274
+ class CaptureRequest(BaseModel):
275
+ kind: Literal["screenshot", "dom_snapshot", "pdf"]
276
+ full_page: bool | None = False
277
+
278
+
279
+ class CaptureResponse(BaseModel):
280
+ kind: Literal["screenshot", "dom_snapshot", "pdf"]
281
+ data: str
282
+ encoding: Literal["base64", "utf8"]
283
+ byte_size: conint(ge=0)
284
+ duration_ms: conint(ge=0)
285
+
286
+
287
+ class CreateApiKeyRequest(BaseModel):
288
+ name: constr(min_length=1, max_length=120)
289
+ scopes: list[Literal["read", "write", "admin"]] = Field(..., min_length=1)
290
+ expires_at: AwareDatetime | None = None
291
+ """
292
+ ISO 8601 timestamp with timezone offset, e.g. 2026-05-02T09:15:00Z
293
+ """
294
+
295
+
296
+ class CreateApiKeyResponse(BaseModel):
297
+ id: constr(
298
+ pattern=r"^key_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
299
+ )
300
+ name: str
301
+ key_prefix: str
302
+ scopes: list[Literal["read", "write", "admin"]]
303
+ last_used_at: AwareDatetime | None
304
+ """
305
+ ISO 8601 timestamp with timezone offset, e.g. 2026-05-02T09:15:00Z
306
+ """
307
+ revoked_at: AwareDatetime | None
308
+ """
309
+ ISO 8601 timestamp with timezone offset, e.g. 2026-05-02T09:15:00Z
310
+ """
311
+ expires_at: AwareDatetime | None
312
+ """
313
+ ISO 8601 timestamp with timezone offset, e.g. 2026-05-02T09:15:00Z
314
+ """
315
+ created_at: AwareDatetime
316
+ """
317
+ ISO 8601 timestamp with timezone offset, e.g. 2026-05-02T09:15:00Z
318
+ """
319
+ plaintext: str
320
+ """
321
+ The plaintext key. Shown once at creation; not retrievable later.
322
+ """
323
+
324
+
325
+ class WebhookEndpoint(BaseModel):
326
+ id: constr(
327
+ pattern=r"^whk_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
328
+ )
329
+ url: AnyUrl
330
+ secret_prefix: str
331
+ events: list[
332
+ Literal[
333
+ "session.completed",
334
+ "session.failed",
335
+ "quota.warning_80pct",
336
+ "quota.exceeded",
337
+ "api_key.revoked",
338
+ ]
339
+ ]
340
+ description: str | None
341
+ active: bool
342
+ consecutive_failures: conint(ge=0)
343
+ last_success_at: AwareDatetime | None
344
+ """
345
+ ISO 8601 timestamp with timezone offset, e.g. 2026-05-02T09:15:00Z
346
+ """
347
+ last_failure_at: AwareDatetime | None
348
+ """
349
+ ISO 8601 timestamp with timezone offset, e.g. 2026-05-02T09:15:00Z
350
+ """
351
+ disabled_at: AwareDatetime | None
352
+ """
353
+ ISO 8601 timestamp with timezone offset, e.g. 2026-05-02T09:15:00Z
354
+ """
355
+ created_at: AwareDatetime
356
+ """
357
+ ISO 8601 timestamp with timezone offset, e.g. 2026-05-02T09:15:00Z
358
+ """
359
+
360
+
361
+ class CreateWebhookRequest(BaseModel):
362
+ url: AnyUrl
363
+ events: list[
364
+ Literal[
365
+ "session.completed",
366
+ "session.failed",
367
+ "quota.warning_80pct",
368
+ "quota.exceeded",
369
+ "api_key.revoked",
370
+ ]
371
+ ] = Field(..., max_length=10, min_length=1)
372
+ description: constr(max_length=200) | None = None
373
+
374
+
375
+ class CreateWebhookResponse(BaseModel):
376
+ id: constr(
377
+ pattern=r"^whk_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
378
+ )
379
+ url: AnyUrl
380
+ secret_prefix: str
381
+ events: list[
382
+ Literal[
383
+ "session.completed",
384
+ "session.failed",
385
+ "quota.warning_80pct",
386
+ "quota.exceeded",
387
+ "api_key.revoked",
388
+ ]
389
+ ]
390
+ description: str | None
391
+ active: bool
392
+ consecutive_failures: conint(ge=0)
393
+ last_success_at: AwareDatetime | None
394
+ """
395
+ ISO 8601 timestamp with timezone offset, e.g. 2026-05-02T09:15:00Z
396
+ """
397
+ last_failure_at: AwareDatetime | None
398
+ """
399
+ ISO 8601 timestamp with timezone offset, e.g. 2026-05-02T09:15:00Z
400
+ """
401
+ disabled_at: AwareDatetime | None
402
+ """
403
+ ISO 8601 timestamp with timezone offset, e.g. 2026-05-02T09:15:00Z
404
+ """
405
+ created_at: AwareDatetime
406
+ """
407
+ ISO 8601 timestamp with timezone offset, e.g. 2026-05-02T09:15:00Z
408
+ """
409
+ secret: str
410
+ """
411
+ Plaintext signing secret. Returned ONCE; not retrievable later.
412
+ """
413
+
414
+
415
+ class WebhookDelivery(BaseModel):
416
+ id: constr(
417
+ pattern=r"^wdl_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
418
+ )
419
+ webhook_id: constr(
420
+ pattern=r"^whk_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
421
+ )
422
+ event_id: UUID
423
+ event_type: Literal[
424
+ "session.completed",
425
+ "session.failed",
426
+ "quota.warning_80pct",
427
+ "quota.exceeded",
428
+ "api_key.revoked",
429
+ ]
430
+ status: Literal["pending", "in_flight", "delivered", "failed", "dlq"]
431
+ attempts: conint(ge=0)
432
+ next_attempt_at: AwareDatetime
433
+ """
434
+ ISO 8601 timestamp with timezone offset, e.g. 2026-05-02T09:15:00Z
435
+ """
436
+ last_response_status: int | None
437
+ last_response_excerpt: str | None
438
+ last_error: str | None
439
+ delivered_at: AwareDatetime | None
440
+ """
441
+ ISO 8601 timestamp with timezone offset, e.g. 2026-05-02T09:15:00Z
442
+ """
443
+ created_at: AwareDatetime
444
+ """
445
+ ISO 8601 timestamp with timezone offset, e.g. 2026-05-02T09:15:00Z
446
+ """
447
+
448
+
449
+ class ListDeliveriesQuery(BaseModel):
450
+ limit: conint(ge=1, le=100) | None = 50
451
+ cursor: str | None = None
452
+ status: Literal["pending", "in_flight", "delivered", "failed", "dlq"] | None = None
453
+
454
+
455
+ class AdminAccount(BaseModel):
456
+ id: constr(
457
+ pattern=r"^acc_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
458
+ )
459
+ email: EmailStr
460
+ name: str | None
461
+ tier: Literal["free", "starter", "solo", "builder", "scale", "enterprise"]
462
+ status: Literal["active", "suspended", "deleted"]
463
+ created_at: AwareDatetime
464
+ """
465
+ ISO 8601 timestamp with timezone offset, e.g. 2026-05-02T09:15:00Z
466
+ """
467
+ updated_at: AwareDatetime
468
+ """
469
+ ISO 8601 timestamp with timezone offset, e.g. 2026-05-02T09:15:00Z
470
+ """
471
+
472
+
473
+ class AdminAuditLogEntry(BaseModel):
474
+ id: UUID
475
+ admin_account_id: str
476
+ admin_key_id: str
477
+ action: Literal[
478
+ "account.tier_changed",
479
+ "account.suspended",
480
+ "account.unsuspended",
481
+ "webhook_delivery.replayed",
482
+ "webhook_delivery.requeued",
483
+ "rate_limit_override.set",
484
+ "rate_limit_override.cleared",
485
+ ]
486
+ target_account_id: str | None
487
+ target_resource_id: str | None
488
+ input_payload: dict[str, Any] | None
489
+ result: str
490
+ ip_address: str | None
491
+ timestamp: AwareDatetime
492
+ """
493
+ ISO 8601 timestamp with timezone offset, e.g. 2026-05-02T09:15:00Z
494
+ """
driftstack/_version.py ADDED
@@ -0,0 +1,10 @@
1
+ """Single source of truth for the SDK's package version.
2
+
3
+ Kept in a tiny module so `__init__.py` can import it without pulling
4
+ in the full dependency graph (httpx, pydantic) before the version is
5
+ needed — useful for tools that scrape the version without installing.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ __version__ = "0.1.0"
driftstack/client.py ADDED
@@ -0,0 +1,120 @@
1
+ """Top-level Driftstack client.
2
+
3
+ Two parallel classes — :class:`Driftstack` (sync, ``httpx.Client``)
4
+ and :class:`AsyncDriftstack` (async, ``httpx.AsyncClient``). Mirrors
5
+ the pattern used by Stripe-Python, OpenAI-Python, Anthropic-Python.
6
+
7
+ Resource accessors are placeholders in this commit — full
8
+ implementations land in PY2. The constructor + auth + transport
9
+ plumbing is covered by the smoke tests in this commit.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import Any
15
+
16
+ import httpx
17
+
18
+ from driftstack.http import AsyncHttpClient, HttpClient
19
+ from driftstack.resources.api_keys import ApiKeysResource, AsyncApiKeysResource
20
+ from driftstack.resources.sessions import AsyncSessionsResource, SessionsResource
21
+ from driftstack.resources.usage import AsyncUsageResource, UsageResource
22
+ from driftstack.resources.webhooks import AsyncWebhooksResource, WebhooksResource
23
+ from driftstack.retry import RetryConfig
24
+
25
+ DEFAULT_BASE_URL = "https://api.driftstack.dev"
26
+
27
+
28
+ def _validate_api_key(api_key: str) -> None:
29
+ if not api_key or not isinstance(api_key, str):
30
+ raise TypeError("Driftstack: api_key is required and must be a string")
31
+
32
+
33
+ # ──────────────────────────────────────────────────────────────────────────
34
+ # Sync
35
+ # ──────────────────────────────────────────────────────────────────────────
36
+
37
+
38
+ class Driftstack:
39
+ """Synchronous Driftstack API client.
40
+
41
+ Example::
42
+
43
+ from driftstack import Driftstack
44
+
45
+ client = Driftstack(api_key="ds_live_…")
46
+ session = client.sessions.create()
47
+ client.sessions.navigate(session.id, {"url": "https://example.com"})
48
+ client.sessions.destroy(session.id)
49
+ """
50
+
51
+ def __init__(
52
+ self,
53
+ api_key: str,
54
+ *,
55
+ base_url: str = DEFAULT_BASE_URL,
56
+ timeout_s: float = 30.0,
57
+ retry: RetryConfig | None = None,
58
+ http_client: httpx.Client | None = None,
59
+ ) -> None:
60
+ _validate_api_key(api_key)
61
+ self._http = HttpClient(
62
+ api_key,
63
+ base_url=base_url,
64
+ timeout_s=timeout_s,
65
+ retry=retry,
66
+ client=http_client,
67
+ )
68
+ self.sessions = SessionsResource(self._http)
69
+ self.api_keys = ApiKeysResource(self._http)
70
+ self.usage = UsageResource(self._http)
71
+ self.webhooks = WebhooksResource(self._http)
72
+
73
+ def close(self) -> None:
74
+ self._http.close()
75
+
76
+ def __enter__(self) -> Driftstack:
77
+ return self
78
+
79
+ def __exit__(self, *_excinfo: Any) -> None:
80
+ self.close()
81
+
82
+
83
+ # ──────────────────────────────────────────────────────────────────────────
84
+ # Async
85
+ # ──────────────────────────────────────────────────────────────────────────
86
+
87
+
88
+ class AsyncDriftstack:
89
+ """Async Driftstack API client. asyncio-compatible."""
90
+
91
+ def __init__(
92
+ self,
93
+ api_key: str,
94
+ *,
95
+ base_url: str = DEFAULT_BASE_URL,
96
+ timeout_s: float = 30.0,
97
+ retry: RetryConfig | None = None,
98
+ http_client: httpx.AsyncClient | None = None,
99
+ ) -> None:
100
+ _validate_api_key(api_key)
101
+ self._http = AsyncHttpClient(
102
+ api_key,
103
+ base_url=base_url,
104
+ timeout_s=timeout_s,
105
+ retry=retry,
106
+ client=http_client,
107
+ )
108
+ self.sessions = AsyncSessionsResource(self._http)
109
+ self.api_keys = AsyncApiKeysResource(self._http)
110
+ self.usage = AsyncUsageResource(self._http)
111
+ self.webhooks = AsyncWebhooksResource(self._http)
112
+
113
+ async def aclose(self) -> None:
114
+ await self._http.aclose()
115
+
116
+ async def __aenter__(self) -> AsyncDriftstack:
117
+ return self
118
+
119
+ async def __aexit__(self, *_excinfo: Any) -> None:
120
+ await self.aclose()