tigrbl-core 0.1.12.dev1__tar.gz → 0.4.0.dev2__tar.gz
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-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/PKG-INFO +10 -1
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/README.md +4 -0
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/pyproject.toml +10 -2
- tigrbl_core-0.4.0.dev2/tigrbl_core/_spec/binding_spec.py +269 -0
- tigrbl_core-0.4.0.dev2/tigrbl_core/_spec/hook_spec.py +149 -0
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/_spec/hook_types.py +12 -3
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/_spec/serde.py +5 -0
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/_spec/session_spec.py +8 -3
- tigrbl_core-0.4.0.dev2/tigrbl_core/canonical_json.py +46 -0
- tigrbl_core-0.1.12.dev1/tigrbl_core/_spec/binding_spec.py +0 -133
- tigrbl_core-0.1.12.dev1/tigrbl_core/_spec/hook_spec.py +0 -48
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/_spec/__init__.py +0 -0
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/_spec/alias_spec.py +0 -0
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/_spec/app_spec.py +0 -0
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/_spec/column_spec.py +0 -0
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/_spec/datatypes.py +0 -0
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/_spec/engine_spec.py +0 -0
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/_spec/field_spec.py +0 -0
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/_spec/io_spec.py +0 -0
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/_spec/middleware_spec.py +0 -0
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/_spec/monotone.py +0 -0
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/_spec/op_spec.py +0 -0
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/_spec/op_utils.py +0 -0
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/_spec/plugins.py +0 -0
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/_spec/registry.py +0 -0
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/_spec/request_spec.py +0 -0
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/_spec/response_resolver.py +0 -0
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/_spec/response_spec.py +0 -0
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/_spec/response_types.py +0 -0
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/_spec/router_spec.py +0 -0
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/_spec/schema_spec.py +0 -0
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/_spec/storage_spec.py +0 -0
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/_spec/table_registry_spec.py +0 -0
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/_spec/table_spec.py +0 -0
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/config/__init__.py +0 -0
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/config/constants.py +0 -0
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/config/defaults.py +0 -0
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/config/engine_traversal.py +0 -0
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/config/resolver.py +0 -0
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/core/__init__.py +0 -0
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/op/__init__.py +0 -0
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/op/canonical.py +0 -0
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/op/collect.py +0 -0
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/op/types.py +0 -0
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/schema/__init__.py +0 -0
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/schema/builder/__init__.py +0 -0
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/schema/builder/build_schema.py +0 -0
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/schema/builder/cache.py +0 -0
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/schema/builder/extras.py +0 -0
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/schema/builder/helpers.py +0 -0
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/schema/builder/list_params.py +0 -0
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/schema/builder/strip_parent_fields.py +0 -0
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/schema/get_schema.py +0 -0
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/schema/spec_json.py +0 -0
- {tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/schema/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tigrbl-core
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0.dev2
|
|
4
4
|
Summary: Core specifications and primitives for the Tigrbl framework.
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
Keywords: tigrbl,sdk,standards,framework
|
|
@@ -20,6 +20,11 @@ Requires-Dist: pyyaml
|
|
|
20
20
|
Requires-Dist: tigrbl-typing
|
|
21
21
|
Requires-Dist: tomli (>=2.0.1) ; python_version < "3.11"
|
|
22
22
|
Requires-Dist: tomli-w
|
|
23
|
+
Project-URL: Discord, https://discord.gg/K4YTAPapjR
|
|
24
|
+
Project-URL: Homepage, https://github.com/tigrbl/tigrbl
|
|
25
|
+
Project-URL: Issues, https://github.com/tigrbl/tigrbl/issues
|
|
26
|
+
Project-URL: Organization, https://github.com/tigrbl
|
|
27
|
+
Project-URL: Repository, https://github.com/tigrbl/tigrbl/tree/master/pkgs/core/tigrbl_core
|
|
23
28
|
Description-Content-Type: text/markdown
|
|
24
29
|
|
|
25
30
|
# tigrbl_core
|
|
@@ -40,6 +45,10 @@ It is not the authoritative location for repository governance, current target s
|
|
|
40
45
|
|
|
41
46
|
## Package identity
|
|
42
47
|
|
|
48
|
+
- canonical repository: `https://github.com/tigrbl/tigrbl`
|
|
49
|
+
- organization: `https://github.com/tigrbl`
|
|
50
|
+
- social: `https://discord.gg/K4YTAPapjR`
|
|
51
|
+
- package path: `https://github.com/tigrbl/tigrbl/tree/master/pkgs/core/tigrbl_core`
|
|
43
52
|
- workspace path: `pkgs/core/tigrbl_core`
|
|
44
53
|
- workspace class: core Python package
|
|
45
54
|
- implementation layout: `tigrbl_core/`
|
|
@@ -16,6 +16,10 @@ It is not the authoritative location for repository governance, current target s
|
|
|
16
16
|
|
|
17
17
|
## Package identity
|
|
18
18
|
|
|
19
|
+
- canonical repository: `https://github.com/tigrbl/tigrbl`
|
|
20
|
+
- organization: `https://github.com/tigrbl`
|
|
21
|
+
- social: `https://discord.gg/K4YTAPapjR`
|
|
22
|
+
- package path: `https://github.com/tigrbl/tigrbl/tree/master/pkgs/core/tigrbl_core`
|
|
19
23
|
- workspace path: `pkgs/core/tigrbl_core`
|
|
20
24
|
- workspace class: core Python package
|
|
21
25
|
- implementation layout: `tigrbl_core/`
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tigrbl-core"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.0.dev2"
|
|
4
4
|
description = "Core specifications and primitives for the Tigrbl framework."
|
|
5
5
|
license = "Apache-2.0"
|
|
6
6
|
readme = "README.md"
|
|
7
|
-
repository = "http://github.com/swarmauri/swarmauri-sdk"
|
|
8
7
|
requires-python = ">=3.10,<3.14"
|
|
9
8
|
classifiers = [
|
|
10
9
|
"Development Status :: 1 - Planning",
|
|
@@ -17,6 +16,7 @@ classifiers = [
|
|
|
17
16
|
"Programming Language :: Python :: 3 :: Only",
|
|
18
17
|
]
|
|
19
18
|
authors = [{ name = "Jacob Stewart", email = "jacob@swarmauri.com" }]
|
|
19
|
+
|
|
20
20
|
dependencies = [
|
|
21
21
|
"tigrbl-typing",
|
|
22
22
|
"pydantic>=2.10,<3",
|
|
@@ -26,6 +26,14 @@ dependencies = [
|
|
|
26
26
|
]
|
|
27
27
|
keywords = ["tigrbl", "sdk", "standards", "framework"]
|
|
28
28
|
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Organization = "https://github.com/tigrbl"
|
|
32
|
+
Discord = "https://discord.gg/K4YTAPapjR"
|
|
33
|
+
Homepage = "https://github.com/tigrbl/tigrbl"
|
|
34
|
+
Repository = "https://github.com/tigrbl/tigrbl/tree/master/pkgs/core/tigrbl_core"
|
|
35
|
+
Issues = "https://github.com/tigrbl/tigrbl/issues"
|
|
36
|
+
|
|
29
37
|
[tool.uv.sources]
|
|
30
38
|
"tigrbl-typing" = { workspace = true }
|
|
31
39
|
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Literal, Optional, Type, Union
|
|
5
|
+
|
|
6
|
+
from ..config.constants import (
|
|
7
|
+
TIGRBL_NESTED_PATHS_ATTR,
|
|
8
|
+
__JSONRPC_DEFAULT_ENDPOINT__,
|
|
9
|
+
)
|
|
10
|
+
from .serde import SerdeMixin
|
|
11
|
+
|
|
12
|
+
Exchange = Literal[
|
|
13
|
+
"request_response",
|
|
14
|
+
"server_stream",
|
|
15
|
+
"event_stream",
|
|
16
|
+
"client_stream",
|
|
17
|
+
"bidirectional",
|
|
18
|
+
"bidirectional_stream",
|
|
19
|
+
"fire_and_forget",
|
|
20
|
+
]
|
|
21
|
+
Framing = Literal["json", "jsonrpc", "sse", "stream", "text", "bytes", "webtransport"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True, slots=True)
|
|
25
|
+
class HttpRestBindingSpec(SerdeMixin):
|
|
26
|
+
proto: Literal["http.rest", "https.rest"]
|
|
27
|
+
methods: tuple[str, ...]
|
|
28
|
+
path: str
|
|
29
|
+
exchange: Exchange = "request_response"
|
|
30
|
+
framing: Framing = "json"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True, slots=True)
|
|
34
|
+
class HttpJsonRpcBindingSpec(SerdeMixin):
|
|
35
|
+
proto: Literal["http.jsonrpc", "https.jsonrpc"]
|
|
36
|
+
rpc_method: str
|
|
37
|
+
endpoint: str = __JSONRPC_DEFAULT_ENDPOINT__
|
|
38
|
+
exchange: Exchange = "request_response"
|
|
39
|
+
framing: Framing = "jsonrpc"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True, slots=True)
|
|
43
|
+
class HttpStreamBindingSpec(SerdeMixin):
|
|
44
|
+
proto: Literal["http.stream", "https.stream"]
|
|
45
|
+
path: str
|
|
46
|
+
methods: tuple[str, ...] = ("GET",)
|
|
47
|
+
exchange: Exchange = "server_stream"
|
|
48
|
+
framing: Framing = "stream"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True, slots=True)
|
|
52
|
+
class SseBindingSpec(SerdeMixin):
|
|
53
|
+
proto: Literal["http.sse", "https.sse"] = "http.sse"
|
|
54
|
+
path: str = "/"
|
|
55
|
+
methods: tuple[str, ...] = ("GET",)
|
|
56
|
+
exchange: Exchange = "server_stream"
|
|
57
|
+
framing: Framing = "sse"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True, slots=True)
|
|
61
|
+
class WsBindingSpec(SerdeMixin):
|
|
62
|
+
proto: Literal["ws", "wss"]
|
|
63
|
+
path: str
|
|
64
|
+
subprotocols: tuple[str, ...] = ()
|
|
65
|
+
exchange: Exchange = "bidirectional_stream"
|
|
66
|
+
framing: Framing = "text"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True, slots=True)
|
|
70
|
+
class WebTransportBindingSpec(SerdeMixin):
|
|
71
|
+
proto: Literal["webtransport"] = "webtransport"
|
|
72
|
+
path: str = "/"
|
|
73
|
+
exchange: Exchange = "bidirectional_stream"
|
|
74
|
+
framing: Framing = "webtransport"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True, slots=True)
|
|
78
|
+
class MessageBindingSpec(SerdeMixin):
|
|
79
|
+
proto: str
|
|
80
|
+
topic: str
|
|
81
|
+
exchange: Exchange = "fire_and_forget"
|
|
82
|
+
framing: Framing = "bytes"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass(frozen=True, slots=True)
|
|
86
|
+
class DatagramBindingSpec(SerdeMixin):
|
|
87
|
+
proto: str
|
|
88
|
+
endpoint: str
|
|
89
|
+
exchange: Exchange = "fire_and_forget"
|
|
90
|
+
framing: Framing = "bytes"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
TransportBindingSpec = Union[
|
|
94
|
+
HttpRestBindingSpec,
|
|
95
|
+
HttpJsonRpcBindingSpec,
|
|
96
|
+
HttpStreamBindingSpec,
|
|
97
|
+
SseBindingSpec,
|
|
98
|
+
WsBindingSpec,
|
|
99
|
+
WebTransportBindingSpec,
|
|
100
|
+
MessageBindingSpec,
|
|
101
|
+
DatagramBindingSpec,
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass(frozen=True, slots=True)
|
|
106
|
+
class BindingSpec(SerdeMixin):
|
|
107
|
+
"""Named binding declaration used for registry composition."""
|
|
108
|
+
|
|
109
|
+
name: str
|
|
110
|
+
spec: TransportBindingSpec
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass(slots=True)
|
|
114
|
+
class BindingRegistrySpec(SerdeMixin):
|
|
115
|
+
"""Simple in-memory registry for named transport bindings."""
|
|
116
|
+
|
|
117
|
+
_bindings: dict[str, BindingSpec] = field(default_factory=dict)
|
|
118
|
+
|
|
119
|
+
def register(self, binding: BindingSpec) -> None:
|
|
120
|
+
self._bindings[binding.name] = binding
|
|
121
|
+
|
|
122
|
+
def get(self, name: str) -> Optional[BindingSpec]:
|
|
123
|
+
return self._bindings.get(name)
|
|
124
|
+
|
|
125
|
+
def values(self) -> tuple[BindingSpec, ...]:
|
|
126
|
+
return tuple(self._bindings.values())
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def resolve_rest_nested_prefix(model: Type) -> Optional[str]:
|
|
130
|
+
"""Return the configured nested REST prefix for ``model`` if present."""
|
|
131
|
+
|
|
132
|
+
cb = getattr(model, TIGRBL_NESTED_PATHS_ATTR, None)
|
|
133
|
+
if callable(cb):
|
|
134
|
+
return cb()
|
|
135
|
+
return getattr(model, "_nested_path", None)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
_EXCHANGE_ALIASES = {
|
|
139
|
+
"bidirectional": "bidirectional_stream",
|
|
140
|
+
"event_stream": "server_stream",
|
|
141
|
+
}
|
|
142
|
+
_CANONICAL_EXCHANGES = {
|
|
143
|
+
"request_response",
|
|
144
|
+
"server_stream",
|
|
145
|
+
"client_stream",
|
|
146
|
+
"bidirectional_stream",
|
|
147
|
+
"fire_and_forget",
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@dataclass(frozen=True, slots=True)
|
|
152
|
+
class BindingEventKey:
|
|
153
|
+
family: str
|
|
154
|
+
family_code: int
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def normalize_exchange(exchange: str | None) -> str:
|
|
158
|
+
token = str(exchange or "")
|
|
159
|
+
normalized = _EXCHANGE_ALIASES.get(token, token)
|
|
160
|
+
if normalized not in _CANONICAL_EXCHANGES:
|
|
161
|
+
raise ValueError(f"invalid exchange token {exchange!r}; expected canonical exchange")
|
|
162
|
+
return normalized
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def matches_exchange_selector(*, selector: str, exchange: str | None) -> bool:
|
|
166
|
+
return normalize_exchange(selector) == normalize_exchange(exchange)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def project_binding_runtime_metadata(binding: TransportBindingSpec) -> dict[str, object]:
|
|
170
|
+
proto = str(getattr(binding, "proto", ""))
|
|
171
|
+
exchange = normalize_exchange(getattr(binding, "exchange", None))
|
|
172
|
+
framing = str(getattr(binding, "framing", ""))
|
|
173
|
+
family = _binding_family(binding)
|
|
174
|
+
_validate_binding_exchange(family, exchange)
|
|
175
|
+
return {
|
|
176
|
+
"proto": proto,
|
|
177
|
+
"exchange": exchange,
|
|
178
|
+
"framing": framing,
|
|
179
|
+
"family": family,
|
|
180
|
+
"subevents": _binding_subevents(family),
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def compile_binding_event_key(binding: TransportBindingSpec) -> BindingEventKey:
|
|
185
|
+
family = str(project_binding_runtime_metadata(binding)["family"])
|
|
186
|
+
codes = {
|
|
187
|
+
"request_response": 10,
|
|
188
|
+
"rpc": 11,
|
|
189
|
+
"stream": 20,
|
|
190
|
+
"event_stream": 21,
|
|
191
|
+
"socket": 30,
|
|
192
|
+
"transport": 31,
|
|
193
|
+
"message": 40,
|
|
194
|
+
"datagram": 41,
|
|
195
|
+
}
|
|
196
|
+
return BindingEventKey(family=family, family_code=codes[family])
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _binding_family(binding: TransportBindingSpec) -> str:
|
|
200
|
+
if isinstance(binding, HttpJsonRpcBindingSpec):
|
|
201
|
+
return "rpc"
|
|
202
|
+
if isinstance(binding, HttpStreamBindingSpec):
|
|
203
|
+
return "stream"
|
|
204
|
+
if isinstance(binding, SseBindingSpec):
|
|
205
|
+
return "event_stream"
|
|
206
|
+
if isinstance(binding, WsBindingSpec):
|
|
207
|
+
return "socket"
|
|
208
|
+
if isinstance(binding, WebTransportBindingSpec):
|
|
209
|
+
return "transport"
|
|
210
|
+
if isinstance(binding, MessageBindingSpec):
|
|
211
|
+
return "message"
|
|
212
|
+
if isinstance(binding, DatagramBindingSpec):
|
|
213
|
+
return "datagram"
|
|
214
|
+
return "request_response"
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _binding_subevents(family: str) -> tuple[str, ...]:
|
|
218
|
+
subevents = {
|
|
219
|
+
"request_response": ("request.received", "response.sent"),
|
|
220
|
+
"rpc": ("rpc.request", "rpc.response"),
|
|
221
|
+
"stream": ("stream.open", "stream.message", "stream.close"),
|
|
222
|
+
"event_stream": ("event_stream.open", "event_stream.event", "event_stream.close"),
|
|
223
|
+
"socket": ("socket.open", "socket.message", "socket.close"),
|
|
224
|
+
"transport": ("transport.open", "transport.datagram", "transport.close"),
|
|
225
|
+
"message": ("message.received", "message.processed"),
|
|
226
|
+
"datagram": ("datagram.received", "datagram.ack"),
|
|
227
|
+
}
|
|
228
|
+
return subevents[family]
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _validate_binding_exchange(family: str, exchange: str) -> None:
|
|
232
|
+
allowed = {
|
|
233
|
+
"request_response": {"request_response"},
|
|
234
|
+
"rpc": {"request_response"},
|
|
235
|
+
"stream": {"server_stream"},
|
|
236
|
+
"event_stream": {"server_stream"},
|
|
237
|
+
"socket": {"bidirectional_stream"},
|
|
238
|
+
"transport": {"bidirectional_stream"},
|
|
239
|
+
"message": {"fire_and_forget"},
|
|
240
|
+
"datagram": {"fire_and_forget"},
|
|
241
|
+
}
|
|
242
|
+
if exchange not in allowed[family]:
|
|
243
|
+
raise ValueError(
|
|
244
|
+
f"invalid exchange {exchange!r} for binding family {family!r}; "
|
|
245
|
+
"expected canonical family exchange"
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
__all__ = [
|
|
250
|
+
"BindingSpec",
|
|
251
|
+
"BindingRegistrySpec",
|
|
252
|
+
"BindingEventKey",
|
|
253
|
+
"DatagramBindingSpec",
|
|
254
|
+
"Exchange",
|
|
255
|
+
"Framing",
|
|
256
|
+
"HttpJsonRpcBindingSpec",
|
|
257
|
+
"HttpRestBindingSpec",
|
|
258
|
+
"HttpStreamBindingSpec",
|
|
259
|
+
"MessageBindingSpec",
|
|
260
|
+
"SseBindingSpec",
|
|
261
|
+
"TransportBindingSpec",
|
|
262
|
+
"WebTransportBindingSpec",
|
|
263
|
+
"WsBindingSpec",
|
|
264
|
+
"compile_binding_event_key",
|
|
265
|
+
"matches_exchange_selector",
|
|
266
|
+
"normalize_exchange",
|
|
267
|
+
"project_binding_runtime_metadata",
|
|
268
|
+
"resolve_rest_nested_prefix",
|
|
269
|
+
]
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Hook specification for Tigrbl v3."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, Optional, Tuple
|
|
7
|
+
|
|
8
|
+
from .binding_spec import Exchange
|
|
9
|
+
from .hook_types import HookPhase, HookPredicate, StepFn
|
|
10
|
+
|
|
11
|
+
from .serde import SerdeMixin
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True, slots=True)
|
|
15
|
+
class HookSpec(SerdeMixin):
|
|
16
|
+
phase: HookPhase
|
|
17
|
+
fn: StepFn
|
|
18
|
+
ops: str | Tuple[str, ...] = "*"
|
|
19
|
+
bindings: Tuple[str, ...] = ()
|
|
20
|
+
framing: Tuple[str, ...] = ()
|
|
21
|
+
exchange: Optional[Exchange] = None
|
|
22
|
+
family: Tuple[str, ...] = ()
|
|
23
|
+
subevents: Tuple[str, ...] = ()
|
|
24
|
+
order: int = 0
|
|
25
|
+
when: Optional[HookPredicate] = None
|
|
26
|
+
name: Optional[str] = None
|
|
27
|
+
description: Optional[str] = None
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def collect(cls, owner: type) -> tuple["HookSpec", ...]:
|
|
31
|
+
hooks: list[HookSpec] = []
|
|
32
|
+
for hook in getattr(owner, "HOOKS", ()) or ():
|
|
33
|
+
if isinstance(hook, HookSpec):
|
|
34
|
+
hooks.append(hook)
|
|
35
|
+
elif isinstance(hook, dict):
|
|
36
|
+
hooks.append(cls(**hook))
|
|
37
|
+
for _, attr in vars(owner).items():
|
|
38
|
+
attr = getattr(attr, "__func__", attr)
|
|
39
|
+
for item in tuple(getattr(attr, "__tigrbl_hook_decls__", ()) or ()):
|
|
40
|
+
if isinstance(item, HookSpec):
|
|
41
|
+
hooks.append(item)
|
|
42
|
+
elif isinstance(item, dict):
|
|
43
|
+
hooks.append(cls(**item))
|
|
44
|
+
declared = getattr(attr, "__tigrbl_hook_spec__", None)
|
|
45
|
+
if isinstance(declared, HookSpec):
|
|
46
|
+
hooks.append(declared)
|
|
47
|
+
elif isinstance(declared, dict):
|
|
48
|
+
hooks.append(cls(**declared))
|
|
49
|
+
return tuple(hooks)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
_RUNTIME_OWNED_PHASES = frozenset(
|
|
53
|
+
{
|
|
54
|
+
"INGRESS_BEGIN",
|
|
55
|
+
"INGRESS_PARSE",
|
|
56
|
+
"INGRESS_ROUTE",
|
|
57
|
+
"INGRESS_DISPATCH",
|
|
58
|
+
"EGRESS_SHAPE",
|
|
59
|
+
"EGRESS_FINALIZE",
|
|
60
|
+
"EMIT",
|
|
61
|
+
"POST_EMIT",
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
_EXCHANGE_ALIASES = {
|
|
65
|
+
"event_stream": "server_stream",
|
|
66
|
+
"sse": "server_stream",
|
|
67
|
+
"stream": "server_stream",
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _as_tuple(value: Any) -> tuple[str, ...]:
|
|
72
|
+
if value in (None, "", ()):
|
|
73
|
+
return ()
|
|
74
|
+
if isinstance(value, str):
|
|
75
|
+
return (value,)
|
|
76
|
+
return tuple(str(item) for item in value)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _hook_phase_value(value: Any) -> str:
|
|
80
|
+
return str(getattr(value, "value", value))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _normalize_exchange(value: Any) -> str | None:
|
|
84
|
+
if value in (None, ""):
|
|
85
|
+
return None
|
|
86
|
+
token = str(value)
|
|
87
|
+
return _EXCHANGE_ALIASES.get(token, token)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def validate_hook_legality(hook: HookSpec) -> None:
|
|
91
|
+
phase = _hook_phase_value(getattr(hook, "phase", None))
|
|
92
|
+
if phase in _RUNTIME_OWNED_PHASES:
|
|
93
|
+
if phase == "POST_EMIT":
|
|
94
|
+
raise ValueError(f"{phase} is a runtime-owned completion fence, not a hook phase")
|
|
95
|
+
raise ValueError(f"{phase} is a runtime-owned canonical phase, not a hook phase")
|
|
96
|
+
try:
|
|
97
|
+
HookPhase(phase)
|
|
98
|
+
except ValueError as exc:
|
|
99
|
+
raise ValueError(f"{phase} is not a governed hook phase") from exc
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def validate_hook_selector_legality(selector: dict[str, Any]) -> None:
|
|
103
|
+
phase = selector.get("phase")
|
|
104
|
+
if phase is not None:
|
|
105
|
+
validate_hook_legality(HookSpec(phase=phase, fn=lambda _ctx: None)) # type: ignore[arg-type]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def matches_hook_selector(hook: HookSpec, metadata: dict[str, Any]) -> bool:
|
|
109
|
+
validate_hook_legality(hook)
|
|
110
|
+
hook_ops = getattr(hook, "ops", "*")
|
|
111
|
+
if hook_ops != "*":
|
|
112
|
+
op = metadata.get("op") or metadata.get("alias") or metadata.get("method")
|
|
113
|
+
if op not in _as_tuple(hook_ops):
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
bindings = _as_tuple(getattr(hook, "bindings", ()))
|
|
117
|
+
if bindings and str(metadata.get("binding")) not in bindings:
|
|
118
|
+
return False
|
|
119
|
+
|
|
120
|
+
hook_exchange = _normalize_exchange(getattr(hook, "exchange", None))
|
|
121
|
+
metadata_exchange = _normalize_exchange(metadata.get("exchange"))
|
|
122
|
+
if hook_exchange is not None and metadata_exchange != hook_exchange:
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
families = _as_tuple(getattr(hook, "family", ()))
|
|
126
|
+
if families and str(metadata.get("family")) not in families:
|
|
127
|
+
return False
|
|
128
|
+
|
|
129
|
+
subevents = _as_tuple(getattr(hook, "subevents", ()))
|
|
130
|
+
if subevents and str(metadata.get("subevent")) not in subevents:
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
framings = _as_tuple(getattr(hook, "framing", ()))
|
|
134
|
+
if framings and str(metadata.get("framing")) not in framings:
|
|
135
|
+
return False
|
|
136
|
+
|
|
137
|
+
return True
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# Backwards compatibility alias
|
|
141
|
+
OpHook = HookSpec
|
|
142
|
+
|
|
143
|
+
__all__ = [
|
|
144
|
+
"HookSpec",
|
|
145
|
+
"OpHook",
|
|
146
|
+
"matches_hook_selector",
|
|
147
|
+
"validate_hook_legality",
|
|
148
|
+
"validate_hook_selector_legality",
|
|
149
|
+
]
|
|
@@ -3,6 +3,8 @@ 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 normalize_phase
|
|
7
|
+
|
|
6
8
|
|
|
7
9
|
class HookPhase(str, Enum):
|
|
8
10
|
PRE_TX_BEGIN = "PRE_TX_BEGIN"
|
|
@@ -11,7 +13,7 @@ class HookPhase(str, Enum):
|
|
|
11
13
|
HANDLER = "HANDLER"
|
|
12
14
|
POST_HANDLER = "POST_HANDLER"
|
|
13
15
|
PRE_COMMIT = "PRE_COMMIT"
|
|
14
|
-
|
|
16
|
+
TX_COMMIT = "TX_COMMIT"
|
|
15
17
|
POST_COMMIT = "POST_COMMIT"
|
|
16
18
|
POST_RESPONSE = "POST_RESPONSE"
|
|
17
19
|
ON_ERROR = "ON_ERROR"
|
|
@@ -21,10 +23,17 @@ class HookPhase(str, Enum):
|
|
|
21
23
|
ON_HANDLER_ERROR = "ON_HANDLER_ERROR"
|
|
22
24
|
ON_POST_HANDLER_ERROR = "ON_POST_HANDLER_ERROR"
|
|
23
25
|
ON_PRE_COMMIT_ERROR = "ON_PRE_COMMIT_ERROR"
|
|
24
|
-
|
|
26
|
+
ON_TX_COMMIT_ERROR = "ON_TX_COMMIT_ERROR"
|
|
25
27
|
ON_POST_COMMIT_ERROR = "ON_POST_COMMIT_ERROR"
|
|
26
28
|
ON_POST_RESPONSE_ERROR = "ON_POST_RESPONSE_ERROR"
|
|
27
|
-
|
|
29
|
+
TX_ROLLBACK = "TX_ROLLBACK"
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def _missing_(cls, value: object):
|
|
33
|
+
normalized = normalize_phase(str(value)) if value is not None else None
|
|
34
|
+
if normalized != value:
|
|
35
|
+
return cls(normalized)
|
|
36
|
+
return None
|
|
28
37
|
|
|
29
38
|
|
|
30
39
|
HookPhases: Tuple[HookPhase, ...] = tuple(HookPhase)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
from base64 import b64decode, b64encode
|
|
4
5
|
from dataclasses import fields, is_dataclass
|
|
5
6
|
from importlib import import_module
|
|
6
7
|
from typing import Any, TypeVar
|
|
@@ -25,6 +26,8 @@ def _resolve_path(path: str) -> Any:
|
|
|
25
26
|
def _serialize_value(value: Any) -> Any:
|
|
26
27
|
if value is None or isinstance(value, (bool, int, float, str)):
|
|
27
28
|
return value
|
|
29
|
+
if isinstance(value, bytes):
|
|
30
|
+
return {"__bytes__": b64encode(value).decode("ascii")}
|
|
28
31
|
if is_dataclass(value):
|
|
29
32
|
payload = {
|
|
30
33
|
f.name: _serialize_value(getattr(value, f.name)) for f in fields(value)
|
|
@@ -53,6 +56,8 @@ def _deserialize_value(value: Any) -> Any:
|
|
|
53
56
|
if isinstance(value, dict):
|
|
54
57
|
if "__tuple__" in value:
|
|
55
58
|
return tuple(_deserialize_value(item) for item in value["__tuple__"])
|
|
59
|
+
if "__bytes__" in value:
|
|
60
|
+
return b64decode(value["__bytes__"].encode("ascii"))
|
|
56
61
|
if "__class__" in value:
|
|
57
62
|
return _resolve_path(value["__class__"])
|
|
58
63
|
if "__callable__" in value:
|
|
@@ -71,7 +71,7 @@ class SessionSpec(SerdeMixin):
|
|
|
71
71
|
|
|
72
72
|
def merge(self, higher: "SessionSpec | Mapping[str, Any] | None") -> "SessionSpec":
|
|
73
73
|
"""
|
|
74
|
-
Overlay another spec on top of this one (
|
|
74
|
+
Overlay another spec on top of this one (explicit fields take precedence).
|
|
75
75
|
Use to implement op > model > router > app precedence.
|
|
76
76
|
"""
|
|
77
77
|
if higher is None:
|
|
@@ -79,13 +79,18 @@ class SessionSpec(SerdeMixin):
|
|
|
79
79
|
h = higher if isinstance(higher, SessionSpec) else SessionSpec.from_any(higher)
|
|
80
80
|
if h is None:
|
|
81
81
|
return self
|
|
82
|
+
defaults = SessionSpec()
|
|
82
83
|
vals: MutableMapping[str, Any] = {
|
|
83
84
|
f.name: getattr(self, f.name) for f in fields(SessionSpec)
|
|
84
85
|
}
|
|
85
86
|
for f in fields(SessionSpec):
|
|
86
87
|
hv = getattr(h, f.name)
|
|
87
|
-
if hv is
|
|
88
|
-
|
|
88
|
+
if hv is None:
|
|
89
|
+
continue
|
|
90
|
+
default = getattr(defaults, f.name)
|
|
91
|
+
if hv == default and vals[f.name] != default:
|
|
92
|
+
continue
|
|
93
|
+
vals[f.name] = hv
|
|
89
94
|
return SessionSpec(**vals) # type: ignore[arg-type]
|
|
90
95
|
|
|
91
96
|
def to_kwargs(self) -> dict[str, Any]:
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""RFC 8785-style canonical JSON helpers for proof-bound payloads."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import math
|
|
7
|
+
from decimal import Decimal
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _reject_non_finite_numbers(value: Any) -> None:
|
|
12
|
+
if isinstance(value, float) and not math.isfinite(value):
|
|
13
|
+
raise ValueError("JCS canonical JSON requires finite JSON numbers")
|
|
14
|
+
if isinstance(value, Decimal) and not value.is_finite():
|
|
15
|
+
raise ValueError("JCS canonical JSON requires finite JSON numbers")
|
|
16
|
+
if isinstance(value, dict):
|
|
17
|
+
for key, item in value.items():
|
|
18
|
+
if not isinstance(key, str):
|
|
19
|
+
raise TypeError("JCS canonical JSON object keys must be strings")
|
|
20
|
+
_reject_non_finite_numbers(item)
|
|
21
|
+
return
|
|
22
|
+
if isinstance(value, (list, tuple)):
|
|
23
|
+
for item in value:
|
|
24
|
+
_reject_non_finite_numbers(item)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def canonicalize(payload: Any) -> str:
|
|
28
|
+
"""Return deterministic UTF-8 JSON text with sorted object members."""
|
|
29
|
+
|
|
30
|
+
_reject_non_finite_numbers(payload)
|
|
31
|
+
return json.dumps(
|
|
32
|
+
payload,
|
|
33
|
+
ensure_ascii=False,
|
|
34
|
+
allow_nan=False,
|
|
35
|
+
separators=(",", ":"),
|
|
36
|
+
sort_keys=True,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def canonical_json_bytes(payload: Any) -> bytes:
|
|
41
|
+
"""Return deterministic UTF-8 JSON bytes with JCS rejection semantics."""
|
|
42
|
+
|
|
43
|
+
return canonicalize(payload).encode("utf-8")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
__all__ = ["canonical_json_bytes", "canonicalize"]
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from dataclasses import dataclass, field
|
|
4
|
-
from typing import Literal, Optional, Type, Union
|
|
5
|
-
|
|
6
|
-
from ..config.constants import (
|
|
7
|
-
TIGRBL_NESTED_PATHS_ATTR,
|
|
8
|
-
__JSONRPC_DEFAULT_ENDPOINT__,
|
|
9
|
-
)
|
|
10
|
-
from .serde import SerdeMixin
|
|
11
|
-
|
|
12
|
-
Exchange = Literal[
|
|
13
|
-
"request_response",
|
|
14
|
-
"server_stream",
|
|
15
|
-
"event_stream",
|
|
16
|
-
"client_stream",
|
|
17
|
-
"bidirectional",
|
|
18
|
-
"bidirectional_stream",
|
|
19
|
-
"fire_and_forget",
|
|
20
|
-
]
|
|
21
|
-
Framing = Literal["json", "jsonrpc", "sse", "stream", "text", "bytes", "webtransport"]
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
@dataclass(frozen=True, slots=True)
|
|
25
|
-
class HttpRestBindingSpec(SerdeMixin):
|
|
26
|
-
proto: Literal["http.rest", "https.rest"]
|
|
27
|
-
methods: tuple[str, ...]
|
|
28
|
-
path: str
|
|
29
|
-
exchange: Exchange = "request_response"
|
|
30
|
-
framing: Framing = "json"
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
@dataclass(frozen=True, slots=True)
|
|
34
|
-
class HttpJsonRpcBindingSpec(SerdeMixin):
|
|
35
|
-
proto: Literal["http.jsonrpc", "https.jsonrpc"]
|
|
36
|
-
rpc_method: str
|
|
37
|
-
endpoint: str = __JSONRPC_DEFAULT_ENDPOINT__
|
|
38
|
-
exchange: Exchange = "request_response"
|
|
39
|
-
framing: Framing = "jsonrpc"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
@dataclass(frozen=True, slots=True)
|
|
43
|
-
class HttpStreamBindingSpec(SerdeMixin):
|
|
44
|
-
proto: Literal["http.stream", "https.stream"]
|
|
45
|
-
path: str
|
|
46
|
-
methods: tuple[str, ...] = ("GET",)
|
|
47
|
-
exchange: Exchange = "server_stream"
|
|
48
|
-
framing: Framing = "stream"
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
@dataclass(frozen=True, slots=True)
|
|
52
|
-
class SseBindingSpec(SerdeMixin):
|
|
53
|
-
proto: Literal["http.sse", "https.sse"] = "http.sse"
|
|
54
|
-
path: str = "/"
|
|
55
|
-
methods: tuple[str, ...] = ("GET",)
|
|
56
|
-
exchange: Exchange = "server_stream"
|
|
57
|
-
framing: Framing = "sse"
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
@dataclass(frozen=True, slots=True)
|
|
61
|
-
class WsBindingSpec(SerdeMixin):
|
|
62
|
-
proto: Literal["ws", "wss"]
|
|
63
|
-
path: str
|
|
64
|
-
subprotocols: tuple[str, ...] = ()
|
|
65
|
-
exchange: Exchange = "bidirectional_stream"
|
|
66
|
-
framing: Framing = "text"
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
@dataclass(frozen=True, slots=True)
|
|
70
|
-
class WebTransportBindingSpec(SerdeMixin):
|
|
71
|
-
proto: Literal["webtransport"] = "webtransport"
|
|
72
|
-
path: str = "/"
|
|
73
|
-
exchange: Exchange = "bidirectional_stream"
|
|
74
|
-
framing: Framing = "webtransport"
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
TransportBindingSpec = Union[
|
|
78
|
-
HttpRestBindingSpec,
|
|
79
|
-
HttpJsonRpcBindingSpec,
|
|
80
|
-
HttpStreamBindingSpec,
|
|
81
|
-
SseBindingSpec,
|
|
82
|
-
WsBindingSpec,
|
|
83
|
-
WebTransportBindingSpec,
|
|
84
|
-
]
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
@dataclass(frozen=True, slots=True)
|
|
88
|
-
class BindingSpec(SerdeMixin):
|
|
89
|
-
"""Named binding declaration used for registry composition."""
|
|
90
|
-
|
|
91
|
-
name: str
|
|
92
|
-
spec: TransportBindingSpec
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
@dataclass(slots=True)
|
|
96
|
-
class BindingRegistrySpec(SerdeMixin):
|
|
97
|
-
"""Simple in-memory registry for named transport bindings."""
|
|
98
|
-
|
|
99
|
-
_bindings: dict[str, BindingSpec] = field(default_factory=dict)
|
|
100
|
-
|
|
101
|
-
def register(self, binding: BindingSpec) -> None:
|
|
102
|
-
self._bindings[binding.name] = binding
|
|
103
|
-
|
|
104
|
-
def get(self, name: str) -> Optional[BindingSpec]:
|
|
105
|
-
return self._bindings.get(name)
|
|
106
|
-
|
|
107
|
-
def values(self) -> tuple[BindingSpec, ...]:
|
|
108
|
-
return tuple(self._bindings.values())
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
def resolve_rest_nested_prefix(model: Type) -> Optional[str]:
|
|
112
|
-
"""Return the configured nested REST prefix for ``model`` if present."""
|
|
113
|
-
|
|
114
|
-
cb = getattr(model, TIGRBL_NESTED_PATHS_ATTR, None)
|
|
115
|
-
if callable(cb):
|
|
116
|
-
return cb()
|
|
117
|
-
return getattr(model, "_nested_path", None)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
__all__ = [
|
|
121
|
-
"BindingSpec",
|
|
122
|
-
"BindingRegistrySpec",
|
|
123
|
-
"Exchange",
|
|
124
|
-
"Framing",
|
|
125
|
-
"HttpJsonRpcBindingSpec",
|
|
126
|
-
"HttpRestBindingSpec",
|
|
127
|
-
"HttpStreamBindingSpec",
|
|
128
|
-
"SseBindingSpec",
|
|
129
|
-
"TransportBindingSpec",
|
|
130
|
-
"WebTransportBindingSpec",
|
|
131
|
-
"WsBindingSpec",
|
|
132
|
-
"resolve_rest_nested_prefix",
|
|
133
|
-
]
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
"""Hook specification for Tigrbl v3."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from dataclasses import dataclass
|
|
6
|
-
from typing import Optional, Tuple
|
|
7
|
-
|
|
8
|
-
from .binding_spec import Exchange
|
|
9
|
-
from .hook_types import HookPhase, HookPredicate, StepFn
|
|
10
|
-
|
|
11
|
-
from .serde import SerdeMixin
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@dataclass(frozen=True, slots=True)
|
|
15
|
-
class HookSpec(SerdeMixin):
|
|
16
|
-
phase: HookPhase
|
|
17
|
-
fn: StepFn
|
|
18
|
-
ops: str | Tuple[str, ...] = "*"
|
|
19
|
-
bindings: Tuple[str, ...] = ()
|
|
20
|
-
exchange: Optional[Exchange] = None
|
|
21
|
-
family: Tuple[str, ...] = ()
|
|
22
|
-
subevents: Tuple[str, ...] = ()
|
|
23
|
-
order: int = 0
|
|
24
|
-
when: Optional[HookPredicate] = None
|
|
25
|
-
name: Optional[str] = None
|
|
26
|
-
description: Optional[str] = None
|
|
27
|
-
|
|
28
|
-
@classmethod
|
|
29
|
-
def collect(cls, owner: type) -> tuple["HookSpec", ...]:
|
|
30
|
-
hooks: list[HookSpec] = []
|
|
31
|
-
for hook in getattr(owner, "HOOKS", ()) or ():
|
|
32
|
-
if isinstance(hook, HookSpec):
|
|
33
|
-
hooks.append(hook)
|
|
34
|
-
elif isinstance(hook, dict):
|
|
35
|
-
hooks.append(cls(**hook))
|
|
36
|
-
for _, attr in vars(owner).items():
|
|
37
|
-
declared = getattr(attr, "__tigrbl_hook_spec__", None)
|
|
38
|
-
if isinstance(declared, HookSpec):
|
|
39
|
-
hooks.append(declared)
|
|
40
|
-
elif isinstance(declared, dict):
|
|
41
|
-
hooks.append(cls(**declared))
|
|
42
|
-
return tuple(hooks)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
# Backwards compatibility alias
|
|
46
|
-
OpHook = HookSpec
|
|
47
|
-
|
|
48
|
-
__all__ = ["HookSpec", "OpHook"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/schema/builder/build_schema.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/schema/builder/list_params.py
RENAMED
|
File without changes
|
{tigrbl_core-0.1.12.dev1 → tigrbl_core-0.4.0.dev2}/tigrbl_core/schema/builder/strip_parent_fields.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|