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.
Files changed (43) hide show
  1. {stacklink_sdk-0.3.15/stacklink_sdk.egg-info → stacklink_sdk-0.3.19}/PKG-INFO +1 -1
  2. {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/pyproject.toml +1 -1
  3. {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/__init__.py +2 -2
  4. {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/bots.py +10 -3
  5. {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/client.py +1 -1
  6. {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/workbench.py +50 -6
  7. {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19/stacklink_sdk.egg-info}/PKG-INFO +1 -1
  8. {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/tests/test_client.py +42 -0
  9. {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/LICENSE +0 -0
  10. {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/README.md +0 -0
  11. {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/setup.cfg +0 -0
  12. {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/api_keys.py +0 -0
  13. {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/approvals.py +0 -0
  14. {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/artifacts.py +0 -0
  15. {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/auth_configs.py +0 -0
  16. {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/billing.py +0 -0
  17. {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/claude_agent.py +0 -0
  18. {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/connected_accounts.py +0 -0
  19. {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/errors.py +0 -0
  20. {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/executions.py +0 -0
  21. {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/knowledge.py +0 -0
  22. {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/logs.py +0 -0
  23. {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/mcp.py +0 -0
  24. {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/memory.py +0 -0
  25. {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/openai_agents.py +0 -0
  26. {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/openai_memory.py +0 -0
  27. {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/projects.py +0 -0
  28. {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/providers.py +0 -0
  29. {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/sessions.py +0 -0
  30. {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/tools.py +0 -0
  31. {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/triggers.py +0 -0
  32. {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/types.py +0 -0
  33. {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/webhooks.py +0 -0
  34. {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink/x_chat.py +0 -0
  35. {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink_sdk.egg-info/SOURCES.txt +0 -0
  36. {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink_sdk.egg-info/dependency_links.txt +0 -0
  37. {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink_sdk.egg-info/requires.txt +0 -0
  38. {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/stacklink_sdk.egg-info/top_level.txt +0 -0
  39. {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/tests/test_google_docs_provider.py +0 -0
  40. {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/tests/test_google_drive_provider.py +0 -0
  41. {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/tests/test_google_sheets_provider.py +0 -0
  42. {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/tests/test_openai_memory.py +0 -0
  43. {stacklink_sdk-0.3.15 → stacklink_sdk-0.3.19}/tests/test_zoho_books_provider.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stacklink-sdk
3
- Version: 0.3.15
3
+ Version: 0.3.19
4
4
  Summary: Python SDK for Stacklink sessions, tools, MCP, auth, workbench, logs, knowledge, and billing.
5
5
  Author: Stacklink
6
6
  License-Expression: LicenseRef-Proprietary
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "stacklink-sdk"
7
- version = "0.3.15"
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.15"
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") == "waiting_for_approval"
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") == "waiting_for_approval":
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",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stacklink-sdk
3
- Version: 0.3.15
3
+ Version: 0.3.19
4
4
  Summary: Python SDK for Stacklink sessions, tools, MCP, auth, workbench, logs, knowledge, and billing.
5
5
  Author: Stacklink
6
6
  License-Expression: LicenseRef-Proprietary
@@ -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