modus-sdk 1.1.2__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.
Potentially problematic release.
This version of modus-sdk might be problematic. Click here for more details.
- modus/__init__.py +117 -0
- modus/_http.py +79 -0
- modus/async_client.py +604 -0
- modus/client.py +604 -0
- modus/crypto.py +77 -0
- modus/exceptions.py +52 -0
- modus/py.typed +0 -0
- modus/types.py +446 -0
- modus_sdk-1.1.2.dist-info/METADATA +296 -0
- modus_sdk-1.1.2.dist-info/RECORD +11 -0
- modus_sdk-1.1.2.dist-info/WHEEL +4 -0
modus/async_client.py
ADDED
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
"""Asynchronous Modus client using httpx."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from ._http import (
|
|
10
|
+
DEFAULT_BASE_URL,
|
|
11
|
+
DEFAULT_TIMEOUT,
|
|
12
|
+
_build_headers,
|
|
13
|
+
_clean_params,
|
|
14
|
+
_parse_response,
|
|
15
|
+
)
|
|
16
|
+
from .types import (
|
|
17
|
+
AnonymousPolicy,
|
|
18
|
+
Attestation,
|
|
19
|
+
AttestationRecord,
|
|
20
|
+
CatalogCreate,
|
|
21
|
+
CheckResult,
|
|
22
|
+
ConsumeRequest,
|
|
23
|
+
ConsumeResult,
|
|
24
|
+
ConstraintTemplate,
|
|
25
|
+
ConstraintType,
|
|
26
|
+
CumulativeState,
|
|
27
|
+
DiscoveryQuery,
|
|
28
|
+
DiscoveryResult,
|
|
29
|
+
EnforceRequest,
|
|
30
|
+
EnforceResult,
|
|
31
|
+
EnforcementAttestation,
|
|
32
|
+
Gate,
|
|
33
|
+
GateCreate,
|
|
34
|
+
GateUpdate,
|
|
35
|
+
Passport,
|
|
36
|
+
PassportIssue,
|
|
37
|
+
PublishResult,
|
|
38
|
+
PublishedCatalog,
|
|
39
|
+
ReissueResult,
|
|
40
|
+
SLAComplianceReport,
|
|
41
|
+
SettlementRequest,
|
|
42
|
+
SettlementSummary,
|
|
43
|
+
VerifyResult,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class AsyncModusClient:
|
|
48
|
+
"""Asynchronous client for the Modus REST API.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
api_key: Modus API key (e.g. ``uni_live_xxx``).
|
|
52
|
+
base_url: API base URL. Defaults to ``https://modusoperator.com``.
|
|
53
|
+
timeout: Request timeout in seconds. Defaults to 30.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
api_key: str,
|
|
59
|
+
*,
|
|
60
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
61
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
62
|
+
) -> None:
|
|
63
|
+
self._api_key = api_key
|
|
64
|
+
self._base_url = base_url.rstrip("/")
|
|
65
|
+
self._client = httpx.AsyncClient(
|
|
66
|
+
base_url=self._base_url,
|
|
67
|
+
headers=_build_headers(api_key),
|
|
68
|
+
timeout=timeout,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
async def close(self) -> None:
|
|
72
|
+
"""Close the underlying HTTP client."""
|
|
73
|
+
await self._client.aclose()
|
|
74
|
+
|
|
75
|
+
async def __aenter__(self) -> AsyncModusClient:
|
|
76
|
+
return self
|
|
77
|
+
|
|
78
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
79
|
+
await self.close()
|
|
80
|
+
|
|
81
|
+
# -- internal helpers ---------------------------------------------------
|
|
82
|
+
|
|
83
|
+
async def _get(self, path: str, params: Optional[dict[str, Any]] = None) -> Any:
|
|
84
|
+
resp = await self._client.get(path, params=_clean_params(params))
|
|
85
|
+
return _parse_response(resp)
|
|
86
|
+
|
|
87
|
+
async def _post(self, path: str, json: Optional[Any] = None) -> Any:
|
|
88
|
+
resp = await self._client.post(path, json=json)
|
|
89
|
+
return _parse_response(resp)
|
|
90
|
+
|
|
91
|
+
async def _patch(self, path: str, json: Optional[Any] = None) -> Any:
|
|
92
|
+
resp = await self._client.patch(path, json=json)
|
|
93
|
+
return _parse_response(resp)
|
|
94
|
+
|
|
95
|
+
async def _put(self, path: str, json: Optional[Any] = None) -> Any:
|
|
96
|
+
resp = await self._client.put(path, json=json)
|
|
97
|
+
return _parse_response(resp)
|
|
98
|
+
|
|
99
|
+
async def _delete(self, path: str) -> Any:
|
|
100
|
+
resp = await self._client.delete(path)
|
|
101
|
+
return _parse_response(resp)
|
|
102
|
+
|
|
103
|
+
# ======================================================================
|
|
104
|
+
# Gates
|
|
105
|
+
# ======================================================================
|
|
106
|
+
|
|
107
|
+
async def list_gates(self) -> list[dict[str, Any]]:
|
|
108
|
+
"""List all gates you own."""
|
|
109
|
+
return await self._get("/api/gates")
|
|
110
|
+
|
|
111
|
+
async def get_gate(self, gate_id: str) -> dict[str, Any]:
|
|
112
|
+
"""Get details for a specific gate."""
|
|
113
|
+
return await self._get(f"/api/gates/{gate_id}")
|
|
114
|
+
|
|
115
|
+
async def create_gate(
|
|
116
|
+
self,
|
|
117
|
+
*,
|
|
118
|
+
name: str,
|
|
119
|
+
gate_id: str,
|
|
120
|
+
profile: str = "L1",
|
|
121
|
+
description: Optional[str] = None,
|
|
122
|
+
allow_self_issued: Optional[bool] = None,
|
|
123
|
+
) -> dict[str, Any]:
|
|
124
|
+
"""Create a new gate."""
|
|
125
|
+
body = GateCreate(
|
|
126
|
+
name=name,
|
|
127
|
+
gate_id=gate_id,
|
|
128
|
+
profile=profile, # type: ignore[arg-type]
|
|
129
|
+
description=description,
|
|
130
|
+
allow_self_issued=allow_self_issued,
|
|
131
|
+
)
|
|
132
|
+
return await self._post("/api/gates", json=body.model_dump(exclude_none=True))
|
|
133
|
+
|
|
134
|
+
async def update_gate(self, gate_id: str, **kwargs: Any) -> dict[str, Any]:
|
|
135
|
+
"""Update settings for an existing gate."""
|
|
136
|
+
body = GateUpdate(**kwargs)
|
|
137
|
+
return await self._patch(f"/api/gates/{gate_id}", json=body.model_dump(exclude_none=True))
|
|
138
|
+
|
|
139
|
+
async def delete_gate(self, gate_id: str) -> dict[str, Any]:
|
|
140
|
+
"""Delete (archive) a gate."""
|
|
141
|
+
return await self._delete(f"/api/gates/{gate_id}")
|
|
142
|
+
|
|
143
|
+
# ======================================================================
|
|
144
|
+
# Passports
|
|
145
|
+
# ======================================================================
|
|
146
|
+
|
|
147
|
+
async def list_passports(self, gate_id: str) -> list[dict[str, Any]]:
|
|
148
|
+
"""List all active passports for a gate."""
|
|
149
|
+
return await self._get(f"/api/gates/{gate_id}/passports")
|
|
150
|
+
|
|
151
|
+
async def get_passport(self, gate_id: str, passport_id: str) -> dict[str, Any]:
|
|
152
|
+
"""Get details for a specific passport."""
|
|
153
|
+
return await self._get(f"/api/gates/{gate_id}/passports/{passport_id}")
|
|
154
|
+
|
|
155
|
+
async def issue_passport(
|
|
156
|
+
self,
|
|
157
|
+
gate_id: str,
|
|
158
|
+
*,
|
|
159
|
+
agent_id: str,
|
|
160
|
+
permissions: list[str],
|
|
161
|
+
agent_name: Optional[str] = None,
|
|
162
|
+
agent_public_key: Optional[str] = None,
|
|
163
|
+
constraints: Optional[dict[str, Any]] = None,
|
|
164
|
+
expires_in_seconds: Optional[int] = None,
|
|
165
|
+
expires_in: Optional[str] = None,
|
|
166
|
+
metadata: Optional[dict[str, Any]] = None,
|
|
167
|
+
) -> dict[str, Any]:
|
|
168
|
+
"""Issue a new passport to an agent."""
|
|
169
|
+
body = PassportIssue(
|
|
170
|
+
agent_id=agent_id,
|
|
171
|
+
permissions=permissions,
|
|
172
|
+
agent_name=agent_name,
|
|
173
|
+
agent_public_key=agent_public_key,
|
|
174
|
+
constraints=constraints,
|
|
175
|
+
expires_in_seconds=expires_in_seconds,
|
|
176
|
+
expires_in=expires_in,
|
|
177
|
+
metadata=metadata,
|
|
178
|
+
)
|
|
179
|
+
return await self._post(
|
|
180
|
+
f"/api/gates/{gate_id}/passports",
|
|
181
|
+
json=body.model_dump(exclude_none=True),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
async def revoke_passport(self, gate_id: str, passport_id: str) -> dict[str, Any]:
|
|
185
|
+
"""Revoke a passport immediately."""
|
|
186
|
+
return await self._delete(f"/api/gates/{gate_id}/passports/{passport_id}")
|
|
187
|
+
|
|
188
|
+
async def reissue_passport(
|
|
189
|
+
self,
|
|
190
|
+
passport_id: str,
|
|
191
|
+
*,
|
|
192
|
+
accept_catalog_version: Optional[int] = None,
|
|
193
|
+
) -> dict[str, Any]:
|
|
194
|
+
"""Re-issue a passport pinned to a newer catalog version."""
|
|
195
|
+
body: dict[str, Any] = {}
|
|
196
|
+
if accept_catalog_version is not None:
|
|
197
|
+
body["accept_catalog_version"] = accept_catalog_version
|
|
198
|
+
return await self._post(f"/api/passports/{passport_id}/reissue", json=body)
|
|
199
|
+
|
|
200
|
+
# ======================================================================
|
|
201
|
+
# Attestations
|
|
202
|
+
# ======================================================================
|
|
203
|
+
|
|
204
|
+
async def list_attestations(
|
|
205
|
+
self,
|
|
206
|
+
gate_id: str,
|
|
207
|
+
*,
|
|
208
|
+
passport_id: Optional[str] = None,
|
|
209
|
+
agent_id: Optional[str] = None,
|
|
210
|
+
permission: Optional[str] = None,
|
|
211
|
+
since: Optional[str] = None,
|
|
212
|
+
limit: Optional[int] = None,
|
|
213
|
+
) -> list[dict[str, Any]]:
|
|
214
|
+
"""List attestations (audit log) for a gate."""
|
|
215
|
+
return await self._get(
|
|
216
|
+
f"/api/gates/{gate_id}/attestations",
|
|
217
|
+
params={
|
|
218
|
+
"passport_id": passport_id,
|
|
219
|
+
"agent_id": agent_id,
|
|
220
|
+
"permission": permission,
|
|
221
|
+
"since": since,
|
|
222
|
+
"limit": limit,
|
|
223
|
+
},
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
async def record_attestation(
|
|
227
|
+
self, gate_id: str, **kwargs: Any
|
|
228
|
+
) -> dict[str, Any]:
|
|
229
|
+
"""Record a new attestation."""
|
|
230
|
+
return await self._post(f"/api/gates/{gate_id}/attestations", json=kwargs)
|
|
231
|
+
|
|
232
|
+
# ======================================================================
|
|
233
|
+
# Permission Catalog
|
|
234
|
+
# ======================================================================
|
|
235
|
+
|
|
236
|
+
async def get_catalog(self, gate_id: str) -> dict[str, Any]:
|
|
237
|
+
"""Get the active draft catalog for a gate."""
|
|
238
|
+
return await self._get(f"/api/gates/{gate_id}/catalog")
|
|
239
|
+
|
|
240
|
+
async def create_catalog(self, gate_id: str, **kwargs: Any) -> dict[str, Any]:
|
|
241
|
+
"""Create or update the permission catalog for a gate."""
|
|
242
|
+
return await self._post(f"/api/gates/{gate_id}/catalog", json=kwargs)
|
|
243
|
+
|
|
244
|
+
async def publish_catalog(
|
|
245
|
+
self,
|
|
246
|
+
gate_id: str,
|
|
247
|
+
*,
|
|
248
|
+
change_summary: Optional[str] = None,
|
|
249
|
+
effective_at: Optional[str] = None,
|
|
250
|
+
) -> dict[str, Any]:
|
|
251
|
+
"""Build, sign, and atomically publish a catalog snapshot."""
|
|
252
|
+
body: dict[str, Any] = {}
|
|
253
|
+
if change_summary is not None:
|
|
254
|
+
body["change_summary"] = change_summary
|
|
255
|
+
if effective_at is not None:
|
|
256
|
+
body["effective_at"] = effective_at
|
|
257
|
+
return await self._post(f"/api/gates/{gate_id}/catalog/publish", json=body)
|
|
258
|
+
|
|
259
|
+
async def list_catalog_versions(self, gate_id: str) -> list[dict[str, Any]]:
|
|
260
|
+
"""List all published catalog versions (metadata only)."""
|
|
261
|
+
return await self._get(f"/api/gates/{gate_id}/catalog/versions")
|
|
262
|
+
|
|
263
|
+
async def get_catalog_version(self, gate_id: str, version: int) -> dict[str, Any]:
|
|
264
|
+
"""Get a specific published catalog version."""
|
|
265
|
+
return await self._get(f"/api/gates/{gate_id}/catalog/{version}")
|
|
266
|
+
|
|
267
|
+
async def get_catalog_impact(self, gate_id: str) -> dict[str, Any]:
|
|
268
|
+
"""Get impact analysis for pending catalog changes."""
|
|
269
|
+
return await self._get(f"/api/gates/{gate_id}/catalog/impact")
|
|
270
|
+
|
|
271
|
+
# ======================================================================
|
|
272
|
+
# Gate Check (Authorization)
|
|
273
|
+
# ======================================================================
|
|
274
|
+
|
|
275
|
+
async def check_gate(
|
|
276
|
+
self,
|
|
277
|
+
gate_id: str,
|
|
278
|
+
*,
|
|
279
|
+
action: str,
|
|
280
|
+
passport_id: Optional[str] = None,
|
|
281
|
+
target: Optional[str] = None,
|
|
282
|
+
) -> dict[str, Any]:
|
|
283
|
+
"""Test a passport against a gate."""
|
|
284
|
+
body: dict[str, Any] = {"action": action}
|
|
285
|
+
if passport_id is not None:
|
|
286
|
+
body["passport_id"] = passport_id
|
|
287
|
+
if target is not None:
|
|
288
|
+
body["target"] = target
|
|
289
|
+
return await self._post(f"/api/gates/{gate_id}/check", json=body)
|
|
290
|
+
|
|
291
|
+
# ======================================================================
|
|
292
|
+
# Constraints
|
|
293
|
+
# ======================================================================
|
|
294
|
+
|
|
295
|
+
async def get_constraints(self, passport_id: str) -> dict[str, Any]:
|
|
296
|
+
"""Get constraints for a passport."""
|
|
297
|
+
return await self._get(f"/api/passports/{passport_id}/constraints")
|
|
298
|
+
|
|
299
|
+
async def set_constraints(
|
|
300
|
+
self, passport_id: str, constraints: dict[str, Any]
|
|
301
|
+
) -> dict[str, Any]:
|
|
302
|
+
"""Set constraints on a passport."""
|
|
303
|
+
return await self._put(f"/api/passports/{passport_id}/constraints", json=constraints)
|
|
304
|
+
|
|
305
|
+
async def list_constraint_types(
|
|
306
|
+
self, *, category: Optional[str] = None
|
|
307
|
+
) -> list[dict[str, Any]]:
|
|
308
|
+
"""List available constraint type definitions."""
|
|
309
|
+
return await self._get("/api/constraints/types", params={"category": category})
|
|
310
|
+
|
|
311
|
+
async def list_constraint_templates(
|
|
312
|
+
self, *, category: Optional[str] = None
|
|
313
|
+
) -> list[dict[str, Any]]:
|
|
314
|
+
"""List system and user constraint templates."""
|
|
315
|
+
return await self._get("/api/constraint-templates", params={"category": category})
|
|
316
|
+
|
|
317
|
+
async def apply_constraint_template(
|
|
318
|
+
self, passport_id: str, template_slug: str
|
|
319
|
+
) -> dict[str, Any]:
|
|
320
|
+
"""Apply a constraint template to a passport."""
|
|
321
|
+
return await self._post(
|
|
322
|
+
f"/api/passports/{passport_id}/constraints",
|
|
323
|
+
json={"template_slug": template_slug},
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
async def create_constraint_template(self, **kwargs: Any) -> dict[str, Any]:
|
|
327
|
+
"""Create a user constraint template."""
|
|
328
|
+
return await self._post("/api/constraint-templates", json=kwargs)
|
|
329
|
+
|
|
330
|
+
# ======================================================================
|
|
331
|
+
# Enforcement (CEL)
|
|
332
|
+
# ======================================================================
|
|
333
|
+
|
|
334
|
+
async def enforce_action(
|
|
335
|
+
self,
|
|
336
|
+
*,
|
|
337
|
+
passport_id: str,
|
|
338
|
+
action: str,
|
|
339
|
+
target: Optional[str] = None,
|
|
340
|
+
cost_cents: Optional[int] = None,
|
|
341
|
+
metadata: Optional[dict[str, Any]] = None,
|
|
342
|
+
) -> dict[str, Any]:
|
|
343
|
+
"""Evaluate constraints and record an enforcement attestation."""
|
|
344
|
+
body = EnforceRequest(
|
|
345
|
+
passport_id=passport_id,
|
|
346
|
+
action=action,
|
|
347
|
+
target=target,
|
|
348
|
+
cost_cents=cost_cents,
|
|
349
|
+
metadata=metadata,
|
|
350
|
+
)
|
|
351
|
+
return await self._post("/api/enforce", json=body.model_dump(exclude_none=True))
|
|
352
|
+
|
|
353
|
+
async def list_enforcement_attestations(
|
|
354
|
+
self,
|
|
355
|
+
passport_id: str,
|
|
356
|
+
*,
|
|
357
|
+
decision: Optional[str] = None,
|
|
358
|
+
limit: Optional[int] = None,
|
|
359
|
+
) -> list[dict[str, Any]]:
|
|
360
|
+
"""List enforcement attestations for a passport."""
|
|
361
|
+
return await self._get(
|
|
362
|
+
f"/api/passports/{passport_id}/enforcement",
|
|
363
|
+
params={"decision": decision, "limit": limit},
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
async def get_enforcement_attestation(self, attestation_id: str) -> dict[str, Any]:
|
|
367
|
+
"""Get a single enforcement attestation by ID."""
|
|
368
|
+
return await self._get(f"/api/enforcement/{attestation_id}")
|
|
369
|
+
|
|
370
|
+
async def verify_enforcement_attestation(self, attestation_id: str) -> dict[str, Any]:
|
|
371
|
+
"""Verify the cryptographic signature of an enforcement attestation."""
|
|
372
|
+
return await self._post(f"/api/enforcement/{attestation_id}/verify")
|
|
373
|
+
|
|
374
|
+
# ======================================================================
|
|
375
|
+
# Anonymous Access
|
|
376
|
+
# ======================================================================
|
|
377
|
+
|
|
378
|
+
async def get_anonymous_policy(self, gate_id: str) -> dict[str, Any]:
|
|
379
|
+
"""Get the anonymous access policy for a gate."""
|
|
380
|
+
return await self._get(f"/api/gates/{gate_id}/anonymous-policy")
|
|
381
|
+
|
|
382
|
+
async def set_anonymous_policy(
|
|
383
|
+
self, gate_id: str, **kwargs: Any
|
|
384
|
+
) -> dict[str, Any]:
|
|
385
|
+
"""Configure anonymous access policy on a gate."""
|
|
386
|
+
return await self._put(f"/api/gates/{gate_id}/anonymous-policy", json=kwargs)
|
|
387
|
+
|
|
388
|
+
async def get_anonymous_log(self, gate_id: str) -> list[dict[str, Any]]:
|
|
389
|
+
"""Get the anonymous access audit log for a gate."""
|
|
390
|
+
return await self._get(f"/api/gates/{gate_id}/anonymous-log")
|
|
391
|
+
|
|
392
|
+
# ======================================================================
|
|
393
|
+
# Cumulative State
|
|
394
|
+
# ======================================================================
|
|
395
|
+
|
|
396
|
+
async def get_cumulative_state(self, passport_id: str) -> dict[str, Any]:
|
|
397
|
+
"""Get spending and rate limit state for a passport."""
|
|
398
|
+
return await self._get(f"/api/passports/{passport_id}/state")
|
|
399
|
+
|
|
400
|
+
async def reset_cumulative_state(
|
|
401
|
+
self,
|
|
402
|
+
passport_id: str,
|
|
403
|
+
*,
|
|
404
|
+
window_type: Optional[str] = None,
|
|
405
|
+
) -> dict[str, Any]:
|
|
406
|
+
"""Reset cumulative spending and rate counters."""
|
|
407
|
+
body: dict[str, Any] = {}
|
|
408
|
+
if window_type is not None:
|
|
409
|
+
body["window_type"] = window_type
|
|
410
|
+
return await self._post(f"/api/passports/{passport_id}/state/reset", json=body)
|
|
411
|
+
|
|
412
|
+
# ======================================================================
|
|
413
|
+
# Commerce: Consumption Attestations
|
|
414
|
+
# ======================================================================
|
|
415
|
+
|
|
416
|
+
async def issue_consumption_attestation(
|
|
417
|
+
self,
|
|
418
|
+
*,
|
|
419
|
+
passport_id: str,
|
|
420
|
+
gate_id: str,
|
|
421
|
+
action: str,
|
|
422
|
+
outcome: str,
|
|
423
|
+
quantity: int = 1,
|
|
424
|
+
agent_pop: Optional[dict[str, Any]] = None,
|
|
425
|
+
request_payload_hash: Optional[str] = None,
|
|
426
|
+
response_payload_hash: Optional[str] = None,
|
|
427
|
+
metadata: Optional[dict[str, Any]] = None,
|
|
428
|
+
) -> dict[str, Any]:
|
|
429
|
+
"""Issue a consumption attestation (bilateral metering)."""
|
|
430
|
+
body: dict[str, Any] = {
|
|
431
|
+
"passport_id": passport_id,
|
|
432
|
+
"gate_id": gate_id,
|
|
433
|
+
"action": action,
|
|
434
|
+
"outcome": outcome,
|
|
435
|
+
"quantity": quantity,
|
|
436
|
+
}
|
|
437
|
+
if agent_pop is not None:
|
|
438
|
+
body["agent_pop"] = agent_pop
|
|
439
|
+
if request_payload_hash is not None:
|
|
440
|
+
body["request_payload_hash"] = request_payload_hash
|
|
441
|
+
if response_payload_hash is not None:
|
|
442
|
+
body["response_payload_hash"] = response_payload_hash
|
|
443
|
+
if metadata is not None:
|
|
444
|
+
body["metadata"] = metadata
|
|
445
|
+
return await self._post("/api/consume", json=body)
|
|
446
|
+
|
|
447
|
+
# ======================================================================
|
|
448
|
+
# Commerce: Discovery
|
|
449
|
+
# ======================================================================
|
|
450
|
+
|
|
451
|
+
async def discover_services(
|
|
452
|
+
self,
|
|
453
|
+
capability: str,
|
|
454
|
+
*,
|
|
455
|
+
max_price_cents: Optional[int] = None,
|
|
456
|
+
min_uptime_bp: Optional[int] = None,
|
|
457
|
+
max_response_time_ms: Optional[int] = None,
|
|
458
|
+
pricing_model: Optional[str] = None,
|
|
459
|
+
sort: Optional[str] = None,
|
|
460
|
+
limit: int = 20,
|
|
461
|
+
offset: int = 0,
|
|
462
|
+
) -> dict[str, Any]:
|
|
463
|
+
"""Discover services by capability (public, no auth required)."""
|
|
464
|
+
return await self._get(
|
|
465
|
+
"/api/discover",
|
|
466
|
+
params={
|
|
467
|
+
"capability": capability,
|
|
468
|
+
"max_price_cents": max_price_cents,
|
|
469
|
+
"min_uptime_bp": min_uptime_bp,
|
|
470
|
+
"max_response_time_ms": max_response_time_ms,
|
|
471
|
+
"pricing_model": pricing_model,
|
|
472
|
+
"sort": sort,
|
|
473
|
+
"limit": limit,
|
|
474
|
+
"offset": offset,
|
|
475
|
+
},
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
# ======================================================================
|
|
479
|
+
# Commerce: Settlement & Billing
|
|
480
|
+
# ======================================================================
|
|
481
|
+
|
|
482
|
+
async def generate_settlement(
|
|
483
|
+
self,
|
|
484
|
+
*,
|
|
485
|
+
gate_id: str,
|
|
486
|
+
period_type: str,
|
|
487
|
+
period_start: str,
|
|
488
|
+
period_end: str,
|
|
489
|
+
agent_id: Optional[str] = None,
|
|
490
|
+
) -> dict[str, Any]:
|
|
491
|
+
"""Generate a billing settlement for a period."""
|
|
492
|
+
body = SettlementRequest(
|
|
493
|
+
gate_id=gate_id,
|
|
494
|
+
period_type=period_type, # type: ignore[arg-type]
|
|
495
|
+
period_start=period_start,
|
|
496
|
+
period_end=period_end,
|
|
497
|
+
agent_id=agent_id,
|
|
498
|
+
)
|
|
499
|
+
return await self._post("/api/billing", json=body.model_dump(exclude_none=True))
|
|
500
|
+
|
|
501
|
+
async def list_settlements(
|
|
502
|
+
self,
|
|
503
|
+
*,
|
|
504
|
+
gate_id: Optional[str] = None,
|
|
505
|
+
agent_id: Optional[str] = None,
|
|
506
|
+
period_type: Optional[str] = None,
|
|
507
|
+
status: Optional[str] = None,
|
|
508
|
+
from_date: Optional[str] = None,
|
|
509
|
+
to_date: Optional[str] = None,
|
|
510
|
+
limit: int = 20,
|
|
511
|
+
offset: int = 0,
|
|
512
|
+
) -> dict[str, Any]:
|
|
513
|
+
"""List settlement summaries."""
|
|
514
|
+
return await self._get(
|
|
515
|
+
"/api/billing",
|
|
516
|
+
params={
|
|
517
|
+
"gate_id": gate_id,
|
|
518
|
+
"agent_id": agent_id,
|
|
519
|
+
"period_type": period_type,
|
|
520
|
+
"status": status,
|
|
521
|
+
"from": from_date,
|
|
522
|
+
"to": to_date,
|
|
523
|
+
"limit": limit,
|
|
524
|
+
"offset": offset,
|
|
525
|
+
},
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
async def get_settlement(self, settlement_id: str) -> dict[str, Any]:
|
|
529
|
+
"""Get a settlement by ID."""
|
|
530
|
+
return await self._get(f"/api/billing/{settlement_id}")
|
|
531
|
+
|
|
532
|
+
async def update_settlement_status(
|
|
533
|
+
self, settlement_id: str, status: str
|
|
534
|
+
) -> dict[str, Any]:
|
|
535
|
+
"""Transition a settlement to a new status."""
|
|
536
|
+
return await self._post(f"/api/billing/{settlement_id}/status", json={"status": status})
|
|
537
|
+
|
|
538
|
+
# ======================================================================
|
|
539
|
+
# Commerce: SLA Compliance
|
|
540
|
+
# ======================================================================
|
|
541
|
+
|
|
542
|
+
async def get_sla_compliance(
|
|
543
|
+
self,
|
|
544
|
+
gate_id: str,
|
|
545
|
+
*,
|
|
546
|
+
period_start: str,
|
|
547
|
+
period_end: str,
|
|
548
|
+
permission_key: Optional[str] = None,
|
|
549
|
+
) -> dict[str, Any]:
|
|
550
|
+
"""Get SLA compliance metrics for a gate."""
|
|
551
|
+
return await self._get(
|
|
552
|
+
f"/api/gates/{gate_id}/sla",
|
|
553
|
+
params={
|
|
554
|
+
"period_start": period_start,
|
|
555
|
+
"period_end": period_end,
|
|
556
|
+
"permission_key": permission_key,
|
|
557
|
+
},
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
# ======================================================================
|
|
561
|
+
# Dry-Run Authorization
|
|
562
|
+
# ======================================================================
|
|
563
|
+
|
|
564
|
+
async def authorize_dry_run(
|
|
565
|
+
self,
|
|
566
|
+
*,
|
|
567
|
+
gate_id: str,
|
|
568
|
+
passport: dict[str, Any],
|
|
569
|
+
requested_permission: str,
|
|
570
|
+
requested_constraints: Optional[dict[str, Any]] = None,
|
|
571
|
+
) -> dict[str, Any]:
|
|
572
|
+
"""Test authorization without executing."""
|
|
573
|
+
body: dict[str, Any] = {
|
|
574
|
+
"gate_id": gate_id,
|
|
575
|
+
"passport": passport,
|
|
576
|
+
"requested_permission": requested_permission,
|
|
577
|
+
}
|
|
578
|
+
if requested_constraints is not None:
|
|
579
|
+
body["requested_constraints"] = requested_constraints
|
|
580
|
+
return await self._post("/api/authorize/dry-run", json=body)
|
|
581
|
+
|
|
582
|
+
# ======================================================================
|
|
583
|
+
# API Keys
|
|
584
|
+
# ======================================================================
|
|
585
|
+
|
|
586
|
+
async def list_api_keys(self) -> list[dict[str, Any]]:
|
|
587
|
+
"""List your API keys."""
|
|
588
|
+
return await self._get("/api/users/api-keys")
|
|
589
|
+
|
|
590
|
+
async def create_api_key(
|
|
591
|
+
self,
|
|
592
|
+
*,
|
|
593
|
+
name: str,
|
|
594
|
+
scopes: Optional[list[str]] = None,
|
|
595
|
+
) -> dict[str, Any]:
|
|
596
|
+
"""Create a new API key."""
|
|
597
|
+
body: dict[str, Any] = {"name": name}
|
|
598
|
+
if scopes is not None:
|
|
599
|
+
body["scopes"] = scopes
|
|
600
|
+
return await self._post("/api/users/api-keys", json=body)
|
|
601
|
+
|
|
602
|
+
async def revoke_api_key(self, key_id: str) -> dict[str, Any]:
|
|
603
|
+
"""Revoke an API key."""
|
|
604
|
+
return await self._delete(f"/api/users/api-keys/{key_id}")
|