stacklink-sdk 0.3.10__tar.gz → 0.3.13__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.13}/PKG-INFO +1 -1
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.13}/pyproject.toml +1 -1
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.13}/stacklink/__init__.py +2 -1
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.13}/stacklink/bots.py +59 -2
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.13}/stacklink/client.py +1 -1
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.13/stacklink_sdk.egg-info}/PKG-INFO +1 -1
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.13}/tests/test_client.py +10 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.13}/LICENSE +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.13}/README.md +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.13}/setup.cfg +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.13}/stacklink/api_keys.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.13}/stacklink/approvals.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.13}/stacklink/artifacts.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.13}/stacklink/auth_configs.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.13}/stacklink/billing.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.13}/stacklink/claude_agent.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.13}/stacklink/connected_accounts.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.13}/stacklink/errors.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.13}/stacklink/executions.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.13}/stacklink/knowledge.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.13}/stacklink/logs.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.13}/stacklink/mcp.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.13}/stacklink/memory.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.13}/stacklink/openai_agents.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.13}/stacklink/openai_memory.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.13}/stacklink/projects.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.13}/stacklink/providers.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.13}/stacklink/sessions.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.13}/stacklink/tools.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.13}/stacklink/triggers.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.13}/stacklink/types.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.13}/stacklink/webhooks.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.13}/stacklink/workbench.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.13}/stacklink/x_chat.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.13}/stacklink_sdk.egg-info/SOURCES.txt +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.13}/stacklink_sdk.egg-info/dependency_links.txt +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.13}/stacklink_sdk.egg-info/requires.txt +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.13}/stacklink_sdk.egg-info/top_level.txt +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.13}/tests/test_google_docs_provider.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.13}/tests/test_google_drive_provider.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.13}/tests/test_google_sheets_provider.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.13}/tests/test_openai_memory.py +0 -0
- {stacklink_sdk-0.3.10 → stacklink_sdk-0.3.13}/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.13"
|
|
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,61 @@ 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 BotRun(TypedDict, total=False):
|
|
58
|
+
id: str
|
|
59
|
+
botId: str
|
|
60
|
+
conversationId: str
|
|
61
|
+
status: str
|
|
62
|
+
finalText: str | None
|
|
63
|
+
output: Any
|
|
64
|
+
error: Any
|
|
65
|
+
waitingOperation: BotConnectionAction | BotExecutionWait | None
|
|
66
|
+
connection: BotConnectionAction | None
|
|
67
|
+
capabilities: list[BotCapabilityFact]
|
|
68
|
+
requirements: list[BotRequirement]
|
|
69
|
+
usage: dict[str, Any]
|
|
70
|
+
display: dict[str, Any]
|
|
71
|
+
|
|
72
|
+
|
|
18
73
|
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
74
|
"""Sign the exact JSON bytes. Reuse request_id when retrying the same event."""
|
|
20
75
|
timestamp = str(int(time.time() if now is None else now))
|
|
@@ -132,10 +187,12 @@ class BotsResource:
|
|
|
132
187
|
def publish(self, bot_id: str, revision: int, channels: dict[str, bool]):
|
|
133
188
|
return self.client.request("POST", self._path(bot_id) + "/publish", json_body={"revision": revision, "channels": channels})
|
|
134
189
|
|
|
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):
|
|
190
|
+
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):
|
|
136
191
|
body: dict[str, Any] = {"sessionId": session_id, "message": message, "variables": variables or {}, "runId": run_id or str(uuid4())}
|
|
137
192
|
if conversation_id:
|
|
138
193
|
body["conversationId"] = conversation_id
|
|
194
|
+
if connection_return_url is not None:
|
|
195
|
+
body["connectionReturnUrl"] = connection_return_url
|
|
139
196
|
return self.client.request("POST", self._path(bot_id) + "/runs", json_body=body)
|
|
140
197
|
|
|
141
198
|
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.13"
|
|
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):
|
|
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
|