ucii-sdk 1.0.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.
- ucii/__init__.py +55 -0
- ucii/_transport.py +208 -0
- ucii/authorization.py +94 -0
- ucii/client.py +92 -0
- ucii/credentials.py +360 -0
- ucii/errors.py +68 -0
- ucii/identity.py +133 -0
- ucii/participant.py +31 -0
- ucii/service_entitlement.py +266 -0
- ucii/signing.py +42 -0
- ucii/verification.py +40 -0
- ucii/x402.py +190 -0
- ucii_sdk-1.0.0.dist-info/METADATA +84 -0
- ucii_sdk-1.0.0.dist-info/RECORD +17 -0
- ucii_sdk-1.0.0.dist-info/WHEEL +5 -0
- ucii_sdk-1.0.0.dist-info/licenses/LICENSE +176 -0
- ucii_sdk-1.0.0.dist-info/top_level.txt +1 -0
ucii/__init__.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Public UCII Python SDK.
|
|
2
|
+
|
|
3
|
+
Only publication-approved interoperability contracts are exported here.
|
|
4
|
+
Private UCII trust, authority, enforcement, fleet, adoption, and provenance
|
|
5
|
+
implementation details are intentionally outside this package surface.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .client import AsyncUCIIClient, UCIIClient
|
|
9
|
+
from .errors import (
|
|
10
|
+
UCIIAPIError,
|
|
11
|
+
UCIIAuthenticationError,
|
|
12
|
+
UCIIAuthorizationError,
|
|
13
|
+
UCIIConfigurationError,
|
|
14
|
+
UCIIConflictError,
|
|
15
|
+
UCIIError,
|
|
16
|
+
UCIIPaymentRequiredError,
|
|
17
|
+
UCIIResourceNotFoundError,
|
|
18
|
+
UCIIServiceError,
|
|
19
|
+
UCIITransportError,
|
|
20
|
+
UCIIValidationError,
|
|
21
|
+
)
|
|
22
|
+
from .participant import ParticipantContext
|
|
23
|
+
from .service_entitlement import (
|
|
24
|
+
ENTITLEMENT_PROOF_VERSION,
|
|
25
|
+
EntitlementProof,
|
|
26
|
+
EntitlementProofChallenge,
|
|
27
|
+
create_entitlement_proof,
|
|
28
|
+
entitlement_proof_header_value,
|
|
29
|
+
generate_entitlement_nonce,
|
|
30
|
+
)
|
|
31
|
+
from .signing import SigningProvider
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"AsyncUCIIClient",
|
|
35
|
+
"ENTITLEMENT_PROOF_VERSION",
|
|
36
|
+
"EntitlementProof",
|
|
37
|
+
"EntitlementProofChallenge",
|
|
38
|
+
"ParticipantContext",
|
|
39
|
+
"SigningProvider",
|
|
40
|
+
"UCIIAPIError",
|
|
41
|
+
"UCIIClient",
|
|
42
|
+
"UCIIAuthenticationError",
|
|
43
|
+
"UCIIAuthorizationError",
|
|
44
|
+
"UCIIConfigurationError",
|
|
45
|
+
"UCIIConflictError",
|
|
46
|
+
"UCIIError",
|
|
47
|
+
"UCIIPaymentRequiredError",
|
|
48
|
+
"UCIIResourceNotFoundError",
|
|
49
|
+
"UCIIServiceError",
|
|
50
|
+
"UCIITransportError",
|
|
51
|
+
"UCIIValidationError",
|
|
52
|
+
"create_entitlement_proof",
|
|
53
|
+
"entitlement_proof_header_value",
|
|
54
|
+
"generate_entitlement_nonce",
|
|
55
|
+
]
|
ucii/_transport.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""Internal UCII SDK HTTP transport implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from .errors import (
|
|
10
|
+
UCIIAPIError,
|
|
11
|
+
UCIIAuthenticationError,
|
|
12
|
+
UCIIAuthorizationError,
|
|
13
|
+
UCIIPaymentRequiredError,
|
|
14
|
+
UCIIConflictError,
|
|
15
|
+
UCIIResourceNotFoundError,
|
|
16
|
+
UCIIServiceError,
|
|
17
|
+
UCIITransportError,
|
|
18
|
+
UCIIValidationError,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _error_from_response(response: httpx.Response) -> UCIIAPIError:
|
|
23
|
+
"""Convert an HTTP error response into the public SDK error model."""
|
|
24
|
+
|
|
25
|
+
status_code = response.status_code
|
|
26
|
+
request_id = response.headers.get("x-request-id")
|
|
27
|
+
details: dict[str, Any] | None = None
|
|
28
|
+
message = f"UCII API request failed with status {status_code}"
|
|
29
|
+
error_code: str | None = None
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
payload = response.json()
|
|
33
|
+
except ValueError:
|
|
34
|
+
payload = None
|
|
35
|
+
|
|
36
|
+
if isinstance(payload, dict):
|
|
37
|
+
detail = payload.get("detail")
|
|
38
|
+
|
|
39
|
+
if isinstance(detail, str):
|
|
40
|
+
message = detail
|
|
41
|
+
elif isinstance(detail, dict):
|
|
42
|
+
details = dict(detail)
|
|
43
|
+
message = str(
|
|
44
|
+
detail.get("message")
|
|
45
|
+
or detail.get("detail")
|
|
46
|
+
or message
|
|
47
|
+
)
|
|
48
|
+
error_code = detail.get("code") or detail.get("error_code")
|
|
49
|
+
elif detail is not None:
|
|
50
|
+
details = {"detail": detail}
|
|
51
|
+
|
|
52
|
+
error_code = error_code or payload.get("code") or payload.get("error_code")
|
|
53
|
+
|
|
54
|
+
if details is None:
|
|
55
|
+
extra = payload.get("details")
|
|
56
|
+
if isinstance(extra, dict):
|
|
57
|
+
details = dict(extra)
|
|
58
|
+
|
|
59
|
+
request_id = request_id or payload.get("request_id")
|
|
60
|
+
|
|
61
|
+
error_kwargs = {
|
|
62
|
+
"message": message,
|
|
63
|
+
"error_code": error_code,
|
|
64
|
+
"request_id": request_id,
|
|
65
|
+
"status_code": status_code,
|
|
66
|
+
"details": details,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if status_code == 402:
|
|
70
|
+
return UCIIPaymentRequiredError(**error_kwargs)
|
|
71
|
+
if status_code == 401:
|
|
72
|
+
return UCIIAuthenticationError(**error_kwargs)
|
|
73
|
+
if status_code == 403:
|
|
74
|
+
return UCIIAuthorizationError(**error_kwargs)
|
|
75
|
+
if status_code == 404:
|
|
76
|
+
return UCIIResourceNotFoundError(**error_kwargs)
|
|
77
|
+
if status_code == 409:
|
|
78
|
+
return UCIIConflictError(**error_kwargs)
|
|
79
|
+
if status_code == 422:
|
|
80
|
+
return UCIIValidationError(**error_kwargs)
|
|
81
|
+
if status_code >= 500:
|
|
82
|
+
return UCIIServiceError(**error_kwargs)
|
|
83
|
+
|
|
84
|
+
return UCIIAPIError(**error_kwargs)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class HTTPTransport:
|
|
88
|
+
"""Internal synchronous HTTP transport."""
|
|
89
|
+
|
|
90
|
+
def __init__(
|
|
91
|
+
self,
|
|
92
|
+
*,
|
|
93
|
+
base_url: str,
|
|
94
|
+
api_key: str | None = None,
|
|
95
|
+
timeout: float = 30.0,
|
|
96
|
+
) -> None:
|
|
97
|
+
self.base_url = base_url.rstrip("/")
|
|
98
|
+
self.api_key = api_key
|
|
99
|
+
self.timeout = timeout
|
|
100
|
+
self._client = httpx.Client(
|
|
101
|
+
base_url=self.base_url,
|
|
102
|
+
timeout=self.timeout,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def request(
|
|
106
|
+
self,
|
|
107
|
+
method: str,
|
|
108
|
+
path: str,
|
|
109
|
+
*,
|
|
110
|
+
json: Any | None = None,
|
|
111
|
+
params: Any | None = None,
|
|
112
|
+
headers: dict[str, str] | None = None,
|
|
113
|
+
) -> httpx.Response:
|
|
114
|
+
request_headers = self._headers()
|
|
115
|
+
if headers:
|
|
116
|
+
request_headers.update(headers)
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
response = self._client.request(
|
|
120
|
+
method,
|
|
121
|
+
path,
|
|
122
|
+
json=json,
|
|
123
|
+
params=params,
|
|
124
|
+
headers=request_headers,
|
|
125
|
+
)
|
|
126
|
+
except httpx.HTTPError as exc:
|
|
127
|
+
raise UCIITransportError(str(exc)) from exc
|
|
128
|
+
|
|
129
|
+
if response.is_error:
|
|
130
|
+
raise _error_from_response(response)
|
|
131
|
+
|
|
132
|
+
return response
|
|
133
|
+
|
|
134
|
+
def close(self) -> None:
|
|
135
|
+
self._client.close()
|
|
136
|
+
|
|
137
|
+
def _headers(self) -> dict[str, str]:
|
|
138
|
+
headers = {
|
|
139
|
+
"Accept": "application/json",
|
|
140
|
+
"Content-Type": "application/json",
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if self.api_key:
|
|
144
|
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
145
|
+
|
|
146
|
+
return headers
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class AsyncHTTPTransport:
|
|
150
|
+
"""Internal asynchronous HTTP transport."""
|
|
151
|
+
|
|
152
|
+
def __init__(
|
|
153
|
+
self,
|
|
154
|
+
*,
|
|
155
|
+
base_url: str,
|
|
156
|
+
api_key: str | None = None,
|
|
157
|
+
timeout: float = 30.0,
|
|
158
|
+
) -> None:
|
|
159
|
+
self.base_url = base_url.rstrip("/")
|
|
160
|
+
self.api_key = api_key
|
|
161
|
+
self.timeout = timeout
|
|
162
|
+
self._client = httpx.AsyncClient(
|
|
163
|
+
base_url=self.base_url,
|
|
164
|
+
timeout=self.timeout,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
async def request(
|
|
168
|
+
self,
|
|
169
|
+
method: str,
|
|
170
|
+
path: str,
|
|
171
|
+
*,
|
|
172
|
+
json: Any | None = None,
|
|
173
|
+
params: Any | None = None,
|
|
174
|
+
headers: dict[str, str] | None = None,
|
|
175
|
+
) -> httpx.Response:
|
|
176
|
+
request_headers = self._headers()
|
|
177
|
+
if headers:
|
|
178
|
+
request_headers.update(headers)
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
response = await self._client.request(
|
|
182
|
+
method,
|
|
183
|
+
path,
|
|
184
|
+
json=json,
|
|
185
|
+
params=params,
|
|
186
|
+
headers=request_headers,
|
|
187
|
+
)
|
|
188
|
+
except httpx.HTTPError as exc:
|
|
189
|
+
raise UCIITransportError(str(exc)) from exc
|
|
190
|
+
|
|
191
|
+
if response.is_error:
|
|
192
|
+
raise _error_from_response(response)
|
|
193
|
+
|
|
194
|
+
return response
|
|
195
|
+
|
|
196
|
+
async def close(self) -> None:
|
|
197
|
+
await self._client.aclose()
|
|
198
|
+
|
|
199
|
+
def _headers(self) -> dict[str, str]:
|
|
200
|
+
headers = {
|
|
201
|
+
"Accept": "application/json",
|
|
202
|
+
"Content-Type": "application/json",
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if self.api_key:
|
|
206
|
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
207
|
+
|
|
208
|
+
return headers
|
ucii/authorization.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Public UCII authorization-check boundary."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _payload(
|
|
9
|
+
*,
|
|
10
|
+
token: str,
|
|
11
|
+
identity_id: str,
|
|
12
|
+
credential_fingerprint: str,
|
|
13
|
+
operation: str,
|
|
14
|
+
message: str,
|
|
15
|
+
signature: str,
|
|
16
|
+
) -> dict[str, Any]:
|
|
17
|
+
"""Construct an authorization-check request payload."""
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
"token": token,
|
|
21
|
+
"identity_id": identity_id,
|
|
22
|
+
"credential_fingerprint": credential_fingerprint,
|
|
23
|
+
"operation": operation,
|
|
24
|
+
"message": message,
|
|
25
|
+
"signature": signature,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AuthorizationNamespace:
|
|
30
|
+
"""Thin synchronous non-executing authorization boundary."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, transport: Any) -> None:
|
|
33
|
+
self._transport = transport
|
|
34
|
+
|
|
35
|
+
def check(
|
|
36
|
+
self,
|
|
37
|
+
*,
|
|
38
|
+
token: str,
|
|
39
|
+
identity_id: str,
|
|
40
|
+
credential_fingerprint: str,
|
|
41
|
+
operation: str,
|
|
42
|
+
message: str,
|
|
43
|
+
signature: str,
|
|
44
|
+
) -> dict[str, Any]:
|
|
45
|
+
"""Evaluate authorization without executing the operation."""
|
|
46
|
+
|
|
47
|
+
response = self._transport.request(
|
|
48
|
+
"POST",
|
|
49
|
+
"/v1/authorization/check",
|
|
50
|
+
json=_payload(
|
|
51
|
+
token=token,
|
|
52
|
+
identity_id=identity_id,
|
|
53
|
+
credential_fingerprint=credential_fingerprint,
|
|
54
|
+
operation=operation,
|
|
55
|
+
message=message,
|
|
56
|
+
signature=signature,
|
|
57
|
+
),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
return response.json()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class AsyncAuthorizationNamespace:
|
|
64
|
+
"""Thin asynchronous non-executing authorization boundary."""
|
|
65
|
+
|
|
66
|
+
def __init__(self, transport: Any) -> None:
|
|
67
|
+
self._transport = transport
|
|
68
|
+
|
|
69
|
+
async def check(
|
|
70
|
+
self,
|
|
71
|
+
*,
|
|
72
|
+
token: str,
|
|
73
|
+
identity_id: str,
|
|
74
|
+
credential_fingerprint: str,
|
|
75
|
+
operation: str,
|
|
76
|
+
message: str,
|
|
77
|
+
signature: str,
|
|
78
|
+
) -> dict[str, Any]:
|
|
79
|
+
"""Evaluate authorization without executing the operation."""
|
|
80
|
+
|
|
81
|
+
response = await self._transport.request(
|
|
82
|
+
"POST",
|
|
83
|
+
"/v1/authorization/check",
|
|
84
|
+
json=_payload(
|
|
85
|
+
token=token,
|
|
86
|
+
identity_id=identity_id,
|
|
87
|
+
credential_fingerprint=credential_fingerprint,
|
|
88
|
+
operation=operation,
|
|
89
|
+
message=message,
|
|
90
|
+
signature=signature,
|
|
91
|
+
),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return response.json()
|
ucii/client.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Minimal public UCII SDK client.
|
|
2
|
+
|
|
3
|
+
This client intentionally exposes only publication-approved interoperability
|
|
4
|
+
namespaces. Private UCII trust, authority, enforcement, economic, fleet,
|
|
5
|
+
adoption, and provenance machinery is not part of this client surface.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from .authorization import (
|
|
11
|
+
AsyncAuthorizationNamespace,
|
|
12
|
+
AuthorizationNamespace,
|
|
13
|
+
)
|
|
14
|
+
from ._transport import AsyncHTTPTransport, HTTPTransport
|
|
15
|
+
from .credentials import AsyncCredentialsNamespace, CredentialsNamespace
|
|
16
|
+
from .identity import AsyncIdentityNamespace, IdentityNamespace
|
|
17
|
+
from .verification import AsyncVerificationNamespace, VerificationNamespace
|
|
18
|
+
from .x402 import AsyncX402Namespace, X402Namespace
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class UCIIClient:
|
|
22
|
+
"""Synchronous client for publication-approved UCII namespaces."""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
*,
|
|
27
|
+
base_url: str,
|
|
28
|
+
api_key: str | None = None,
|
|
29
|
+
timeout: float = 30.0,
|
|
30
|
+
) -> None:
|
|
31
|
+
self.base_url = base_url.rstrip("/")
|
|
32
|
+
self.api_key = api_key
|
|
33
|
+
self.timeout = timeout
|
|
34
|
+
|
|
35
|
+
self._transport = HTTPTransport(
|
|
36
|
+
base_url=self.base_url,
|
|
37
|
+
api_key=self.api_key,
|
|
38
|
+
timeout=self.timeout,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
self.identity = IdentityNamespace(self._transport)
|
|
42
|
+
self.authorization = AuthorizationNamespace(self._transport)
|
|
43
|
+
self.credentials = CredentialsNamespace(self._transport)
|
|
44
|
+
self.verification = VerificationNamespace(self._transport)
|
|
45
|
+
self.x402 = X402Namespace(self._transport)
|
|
46
|
+
|
|
47
|
+
def close(self) -> None:
|
|
48
|
+
"""Close the underlying HTTP transport."""
|
|
49
|
+
self._transport.close()
|
|
50
|
+
|
|
51
|
+
def __enter__(self) -> "UCIIClient":
|
|
52
|
+
return self
|
|
53
|
+
|
|
54
|
+
def __exit__(self, exc_type, exc_value, traceback) -> None:
|
|
55
|
+
self.close()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class AsyncUCIIClient:
|
|
59
|
+
"""Asynchronous client for publication-approved UCII namespaces."""
|
|
60
|
+
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
*,
|
|
64
|
+
base_url: str,
|
|
65
|
+
api_key: str | None = None,
|
|
66
|
+
timeout: float = 30.0,
|
|
67
|
+
) -> None:
|
|
68
|
+
self.base_url = base_url.rstrip("/")
|
|
69
|
+
self.api_key = api_key
|
|
70
|
+
self.timeout = timeout
|
|
71
|
+
|
|
72
|
+
self._transport = AsyncHTTPTransport(
|
|
73
|
+
base_url=self.base_url,
|
|
74
|
+
api_key=self.api_key,
|
|
75
|
+
timeout=self.timeout,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
self.identity = AsyncIdentityNamespace(self._transport)
|
|
79
|
+
self.authorization = AsyncAuthorizationNamespace(self._transport)
|
|
80
|
+
self.credentials = AsyncCredentialsNamespace(self._transport)
|
|
81
|
+
self.verification = AsyncVerificationNamespace(self._transport)
|
|
82
|
+
self.x402 = AsyncX402Namespace(self._transport)
|
|
83
|
+
|
|
84
|
+
async def close(self) -> None:
|
|
85
|
+
"""Close the underlying HTTP transport."""
|
|
86
|
+
await self._transport.close()
|
|
87
|
+
|
|
88
|
+
async def __aenter__(self) -> "AsyncUCIIClient":
|
|
89
|
+
return self
|
|
90
|
+
|
|
91
|
+
async def __aexit__(self, exc_type, exc_value, traceback) -> None:
|
|
92
|
+
await self.close()
|