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 +60 -0
- driftstack/_generated/__init__.py +6 -0
- driftstack/_generated/models.py +494 -0
- driftstack/_version.py +10 -0
- driftstack/client.py +120 -0
- driftstack/errors.py +194 -0
- driftstack/http.py +291 -0
- driftstack/py.typed +0 -0
- driftstack/resources/__init__.py +29 -0
- driftstack/resources/_common.py +38 -0
- driftstack/resources/api_keys.py +61 -0
- driftstack/resources/sessions.py +156 -0
- driftstack/resources/usage.py +29 -0
- driftstack/resources/webhooks.py +110 -0
- driftstack/retry.py +105 -0
- driftstack/webhook_signature.py +106 -0
- driftstack_sdk-0.1.0.dist-info/METADATA +226 -0
- driftstack_sdk-0.1.0.dist-info/RECORD +19 -0
- driftstack_sdk-0.1.0.dist-info/WHEEL +4 -0
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,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()
|