contractgate 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.
- contractgate/__init__.py +107 -0
- contractgate/_transport.py +280 -0
- contractgate/_version.py +1 -0
- contractgate/async_client.py +205 -0
- contractgate/client.py +237 -0
- contractgate/contract.py +426 -0
- contractgate/exceptions.py +126 -0
- contractgate/models.py +260 -0
- contractgate/validator.py +395 -0
- contractgate-0.1.0.dist-info/METADATA +118 -0
- contractgate-0.1.0.dist-info/RECORD +13 -0
- contractgate-0.1.0.dist-info/WHEEL +4 -0
- contractgate-0.1.0.dist-info/licenses/LICENSE +21 -0
contractgate/__init__.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""ContractGate Python SDK.
|
|
2
|
+
|
|
3
|
+
First-party client and pure-Python validator for the ContractGate
|
|
4
|
+
semantic contract enforcement gateway.
|
|
5
|
+
|
|
6
|
+
Public surface:
|
|
7
|
+
Client, AsyncClient -- HTTP clients (sync, async)
|
|
8
|
+
Contract, CompiledContract -- local contract parse + compile
|
|
9
|
+
FieldDefinition, FieldType -- ontology types
|
|
10
|
+
MetricDefinition, MetricType -- metric types
|
|
11
|
+
Transform, TransformKind, -- RFC-004 declarations (declared, not run)
|
|
12
|
+
MaskStyle
|
|
13
|
+
ValidationResult, Violation, -- validator outputs
|
|
14
|
+
ViolationKind
|
|
15
|
+
BatchIngestResponse, -- HTTP response shapes
|
|
16
|
+
IngestEventResult,
|
|
17
|
+
AuditEntry, ContractResponse,
|
|
18
|
+
VersionResponse, VersionSummary,
|
|
19
|
+
IngestionStats
|
|
20
|
+
ContractGateError, HTTPError, -- error hierarchy
|
|
21
|
+
BadRequestError, AuthError,
|
|
22
|
+
NotFoundError, ConflictError,
|
|
23
|
+
ValidationFailedError,
|
|
24
|
+
ServerError, ConnectionError,
|
|
25
|
+
ContractCompileError
|
|
26
|
+
|
|
27
|
+
See README.md for usage. See ../docs/rfcs/005-python-sdk.md for the
|
|
28
|
+
design rationale.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from contractgate._version import __version__
|
|
32
|
+
from contractgate.async_client import AsyncClient
|
|
33
|
+
from contractgate.client import Client
|
|
34
|
+
from contractgate.contract import (
|
|
35
|
+
CompiledContract,
|
|
36
|
+
Contract,
|
|
37
|
+
FieldDefinition,
|
|
38
|
+
FieldType,
|
|
39
|
+
MaskStyle,
|
|
40
|
+
MetricDefinition,
|
|
41
|
+
MetricType,
|
|
42
|
+
Transform,
|
|
43
|
+
TransformKind,
|
|
44
|
+
)
|
|
45
|
+
from contractgate.exceptions import (
|
|
46
|
+
AuthError,
|
|
47
|
+
BadRequestError,
|
|
48
|
+
ConflictError,
|
|
49
|
+
ConnectionError,
|
|
50
|
+
ContractCompileError,
|
|
51
|
+
ContractGateError,
|
|
52
|
+
HTTPError,
|
|
53
|
+
NotFoundError,
|
|
54
|
+
ServerError,
|
|
55
|
+
ValidationFailedError,
|
|
56
|
+
)
|
|
57
|
+
from contractgate.models import (
|
|
58
|
+
AuditEntry,
|
|
59
|
+
BatchIngestResponse,
|
|
60
|
+
ContractResponse,
|
|
61
|
+
IngestEventResult,
|
|
62
|
+
IngestionStats,
|
|
63
|
+
ValidationResult,
|
|
64
|
+
VersionResponse,
|
|
65
|
+
VersionSummary,
|
|
66
|
+
Violation,
|
|
67
|
+
ViolationKind,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
__all__ = [
|
|
71
|
+
"__version__",
|
|
72
|
+
# Clients
|
|
73
|
+
"Client",
|
|
74
|
+
"AsyncClient",
|
|
75
|
+
# Contract / validator
|
|
76
|
+
"Contract",
|
|
77
|
+
"CompiledContract",
|
|
78
|
+
"FieldDefinition",
|
|
79
|
+
"FieldType",
|
|
80
|
+
"MetricDefinition",
|
|
81
|
+
"MetricType",
|
|
82
|
+
"Transform",
|
|
83
|
+
"TransformKind",
|
|
84
|
+
"MaskStyle",
|
|
85
|
+
# Models
|
|
86
|
+
"ValidationResult",
|
|
87
|
+
"Violation",
|
|
88
|
+
"ViolationKind",
|
|
89
|
+
"BatchIngestResponse",
|
|
90
|
+
"IngestEventResult",
|
|
91
|
+
"AuditEntry",
|
|
92
|
+
"ContractResponse",
|
|
93
|
+
"VersionResponse",
|
|
94
|
+
"VersionSummary",
|
|
95
|
+
"IngestionStats",
|
|
96
|
+
# Errors
|
|
97
|
+
"ContractGateError",
|
|
98
|
+
"HTTPError",
|
|
99
|
+
"BadRequestError",
|
|
100
|
+
"AuthError",
|
|
101
|
+
"NotFoundError",
|
|
102
|
+
"ConflictError",
|
|
103
|
+
"ValidationFailedError",
|
|
104
|
+
"ServerError",
|
|
105
|
+
"ConnectionError",
|
|
106
|
+
"ContractCompileError",
|
|
107
|
+
]
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""Shared transport wiring for sync + async HTTP clients.
|
|
2
|
+
|
|
3
|
+
Centralizes:
|
|
4
|
+
- URL building (``base_url`` + path joining),
|
|
5
|
+
- default headers (``x-api-key``, optional ``x-org-id``,
|
|
6
|
+
``User-Agent``),
|
|
7
|
+
- request shaping for ingest, audit, contract reads,
|
|
8
|
+
- response decode + error mapping (``status_to_exception``).
|
|
9
|
+
|
|
10
|
+
Both ``Client`` (httpx.Client) and ``AsyncClient`` (httpx.AsyncClient)
|
|
11
|
+
delegate request building here so they cannot drift on auth or path
|
|
12
|
+
shape. They differ only in *how* the request is dispatched (sync vs
|
|
13
|
+
``await``).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from typing import Any, Dict, List, Mapping, Optional, Tuple
|
|
21
|
+
|
|
22
|
+
from contractgate._version import __version__
|
|
23
|
+
from contractgate.exceptions import raise_for_status
|
|
24
|
+
|
|
25
|
+
# Default timeout matches the gateway's own 30s upper bound (see
|
|
26
|
+
# ``TimeoutLayer`` in ``src/main.rs``). Callers can override per-call.
|
|
27
|
+
DEFAULT_TIMEOUT_S = 30.0
|
|
28
|
+
|
|
29
|
+
USER_AGENT = f"contractgate-python/{__version__}"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class TransportConfig:
|
|
34
|
+
base_url: str
|
|
35
|
+
api_key: Optional[str]
|
|
36
|
+
org_id: Optional[str]
|
|
37
|
+
timeout: float
|
|
38
|
+
|
|
39
|
+
def headers(self, extra: Optional[Mapping[str, str]] = None) -> Dict[str, str]:
|
|
40
|
+
h: Dict[str, str] = {
|
|
41
|
+
"User-Agent": USER_AGENT,
|
|
42
|
+
"Accept": "application/json",
|
|
43
|
+
}
|
|
44
|
+
if self.api_key:
|
|
45
|
+
h["x-api-key"] = self.api_key
|
|
46
|
+
if self.org_id:
|
|
47
|
+
h["x-org-id"] = self.org_id
|
|
48
|
+
if extra:
|
|
49
|
+
h.update(extra)
|
|
50
|
+
return h
|
|
51
|
+
|
|
52
|
+
def url(self, path: str) -> str:
|
|
53
|
+
# ``base_url`` may or may not have a trailing slash; ``path`` must
|
|
54
|
+
# always start with one. Defensive join — httpx accepts both forms.
|
|
55
|
+
base = self.base_url.rstrip("/")
|
|
56
|
+
if not path.startswith("/"):
|
|
57
|
+
path = "/" + path
|
|
58
|
+
return base + path
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
# Request specs (built sync, dispatched by either client)
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True)
|
|
67
|
+
class RequestSpec:
|
|
68
|
+
"""A fully-built request — what the sync/async client dispatches."""
|
|
69
|
+
|
|
70
|
+
method: str
|
|
71
|
+
url: str
|
|
72
|
+
headers: Dict[str, str]
|
|
73
|
+
params: Optional[Dict[str, Any]]
|
|
74
|
+
json_body: Optional[Any]
|
|
75
|
+
timeout: float
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def build_ingest_request(
|
|
79
|
+
cfg: TransportConfig,
|
|
80
|
+
contract_id: str,
|
|
81
|
+
events: Any,
|
|
82
|
+
*,
|
|
83
|
+
version: Optional[str] = None,
|
|
84
|
+
dry_run: bool = False,
|
|
85
|
+
atomic: bool = False,
|
|
86
|
+
timeout: Optional[float] = None,
|
|
87
|
+
) -> RequestSpec:
|
|
88
|
+
"""Build a POST to ``/ingest/{contract_id}[@version]``.
|
|
89
|
+
|
|
90
|
+
``events`` may be a single dict (the gateway treats a non-array
|
|
91
|
+
body as a one-event batch) or a list. ``version`` becomes the
|
|
92
|
+
``X-Contract-Version`` header so the gateway can resolve to a
|
|
93
|
+
specific pin (header > path-suffix > default-stable, per RFC-002).
|
|
94
|
+
"""
|
|
95
|
+
path = f"/ingest/{contract_id}"
|
|
96
|
+
extra_headers: Dict[str, str] = {}
|
|
97
|
+
if version is not None:
|
|
98
|
+
# Header takes precedence over path-suffix per RFC-002. We use
|
|
99
|
+
# the header form by default — clients that need the @version
|
|
100
|
+
# path form for environments that strip headers can pass
|
|
101
|
+
# ``contract_id="<uuid>@<version>"`` and leave ``version=None``.
|
|
102
|
+
extra_headers["X-Contract-Version"] = version
|
|
103
|
+
params: Dict[str, Any] = {}
|
|
104
|
+
if dry_run:
|
|
105
|
+
params["dry_run"] = "true"
|
|
106
|
+
if atomic:
|
|
107
|
+
params["atomic"] = "true"
|
|
108
|
+
return RequestSpec(
|
|
109
|
+
method="POST",
|
|
110
|
+
url=cfg.url(path),
|
|
111
|
+
headers=cfg.headers(extra_headers),
|
|
112
|
+
params=params or None,
|
|
113
|
+
json_body=events,
|
|
114
|
+
timeout=timeout if timeout is not None else cfg.timeout,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def build_audit_request(
|
|
119
|
+
cfg: TransportConfig,
|
|
120
|
+
*,
|
|
121
|
+
contract_id: Optional[str] = None,
|
|
122
|
+
limit: int = 50,
|
|
123
|
+
offset: int = 0,
|
|
124
|
+
timeout: Optional[float] = None,
|
|
125
|
+
) -> RequestSpec:
|
|
126
|
+
params: Dict[str, Any] = {"limit": str(limit), "offset": str(offset)}
|
|
127
|
+
if contract_id is not None:
|
|
128
|
+
params["contract_id"] = contract_id
|
|
129
|
+
return RequestSpec(
|
|
130
|
+
method="GET",
|
|
131
|
+
url=cfg.url("/audit"),
|
|
132
|
+
headers=cfg.headers(),
|
|
133
|
+
params=params,
|
|
134
|
+
json_body=None,
|
|
135
|
+
timeout=timeout if timeout is not None else cfg.timeout,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def build_get_contract_request(
|
|
140
|
+
cfg: TransportConfig,
|
|
141
|
+
contract_id: str,
|
|
142
|
+
*,
|
|
143
|
+
timeout: Optional[float] = None,
|
|
144
|
+
) -> RequestSpec:
|
|
145
|
+
return RequestSpec(
|
|
146
|
+
method="GET",
|
|
147
|
+
url=cfg.url(f"/contracts/{contract_id}"),
|
|
148
|
+
headers=cfg.headers(),
|
|
149
|
+
params=None,
|
|
150
|
+
json_body=None,
|
|
151
|
+
timeout=timeout if timeout is not None else cfg.timeout,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def build_list_contracts_request(
|
|
156
|
+
cfg: TransportConfig,
|
|
157
|
+
*,
|
|
158
|
+
timeout: Optional[float] = None,
|
|
159
|
+
) -> RequestSpec:
|
|
160
|
+
return RequestSpec(
|
|
161
|
+
method="GET",
|
|
162
|
+
url=cfg.url("/contracts"),
|
|
163
|
+
headers=cfg.headers(),
|
|
164
|
+
params=None,
|
|
165
|
+
json_body=None,
|
|
166
|
+
timeout=timeout if timeout is not None else cfg.timeout,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def build_get_version_request(
|
|
171
|
+
cfg: TransportConfig,
|
|
172
|
+
contract_id: str,
|
|
173
|
+
version: str,
|
|
174
|
+
*,
|
|
175
|
+
timeout: Optional[float] = None,
|
|
176
|
+
) -> RequestSpec:
|
|
177
|
+
return RequestSpec(
|
|
178
|
+
method="GET",
|
|
179
|
+
url=cfg.url(f"/contracts/{contract_id}/versions/{version}"),
|
|
180
|
+
headers=cfg.headers(),
|
|
181
|
+
params=None,
|
|
182
|
+
json_body=None,
|
|
183
|
+
timeout=timeout if timeout is not None else cfg.timeout,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def build_list_versions_request(
|
|
188
|
+
cfg: TransportConfig,
|
|
189
|
+
contract_id: str,
|
|
190
|
+
*,
|
|
191
|
+
timeout: Optional[float] = None,
|
|
192
|
+
) -> RequestSpec:
|
|
193
|
+
return RequestSpec(
|
|
194
|
+
method="GET",
|
|
195
|
+
url=cfg.url(f"/contracts/{contract_id}/versions"),
|
|
196
|
+
headers=cfg.headers(),
|
|
197
|
+
params=None,
|
|
198
|
+
json_body=None,
|
|
199
|
+
timeout=timeout if timeout is not None else cfg.timeout,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def build_latest_stable_request(
|
|
204
|
+
cfg: TransportConfig,
|
|
205
|
+
contract_id: str,
|
|
206
|
+
*,
|
|
207
|
+
timeout: Optional[float] = None,
|
|
208
|
+
) -> RequestSpec:
|
|
209
|
+
return RequestSpec(
|
|
210
|
+
method="GET",
|
|
211
|
+
url=cfg.url(f"/contracts/{contract_id}/versions/latest-stable"),
|
|
212
|
+
headers=cfg.headers(),
|
|
213
|
+
params=None,
|
|
214
|
+
json_body=None,
|
|
215
|
+
timeout=timeout if timeout is not None else cfg.timeout,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def build_global_stats_request(
|
|
220
|
+
cfg: TransportConfig,
|
|
221
|
+
*,
|
|
222
|
+
timeout: Optional[float] = None,
|
|
223
|
+
) -> RequestSpec:
|
|
224
|
+
return RequestSpec(
|
|
225
|
+
method="GET",
|
|
226
|
+
url=cfg.url("/stats"),
|
|
227
|
+
headers=cfg.headers(),
|
|
228
|
+
params=None,
|
|
229
|
+
json_body=None,
|
|
230
|
+
timeout=timeout if timeout is not None else cfg.timeout,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def build_playground_request(
|
|
235
|
+
cfg: TransportConfig,
|
|
236
|
+
yaml_content: str,
|
|
237
|
+
event: Any,
|
|
238
|
+
*,
|
|
239
|
+
timeout: Optional[float] = None,
|
|
240
|
+
) -> RequestSpec:
|
|
241
|
+
return RequestSpec(
|
|
242
|
+
method="POST",
|
|
243
|
+
url=cfg.url("/playground/validate"),
|
|
244
|
+
headers=cfg.headers({"Content-Type": "application/json"}),
|
|
245
|
+
params=None,
|
|
246
|
+
json_body={"yaml_content": yaml_content, "event": event},
|
|
247
|
+
timeout=timeout if timeout is not None else cfg.timeout,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
# ---------------------------------------------------------------------------
|
|
252
|
+
# Response decoding
|
|
253
|
+
# ---------------------------------------------------------------------------
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def decode_response(status: int, raw_body: bytes) -> Tuple[int, Any]:
|
|
257
|
+
"""Decode the JSON body (best-effort) and raise on non-2xx.
|
|
258
|
+
|
|
259
|
+
Returns ``(status, decoded_body)``. ``decoded_body`` is whatever
|
|
260
|
+
JSON parsing produced; if parsing fails the raw text is returned
|
|
261
|
+
instead so the error message stays useful.
|
|
262
|
+
"""
|
|
263
|
+
text = raw_body.decode("utf-8", errors="replace") if raw_body else ""
|
|
264
|
+
body: Any = None
|
|
265
|
+
if text:
|
|
266
|
+
try:
|
|
267
|
+
body = json.loads(text)
|
|
268
|
+
except json.JSONDecodeError:
|
|
269
|
+
body = text
|
|
270
|
+
raise_for_status(status, body)
|
|
271
|
+
return status, body
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def expect_list(body: Any) -> List[Any]:
|
|
275
|
+
"""Coerce a list response, defending against unexpected null."""
|
|
276
|
+
if body is None:
|
|
277
|
+
return []
|
|
278
|
+
if isinstance(body, list):
|
|
279
|
+
return body
|
|
280
|
+
raise ValueError(f"expected list response, got {type(body).__name__}")
|
contractgate/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Asynchronous HTTP client.
|
|
2
|
+
|
|
3
|
+
Thin wrapper over ``httpx.AsyncClient``. Mirrors :py:class:`Client`
|
|
4
|
+
method-for-method; the only difference is that every entry point is
|
|
5
|
+
``async def`` and returns an awaitable.
|
|
6
|
+
|
|
7
|
+
Use as an async context manager so the underlying ``httpx.AsyncClient``
|
|
8
|
+
is closed cleanly:
|
|
9
|
+
|
|
10
|
+
async with AsyncClient(base_url=..., api_key=...) as cg:
|
|
11
|
+
result = await cg.ingest(contract_id="...", events=[...])
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import Any, List, Optional
|
|
17
|
+
|
|
18
|
+
import httpx
|
|
19
|
+
|
|
20
|
+
from contractgate import _transport as _t
|
|
21
|
+
from contractgate.exceptions import ConnectionError as _ConnectionError
|
|
22
|
+
from contractgate.models import (
|
|
23
|
+
AuditEntry,
|
|
24
|
+
BatchIngestResponse,
|
|
25
|
+
ContractResponse,
|
|
26
|
+
IngestionStats,
|
|
27
|
+
VersionResponse,
|
|
28
|
+
VersionSummary,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AsyncClient:
|
|
33
|
+
"""Asynchronous client for the ContractGate gateway."""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
*,
|
|
38
|
+
base_url: str,
|
|
39
|
+
api_key: Optional[str] = None,
|
|
40
|
+
org_id: Optional[str] = None,
|
|
41
|
+
timeout: float = _t.DEFAULT_TIMEOUT_S,
|
|
42
|
+
transport: Optional[httpx.AsyncBaseTransport] = None,
|
|
43
|
+
) -> None:
|
|
44
|
+
self._cfg = _t.TransportConfig(
|
|
45
|
+
base_url=base_url,
|
|
46
|
+
api_key=api_key,
|
|
47
|
+
org_id=org_id,
|
|
48
|
+
timeout=timeout,
|
|
49
|
+
)
|
|
50
|
+
self._http = httpx.AsyncClient(transport=transport, timeout=timeout)
|
|
51
|
+
|
|
52
|
+
# ------------------------------------------------------------------
|
|
53
|
+
# Async context-manager + cleanup
|
|
54
|
+
# ------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
async def aclose(self) -> None:
|
|
57
|
+
await self._http.aclose()
|
|
58
|
+
|
|
59
|
+
async def __aenter__(self) -> "AsyncClient":
|
|
60
|
+
return self
|
|
61
|
+
|
|
62
|
+
async def __aexit__(self, exc_type, exc, tb) -> None:
|
|
63
|
+
await self.aclose()
|
|
64
|
+
|
|
65
|
+
# ------------------------------------------------------------------
|
|
66
|
+
# Ingest
|
|
67
|
+
# ------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
async def ingest(
|
|
70
|
+
self,
|
|
71
|
+
*,
|
|
72
|
+
contract_id: str,
|
|
73
|
+
events: Any,
|
|
74
|
+
version: Optional[str] = None,
|
|
75
|
+
dry_run: bool = False,
|
|
76
|
+
atomic: bool = False,
|
|
77
|
+
timeout: Optional[float] = None,
|
|
78
|
+
) -> BatchIngestResponse:
|
|
79
|
+
spec = _t.build_ingest_request(
|
|
80
|
+
self._cfg,
|
|
81
|
+
contract_id,
|
|
82
|
+
events,
|
|
83
|
+
version=version,
|
|
84
|
+
dry_run=dry_run,
|
|
85
|
+
atomic=atomic,
|
|
86
|
+
timeout=timeout,
|
|
87
|
+
)
|
|
88
|
+
_, body = await self._dispatch(spec)
|
|
89
|
+
return BatchIngestResponse.from_dict(body)
|
|
90
|
+
|
|
91
|
+
# ------------------------------------------------------------------
|
|
92
|
+
# Audit / stats
|
|
93
|
+
# ------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
async def audit(
|
|
96
|
+
self,
|
|
97
|
+
*,
|
|
98
|
+
contract_id: Optional[str] = None,
|
|
99
|
+
limit: int = 50,
|
|
100
|
+
offset: int = 0,
|
|
101
|
+
timeout: Optional[float] = None,
|
|
102
|
+
) -> List[AuditEntry]:
|
|
103
|
+
spec = _t.build_audit_request(
|
|
104
|
+
self._cfg,
|
|
105
|
+
contract_id=contract_id,
|
|
106
|
+
limit=limit,
|
|
107
|
+
offset=offset,
|
|
108
|
+
timeout=timeout,
|
|
109
|
+
)
|
|
110
|
+
_, body = await self._dispatch(spec)
|
|
111
|
+
return [AuditEntry.from_dict(r) for r in _t.expect_list(body)]
|
|
112
|
+
|
|
113
|
+
async def stats(self, *, timeout: Optional[float] = None) -> IngestionStats:
|
|
114
|
+
spec = _t.build_global_stats_request(self._cfg, timeout=timeout)
|
|
115
|
+
_, body = await self._dispatch(spec)
|
|
116
|
+
return IngestionStats.from_dict(body)
|
|
117
|
+
|
|
118
|
+
# ------------------------------------------------------------------
|
|
119
|
+
# Contract reads
|
|
120
|
+
# ------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
async def get_contract(
|
|
123
|
+
self,
|
|
124
|
+
contract_id: str,
|
|
125
|
+
*,
|
|
126
|
+
timeout: Optional[float] = None,
|
|
127
|
+
) -> ContractResponse:
|
|
128
|
+
spec = _t.build_get_contract_request(self._cfg, contract_id, timeout=timeout)
|
|
129
|
+
_, body = await self._dispatch(spec)
|
|
130
|
+
return ContractResponse.from_dict(body)
|
|
131
|
+
|
|
132
|
+
async def list_contracts(
|
|
133
|
+
self, *, timeout: Optional[float] = None
|
|
134
|
+
) -> List[ContractResponse]:
|
|
135
|
+
spec = _t.build_list_contracts_request(self._cfg, timeout=timeout)
|
|
136
|
+
_, body = await self._dispatch(spec)
|
|
137
|
+
return [ContractResponse.from_dict(r) for r in _t.expect_list(body)]
|
|
138
|
+
|
|
139
|
+
async def list_versions(
|
|
140
|
+
self,
|
|
141
|
+
contract_id: str,
|
|
142
|
+
*,
|
|
143
|
+
timeout: Optional[float] = None,
|
|
144
|
+
) -> List[VersionSummary]:
|
|
145
|
+
spec = _t.build_list_versions_request(self._cfg, contract_id, timeout=timeout)
|
|
146
|
+
_, body = await self._dispatch(spec)
|
|
147
|
+
return [VersionSummary.from_dict(r) for r in _t.expect_list(body)]
|
|
148
|
+
|
|
149
|
+
async def get_version(
|
|
150
|
+
self,
|
|
151
|
+
contract_id: str,
|
|
152
|
+
version: str,
|
|
153
|
+
*,
|
|
154
|
+
timeout: Optional[float] = None,
|
|
155
|
+
) -> VersionResponse:
|
|
156
|
+
spec = _t.build_get_version_request(
|
|
157
|
+
self._cfg, contract_id, version, timeout=timeout
|
|
158
|
+
)
|
|
159
|
+
_, body = await self._dispatch(spec)
|
|
160
|
+
return VersionResponse.from_dict(body)
|
|
161
|
+
|
|
162
|
+
async def get_latest_stable(
|
|
163
|
+
self,
|
|
164
|
+
contract_id: str,
|
|
165
|
+
*,
|
|
166
|
+
timeout: Optional[float] = None,
|
|
167
|
+
) -> VersionResponse:
|
|
168
|
+
spec = _t.build_latest_stable_request(self._cfg, contract_id, timeout=timeout)
|
|
169
|
+
_, body = await self._dispatch(spec)
|
|
170
|
+
return VersionResponse.from_dict(body)
|
|
171
|
+
|
|
172
|
+
# ------------------------------------------------------------------
|
|
173
|
+
# Playground
|
|
174
|
+
# ------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
async def playground_validate(
|
|
177
|
+
self,
|
|
178
|
+
*,
|
|
179
|
+
yaml_content: str,
|
|
180
|
+
event: Any,
|
|
181
|
+
timeout: Optional[float] = None,
|
|
182
|
+
) -> Any:
|
|
183
|
+
spec = _t.build_playground_request(
|
|
184
|
+
self._cfg, yaml_content, event, timeout=timeout
|
|
185
|
+
)
|
|
186
|
+
_, body = await self._dispatch(spec)
|
|
187
|
+
return body
|
|
188
|
+
|
|
189
|
+
# ------------------------------------------------------------------
|
|
190
|
+
# Internals
|
|
191
|
+
# ------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
async def _dispatch(self, spec: _t.RequestSpec):
|
|
194
|
+
try:
|
|
195
|
+
r = await self._http.request(
|
|
196
|
+
spec.method,
|
|
197
|
+
spec.url,
|
|
198
|
+
headers=spec.headers,
|
|
199
|
+
params=spec.params,
|
|
200
|
+
json=spec.json_body,
|
|
201
|
+
timeout=spec.timeout,
|
|
202
|
+
)
|
|
203
|
+
except httpx.HTTPError as e:
|
|
204
|
+
raise _ConnectionError(str(e)) from e
|
|
205
|
+
return _t.decode_response(r.status_code, r.content)
|