@adhd/apigen-python-env 0.2.0 → 0.2.1
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.
- package/CHANGELOG.md +8 -0
- package/index.js +6 -6
- package/index.mjs +56 -53
- package/lib/python-env.d.ts +14 -4
- package/package.json +2 -2
- package/python/apigen_logical.py +628 -0
- package/python/apigen_python/__init__.py +1 -0
- package/python/apigen_python/errors.py +99 -0
- package/python/apigen_python/extractor.py +578 -0
- package/python/apigen_python/flask_server.py +748 -0
- package/python/apigen_python/gateway_adapter.py +260 -0
- package/python/apigen_python/grpc_server.py +954 -0
- package/python/apigen_python/plugin_echo.py +100 -0
- package/python/apigen_python/runtime.py +228 -0
- package/python/apigen_python/validator.py +135 -0
- package/python/conformance_vectors.json +865 -0
- package/python/pyproject.toml +59 -0
- package/python/run_tests.py +2118 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""
|
|
2
|
+
apigen_python.plugin_echo — trivial echo plugin for round-trip testing (SPEC §14).
|
|
3
|
+
|
|
4
|
+
This is the "one echo plugin" required by the host contract: a simple Python
|
|
5
|
+
function that the gateway can call to prove the full IPC round-trip works.
|
|
6
|
+
|
|
7
|
+
The echo operation accepts any input dict and returns it unchanged together with
|
|
8
|
+
a 'host' marker, proving that the Python runtime received the call and responded.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def echo(message: str = "") -> dict[str, Any]:
|
|
17
|
+
"""Echo the message back with a host marker.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
message: Any string message.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
A dict containing the echoed message and the host identifier.
|
|
24
|
+
"""
|
|
25
|
+
return {
|
|
26
|
+
"message": message,
|
|
27
|
+
"host": "python",
|
|
28
|
+
"echo": True,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def ping() -> dict[str, Any]:
|
|
33
|
+
"""Health-check ping — returns a minimal ok payload.
|
|
34
|
+
|
|
35
|
+
Used by the gateway adapter's readiness probe.
|
|
36
|
+
"""
|
|
37
|
+
return {"status": "ok", "host": "python"}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
# Plugin operation descriptors — used by the gateway adapter to register these
|
|
42
|
+
# fns without running a full extraction pass.
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
ECHO_OPERATION: dict[str, Any] = {
|
|
46
|
+
"id": "echo/echo",
|
|
47
|
+
"host": "python",
|
|
48
|
+
"namespace": {"raw": "echo", "words": ["echo"]},
|
|
49
|
+
"path": [{"raw": "echo", "words": ["echo"]}],
|
|
50
|
+
"kind": "action",
|
|
51
|
+
"async": False,
|
|
52
|
+
"streaming": False,
|
|
53
|
+
"safe": False,
|
|
54
|
+
"input": {
|
|
55
|
+
"type": "object",
|
|
56
|
+
"properties": {
|
|
57
|
+
"message": {"type": "string"},
|
|
58
|
+
},
|
|
59
|
+
"required": [],
|
|
60
|
+
},
|
|
61
|
+
"output": {
|
|
62
|
+
"type": "object",
|
|
63
|
+
"properties": {
|
|
64
|
+
"message": {"type": "string"},
|
|
65
|
+
"host": {"type": "string"},
|
|
66
|
+
"echo": {"type": "boolean"},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
"envelope": {},
|
|
70
|
+
"typeText": None,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
PING_OPERATION: dict[str, Any] = {
|
|
74
|
+
"id": "echo/ping",
|
|
75
|
+
"host": "python",
|
|
76
|
+
"namespace": {"raw": "echo", "words": ["echo"]},
|
|
77
|
+
"path": [{"raw": "ping", "words": ["ping"]}],
|
|
78
|
+
"kind": "query",
|
|
79
|
+
"async": False,
|
|
80
|
+
"streaming": False,
|
|
81
|
+
"safe": True,
|
|
82
|
+
"input": {"type": "object", "properties": {}},
|
|
83
|
+
"output": {
|
|
84
|
+
"type": "object",
|
|
85
|
+
"properties": {
|
|
86
|
+
"status": {"type": "string"},
|
|
87
|
+
"host": {"type": "string"},
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
"envelope": {},
|
|
91
|
+
"typeText": None,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# Registry entries used by the gateway adapter.
|
|
95
|
+
ECHO_REGISTRY: dict[str, Any] = {
|
|
96
|
+
"echo/echo": echo,
|
|
97
|
+
"echo/ping": ping,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
ECHO_OPERATIONS: list[dict[str, Any]] = [ECHO_OPERATION, PING_OPERATION]
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""
|
|
2
|
+
apigen_python.runtime — Python host harness: invoke / validate / dispatch (SPEC §8 / §14).
|
|
3
|
+
|
|
4
|
+
The runtime receives a HostRequest (from the gateway or directly from a plugin),
|
|
5
|
+
validates input against the Operation's JSON-Schema, dispatches to the Python fn,
|
|
6
|
+
and maps exceptions to §9 ApiError codes.
|
|
7
|
+
|
|
8
|
+
Lifecycle:
|
|
9
|
+
runtime = Runtime(fn_registry)
|
|
10
|
+
result = await runtime.invoke(request, signal) # async path
|
|
11
|
+
result = runtime.invoke_sync(request) # sync path (for tests / WSGI)
|
|
12
|
+
|
|
13
|
+
The fn_registry maps operation id → callable.
|
|
14
|
+
|
|
15
|
+
SPEC §6: validation is a fast-fail pre-filter, NOT the authoritative type gate.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import asyncio
|
|
21
|
+
import inspect
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from .errors import ApiError, ApiErrorCode
|
|
25
|
+
from .validator import validate, ValidationError
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# HostRequest — matches gateway.ts HostRequest (the cross-language IPC contract)
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
class HostRequest:
|
|
33
|
+
"""The single request the gateway forwards to the Python host runtime.
|
|
34
|
+
|
|
35
|
+
Fields mirror gateway.ts::HostRequest exactly so the IPC adapter can
|
|
36
|
+
deserialise directly into this class.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
operation: dict[str, Any],
|
|
42
|
+
data: dict[str, Any],
|
|
43
|
+
envelope: dict[str, Any],
|
|
44
|
+
transport: str,
|
|
45
|
+
pre_validated: bool = False,
|
|
46
|
+
) -> None:
|
|
47
|
+
self.operation = operation # full §4 Operation dict
|
|
48
|
+
self.data = data # bare domain params (ctx excluded)
|
|
49
|
+
self.envelope = envelope # request side-channel (session, auth, …)
|
|
50
|
+
self.transport = transport # 'http' | 'grpc' | 'mcp' | 'cli'
|
|
51
|
+
self.pre_validated = pre_validated # True when the transport layer already
|
|
52
|
+
# validated wire data before logical decode;
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
# Runtime
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
class Runtime:
|
|
60
|
+
"""Python host harness — validate → dispatch (SPEC §8 / §14).
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
fn_registry: Mapping of operation id → Python callable.
|
|
64
|
+
The callable receives keyword arguments matching the operation's
|
|
65
|
+
input schema properties. The 'ctx' parameter (if present) receives
|
|
66
|
+
the envelope dict.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def __init__(self, fn_registry: dict[str, Any]) -> None:
|
|
70
|
+
self._registry = fn_registry
|
|
71
|
+
|
|
72
|
+
# ------------------------------------------------------------------
|
|
73
|
+
# Core dispatch
|
|
74
|
+
# ------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
def _resolve(self, op_id: str) -> Any:
|
|
77
|
+
"""Look up the function for an operation id.
|
|
78
|
+
|
|
79
|
+
Raises:
|
|
80
|
+
ApiError(not_found): if no function is registered for op_id.
|
|
81
|
+
"""
|
|
82
|
+
fn = self._registry.get(op_id)
|
|
83
|
+
if fn is None:
|
|
84
|
+
raise ApiError("not_found", f"no handler registered for operation '{op_id}'")
|
|
85
|
+
return fn
|
|
86
|
+
|
|
87
|
+
def _validate_input(self, op: dict[str, Any], data: dict[str, Any]) -> None:
|
|
88
|
+
"""Validate *data* against the operation's input schema (SPEC §6).
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
ApiError(invalid_argument): if validation fails.
|
|
92
|
+
"""
|
|
93
|
+
input_schema = op.get("input", {})
|
|
94
|
+
if not input_schema:
|
|
95
|
+
return
|
|
96
|
+
try:
|
|
97
|
+
validate(input_schema, data)
|
|
98
|
+
except ValidationError as exc:
|
|
99
|
+
raise ApiError("invalid_argument", f"input validation failed: {exc}") from exc
|
|
100
|
+
|
|
101
|
+
def _validate_input_if_needed(self, req: "HostRequest") -> None:
|
|
102
|
+
"""Validate request input unless the transport already pre-validated it.
|
|
103
|
+
|
|
104
|
+
When a transport layer (e.g. flask_server) validates wire data BEFORE
|
|
105
|
+
logical-type decoding and marks the request as pre_validated=True, the
|
|
106
|
+
runtime skips its own re-validation. This prevents decoded native
|
|
107
|
+
values (e.g. decimal.Decimal, datetime) from being re-validated against
|
|
108
|
+
the wire schema ({"type":"string","format":"decimal"}) and incorrectly
|
|
109
|
+
failing.
|
|
110
|
+
"""
|
|
111
|
+
if req.pre_validated:
|
|
112
|
+
return
|
|
113
|
+
self._validate_input(req.operation, req.data)
|
|
114
|
+
|
|
115
|
+
def _build_kwargs(self, fn: Any, data: dict[str, Any], envelope: dict[str, Any]) -> dict[str, Any]:
|
|
116
|
+
"""Build keyword-argument dict for the function from data + envelope.
|
|
117
|
+
|
|
118
|
+
If the function has a first parameter named 'ctx', it receives the
|
|
119
|
+
envelope dict. All other parameters are pulled from data.
|
|
120
|
+
"""
|
|
121
|
+
try:
|
|
122
|
+
sig = inspect.signature(fn)
|
|
123
|
+
except (ValueError, TypeError):
|
|
124
|
+
return dict(data)
|
|
125
|
+
|
|
126
|
+
params = list(sig.parameters.values())
|
|
127
|
+
kwargs: dict[str, Any] = {}
|
|
128
|
+
|
|
129
|
+
# Strip 'self' for bound methods.
|
|
130
|
+
if params and params[0].name == "self":
|
|
131
|
+
params = params[1:]
|
|
132
|
+
|
|
133
|
+
# 'ctx' first-param receives the envelope (§4 inv:ctx-name-only).
|
|
134
|
+
if params and params[0].name == "ctx":
|
|
135
|
+
kwargs["ctx"] = envelope
|
|
136
|
+
params = params[1:]
|
|
137
|
+
|
|
138
|
+
# Remaining params from data.
|
|
139
|
+
for p in params:
|
|
140
|
+
if p.kind in (
|
|
141
|
+
inspect.Parameter.VAR_POSITIONAL,
|
|
142
|
+
inspect.Parameter.VAR_KEYWORD,
|
|
143
|
+
):
|
|
144
|
+
continue
|
|
145
|
+
if p.name in data:
|
|
146
|
+
kwargs[p.name] = data[p.name]
|
|
147
|
+
|
|
148
|
+
return kwargs
|
|
149
|
+
|
|
150
|
+
def _call_fn(self, fn: Any, kwargs: dict[str, Any]) -> Any:
|
|
151
|
+
"""Call a synchronous function, mapping exceptions to ApiError."""
|
|
152
|
+
try:
|
|
153
|
+
return fn(**kwargs)
|
|
154
|
+
except ApiError:
|
|
155
|
+
raise
|
|
156
|
+
except TypeError as exc:
|
|
157
|
+
raise ApiError("invalid_argument", f"dispatch type error: {exc}") from exc
|
|
158
|
+
except Exception as exc:
|
|
159
|
+
raise ApiError("internal", f"dispatch error: {exc}") from exc
|
|
160
|
+
|
|
161
|
+
async def _call_fn_async(self, fn: Any, kwargs: dict[str, Any]) -> Any:
|
|
162
|
+
"""Call an async function (coroutine), mapping exceptions to ApiError."""
|
|
163
|
+
try:
|
|
164
|
+
result = fn(**kwargs)
|
|
165
|
+
if inspect.isawaitable(result):
|
|
166
|
+
return await result
|
|
167
|
+
return result
|
|
168
|
+
except ApiError:
|
|
169
|
+
raise
|
|
170
|
+
except TypeError as exc:
|
|
171
|
+
raise ApiError("invalid_argument", f"dispatch type error: {exc}") from exc
|
|
172
|
+
except Exception as exc:
|
|
173
|
+
raise ApiError("internal", f"dispatch error: {exc}") from exc
|
|
174
|
+
|
|
175
|
+
# ------------------------------------------------------------------
|
|
176
|
+
# Public invoke API
|
|
177
|
+
# ------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
async def invoke(self, req: HostRequest, signal: Any = None) -> Any:
|
|
180
|
+
"""Invoke an operation asynchronously.
|
|
181
|
+
|
|
182
|
+
Validates input, resolves the function, dispatches, and returns the result.
|
|
183
|
+
Cancellation via *signal* is not yet propagated to the fn (future work).
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
req: The HostRequest from the gateway or plugin.
|
|
187
|
+
signal: Optional cancellation signal (reserved; not yet consumed).
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
The function's return value (awaited if async).
|
|
191
|
+
|
|
192
|
+
Raises:
|
|
193
|
+
ApiError: on validation failure, not-found, or dispatch error.
|
|
194
|
+
"""
|
|
195
|
+
op = req.operation
|
|
196
|
+
fn = self._resolve(op["id"])
|
|
197
|
+
self._validate_input_if_needed(req)
|
|
198
|
+
kwargs = self._build_kwargs(fn, req.data, req.envelope)
|
|
199
|
+
|
|
200
|
+
if inspect.iscoroutinefunction(fn) or inspect.isasyncgenfunction(fn):
|
|
201
|
+
return await self._call_fn_async(fn, kwargs)
|
|
202
|
+
# Sync fn — run in thread pool to avoid blocking the event loop.
|
|
203
|
+
loop = asyncio.get_event_loop()
|
|
204
|
+
return await loop.run_in_executor(None, lambda: self._call_fn(fn, kwargs))
|
|
205
|
+
|
|
206
|
+
def invoke_sync(self, req: HostRequest) -> Any:
|
|
207
|
+
"""Invoke an operation synchronously (for tests and WSGI contexts).
|
|
208
|
+
|
|
209
|
+
For async functions this runs them in a new event loop; for sync
|
|
210
|
+
functions it calls them directly.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
req: The HostRequest.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
The function's return value.
|
|
217
|
+
|
|
218
|
+
Raises:
|
|
219
|
+
ApiError: on validation failure, not-found, or dispatch error.
|
|
220
|
+
"""
|
|
221
|
+
op = req.operation
|
|
222
|
+
fn = self._resolve(op["id"])
|
|
223
|
+
self._validate_input_if_needed(req)
|
|
224
|
+
kwargs = self._build_kwargs(fn, req.data, req.envelope)
|
|
225
|
+
|
|
226
|
+
if inspect.iscoroutinefunction(fn) or inspect.isasyncgenfunction(fn):
|
|
227
|
+
return asyncio.run(self._call_fn_async(fn, kwargs))
|
|
228
|
+
return self._call_fn(fn, kwargs)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""
|
|
2
|
+
apigen_python.validator — JSON-Schema 2020-12 validation for the Python host (SPEC §6).
|
|
3
|
+
|
|
4
|
+
Implements validation as a fast-fail pre-filter. Tries to use the ``jsonschema``
|
|
5
|
+
library if installed; falls back to a minimal stdlib-only validator that covers
|
|
6
|
+
the common cases used by the conformance vectors.
|
|
7
|
+
|
|
8
|
+
SPEC §6 note: validation is necessary-not-sufficient. The Python host validates
|
|
9
|
+
as a pre-filter; typed dispatch is the authoritative boundary.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
# Try to import jsonschema (optional dependency)
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
import jsonschema # type: ignore[import]
|
|
23
|
+
_HAS_JSONSCHEMA = True
|
|
24
|
+
except ImportError:
|
|
25
|
+
_HAS_JSONSCHEMA = False
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# Minimal stdlib validator (fallback when jsonschema is not installed)
|
|
30
|
+
#
|
|
31
|
+
# Covers: type, required, properties, oneOf, items, additionalProperties.
|
|
32
|
+
# Sufficient for all conformance vector schemas.
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
def _check_type(schema_type: str, value: Any) -> bool:
|
|
36
|
+
type_map = {
|
|
37
|
+
"object": lambda v: isinstance(v, dict),
|
|
38
|
+
"array": lambda v: isinstance(v, list),
|
|
39
|
+
"string": lambda v: isinstance(v, str),
|
|
40
|
+
"integer": lambda v: isinstance(v, int) and not isinstance(v, bool),
|
|
41
|
+
"number": lambda v: isinstance(v, (int, float)) and not isinstance(v, bool),
|
|
42
|
+
"boolean": lambda v: isinstance(v, bool),
|
|
43
|
+
"null": lambda v: v is None,
|
|
44
|
+
}
|
|
45
|
+
checker = type_map.get(schema_type)
|
|
46
|
+
return checker(value) if checker else True
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _minimal_validate(schema: dict[str, Any], value: Any) -> bool:
|
|
50
|
+
"""Validate *value* against *schema* using a minimal subset of JSON Schema.
|
|
51
|
+
|
|
52
|
+
Returns True if valid, False if invalid.
|
|
53
|
+
Does NOT raise; the caller decides how to surface failure.
|
|
54
|
+
"""
|
|
55
|
+
if not isinstance(schema, dict):
|
|
56
|
+
return True # empty / non-schema → accept
|
|
57
|
+
|
|
58
|
+
# type keyword
|
|
59
|
+
schema_type = schema.get("type")
|
|
60
|
+
if schema_type is not None:
|
|
61
|
+
if not _check_type(schema_type, value):
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
# oneOf
|
|
65
|
+
one_of = schema.get("oneOf")
|
|
66
|
+
if one_of is not None:
|
|
67
|
+
matches = sum(1 for s in one_of if _minimal_validate(s, value))
|
|
68
|
+
if matches != 1:
|
|
69
|
+
# Relax to "at least one" (some schemas use oneOf where anyOf is meant).
|
|
70
|
+
if matches == 0:
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
# required + properties (object only)
|
|
74
|
+
if schema_type == "object" and isinstance(value, dict):
|
|
75
|
+
required = schema.get("required", [])
|
|
76
|
+
for key in required:
|
|
77
|
+
if key not in value:
|
|
78
|
+
return False
|
|
79
|
+
properties = schema.get("properties", {})
|
|
80
|
+
for prop_name, prop_schema in properties.items():
|
|
81
|
+
if prop_name in value:
|
|
82
|
+
if not _minimal_validate(prop_schema, value[prop_name]):
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
# items (array only)
|
|
86
|
+
if schema_type == "array" and isinstance(value, list):
|
|
87
|
+
items_schema = schema.get("items")
|
|
88
|
+
if items_schema:
|
|
89
|
+
for item in value:
|
|
90
|
+
if not _minimal_validate(items_schema, item):
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
return True
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
# Public API
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
class ValidationError(Exception):
|
|
101
|
+
"""Raised when input data fails JSON-Schema validation."""
|
|
102
|
+
|
|
103
|
+
def __init__(self, message: str, path: str = "") -> None:
|
|
104
|
+
super().__init__(message)
|
|
105
|
+
self.path = path
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def validate(schema: dict[str, Any], value: Any) -> None:
|
|
109
|
+
"""Validate *value* against *schema*.
|
|
110
|
+
|
|
111
|
+
Uses ``jsonschema`` if installed (full JSON-Schema 2020-12), otherwise falls
|
|
112
|
+
back to the minimal stdlib validator.
|
|
113
|
+
|
|
114
|
+
Raises:
|
|
115
|
+
ValidationError: if the value does not satisfy the schema.
|
|
116
|
+
"""
|
|
117
|
+
if _HAS_JSONSCHEMA:
|
|
118
|
+
try:
|
|
119
|
+
jsonschema.validate(value, schema)
|
|
120
|
+
except jsonschema.ValidationError as exc:
|
|
121
|
+
raise ValidationError(str(exc.message)) from exc
|
|
122
|
+
else:
|
|
123
|
+
if not _minimal_validate(schema, value):
|
|
124
|
+
raise ValidationError(
|
|
125
|
+
f"Value does not satisfy schema (minimal validator): {value!r}"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def is_valid(schema: dict[str, Any], value: Any) -> bool:
|
|
130
|
+
"""Return True if *value* satisfies *schema*, False otherwise. Never raises."""
|
|
131
|
+
try:
|
|
132
|
+
validate(schema, value)
|
|
133
|
+
return True
|
|
134
|
+
except ValidationError:
|
|
135
|
+
return False
|