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.
- {stacklink_sdk-0.3.10/stacklink_sdk.egg-info → stacklink_sdk-0.3.14}/PKG-INFO +1 -1
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/pyproject.toml +1 -1
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/__init__.py +2 -1
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/bots.py +107 -2
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/client.py +1 -1
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14/stacklink_sdk.egg-info}/PKG-INFO +1 -1
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/tests/test_client.py +41 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/LICENSE +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/README.md +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/setup.cfg +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/api_keys.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/approvals.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/artifacts.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/auth_configs.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/billing.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/claude_agent.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/connected_accounts.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/errors.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/executions.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/knowledge.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/logs.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/mcp.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/memory.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/openai_agents.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/openai_memory.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/projects.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/providers.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/sessions.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/tools.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/triggers.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/types.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/webhooks.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/workbench.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink/x_chat.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink_sdk.egg-info/SOURCES.txt +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink_sdk.egg-info/dependency_links.txt +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink_sdk.egg-info/requires.txt +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/stacklink_sdk.egg-info/top_level.txt +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/tests/test_google_docs_provider.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/tests/test_google_drive_provider.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/tests/test_google_sheets_provider.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/tests/test_openai_memory.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.14}/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.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.
|
|
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
|
|
|
@@ -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
|
|
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
|