tigrbl-core 0.4.2.dev4__py3-none-any.whl → 0.4.3.dev4__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.
- tigrbl_core/_spec/__init__.py +36 -0
- tigrbl_core/_spec/app_spec.py +38 -1
- tigrbl_core/_spec/binding_spec.py +28 -11
- tigrbl_core/_spec/exceptions.py +16 -0
- tigrbl_core/_spec/exposure_policy.py +112 -0
- tigrbl_core/_spec/hook_types.py +2 -2
- tigrbl_core/_spec/op_spec.py +9 -0
- tigrbl_core/_spec/path_spec.py +17 -1
- tigrbl_core/_spec/router_spec.py +7 -0
- tigrbl_core/_spec/table_profile_bindings.py +466 -0
- tigrbl_core/_spec/table_profile_spec.py +536 -0
- tigrbl_core/_spec/table_spec.py +25 -1
- tigrbl_core/_spec/transport_stack.py +120 -0
- tigrbl_core/_spec/well_known_spec.py +69 -0
- tigrbl_core/config/constants.py +3 -0
- tigrbl_core/schema/__init__.py +28 -0
- tigrbl_core/schema/provenance.py +191 -0
- tigrbl_core/schema/provenance_inventory.py +249 -0
- tigrbl_core/schema/spec_json.py +7 -795
- tigrbl_core/schema/surface_chains.json +2842 -0
- {tigrbl_core-0.4.2.dev4.dist-info → tigrbl_core-0.4.3.dev4.dist-info}/METADATA +85 -4
- {tigrbl_core-0.4.2.dev4.dist-info → tigrbl_core-0.4.3.dev4.dist-info}/RECORD +25 -16
- {tigrbl_core-0.4.2.dev4.dist-info → tigrbl_core-0.4.3.dev4.dist-info}/WHEEL +0 -0
- {tigrbl_core-0.4.2.dev4.dist-info → tigrbl_core-0.4.3.dev4.dist-info}/licenses/LICENSE +0 -0
- {tigrbl_core-0.4.2.dev4.dist-info → tigrbl_core-0.4.3.dev4.dist-info}/licenses/NOTICE +0 -0
tigrbl_core/_spec/__init__.py
CHANGED
|
@@ -29,6 +29,8 @@ _EXPORTS = {
|
|
|
29
29
|
"HttpStreamBindingSpec": "binding_spec",
|
|
30
30
|
"SseBindingSpec": "binding_spec",
|
|
31
31
|
"WebTransportBindingSpec": "binding_spec",
|
|
32
|
+
"WELL_KNOWN_PREFIX": "well_known_spec",
|
|
33
|
+
"WellKnownResourceSpec": "well_known_spec",
|
|
32
34
|
"WsBindingSpec": "binding_spec",
|
|
33
35
|
"resolve_rest_nested_prefix": "binding_spec",
|
|
34
36
|
"ColumnSpec": "column_spec",
|
|
@@ -80,6 +82,8 @@ _EXPORTS = {
|
|
|
80
82
|
"validate_app_framing_for_binding": "binding_spec",
|
|
81
83
|
"validate_binding_profile_exchange": "binding_spec",
|
|
82
84
|
"validate_path_binding": "path_spec",
|
|
85
|
+
"normalize_well_known_name": "well_known_spec",
|
|
86
|
+
"well_known_path": "well_known_spec",
|
|
83
87
|
"PHASE": "op_spec",
|
|
84
88
|
"PHASES": "op_spec",
|
|
85
89
|
"HookPhase": "hook_spec",
|
|
@@ -106,7 +110,39 @@ _EXPORTS = {
|
|
|
106
110
|
"TypeAdapter": "datatypes",
|
|
107
111
|
"TypeRegistry": "datatypes",
|
|
108
112
|
"TableSpec": "table_spec",
|
|
113
|
+
"BindingToken": "table_profile_bindings",
|
|
114
|
+
"LoweredBinding": "table_profile_bindings",
|
|
115
|
+
"lower_binding_tokens_for_ops": "table_profile_bindings",
|
|
116
|
+
"lower_default_bindings_for_op": "table_profile_bindings",
|
|
117
|
+
"lower_table_profile_bindings": "table_profile_bindings",
|
|
118
|
+
"TableProfileSpec": "table_profile_spec",
|
|
119
|
+
"BuiltinTableProfile": "table_profile_spec",
|
|
120
|
+
"BUILTIN_TABLE_PROFILE_DEFINITIONS": "table_profile_spec",
|
|
121
|
+
"BUILTIN_TABLE_PROFILE_KINDS": "table_profile_spec",
|
|
122
|
+
"TableProfileError": "table_profile_spec",
|
|
123
|
+
"TableProfileBindingFamily": "table_profile_spec",
|
|
124
|
+
"TableProfileRole": "table_profile_spec",
|
|
125
|
+
"PLAIN_TABLE_PROFILE": "table_profile_spec",
|
|
126
|
+
"CRUD_TABLE_PROFILE": "table_profile_spec",
|
|
127
|
+
"REALTIME_TABLE_PROFILE": "table_profile_spec",
|
|
128
|
+
"coerce_table_profile": "table_profile_spec",
|
|
129
|
+
"get_builtin_table_profile_definition": "table_profile_spec",
|
|
130
|
+
"get_table_profile": "table_profile_spec",
|
|
131
|
+
"iter_builtin_table_profile_definitions": "table_profile_spec",
|
|
132
|
+
"make_builtin_table_profile": "table_profile_spec",
|
|
133
|
+
"register_table_profile": "table_profile_spec",
|
|
109
134
|
"TableRegistrySpec": "table_registry_spec",
|
|
135
|
+
"BINDING_STACK_PROJECTIONS": "transport_stack",
|
|
136
|
+
"BindingStackError": "transport_stack",
|
|
137
|
+
"BindingStackProjection": "transport_stack",
|
|
138
|
+
"binding_stack_maturity": "transport_stack",
|
|
139
|
+
"classify_binding_stack": "transport_stack",
|
|
140
|
+
"compose_h3_binding_projections": "transport_stack",
|
|
141
|
+
"require_binding_stack": "transport_stack",
|
|
142
|
+
"ExposureDecision": "exposure_policy",
|
|
143
|
+
"ExposurePolicyError": "exposure_policy",
|
|
144
|
+
"exposed_surfaces": "exposure_policy",
|
|
145
|
+
"resolve_exposure_policy": "exposure_policy",
|
|
110
146
|
}
|
|
111
147
|
|
|
112
148
|
__all__ = list(_EXPORTS)
|
tigrbl_core/_spec/app_spec.py
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
from dataclasses import dataclass, field
|
|
4
4
|
from typing import Any, Callable, Optional, Sequence
|
|
5
|
+
import warnings
|
|
5
6
|
|
|
6
7
|
from .._spec.engine_spec import EngineCfg, EngineSpec
|
|
7
8
|
from .._spec.monotone import as_tuple, merge_mro_sequence_attr
|
|
8
9
|
from .._spec.response_spec import ResponseSpec
|
|
10
|
+
from .._spec.well_known_spec import WellKnownResourceSpec
|
|
9
11
|
from .serde import SerdeMixin
|
|
10
12
|
|
|
11
13
|
|
|
@@ -45,12 +47,15 @@ def normalize_app_spec(spec: "AppSpec") -> "AppSpec":
|
|
|
45
47
|
title=str(spec.title or "Tigrbl"),
|
|
46
48
|
description=spec.description,
|
|
47
49
|
version=str(spec.version or "0.1.0"),
|
|
48
|
-
execution_backend=
|
|
50
|
+
execution_backend=normalize_execution_backend(
|
|
51
|
+
getattr(spec, "execution_backend", None),
|
|
52
|
+
),
|
|
49
53
|
engine=spec.engine,
|
|
50
54
|
engine_name=spec.engine_name,
|
|
51
55
|
engines=_seqify(spec.engines),
|
|
52
56
|
routers=routers,
|
|
53
57
|
ops=ops,
|
|
58
|
+
well_known=_seqify(spec.well_known),
|
|
54
59
|
tables=tables,
|
|
55
60
|
schemas=_seqify(spec.schemas),
|
|
56
61
|
hooks=_seqify(spec.hooks),
|
|
@@ -64,6 +69,23 @@ def normalize_app_spec(spec: "AppSpec") -> "AppSpec":
|
|
|
64
69
|
)
|
|
65
70
|
|
|
66
71
|
|
|
72
|
+
def normalize_execution_backend(value: Any) -> str:
|
|
73
|
+
lowered = str(value or "auto").strip().lower()
|
|
74
|
+
if not lowered:
|
|
75
|
+
return "auto"
|
|
76
|
+
if lowered == "rust":
|
|
77
|
+
warnings.warn(
|
|
78
|
+
"AppSpec.execution_backend='rust' is deprecated; "
|
|
79
|
+
"Tigrbl runtime execution is Python-only.",
|
|
80
|
+
DeprecationWarning,
|
|
81
|
+
stacklevel=2,
|
|
82
|
+
)
|
|
83
|
+
return "python"
|
|
84
|
+
if lowered in {"auto", "python"}:
|
|
85
|
+
return lowered
|
|
86
|
+
raise ValueError(f"unsupported execution backend: {value!r}")
|
|
87
|
+
|
|
88
|
+
|
|
67
89
|
@dataclass(eq=False)
|
|
68
90
|
class AppSpec(SerdeMixin):
|
|
69
91
|
"""
|
|
@@ -83,6 +105,7 @@ class AppSpec(SerdeMixin):
|
|
|
83
105
|
|
|
84
106
|
# NEW: orchestration/topology knobs
|
|
85
107
|
ops: Sequence[Any] = field(default_factory=tuple) # op descriptors or specs
|
|
108
|
+
well_known: Sequence[WellKnownResourceSpec] = field(default_factory=tuple)
|
|
86
109
|
tables: Sequence[Any] = field(default_factory=tuple) # table refs owned by app
|
|
87
110
|
schemas: Sequence[Any] = field(default_factory=tuple) # schema classes/defs
|
|
88
111
|
hooks: Sequence[Callable[..., Any]] = field(default_factory=tuple)
|
|
@@ -103,9 +126,11 @@ class AppSpec(SerdeMixin):
|
|
|
103
126
|
lifespan: Optional[Callable[..., Any]] = None
|
|
104
127
|
|
|
105
128
|
def __post_init__(self) -> None:
|
|
129
|
+
self.execution_backend = normalize_execution_backend(self.execution_backend)
|
|
106
130
|
self.engines = _seqify(self.engines)
|
|
107
131
|
self.routers = _seqify(self.routers)
|
|
108
132
|
self.ops = _seqify(self.ops)
|
|
133
|
+
self.well_known = _seqify(self.well_known)
|
|
109
134
|
self.tables = _seqify(self.tables)
|
|
110
135
|
self.schemas = _seqify(self.schemas)
|
|
111
136
|
self.hooks = _seqify(self.hooks)
|
|
@@ -113,6 +138,14 @@ class AppSpec(SerdeMixin):
|
|
|
113
138
|
self.deps = _seqify(self.deps)
|
|
114
139
|
self.middlewares = _seqify(self.middlewares)
|
|
115
140
|
|
|
141
|
+
for resource in self.well_known:
|
|
142
|
+
if isinstance(resource, str):
|
|
143
|
+
raise TypeError("AppSpec.well_known entries must be nested specs, not strings.")
|
|
144
|
+
if not isinstance(resource, WellKnownResourceSpec):
|
|
145
|
+
raise TypeError(
|
|
146
|
+
"AppSpec.well_known entries must be WellKnownResourceSpec; "
|
|
147
|
+
f"got {type(resource).__name__}."
|
|
148
|
+
)
|
|
116
149
|
validate_engine_inventory(self.engines)
|
|
117
150
|
validate_engine_name_binding(
|
|
118
151
|
self.engine_name,
|
|
@@ -172,6 +205,8 @@ class AppSpec(SerdeMixin):
|
|
|
172
205
|
|
|
173
206
|
@classmethod
|
|
174
207
|
def from_dict(cls, payload: dict[str, Any]) -> "AppSpec":
|
|
208
|
+
if "routes" in payload:
|
|
209
|
+
raise ValueError("AppSpec does not accept 'routes'; use path-owned specs.")
|
|
175
210
|
return super().from_dict(payload)
|
|
176
211
|
|
|
177
212
|
@classmethod
|
|
@@ -255,6 +290,7 @@ class AppSpec(SerdeMixin):
|
|
|
255
290
|
or ()
|
|
256
291
|
),
|
|
257
292
|
ops=tuple(merge_seq_attr(app, "OPS") or ()),
|
|
293
|
+
well_known=tuple(merge_seq_attr(app, "WELL_KNOWN") or ()),
|
|
258
294
|
tables=tuple(merge_seq_attr(app, "TABLES") or ()),
|
|
259
295
|
schemas=tuple(merge_seq_attr(app, "SCHEMAS") or ()),
|
|
260
296
|
hooks=tuple(merge_seq_attr(app, "HOOKS") or ()),
|
|
@@ -277,6 +313,7 @@ class AppSpec(SerdeMixin):
|
|
|
277
313
|
engines=tuple(spec.engines or ()),
|
|
278
314
|
routers=tuple(spec.routers or ()),
|
|
279
315
|
ops=tuple(spec.ops or ()),
|
|
316
|
+
well_known=tuple(spec.well_known or ()),
|
|
280
317
|
tables=tuple(spec.tables or ()),
|
|
281
318
|
schemas=tuple(spec.schemas or ()),
|
|
282
319
|
hooks=tuple(spec.hooks or ()),
|
|
@@ -28,6 +28,7 @@ Framing = Literal[
|
|
|
28
28
|
"binary",
|
|
29
29
|
"bytes",
|
|
30
30
|
"webtransport",
|
|
31
|
+
"multipart/form-data",
|
|
31
32
|
]
|
|
32
33
|
BindingProfile = Literal[
|
|
33
34
|
"rest",
|
|
@@ -52,16 +53,16 @@ WebTransportLane = Literal[
|
|
|
52
53
|
]
|
|
53
54
|
|
|
54
55
|
APP_LEVEL_FRAMING_SUPPORT: dict[str, tuple[str, ...]] = {
|
|
55
|
-
"http.rest": ("json",),
|
|
56
|
-
"https.rest": ("json",),
|
|
56
|
+
"http.rest": ("json", "multipart/form-data"),
|
|
57
|
+
"https.rest": ("json", "multipart/form-data"),
|
|
57
58
|
"http.jsonrpc": ("jsonrpc",),
|
|
58
59
|
"https.jsonrpc": ("jsonrpc",),
|
|
59
60
|
"http.stream": ("stream", "bytes", "binary", "text", "json", "ndjson"),
|
|
60
61
|
"https.stream": ("stream", "bytes", "binary", "text", "json", "ndjson"),
|
|
61
62
|
"http.sse": ("sse",),
|
|
62
63
|
"https.sse": ("sse",),
|
|
63
|
-
"ws": ("text", "bytes", "binary", "json", "jsonrpc"),
|
|
64
|
-
"wss": ("text", "bytes", "binary", "json", "jsonrpc"),
|
|
64
|
+
"ws": ("text", "bytes", "binary", "json", "jsonrpc", "ndjson"),
|
|
65
|
+
"wss": ("text", "bytes", "binary", "json", "jsonrpc", "ndjson"),
|
|
65
66
|
"webtransport": ("webtransport",),
|
|
66
67
|
}
|
|
67
68
|
|
|
@@ -70,8 +71,8 @@ BINDING_PROFILE_EXCHANGE_SUPPORT: dict[str, tuple[str, ...]] = {
|
|
|
70
71
|
"https.rest": ("request_response",),
|
|
71
72
|
"http.jsonrpc": ("request_response",),
|
|
72
73
|
"https.jsonrpc": ("request_response",),
|
|
73
|
-
"http.stream": ("server_stream",),
|
|
74
|
-
"https.stream": ("server_stream",),
|
|
74
|
+
"http.stream": ("server_stream", "client_stream"),
|
|
75
|
+
"https.stream": ("server_stream", "client_stream"),
|
|
75
76
|
"http.sse": ("server_stream",),
|
|
76
77
|
"https.sse": ("server_stream",),
|
|
77
78
|
"ws": ("bidirectional_stream",),
|
|
@@ -154,17 +155,16 @@ def validate_app_framing_for_binding(
|
|
|
154
155
|
allowed = APP_LEVEL_FRAMING_SUPPORT.get(binding_kind)
|
|
155
156
|
if allowed is None:
|
|
156
157
|
raise ValueError(f"unsupported binding kind {binding_kind!r}")
|
|
157
|
-
if selected == "ndjson" and binding_kind in {"ws", "wss"}:
|
|
158
|
-
raise ValueError("WebSocket ndjson framing is unsupported and must fail closed")
|
|
159
158
|
if selected not in allowed:
|
|
160
159
|
raise ValueError(
|
|
161
160
|
f"unsupported app-level framing {selected!r} for binding {binding_kind!r}"
|
|
162
161
|
)
|
|
163
|
-
if selected
|
|
162
|
+
if selected in {"jsonrpc", "ndjson"} and binding_kind in {"ws", "wss"}:
|
|
164
163
|
lowered = tuple(str(item).lower() for item in subprotocols)
|
|
165
|
-
if
|
|
164
|
+
if selected not in lowered:
|
|
166
165
|
raise ValueError(
|
|
167
|
-
"WebSocket
|
|
166
|
+
f"WebSocket {selected} framing requires subprotocols to include "
|
|
167
|
+
f"'{selected}'."
|
|
168
168
|
)
|
|
169
169
|
if selected == "ndjson" and "jsonrpc" in binding_kind:
|
|
170
170
|
raise ValueError("ndjson is not a JSON-RPC framing substitute")
|
|
@@ -583,6 +583,23 @@ def project_binding_runtime_metadata(binding: TransportBindingSpec) -> dict[str,
|
|
|
583
583
|
if isinstance(binding, WebTransportBindingSpec):
|
|
584
584
|
metadata["lane"] = binding.lane or webtransport_lane_for_profile(binding.profile)
|
|
585
585
|
metadata["inner_framing"] = inner_framing
|
|
586
|
+
if metadata["lane"] == "bidi_stream":
|
|
587
|
+
metadata["direction"] = "bidirectional"
|
|
588
|
+
elif metadata["lane"] == "unidi_client_stream":
|
|
589
|
+
metadata["stream_initiator"] = "client"
|
|
590
|
+
metadata["direction"] = "client_to_server"
|
|
591
|
+
elif metadata["lane"] == "unidi_server_stream":
|
|
592
|
+
metadata["stream_initiator"] = "server"
|
|
593
|
+
metadata["direction"] = "server_to_client"
|
|
594
|
+
elif family == "stream" and proto in {"http.stream", "https.stream"}:
|
|
595
|
+
if exchange == "client_stream":
|
|
596
|
+
metadata["carrier_kind"] = "http_request_body"
|
|
597
|
+
metadata["stream_initiator"] = "client"
|
|
598
|
+
metadata["direction"] = "client_to_server"
|
|
599
|
+
elif exchange == "server_stream":
|
|
600
|
+
metadata["carrier_kind"] = "http_response_body"
|
|
601
|
+
metadata["stream_initiator"] = "server"
|
|
602
|
+
metadata["direction"] = "server_to_client"
|
|
586
603
|
return metadata
|
|
587
604
|
|
|
588
605
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class InvalidHookPhaseError(ValueError):
|
|
7
|
+
"""Raised when a hook phase is not one of the supported declarative phases."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, *, phase: str, allowed_phases: Iterable[str]):
|
|
10
|
+
self.phase = phase
|
|
11
|
+
self.allowed_phases = tuple(allowed_phases)
|
|
12
|
+
options = ", ".join(self.allowed_phases)
|
|
13
|
+
super().__init__(f"Invalid hook phase '{phase}'. Valid phases are: {options}.")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
__all__ = ["InvalidHookPhaseError"]
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .binding_spec import canonical_binding_kind
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
DOCS_SURFACES = frozenset({"openapi", "openrpc"})
|
|
11
|
+
RUNTIME_SURFACES = frozenset({"http", "jsonrpc", "stream", "sse", "websocket", "webtransport"})
|
|
12
|
+
SUPPORTED_BINDING_KINDS = frozenset(
|
|
13
|
+
{
|
|
14
|
+
"http.rest",
|
|
15
|
+
"http.jsonrpc",
|
|
16
|
+
"http.stream",
|
|
17
|
+
"http.sse",
|
|
18
|
+
"ws",
|
|
19
|
+
"webtransport",
|
|
20
|
+
}
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True, slots=True)
|
|
25
|
+
class ExposureDecision:
|
|
26
|
+
docs: bool
|
|
27
|
+
runtime: bool
|
|
28
|
+
docs_source: str
|
|
29
|
+
runtime_source: str
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ExposurePolicyError(ValueError):
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def resolve_exposure_policy(
|
|
37
|
+
*,
|
|
38
|
+
docs_exposure: str = "default",
|
|
39
|
+
runtime_exposure: str = "none",
|
|
40
|
+
binding: Any | None = None,
|
|
41
|
+
) -> ExposureDecision:
|
|
42
|
+
docs = _enabled(docs_exposure, default=True)
|
|
43
|
+
runtime = _enabled(runtime_exposure, default=False)
|
|
44
|
+
if runtime:
|
|
45
|
+
if binding is None:
|
|
46
|
+
raise ExposurePolicyError("runtime exposure requires declared transport binding")
|
|
47
|
+
_require_supported_binding(binding)
|
|
48
|
+
if docs and binding is not None:
|
|
49
|
+
_require_supported_binding(binding)
|
|
50
|
+
return ExposureDecision(
|
|
51
|
+
docs=docs,
|
|
52
|
+
runtime=runtime,
|
|
53
|
+
docs_source=str(docs_exposure),
|
|
54
|
+
runtime_source=str(runtime_exposure),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def exposed_surfaces(decision: ExposureDecision, binding: Any | None = None) -> tuple[str, ...]:
|
|
59
|
+
surfaces: list[str] = []
|
|
60
|
+
if decision.docs:
|
|
61
|
+
surfaces.extend(("openapi", "openrpc"))
|
|
62
|
+
if decision.runtime:
|
|
63
|
+
if binding is None:
|
|
64
|
+
raise ExposurePolicyError("runtime exposure requires declared transport binding")
|
|
65
|
+
surfaces.append(_runtime_surface(binding))
|
|
66
|
+
return tuple(surfaces)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _enabled(value: str, *, default: bool) -> bool:
|
|
70
|
+
token = str(value or "default").strip().lower()
|
|
71
|
+
if token == "default":
|
|
72
|
+
return default
|
|
73
|
+
if token in {"enabled", "on", "true"}:
|
|
74
|
+
return True
|
|
75
|
+
if token in {"disabled", "none", "off", "false"}:
|
|
76
|
+
return False
|
|
77
|
+
raise ExposurePolicyError(f"unsupported exposure value: {value!r}")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _require_supported_binding(binding: Any) -> str:
|
|
81
|
+
kind = _binding_kind(binding)
|
|
82
|
+
if kind not in SUPPORTED_BINDING_KINDS:
|
|
83
|
+
raise ExposurePolicyError(f"unsupported transport binding: {kind}")
|
|
84
|
+
return kind
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _runtime_surface(binding: Any) -> str:
|
|
88
|
+
kind = _require_supported_binding(binding)
|
|
89
|
+
return {
|
|
90
|
+
"http.rest": "http",
|
|
91
|
+
"http.jsonrpc": "jsonrpc",
|
|
92
|
+
"http.stream": "stream",
|
|
93
|
+
"http.sse": "sse",
|
|
94
|
+
"ws": "websocket",
|
|
95
|
+
"webtransport": "webtransport",
|
|
96
|
+
}[kind]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _binding_kind(binding: Any) -> str:
|
|
100
|
+
if isinstance(binding, Mapping):
|
|
101
|
+
return str(binding.get("kind") or binding.get("proto") or "")
|
|
102
|
+
return canonical_binding_kind(binding)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
__all__ = [
|
|
106
|
+
"DOCS_SURFACES",
|
|
107
|
+
"RUNTIME_SURFACES",
|
|
108
|
+
"ExposureDecision",
|
|
109
|
+
"ExposurePolicyError",
|
|
110
|
+
"exposed_surfaces",
|
|
111
|
+
"resolve_exposure_policy",
|
|
112
|
+
]
|
tigrbl_core/_spec/hook_types.py
CHANGED
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from enum import Enum
|
|
4
4
|
from typing import Any, Awaitable, Callable, Tuple
|
|
5
5
|
|
|
6
|
-
from tigrbl_typing.phases import
|
|
6
|
+
from tigrbl_typing.phases import canonicalize_phase_input
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class HookPhase(str, Enum):
|
|
@@ -30,7 +30,7 @@ class HookPhase(str, Enum):
|
|
|
30
30
|
|
|
31
31
|
@classmethod
|
|
32
32
|
def _missing_(cls, value: object):
|
|
33
|
-
normalized =
|
|
33
|
+
normalized = canonicalize_phase_input(str(value)) if value is not None else None
|
|
34
34
|
if normalized != value:
|
|
35
35
|
return cls(normalized)
|
|
36
36
|
return None
|
tigrbl_core/_spec/op_spec.py
CHANGED
|
@@ -206,6 +206,15 @@ def _as_specs(value: Any, table: type) -> List["OpSpec"]:
|
|
|
206
206
|
|
|
207
207
|
|
|
208
208
|
def _generate_canonical(table: type) -> List["OpSpec"]:
|
|
209
|
+
profile = getattr(table, "TABLE_PROFILE", None)
|
|
210
|
+
if profile is not None:
|
|
211
|
+
try:
|
|
212
|
+
from tigrbl_core._spec.table_profile_spec import coerce_table_profile
|
|
213
|
+
|
|
214
|
+
return list(coerce_table_profile(profile).bind_table(table).ops)
|
|
215
|
+
except Exception:
|
|
216
|
+
raise
|
|
217
|
+
|
|
209
218
|
from tigrbl_core.op.canonical import should_wire_canonical
|
|
210
219
|
|
|
211
220
|
specs: List[OpSpec] = []
|
tigrbl_core/_spec/path_spec.py
CHANGED
|
@@ -15,6 +15,7 @@ from .binding_spec import (
|
|
|
15
15
|
from .op_spec import OpSpec
|
|
16
16
|
from .serde import SerdeMixin
|
|
17
17
|
from .table_spec import TableSpec
|
|
18
|
+
from .well_known_spec import WellKnownResourceSpec
|
|
18
19
|
|
|
19
20
|
PathKind = Literal[
|
|
20
21
|
"resource",
|
|
@@ -31,6 +32,7 @@ PathKind = Literal[
|
|
|
31
32
|
"docs-uix",
|
|
32
33
|
"static",
|
|
33
34
|
"mount",
|
|
35
|
+
"well-known",
|
|
34
36
|
]
|
|
35
37
|
|
|
36
38
|
|
|
@@ -48,6 +50,7 @@ class PathSpec(SerdeMixin):
|
|
|
48
50
|
docs_uix: Any | None = None
|
|
49
51
|
static: Any | None = None
|
|
50
52
|
mount: Any | None = None
|
|
53
|
+
well_known: WellKnownResourceSpec | None = None
|
|
51
54
|
|
|
52
55
|
def __post_init__(self) -> None:
|
|
53
56
|
if not isinstance(self.path, str) or not self.path.startswith("/"):
|
|
@@ -67,7 +70,13 @@ class PathSpec(SerdeMixin):
|
|
|
67
70
|
if "routes" in payload:
|
|
68
71
|
raise ValueError("PathSpec does not accept 'routes'; use path-owned specs.")
|
|
69
72
|
if "engine" in payload or "engine_name" in payload:
|
|
70
|
-
|
|
73
|
+
rejected = ", ".join(
|
|
74
|
+
field for field in ("engine", "engine_name") if field in payload
|
|
75
|
+
)
|
|
76
|
+
raise ValueError(
|
|
77
|
+
"PathSpec does not own engines; rejected "
|
|
78
|
+
f"{rejected}; bind engines at app, router, table, or op scope."
|
|
79
|
+
)
|
|
71
80
|
return super().from_dict(payload)
|
|
72
81
|
|
|
73
82
|
def binding_path(self, binding: TransportBindingSpec) -> str:
|
|
@@ -134,6 +143,7 @@ class PathSpec(SerdeMixin):
|
|
|
134
143
|
"docs-uix",
|
|
135
144
|
"static",
|
|
136
145
|
"mount",
|
|
146
|
+
"well-known",
|
|
137
147
|
}
|
|
138
148
|
if self.kind not in valid:
|
|
139
149
|
raise ValueError(f"PathSpec.kind must be a canonical path kind; got {self.kind!r}.")
|
|
@@ -144,6 +154,7 @@ class PathSpec(SerdeMixin):
|
|
|
144
154
|
"docs-uix": ("docs_uix", self.docs_uix),
|
|
145
155
|
"static": ("static", self.static),
|
|
146
156
|
"mount": ("mount", self.mount),
|
|
157
|
+
"well-known": ("well_known", self.well_known),
|
|
147
158
|
}
|
|
148
159
|
if self.kind in payload_fields:
|
|
149
160
|
field_name, value = payload_fields[self.kind]
|
|
@@ -155,11 +166,16 @@ class PathSpec(SerdeMixin):
|
|
|
155
166
|
("docs_uix", self.docs_uix),
|
|
156
167
|
("static", self.static),
|
|
157
168
|
("mount", self.mount),
|
|
169
|
+
("well_known", self.well_known),
|
|
158
170
|
):
|
|
159
171
|
if value is not None:
|
|
160
172
|
raise ValueError(
|
|
161
173
|
f"PathSpec.{field_name} is only valid on its matching canonical path kind."
|
|
162
174
|
)
|
|
175
|
+
if self.well_known is not None and not isinstance(
|
|
176
|
+
self.well_known, WellKnownResourceSpec
|
|
177
|
+
):
|
|
178
|
+
raise TypeError("PathSpec.well_known must be WellKnownResourceSpec.")
|
|
163
179
|
|
|
164
180
|
def _validate_tables(self) -> None:
|
|
165
181
|
for table in self.tables:
|
tigrbl_core/_spec/router_spec.py
CHANGED
|
@@ -10,6 +10,7 @@ from .._spec.path_spec import PathSpec
|
|
|
10
10
|
from .._spec.response_spec import ResponseSpec
|
|
11
11
|
from .._spec.schema_spec import SchemaSpec
|
|
12
12
|
from .._spec.table_spec import TableSpec
|
|
13
|
+
from .._spec.well_known_spec import WellKnownResourceSpec
|
|
13
14
|
from .serde import SerdeMixin
|
|
14
15
|
|
|
15
16
|
|
|
@@ -29,10 +30,13 @@ class RouterSpec(SerdeMixin):
|
|
|
29
30
|
deps: Sequence[Callable[..., Any]] = field(default_factory=tuple)
|
|
30
31
|
middlewares: Sequence[Any] = field(default_factory=tuple)
|
|
31
32
|
response: Optional[ResponseSpec] = None
|
|
33
|
+
well_known: Sequence[WellKnownResourceSpec] = field(default_factory=tuple)
|
|
32
34
|
paths: Sequence[PathSpec] = field(default_factory=tuple)
|
|
33
35
|
tables: Sequence[Any] = field(default_factory=tuple)
|
|
34
36
|
|
|
35
37
|
def __post_init__(self) -> None:
|
|
38
|
+
self.well_known = tuple(self.well_known or ())
|
|
39
|
+
self._validate_nested_specs("well_known", self.well_known, (WellKnownResourceSpec,))
|
|
36
40
|
self._validate_nested_specs("paths", self.paths, (PathSpec,))
|
|
37
41
|
self._validate_nested_specs("tables", self.tables, (TableSpec,))
|
|
38
42
|
self._validate_nested_specs("ops", self.ops, (OpSpec,))
|
|
@@ -44,6 +48,8 @@ class RouterSpec(SerdeMixin):
|
|
|
44
48
|
raise ValueError(
|
|
45
49
|
"RouterSpec does not accept 'models'; use 'tables' with nested TableSpec values."
|
|
46
50
|
)
|
|
51
|
+
if "routes" in payload:
|
|
52
|
+
raise ValueError("RouterSpec does not accept 'routes'; use path-owned specs.")
|
|
47
53
|
return super().from_dict(payload)
|
|
48
54
|
|
|
49
55
|
@staticmethod
|
|
@@ -102,6 +108,7 @@ class RouterSpec(SerdeMixin):
|
|
|
102
108
|
engine_name=engine_name,
|
|
103
109
|
tags=merge_seq_attr(router, "TAGS", include_inherited=True),
|
|
104
110
|
ops=merge_seq_attr(router, "OPS", include_inherited=True),
|
|
111
|
+
well_known=merge_seq_attr(router, "WELL_KNOWN", include_inherited=True),
|
|
105
112
|
paths=merge_seq_attr(router, "PATHS", include_inherited=True),
|
|
106
113
|
tables=merge_seq_attr(router, "TABLES", include_inherited=True),
|
|
107
114
|
schemas=merge_seq_attr(router, "SCHEMAS", include_inherited=True),
|