stacklink-sdk 0.2.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.
- stacklink/__init__.py +42 -0
- stacklink/api_keys.py +18 -0
- stacklink/artifacts.py +9 -0
- stacklink/auth_configs.py +29 -0
- stacklink/billing.py +43 -0
- stacklink/claude_agent.py +29 -0
- stacklink/client.py +292 -0
- stacklink/connected_accounts.py +95 -0
- stacklink/errors.py +15 -0
- stacklink/executions.py +45 -0
- stacklink/knowledge.py +24 -0
- stacklink/logs.py +29 -0
- stacklink/mcp.py +76 -0
- stacklink/memory.py +235 -0
- stacklink/openai_agents.py +40 -0
- stacklink/openai_memory.py +432 -0
- stacklink/projects.py +20 -0
- stacklink/sessions.py +223 -0
- stacklink/tools.py +144 -0
- stacklink/triggers.py +27 -0
- stacklink/types.py +290 -0
- stacklink/webhooks.py +29 -0
- stacklink/workbench.py +1090 -0
- stacklink_sdk-0.2.0.dist-info/METADATA +71 -0
- stacklink_sdk-0.2.0.dist-info/RECORD +28 -0
- stacklink_sdk-0.2.0.dist-info/WHEEL +5 -0
- stacklink_sdk-0.2.0.dist-info/licenses/LICENSE +9 -0
- stacklink_sdk-0.2.0.dist-info/top_level.txt +1 -0
stacklink/workbench.py
ADDED
|
@@ -0,0 +1,1090 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import time
|
|
5
|
+
from typing import Any, Iterator, Literal, Mapping, TypedDict, cast
|
|
6
|
+
from urllib.error import HTTPError
|
|
7
|
+
from urllib.request import Request, urlopen
|
|
8
|
+
|
|
9
|
+
from .errors import StacklinkError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
WorkbenchLanguage = Literal["python", "javascript", "typescript"]
|
|
13
|
+
WorkbenchOutputMode = Literal["inline", "preview", "events", "artifact"]
|
|
14
|
+
WorkbenchExecutionKind = Literal["workbench_code", "workbench_bash"]
|
|
15
|
+
WorkbenchReplayEnvironmentMode = Literal["require_match", "restore_snapshot", "current"]
|
|
16
|
+
WorkbenchExecutionStatus = Literal[
|
|
17
|
+
"queued",
|
|
18
|
+
"started",
|
|
19
|
+
"running",
|
|
20
|
+
"cancel_requested",
|
|
21
|
+
"completed",
|
|
22
|
+
"failed",
|
|
23
|
+
"cancelled",
|
|
24
|
+
"timed_out",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class _WorkbenchUnset:
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
_WORKBENCH_UNSET = _WorkbenchUnset()
|
|
33
|
+
_REUSABLE_RUN_COMPARISON_WAIT_SECONDS = 120.0
|
|
34
|
+
_REUSABLE_RUN_COMPARISON_POLL_SECONDS = 1.0
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class WorkbenchReusableRunRequest(TypedDict, total=False):
|
|
38
|
+
mode: Literal["auto", "off"]
|
|
39
|
+
seriesId: str
|
|
40
|
+
name: str
|
|
41
|
+
description: str
|
|
42
|
+
versionReason: str
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class WorkbenchSession(TypedDict, total=False):
|
|
46
|
+
id: str
|
|
47
|
+
platformSessionId: str
|
|
48
|
+
consumerAppId: str
|
|
49
|
+
tenantId: str
|
|
50
|
+
status: str
|
|
51
|
+
runtimeProvider: str | None
|
|
52
|
+
runtimeSessionId: str | None
|
|
53
|
+
sandboxSize: Literal["standard", "medium", "large"]
|
|
54
|
+
runtimeSandboxSize: Literal["standard", "medium", "large"] | None
|
|
55
|
+
runtimeGeneration: int
|
|
56
|
+
runtimeEnvironmentRestoredGeneration: int
|
|
57
|
+
activeExecutionCount: int
|
|
58
|
+
workspaceStorageKind: str
|
|
59
|
+
workspaceStorageRef: str | None
|
|
60
|
+
environmentManifest: dict[str, Any]
|
|
61
|
+
lastActiveAt: str
|
|
62
|
+
destroyedAt: str | None
|
|
63
|
+
createdAt: str
|
|
64
|
+
updatedAt: str
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class WorkbenchSessionUseResponse(TypedDict):
|
|
68
|
+
workbenchSession: WorkbenchSession
|
|
69
|
+
reused: bool
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class WorkbenchSessionDestroyResponse(TypedDict):
|
|
73
|
+
workbenchSession: WorkbenchSession
|
|
74
|
+
computeSuspended: bool
|
|
75
|
+
retainedFiles: bool
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class WorkbenchWorkspaceDeleteReceipt(TypedDict):
|
|
79
|
+
deleted: Literal[True]
|
|
80
|
+
workspaceKey: str
|
|
81
|
+
deletedWorkspaceObjects: int
|
|
82
|
+
deletedExecutionObjects: int
|
|
83
|
+
deletedObjects: int
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class WorkbenchDeletedDatabaseRows(TypedDict):
|
|
87
|
+
workbenchSessions: int
|
|
88
|
+
backgroundJobs: int
|
|
89
|
+
files: int
|
|
90
|
+
fileUploads: int
|
|
91
|
+
artifacts: int
|
|
92
|
+
executionLogs: int
|
|
93
|
+
executionEvents: int
|
|
94
|
+
executionEventStreams: int
|
|
95
|
+
runDefinitions: int
|
|
96
|
+
environmentSnapshots: int
|
|
97
|
+
usageEvents: int
|
|
98
|
+
costLedger: int
|
|
99
|
+
budgetPolicies: int
|
|
100
|
+
budgetReservations: int
|
|
101
|
+
runtimeHeartbeats: int
|
|
102
|
+
approvalRequests: int
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class WorkbenchSessionDeleteReceipt(TypedDict):
|
|
106
|
+
deleted: Literal[True]
|
|
107
|
+
workbenchSessionId: str
|
|
108
|
+
platformSessionId: str
|
|
109
|
+
computeDestroyed: bool
|
|
110
|
+
deletedArtifactObjects: int
|
|
111
|
+
deletedUploadObjects: int
|
|
112
|
+
deletedSnapshots: int
|
|
113
|
+
workspace: WorkbenchWorkspaceDeleteReceipt
|
|
114
|
+
deletedDatabaseRows: WorkbenchDeletedDatabaseRows
|
|
115
|
+
databaseDeleted: Literal[True]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class WorkbenchRunInput(TypedDict, total=False):
|
|
119
|
+
language: WorkbenchLanguage
|
|
120
|
+
code: str
|
|
121
|
+
workbenchSessionId: str
|
|
122
|
+
timeoutMs: int
|
|
123
|
+
background: bool
|
|
124
|
+
notifyOnComplete: bool
|
|
125
|
+
outputMode: WorkbenchOutputMode
|
|
126
|
+
maxOutputBytes: int
|
|
127
|
+
reusableRun: WorkbenchReusableRunRequest
|
|
128
|
+
currentStep: str
|
|
129
|
+
currentStepMetric: str
|
|
130
|
+
thought: str
|
|
131
|
+
metadata: dict[str, Any]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class WorkbenchBashInput(TypedDict, total=False):
|
|
135
|
+
command: str
|
|
136
|
+
workbenchSessionId: str
|
|
137
|
+
timeoutMs: int
|
|
138
|
+
background: bool
|
|
139
|
+
notifyOnComplete: bool
|
|
140
|
+
outputMode: WorkbenchOutputMode
|
|
141
|
+
maxOutputBytes: int
|
|
142
|
+
reusableRun: WorkbenchReusableRunRequest
|
|
143
|
+
currentStep: str
|
|
144
|
+
currentStepMetric: str
|
|
145
|
+
thought: str
|
|
146
|
+
metadata: dict[str, Any]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class WorkbenchExecutionLog(TypedDict, total=False):
|
|
150
|
+
id: str
|
|
151
|
+
workbenchSessionId: str
|
|
152
|
+
operation: str
|
|
153
|
+
language: WorkbenchLanguage | None
|
|
154
|
+
status: WorkbenchExecutionStatus
|
|
155
|
+
durationMs: int | None
|
|
156
|
+
artifactId: str | None
|
|
157
|
+
timeoutMs: int | None
|
|
158
|
+
deadlineAt: str | None
|
|
159
|
+
cancelRequestedAt: str | None
|
|
160
|
+
cancelledAt: str | None
|
|
161
|
+
runtimeProvider: str | None
|
|
162
|
+
runtimeSessionId: str | None
|
|
163
|
+
runtimeProcessId: str | None
|
|
164
|
+
runtimeOutputCursor: str | None
|
|
165
|
+
approvalRequestId: str | None
|
|
166
|
+
idempotencyKey: str | None
|
|
167
|
+
runtimeStartedAt: str | None
|
|
168
|
+
startedAt: str
|
|
169
|
+
completedAt: str | None
|
|
170
|
+
metadata: dict[str, Any]
|
|
171
|
+
result: Any
|
|
172
|
+
reusableRun: "WorkbenchReusableRunState"
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class WorkbenchObservableResultTerminal(TypedDict):
|
|
176
|
+
status: Literal["completed", "failed", "cancelled", "timed_out"]
|
|
177
|
+
exitCode: int | None
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class WorkbenchObservableResultOutput(TypedDict):
|
|
181
|
+
orderedSha256: str
|
|
182
|
+
stdoutSha256: str
|
|
183
|
+
stderrSha256: str
|
|
184
|
+
structuredResultSha256: str
|
|
185
|
+
byteCount: int
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class WorkbenchObservableResultArtifact(TypedDict):
|
|
189
|
+
sha256: str
|
|
190
|
+
sizeBytes: int
|
|
191
|
+
contentType: str | None
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class WorkbenchObservableResultV1(TypedDict):
|
|
195
|
+
contractVersion: Literal["workbench.observable-result.v1"]
|
|
196
|
+
terminal: WorkbenchObservableResultTerminal
|
|
197
|
+
output: WorkbenchObservableResultOutput
|
|
198
|
+
artifact: WorkbenchObservableResultArtifact | None
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class WorkbenchReusableRunState(TypedDict, total=False):
|
|
202
|
+
mode: Literal["auto", "off"]
|
|
203
|
+
status: Literal["pending", "ready", "failed", "skipped"]
|
|
204
|
+
seriesId: str | None
|
|
205
|
+
name: str | None
|
|
206
|
+
description: str | None
|
|
207
|
+
versionReason: str | None
|
|
208
|
+
errorCode: str | None
|
|
209
|
+
runDefinitionId: str | None
|
|
210
|
+
version: int | None
|
|
211
|
+
replayOfRunDefinitionId: str | None
|
|
212
|
+
observableResultContractVersion: str | None
|
|
213
|
+
observableResult: WorkbenchObservableResultV1 | None
|
|
214
|
+
observableResultSha256: str | None
|
|
215
|
+
comparisonStatus: Literal["matched", "mismatched"] | None
|
|
216
|
+
comparison: "WorkbenchRunComparison | None"
|
|
217
|
+
comparedAt: str | None
|
|
218
|
+
nextAction: Literal["get_execution", "get_reusable_run"] | None
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class WorkbenchRuntimeDiagnostics(TypedDict, total=False):
|
|
222
|
+
code: str
|
|
223
|
+
message: str
|
|
224
|
+
runtimeStatus: str
|
|
225
|
+
exitCode: int
|
|
226
|
+
error: Any
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class WorkbenchRuntimeResult(TypedDict, total=False):
|
|
230
|
+
status: str
|
|
231
|
+
success: bool
|
|
232
|
+
exitCode: int | None
|
|
233
|
+
terminalReason: str
|
|
234
|
+
stdout: str
|
|
235
|
+
stderr: str
|
|
236
|
+
outputBytes: int
|
|
237
|
+
outputTruncated: bool
|
|
238
|
+
diagnostics: WorkbenchRuntimeDiagnostics
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class WorkbenchRuntimeIdentity(TypedDict, total=False):
|
|
242
|
+
provider: str
|
|
243
|
+
sessionId: str
|
|
244
|
+
workspaceKey: str
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class WorkbenchRuntimeEnvelope(TypedDict):
|
|
248
|
+
runtime: WorkbenchRuntimeIdentity
|
|
249
|
+
result: WorkbenchRuntimeResult
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class WorkbenchBackgroundExecutionResult(TypedDict):
|
|
253
|
+
executionKind: WorkbenchExecutionKind
|
|
254
|
+
executionId: str
|
|
255
|
+
status: Literal["queued", "running"]
|
|
256
|
+
runtimeProcessId: str | None
|
|
257
|
+
outputMode: Literal["events"]
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class WorkbenchOutputPreview(TypedDict):
|
|
261
|
+
outputMode: Literal["preview"]
|
|
262
|
+
outputBytes: int
|
|
263
|
+
preview: str
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class WorkbenchFile(TypedDict, total=False):
|
|
267
|
+
id: str
|
|
268
|
+
path: str
|
|
269
|
+
contentType: str
|
|
270
|
+
sizeBytes: int | str
|
|
271
|
+
storageKind: str
|
|
272
|
+
sha256: str | None
|
|
273
|
+
etag: str | None
|
|
274
|
+
contentBase64: str
|
|
275
|
+
contentEncoding: Literal["base64"]
|
|
276
|
+
createdAt: str
|
|
277
|
+
updatedAt: str
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class WorkbenchFileResponse(TypedDict):
|
|
281
|
+
file: WorkbenchFile
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class WorkbenchFileListResponse(TypedDict, total=False):
|
|
285
|
+
files: list[WorkbenchFile]
|
|
286
|
+
runtimeFiles: list[WorkbenchFile]
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
class WorkbenchFileMutationResponse(TypedDict, total=False):
|
|
290
|
+
file: WorkbenchFile
|
|
291
|
+
uploadId: str
|
|
292
|
+
finalized: bool
|
|
293
|
+
deleted: bool
|
|
294
|
+
path: str
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class WorkbenchFileSignedUpload(TypedDict):
|
|
298
|
+
uploadId: str
|
|
299
|
+
path: str
|
|
300
|
+
contentType: str
|
|
301
|
+
sizeBytes: int
|
|
302
|
+
storageKind: Literal["r2_workspace"]
|
|
303
|
+
uploadUrl: str
|
|
304
|
+
expiresAt: str
|
|
305
|
+
requiredHeaders: dict[str, str]
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class WorkbenchFileUploadPrepareResponse(TypedDict):
|
|
309
|
+
upload: WorkbenchFileSignedUpload
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class WorkbenchFileSignedDownload(TypedDict):
|
|
313
|
+
file: WorkbenchFile
|
|
314
|
+
downloadUrl: str
|
|
315
|
+
expiresAt: str
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
class WorkbenchArtifact(TypedDict, total=False):
|
|
319
|
+
id: str
|
|
320
|
+
artifactRef: str
|
|
321
|
+
path: str
|
|
322
|
+
label: str | None
|
|
323
|
+
contentType: str | None
|
|
324
|
+
sizeBytes: int | str | None
|
|
325
|
+
sha256: str | None
|
|
326
|
+
storageKind: str
|
|
327
|
+
status: str
|
|
328
|
+
downloadUrl: str
|
|
329
|
+
createdAt: str
|
|
330
|
+
updatedAt: str
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
class WorkbenchArtifactResponse(TypedDict, total=False):
|
|
334
|
+
workbenchSession: WorkbenchSession
|
|
335
|
+
artifact: WorkbenchArtifact
|
|
336
|
+
deleted: bool
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
class WorkbenchArtifactListResponse(TypedDict, total=False):
|
|
340
|
+
workbenchSession: WorkbenchSession
|
|
341
|
+
artifacts: list[WorkbenchArtifact]
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
class WorkbenchArtifactDownloadResponse(TypedDict, total=False):
|
|
345
|
+
artifact: WorkbenchArtifact
|
|
346
|
+
downloadUrl: str
|
|
347
|
+
expiresAt: str
|
|
348
|
+
contentType: str
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
class WorkbenchRunComparison(TypedDict, total=False):
|
|
352
|
+
contractVersion: Literal["workbench.observable-result.v1"]
|
|
353
|
+
status: Literal["pending", "pending_finalization", "matched", "mismatched"]
|
|
354
|
+
sourceExecutionId: str
|
|
355
|
+
replayExecutionId: str
|
|
356
|
+
terminalMatch: bool
|
|
357
|
+
outputMatch: bool
|
|
358
|
+
artifactMatch: bool
|
|
359
|
+
sourceObservableResultSha256: str
|
|
360
|
+
replayObservableResultSha256: str
|
|
361
|
+
nextAction: Literal["get_reusable_run"]
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
class WorkbenchRunReplay(TypedDict):
|
|
365
|
+
executionId: str
|
|
366
|
+
status: WorkbenchExecutionStatus
|
|
367
|
+
comparison: WorkbenchRunComparison
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
class WorkbenchRunDefinition(TypedDict, total=False):
|
|
371
|
+
id: str
|
|
372
|
+
seriesId: str
|
|
373
|
+
version: int
|
|
374
|
+
workbenchSessionId: str
|
|
375
|
+
platformSessionId: str
|
|
376
|
+
sourceExecutionId: str
|
|
377
|
+
environmentSnapshotId: str | None
|
|
378
|
+
name: str
|
|
379
|
+
description: str | None
|
|
380
|
+
kind: Literal["code", "bash"]
|
|
381
|
+
language: WorkbenchLanguage | None
|
|
382
|
+
source: str
|
|
383
|
+
publicationStatus: Literal["draft", "published", "retired"]
|
|
384
|
+
validationStatus: Literal["unvalidated", "matched", "mismatched"]
|
|
385
|
+
validatedAt: str | None
|
|
386
|
+
validatedByExecutionId: str | None
|
|
387
|
+
sourceObservableResult: dict[str, Any] | None
|
|
388
|
+
sourceObservableResultSha256: str | None
|
|
389
|
+
sourceCodeSha256: str | None
|
|
390
|
+
environmentManifestSha256: str | None
|
|
391
|
+
creationMode: Literal["auto", "migrated_manual"]
|
|
392
|
+
versionReason: str | None
|
|
393
|
+
environmentManifest: dict[str, Any]
|
|
394
|
+
metadata: dict[str, Any]
|
|
395
|
+
replayCount: int
|
|
396
|
+
replays: list[WorkbenchRunReplay]
|
|
397
|
+
publishedAt: str | None
|
|
398
|
+
retiredAt: str | None
|
|
399
|
+
createdAt: str
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
class WorkbenchRunDefinitionResponse(TypedDict):
|
|
403
|
+
runDefinition: WorkbenchRunDefinition
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
class WorkbenchRunDefinitionListResponse(TypedDict):
|
|
407
|
+
runDefinitions: list[WorkbenchRunDefinition]
|
|
408
|
+
limit: int
|
|
409
|
+
hasMore: bool
|
|
410
|
+
nextCursor: str | None
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
class WorkbenchRunComparisonResponse(TypedDict):
|
|
414
|
+
runDefinition: WorkbenchRunDefinition
|
|
415
|
+
execution: "WorkbenchExecutionResponse"
|
|
416
|
+
environment: "WorkbenchRunEnvironmentResolution"
|
|
417
|
+
comparison: WorkbenchRunComparison
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
class WorkbenchRunEnvironmentResolution(TypedDict):
|
|
421
|
+
mode: WorkbenchReplayEnvironmentMode
|
|
422
|
+
status: Literal["queued", "completed", "failed"]
|
|
423
|
+
matchedBeforeReplay: bool | None
|
|
424
|
+
restored: bool
|
|
425
|
+
snapshotId: str | None
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
class WorkbenchRunActivationResponse(TypedDict, total=False):
|
|
429
|
+
action: Literal["publish", "rollback"]
|
|
430
|
+
runDefinition: WorkbenchRunDefinition
|
|
431
|
+
previousPublishedRunDefinitionId: str | None
|
|
432
|
+
previousPublishedVersion: int | None
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
class WorkbenchExecutionEvent(TypedDict, total=False):
|
|
436
|
+
id: str
|
|
437
|
+
sequence: str | int
|
|
438
|
+
executionKind: WorkbenchExecutionKind
|
|
439
|
+
executionId: str
|
|
440
|
+
eventType: str
|
|
441
|
+
status: str | None
|
|
442
|
+
stream: Literal["stdout", "stderr"] | None
|
|
443
|
+
message: str | None
|
|
444
|
+
data: Any
|
|
445
|
+
metadata: dict[str, Any]
|
|
446
|
+
occurredAt: str
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
class WorkbenchExecutionEventsPage(TypedDict, total=False):
|
|
450
|
+
events: list[WorkbenchExecutionEvent]
|
|
451
|
+
nextCursor: str | None
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
class WorkbenchExecutionStateResponse(TypedDict, total=False):
|
|
455
|
+
execution: WorkbenchExecutionLog
|
|
456
|
+
events: list[WorkbenchExecutionEvent]
|
|
457
|
+
cancelled: bool
|
|
458
|
+
remoteCancellation: dict[str, Any]
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
class WorkbenchExecutionResponse(TypedDict, total=False):
|
|
462
|
+
workbenchSession: WorkbenchSession
|
|
463
|
+
executionKind: WorkbenchExecutionKind
|
|
464
|
+
executionId: str
|
|
465
|
+
status: WorkbenchExecutionStatus
|
|
466
|
+
outputMode: WorkbenchOutputMode
|
|
467
|
+
execution: WorkbenchExecutionLog
|
|
468
|
+
executionLog: WorkbenchExecutionLog
|
|
469
|
+
result: WorkbenchRuntimeEnvelope | WorkbenchBackgroundExecutionResult | WorkbenchOutputPreview
|
|
470
|
+
artifact: WorkbenchArtifact
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
class WorkbenchRun:
|
|
474
|
+
def __init__(self, client, platform_session_id: str, response: WorkbenchExecutionResponse):
|
|
475
|
+
self.client = client
|
|
476
|
+
self.platform_session_id = platform_session_id
|
|
477
|
+
self.response = response
|
|
478
|
+
self.execution_id = str(response["executionId"])
|
|
479
|
+
self.execution_kind = str(response["executionKind"])
|
|
480
|
+
|
|
481
|
+
def events(self, *, poll_interval_seconds: float = 0.5, limit: int = 200) -> Iterator[WorkbenchExecutionEvent]:
|
|
482
|
+
after_sequence: str | None = None
|
|
483
|
+
while True:
|
|
484
|
+
page = cast(WorkbenchExecutionEventsPage, self.client.executions.events(
|
|
485
|
+
self.platform_session_id,
|
|
486
|
+
self.execution_kind,
|
|
487
|
+
self.execution_id,
|
|
488
|
+
afterSequence=after_sequence,
|
|
489
|
+
limit=limit,
|
|
490
|
+
))
|
|
491
|
+
for event in page.get("events", []):
|
|
492
|
+
yield event
|
|
493
|
+
sequence = event.get("sequence")
|
|
494
|
+
if isinstance(sequence, (str, int)):
|
|
495
|
+
after_sequence = str(sequence)
|
|
496
|
+
if page.get("hasMore") is True:
|
|
497
|
+
next_cursor = page.get("nextCursor")
|
|
498
|
+
if not isinstance(next_cursor, str) or not next_cursor:
|
|
499
|
+
raise StacklinkError("Execution event page reported hasMore without nextCursor.")
|
|
500
|
+
after_sequence = next_cursor
|
|
501
|
+
continue
|
|
502
|
+
current = cast(WorkbenchExecutionStateResponse, self.client.executions.get(
|
|
503
|
+
self.platform_session_id,
|
|
504
|
+
self.execution_kind,
|
|
505
|
+
self.execution_id,
|
|
506
|
+
))
|
|
507
|
+
execution = current.get("execution", {})
|
|
508
|
+
if isinstance(execution, dict) and execution.get("status") in {
|
|
509
|
+
"completed",
|
|
510
|
+
"failed",
|
|
511
|
+
"cancelled",
|
|
512
|
+
"timed_out",
|
|
513
|
+
}:
|
|
514
|
+
return
|
|
515
|
+
time.sleep(poll_interval_seconds)
|
|
516
|
+
|
|
517
|
+
def wait(self, *, wait_timeout_ms: int | None = None, poll_interval_ms: int | None = None) -> WorkbenchExecutionStateResponse:
|
|
518
|
+
body = {
|
|
519
|
+
key: value
|
|
520
|
+
for key, value in {
|
|
521
|
+
"waitTimeoutMs": wait_timeout_ms,
|
|
522
|
+
"pollIntervalMs": poll_interval_ms,
|
|
523
|
+
}.items()
|
|
524
|
+
if value is not None
|
|
525
|
+
}
|
|
526
|
+
return cast(WorkbenchExecutionStateResponse, self.client.executions.wait(
|
|
527
|
+
self.platform_session_id,
|
|
528
|
+
self.execution_kind,
|
|
529
|
+
self.execution_id,
|
|
530
|
+
body,
|
|
531
|
+
))
|
|
532
|
+
|
|
533
|
+
def cancel(self, reason: str | None = None) -> WorkbenchExecutionStateResponse:
|
|
534
|
+
return cast(WorkbenchExecutionStateResponse, self.client.executions.cancel(
|
|
535
|
+
self.platform_session_id,
|
|
536
|
+
self.execution_kind,
|
|
537
|
+
self.execution_id,
|
|
538
|
+
reason,
|
|
539
|
+
))
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def _wait_for_workbench_response(
|
|
543
|
+
client,
|
|
544
|
+
request: Mapping[str, Any],
|
|
545
|
+
accepted: WorkbenchExecutionResponse,
|
|
546
|
+
) -> WorkbenchExecutionResponse:
|
|
547
|
+
session_id = request.get("sessionId")
|
|
548
|
+
execution_id = accepted.get("executionId")
|
|
549
|
+
execution_kind = accepted.get("executionKind")
|
|
550
|
+
if (
|
|
551
|
+
not isinstance(session_id, str)
|
|
552
|
+
or not isinstance(execution_id, str)
|
|
553
|
+
or execution_kind not in {"workbench_code", "workbench_bash"}
|
|
554
|
+
):
|
|
555
|
+
return accepted
|
|
556
|
+
|
|
557
|
+
while True:
|
|
558
|
+
waited = cast(WorkbenchExecutionStateResponse, client.executions.wait(
|
|
559
|
+
session_id,
|
|
560
|
+
execution_kind,
|
|
561
|
+
execution_id,
|
|
562
|
+
{"waitTimeoutMs": 10_000, "pollIntervalMs": 500},
|
|
563
|
+
))
|
|
564
|
+
execution = waited.get("execution")
|
|
565
|
+
if not isinstance(execution, dict) or execution.get("status") not in {
|
|
566
|
+
"completed",
|
|
567
|
+
"failed",
|
|
568
|
+
"cancelled",
|
|
569
|
+
"timed_out",
|
|
570
|
+
}:
|
|
571
|
+
continue
|
|
572
|
+
response: WorkbenchExecutionResponse = {
|
|
573
|
+
**accepted,
|
|
574
|
+
"executionId": str(execution.get("executionId") or execution.get("id") or execution_id),
|
|
575
|
+
"executionKind": cast(WorkbenchExecutionKind, execution.get("executionKind") or execution_kind),
|
|
576
|
+
"status": cast(WorkbenchExecutionStatus, execution["status"]),
|
|
577
|
+
"execution": cast(WorkbenchExecutionLog, execution),
|
|
578
|
+
"executionLog": cast(WorkbenchExecutionLog, execution),
|
|
579
|
+
}
|
|
580
|
+
output_mode = execution.get("outputMode")
|
|
581
|
+
if output_mode in {"inline", "preview", "events", "artifact"}:
|
|
582
|
+
response["outputMode"] = cast(WorkbenchOutputMode, output_mode)
|
|
583
|
+
if "result" in execution:
|
|
584
|
+
response["result"] = cast(
|
|
585
|
+
WorkbenchRuntimeEnvelope | WorkbenchBackgroundExecutionResult | WorkbenchOutputPreview,
|
|
586
|
+
execution["result"],
|
|
587
|
+
)
|
|
588
|
+
artifact = execution.get("artifact")
|
|
589
|
+
if isinstance(artifact, dict):
|
|
590
|
+
response["artifact"] = cast(WorkbenchArtifact, artifact)
|
|
591
|
+
return response
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
class WorkbenchFiles:
|
|
595
|
+
def __init__(self, sandbox: "SessionSandbox"):
|
|
596
|
+
self.sandbox = sandbox
|
|
597
|
+
|
|
598
|
+
def list(self) -> WorkbenchFileListResponse:
|
|
599
|
+
return cast(WorkbenchFileListResponse, self.sandbox.client.request(
|
|
600
|
+
"GET",
|
|
601
|
+
f"/v1/workbench/sessions/{self.sandbox.ensure_session_id()}/files",
|
|
602
|
+
query={"sessionId": self.sandbox.platform_session_id},
|
|
603
|
+
))
|
|
604
|
+
|
|
605
|
+
def read(self, path: str) -> WorkbenchFileResponse:
|
|
606
|
+
return cast(WorkbenchFileResponse, self.sandbox.client.request(
|
|
607
|
+
"GET",
|
|
608
|
+
f"/v1/workbench/sessions/{self.sandbox.ensure_session_id()}/files",
|
|
609
|
+
query={"sessionId": self.sandbox.platform_session_id, "path": path},
|
|
610
|
+
))
|
|
611
|
+
|
|
612
|
+
def upload(
|
|
613
|
+
self,
|
|
614
|
+
path: str,
|
|
615
|
+
content: str | bytes,
|
|
616
|
+
*,
|
|
617
|
+
content_type: str | None = None,
|
|
618
|
+
metadata: Mapping[str, Any] | None = None,
|
|
619
|
+
) -> WorkbenchFileResponse:
|
|
620
|
+
raw = content.encode("utf-8") if isinstance(content, str) else content
|
|
621
|
+
body = {
|
|
622
|
+
"sessionId": self.sandbox.platform_session_id,
|
|
623
|
+
"path": path,
|
|
624
|
+
"contentBase64": base64.b64encode(raw).decode("ascii"),
|
|
625
|
+
"metadata": dict(metadata or {}),
|
|
626
|
+
}
|
|
627
|
+
if content_type is not None:
|
|
628
|
+
body["contentType"] = content_type
|
|
629
|
+
return cast(WorkbenchFileResponse, self.sandbox.client.request(
|
|
630
|
+
"PUT",
|
|
631
|
+
f"/v1/workbench/sessions/{self.sandbox.ensure_session_id()}/files",
|
|
632
|
+
json_body=body,
|
|
633
|
+
))
|
|
634
|
+
|
|
635
|
+
def prepare_upload(self, path: str, *, content_type: str, size_bytes: int) -> WorkbenchFileUploadPrepareResponse:
|
|
636
|
+
return cast(WorkbenchFileUploadPrepareResponse, self.sandbox.client.request(
|
|
637
|
+
"POST",
|
|
638
|
+
f"/v1/workbench/sessions/{self.sandbox.ensure_session_id()}/files/upload-url",
|
|
639
|
+
json_body={
|
|
640
|
+
"sessionId": self.sandbox.platform_session_id,
|
|
641
|
+
"path": path,
|
|
642
|
+
"contentType": content_type,
|
|
643
|
+
"sizeBytes": size_bytes,
|
|
644
|
+
},
|
|
645
|
+
))
|
|
646
|
+
|
|
647
|
+
def finalize_upload(
|
|
648
|
+
self,
|
|
649
|
+
upload_id: str,
|
|
650
|
+
*,
|
|
651
|
+
metadata: Mapping[str, Any] | None = None,
|
|
652
|
+
) -> WorkbenchFileMutationResponse:
|
|
653
|
+
return cast(WorkbenchFileMutationResponse, self.sandbox.client.request(
|
|
654
|
+
"POST",
|
|
655
|
+
f"/v1/workbench/sessions/{self.sandbox.ensure_session_id()}/files/finalize-upload",
|
|
656
|
+
json_body={
|
|
657
|
+
"sessionId": self.sandbox.platform_session_id,
|
|
658
|
+
"uploadId": upload_id,
|
|
659
|
+
"metadata": dict(metadata or {}),
|
|
660
|
+
},
|
|
661
|
+
))
|
|
662
|
+
|
|
663
|
+
def upload_large(
|
|
664
|
+
self,
|
|
665
|
+
path: str,
|
|
666
|
+
content: str | bytes,
|
|
667
|
+
*,
|
|
668
|
+
content_type: str | None = None,
|
|
669
|
+
metadata: Mapping[str, Any] | None = None,
|
|
670
|
+
) -> WorkbenchFileMutationResponse:
|
|
671
|
+
raw = content.encode("utf-8") if isinstance(content, str) else content
|
|
672
|
+
resolved_content_type = content_type or (
|
|
673
|
+
"text/plain; charset=utf-8" if isinstance(content, str) else "application/octet-stream"
|
|
674
|
+
)
|
|
675
|
+
prepared = self.prepare_upload(path, content_type=resolved_content_type, size_bytes=len(raw))
|
|
676
|
+
upload = prepared.get("upload") if isinstance(prepared, dict) else None
|
|
677
|
+
if (
|
|
678
|
+
not isinstance(upload, dict)
|
|
679
|
+
or not isinstance(upload.get("uploadUrl"), str)
|
|
680
|
+
or not isinstance(upload.get("uploadId"), str)
|
|
681
|
+
):
|
|
682
|
+
raise StacklinkError("Stacklink did not return a signed Workbench upload URL.")
|
|
683
|
+
headers = upload.get("requiredHeaders") if isinstance(upload.get("requiredHeaders"), dict) else {}
|
|
684
|
+
request = Request(upload["uploadUrl"], data=raw, method="PUT", headers={str(key): str(value) for key, value in headers.items()})
|
|
685
|
+
try:
|
|
686
|
+
with urlopen(request, timeout=self.sandbox.client.timeout) as response:
|
|
687
|
+
if response.status >= 400:
|
|
688
|
+
raise StacklinkError(f"Workbench file upload failed with {response.status}.")
|
|
689
|
+
except HTTPError as error:
|
|
690
|
+
raise StacklinkError(f"Workbench file upload failed with {error.code}.") from error
|
|
691
|
+
return self.finalize_upload(
|
|
692
|
+
upload["uploadId"],
|
|
693
|
+
metadata=metadata,
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
def download(self, path: str) -> bytes:
|
|
697
|
+
signed = self.sandbox.client.request(
|
|
698
|
+
"GET",
|
|
699
|
+
f"/v1/workbench/sessions/{self.sandbox.ensure_session_id()}/files/download-url",
|
|
700
|
+
query={"sessionId": self.sandbox.platform_session_id, "path": path},
|
|
701
|
+
)
|
|
702
|
+
download_url = signed.get("downloadUrl") if isinstance(signed, dict) else None
|
|
703
|
+
if not isinstance(download_url, str):
|
|
704
|
+
raise StacklinkError("Stacklink did not return a signed Workbench download URL.")
|
|
705
|
+
try:
|
|
706
|
+
with urlopen(Request(download_url, method="GET"), timeout=self.sandbox.client.timeout) as response:
|
|
707
|
+
return response.read()
|
|
708
|
+
except HTTPError as error:
|
|
709
|
+
raise StacklinkError(f"Workbench file download failed with {error.code}.") from error
|
|
710
|
+
|
|
711
|
+
def delete(self, path: str) -> WorkbenchFileMutationResponse:
|
|
712
|
+
return cast(WorkbenchFileMutationResponse, self.sandbox.client.request(
|
|
713
|
+
"DELETE",
|
|
714
|
+
f"/v1/workbench/sessions/{self.sandbox.ensure_session_id()}/files",
|
|
715
|
+
query={"sessionId": self.sandbox.platform_session_id, "path": path},
|
|
716
|
+
))
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
class WorkbenchRuns:
|
|
720
|
+
def __init__(self, sandbox: "SessionSandbox"):
|
|
721
|
+
self.sandbox = sandbox
|
|
722
|
+
|
|
723
|
+
def list(
|
|
724
|
+
self,
|
|
725
|
+
*,
|
|
726
|
+
series_id: str | None = None,
|
|
727
|
+
cursor: str | None = None,
|
|
728
|
+
limit: int | None = None,
|
|
729
|
+
) -> WorkbenchRunDefinitionListResponse:
|
|
730
|
+
query = {
|
|
731
|
+
"sessionId": self.sandbox.platform_session_id,
|
|
732
|
+
"workbenchSessionId": self.sandbox.ensure_session_id(),
|
|
733
|
+
}
|
|
734
|
+
if series_id is not None:
|
|
735
|
+
query["seriesId"] = series_id
|
|
736
|
+
if cursor is not None:
|
|
737
|
+
query["cursor"] = cursor
|
|
738
|
+
if limit is not None:
|
|
739
|
+
query["limit"] = limit
|
|
740
|
+
return cast(
|
|
741
|
+
WorkbenchRunDefinitionListResponse,
|
|
742
|
+
self.sandbox.client.request("GET", "/v1/workbench/run-definitions", query=query),
|
|
743
|
+
)
|
|
744
|
+
|
|
745
|
+
def get(self, run_definition_id: str) -> WorkbenchRunDefinitionResponse:
|
|
746
|
+
return cast(
|
|
747
|
+
WorkbenchRunDefinitionResponse,
|
|
748
|
+
self.sandbox.client.request(
|
|
749
|
+
"GET",
|
|
750
|
+
f"/v1/workbench/run-definitions/{run_definition_id}",
|
|
751
|
+
query={"sessionId": self.sandbox.platform_session_id},
|
|
752
|
+
),
|
|
753
|
+
)
|
|
754
|
+
|
|
755
|
+
def update(
|
|
756
|
+
self,
|
|
757
|
+
run_definition_id: str,
|
|
758
|
+
*,
|
|
759
|
+
name: str | _WorkbenchUnset = _WORKBENCH_UNSET,
|
|
760
|
+
description: str | None | _WorkbenchUnset = _WORKBENCH_UNSET,
|
|
761
|
+
version_reason: str | None | _WorkbenchUnset = _WORKBENCH_UNSET,
|
|
762
|
+
) -> WorkbenchRunDefinitionResponse:
|
|
763
|
+
body: dict[str, Any] = {"sessionId": self.sandbox.platform_session_id}
|
|
764
|
+
if not isinstance(name, _WorkbenchUnset):
|
|
765
|
+
body["name"] = name
|
|
766
|
+
if not isinstance(description, _WorkbenchUnset):
|
|
767
|
+
body["description"] = description
|
|
768
|
+
if not isinstance(version_reason, _WorkbenchUnset):
|
|
769
|
+
body["versionReason"] = version_reason
|
|
770
|
+
return cast(
|
|
771
|
+
WorkbenchRunDefinitionResponse,
|
|
772
|
+
self.sandbox.client.request(
|
|
773
|
+
"PATCH",
|
|
774
|
+
f"/v1/workbench/run-definitions/{run_definition_id}",
|
|
775
|
+
json_body=body,
|
|
776
|
+
),
|
|
777
|
+
)
|
|
778
|
+
|
|
779
|
+
def save(
|
|
780
|
+
self,
|
|
781
|
+
execution_id: str,
|
|
782
|
+
name: str,
|
|
783
|
+
*,
|
|
784
|
+
series_id: str | None = None,
|
|
785
|
+
description: str | None = None,
|
|
786
|
+
metadata: Mapping[str, Any] | None = None,
|
|
787
|
+
) -> WorkbenchRunDefinitionResponse:
|
|
788
|
+
body = {
|
|
789
|
+
"sessionId": self.sandbox.platform_session_id,
|
|
790
|
+
"workbenchSessionId": self.sandbox.ensure_session_id(),
|
|
791
|
+
"executionId": execution_id,
|
|
792
|
+
"name": name,
|
|
793
|
+
"metadata": dict(metadata or {}),
|
|
794
|
+
}
|
|
795
|
+
if series_id is not None:
|
|
796
|
+
body["seriesId"] = series_id
|
|
797
|
+
if description is not None:
|
|
798
|
+
body["description"] = description
|
|
799
|
+
return cast(
|
|
800
|
+
WorkbenchRunDefinitionResponse,
|
|
801
|
+
self.sandbox.client.request("POST", "/v1/workbench/run-definitions", json_body=body),
|
|
802
|
+
)
|
|
803
|
+
|
|
804
|
+
def replay(
|
|
805
|
+
self,
|
|
806
|
+
run_definition_id: str,
|
|
807
|
+
*,
|
|
808
|
+
background: bool = False,
|
|
809
|
+
timeout_ms: int | None = None,
|
|
810
|
+
environment_mode: WorkbenchReplayEnvironmentMode = "require_match",
|
|
811
|
+
idempotency_key: str,
|
|
812
|
+
) -> WorkbenchRunComparisonResponse:
|
|
813
|
+
body = {
|
|
814
|
+
"sessionId": self.sandbox.platform_session_id,
|
|
815
|
+
"background": background,
|
|
816
|
+
"environmentMode": environment_mode,
|
|
817
|
+
"idempotencyKey": idempotency_key,
|
|
818
|
+
}
|
|
819
|
+
if timeout_ms is not None:
|
|
820
|
+
body["timeoutMs"] = timeout_ms
|
|
821
|
+
accepted = cast(WorkbenchRunComparisonResponse, self.sandbox.client.request(
|
|
822
|
+
"POST",
|
|
823
|
+
f"/v1/workbench/run-definitions/{run_definition_id}/replay",
|
|
824
|
+
json_body=body,
|
|
825
|
+
))
|
|
826
|
+
if background:
|
|
827
|
+
return accepted
|
|
828
|
+
completed = _wait_for_workbench_response(
|
|
829
|
+
self.sandbox.client,
|
|
830
|
+
body,
|
|
831
|
+
accepted["execution"],
|
|
832
|
+
)
|
|
833
|
+
definition = accepted["runDefinition"]
|
|
834
|
+
execution_id = str(completed.get("executionId") or accepted["comparison"].get("replayExecutionId"))
|
|
835
|
+
comparison = accepted["comparison"]
|
|
836
|
+
comparison_deadline = time.monotonic() + _REUSABLE_RUN_COMPARISON_WAIT_SECONDS
|
|
837
|
+
while True:
|
|
838
|
+
current = self.get(run_definition_id).get("runDefinition", {})
|
|
839
|
+
replay = next(
|
|
840
|
+
(
|
|
841
|
+
entry
|
|
842
|
+
for entry in current.get("replays", [])
|
|
843
|
+
if entry.get("executionId") == execution_id
|
|
844
|
+
),
|
|
845
|
+
None,
|
|
846
|
+
)
|
|
847
|
+
if isinstance(replay, dict) and isinstance(replay.get("comparison"), dict):
|
|
848
|
+
comparison = cast(WorkbenchRunComparison, replay["comparison"])
|
|
849
|
+
if comparison.get("status") in {"matched", "mismatched"}:
|
|
850
|
+
break
|
|
851
|
+
if time.monotonic() >= comparison_deadline:
|
|
852
|
+
break
|
|
853
|
+
time.sleep(_REUSABLE_RUN_COMPARISON_POLL_SECONDS)
|
|
854
|
+
execution_log = completed.get("executionLog")
|
|
855
|
+
metadata = execution_log.get("metadata") if isinstance(execution_log, dict) else None
|
|
856
|
+
environment = metadata.get("replayEnvironment") if isinstance(metadata, dict) else None
|
|
857
|
+
return {
|
|
858
|
+
**accepted,
|
|
859
|
+
"execution": completed,
|
|
860
|
+
"environment": cast(
|
|
861
|
+
WorkbenchRunEnvironmentResolution,
|
|
862
|
+
environment if isinstance(environment, dict) else accepted["environment"],
|
|
863
|
+
),
|
|
864
|
+
"comparison": cast(
|
|
865
|
+
WorkbenchRunComparison,
|
|
866
|
+
comparison,
|
|
867
|
+
),
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
def publish(
|
|
871
|
+
self,
|
|
872
|
+
run_definition_id: str,
|
|
873
|
+
expected_published_version: int | None,
|
|
874
|
+
) -> WorkbenchRunActivationResponse:
|
|
875
|
+
return cast(WorkbenchRunActivationResponse, self.sandbox.client.request(
|
|
876
|
+
"POST",
|
|
877
|
+
f"/v1/workbench/run-definitions/{run_definition_id}/publish",
|
|
878
|
+
json_body={
|
|
879
|
+
"sessionId": self.sandbox.platform_session_id,
|
|
880
|
+
"expectedPublishedVersion": expected_published_version,
|
|
881
|
+
},
|
|
882
|
+
))
|
|
883
|
+
|
|
884
|
+
def rollback(
|
|
885
|
+
self,
|
|
886
|
+
run_definition_id: str,
|
|
887
|
+
expected_published_version: int | None,
|
|
888
|
+
) -> WorkbenchRunActivationResponse:
|
|
889
|
+
return cast(WorkbenchRunActivationResponse, self.sandbox.client.request(
|
|
890
|
+
"POST",
|
|
891
|
+
f"/v1/workbench/run-definitions/{run_definition_id}/rollback",
|
|
892
|
+
json_body={
|
|
893
|
+
"sessionId": self.sandbox.platform_session_id,
|
|
894
|
+
"expectedPublishedVersion": expected_published_version,
|
|
895
|
+
},
|
|
896
|
+
))
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
class WorkbenchArtifacts:
|
|
900
|
+
def __init__(self, sandbox: "SessionSandbox"):
|
|
901
|
+
self.sandbox = sandbox
|
|
902
|
+
|
|
903
|
+
def list(self) -> WorkbenchArtifactListResponse:
|
|
904
|
+
return cast(WorkbenchArtifactListResponse, self.sandbox.client.request(
|
|
905
|
+
"GET",
|
|
906
|
+
"/v1/workbench/artifacts",
|
|
907
|
+
query={"workbenchSessionId": self.sandbox.ensure_session_id()},
|
|
908
|
+
))
|
|
909
|
+
|
|
910
|
+
def promote(
|
|
911
|
+
self,
|
|
912
|
+
path: str,
|
|
913
|
+
*,
|
|
914
|
+
label: str | None = None,
|
|
915
|
+
content_type: str | None = None,
|
|
916
|
+
metadata: Mapping[str, Any] | None = None,
|
|
917
|
+
) -> WorkbenchArtifactResponse:
|
|
918
|
+
body: dict[str, Any] = {
|
|
919
|
+
"sessionId": self.sandbox.platform_session_id,
|
|
920
|
+
"workbenchSessionId": self.sandbox.ensure_session_id(),
|
|
921
|
+
"path": path,
|
|
922
|
+
"metadata": dict(metadata or {}),
|
|
923
|
+
}
|
|
924
|
+
if label is not None:
|
|
925
|
+
body["label"] = label
|
|
926
|
+
if content_type is not None:
|
|
927
|
+
body["contentType"] = content_type
|
|
928
|
+
return cast(WorkbenchArtifactResponse, self.sandbox.client.request("POST", "/v1/workbench/artifacts", json_body=body))
|
|
929
|
+
|
|
930
|
+
def download_url(self, artifact_id: str) -> WorkbenchArtifactDownloadResponse:
|
|
931
|
+
return cast(WorkbenchArtifactDownloadResponse, self.sandbox.client.workbench.artifact_download_url(artifact_id))
|
|
932
|
+
|
|
933
|
+
def download(self, artifact_id: str) -> bytes:
|
|
934
|
+
return self.sandbox.client.workbench.download_artifact(artifact_id)
|
|
935
|
+
|
|
936
|
+
def delete(self, artifact_id: str, *, path: str | None = None) -> WorkbenchArtifactResponse:
|
|
937
|
+
return cast(WorkbenchArtifactResponse, self.sandbox.client.request(
|
|
938
|
+
"DELETE",
|
|
939
|
+
f"/v1/workbench/artifacts/{artifact_id}",
|
|
940
|
+
query={
|
|
941
|
+
"sessionId": self.sandbox.platform_session_id,
|
|
942
|
+
"workbenchSessionId": self.sandbox.ensure_session_id(),
|
|
943
|
+
"path": path,
|
|
944
|
+
},
|
|
945
|
+
))
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
class SessionSandbox:
|
|
949
|
+
def __init__(self, client, platform_session_id: str):
|
|
950
|
+
self.client = client
|
|
951
|
+
self.platform_session_id = platform_session_id
|
|
952
|
+
self.workbench_session_id: str | None = None
|
|
953
|
+
self.files = WorkbenchFiles(self)
|
|
954
|
+
self.runs = WorkbenchRuns(self)
|
|
955
|
+
self.artifacts = WorkbenchArtifacts(self)
|
|
956
|
+
|
|
957
|
+
def use(
|
|
958
|
+
self,
|
|
959
|
+
workbench_session_id: str | None = None,
|
|
960
|
+
*,
|
|
961
|
+
create_new: bool = False,
|
|
962
|
+
metadata: Mapping[str, Any] | None = None,
|
|
963
|
+
) -> WorkbenchSessionUseResponse:
|
|
964
|
+
response = cast(WorkbenchSessionUseResponse, self.client.request(
|
|
965
|
+
"POST",
|
|
966
|
+
"/v1/workbench/sessions",
|
|
967
|
+
json_body={
|
|
968
|
+
"sessionId": self.platform_session_id,
|
|
969
|
+
"workbenchSessionId": workbench_session_id,
|
|
970
|
+
"createNew": create_new,
|
|
971
|
+
"metadata": dict(metadata or {}),
|
|
972
|
+
},
|
|
973
|
+
))
|
|
974
|
+
self.workbench_session_id = str(response["workbenchSession"]["id"])
|
|
975
|
+
return response
|
|
976
|
+
|
|
977
|
+
def ensure_session_id(self) -> str:
|
|
978
|
+
if self.workbench_session_id is None:
|
|
979
|
+
self.use()
|
|
980
|
+
assert self.workbench_session_id is not None
|
|
981
|
+
return self.workbench_session_id
|
|
982
|
+
|
|
983
|
+
def run(self, body: WorkbenchRunInput) -> WorkbenchRun:
|
|
984
|
+
request = {
|
|
985
|
+
**body,
|
|
986
|
+
"sessionId": self.platform_session_id,
|
|
987
|
+
"workbenchSessionId": body.get("workbenchSessionId") or self.ensure_session_id(),
|
|
988
|
+
"operation": "run_code",
|
|
989
|
+
}
|
|
990
|
+
response: WorkbenchExecutionResponse = self.client.request("POST", "/v1/workbench/run", json_body=request)
|
|
991
|
+
if body.get("background") is not True:
|
|
992
|
+
response = _wait_for_workbench_response(self.client, request, response)
|
|
993
|
+
self.workbench_session_id = str(response["workbenchSession"]["id"])
|
|
994
|
+
return WorkbenchRun(self.client, self.platform_session_id, response)
|
|
995
|
+
|
|
996
|
+
def bash(self, body: WorkbenchBashInput) -> WorkbenchRun:
|
|
997
|
+
request = {
|
|
998
|
+
**body,
|
|
999
|
+
"sessionId": self.platform_session_id,
|
|
1000
|
+
"workbenchSessionId": body.get("workbenchSessionId") or self.ensure_session_id(),
|
|
1001
|
+
}
|
|
1002
|
+
response: WorkbenchExecutionResponse = self.client.request("POST", "/v1/workbench/bash", json_body=request)
|
|
1003
|
+
if body.get("background") is not True:
|
|
1004
|
+
response = _wait_for_workbench_response(self.client, request, response)
|
|
1005
|
+
self.workbench_session_id = str(response["workbenchSession"]["id"])
|
|
1006
|
+
return WorkbenchRun(self.client, self.platform_session_id, response)
|
|
1007
|
+
|
|
1008
|
+
def destroy(self) -> WorkbenchSessionDestroyResponse:
|
|
1009
|
+
return cast(WorkbenchSessionDestroyResponse, self.client.workbench.destroy(
|
|
1010
|
+
self.platform_session_id,
|
|
1011
|
+
self.ensure_session_id(),
|
|
1012
|
+
))
|
|
1013
|
+
|
|
1014
|
+
def delete(self) -> WorkbenchSessionDeleteReceipt:
|
|
1015
|
+
result = self.client.workbench.delete_session(
|
|
1016
|
+
self.platform_session_id,
|
|
1017
|
+
self.ensure_session_id(),
|
|
1018
|
+
)
|
|
1019
|
+
self.workbench_session_id = None
|
|
1020
|
+
return result
|
|
1021
|
+
|
|
1022
|
+
|
|
1023
|
+
class WorkbenchResource:
|
|
1024
|
+
def __init__(self, client):
|
|
1025
|
+
self.client = client
|
|
1026
|
+
|
|
1027
|
+
def bind(self, platform_session_id: str) -> SessionSandbox:
|
|
1028
|
+
return SessionSandbox(self.client, platform_session_id)
|
|
1029
|
+
|
|
1030
|
+
def models(self):
|
|
1031
|
+
return self.client.request("GET", "/v1/workbench/models")
|
|
1032
|
+
|
|
1033
|
+
def run(self, body: dict[str, Any]):
|
|
1034
|
+
response = cast(WorkbenchExecutionResponse, self.client.request("POST", "/v1/workbench/run", json_body=body))
|
|
1035
|
+
if body.get("operation") == "run_code" and body.get("background") is not True:
|
|
1036
|
+
return _wait_for_workbench_response(self.client, body, response)
|
|
1037
|
+
return response
|
|
1038
|
+
|
|
1039
|
+
def bash(self, body: dict[str, Any]):
|
|
1040
|
+
response = cast(WorkbenchExecutionResponse, self.client.request("POST", "/v1/workbench/bash", json_body=body))
|
|
1041
|
+
if body.get("background") is not True:
|
|
1042
|
+
return _wait_for_workbench_response(self.client, body, response)
|
|
1043
|
+
return response
|
|
1044
|
+
|
|
1045
|
+
def destroy(self, session_id: str, workbench_session_id: str):
|
|
1046
|
+
return self.run({
|
|
1047
|
+
"sessionId": session_id,
|
|
1048
|
+
"workbenchSessionId": workbench_session_id,
|
|
1049
|
+
"operation": "destroy",
|
|
1050
|
+
})
|
|
1051
|
+
|
|
1052
|
+
def delete_session(self, session_id: str, workbench_session_id: str) -> WorkbenchSessionDeleteReceipt:
|
|
1053
|
+
return cast(WorkbenchSessionDeleteReceipt, self.client.request(
|
|
1054
|
+
"DELETE",
|
|
1055
|
+
f"/v1/workbench/sessions/{workbench_session_id}",
|
|
1056
|
+
query={"sessionId": session_id},
|
|
1057
|
+
))
|
|
1058
|
+
|
|
1059
|
+
def sessions(self, session_id: str):
|
|
1060
|
+
return self.client.request("GET", f"/v1/sessions/{session_id}/workbench")
|
|
1061
|
+
|
|
1062
|
+
def list_sessions(self, **query):
|
|
1063
|
+
return self.client.request("GET", "/v1/workbench/sessions", query=query)
|
|
1064
|
+
|
|
1065
|
+
def get_session(self, workbench_session_id: str):
|
|
1066
|
+
return self.client.request("GET", f"/v1/workbench/sessions/{workbench_session_id}")
|
|
1067
|
+
|
|
1068
|
+
def usage(self, workbench_session_id: str):
|
|
1069
|
+
return self.client.request("GET", f"/v1/workbench/sessions/{workbench_session_id}/usage")
|
|
1070
|
+
|
|
1071
|
+
def list_artifacts(self, **query):
|
|
1072
|
+
return self.client.request("GET", "/v1/workbench/artifacts", query=query)
|
|
1073
|
+
|
|
1074
|
+
def artifact_download_url(self, artifact_id: str):
|
|
1075
|
+
return self.client.request("GET", f"/v1/workbench/artifacts/{artifact_id}/download-url")
|
|
1076
|
+
|
|
1077
|
+
def download_artifact(self, artifact_id: str) -> bytes:
|
|
1078
|
+
return self.client.request_raw("GET", f"/v1/workbench/artifacts/{artifact_id}/download")
|
|
1079
|
+
|
|
1080
|
+
def list_budgets(self, **query):
|
|
1081
|
+
return self.client.request("GET", "/v1/workbench/budgets", query=query)
|
|
1082
|
+
|
|
1083
|
+
def create_budget(self, body: dict[str, Any]):
|
|
1084
|
+
return self.client.request("POST", "/v1/workbench/budgets", json_body=body)
|
|
1085
|
+
|
|
1086
|
+
def update_budget(self, budget_policy_id: str, body: dict[str, Any]):
|
|
1087
|
+
return self.client.request("PATCH", f"/v1/workbench/budgets/{budget_policy_id}", json_body=body)
|
|
1088
|
+
|
|
1089
|
+
def delete_budget(self, budget_policy_id: str):
|
|
1090
|
+
return self.client.request("DELETE", f"/v1/workbench/budgets/{budget_policy_id}")
|