stacklink-sdk 0.3.10__tar.gz → 0.3.14__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.10/stacklink_sdk.egg-info → stacklink_sdk-0.3.14}/PKG-INFO +1 -1
  2. {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/pyproject.toml +1 -1
  3. {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/__init__.py +2 -1
  4. {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/bots.py +107 -2
  5. {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/client.py +1 -1
  6. {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14/stacklink_sdk.egg-info}/PKG-INFO +1 -1
  7. {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/tests/test_client.py +41 -0
  8. {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/LICENSE +0 -0
  9. {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/README.md +0 -0
  10. {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/setup.cfg +0 -0
  11. {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/api_keys.py +0 -0
  12. {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/approvals.py +0 -0
  13. {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/artifacts.py +0 -0
  14. {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/auth_configs.py +0 -0
  15. {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/billing.py +0 -0
  16. {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/claude_agent.py +0 -0
  17. {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/connected_accounts.py +0 -0
  18. {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/errors.py +0 -0
  19. {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/executions.py +0 -0
  20. {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/knowledge.py +0 -0
  21. {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/logs.py +0 -0
  22. {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/mcp.py +0 -0
  23. {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/memory.py +0 -0
  24. {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/openai_agents.py +0 -0
  25. {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/openai_memory.py +0 -0
  26. {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/projects.py +0 -0
  27. {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/providers.py +0 -0
  28. {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/sessions.py +0 -0
  29. {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/tools.py +0 -0
  30. {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/triggers.py +0 -0
  31. {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/types.py +0 -0
  32. {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/webhooks.py +0 -0
  33. {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/workbench.py +0 -0
  34. {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/x_chat.py +0 -0
  35. {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink_sdk.egg-info/SOURCES.txt +0 -0
  36. {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink_sdk.egg-info/dependency_links.txt +0 -0
  37. {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink_sdk.egg-info/requires.txt +0 -0
  38. {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink_sdk.egg-info/top_level.txt +0 -0
  39. {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/tests/test_google_docs_provider.py +0 -0
  40. {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/tests/test_google_drive_provider.py +0 -0
  41. {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/tests/test_google_sheets_provider.py +0 -0
  42. {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/tests/test_openai_memory.py +0 -0
  43. {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/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.10
3
+ Version: 0.3.14
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.10"
7
+ version = "0.3.14"
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
3
+ from .bots import 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,6 +27,7 @@ from .types import (
27
27
  )
28
28
 
29
29
  __all__ = [
30
+ "BotRun", "BotConnectionAction", "BotExecutionWait", "BotCapabilityFact", "BotRequirement",
30
31
  "ApprovalDecision", "ApprovalRecord", "ApprovalEvent", "ApprovalPermissions",
31
32
  "CustomerApprovalGrantInput",
32
33
  "Stacklink",
@@ -1,6 +1,6 @@
1
1
  """Hosted bot management and durable runs for sync and async clients."""
2
2
  from __future__ import annotations
3
- from typing import Any
3
+ from typing import Any, Literal, TypedDict
4
4
  from contextlib import closing, aclosing
5
5
  import asyncio
6
6
  import base64
@@ -15,6 +15,106 @@ from urllib.parse import quote
15
15
  from uuid import uuid4
16
16
 
17
17
 
18
+ class _BotCapabilityAccount(TypedDict, total=False):
19
+ account: Literal["project", "customer"]
20
+
21
+
22
+ class BotCapabilityFact(_BotCapabilityAccount):
23
+ id: str
24
+ status: Literal["enabled", "disabled", "sample"]
25
+ owner: Literal["project_owner", "customer"]
26
+
27
+
28
+ class _BotConnectionDetails(TypedDict, total=False):
29
+ providerName: str
30
+ providerIconUrl: str
31
+ intent: Literal["connect", "reconnect"]
32
+ canResume: bool
33
+
34
+
35
+ class BotConnectionAction(_BotConnectionDetails):
36
+ kind: Literal["connection"]
37
+ id: str
38
+ providerKey: str
39
+ owner: Literal["customer", "project_owner"]
40
+ status: str
41
+ expiresAt: str
42
+ action: dict[str, str] | None
43
+
44
+
45
+ class BotExecutionWait(TypedDict):
46
+ kind: Literal["execution"]
47
+ id: str
48
+
49
+
50
+ class BotRequirement(TypedDict):
51
+ capabilityId: str
52
+ code: str
53
+ owner: Literal["project_owner", "stacklink"]
54
+ nextAction: Literal["configure_bot", "repair_connection", "use_live_data", "contact_administrator", "unavailable"]
55
+
56
+
57
+ class BotAttachment(TypedDict, total=False):
58
+ id: str
59
+ name: str
60
+ mediaType: str
61
+ sizeBytes: int
62
+ sha256: str | None
63
+ status: Literal["uploading", "uploaded", "preparing", "ready", "failed", "gone"]
64
+ facts: dict[str, Any]
65
+ warnings: list[dict[str, str]]
66
+ error: dict[str, str] | None
67
+
68
+
69
+ class BotFileUpload(TypedDict):
70
+ fileId: str
71
+ uploadUrl: str
72
+ requiredHeaders: dict[str, str]
73
+ expiresAt: str
74
+
75
+
76
+ class BotFiles:
77
+ """Sync and async clients share this contract; upload bytes with the returned headers."""
78
+ def __init__(self, client):
79
+ self.client = client
80
+
81
+ def _path(self, bot_id: str, file_id: str | None = None):
82
+ return "/v1/bots/" + quote(bot_id, safe="") + "/files" + ("/" + quote(file_id, safe="") if file_id else "")
83
+
84
+ def create_upload(self, bot_id: str, *, session_id: str, name: str, media_type: str, size_bytes: int):
85
+ return self.client.request("POST", self._path(bot_id), json_body={"sessionId": session_id, "name": name, "mediaType": media_type, "sizeBytes": size_bytes})
86
+
87
+ def finalize(self, bot_id: str, file_id: str, *, session_id: str):
88
+ return self.client.request("POST", self._path(bot_id, file_id) + "/finalize", json_body={"sessionId": session_id})
89
+
90
+ def get(self, bot_id: str, file_id: str, *, session_id: str):
91
+ return self.client.request("GET", self._path(bot_id, file_id), query={"sessionId": session_id})
92
+
93
+ def link(self, bot_id: str, file_id: str, *, session_id: str):
94
+ return self.client.request("POST", self._path(bot_id, file_id) + "/link", json_body={"sessionId": session_id})
95
+
96
+
97
+ class BotRun(TypedDict, total=False):
98
+ attachments: list[BotAttachment]
99
+ citations: list[dict[str, Any]]
100
+ outputs: list[dict[str, Any]]
101
+ coverage: list[dict[str, Any]]
102
+ updates: list[dict[str, str]]
103
+ id: str
104
+ botId: str
105
+ conversationId: str
106
+ status: str
107
+ finalText: str | None
108
+ output: Any
109
+ error: Any
110
+ waitingOperation: BotConnectionAction | BotExecutionWait | None
111
+ connection: BotConnectionAction | None
112
+ capabilities: list[BotCapabilityFact]
113
+ requirements: list[BotRequirement]
114
+ usage: dict[str, Any]
115
+ display: dict[str, Any]
116
+
117
+
18
118
  def sign_bot_trigger_request(*, trigger_id: str, secret: str, body: str, request_id: str | None = None, now: float | None = None) -> dict[str, str]:
19
119
  """Sign the exact JSON bytes. Reuse request_id when retrying the same event."""
20
120
  timestamp = str(int(time.time() if now is None else now))
@@ -60,6 +160,7 @@ def verify_bot_api_request(*, method: str, url: str, body: str, headers: dict[st
60
160
  class BotsResource:
61
161
  def __init__(self, client, *, async_mode: bool = False):
62
162
  self.client = client
163
+ self.files = BotFiles(client)
63
164
  self.async_mode = async_mode
64
165
 
65
166
  def _path(self, bot_id: str, run_id: str | None = None) -> str:
@@ -132,10 +233,14 @@ class BotsResource:
132
233
  def publish(self, bot_id: str, revision: int, channels: dict[str, bool]):
133
234
  return self.client.request("POST", self._path(bot_id) + "/publish", json_body={"revision": revision, "channels": channels})
134
235
 
135
- def run(self, bot_id: str, *, session_id: str, message: str, variables: dict[str, Any] | None = None, conversation_id: str | None = None, run_id: str | None = None):
236
+ def run(self, bot_id: str, *, session_id: str, message: str, variables: dict[str, Any] | None = None, conversation_id: str | None = None, run_id: str | None = None, connection_return_url: str | None = None, attachments: list[str] | None = None):
136
237
  body: dict[str, Any] = {"sessionId": session_id, "message": message, "variables": variables or {}, "runId": run_id or str(uuid4())}
238
+ if attachments is not None:
239
+ body["attachments"] = attachments
137
240
  if conversation_id:
138
241
  body["conversationId"] = conversation_id
242
+ if connection_return_url is not None:
243
+ body["connectionReturnUrl"] = connection_return_url
139
244
  return self.client.request("POST", self._path(bot_id) + "/runs", json_body=body)
140
245
 
141
246
  def get_run(self, bot_id: str, run_id: str):
@@ -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.10"
40
+ STACKLINK_PYTHON_SDK_VERSION = "0.3.14"
41
41
  STACKLINK_PYTHON_USER_AGENT = f"stacklink-python/{STACKLINK_PYTHON_SDK_VERSION}"
42
42
 
43
43
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stacklink-sdk
3
- Version: 0.3.10
3
+ Version: 0.3.14
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
@@ -130,6 +130,16 @@ class StacklinkClientTests(unittest.TestCase):
130
130
  self.assertEqual(headers["stacklink-signature"], hmac.new(b"fixture", signed.encode(), hashlib.sha256).hexdigest())
131
131
  self.assertNotEqual(headers["stacklink-signature"], sign_bot_trigger_request(trigger_id="trigger-one", secret="fixture", body=body + " ", request_id="event-one", now=1000)["stacklink-signature"])
132
132
 
133
+ def test_bot_connection_return_url_uses_the_admitted_run_contract(self):
134
+ calls = []
135
+ def transport(method, path, headers, body, query, timeout):
136
+ calls.append((method, path, body))
137
+ return 200, {}, {"id": "run"}
138
+ client = Stacklink(api_key="ppk_test", base_url="http://stacklink.test", transport=transport)
139
+ client.bots.run("bot-one", session_id="session-one", message="Read profile", connection_return_url="https://client.example/connected")
140
+ self.assertEqual(calls[0][2]["connectionReturnUrl"], "https://client.example/connected")
141
+ self.assertEqual(calls[0][2]["sessionId"], "session-one")
142
+
133
143
  def test_bot_management_uses_shared_routes_and_exact_customer_session(self):
134
144
  calls = []
135
145
  def transport(method, path, headers, body, query, timeout):
@@ -149,6 +159,37 @@ class StacklinkClientTests(unittest.TestCase):
149
159
  ("POST", "/v1/bots/bot-one/apis/api-one/signing-key", {"action": "activate", "keyId": "key-two"}),
150
160
  ])
151
161
 
162
+ def test_bot_files_preserve_customer_identity_for_sync_and_async_clients(self):
163
+ expected = [
164
+ ("POST", "/v1/bots/bot-one/files", {"sessionId": "customer-a", "name": "notes.sql", "mediaType": "text/plain", "sizeBytes": 42}, {}),
165
+ ("POST", "/v1/bots/bot-one/files/file-one/finalize", {"sessionId": "customer-a"}, {}),
166
+ ("GET", "/v1/bots/bot-one/files/file-one", None, {"sessionId": "customer-a"}),
167
+ ("POST", "/v1/bots/bot-one/files/file-one/link", {"sessionId": "customer-a"}, {}),
168
+ ]
169
+ sync_calls = []
170
+ def transport(method, path, headers, body, query, timeout):
171
+ sync_calls.append((method, path, body, query))
172
+ return 200, {}, {"ok": True}
173
+ client = Stacklink(api_key="test", base_url="https://stacklink.test", transport=transport)
174
+ client.bots.files.create_upload("bot-one", session_id="customer-a", name="notes.sql", media_type="text/plain", size_bytes=42)
175
+ client.bots.files.finalize("bot-one", "file-one", session_id="customer-a")
176
+ client.bots.files.get("bot-one", "file-one", session_id="customer-a")
177
+ client.bots.files.link("bot-one", "file-one", session_id="customer-a")
178
+ self.assertEqual(sync_calls, expected)
179
+ async_calls = []
180
+ async def handler(request):
181
+ async_calls.append((request.method, request.url.path, json.loads(request.content) if request.content else None, dict(request.url.params)))
182
+ return httpx.Response(200, json={"ok": True})
183
+ async def run():
184
+ async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as http:
185
+ client = AsyncStacklink(api_key="test", base_url="https://stacklink.test", http_client=http)
186
+ await client.bots.files.create_upload("bot-one", session_id="customer-a", name="notes.sql", media_type="text/plain", size_bytes=42)
187
+ await client.bots.files.finalize("bot-one", "file-one", session_id="customer-a")
188
+ await client.bots.files.get("bot-one", "file-one", session_id="customer-a")
189
+ await client.bots.files.link("bot-one", "file-one", session_id="customer-a")
190
+ asyncio.run(run())
191
+ self.assertEqual(async_calls, expected)
192
+
152
193
  def test_callback_signature_uses_original_body_and_expiry(self):
153
194
  import base64, hashlib, hmac
154
195
  from stacklink import verify_webhook_signature
File without changes
File without changes
File without changes