stacklink-sdk 0.2.0__py3-none-any.whl

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/__init__.py ADDED
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+
3
+ from .client import STACKLINK_PYTHON_SDK_VERSION, Stacklink
4
+ from .claude_agent import create_stacklink_claude_agent_tool_config
5
+ from .errors import RateLimitError, StacklinkError
6
+ from .openai_agents import create_stacklink_openai_agents_tools
7
+ from .openai_memory import StacklinkMemoryLifecycleError, with_stacklink_memory
8
+ from .types import (
9
+ ProjectProvider,
10
+ ProviderMarketplaceMetadata,
11
+ ProviderProfile,
12
+ ProviderProfileCapabilityGroup,
13
+ ProviderProfileCapabilityItem,
14
+ ProviderProfileDetails,
15
+ ProviderTool,
16
+ ProviderToolkit,
17
+ StacklinkBillingDescriptor,
18
+ ToolSchemasResponse,
19
+ ToolSearchResponse,
20
+ )
21
+
22
+ __all__ = [
23
+ "Stacklink",
24
+ "STACKLINK_PYTHON_SDK_VERSION",
25
+ "StacklinkError",
26
+ "RateLimitError",
27
+ "StacklinkMemoryLifecycleError",
28
+ "with_stacklink_memory",
29
+ "create_stacklink_openai_agents_tools",
30
+ "create_stacklink_claude_agent_tool_config",
31
+ "ProjectProvider",
32
+ "ProviderMarketplaceMetadata",
33
+ "ProviderProfile",
34
+ "ProviderProfileCapabilityGroup",
35
+ "ProviderProfileCapabilityItem",
36
+ "ProviderProfileDetails",
37
+ "ProviderTool",
38
+ "ProviderToolkit",
39
+ "StacklinkBillingDescriptor",
40
+ "ToolSearchResponse",
41
+ "ToolSchemasResponse",
42
+ ]
stacklink/api_keys.py ADDED
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
6
+ class ApiKeysResource:
7
+ def __init__(self, client):
8
+ self.client = client
9
+
10
+ def list(self):
11
+ return self.client.request("GET", "/v1/api-keys")
12
+
13
+ def create(self, body: dict[str, Any]):
14
+ return self.client.request("POST", "/v1/api-keys", json_body=body)
15
+
16
+ def revoke(self, api_key_id: str):
17
+ return self.client.request("DELETE", f"/v1/api-keys/{api_key_id}")
18
+
stacklink/artifacts.py ADDED
@@ -0,0 +1,9 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class ArtifactsResource:
5
+ def __init__(self, client):
6
+ self.client = client
7
+
8
+ def get(self, artifact_id: str):
9
+ return self.client.request("GET", f"/v1/artifacts/{artifact_id}")
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
6
+ class AuthConfigsResource:
7
+ def __init__(self, client):
8
+ self.client = client
9
+
10
+ def list(self, **query):
11
+ return self.client.request("GET", "/v1/auth-configs", query=query)
12
+
13
+ def create(self, body: dict[str, Any]):
14
+ return self.client.request("POST", "/v1/auth-configs", json_body=body)
15
+
16
+ def get(self, auth_config_id: str):
17
+ return self.client.request("GET", f"/v1/auth-configs/{auth_config_id}")
18
+
19
+ def update(self, auth_config_id: str, body: dict[str, Any]):
20
+ return self.client.request("PATCH", f"/v1/auth-configs/{auth_config_id}", json_body=body)
21
+
22
+ def enable(self, auth_config_id: str):
23
+ return self.client.request("POST", f"/v1/auth-configs/{auth_config_id}/enable")
24
+
25
+ def disable(self, auth_config_id: str):
26
+ return self.client.request("POST", f"/v1/auth-configs/{auth_config_id}/disable")
27
+
28
+ def delete(self, auth_config_id: str):
29
+ return self.client.request("DELETE", f"/v1/auth-configs/{auth_config_id}")
stacklink/billing.py ADDED
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
6
+ class BillingResource:
7
+ def __init__(self, client):
8
+ self.client = client
9
+
10
+ def plans(self):
11
+ return self.client.request("GET", "/v1/billing/plans")
12
+
13
+ def rate_card(self, **query):
14
+ return self.client.request("GET", "/v1/billing/rate-card", query=query)
15
+
16
+ def subscription(self):
17
+ return self.client.request("GET", "/v1/subscription")
18
+
19
+ def usage(self, **query):
20
+ return self.client.request("GET", "/v1/usage", query=query)
21
+
22
+ def usage_events(self, **query):
23
+ return self.client.request("GET", "/v1/usage/events", query=query)
24
+
25
+ def create_checkout(self, plan_key: str, success_url: str | None = None, cancel_url: str | None = None):
26
+ body: dict[str, Any] = {"planKey": plan_key}
27
+ if success_url is not None:
28
+ body["successUrl"] = success_url
29
+ if cancel_url is not None:
30
+ body["cancelUrl"] = cancel_url
31
+ return self.client.request("POST", "/v1/billing/checkout", json_body=body)
32
+
33
+ def create_portal(self, return_url: str | None = None):
34
+ body: dict[str, Any] = {}
35
+ if return_url is not None:
36
+ body["returnUrl"] = return_url
37
+ return self.client.request("POST", "/v1/billing/portal", json_body=body)
38
+
39
+ def create_overage_invoice_line(self):
40
+ return self.client.request("POST", "/v1/billing/overage-invoice-line")
41
+
42
+ def set_workspace_plan_for_project(self, project_id: str, body: dict[str, Any]):
43
+ return self.client.request("POST", f"/v1/projects/{project_id}/subscription/plan", json_body=body)
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, TypedDict
4
+
5
+ if TYPE_CHECKING:
6
+ from .sessions import Session
7
+
8
+
9
+ class StacklinkClaudeAgentToolConfig(TypedDict):
10
+ mcp_servers: dict[str, dict[str, Any]]
11
+ allowed_tools: list[str]
12
+
13
+
14
+ def create_stacklink_claude_agent_tool_config(
15
+ session: "Session",
16
+ ) -> StacklinkClaudeAgentToolConfig:
17
+ connection = session.mcp_connection()
18
+ return {
19
+ "mcp_servers": {
20
+ "stacklink": {
21
+ "type": "http",
22
+ "url": connection["url"],
23
+ "headers": dict(connection["headers"]),
24
+ }
25
+ },
26
+ "allowed_tools": [
27
+ f"mcp__stacklink__{tool.name}" for tool in session.tools().values()
28
+ ],
29
+ }
stacklink/client.py ADDED
@@ -0,0 +1,292 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import time
6
+ from typing import Any, Iterator, Mapping
7
+ from urllib.error import HTTPError
8
+ from urllib.parse import urlencode
9
+ from urllib.request import Request, urlopen
10
+
11
+ from .api_keys import ApiKeysResource
12
+ from .artifacts import ArtifactsResource
13
+ from .auth_configs import AuthConfigsResource
14
+ from .billing import BillingResource
15
+ from .connected_accounts import ConnectedAccountsResource
16
+ from .errors import RateLimitError, StacklinkError
17
+ from .executions import ExecutionsResource
18
+ from .knowledge import KnowledgeResource
19
+ from .logs import LogsResource
20
+ from .memory import MemoryResource
21
+ from .mcp import McpResource
22
+ from .projects import ProjectsResource
23
+ from .sessions import SessionsResource
24
+ from .tools import ToolsResource
25
+ from .triggers import TriggersResource
26
+ from .types import Json, Transport
27
+ from .webhooks import WebhooksResource
28
+ from .workbench import WorkbenchResource
29
+
30
+ STACKLINK_PYTHON_SDK_VERSION = "0.2.0"
31
+ STACKLINK_PYTHON_USER_AGENT = f"stacklink-python/{STACKLINK_PYTHON_SDK_VERSION}"
32
+
33
+
34
+ class Stacklink:
35
+ def __init__(
36
+ self,
37
+ api_key: str | None = None,
38
+ registration_key: str | None = None,
39
+ base_url: str | None = None,
40
+ timeout_ms: int | None = None,
41
+ transport: Transport | None = None,
42
+ cache_enabled: bool | None = None,
43
+ cache_ttl_seconds: int | None = None,
44
+ ):
45
+ self.api_key = api_key or os.getenv("STACKLINK_API_KEY")
46
+ if not self.api_key:
47
+ raise StacklinkError("STACKLINK_API_KEY is required.")
48
+ self.registration_key = registration_key or os.getenv("STACKLINK_REGISTRATION_KEY")
49
+ self.base_url = (base_url or os.getenv("STACKLINK_BASE_URL") or "http://localhost:3000").rstrip("/")
50
+ self.timeout = (timeout_ms or int(os.getenv("STACKLINK_TIMEOUT_MS", "30000"))) / 1000
51
+ self._transport = transport
52
+ self.cache_enabled = cache_enabled if cache_enabled is not None else True
53
+ self.cache_ttl_seconds = cache_ttl_seconds or int(os.getenv("STACKLINK_CACHE_TTL_SECONDS", "120"))
54
+ self._cache: dict[str, tuple[float, Json]] = {}
55
+ self.sessions = SessionsResource(self)
56
+ self.tools = ToolsResource(self)
57
+ self.mcp = McpResource(self)
58
+ self.executions = ExecutionsResource(self)
59
+ self.workbench = WorkbenchResource(self)
60
+ self.auth_configs = AuthConfigsResource(self)
61
+ self.connected_accounts = ConnectedAccountsResource(self)
62
+ self.triggers = TriggersResource(self)
63
+ self.webhooks = WebhooksResource(self)
64
+ self.logs = LogsResource(self)
65
+ self.memory = MemoryResource(self)
66
+ self.knowledge = KnowledgeResource(self)
67
+ self.billing = BillingResource(self)
68
+ self.api_keys = ApiKeysResource(self)
69
+ self.projects = ProjectsResource(self)
70
+ self.artifacts = ArtifactsResource(self)
71
+
72
+ def clear_cache(self, prefix: str | None = None) -> None:
73
+ if prefix is None:
74
+ self._cache.clear()
75
+ return
76
+ for key in list(self._cache.keys()):
77
+ if key.startswith(prefix):
78
+ del self._cache[key]
79
+
80
+ def cached_request(
81
+ self,
82
+ cache_key: str,
83
+ method: str,
84
+ path: str,
85
+ *,
86
+ json_body: Json | None = None,
87
+ query: Mapping[str, Any] | None = None,
88
+ ttl_seconds: int | None = None,
89
+ bypass_cache: bool = False,
90
+ ) -> Json:
91
+ if not self.cache_enabled or bypass_cache:
92
+ return self.request(method, path, json_body=json_body, query=query)
93
+ now = time.time()
94
+ cached = self._cache.get(cache_key)
95
+ if cached and cached[0] > now:
96
+ return cached[1]
97
+ value = self.request(method, path, json_body=json_body, query=query)
98
+ self._cache[cache_key] = (now + (ttl_seconds or self.cache_ttl_seconds), value)
99
+ return value
100
+
101
+ def request(
102
+ self,
103
+ method: str,
104
+ path: str,
105
+ *,
106
+ json_body: Json | None = None,
107
+ query: Mapping[str, Any] | None = None,
108
+ headers: Mapping[str, str] | None = None,
109
+ registration: bool = False,
110
+ ) -> Json:
111
+ if registration:
112
+ if not self.registration_key:
113
+ raise StacklinkError("STACKLINK_REGISTRATION_KEY is required for registration/admin SDK methods.")
114
+ request_headers = {
115
+ "x-stacklink-registration-key": self.registration_key,
116
+ "content-type": "application/json",
117
+ **(headers or {}),
118
+ "user-agent": STACKLINK_PYTHON_USER_AGENT,
119
+ "x-stacklink-client-channel": "python_sdk",
120
+ "x-stacklink-client-name": "stacklink-python",
121
+ "x-stacklink-client-version": STACKLINK_PYTHON_SDK_VERSION,
122
+ }
123
+ else:
124
+ request_headers = {
125
+ "authorization": f"Bearer {self.api_key}",
126
+ "content-type": "application/json",
127
+ **(headers or {}),
128
+ "user-agent": STACKLINK_PYTHON_USER_AGENT,
129
+ "x-stacklink-client-channel": "python_sdk",
130
+ "x-stacklink-client-name": "stacklink-python",
131
+ "x-stacklink-client-version": STACKLINK_PYTHON_SDK_VERSION,
132
+ }
133
+ query_values = {key: value for key, value in (query or {}).items() if value is not None}
134
+ if self._transport:
135
+ status, response_headers, body = self._transport(method, path, request_headers, json_body, query_values, self.timeout)
136
+ return self._handle_response(status, response_headers, body)
137
+
138
+ url = f"{self.base_url}{path}"
139
+ if query_values:
140
+ url = f"{url}?{urlencode(query_values, doseq=True)}"
141
+ data = None if json_body is None else json.dumps(json_body).encode("utf-8")
142
+ request = Request(url, data=data, method=method, headers=request_headers)
143
+ try:
144
+ with urlopen(request, timeout=self.timeout) as response:
145
+ raw = response.read().decode("utf-8")
146
+ body = json.loads(raw) if raw else None
147
+ return self._handle_response(response.status, dict(response.headers.items()), body)
148
+ except HTTPError as error:
149
+ raw = error.read().decode("utf-8")
150
+ try:
151
+ body = json.loads(raw) if raw else {}
152
+ except json.JSONDecodeError:
153
+ body = {"message": raw}
154
+ return self._handle_response(error.code, dict(error.headers.items()), body)
155
+
156
+ def request_raw(
157
+ self,
158
+ method: str,
159
+ path: str,
160
+ *,
161
+ query: Mapping[str, Any] | None = None,
162
+ headers: Mapping[str, str] | None = None,
163
+ ) -> bytes:
164
+ request_headers = {
165
+ "authorization": f"Bearer {self.api_key}",
166
+ **(headers or {}),
167
+ "x-stacklink-client-channel": "python_sdk",
168
+ "x-stacklink-client-name": "stacklink-python",
169
+ "x-stacklink-client-version": STACKLINK_PYTHON_SDK_VERSION,
170
+ }
171
+ query_values = {key: value for key, value in (query or {}).items() if value is not None}
172
+ if self._transport:
173
+ status, response_headers, body = self._transport(method, path, request_headers, None, query_values, self.timeout)
174
+ if status >= 400:
175
+ self._handle_response(status, response_headers, body)
176
+ if isinstance(body, bytes):
177
+ return body
178
+ if isinstance(body, str):
179
+ return body.encode("utf-8")
180
+ return json.dumps(body).encode("utf-8")
181
+
182
+ url = f"{self.base_url}{path}"
183
+ if query_values:
184
+ url = f"{url}?{urlencode(query_values, doseq=True)}"
185
+ request = Request(url, method=method, headers=request_headers)
186
+ try:
187
+ with urlopen(request, timeout=self.timeout) as response:
188
+ return response.read()
189
+ except HTTPError as error:
190
+ raw = error.read().decode("utf-8")
191
+ try:
192
+ body = json.loads(raw) if raw else {}
193
+ except json.JSONDecodeError:
194
+ body = {"message": raw}
195
+ self._handle_response(error.code, dict(error.headers.items()), body)
196
+ return b""
197
+
198
+ def request_sse(
199
+ self,
200
+ method: str,
201
+ path: str,
202
+ *,
203
+ query: Mapping[str, Any] | None = None,
204
+ headers: Mapping[str, str] | None = None,
205
+ ) -> Iterator[dict[str, Any]]:
206
+ request_headers = {
207
+ "authorization": f"Bearer {self.api_key}",
208
+ "accept": "text/event-stream",
209
+ **(headers or {}),
210
+ }
211
+ query_values = {key: value for key, value in (query or {}).items() if value is not None}
212
+ if self._transport:
213
+ status, response_headers, body = self._transport(method, path, request_headers, None, query_values, self.timeout)
214
+ if status >= 400:
215
+ self._handle_response(status, response_headers, body)
216
+ if isinstance(body, str):
217
+ yield from self._parse_sse_lines(body.splitlines())
218
+ elif isinstance(body, list):
219
+ for event in body:
220
+ if isinstance(event, dict):
221
+ yield event
222
+ return
223
+
224
+ url = f"{self.base_url}{path}"
225
+ if query_values:
226
+ url = f"{url}?{urlencode(query_values, doseq=True)}"
227
+ request = Request(url, method=method, headers=request_headers)
228
+ try:
229
+ with urlopen(request, timeout=self.timeout) as response:
230
+ yield from self._parse_sse_lines(line.decode("utf-8") for line in response)
231
+ except HTTPError as error:
232
+ raw = error.read().decode("utf-8")
233
+ try:
234
+ body = json.loads(raw) if raw else {}
235
+ except json.JSONDecodeError:
236
+ body = {"message": raw}
237
+ self._handle_response(error.code, dict(error.headers.items()), body)
238
+
239
+ def _handle_response(self, status: int, headers: Mapping[str, str], body: Json) -> Json:
240
+ if status < 400:
241
+ return body
242
+ message = body.get("message") if isinstance(body, dict) else None
243
+ code = body.get("code") if isinstance(body, dict) else None
244
+ details = body.get("details") if isinstance(body, dict) else None
245
+ if status == 429:
246
+ raise RateLimitError(message or "Stacklink rate limit exceeded.", status_code=status, code=code, details=details, headers=headers)
247
+ raise StacklinkError(message or "Stacklink request failed.", status_code=status, code=code, details=details)
248
+
249
+ def _parse_sse_lines(self, lines) -> Iterator[dict[str, Any]]:
250
+ event_id = None
251
+ event_name = None
252
+ data_lines: list[str] = []
253
+
254
+ def emit_event():
255
+ if not data_lines:
256
+ return None
257
+ data_text = "\n".join(data_lines)
258
+ try:
259
+ data: Any = json.loads(data_text)
260
+ except json.JSONDecodeError:
261
+ data = data_text
262
+ return {
263
+ "id": event_id,
264
+ "event": event_name,
265
+ "data": data,
266
+ }
267
+
268
+ for raw_line in lines:
269
+ line = str(raw_line).rstrip("\r\n")
270
+ if line == "":
271
+ event = emit_event()
272
+ if event is not None:
273
+ yield event
274
+ event_id = None
275
+ event_name = None
276
+ data_lines = []
277
+ continue
278
+ if line.startswith(":"):
279
+ continue
280
+ field, separator, value = line.partition(":")
281
+ if separator and value.startswith(" "):
282
+ value = value[1:]
283
+ if field == "id":
284
+ event_id = value
285
+ elif field == "event":
286
+ event_name = value
287
+ elif field == "data":
288
+ data_lines.append(value)
289
+
290
+ event = emit_event()
291
+ if event is not None:
292
+ yield event
@@ -0,0 +1,95 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from typing import Any
5
+
6
+
7
+ class ConnectedAccountsResource:
8
+ def __init__(self, client):
9
+ self.client = client
10
+
11
+ def list(self, **query):
12
+ return self.client.request("GET", "/v1/connections", query=query)
13
+
14
+ def get(self, account_id: str):
15
+ return self.client.request("GET", f"/v1/connections/{account_id}")
16
+
17
+ def list_tools(
18
+ self,
19
+ account_id: str,
20
+ *,
21
+ include_planned: bool | None = None,
22
+ tool_keys: list[str] | None = None,
23
+ ):
24
+ query: dict[str, Any] = {}
25
+ if include_planned is not None:
26
+ query["includePlanned"] = str(include_planned).lower()
27
+ if tool_keys:
28
+ query["toolKeys"] = tool_keys
29
+ return self.client.request("GET", f"/v1/connections/{account_id}/tools", query=query)
30
+
31
+ def list_resources(self, account_id: str):
32
+ return self.client.request("GET", f"/v1/connections/{account_id}/resources")
33
+
34
+ def refresh_resources(self, account_id: str):
35
+ return self.client.request("POST", f"/v1/connections/{account_id}/resources/refresh")
36
+
37
+ def resolve_resource(self, account_id: str, *, resource_key: str, external_resource_id: str):
38
+ return self.client.request(
39
+ "POST",
40
+ f"/v1/connections/{account_id}/resources/resolve",
41
+ json_body={
42
+ "resourceKey": resource_key,
43
+ "externalResourceId": external_resource_id,
44
+ },
45
+ )
46
+
47
+ def update(self, account_id: str, body: dict[str, Any]):
48
+ return self.client.request("PATCH", f"/v1/connections/{account_id}", json_body=body)
49
+
50
+ def enable(self, account_id: str):
51
+ return self.client.request("POST", f"/v1/connections/{account_id}/enable")
52
+
53
+ def disable(self, account_id: str):
54
+ return self.client.request("POST", f"/v1/connections/{account_id}/disable")
55
+
56
+ def refresh(self, account_id: str):
57
+ return self.client.request("POST", f"/v1/connections/{account_id}/refresh")
58
+
59
+ def delete(self, account_id: str):
60
+ return self.client.request("DELETE", f"/v1/connections/{account_id}")
61
+
62
+ def validate(self, account_id: str):
63
+ return self.client.request("POST", f"/v1/connections/{account_id}/validate")
64
+
65
+ def inspect_connect_token(self, public_token: str):
66
+ return self.client.request("GET", f"/v1/connect/{public_token}")
67
+
68
+ def complete_connect_token(self, public_token: str, body: dict[str, Any]):
69
+ return self.client.request("POST", f"/v1/connect/{public_token}/complete", json_body=body)
70
+
71
+ def get_connection_request(self, request_id: str):
72
+ return self.client.request("GET", f"/v1/connection-requests/{request_id}")
73
+
74
+ def wait_for_connections(self, request_ids: list[str], timeout_seconds: float = 120, interval_seconds: float = 2):
75
+ started_at = time.monotonic()
76
+ while time.monotonic() - started_at <= timeout_seconds:
77
+ connection_requests = [
78
+ self.get_connection_request(request_id).get("connectionRequest")
79
+ for request_id in request_ids
80
+ ]
81
+ statuses = [request.get("status") for request in connection_requests if isinstance(request, dict)]
82
+ if len(statuses) == len(request_ids) and all(status == "completed" for status in statuses):
83
+ return {"status": "completed", "connectionRequests": connection_requests}
84
+ terminal = next(
85
+ (
86
+ request
87
+ for request in connection_requests
88
+ if isinstance(request, dict) and request.get("status") in {"failed", "expired", "cancelled"}
89
+ ),
90
+ None,
91
+ )
92
+ if isinstance(terminal, dict):
93
+ raise RuntimeError(f"Stacklink connection ended with status {terminal.get('status')}.")
94
+ time.sleep(interval_seconds)
95
+ raise TimeoutError("Timed out waiting for Stacklink connections.")
stacklink/errors.py ADDED
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ class StacklinkError(Exception):
4
+ def __init__(self, message: str, *, status_code: int | None = None, code: str | None = None, details=None):
5
+ super().__init__(message)
6
+ self.status_code = status_code
7
+ self.code = code
8
+ self.details = details
9
+
10
+
11
+ class RateLimitError(StacklinkError):
12
+ def __init__(self, message: str, *, status_code: int = 429, code: str | None = "rate_limited", details=None, headers=None):
13
+ super().__init__(message, status_code=status_code, code=code, details=details)
14
+ self.headers = headers or {}
15
+
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, TypedDict, cast
4
+
5
+
6
+ class ExecutionListPage(TypedDict):
7
+ executions: list[dict[str, Any]]
8
+ limit: int
9
+ nextCursor: str | None
10
+ liveCount: int
11
+
12
+
13
+ class ExecutionEventsPage(TypedDict):
14
+ events: list[dict[str, Any]]
15
+ limit: int
16
+ hasMore: bool
17
+ nextCursor: str | None
18
+
19
+
20
+ class ExecutionsResource:
21
+ def __init__(self, client):
22
+ self.client = client
23
+
24
+ def list(self, session_id: str, **query) -> ExecutionListPage:
25
+ return cast(ExecutionListPage, self.client.request(
26
+ "GET",
27
+ "/v1/executions",
28
+ query={"sessionId": session_id, **query},
29
+ ))
30
+
31
+ def get(self, session_id: str, execution_kind: str, execution_id: str, **query):
32
+ return self.client.request("GET", f"/v1/executions/{execution_kind}/{execution_id}", query={"sessionId": session_id, **query})
33
+
34
+ def events(self, session_id: str, execution_kind: str, execution_id: str, **query) -> ExecutionEventsPage:
35
+ return cast(ExecutionEventsPage, self.client.request(
36
+ "GET",
37
+ f"/v1/executions/{execution_kind}/{execution_id}/events",
38
+ query={"sessionId": session_id, **query},
39
+ ))
40
+
41
+ def wait(self, session_id: str, execution_kind: str, execution_id: str, body: dict[str, Any] | None = None):
42
+ return self.client.request("POST", f"/v1/executions/{execution_kind}/{execution_id}/wait", json_body={"sessionId": session_id, **(body or {})})
43
+
44
+ def cancel(self, session_id: str, execution_kind: str, execution_id: str, reason: str | None = None):
45
+ return self.client.request("POST", f"/v1/executions/{execution_kind}/{execution_id}/cancel", json_body={"sessionId": session_id, "reason": reason})
stacklink/knowledge.py ADDED
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
6
+ class KnowledgeResource:
7
+ def __init__(self, client):
8
+ self.client = client
9
+
10
+ def packs(self, **query):
11
+ return self.client.request("GET", "/v1/knowledge/packs", query=query)
12
+
13
+ def capabilities(self, **query):
14
+ return self.client.request("GET", "/v1/knowledge/capabilities", query=query)
15
+
16
+ def search(self, body: dict[str, Any]):
17
+ return self.client.request("POST", "/v1/knowledge/search", json_body=body)
18
+
19
+ def lookup_citation(self, body: dict[str, Any]):
20
+ return self.client.request("POST", "/v1/knowledge/citations/lookup", json_body=body)
21
+
22
+ def verify_answer(self, body: dict[str, Any]):
23
+ return self.client.request("POST", "/v1/knowledge/answers/verify", json_body=body)
24
+
stacklink/logs.py ADDED
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ class LogsResource:
4
+ def __init__(self, client):
5
+ self.client = client
6
+
7
+ def query(self, **query):
8
+ return self.requests(**query)
9
+
10
+ def summary(self, **query):
11
+ return self.client.request("GET", "/v1/logs/summary", query=query)
12
+
13
+ def requests(self, **query):
14
+ return self.client.request("GET", "/v1/logs/requests", query=query)
15
+
16
+ def tools(self, **query):
17
+ return self.client.request("GET", "/v1/logs/tools", query=query)
18
+
19
+ def triggers(self, **query):
20
+ return self.client.request("GET", "/v1/logs/triggers", query=query)
21
+
22
+ def webhook_deliveries(self, **query):
23
+ return self.client.request("GET", "/v1/logs/webhook-deliveries", query=query)
24
+
25
+ def request_log(self, log_id: str):
26
+ return self.client.request("GET", f"/v1/logs/requests/{log_id}")
27
+
28
+ def tool_log(self, log_id: str):
29
+ return self.client.request("GET", f"/v1/logs/tools/{log_id}")