stacklink-sdk 0.3.14__tar.gz → 0.3.19__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.
- {stacklink_sdk-0.3.14/stacklink_sdk.egg-info → stacklink_sdk-0.3.19}/PKG-INFO +1 -1
- {stacklink_sdk-0.3.14 → stacklink_sdk-0.3.19}/pyproject.toml +1 -1
- {stacklink_sdk-0.3.14 → stacklink_sdk-0.3.19}/stacklink/__init__.py +2 -2
- {stacklink_sdk-0.3.14 → stacklink_sdk-0.3.19}/stacklink/billing.py +42 -3
- {stacklink_sdk-0.3.14 → stacklink_sdk-0.3.19}/stacklink/bots.py +10 -3
- {stacklink_sdk-0.3.14 → stacklink_sdk-0.3.19}/stacklink/client.py +1 -1
- {stacklink_sdk-0.3.14 → stacklink_sdk-0.3.19}/stacklink/workbench.py +77 -6
- {stacklink_sdk-0.3.14 → stacklink_sdk-0.3.19/stacklink_sdk.egg-info}/PKG-INFO +1 -1
- {stacklink_sdk-0.3.14 → stacklink_sdk-0.3.19}/tests/test_client.py +42 -0
- {stacklink_sdk-0.3.14 → stacklink_sdk-0.3.19}/LICENSE +0 -0
- {stacklink_sdk-0.3.14 → stacklink_sdk-0.3.19}/README.md +0 -0
- {stacklink_sdk-0.3.14 → stacklink_sdk-0.3.19}/setup.cfg +0 -0
- {stacklink_sdk-0.3.14 → stacklink_sdk-0.3.19}/stacklink/api_keys.py +0 -0
- {stacklink_sdk-0.3.14 → stacklink_sdk-0.3.19}/stacklink/approvals.py +0 -0
- {stacklink_sdk-0.3.14 → stacklink_sdk-0.3.19}/stacklink/artifacts.py +0 -0
- {stacklink_sdk-0.3.14 → stacklink_sdk-0.3.19}/stacklink/auth_configs.py +0 -0
- {stacklink_sdk-0.3.14 → stacklink_sdk-0.3.19}/stacklink/claude_agent.py +0 -0
- {stacklink_sdk-0.3.14 → stacklink_sdk-0.3.19}/stacklink/connected_accounts.py +0 -0
- {stacklink_sdk-0.3.14 → stacklink_sdk-0.3.19}/stacklink/errors.py +0 -0
- {stacklink_sdk-0.3.14 → stacklink_sdk-0.3.19}/stacklink/executions.py +0 -0
- {stacklink_sdk-0.3.14 → stacklink_sdk-0.3.19}/stacklink/knowledge.py +0 -0
- {stacklink_sdk-0.3.14 → stacklink_sdk-0.3.19}/stacklink/logs.py +0 -0
- {stacklink_sdk-0.3.14 → stacklink_sdk-0.3.19}/stacklink/mcp.py +0 -0
- {stacklink_sdk-0.3.14 → stacklink_sdk-0.3.19}/stacklink/memory.py +0 -0
- {stacklink_sdk-0.3.14 → stacklink_sdk-0.3.19}/stacklink/openai_agents.py +0 -0
- {stacklink_sdk-0.3.14 → stacklink_sdk-0.3.19}/stacklink/openai_memory.py +0 -0
- {stacklink_sdk-0.3.14 → stacklink_sdk-0.3.19}/stacklink/projects.py +0 -0
- {stacklink_sdk-0.3.14 → stacklink_sdk-0.3.19}/stacklink/providers.py +0 -0
- {stacklink_sdk-0.3.14 → stacklink_sdk-0.3.19}/stacklink/sessions.py +0 -0
- {stacklink_sdk-0.3.14 → stacklink_sdk-0.3.19}/stacklink/tools.py +0 -0
- {stacklink_sdk-0.3.14 → stacklink_sdk-0.3.19}/stacklink/triggers.py +0 -0
- {stacklink_sdk-0.3.14 → stacklink_sdk-0.3.19}/stacklink/types.py +0 -0
- {stacklink_sdk-0.3.14 → stacklink_sdk-0.3.19}/stacklink/webhooks.py +0 -0
- {stacklink_sdk-0.3.14 → stacklink_sdk-0.3.19}/stacklink/x_chat.py +0 -0
- {stacklink_sdk-0.3.14 → stacklink_sdk-0.3.19}/stacklink_sdk.egg-info/SOURCES.txt +0 -0
- {stacklink_sdk-0.3.14 → stacklink_sdk-0.3.19}/stacklink_sdk.egg-info/dependency_links.txt +0 -0
- {stacklink_sdk-0.3.14 → stacklink_sdk-0.3.19}/stacklink_sdk.egg-info/requires.txt +0 -0
- {stacklink_sdk-0.3.14 → stacklink_sdk-0.3.19}/stacklink_sdk.egg-info/top_level.txt +0 -0
- {stacklink_sdk-0.3.14 → stacklink_sdk-0.3.19}/tests/test_google_docs_provider.py +0 -0
- {stacklink_sdk-0.3.14 → stacklink_sdk-0.3.19}/tests/test_google_drive_provider.py +0 -0
- {stacklink_sdk-0.3.14 → stacklink_sdk-0.3.19}/tests/test_google_sheets_provider.py +0 -0
- {stacklink_sdk-0.3.14 → stacklink_sdk-0.3.19}/tests/test_openai_memory.py +0 -0
- {stacklink_sdk-0.3.14 → stacklink_sdk-0.3.19}/tests/test_zoho_books_provider.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "stacklink-sdk"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.19"
|
|
8
8
|
description = "Python SDK for Stacklink sessions, tools, MCP, auth, workbench, logs, knowledge, and billing."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "LicenseRef-Proprietary"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from .bots import verify_bot_api_request, sign_bot_trigger_request, BotRun, BotConnectionAction, BotExecutionWait, BotCapabilityFact, BotRequirement
|
|
3
|
+
from .bots import BotAvatar, verify_bot_api_request, sign_bot_trigger_request, BotRun, BotConnectionAction, BotExecutionWait, BotCapabilityFact, BotRequirement
|
|
4
4
|
from .approvals import ApprovalDecision, ApprovalRecord, ApprovalEvent, ApprovalPermissions, CustomerApprovalGrantInput
|
|
5
5
|
from .webhooks import verify_webhook_signature
|
|
6
6
|
from .client import AsyncStacklink, STACKLINK_PYTHON_SDK_VERSION, Stacklink
|
|
@@ -27,7 +27,7 @@ from .types import (
|
|
|
27
27
|
)
|
|
28
28
|
|
|
29
29
|
__all__ = [
|
|
30
|
-
"BotRun", "BotConnectionAction", "BotExecutionWait", "BotCapabilityFact", "BotRequirement",
|
|
30
|
+
"BotAvatar", "BotRun", "BotConnectionAction", "BotExecutionWait", "BotCapabilityFact", "BotRequirement",
|
|
31
31
|
"ApprovalDecision", "ApprovalRecord", "ApprovalEvent", "ApprovalPermissions",
|
|
32
32
|
"CustomerApprovalGrantInput",
|
|
33
33
|
"Stacklink",
|
|
@@ -1,6 +1,45 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import Any
|
|
3
|
+
from typing import Any, TypedDict, cast
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ResourceAllowanceAdjustment(TypedDict, total=False):
|
|
7
|
+
credited: int
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ResourceUsage(ResourceAllowanceAdjustment):
|
|
11
|
+
key: str
|
|
12
|
+
label: str
|
|
13
|
+
unit: str
|
|
14
|
+
included: int
|
|
15
|
+
used: int
|
|
16
|
+
reserved: int
|
|
17
|
+
remaining: int
|
|
18
|
+
additional: int
|
|
19
|
+
additionalCents: int
|
|
20
|
+
priceMicros: str
|
|
21
|
+
unitQuantity: int
|
|
22
|
+
rateVersion: str
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AdditionalSpending(TypedDict):
|
|
26
|
+
approvedCents: int
|
|
27
|
+
usedCents: int
|
|
28
|
+
reservedCents: int
|
|
29
|
+
availableCents: int
|
|
30
|
+
currency: str
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class UsageSummary(TypedDict, total=False):
|
|
34
|
+
agreementVersion: str
|
|
35
|
+
resources: list[ResourceUsage]
|
|
36
|
+
spending: dict[str, AdditionalSpending]
|
|
37
|
+
workspace: dict[str, Any]
|
|
38
|
+
subscription: dict[str, Any]
|
|
39
|
+
plan: dict[str, Any]
|
|
40
|
+
period: dict[str, str]
|
|
41
|
+
usage: dict[str, Any]
|
|
42
|
+
overagePending: bool
|
|
4
43
|
|
|
5
44
|
|
|
6
45
|
class BillingResource:
|
|
@@ -16,8 +55,8 @@ class BillingResource:
|
|
|
16
55
|
def subscription(self):
|
|
17
56
|
return self.client.request("GET", "/v1/subscription")
|
|
18
57
|
|
|
19
|
-
def usage(self, **query):
|
|
20
|
-
return self.client.request("GET", "/v1/usage", query=query)
|
|
58
|
+
def usage(self, **query) -> UsageSummary:
|
|
59
|
+
return cast(UsageSummary, self.client.request("GET", "/v1/usage", query=query))
|
|
21
60
|
|
|
22
61
|
def usage_events(self, **query):
|
|
23
62
|
return self.client.request("GET", "/v1/usage/events", query=query)
|
|
@@ -15,8 +15,15 @@ from urllib.parse import quote
|
|
|
15
15
|
from uuid import uuid4
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
class BotAvatar(TypedDict):
|
|
19
|
+
version: Literal[1]
|
|
20
|
+
style: Literal["clay", "pebble", "shapes", "letters"]
|
|
21
|
+
seed: str
|
|
22
|
+
tone: Literal["auto", "mint", "teal", "sky", "iris", "plum", "rose", "coral", "apricot", "sun", "lime", "slate", "ink"]
|
|
23
|
+
|
|
24
|
+
|
|
18
25
|
class _BotCapabilityAccount(TypedDict, total=False):
|
|
19
|
-
account: Literal["project", "customer"]
|
|
26
|
+
account: Literal["project", "customer", "deployment"]
|
|
20
27
|
|
|
21
28
|
|
|
22
29
|
class BotCapabilityFact(_BotCapabilityAccount):
|
|
@@ -218,8 +225,8 @@ class BotsResource:
|
|
|
218
225
|
def set_state(self, bot_id: str, state: str, *, pause_for: str = "manual", reason: str = ""):
|
|
219
226
|
return self.client.request("POST", self._path(bot_id) + "/state", json_body={"state": state, "pauseFor": pause_for, "reason": reason})
|
|
220
227
|
|
|
221
|
-
def create(self, name: str):
|
|
222
|
-
return self.client.request("POST", "/v1/bots", json_body={"name": name})
|
|
228
|
+
def create(self, name: str, *, public_description: str = "", icon_asset_id: str | None = None, avatar: BotAvatar | None = None):
|
|
229
|
+
return self.client.request("POST", "/v1/bots", json_body={"name": name, "publicDescription": public_description, "iconAssetId": icon_asset_id, **({"avatar": avatar} if avatar is not None else {})})
|
|
223
230
|
|
|
224
231
|
def set_customer_credential(self, bot_id: str, *, api_id: str, session_id: str, secret: str):
|
|
225
232
|
return self.client.request("POST", self._path(bot_id) + "/credentials", json_body={"apiId": api_id, "sessionId": session_id, "secret": secret})
|
|
@@ -37,7 +37,7 @@ from .types import Json, Transport
|
|
|
37
37
|
from .webhooks import WebhooksResource
|
|
38
38
|
from .workbench import WorkbenchResource
|
|
39
39
|
|
|
40
|
-
STACKLINK_PYTHON_SDK_VERSION = "0.3.
|
|
40
|
+
STACKLINK_PYTHON_SDK_VERSION = "0.3.19"
|
|
41
41
|
STACKLINK_PYTHON_USER_AGENT = f"stacklink-python/{STACKLINK_PYTHON_SDK_VERSION}"
|
|
42
42
|
|
|
43
43
|
|
|
@@ -18,6 +18,10 @@ WorkbenchExecutionKind = Literal["workbench_code", "workbench_bash"]
|
|
|
18
18
|
WorkbenchReplayEnvironmentMode = Literal["require_match", "restore_snapshot", "current"]
|
|
19
19
|
WorkbenchExecutionStatus = Literal[
|
|
20
20
|
"waiting_for_approval",
|
|
21
|
+
"checkpointing",
|
|
22
|
+
"waiting_saved",
|
|
23
|
+
"waiting_ready",
|
|
24
|
+
"resuming",
|
|
21
25
|
"queued",
|
|
22
26
|
"started",
|
|
23
27
|
"running",
|
|
@@ -68,6 +72,28 @@ class WorkbenchSession(TypedDict, total=False):
|
|
|
68
72
|
updatedAt: str
|
|
69
73
|
|
|
70
74
|
|
|
75
|
+
class WorkbenchComputeState(TypedDict, total=False):
|
|
76
|
+
status: str
|
|
77
|
+
stopped: bool
|
|
78
|
+
fundedThrough: int | None
|
|
79
|
+
activeExecutionCount: int
|
|
80
|
+
generation: int | None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class WorkbenchRuntimeCapacity(TypedDict):
|
|
84
|
+
measurement: Literal["customer_active_ms", "legacy_operations"]
|
|
85
|
+
settlementStatus: Literal["pending", "acknowledgement_pending", "settled"]
|
|
86
|
+
reservedMilliseconds: int
|
|
87
|
+
workspace: dict[str, Any] | None
|
|
88
|
+
intervals: list[dict[str, Any]]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class WorkbenchComputeStatus(TypedDict):
|
|
92
|
+
workbenchSession: WorkbenchSession
|
|
93
|
+
compute: WorkbenchComputeState
|
|
94
|
+
billing: WorkbenchRuntimeCapacity
|
|
95
|
+
|
|
96
|
+
|
|
71
97
|
class WorkbenchSessionUseResponse(TypedDict):
|
|
72
98
|
workbenchSession: WorkbenchSession
|
|
73
99
|
reused: bool
|
|
@@ -119,7 +145,30 @@ class WorkbenchSessionDeleteReceipt(TypedDict):
|
|
|
119
145
|
databaseDeleted: Literal[True]
|
|
120
146
|
|
|
121
147
|
|
|
148
|
+
class WorkbenchWaitContinuation(TypedDict):
|
|
149
|
+
language: WorkbenchLanguage
|
|
150
|
+
code: str
|
|
151
|
+
timeoutMs: int
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class WorkbenchKeepReadyConsent(TypedDict):
|
|
155
|
+
maximumWaitMs: int
|
|
156
|
+
maximumChargeMicros: str
|
|
157
|
+
priceMicros: str
|
|
158
|
+
unitQuantity: int
|
|
159
|
+
rateVersion: str
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class WorkbenchWaitRequest(TypedDict, total=False):
|
|
163
|
+
mode: Literal["saved", "keep_ready"]
|
|
164
|
+
reason: Literal["approval", "connection", "user_input", "external_event"]
|
|
165
|
+
maximumWaitMs: int
|
|
166
|
+
continuation: WorkbenchWaitContinuation
|
|
167
|
+
keepReady: WorkbenchKeepReadyConsent
|
|
168
|
+
|
|
169
|
+
|
|
122
170
|
class WorkbenchRunInput(TypedDict, total=False):
|
|
171
|
+
afterExecution: WorkbenchWaitRequest
|
|
123
172
|
language: WorkbenchLanguage
|
|
124
173
|
code: str
|
|
125
174
|
workbenchSessionId: str
|
|
@@ -136,6 +185,7 @@ class WorkbenchRunInput(TypedDict, total=False):
|
|
|
136
185
|
|
|
137
186
|
|
|
138
187
|
class WorkbenchBashInput(TypedDict, total=False):
|
|
188
|
+
afterExecution: WorkbenchWaitRequest
|
|
139
189
|
command: str
|
|
140
190
|
workbenchSessionId: str
|
|
141
191
|
timeoutMs: int
|
|
@@ -151,6 +201,8 @@ class WorkbenchBashInput(TypedDict, total=False):
|
|
|
151
201
|
|
|
152
202
|
|
|
153
203
|
class WorkbenchExecutionLog(TypedDict, total=False):
|
|
204
|
+
waitRevision: int
|
|
205
|
+
waitState: dict[str, Any] | None
|
|
154
206
|
id: str
|
|
155
207
|
workbenchSessionId: str
|
|
156
208
|
operation: str
|
|
@@ -465,7 +517,7 @@ class WorkbenchExecutionStateResponse(TypedDict, total=False):
|
|
|
465
517
|
class WorkbenchExecutionResponse(TypedDict, total=False):
|
|
466
518
|
approvalRequestId: str
|
|
467
519
|
executed: bool
|
|
468
|
-
nextAction: Literal["wait_for_approval"]
|
|
520
|
+
nextAction: Literal["wait_for_approval", "resume_saved_work"]
|
|
469
521
|
workbenchSession: WorkbenchSession
|
|
470
522
|
executionKind: WorkbenchExecutionKind
|
|
471
523
|
executionId: str
|
|
@@ -583,7 +635,7 @@ def _wait_for_workbench_response(
|
|
|
583
635
|
execution_id = accepted.get("executionId")
|
|
584
636
|
execution_kind = accepted.get("executionKind")
|
|
585
637
|
if (
|
|
586
|
-
accepted.get("status")
|
|
638
|
+
accepted.get("status") in ("waiting_for_approval", "waiting_saved", "waiting_ready")
|
|
587
639
|
or not isinstance(session_id, str)
|
|
588
640
|
or not isinstance(execution_id, str)
|
|
589
641
|
or execution_kind not in {"workbench_code", "workbench_bash"}
|
|
@@ -598,13 +650,15 @@ def _wait_for_workbench_response(
|
|
|
598
650
|
{"waitTimeoutMs": 10_000},
|
|
599
651
|
))
|
|
600
652
|
execution = waited.get("execution")
|
|
601
|
-
if not isinstance(execution, dict) or execution.get("status") not in {
|
|
653
|
+
if not isinstance(execution, dict) or (execution.get("nextAction") != "wait_for_approval" and execution.get("status") not in {
|
|
602
654
|
"waiting_for_approval",
|
|
655
|
+
"waiting_saved",
|
|
656
|
+
"waiting_ready",
|
|
603
657
|
"completed",
|
|
604
658
|
"failed",
|
|
605
659
|
"cancelled",
|
|
606
660
|
"timed_out",
|
|
607
|
-
}:
|
|
661
|
+
}):
|
|
608
662
|
continue
|
|
609
663
|
response: WorkbenchExecutionResponse = {
|
|
610
664
|
**accepted,
|
|
@@ -616,9 +670,9 @@ def _wait_for_workbench_response(
|
|
|
616
670
|
}
|
|
617
671
|
if isinstance(execution.get("approvalRequestId"), str):
|
|
618
672
|
response["approvalRequestId"] = execution["approvalRequestId"]
|
|
619
|
-
if execution.get("status")
|
|
673
|
+
if execution.get("status") in ("waiting_for_approval", "waiting_saved", "waiting_ready") or execution.get("nextAction") == "wait_for_approval":
|
|
620
674
|
response["executed"] = False
|
|
621
|
-
response["nextAction"] = "wait_for_approval"
|
|
675
|
+
response["nextAction"] = "wait_for_approval" if execution.get("status") == "waiting_for_approval" or execution.get("nextAction") == "wait_for_approval" else "resume_saved_work"
|
|
622
676
|
output_mode = execution.get("outputMode")
|
|
623
677
|
if output_mode in {"inline", "preview", "events", "artifact"}:
|
|
624
678
|
response["outputMode"] = cast(WorkbenchOutputMode, output_mode)
|
|
@@ -996,6 +1050,12 @@ class SessionSandbox:
|
|
|
996
1050
|
self.runs = WorkbenchRuns(self)
|
|
997
1051
|
self.artifacts = WorkbenchArtifacts(self)
|
|
998
1052
|
|
|
1053
|
+
def resume(self, execution_id: str, expected_wait_revision: int, checkpoint_id: str) -> WorkbenchExecutionResponse:
|
|
1054
|
+
return self.client.request("POST", "/v1/workbench/resume", json_body={
|
|
1055
|
+
"sessionId": self.platform_session_id, "executionId": execution_id,
|
|
1056
|
+
"expectedWaitRevision": expected_wait_revision, "checkpointId": checkpoint_id,
|
|
1057
|
+
})
|
|
1058
|
+
|
|
999
1059
|
def use(
|
|
1000
1060
|
self,
|
|
1001
1061
|
workbench_session_id: str | None = None,
|
|
@@ -1084,6 +1144,17 @@ class WorkbenchResource:
|
|
|
1084
1144
|
return _wait_for_workbench_response(self.client, body, response)
|
|
1085
1145
|
return response
|
|
1086
1146
|
|
|
1147
|
+
def resume(self, session_id: str, execution_id: str, *, expected_wait_revision: int, checkpoint_id: str):
|
|
1148
|
+
return self.client.request("POST", "/v1/workbench/resume", json_body={
|
|
1149
|
+
"sessionId": session_id, "executionId": execution_id,
|
|
1150
|
+
"expectedWaitRevision": expected_wait_revision, "checkpointId": checkpoint_id,
|
|
1151
|
+
})
|
|
1152
|
+
|
|
1153
|
+
def compute_status(self, session_id: str, workbench_session_id: str) -> WorkbenchComputeStatus:
|
|
1154
|
+
return cast(WorkbenchComputeStatus, self.client.request("POST", "/v1/workbench/run", json_body={
|
|
1155
|
+
"sessionId": session_id, "workbenchSessionId": workbench_session_id, "operation": "compute_status",
|
|
1156
|
+
}))
|
|
1157
|
+
|
|
1087
1158
|
def destroy(self, session_id: str, workbench_session_id: str):
|
|
1088
1159
|
return self.run({
|
|
1089
1160
|
"sessionId": session_id,
|
|
@@ -63,6 +63,24 @@ def stalled_stream_server(*, final_result=False, execution_page=False):
|
|
|
63
63
|
|
|
64
64
|
|
|
65
65
|
class StacklinkClientTests(unittest.TestCase):
|
|
66
|
+
def test_bot_avatar_recipe_is_preserved_for_sync_and_async_create(self):
|
|
67
|
+
recipe = {"version": 1, "style": "clay", "seed": "Sales assistant", "tone": "mint"}
|
|
68
|
+
payloads = []
|
|
69
|
+
def respond(request):
|
|
70
|
+
payload = json.loads(request.content)
|
|
71
|
+
payloads.append(payload)
|
|
72
|
+
return httpx.Response(200, json={"id": "bot", "identity": {"avatar": payload["avatar"], "pictureUrl": "https://example.test/avatar.png"}})
|
|
73
|
+
with httpx.Client(transport=httpx.MockTransport(respond)) as http:
|
|
74
|
+
client = Stacklink(api_key="test", base_url="https://example.test", http_client=http)
|
|
75
|
+
self.assertEqual(client.bots.create("Sales", avatar=recipe)["identity"]["avatar"], recipe)
|
|
76
|
+
async def check_async():
|
|
77
|
+
async with httpx.AsyncClient(transport=httpx.MockTransport(respond)) as http:
|
|
78
|
+
client = AsyncStacklink(api_key="test", base_url="https://example.test", http_client=http)
|
|
79
|
+
result = await client.bots.create("Sales", avatar=recipe)
|
|
80
|
+
self.assertEqual(result["identity"]["avatar"], recipe)
|
|
81
|
+
asyncio.run(check_async())
|
|
82
|
+
self.assertEqual([item["avatar"] for item in payloads], [recipe, recipe])
|
|
83
|
+
|
|
66
84
|
def test_customer_grant_management_uses_session_identity(self):
|
|
67
85
|
calls = []
|
|
68
86
|
def transport(method, path, headers, body, query, timeout):
|
|
@@ -99,6 +117,30 @@ class StacklinkClientTests(unittest.TestCase):
|
|
|
99
117
|
self.assertFalse(result["executed"])
|
|
100
118
|
self.assertEqual(len(calls), 2 if admission_status == "queued" else 1)
|
|
101
119
|
|
|
120
|
+
def test_saved_wait_returns_to_caller_and_preserves_resume_identity(self):
|
|
121
|
+
for status in ("waiting_saved", "waiting_ready", "resuming"):
|
|
122
|
+
calls = []
|
|
123
|
+
state = {"id": "execution-1", "executionKind": "workbench_code", "status": status,
|
|
124
|
+
"waitRevision": 2, "waitState": {"checkpointId": "checkpoint-1"}}
|
|
125
|
+
if status == "resuming":
|
|
126
|
+
state.update({"nextAction": "wait_for_approval", "approvalRequestId": "approval-2"})
|
|
127
|
+
def transport(method, path, headers, body, query, timeout):
|
|
128
|
+
calls.append((path, body))
|
|
129
|
+
if path == "/v1/workbench/run":
|
|
130
|
+
return 202, {}, {"executionId": state["id"], "executionKind": "workbench_code", "status": "queued"}
|
|
131
|
+
if path.endswith("/wait") and len(calls) == 2:
|
|
132
|
+
return 200, {}, {"execution": state}
|
|
133
|
+
if path == "/v1/workbench/resume":
|
|
134
|
+
return 202, {}, {**state, "status": "resuming"}
|
|
135
|
+
raise AssertionError(f"Unexpected saved wait polling: {path}")
|
|
136
|
+
client = Stacklink(api_key="ppk_test", transport=transport)
|
|
137
|
+
result = client.workbench.run({"sessionId": "session-1", "operation": "run_code", "code": "print(1)"})
|
|
138
|
+
self.assertEqual(result["status"], status)
|
|
139
|
+
self.assertEqual(result["nextAction"], "wait_for_approval" if status == "resuming" else "resume_saved_work")
|
|
140
|
+
self.assertEqual(len(calls), 2)
|
|
141
|
+
client.workbench.resume("session-1", "execution-1", expected_wait_revision=2, checkpoint_id="checkpoint-1")
|
|
142
|
+
self.assertEqual(calls[-1], ("/v1/workbench/resume", {"sessionId": "session-1", "executionId": "execution-1", "expectedWaitRevision": 2, "checkpointId": "checkpoint-1"}))
|
|
143
|
+
|
|
102
144
|
def test_approvals_observe_reconnect_and_customer_identity(self):
|
|
103
145
|
calls = []
|
|
104
146
|
def transport(method, path, headers, body, query, timeout):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|