settld-api-sdk-python 0.1.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.
@@ -0,0 +1,3 @@
1
+ from .client import SettldClient, SettldApiError
2
+
3
+ __all__ = ["SettldClient", "SettldApiError"]
@@ -0,0 +1,346 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import random
5
+ import time
6
+ import uuid
7
+ from typing import Any, Dict, Optional
8
+ from urllib import error, parse, request
9
+
10
+
11
+ def _assert_non_empty_string(value: Any, name: str) -> str:
12
+ if not isinstance(value, str) or value.strip() == "":
13
+ raise ValueError(f"{name} must be a non-empty string")
14
+ return value
15
+
16
+
17
+ def _random_request_id() -> str:
18
+ return f"req_{uuid.uuid4().hex}"
19
+
20
+
21
+ def _normalize_prefix(value: Optional[str], fallback: str) -> str:
22
+ if isinstance(value, str) and value.strip():
23
+ return value.strip()
24
+ return fallback
25
+
26
+
27
+ class SettldApiError(Exception):
28
+ def __init__(
29
+ self,
30
+ *,
31
+ status: int,
32
+ message: str,
33
+ code: Optional[str] = None,
34
+ details: Any = None,
35
+ request_id: Optional[str] = None,
36
+ ) -> None:
37
+ super().__init__(message)
38
+ self.status = status
39
+ self.code = code
40
+ self.details = details
41
+ self.request_id = request_id
42
+
43
+ def to_dict(self) -> Dict[str, Any]:
44
+ return {
45
+ "status": self.status,
46
+ "code": self.code,
47
+ "message": str(self),
48
+ "details": self.details,
49
+ "requestId": self.request_id,
50
+ }
51
+
52
+
53
+ class SettldClient:
54
+ def __init__(
55
+ self,
56
+ *,
57
+ base_url: str,
58
+ tenant_id: str,
59
+ protocol: str = "1.0",
60
+ api_key: Optional[str] = None,
61
+ user_agent: Optional[str] = None,
62
+ timeout_seconds: float = 30.0,
63
+ ) -> None:
64
+ self.base_url = _assert_non_empty_string(base_url, "base_url").rstrip("/")
65
+ self.tenant_id = _assert_non_empty_string(tenant_id, "tenant_id")
66
+ self.protocol = protocol
67
+ self.api_key = api_key
68
+ self.user_agent = user_agent
69
+ self.timeout_seconds = timeout_seconds
70
+
71
+ def _request(
72
+ self,
73
+ method: str,
74
+ path: str,
75
+ *,
76
+ body: Optional[Dict[str, Any]] = None,
77
+ request_id: Optional[str] = None,
78
+ idempotency_key: Optional[str] = None,
79
+ expected_prev_chain_hash: Optional[str] = None,
80
+ timeout_seconds: Optional[float] = None,
81
+ ) -> Dict[str, Any]:
82
+ rid = request_id if request_id else _random_request_id()
83
+ headers = {
84
+ "content-type": "application/json",
85
+ "x-proxy-tenant-id": self.tenant_id,
86
+ "x-settld-protocol": self.protocol,
87
+ "x-request-id": rid,
88
+ }
89
+ if self.user_agent:
90
+ headers["user-agent"] = self.user_agent
91
+ if self.api_key:
92
+ headers["authorization"] = f"Bearer {self.api_key}"
93
+ if idempotency_key:
94
+ headers["x-idempotency-key"] = str(idempotency_key)
95
+ if expected_prev_chain_hash:
96
+ headers["x-proxy-expected-prev-chain-hash"] = str(expected_prev_chain_hash)
97
+
98
+ url = parse.urljoin(f"{self.base_url}/", path.lstrip("/"))
99
+ payload = None if body is None else json.dumps(body).encode("utf-8")
100
+ req = request.Request(url=url, data=payload, method=method, headers=headers)
101
+ timeout = self.timeout_seconds if timeout_seconds is None else timeout_seconds
102
+ try:
103
+ with request.urlopen(req, timeout=timeout) as response:
104
+ raw = response.read().decode("utf-8")
105
+ parsed = None
106
+ if raw:
107
+ try:
108
+ parsed = json.loads(raw)
109
+ except json.JSONDecodeError:
110
+ parsed = {"raw": raw}
111
+ response_headers = {str(k).lower(): str(v) for k, v in response.headers.items()}
112
+ return {
113
+ "ok": True,
114
+ "status": int(response.status),
115
+ "requestId": response_headers.get("x-request-id"),
116
+ "body": parsed,
117
+ "headers": response_headers,
118
+ }
119
+ except error.HTTPError as http_error:
120
+ raw = http_error.read().decode("utf-8")
121
+ parsed: Any = {}
122
+ if raw:
123
+ try:
124
+ parsed = json.loads(raw)
125
+ except json.JSONDecodeError:
126
+ parsed = {"raw": raw}
127
+ response_headers = {str(k).lower(): str(v) for k, v in http_error.headers.items()}
128
+ raise SettldApiError(
129
+ status=int(http_error.code),
130
+ code=parsed.get("code") if isinstance(parsed, dict) else None,
131
+ message=parsed.get("error", f"request failed ({http_error.code})") if isinstance(parsed, dict) else f"request failed ({http_error.code})",
132
+ details=parsed.get("details") if isinstance(parsed, dict) else None,
133
+ request_id=response_headers.get("x-request-id"),
134
+ ) from http_error
135
+
136
+ def register_agent(self, body: Dict[str, Any], **opts: Any) -> Dict[str, Any]:
137
+ if not isinstance(body, dict):
138
+ raise ValueError("body is required")
139
+ _assert_non_empty_string(body.get("publicKeyPem"), "body.publicKeyPem")
140
+ return self._request("POST", "/agents/register", body=body, **opts)
141
+
142
+ def credit_agent_wallet(self, agent_id: str, body: Dict[str, Any], **opts: Any) -> Dict[str, Any]:
143
+ _assert_non_empty_string(agent_id, "agent_id")
144
+ if not isinstance(body, dict):
145
+ raise ValueError("body is required")
146
+ return self._request("POST", f"/agents/{parse.quote(agent_id, safe='')}/wallet/credit", body=body, **opts)
147
+
148
+ def get_agent_wallet(self, agent_id: str, **opts: Any) -> Dict[str, Any]:
149
+ _assert_non_empty_string(agent_id, "agent_id")
150
+ return self._request("GET", f"/agents/{parse.quote(agent_id, safe='')}/wallet", **opts)
151
+
152
+ def create_agent_run(self, agent_id: str, body: Optional[Dict[str, Any]] = None, **opts: Any) -> Dict[str, Any]:
153
+ _assert_non_empty_string(agent_id, "agent_id")
154
+ run_body = {} if body is None else body
155
+ if not isinstance(run_body, dict):
156
+ raise ValueError("body must be an object")
157
+ return self._request("POST", f"/agents/{parse.quote(agent_id, safe='')}/runs", body=run_body, **opts)
158
+
159
+ def append_agent_run_event(
160
+ self,
161
+ agent_id: str,
162
+ run_id: str,
163
+ body: Dict[str, Any],
164
+ *,
165
+ expected_prev_chain_hash: str,
166
+ **opts: Any,
167
+ ) -> Dict[str, Any]:
168
+ _assert_non_empty_string(agent_id, "agent_id")
169
+ _assert_non_empty_string(run_id, "run_id")
170
+ _assert_non_empty_string(expected_prev_chain_hash, "expected_prev_chain_hash")
171
+ if not isinstance(body, dict):
172
+ raise ValueError("body is required")
173
+ _assert_non_empty_string(body.get("type"), "body.type")
174
+ return self._request(
175
+ "POST",
176
+ f"/agents/{parse.quote(agent_id, safe='')}/runs/{parse.quote(run_id, safe='')}/events",
177
+ body=body,
178
+ expected_prev_chain_hash=expected_prev_chain_hash,
179
+ **opts,
180
+ )
181
+
182
+ def get_agent_run(self, agent_id: str, run_id: str, **opts: Any) -> Dict[str, Any]:
183
+ _assert_non_empty_string(agent_id, "agent_id")
184
+ _assert_non_empty_string(run_id, "run_id")
185
+ return self._request("GET", f"/agents/{parse.quote(agent_id, safe='')}/runs/{parse.quote(run_id, safe='')}", **opts)
186
+
187
+ def list_agent_run_events(self, agent_id: str, run_id: str, **opts: Any) -> Dict[str, Any]:
188
+ _assert_non_empty_string(agent_id, "agent_id")
189
+ _assert_non_empty_string(run_id, "run_id")
190
+ return self._request("GET", f"/agents/{parse.quote(agent_id, safe='')}/runs/{parse.quote(run_id, safe='')}/events", **opts)
191
+
192
+ def get_run_verification(self, run_id: str, **opts: Any) -> Dict[str, Any]:
193
+ _assert_non_empty_string(run_id, "run_id")
194
+ return self._request("GET", f"/runs/{parse.quote(run_id, safe='')}/verification", **opts)
195
+
196
+ def get_run_settlement(self, run_id: str, **opts: Any) -> Dict[str, Any]:
197
+ _assert_non_empty_string(run_id, "run_id")
198
+ return self._request("GET", f"/runs/{parse.quote(run_id, safe='')}/settlement", **opts)
199
+
200
+ def first_verified_run(
201
+ self,
202
+ params: Dict[str, Any],
203
+ *,
204
+ idempotency_prefix: Optional[str] = None,
205
+ request_id_prefix: Optional[str] = None,
206
+ timeout_seconds: Optional[float] = None,
207
+ ) -> Dict[str, Any]:
208
+ if not isinstance(params, dict):
209
+ raise ValueError("params must be an object")
210
+ payee_agent = params.get("payee_agent")
211
+ if not isinstance(payee_agent, dict):
212
+ raise ValueError("params.payee_agent is required")
213
+ _assert_non_empty_string(payee_agent.get("publicKeyPem"), "params.payee_agent.publicKeyPem")
214
+
215
+ step_prefix = _normalize_prefix(
216
+ idempotency_prefix,
217
+ f"sdk_first_verified_run_{int(time.time() * 1000):x}_{random.randint(0, 0xFFFFFF):06x}",
218
+ )
219
+ request_prefix = _normalize_prefix(request_id_prefix, _random_request_id())
220
+
221
+ def make_step_opts(step: str, **extra: Any) -> Dict[str, Any]:
222
+ out = {
223
+ "request_id": f"{request_prefix}_{step}",
224
+ "idempotency_key": f"{step_prefix}_{step}",
225
+ "timeout_seconds": timeout_seconds,
226
+ }
227
+ out.update(extra)
228
+ return out
229
+
230
+ payee_registration = self.register_agent(payee_agent, **make_step_opts("register_payee"))
231
+ payee_agent_id = payee_registration.get("body", {}).get("agentIdentity", {}).get("agentId")
232
+ _assert_non_empty_string(payee_agent_id, "payee_agent_id")
233
+
234
+ payer_registration = None
235
+ payer_credit = None
236
+ payer_agent_id = None
237
+ payer_agent = params.get("payer_agent")
238
+ if payer_agent is not None:
239
+ if not isinstance(payer_agent, dict):
240
+ raise ValueError("params.payer_agent must be an object")
241
+ _assert_non_empty_string(payer_agent.get("publicKeyPem"), "params.payer_agent.publicKeyPem")
242
+ payer_registration = self.register_agent(payer_agent, **make_step_opts("register_payer"))
243
+ payer_agent_id = payer_registration.get("body", {}).get("agentIdentity", {}).get("agentId")
244
+ _assert_non_empty_string(payer_agent_id, "payer_agent_id")
245
+
246
+ settlement = params.get("settlement") if isinstance(params.get("settlement"), dict) else None
247
+ settlement_amount_cents = settlement.get("amountCents") if settlement else None
248
+ settlement_currency = settlement.get("currency", "USD") if settlement else "USD"
249
+ settlement_payer_agent_id = (
250
+ settlement.get("payerAgentId")
251
+ if settlement and isinstance(settlement.get("payerAgentId"), str)
252
+ else payer_agent_id
253
+ )
254
+ if settlement_amount_cents is not None and settlement_payer_agent_id is None:
255
+ raise ValueError("params.payer_agent or params.settlement.payerAgentId is required when settlement is requested")
256
+
257
+ payer_credit_input = params.get("payer_credit")
258
+ if payer_credit_input is not None:
259
+ if not isinstance(payer_credit_input, dict):
260
+ raise ValueError("params.payer_credit must be an object")
261
+ payer_credit_amount = payer_credit_input.get("amountCents")
262
+ if not isinstance(payer_credit_amount, (int, float)) or payer_credit_amount <= 0:
263
+ raise ValueError("params.payer_credit.amountCents must be a positive number")
264
+ if not payer_agent_id:
265
+ raise ValueError("params.payer_agent is required when params.payer_credit is provided")
266
+ payer_credit = self.credit_agent_wallet(
267
+ payer_agent_id,
268
+ {
269
+ "amountCents": int(payer_credit_amount),
270
+ "currency": payer_credit_input.get("currency", settlement_currency),
271
+ },
272
+ **make_step_opts("credit_payer_wallet"),
273
+ )
274
+
275
+ run_body = dict(params.get("run") or {})
276
+ if settlement_amount_cents is not None:
277
+ if not isinstance(settlement_amount_cents, (int, float)) or settlement_amount_cents <= 0:
278
+ raise ValueError("params.settlement.amountCents must be a positive number")
279
+ run_body["settlement"] = {
280
+ "payerAgentId": settlement_payer_agent_id,
281
+ "amountCents": int(settlement_amount_cents),
282
+ "currency": settlement_currency,
283
+ }
284
+
285
+ run_created = self.create_agent_run(payee_agent_id, run_body, **make_step_opts("create_run"))
286
+ run_id = run_created.get("body", {}).get("run", {}).get("runId")
287
+ _assert_non_empty_string(run_id, "run_id")
288
+ prev_chain_hash = run_created.get("body", {}).get("run", {}).get("lastChainHash")
289
+ _assert_non_empty_string(prev_chain_hash, "run_created.body.run.lastChainHash")
290
+
291
+ actor = params.get("actor") or {"type": "agent", "id": payee_agent_id}
292
+ started_payload = params.get("started_payload") or {"startedBy": "sdk.first_verified_run"}
293
+ run_started = self.append_agent_run_event(
294
+ payee_agent_id,
295
+ run_id,
296
+ {"type": "RUN_STARTED", "actor": actor, "payload": started_payload},
297
+ expected_prev_chain_hash=prev_chain_hash,
298
+ **make_step_opts("run_started"),
299
+ )
300
+ prev_chain_hash = run_started.get("body", {}).get("run", {}).get("lastChainHash")
301
+ _assert_non_empty_string(prev_chain_hash, "run_started.body.run.lastChainHash")
302
+
303
+ evidence_ref = params.get("evidence_ref") if isinstance(params.get("evidence_ref"), str) and params.get("evidence_ref").strip() else f"evidence://{run_id}/output.json"
304
+ evidence_payload = params.get("evidence_payload") if isinstance(params.get("evidence_payload"), dict) else {"evidenceRef": evidence_ref}
305
+ run_evidence_added = self.append_agent_run_event(
306
+ payee_agent_id,
307
+ run_id,
308
+ {"type": "EVIDENCE_ADDED", "actor": actor, "payload": evidence_payload},
309
+ expected_prev_chain_hash=prev_chain_hash,
310
+ **make_step_opts("evidence_added"),
311
+ )
312
+ prev_chain_hash = run_evidence_added.get("body", {}).get("run", {}).get("lastChainHash")
313
+ _assert_non_empty_string(prev_chain_hash, "run_evidence_added.body.run.lastChainHash")
314
+
315
+ completed_payload = dict(params.get("completed_payload") or {})
316
+ output_ref = params.get("output_ref") if isinstance(params.get("output_ref"), str) and params.get("output_ref").strip() else evidence_ref
317
+ completed_payload["outputRef"] = output_ref
318
+ if isinstance(params.get("completed_metrics"), dict):
319
+ completed_payload["metrics"] = params.get("completed_metrics")
320
+ run_completed = self.append_agent_run_event(
321
+ payee_agent_id,
322
+ run_id,
323
+ {"type": "RUN_COMPLETED", "actor": actor, "payload": completed_payload},
324
+ expected_prev_chain_hash=prev_chain_hash,
325
+ **make_step_opts("run_completed"),
326
+ )
327
+
328
+ run = self.get_agent_run(payee_agent_id, run_id, **make_step_opts("get_run"))
329
+ verification = self.get_run_verification(run_id, **make_step_opts("get_verification"))
330
+ settlement_out = None
331
+ if run_body.get("settlement") or run_created.get("body", {}).get("settlement") or run_completed.get("body", {}).get("settlement"):
332
+ settlement_out = self.get_run_settlement(run_id, **make_step_opts("get_settlement"))
333
+
334
+ return {
335
+ "ids": {"run_id": run_id, "payee_agent_id": payee_agent_id, "payer_agent_id": payer_agent_id},
336
+ "payee_registration": payee_registration,
337
+ "payer_registration": payer_registration,
338
+ "payer_credit": payer_credit,
339
+ "run_created": run_created,
340
+ "run_started": run_started,
341
+ "run_evidence_added": run_evidence_added,
342
+ "run_completed": run_completed,
343
+ "run": run,
344
+ "verification": verification,
345
+ "settlement": settlement_out,
346
+ }
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.4
2
+ Name: settld-api-sdk-python
3
+ Version: 0.1.0
4
+ Summary: Settld API SDK (Python)
5
+ Author: Settld
6
+ License: UNLICENSED
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+
10
+ # Settld API SDK (Python)
11
+
12
+ Python client for Settld API endpoints, including a high-level `first_verified_run` helper.
13
+
14
+ Quickstart docs live in `docs/QUICKSTART_SDK_PYTHON.md` at repo root.
@@ -0,0 +1,6 @@
1
+ settld_api_sdk/__init__.py,sha256=GzeaUBCtPstGn5CE-hwFiAM8sOSJwR0WeTe3BDVplu0,95
2
+ settld_api_sdk/client.py,sha256=XJmRhmUu_HCczq72-lZV42W70utj49HqXhTDKUsqs8g,16008
3
+ settld_api_sdk_python-0.1.0.dist-info/METADATA,sha256=QITPteAPyJOomW0uEZVP_aPSn4KqcCDvHvv-ShSVl8U,387
4
+ settld_api_sdk_python-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
5
+ settld_api_sdk_python-0.1.0.dist-info/top_level.txt,sha256=MVDdFXCkNCGGiH6fQpKHqNQozINOm9z1JHVg2u3Udes,15
6
+ settld_api_sdk_python-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ settld_api_sdk