stacklink-sdk 0.3.15__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.15/stacklink_sdk.egg-info → stacklink_sdk-0.3.19}/PKG-INFO +1 -1
- {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/pyproject.toml +1 -1
- {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/__init__.py +2 -2
- {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/bots.py +10 -3
- {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/client.py +1 -1
- {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/workbench.py +50 -6
- {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19/stacklink_sdk.egg-info}/PKG-INFO +1 -1
- {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/tests/test_client.py +42 -0
- {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/LICENSE +0 -0
- {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/README.md +0 -0
- {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/setup.cfg +0 -0
- {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/api_keys.py +0 -0
- {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/approvals.py +0 -0
- {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/artifacts.py +0 -0
- {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/auth_configs.py +0 -0
- {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/billing.py +0 -0
- {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/claude_agent.py +0 -0
- {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/connected_accounts.py +0 -0
- {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/errors.py +0 -0
- {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/executions.py +0 -0
- {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/knowledge.py +0 -0
- {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/logs.py +0 -0
- {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/mcp.py +0 -0
- {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/memory.py +0 -0
- {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/openai_agents.py +0 -0
- {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/openai_memory.py +0 -0
- {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/projects.py +0 -0
- {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/providers.py +0 -0
- {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/sessions.py +0 -0
- {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/tools.py +0 -0
- {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/triggers.py +0 -0
- {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/types.py +0 -0
- {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/webhooks.py +0 -0
- {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/x_chat.py +0 -0
- {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink_sdk.egg-info/SOURCES.txt +0 -0
- {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink_sdk.egg-info/dependency_links.txt +0 -0
- {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink_sdk.egg-info/requires.txt +0 -0
- {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink_sdk.egg-info/top_level.txt +0 -0
- {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/tests/test_google_docs_provider.py +0 -0
- {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/tests/test_google_drive_provider.py +0 -0
- {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/tests/test_google_sheets_provider.py +0 -0
- {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/tests/test_openai_memory.py +0 -0
- {stacklink_sdk-0.3.15 → 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",
|
|
@@ -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",
|
|
@@ -141,7 +145,30 @@ class WorkbenchSessionDeleteReceipt(TypedDict):
|
|
|
141
145
|
databaseDeleted: Literal[True]
|
|
142
146
|
|
|
143
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
|
+
|
|
144
170
|
class WorkbenchRunInput(TypedDict, total=False):
|
|
171
|
+
afterExecution: WorkbenchWaitRequest
|
|
145
172
|
language: WorkbenchLanguage
|
|
146
173
|
code: str
|
|
147
174
|
workbenchSessionId: str
|
|
@@ -158,6 +185,7 @@ class WorkbenchRunInput(TypedDict, total=False):
|
|
|
158
185
|
|
|
159
186
|
|
|
160
187
|
class WorkbenchBashInput(TypedDict, total=False):
|
|
188
|
+
afterExecution: WorkbenchWaitRequest
|
|
161
189
|
command: str
|
|
162
190
|
workbenchSessionId: str
|
|
163
191
|
timeoutMs: int
|
|
@@ -173,6 +201,8 @@ class WorkbenchBashInput(TypedDict, total=False):
|
|
|
173
201
|
|
|
174
202
|
|
|
175
203
|
class WorkbenchExecutionLog(TypedDict, total=False):
|
|
204
|
+
waitRevision: int
|
|
205
|
+
waitState: dict[str, Any] | None
|
|
176
206
|
id: str
|
|
177
207
|
workbenchSessionId: str
|
|
178
208
|
operation: str
|
|
@@ -487,7 +517,7 @@ class WorkbenchExecutionStateResponse(TypedDict, total=False):
|
|
|
487
517
|
class WorkbenchExecutionResponse(TypedDict, total=False):
|
|
488
518
|
approvalRequestId: str
|
|
489
519
|
executed: bool
|
|
490
|
-
nextAction: Literal["wait_for_approval"]
|
|
520
|
+
nextAction: Literal["wait_for_approval", "resume_saved_work"]
|
|
491
521
|
workbenchSession: WorkbenchSession
|
|
492
522
|
executionKind: WorkbenchExecutionKind
|
|
493
523
|
executionId: str
|
|
@@ -605,7 +635,7 @@ def _wait_for_workbench_response(
|
|
|
605
635
|
execution_id = accepted.get("executionId")
|
|
606
636
|
execution_kind = accepted.get("executionKind")
|
|
607
637
|
if (
|
|
608
|
-
accepted.get("status")
|
|
638
|
+
accepted.get("status") in ("waiting_for_approval", "waiting_saved", "waiting_ready")
|
|
609
639
|
or not isinstance(session_id, str)
|
|
610
640
|
or not isinstance(execution_id, str)
|
|
611
641
|
or execution_kind not in {"workbench_code", "workbench_bash"}
|
|
@@ -620,13 +650,15 @@ def _wait_for_workbench_response(
|
|
|
620
650
|
{"waitTimeoutMs": 10_000},
|
|
621
651
|
))
|
|
622
652
|
execution = waited.get("execution")
|
|
623
|
-
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 {
|
|
624
654
|
"waiting_for_approval",
|
|
655
|
+
"waiting_saved",
|
|
656
|
+
"waiting_ready",
|
|
625
657
|
"completed",
|
|
626
658
|
"failed",
|
|
627
659
|
"cancelled",
|
|
628
660
|
"timed_out",
|
|
629
|
-
}:
|
|
661
|
+
}):
|
|
630
662
|
continue
|
|
631
663
|
response: WorkbenchExecutionResponse = {
|
|
632
664
|
**accepted,
|
|
@@ -638,9 +670,9 @@ def _wait_for_workbench_response(
|
|
|
638
670
|
}
|
|
639
671
|
if isinstance(execution.get("approvalRequestId"), str):
|
|
640
672
|
response["approvalRequestId"] = execution["approvalRequestId"]
|
|
641
|
-
if execution.get("status")
|
|
673
|
+
if execution.get("status") in ("waiting_for_approval", "waiting_saved", "waiting_ready") or execution.get("nextAction") == "wait_for_approval":
|
|
642
674
|
response["executed"] = False
|
|
643
|
-
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"
|
|
644
676
|
output_mode = execution.get("outputMode")
|
|
645
677
|
if output_mode in {"inline", "preview", "events", "artifact"}:
|
|
646
678
|
response["outputMode"] = cast(WorkbenchOutputMode, output_mode)
|
|
@@ -1018,6 +1050,12 @@ class SessionSandbox:
|
|
|
1018
1050
|
self.runs = WorkbenchRuns(self)
|
|
1019
1051
|
self.artifacts = WorkbenchArtifacts(self)
|
|
1020
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
|
+
|
|
1021
1059
|
def use(
|
|
1022
1060
|
self,
|
|
1023
1061
|
workbench_session_id: str | None = None,
|
|
@@ -1106,6 +1144,12 @@ class WorkbenchResource:
|
|
|
1106
1144
|
return _wait_for_workbench_response(self.client, body, response)
|
|
1107
1145
|
return response
|
|
1108
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
|
+
|
|
1109
1153
|
def compute_status(self, session_id: str, workbench_session_id: str) -> WorkbenchComputeStatus:
|
|
1110
1154
|
return cast(WorkbenchComputeStatus, self.client.request("POST", "/v1/workbench/run", json_body={
|
|
1111
1155
|
"sessionId": session_id, "workbenchSessionId": workbench_session_id, "operation": "compute_status",
|
|
@@ -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
|
|
File without changes
|