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 ADDED
@@ -0,0 +1,117 @@
1
+ """Modus Python SDK — trust infrastructure for AI agents."""
2
+
3
+ from .async_client import AsyncModusClient
4
+ from .client import ModusClient
5
+ from .crypto import (
6
+ compute_content_hash,
7
+ verify_attestation_signature,
8
+ verify_content_hash,
9
+ verify_ed25519,
10
+ )
11
+ from .exceptions import (
12
+ AuthenticationError,
13
+ AuthorizationError,
14
+ ConflictError,
15
+ NotFoundError,
16
+ ModusError,
17
+ RateLimitError,
18
+ ValidationError,
19
+ )
20
+ from .types import (
21
+ AgentPoP,
22
+ AnonymousPolicy,
23
+ Attestation,
24
+ AttestationRecord,
25
+ CatalogCreate,
26
+ CatalogPermission,
27
+ CatalogSnapshot,
28
+ CheckResult,
29
+ ConsumeRequest,
30
+ ConsumeResult,
31
+ ConstraintTemplate,
32
+ ConstraintType,
33
+ CumulativeState,
34
+ DiscoveryQuery,
35
+ DiscoveryResult,
36
+ EnforceRequest,
37
+ EnforceResult,
38
+ EnforcementAttestation,
39
+ Gate,
40
+ GateCreate,
41
+ GateUpdate,
42
+ Issuer,
43
+ Passport,
44
+ PassportIssue,
45
+ PublishedCatalog,
46
+ PublishResult,
47
+ ReissueResult,
48
+ SettlementRequest,
49
+ SettlementSummary,
50
+ SLAComplianceReport,
51
+ SLAMetric,
52
+ VerifyResult,
53
+ )
54
+
55
+ __all__ = [
56
+ # Clients
57
+ "ModusClient",
58
+ "AsyncModusClient",
59
+ # Exceptions
60
+ "ModusError",
61
+ "AuthenticationError",
62
+ "AuthorizationError",
63
+ "NotFoundError",
64
+ "RateLimitError",
65
+ "ValidationError",
66
+ "ConflictError",
67
+ # Crypto
68
+ "verify_ed25519",
69
+ "compute_content_hash",
70
+ "verify_attestation_signature",
71
+ "verify_content_hash",
72
+ # Types — Gates
73
+ "Gate",
74
+ "GateCreate",
75
+ "GateUpdate",
76
+ # Types — Passports
77
+ "Passport",
78
+ "PassportIssue",
79
+ # Types — Issuers
80
+ "Issuer",
81
+ # Types — Attestations
82
+ "Attestation",
83
+ "AttestationRecord",
84
+ # Types — Catalog
85
+ "CatalogPermission",
86
+ "CatalogCreate",
87
+ "CatalogSnapshot",
88
+ "PublishedCatalog",
89
+ "PublishResult",
90
+ # Types — Constraints
91
+ "ConstraintType",
92
+ "ConstraintTemplate",
93
+ # Types — Enforcement
94
+ "EnforceRequest",
95
+ "EnforceResult",
96
+ "EnforcementAttestation",
97
+ # Types — Anonymous
98
+ "AnonymousPolicy",
99
+ # Types — State
100
+ "CumulativeState",
101
+ # Types — Commerce
102
+ "AgentPoP",
103
+ "ConsumeRequest",
104
+ "ConsumeResult",
105
+ "DiscoveryResult",
106
+ "DiscoveryQuery",
107
+ "SettlementRequest",
108
+ "SettlementSummary",
109
+ "SLAMetric",
110
+ "SLAComplianceReport",
111
+ # Types — Check & Verify
112
+ "CheckResult",
113
+ "ReissueResult",
114
+ "VerifyResult",
115
+ ]
116
+
117
+ __version__ = "1.1.0"
modus/_http.py ADDED
@@ -0,0 +1,79 @@
1
+ """Low-level HTTP transport shared by sync and async clients."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Optional
6
+
7
+ import httpx
8
+
9
+ from .exceptions import (
10
+ AuthenticationError,
11
+ AuthorizationError,
12
+ ConflictError,
13
+ NotFoundError,
14
+ ModusError,
15
+ RateLimitError,
16
+ ValidationError,
17
+ )
18
+
19
+ DEFAULT_BASE_URL = "https://modusoperator.com"
20
+ DEFAULT_TIMEOUT = 30.0
21
+
22
+
23
+ def _raise_for_status(response: httpx.Response) -> None:
24
+ """Map HTTP error status codes to typed exceptions."""
25
+ if response.is_success:
26
+ return
27
+
28
+ try:
29
+ body = response.json()
30
+ message = body.get("message") or body.get("error") or f"API error: {response.status_code}"
31
+ except Exception:
32
+ body = None
33
+ message = f"API error: {response.status_code} {response.reason_phrase}"
34
+
35
+ kwargs: dict[str, Any] = {"status_code": response.status_code, "body": body}
36
+
37
+ if response.status_code == 401:
38
+ raise AuthenticationError(message, **kwargs)
39
+ if response.status_code == 403:
40
+ raise AuthorizationError(message, **kwargs)
41
+ if response.status_code == 404:
42
+ raise NotFoundError(message, **kwargs)
43
+ if response.status_code == 409:
44
+ raise ConflictError(message, **kwargs)
45
+ if response.status_code == 429:
46
+ retry_after = response.headers.get("retry-after")
47
+ raise RateLimitError(
48
+ message,
49
+ retry_after=float(retry_after) if retry_after else None,
50
+ **kwargs,
51
+ )
52
+ if response.status_code in (400, 422):
53
+ raise ValidationError(message, **kwargs)
54
+
55
+ raise ModusError(message, **kwargs)
56
+
57
+
58
+ def _parse_response(response: httpx.Response) -> Any:
59
+ """Parse a successful HTTP response, handling 204 No Content."""
60
+ _raise_for_status(response)
61
+ if response.status_code == 204:
62
+ return {}
63
+ if not response.content:
64
+ return {}
65
+ return response.json()
66
+
67
+
68
+ def _build_headers(api_key: str) -> dict[str, str]:
69
+ return {
70
+ "Authorization": f"Bearer {api_key}",
71
+ "Content-Type": "application/json",
72
+ }
73
+
74
+
75
+ def _clean_params(params: Optional[dict[str, Any]]) -> Optional[dict[str, Any]]:
76
+ """Remove None values from query params."""
77
+ if params is None:
78
+ return None
79
+ return {k: v for k, v in params.items() if v is not None}