straleio 0.1.0__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.
- straleio-0.1.0/PKG-INFO +20 -0
- straleio-0.1.0/pyproject.toml +31 -0
- straleio-0.1.0/setup.cfg +4 -0
- straleio-0.1.0/straleio/__init__.py +52 -0
- straleio-0.1.0/straleio/client.py +314 -0
- straleio-0.1.0/straleio/errors.py +95 -0
- straleio-0.1.0/straleio/py.typed +0 -0
- straleio-0.1.0/straleio/types.py +118 -0
- straleio-0.1.0/straleio.egg-info/PKG-INFO +20 -0
- straleio-0.1.0/straleio.egg-info/SOURCES.txt +11 -0
- straleio-0.1.0/straleio.egg-info/dependency_links.txt +1 -0
- straleio-0.1.0/straleio.egg-info/requires.txt +1 -0
- straleio-0.1.0/straleio.egg-info/top_level.txt +1 -0
straleio-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: straleio
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the Strale API — let AI agents buy capabilities at runtime
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://strale.io
|
|
7
|
+
Project-URL: Repository, https://github.com/petter_lindstrom/strale
|
|
8
|
+
Keywords: strale,ai,agent,capabilities,api,sdk
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: httpx>=0.24.0
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "straleio"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python SDK for the Strale API — let AI agents buy capabilities at runtime"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
dependencies = ["httpx>=0.24.0"]
|
|
13
|
+
keywords = ["strale", "ai", "agent", "capabilities", "api", "sdk"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.9",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Topic :: Software Development :: Libraries",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://strale.io"
|
|
28
|
+
Repository = "https://github.com/petter_lindstrom/strale"
|
|
29
|
+
|
|
30
|
+
[tool.setuptools.packages.find]
|
|
31
|
+
include = ["straleio*"]
|
straleio-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Strale Python SDK — let AI agents buy capabilities at runtime."""
|
|
2
|
+
|
|
3
|
+
from .client import Strale
|
|
4
|
+
from .errors import (
|
|
5
|
+
CapabilityUnavailableError,
|
|
6
|
+
ExecutionFailedError,
|
|
7
|
+
InsufficientBalanceError,
|
|
8
|
+
InvalidRequestError,
|
|
9
|
+
NoMatchingCapabilityError,
|
|
10
|
+
NotFoundError,
|
|
11
|
+
RateLimitedError,
|
|
12
|
+
StraleError,
|
|
13
|
+
TimeoutExceededError,
|
|
14
|
+
UnauthorizedError,
|
|
15
|
+
)
|
|
16
|
+
from .types import (
|
|
17
|
+
BalanceResponse,
|
|
18
|
+
Capability,
|
|
19
|
+
DoRequest,
|
|
20
|
+
DoResponse,
|
|
21
|
+
DryRunResponse,
|
|
22
|
+
Provenance,
|
|
23
|
+
Transaction,
|
|
24
|
+
TransactionDetail,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
# Client
|
|
29
|
+
"Strale",
|
|
30
|
+
# Types
|
|
31
|
+
"DoRequest",
|
|
32
|
+
"DoResponse",
|
|
33
|
+
"DryRunResponse",
|
|
34
|
+
"Provenance",
|
|
35
|
+
"Capability",
|
|
36
|
+
"BalanceResponse",
|
|
37
|
+
"Transaction",
|
|
38
|
+
"TransactionDetail",
|
|
39
|
+
# Errors
|
|
40
|
+
"StraleError",
|
|
41
|
+
"InsufficientBalanceError",
|
|
42
|
+
"NoMatchingCapabilityError",
|
|
43
|
+
"CapabilityUnavailableError",
|
|
44
|
+
"ExecutionFailedError",
|
|
45
|
+
"TimeoutExceededError",
|
|
46
|
+
"InvalidRequestError",
|
|
47
|
+
"RateLimitedError",
|
|
48
|
+
"UnauthorizedError",
|
|
49
|
+
"NotFoundError",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"""Strale Python SDK client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import time
|
|
7
|
+
from typing import Any, Dict, List, Optional, Union
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from .errors import StraleError, create_error
|
|
12
|
+
from .types import (
|
|
13
|
+
BalanceResponse,
|
|
14
|
+
Capability,
|
|
15
|
+
DoRequest,
|
|
16
|
+
DoResponse,
|
|
17
|
+
DryRunResponse,
|
|
18
|
+
Provenance,
|
|
19
|
+
Transaction,
|
|
20
|
+
TransactionDetail,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
DEFAULT_BASE_URL = "https://strale-production.up.railway.app"
|
|
24
|
+
DEFAULT_TIMEOUT = 60.0
|
|
25
|
+
DEFAULT_POLL_INTERVAL = 2.0
|
|
26
|
+
DEFAULT_MAX_POLL_WAIT = 120.0
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Strale:
|
|
30
|
+
"""Strale API client.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
api_key: API key (starts with sk_).
|
|
34
|
+
base_url: Base URL of the Strale API.
|
|
35
|
+
default_max_price_cents: Default max_price_cents for do() calls.
|
|
36
|
+
timeout: HTTP request timeout in seconds. Defaults to 60.
|
|
37
|
+
poll_interval: Poll interval in seconds for async responses. Defaults to 2.
|
|
38
|
+
max_poll_wait: Max wait in seconds for async polling. Defaults to 120.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
api_key: str,
|
|
44
|
+
*,
|
|
45
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
46
|
+
default_max_price_cents: Optional[int] = None,
|
|
47
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
48
|
+
poll_interval: float = DEFAULT_POLL_INTERVAL,
|
|
49
|
+
max_poll_wait: float = DEFAULT_MAX_POLL_WAIT,
|
|
50
|
+
):
|
|
51
|
+
if not api_key:
|
|
52
|
+
raise ValueError("api_key is required")
|
|
53
|
+
self._api_key = api_key
|
|
54
|
+
self._base_url = base_url.rstrip("/")
|
|
55
|
+
self._default_max_price_cents = default_max_price_cents
|
|
56
|
+
self._timeout = timeout
|
|
57
|
+
self._poll_interval = poll_interval
|
|
58
|
+
self._max_poll_wait = max_poll_wait
|
|
59
|
+
self._client = httpx.Client(
|
|
60
|
+
base_url=self._base_url,
|
|
61
|
+
timeout=self._timeout,
|
|
62
|
+
headers={"Authorization": f"Bearer {self._api_key}"},
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def close(self) -> None:
|
|
66
|
+
"""Close the underlying HTTP client."""
|
|
67
|
+
self._client.close()
|
|
68
|
+
|
|
69
|
+
def __enter__(self) -> "Strale":
|
|
70
|
+
return self
|
|
71
|
+
|
|
72
|
+
def __exit__(self, *args: Any) -> None:
|
|
73
|
+
self.close()
|
|
74
|
+
|
|
75
|
+
# ─── Core: execute a capability ────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
def do(self, request: DoRequest) -> Union[DoResponse, DryRunResponse]:
|
|
78
|
+
"""Execute a capability.
|
|
79
|
+
|
|
80
|
+
If the server returns status "executing" (async), auto-polls until
|
|
81
|
+
completed or failed.
|
|
82
|
+
"""
|
|
83
|
+
body: Dict[str, Any] = {
|
|
84
|
+
"max_price_cents": request.max_price_cents
|
|
85
|
+
or self._default_max_price_cents,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if request.task is not None:
|
|
89
|
+
body["task"] = request.task
|
|
90
|
+
if request.capability_slug is not None:
|
|
91
|
+
body["capability_slug"] = request.capability_slug
|
|
92
|
+
if request.inputs is not None:
|
|
93
|
+
body["inputs"] = request.inputs
|
|
94
|
+
if request.timeout_seconds is not None:
|
|
95
|
+
body["timeout_seconds"] = request.timeout_seconds
|
|
96
|
+
if request.dry_run:
|
|
97
|
+
body["dry_run"] = True
|
|
98
|
+
|
|
99
|
+
headers: Dict[str, str] = {}
|
|
100
|
+
if request.idempotency_key:
|
|
101
|
+
headers["Idempotency-Key"] = request.idempotency_key
|
|
102
|
+
|
|
103
|
+
data = self._request("POST", "/v1/do", json=body, extra_headers=headers)
|
|
104
|
+
|
|
105
|
+
# Dry run returns immediately
|
|
106
|
+
if data.get("dry_run"):
|
|
107
|
+
return DryRunResponse(
|
|
108
|
+
dry_run=True,
|
|
109
|
+
would_execute=data["would_execute"],
|
|
110
|
+
price_cents=data["price_cents"],
|
|
111
|
+
wallet_balance_cents=data["wallet_balance_cents"],
|
|
112
|
+
wallet_sufficient=data["wallet_sufficient"],
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
response = self._parse_do_response(data)
|
|
116
|
+
|
|
117
|
+
# If async (status: "executing"), auto-poll until terminal state
|
|
118
|
+
if response.status == "executing":
|
|
119
|
+
return self._poll_transaction(response)
|
|
120
|
+
|
|
121
|
+
return response
|
|
122
|
+
|
|
123
|
+
# ─── Capabilities ──────────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
def capabilities(self) -> List[Capability]:
|
|
126
|
+
"""List all available capabilities."""
|
|
127
|
+
data = self._request("GET", "/v1/capabilities")
|
|
128
|
+
return [self._parse_capability(c) for c in data["capabilities"]]
|
|
129
|
+
|
|
130
|
+
def capability(self, slug: str) -> Capability:
|
|
131
|
+
"""Get details for a specific capability."""
|
|
132
|
+
data = self._request("GET", f"/v1/capabilities/{slug}")
|
|
133
|
+
return self._parse_capability(data)
|
|
134
|
+
|
|
135
|
+
# ─── Wallet ────────────────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
def balance(self) -> BalanceResponse:
|
|
138
|
+
"""Get current wallet balance."""
|
|
139
|
+
data = self._request("GET", "/v1/wallet/balance")
|
|
140
|
+
return BalanceResponse(
|
|
141
|
+
balance_cents=data["balance_cents"],
|
|
142
|
+
currency=data["currency"],
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# ─── Transactions ──────────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
def transactions(self) -> List[Transaction]:
|
|
148
|
+
"""List recent transactions."""
|
|
149
|
+
data = self._request("GET", "/v1/transactions")
|
|
150
|
+
return [
|
|
151
|
+
Transaction(
|
|
152
|
+
id=t["id"],
|
|
153
|
+
status=t["status"],
|
|
154
|
+
capability_slug=t["capability_slug"],
|
|
155
|
+
price_cents=t["price_cents"],
|
|
156
|
+
latency_ms=t["latency_ms"],
|
|
157
|
+
created_at=t["created_at"],
|
|
158
|
+
completed_at=t.get("completed_at"),
|
|
159
|
+
)
|
|
160
|
+
for t in data["transactions"]
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
def transaction(self, id: str) -> TransactionDetail:
|
|
164
|
+
"""Get details for a specific transaction."""
|
|
165
|
+
data = self._request("GET", f"/v1/transactions/{id}")
|
|
166
|
+
prov = data.get("provenance")
|
|
167
|
+
return TransactionDetail(
|
|
168
|
+
id=data["id"],
|
|
169
|
+
status=data["status"],
|
|
170
|
+
capability_slug=data["capability_slug"],
|
|
171
|
+
input=data.get("input", {}),
|
|
172
|
+
output=data.get("output"),
|
|
173
|
+
error=data.get("error"),
|
|
174
|
+
price_cents=data["price_cents"],
|
|
175
|
+
latency_ms=data["latency_ms"],
|
|
176
|
+
provenance=Provenance(
|
|
177
|
+
source=prov["source"], fetched_at=prov["fetched_at"]
|
|
178
|
+
)
|
|
179
|
+
if prov
|
|
180
|
+
else None,
|
|
181
|
+
created_at=data["created_at"],
|
|
182
|
+
completed_at=data.get("completed_at"),
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# ─── Auto-poll for async responses ─────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
def _poll_transaction(self, initial: DoResponse) -> DoResponse:
|
|
188
|
+
deadline = time.monotonic() + self._max_poll_wait
|
|
189
|
+
last_status = initial.status
|
|
190
|
+
|
|
191
|
+
while time.monotonic() < deadline:
|
|
192
|
+
time.sleep(self._poll_interval)
|
|
193
|
+
|
|
194
|
+
detail = self.transaction(initial.transaction_id)
|
|
195
|
+
|
|
196
|
+
if detail.status == "completed":
|
|
197
|
+
return DoResponse(
|
|
198
|
+
transaction_id=detail.id,
|
|
199
|
+
status="completed",
|
|
200
|
+
capability_used=detail.capability_slug,
|
|
201
|
+
price_cents=detail.price_cents,
|
|
202
|
+
latency_ms=detail.latency_ms,
|
|
203
|
+
wallet_balance_cents=initial.wallet_balance_cents,
|
|
204
|
+
output=detail.output or {},
|
|
205
|
+
provenance=detail.provenance
|
|
206
|
+
or Provenance(source="unknown", fetched_at=""),
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
if detail.status == "failed":
|
|
210
|
+
raise StraleError(
|
|
211
|
+
"execution_failed",
|
|
212
|
+
detail.error or "Capability execution failed",
|
|
213
|
+
500,
|
|
214
|
+
{"transaction_id": detail.id},
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
last_status = detail.status
|
|
218
|
+
|
|
219
|
+
raise StraleError(
|
|
220
|
+
"timeout_exceeded",
|
|
221
|
+
f"Timed out waiting for transaction {initial.transaction_id} "
|
|
222
|
+
f"(last status: {last_status})",
|
|
223
|
+
408,
|
|
224
|
+
{"transaction_id": initial.transaction_id},
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# ─── HTTP layer ────────────────────────────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
def _request(
|
|
230
|
+
self,
|
|
231
|
+
method: str,
|
|
232
|
+
path: str,
|
|
233
|
+
*,
|
|
234
|
+
json: Optional[Dict[str, Any]] = None,
|
|
235
|
+
extra_headers: Optional[Dict[str, str]] = None,
|
|
236
|
+
) -> Dict[str, Any]:
|
|
237
|
+
try:
|
|
238
|
+
response = self._client.request(
|
|
239
|
+
method,
|
|
240
|
+
path,
|
|
241
|
+
json=json,
|
|
242
|
+
headers=extra_headers,
|
|
243
|
+
)
|
|
244
|
+
except httpx.TimeoutException:
|
|
245
|
+
raise StraleError(
|
|
246
|
+
"timeout_exceeded",
|
|
247
|
+
f"Request timed out after {self._timeout}s",
|
|
248
|
+
408,
|
|
249
|
+
)
|
|
250
|
+
except httpx.HTTPError as e:
|
|
251
|
+
raise StraleError(
|
|
252
|
+
"execution_failed",
|
|
253
|
+
f"Network error: {e}",
|
|
254
|
+
0,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
data = response.json()
|
|
259
|
+
except Exception:
|
|
260
|
+
raise StraleError(
|
|
261
|
+
"execution_failed",
|
|
262
|
+
f"Invalid JSON response (HTTP {response.status_code})",
|
|
263
|
+
response.status_code,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
if not response.is_success:
|
|
267
|
+
if isinstance(data, dict) and "error_code" in data:
|
|
268
|
+
raise create_error(
|
|
269
|
+
data["error_code"],
|
|
270
|
+
data.get("message", "Unknown error"),
|
|
271
|
+
response.status_code,
|
|
272
|
+
data.get("details"),
|
|
273
|
+
)
|
|
274
|
+
raise StraleError(
|
|
275
|
+
"execution_failed",
|
|
276
|
+
f"HTTP {response.status_code}: {data}",
|
|
277
|
+
response.status_code,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
return data
|
|
281
|
+
|
|
282
|
+
# ─── Parsers ───────────────────────────────────────────────────────────────
|
|
283
|
+
|
|
284
|
+
@staticmethod
|
|
285
|
+
def _parse_do_response(data: Dict[str, Any]) -> DoResponse:
|
|
286
|
+
prov = data.get("provenance")
|
|
287
|
+
return DoResponse(
|
|
288
|
+
transaction_id=data["transaction_id"],
|
|
289
|
+
status=data["status"],
|
|
290
|
+
capability_used=data.get("capability_used", ""),
|
|
291
|
+
price_cents=data.get("price_cents", 0),
|
|
292
|
+
latency_ms=data.get("latency_ms", 0),
|
|
293
|
+
wallet_balance_cents=data.get("wallet_balance_cents", 0),
|
|
294
|
+
output=data.get("output", {}),
|
|
295
|
+
provenance=Provenance(
|
|
296
|
+
source=prov["source"], fetched_at=prov["fetched_at"]
|
|
297
|
+
)
|
|
298
|
+
if prov
|
|
299
|
+
else Provenance(source="unknown", fetched_at=""),
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
@staticmethod
|
|
303
|
+
def _parse_capability(data: Dict[str, Any]) -> Capability:
|
|
304
|
+
return Capability(
|
|
305
|
+
slug=data["slug"],
|
|
306
|
+
name=data["name"],
|
|
307
|
+
description=data["description"],
|
|
308
|
+
category=data["category"],
|
|
309
|
+
price_cents=data["price_cents"],
|
|
310
|
+
input_schema=data.get("input_schema"),
|
|
311
|
+
output_schema=data.get("output_schema"),
|
|
312
|
+
avg_latency_ms=data.get("avg_latency_ms", 0),
|
|
313
|
+
success_rate=data.get("success_rate", 0.0),
|
|
314
|
+
)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Error classes for the Strale Python SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class StraleError(Exception):
|
|
9
|
+
"""Base error class for all Strale API errors."""
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
error_code: str,
|
|
14
|
+
message: str,
|
|
15
|
+
status_code: int,
|
|
16
|
+
details: Optional[Dict[str, Any]] = None,
|
|
17
|
+
):
|
|
18
|
+
super().__init__(message)
|
|
19
|
+
self.error_code = error_code
|
|
20
|
+
self.status_code = status_code
|
|
21
|
+
self.details = details or {}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class InsufficientBalanceError(StraleError):
|
|
25
|
+
def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
|
|
26
|
+
super().__init__("insufficient_balance", message, 402, details)
|
|
27
|
+
self.wallet_balance_cents: int = (details or {}).get("wallet_balance_cents", 0)
|
|
28
|
+
self.required_cents: int = (details or {}).get("required_cents", 0)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class NoMatchingCapabilityError(StraleError):
|
|
32
|
+
def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
|
|
33
|
+
super().__init__("no_matching_capability", message, 404, details)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class CapabilityUnavailableError(StraleError):
|
|
37
|
+
def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
|
|
38
|
+
super().__init__("capability_unavailable", message, 503, details)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ExecutionFailedError(StraleError):
|
|
42
|
+
def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
|
|
43
|
+
super().__init__("execution_failed", message, 500, details)
|
|
44
|
+
self.transaction_id: Optional[str] = (details or {}).get("transaction_id")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TimeoutExceededError(StraleError):
|
|
48
|
+
def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
|
|
49
|
+
super().__init__("timeout_exceeded", message, 408, details)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class InvalidRequestError(StraleError):
|
|
53
|
+
def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
|
|
54
|
+
super().__init__("invalid_request", message, 400, details)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class RateLimitedError(StraleError):
|
|
58
|
+
def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
|
|
59
|
+
super().__init__("rate_limited", message, 429, details)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class UnauthorizedError(StraleError):
|
|
63
|
+
def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
|
|
64
|
+
super().__init__("unauthorized", message, 401, details)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class NotFoundError(StraleError):
|
|
68
|
+
def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
|
|
69
|
+
super().__init__("not_found", message, 404, details)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
_ERROR_MAP = {
|
|
73
|
+
"insufficient_balance": InsufficientBalanceError,
|
|
74
|
+
"no_matching_capability": NoMatchingCapabilityError,
|
|
75
|
+
"capability_unavailable": CapabilityUnavailableError,
|
|
76
|
+
"execution_failed": ExecutionFailedError,
|
|
77
|
+
"timeout_exceeded": TimeoutExceededError,
|
|
78
|
+
"invalid_request": InvalidRequestError,
|
|
79
|
+
"rate_limited": RateLimitedError,
|
|
80
|
+
"unauthorized": UnauthorizedError,
|
|
81
|
+
"not_found": NotFoundError,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def create_error(
|
|
86
|
+
error_code: str,
|
|
87
|
+
message: str,
|
|
88
|
+
status_code: int,
|
|
89
|
+
details: Optional[Dict[str, Any]] = None,
|
|
90
|
+
) -> StraleError:
|
|
91
|
+
"""Create a typed error from an API error response."""
|
|
92
|
+
cls = _ERROR_MAP.get(error_code, StraleError)
|
|
93
|
+
if cls is StraleError:
|
|
94
|
+
return StraleError(error_code, message, status_code, details)
|
|
95
|
+
return cls(message, details)
|
|
File without changes
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Type definitions for the Strale Python SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any, Dict, List, Literal, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class DoRequest:
|
|
11
|
+
"""Request payload for POST /v1/do."""
|
|
12
|
+
|
|
13
|
+
max_price_cents: int
|
|
14
|
+
"""Maximum price in EUR cents you're willing to pay."""
|
|
15
|
+
|
|
16
|
+
task: Optional[str] = None
|
|
17
|
+
"""Natural language description of what you need done."""
|
|
18
|
+
|
|
19
|
+
capability_slug: Optional[str] = None
|
|
20
|
+
"""Direct capability slug override (bypasses matching)."""
|
|
21
|
+
|
|
22
|
+
inputs: Optional[Dict[str, Any]] = None
|
|
23
|
+
"""Structured input for the capability."""
|
|
24
|
+
|
|
25
|
+
timeout_seconds: Optional[int] = None
|
|
26
|
+
"""Execution timeout in seconds (max 60)."""
|
|
27
|
+
|
|
28
|
+
dry_run: bool = False
|
|
29
|
+
"""If True, returns what would execute without charging."""
|
|
30
|
+
|
|
31
|
+
idempotency_key: Optional[str] = None
|
|
32
|
+
"""Idempotency key for safe retries."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class Provenance:
|
|
37
|
+
"""Provenance metadata for a capability execution."""
|
|
38
|
+
|
|
39
|
+
source: str
|
|
40
|
+
fetched_at: str
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class DoResponse:
|
|
45
|
+
"""Response from POST /v1/do (completed or executing)."""
|
|
46
|
+
|
|
47
|
+
transaction_id: str
|
|
48
|
+
status: str
|
|
49
|
+
capability_used: str
|
|
50
|
+
price_cents: int
|
|
51
|
+
latency_ms: int
|
|
52
|
+
wallet_balance_cents: int
|
|
53
|
+
output: Dict[str, Any]
|
|
54
|
+
provenance: Provenance
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class DryRunResponse:
|
|
59
|
+
"""Response from POST /v1/do with dry_run=True."""
|
|
60
|
+
|
|
61
|
+
dry_run: bool
|
|
62
|
+
would_execute: str
|
|
63
|
+
price_cents: int
|
|
64
|
+
wallet_balance_cents: int
|
|
65
|
+
wallet_sufficient: bool
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class Capability:
|
|
70
|
+
"""A capability available on the Strale platform."""
|
|
71
|
+
|
|
72
|
+
slug: str
|
|
73
|
+
name: str
|
|
74
|
+
description: str
|
|
75
|
+
category: str
|
|
76
|
+
price_cents: int
|
|
77
|
+
input_schema: Any
|
|
78
|
+
output_schema: Any
|
|
79
|
+
avg_latency_ms: int
|
|
80
|
+
success_rate: float
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class BalanceResponse:
|
|
85
|
+
"""Wallet balance response."""
|
|
86
|
+
|
|
87
|
+
balance_cents: int
|
|
88
|
+
currency: str
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass
|
|
92
|
+
class Transaction:
|
|
93
|
+
"""Transaction summary from list endpoint."""
|
|
94
|
+
|
|
95
|
+
id: str
|
|
96
|
+
status: str
|
|
97
|
+
capability_slug: str
|
|
98
|
+
price_cents: int
|
|
99
|
+
latency_ms: int
|
|
100
|
+
created_at: str
|
|
101
|
+
completed_at: Optional[str]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass
|
|
105
|
+
class TransactionDetail:
|
|
106
|
+
"""Full transaction detail."""
|
|
107
|
+
|
|
108
|
+
id: str
|
|
109
|
+
status: str
|
|
110
|
+
capability_slug: str
|
|
111
|
+
input: Dict[str, Any]
|
|
112
|
+
output: Optional[Dict[str, Any]]
|
|
113
|
+
error: Optional[str]
|
|
114
|
+
price_cents: int
|
|
115
|
+
latency_ms: int
|
|
116
|
+
provenance: Optional[Provenance]
|
|
117
|
+
created_at: str
|
|
118
|
+
completed_at: Optional[str]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: straleio
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the Strale API — let AI agents buy capabilities at runtime
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://strale.io
|
|
7
|
+
Project-URL: Repository, https://github.com/petter_lindstrom/strale
|
|
8
|
+
Keywords: strale,ai,agent,capabilities,api,sdk
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: httpx>=0.24.0
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
pyproject.toml
|
|
2
|
+
straleio/__init__.py
|
|
3
|
+
straleio/client.py
|
|
4
|
+
straleio/errors.py
|
|
5
|
+
straleio/py.typed
|
|
6
|
+
straleio/types.py
|
|
7
|
+
straleio.egg-info/PKG-INFO
|
|
8
|
+
straleio.egg-info/SOURCES.txt
|
|
9
|
+
straleio.egg-info/dependency_links.txt
|
|
10
|
+
straleio.egg-info/requires.txt
|
|
11
|
+
straleio.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
httpx>=0.24.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
straleio
|