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