tigrbl 0.0.1.dev1__py3-none-any.whl → 0.3.0.dev2__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.
Files changed (252) hide show
  1. tigrbl/README.md +94 -0
  2. tigrbl/__init__.py +139 -14
  3. tigrbl/api/__init__.py +6 -0
  4. tigrbl/api/_api.py +72 -0
  5. tigrbl/api/api_spec.py +30 -0
  6. tigrbl/api/mro_collect.py +43 -0
  7. tigrbl/api/shortcuts.py +56 -0
  8. tigrbl/api/tigrbl_api.py +286 -0
  9. tigrbl/app/__init__.py +0 -0
  10. tigrbl/app/_app.py +61 -0
  11. tigrbl/app/app_spec.py +42 -0
  12. tigrbl/app/mro_collect.py +67 -0
  13. tigrbl/app/shortcuts.py +65 -0
  14. tigrbl/app/tigrbl_app.py +314 -0
  15. tigrbl/bindings/__init__.py +73 -0
  16. tigrbl/bindings/api/__init__.py +12 -0
  17. tigrbl/bindings/api/common.py +109 -0
  18. tigrbl/bindings/api/include.py +256 -0
  19. tigrbl/bindings/api/resource_proxy.py +149 -0
  20. tigrbl/bindings/api/rpc.py +111 -0
  21. tigrbl/bindings/columns.py +49 -0
  22. tigrbl/bindings/handlers/__init__.py +11 -0
  23. tigrbl/bindings/handlers/builder.py +119 -0
  24. tigrbl/bindings/handlers/ctx.py +74 -0
  25. tigrbl/bindings/handlers/identifiers.py +228 -0
  26. tigrbl/bindings/handlers/namespaces.py +51 -0
  27. tigrbl/bindings/handlers/steps.py +276 -0
  28. tigrbl/bindings/hooks.py +311 -0
  29. tigrbl/bindings/model.py +194 -0
  30. tigrbl/bindings/model_helpers.py +139 -0
  31. tigrbl/bindings/model_registry.py +77 -0
  32. tigrbl/bindings/rest/__init__.py +7 -0
  33. tigrbl/bindings/rest/attach.py +34 -0
  34. tigrbl/bindings/rest/collection.py +265 -0
  35. tigrbl/bindings/rest/common.py +116 -0
  36. tigrbl/bindings/rest/fastapi.py +76 -0
  37. tigrbl/bindings/rest/helpers.py +119 -0
  38. tigrbl/bindings/rest/io.py +317 -0
  39. tigrbl/bindings/rest/member.py +367 -0
  40. tigrbl/bindings/rest/router.py +292 -0
  41. tigrbl/bindings/rest/routing.py +133 -0
  42. tigrbl/bindings/rpc.py +364 -0
  43. tigrbl/bindings/schemas/__init__.py +11 -0
  44. tigrbl/bindings/schemas/builder.py +348 -0
  45. tigrbl/bindings/schemas/defaults.py +260 -0
  46. tigrbl/bindings/schemas/utils.py +193 -0
  47. tigrbl/column/README.md +62 -0
  48. tigrbl/column/__init__.py +72 -0
  49. tigrbl/column/_column.py +96 -0
  50. tigrbl/column/column_spec.py +40 -0
  51. tigrbl/column/field_spec.py +31 -0
  52. tigrbl/column/infer/__init__.py +25 -0
  53. tigrbl/column/infer/core.py +92 -0
  54. tigrbl/column/infer/jsonhints.py +44 -0
  55. tigrbl/column/infer/planning.py +133 -0
  56. tigrbl/column/infer/types.py +102 -0
  57. tigrbl/column/infer/utils.py +59 -0
  58. tigrbl/column/io_spec.py +133 -0
  59. tigrbl/column/mro_collect.py +59 -0
  60. tigrbl/column/shortcuts.py +89 -0
  61. tigrbl/column/storage_spec.py +65 -0
  62. tigrbl/config/__init__.py +19 -0
  63. tigrbl/config/constants.py +224 -0
  64. tigrbl/config/defaults.py +29 -0
  65. tigrbl/config/resolver.py +295 -0
  66. tigrbl/core/__init__.py +47 -0
  67. tigrbl/core/crud/__init__.py +36 -0
  68. tigrbl/core/crud/bulk.py +168 -0
  69. tigrbl/core/crud/helpers/__init__.py +76 -0
  70. tigrbl/core/crud/helpers/db.py +92 -0
  71. tigrbl/core/crud/helpers/enum.py +86 -0
  72. tigrbl/core/crud/helpers/filters.py +162 -0
  73. tigrbl/core/crud/helpers/model.py +123 -0
  74. tigrbl/core/crud/helpers/normalize.py +99 -0
  75. tigrbl/core/crud/ops.py +235 -0
  76. tigrbl/ddl/__init__.py +344 -0
  77. tigrbl/decorators.py +17 -0
  78. tigrbl/deps/__init__.py +20 -0
  79. tigrbl/deps/fastapi.py +45 -0
  80. tigrbl/deps/favicon.svg +4 -0
  81. tigrbl/deps/jinja.py +27 -0
  82. tigrbl/deps/pydantic.py +10 -0
  83. tigrbl/deps/sqlalchemy.py +94 -0
  84. tigrbl/deps/starlette.py +36 -0
  85. tigrbl/engine/__init__.py +26 -0
  86. tigrbl/engine/_engine.py +130 -0
  87. tigrbl/engine/bind.py +33 -0
  88. tigrbl/engine/builders.py +236 -0
  89. tigrbl/engine/collect.py +111 -0
  90. tigrbl/engine/decorators.py +108 -0
  91. tigrbl/engine/engine_spec.py +261 -0
  92. tigrbl/engine/resolver.py +224 -0
  93. tigrbl/engine/shortcuts.py +216 -0
  94. tigrbl/hook/__init__.py +21 -0
  95. tigrbl/hook/_hook.py +22 -0
  96. tigrbl/hook/decorators.py +28 -0
  97. tigrbl/hook/hook_spec.py +24 -0
  98. tigrbl/hook/mro_collect.py +98 -0
  99. tigrbl/hook/shortcuts.py +44 -0
  100. tigrbl/hook/types.py +76 -0
  101. tigrbl/op/__init__.py +50 -0
  102. tigrbl/op/_op.py +31 -0
  103. tigrbl/op/canonical.py +31 -0
  104. tigrbl/op/collect.py +11 -0
  105. tigrbl/op/decorators.py +238 -0
  106. tigrbl/op/model_registry.py +301 -0
  107. tigrbl/op/mro_collect.py +99 -0
  108. tigrbl/op/resolver.py +216 -0
  109. tigrbl/op/types.py +136 -0
  110. tigrbl/orm/__init__.py +1 -0
  111. tigrbl/orm/mixins/_RowBound.py +83 -0
  112. tigrbl/orm/mixins/__init__.py +95 -0
  113. tigrbl/orm/mixins/bootstrappable.py +113 -0
  114. tigrbl/orm/mixins/bound.py +47 -0
  115. tigrbl/orm/mixins/edges.py +40 -0
  116. tigrbl/orm/mixins/fields.py +165 -0
  117. tigrbl/orm/mixins/hierarchy.py +54 -0
  118. tigrbl/orm/mixins/key_digest.py +44 -0
  119. tigrbl/orm/mixins/lifecycle.py +115 -0
  120. tigrbl/orm/mixins/locks.py +51 -0
  121. tigrbl/orm/mixins/markers.py +16 -0
  122. tigrbl/orm/mixins/operations.py +57 -0
  123. tigrbl/orm/mixins/ownable.py +337 -0
  124. tigrbl/orm/mixins/principals.py +98 -0
  125. tigrbl/orm/mixins/tenant_bound.py +301 -0
  126. tigrbl/orm/mixins/upsertable.py +111 -0
  127. tigrbl/orm/mixins/utils.py +49 -0
  128. tigrbl/orm/tables/__init__.py +72 -0
  129. tigrbl/orm/tables/_base.py +8 -0
  130. tigrbl/orm/tables/audit.py +56 -0
  131. tigrbl/orm/tables/client.py +25 -0
  132. tigrbl/orm/tables/group.py +29 -0
  133. tigrbl/orm/tables/org.py +30 -0
  134. tigrbl/orm/tables/rbac.py +76 -0
  135. tigrbl/orm/tables/status.py +106 -0
  136. tigrbl/orm/tables/tenant.py +22 -0
  137. tigrbl/orm/tables/user.py +39 -0
  138. tigrbl/response/README.md +34 -0
  139. tigrbl/response/__init__.py +33 -0
  140. tigrbl/response/bind.py +12 -0
  141. tigrbl/response/decorators.py +37 -0
  142. tigrbl/response/resolver.py +83 -0
  143. tigrbl/response/shortcuts.py +144 -0
  144. tigrbl/response/types.py +49 -0
  145. tigrbl/rest/__init__.py +27 -0
  146. tigrbl/runtime/README.md +129 -0
  147. tigrbl/runtime/__init__.py +20 -0
  148. tigrbl/runtime/atoms/__init__.py +102 -0
  149. tigrbl/runtime/atoms/emit/__init__.py +42 -0
  150. tigrbl/runtime/atoms/emit/paired_post.py +158 -0
  151. tigrbl/runtime/atoms/emit/paired_pre.py +106 -0
  152. tigrbl/runtime/atoms/emit/readtime_alias.py +120 -0
  153. tigrbl/runtime/atoms/out/__init__.py +38 -0
  154. tigrbl/runtime/atoms/out/masking.py +135 -0
  155. tigrbl/runtime/atoms/refresh/__init__.py +38 -0
  156. tigrbl/runtime/atoms/refresh/demand.py +130 -0
  157. tigrbl/runtime/atoms/resolve/__init__.py +40 -0
  158. tigrbl/runtime/atoms/resolve/assemble.py +167 -0
  159. tigrbl/runtime/atoms/resolve/paired_gen.py +147 -0
  160. tigrbl/runtime/atoms/response/__init__.py +17 -0
  161. tigrbl/runtime/atoms/response/negotiate.py +30 -0
  162. tigrbl/runtime/atoms/response/negotiation.py +43 -0
  163. tigrbl/runtime/atoms/response/render.py +36 -0
  164. tigrbl/runtime/atoms/response/renderer.py +116 -0
  165. tigrbl/runtime/atoms/response/template.py +44 -0
  166. tigrbl/runtime/atoms/response/templates.py +88 -0
  167. tigrbl/runtime/atoms/schema/__init__.py +40 -0
  168. tigrbl/runtime/atoms/schema/collect_in.py +21 -0
  169. tigrbl/runtime/atoms/schema/collect_out.py +21 -0
  170. tigrbl/runtime/atoms/storage/__init__.py +38 -0
  171. tigrbl/runtime/atoms/storage/to_stored.py +167 -0
  172. tigrbl/runtime/atoms/wire/__init__.py +45 -0
  173. tigrbl/runtime/atoms/wire/build_in.py +166 -0
  174. tigrbl/runtime/atoms/wire/build_out.py +87 -0
  175. tigrbl/runtime/atoms/wire/dump.py +206 -0
  176. tigrbl/runtime/atoms/wire/validate_in.py +227 -0
  177. tigrbl/runtime/context.py +206 -0
  178. tigrbl/runtime/errors/__init__.py +61 -0
  179. tigrbl/runtime/errors/converters.py +214 -0
  180. tigrbl/runtime/errors/exceptions.py +124 -0
  181. tigrbl/runtime/errors/mappings.py +71 -0
  182. tigrbl/runtime/errors/utils.py +150 -0
  183. tigrbl/runtime/events.py +209 -0
  184. tigrbl/runtime/executor/__init__.py +6 -0
  185. tigrbl/runtime/executor/guards.py +132 -0
  186. tigrbl/runtime/executor/helpers.py +88 -0
  187. tigrbl/runtime/executor/invoke.py +150 -0
  188. tigrbl/runtime/executor/types.py +84 -0
  189. tigrbl/runtime/kernel.py +628 -0
  190. tigrbl/runtime/labels.py +353 -0
  191. tigrbl/runtime/opview.py +87 -0
  192. tigrbl/runtime/ordering.py +256 -0
  193. tigrbl/runtime/system.py +279 -0
  194. tigrbl/runtime/trace.py +330 -0
  195. tigrbl/schema/__init__.py +38 -0
  196. tigrbl/schema/_schema.py +27 -0
  197. tigrbl/schema/builder/__init__.py +17 -0
  198. tigrbl/schema/builder/build_schema.py +209 -0
  199. tigrbl/schema/builder/cache.py +24 -0
  200. tigrbl/schema/builder/compat.py +16 -0
  201. tigrbl/schema/builder/extras.py +85 -0
  202. tigrbl/schema/builder/helpers.py +51 -0
  203. tigrbl/schema/builder/list_params.py +117 -0
  204. tigrbl/schema/builder/strip_parent_fields.py +70 -0
  205. tigrbl/schema/collect.py +55 -0
  206. tigrbl/schema/decorators.py +68 -0
  207. tigrbl/schema/get_schema.py +86 -0
  208. tigrbl/schema/schema_spec.py +20 -0
  209. tigrbl/schema/shortcuts.py +42 -0
  210. tigrbl/schema/types.py +34 -0
  211. tigrbl/schema/utils.py +143 -0
  212. tigrbl/shortcuts.py +22 -0
  213. tigrbl/specs.py +44 -0
  214. tigrbl/system/__init__.py +12 -0
  215. tigrbl/system/diagnostics/__init__.py +24 -0
  216. tigrbl/system/diagnostics/compat.py +31 -0
  217. tigrbl/system/diagnostics/healthz.py +41 -0
  218. tigrbl/system/diagnostics/hookz.py +51 -0
  219. tigrbl/system/diagnostics/kernelz.py +20 -0
  220. tigrbl/system/diagnostics/methodz.py +43 -0
  221. tigrbl/system/diagnostics/router.py +73 -0
  222. tigrbl/system/diagnostics/utils.py +43 -0
  223. tigrbl/table/__init__.py +9 -0
  224. tigrbl/table/_base.py +237 -0
  225. tigrbl/table/_table.py +54 -0
  226. tigrbl/table/mro_collect.py +69 -0
  227. tigrbl/table/shortcuts.py +57 -0
  228. tigrbl/table/table_spec.py +28 -0
  229. tigrbl/transport/__init__.py +74 -0
  230. tigrbl/transport/jsonrpc/__init__.py +19 -0
  231. tigrbl/transport/jsonrpc/dispatcher.py +352 -0
  232. tigrbl/transport/jsonrpc/helpers.py +115 -0
  233. tigrbl/transport/jsonrpc/models.py +41 -0
  234. tigrbl/transport/rest/__init__.py +25 -0
  235. tigrbl/transport/rest/aggregator.py +132 -0
  236. tigrbl/types/__init__.py +174 -0
  237. tigrbl/types/allow_anon_provider.py +19 -0
  238. tigrbl/types/authn_abc.py +30 -0
  239. tigrbl/types/nested_path_provider.py +22 -0
  240. tigrbl/types/op.py +35 -0
  241. tigrbl/types/op_config_provider.py +17 -0
  242. tigrbl/types/op_verb_alias_provider.py +33 -0
  243. tigrbl/types/request_extras_provider.py +22 -0
  244. tigrbl/types/response_extras_provider.py +22 -0
  245. tigrbl/types/table_config_provider.py +13 -0
  246. tigrbl-0.3.0.dev2.dist-info/LICENSE +201 -0
  247. tigrbl-0.3.0.dev2.dist-info/METADATA +501 -0
  248. tigrbl-0.3.0.dev2.dist-info/RECORD +249 -0
  249. tigrbl/ExampleAgent.py +0 -1
  250. tigrbl-0.0.1.dev1.dist-info/METADATA +0 -18
  251. tigrbl-0.0.1.dev1.dist-info/RECORD +0 -5
  252. {tigrbl-0.0.1.dev1.dist-info → tigrbl-0.3.0.dev2.dist-info}/WHEEL +0 -0
@@ -0,0 +1,301 @@
1
+ # tigrbl/v3/ops/model_registry.py
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import replace
5
+ from threading import RLock
6
+ from typing import (
7
+ Any,
8
+ Callable,
9
+ Dict,
10
+ Iterable,
11
+ Iterator,
12
+ List,
13
+ Mapping,
14
+ Optional,
15
+ Tuple,
16
+ )
17
+ import weakref
18
+
19
+ from .types import OpSpec, TargetOp
20
+
21
+ # Listener signature: (registry, changed_keys) -> None
22
+ # where changed_keys is a set of (alias, target) tuples indicating what changed.
23
+ Listener = Callable[["OpspecRegistry", set[Tuple[str, TargetOp]]], None]
24
+
25
+
26
+ def _spec_key(sp: OpSpec) -> Tuple[str, TargetOp]:
27
+ return (sp.alias, sp.target)
28
+
29
+
30
+ def _ensure_table(sp: OpSpec, table: type) -> OpSpec:
31
+ return sp if sp.table is table else replace(sp, table=table)
32
+
33
+
34
+ def _coerce_to_specs(value: Any, table: type) -> List[OpSpec]:
35
+ """
36
+ Accept flexible inputs (for back-compat with v2):
37
+ • OpSpec
38
+ • Iterable[OpSpec]
39
+ • Mapping[str, dict] (alias -> kwargs for OpSpec)
40
+ • Iterable[Mapping[str, Any]] (each includes 'alias' & 'target')
41
+ """
42
+ specs: List[OpSpec] = []
43
+ if value is None:
44
+ return specs
45
+
46
+ def _from_kwargs(kwargs: Mapping[str, Any]) -> Optional[OpSpec]:
47
+ if "alias" not in kwargs or "target" not in kwargs:
48
+ return None
49
+ try:
50
+ return OpSpec(table=table, **dict(kwargs)) # type: ignore[arg-type]
51
+ except TypeError:
52
+ return None
53
+
54
+ if isinstance(value, OpSpec):
55
+ specs.append(_ensure_table(value, table))
56
+ elif isinstance(value, Mapping):
57
+ for maybe_alias, cfg in value.items():
58
+ if isinstance(cfg, OpSpec):
59
+ specs.append(_ensure_table(cfg, table))
60
+ elif isinstance(cfg, Mapping):
61
+ kw = dict(cfg)
62
+ kw.setdefault("alias", maybe_alias)
63
+ sp = _from_kwargs(kw)
64
+ if sp:
65
+ specs.append(sp)
66
+ elif isinstance(value, Iterable):
67
+ for item in value:
68
+ if isinstance(item, OpSpec):
69
+ specs.append(_ensure_table(item, table))
70
+ elif isinstance(item, Mapping):
71
+ sp = _from_kwargs(item)
72
+ if sp:
73
+ specs.append(sp)
74
+ return specs
75
+
76
+
77
+ class OpspecRegistry:
78
+ """
79
+ Per-model OpSpec registry with change notifications.
80
+
81
+ - Stores specs keyed by (alias, target).
82
+ - Adds/sets/removes specs and notifies listeners with the changed keys.
83
+ - Binder should call `subscribe(...)` to rebuild a model's namespaces when the
84
+ registry changes (partial rebuild is possible based on changed keys).
85
+
86
+ Thread-safe via an instance-level RLock.
87
+ """
88
+
89
+ __slots__ = ("_table", "_items", "_lock", "_listeners", "_version")
90
+
91
+ def __init__(self, table: type) -> None:
92
+ self._table: type = table
93
+ self._items: Dict[Tuple[str, TargetOp], OpSpec] = {}
94
+ self._lock = RLock()
95
+ # store weakrefs to listener callables where possible; fallback to strong refs
96
+ self._listeners: List[
97
+ Callable[[OpspecRegistry, set[Tuple[str, TargetOp]]], None]
98
+ ] = []
99
+ self._version: int = 0
100
+
101
+ # ---------------------------- Introspection ---------------------------- #
102
+
103
+ @property
104
+ def table(self) -> type:
105
+ return self._table
106
+
107
+ @property
108
+ def version(self) -> int:
109
+ return self._version
110
+
111
+ def keys(self) -> Iterator[Tuple[str, TargetOp]]:
112
+ with self._lock:
113
+ return iter(tuple(self._items.keys()))
114
+
115
+ def items(self) -> Iterator[Tuple[Tuple[str, TargetOp], OpSpec]]:
116
+ with self._lock:
117
+ return iter(tuple(self._items.items()))
118
+
119
+ def values(self) -> Iterator[OpSpec]:
120
+ with self._lock:
121
+ return iter(tuple(self._items.values()))
122
+
123
+ def get_all(self) -> Tuple[OpSpec, ...]:
124
+ """Stable snapshot of all specs."""
125
+ with self._lock:
126
+ return tuple(self._items.values())
127
+
128
+ # ------------------------------ Listeners ------------------------------ #
129
+
130
+ def subscribe(self, fn: Listener) -> None:
131
+ """
132
+ Register a listener to be called on changes.
133
+ NOTE: The listener should be idempotent. It receives (registry, changed_keys).
134
+ """
135
+ with self._lock:
136
+ # Avoid duplicate subscriptions
137
+ if fn not in self._listeners:
138
+ self._listeners.append(fn)
139
+
140
+ def unsubscribe(self, fn: Listener) -> None:
141
+ with self._lock:
142
+ try:
143
+ self._listeners.remove(fn)
144
+ except ValueError:
145
+ pass
146
+
147
+ def _notify(self, changed: set[Tuple[str, TargetOp]]) -> None:
148
+ # Snapshot listeners to avoid mutation issues during callbacks
149
+ listeners: Tuple[Listener, ...]
150
+ with self._lock:
151
+ listeners = tuple(self._listeners)
152
+ for fn in listeners:
153
+ try:
154
+ fn(self, changed)
155
+ except Exception:
156
+ # Never let a listener error break the registry
157
+ pass
158
+
159
+ # ------------------------------- Mutators ------------------------------ #
160
+
161
+ def add(self, specs: Iterable[OpSpec] | OpSpec) -> set[Tuple[str, TargetOp]]:
162
+ """
163
+ Add or overwrite one or more specs.
164
+ Returns the set of changed keys.
165
+ """
166
+ if isinstance(specs, OpSpec):
167
+ specs = (specs,)
168
+
169
+ changed: set[Tuple[str, TargetOp]] = set()
170
+ with self._lock:
171
+ for sp in specs:
172
+ sp = _ensure_table(sp, self._table)
173
+ k = _spec_key(sp)
174
+ if self._items.get(k) is sp:
175
+ continue # exact object already present
176
+ self._items[k] = sp
177
+ changed.add(k)
178
+ if changed:
179
+ self._version += 1
180
+ if changed:
181
+ self._notify(changed)
182
+ return changed
183
+
184
+ def set(self, specs: Iterable[OpSpec]) -> set[Tuple[str, TargetOp]]:
185
+ """
186
+ Replace all specs with the provided iterable.
187
+ Returns the set of changed keys (union of removed + added/updated).
188
+ """
189
+ new_map: Dict[Tuple[str, TargetOp], OpSpec] = {}
190
+ for sp in specs:
191
+ sp = _ensure_table(sp, self._table)
192
+ new_map[_spec_key(sp)] = sp
193
+
194
+ with self._lock:
195
+ old_keys = set(self._items.keys())
196
+ new_keys = set(new_map.keys())
197
+
198
+ removed = old_keys - new_keys
199
+ added_or_updated = {
200
+ k for k in new_keys if self._items.get(k) is not new_map[k]
201
+ }
202
+
203
+ changed = removed | added_or_updated
204
+ self._items = new_map
205
+ if changed:
206
+ self._version += 1
207
+
208
+ if changed:
209
+ self._notify(changed)
210
+ return changed
211
+
212
+ def remove(
213
+ self, alias: str, target: TargetOp | None = None
214
+ ) -> set[Tuple[str, TargetOp]]:
215
+ """
216
+ Remove specs by alias (optionally constrain to a specific target).
217
+ Returns the set of removed keys.
218
+ """
219
+ removed: set[Tuple[str, TargetOp]] = set()
220
+ with self._lock:
221
+ if target is None:
222
+ # remove all targets under this alias
223
+ for k in list(self._items.keys()):
224
+ if k[0] == alias:
225
+ self._items.pop(k, None)
226
+ removed.add(k)
227
+ else:
228
+ k = (alias, target)
229
+ if k in self._items:
230
+ self._items.pop(k, None)
231
+ removed.add(k)
232
+
233
+ if removed:
234
+ self._version += 1
235
+
236
+ if removed:
237
+ self._notify(removed)
238
+ return removed
239
+
240
+ def clear(self) -> None:
241
+ with self._lock:
242
+ if not self._items:
243
+ return
244
+ self._items.clear()
245
+ self._version += 1
246
+ self._notify(set())
247
+
248
+
249
+ # ------------------------------------------------------------------------------
250
+ # Per-model registry storage (weak keys so classes can be GC'd)
251
+ # ------------------------------------------------------------------------------
252
+
253
+ _REGISTRIES: "weakref.WeakKeyDictionary[type, OpspecRegistry]" = (
254
+ weakref.WeakKeyDictionary()
255
+ )
256
+ _REG_LOCK = RLock()
257
+
258
+
259
+ def get_registry(table: type) -> OpspecRegistry:
260
+ with _REG_LOCK:
261
+ reg = _REGISTRIES.get(table)
262
+ if reg is None:
263
+ reg = OpspecRegistry(table)
264
+ _REGISTRIES[table] = reg
265
+ return reg
266
+
267
+
268
+ # ------------------------------------------------------------------------------
269
+ # Back-compat helpers (v2-style imperative API)
270
+ # ------------------------------------------------------------------------------
271
+
272
+
273
+ def register_ops(table: type, specs: Any) -> set[Tuple[str, TargetOp]]:
274
+ """
275
+ Imperative registration (back-compat).
276
+ Accepts OpSpec, iterable of OpSpec, mapping forms, etc.
277
+ Triggers listeners (i.e., binder refresh) on change.
278
+ """
279
+ reg = get_registry(table)
280
+ coerced = _coerce_to_specs(specs, table)
281
+ return reg.add(coerced)
282
+
283
+
284
+ def get_registered_ops(table: type) -> Tuple[OpSpec, ...]:
285
+ """
286
+ Back-compat reader used by the collector.
287
+ """
288
+ return get_registry(table).get_all()
289
+
290
+
291
+ def clear_registry(table: type) -> None:
292
+ get_registry(table).clear()
293
+
294
+
295
+ __all__ = [
296
+ "OpspecRegistry",
297
+ "get_registry",
298
+ "register_ops",
299
+ "get_registered_ops",
300
+ "clear_registry",
301
+ ]
@@ -0,0 +1,99 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from functools import lru_cache
5
+ from typing import Any, Callable, Dict
6
+
7
+ from .types import OpSpec
8
+ from .decorators import _maybe_await, _OpDecl, _infer_arity, _normalize_persist, _unwrap
9
+ from ..runtime.executor import _Ctx
10
+
11
+ logger = logging.getLogger("uvicorn")
12
+
13
+
14
+ def _merge_mro_dict(cls: type, attr: str) -> Dict[str, Any]:
15
+ merged: Dict[str, Any] = {}
16
+ for base in reversed(cls.__mro__):
17
+ merged.update(getattr(base, attr, {}) or {})
18
+ return merged
19
+
20
+
21
+ @lru_cache(maxsize=None)
22
+ def mro_alias_map_for(table: type) -> Dict[str, str]:
23
+ """Collect alias overrides across the table's MRO."""
24
+ return _merge_mro_dict(table, "__tigrbl_aliases__")
25
+
26
+
27
+ def _wrap_ctx_core(table: type, func: Callable[..., Any]) -> Callable[..., Any]:
28
+ """Adapt `(cls, ctx)` op to `(p, *, db, request, ctx)` handler signature."""
29
+
30
+ async def core(p=None, *, db=None, request=None, ctx: Dict[str, Any] | None = None):
31
+ ctx = _Ctx.ensure(request=request, db=db, seed=ctx)
32
+ if p is not None:
33
+ ctx["payload"] = p
34
+ bound = func.__get__(table, table)
35
+ res = await _maybe_await(bound(ctx))
36
+ return res if res is not None else ctx.get("result")
37
+
38
+ core.__name__ = getattr(func, "__name__", "core")
39
+ core.__qualname__ = getattr(func, "__qualname__", core.__name__)
40
+ return core
41
+
42
+
43
+ @lru_cache(maxsize=None)
44
+ def mro_collect_decorated_ops(table: type) -> list[OpSpec]:
45
+ """Collect ctx-only op declarations across the table's MRO."""
46
+
47
+ logger.info("Collecting decorated ops for %s", table.__name__)
48
+ out: list[OpSpec] = []
49
+ seen: set[str] = set()
50
+
51
+ for base in table.__mro__:
52
+ for name, attr in vars(base).items():
53
+ if name in seen:
54
+ continue
55
+ func = _unwrap(attr)
56
+ decl: _OpDecl | None = getattr(func, "__tigrbl_op_decl__", None)
57
+ if not decl:
58
+ continue
59
+
60
+ target = decl.target or "custom"
61
+ arity = decl.arity or _infer_arity(target)
62
+ persist = _normalize_persist(decl.persist)
63
+ alias = decl.alias or name
64
+
65
+ expose_kwargs: dict[str, Any] = {}
66
+ extra: dict[str, Any] = {}
67
+ if decl.rest is not None:
68
+ expose_kwargs["expose_routes"] = bool(decl.rest)
69
+ elif alias != target and target in {
70
+ "read",
71
+ "update",
72
+ "delete",
73
+ "list",
74
+ "clear",
75
+ }:
76
+ expose_kwargs["expose_routes"] = False
77
+
78
+ spec = OpSpec(
79
+ table=table,
80
+ alias=alias,
81
+ target=target,
82
+ arity=arity,
83
+ persist=persist,
84
+ handler=_wrap_ctx_core(table, func),
85
+ request_model=decl.request_schema,
86
+ response_model=decl.response_schema,
87
+ hooks=(),
88
+ status_code=decl.status_code,
89
+ extra=extra,
90
+ **expose_kwargs,
91
+ )
92
+ out.append(spec)
93
+ seen.add(name)
94
+
95
+ logger.debug("Collected %d ops for %s", len(out), table.__name__)
96
+ return out
97
+
98
+
99
+ __all__ = ["mro_alias_map_for", "mro_collect_decorated_ops"]
tigrbl/op/resolver.py ADDED
@@ -0,0 +1,216 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import replace
4
+ import logging
5
+ import re
6
+ from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence, Tuple
7
+
8
+ from .types import OpSpec, TargetOp
9
+ from ..config.constants import TIGRBL_OPS_ATTR
10
+ from .canonical import should_wire_canonical
11
+ from .mro_collect import mro_alias_map_for
12
+
13
+ try:
14
+ # Per-model registry (observable, triggers rebind elsewhere)
15
+ from .model_registry import get_registered_ops # type: ignore
16
+ except Exception: # pragma: no cover
17
+
18
+ def get_registered_ops(model: type) -> Sequence[OpSpec]: # shim
19
+ return ()
20
+
21
+
22
+ logger = logging.getLogger("uvicorn")
23
+
24
+ _ALIAS_RE = re.compile(r"^[a-z][a-z0-9_]*$")
25
+
26
+
27
+ def _ensure_spec_table(spec: OpSpec, table: type) -> OpSpec:
28
+ if spec.table is table:
29
+ return spec
30
+ return replace(spec, table=table)
31
+
32
+
33
+ def _as_specs(value: Any, table: type) -> List[OpSpec]:
34
+ """Normalize various `__tigrbl_ops__` shapes to a list of OpSpec."""
35
+ specs: List[OpSpec] = []
36
+ if value is None:
37
+ return specs
38
+
39
+ def _from_kwargs(kwargs: Mapping[str, Any]) -> Optional[OpSpec]:
40
+ if "alias" not in kwargs or "target" not in kwargs:
41
+ return None
42
+ try:
43
+ spec = OpSpec(table=table, hooks=(), extra={}, **kwargs)
44
+ return spec
45
+ except Exception: # pragma: no cover - defensive
46
+ return None
47
+
48
+ if isinstance(value, OpSpec):
49
+ specs.append(_ensure_spec_table(value, table))
50
+ elif isinstance(value, Mapping):
51
+ for alias, cfg in value.items():
52
+ if isinstance(cfg, Mapping):
53
+ spec = _from_kwargs({"alias": alias, **cfg})
54
+ if spec:
55
+ specs.append(spec)
56
+ elif isinstance(value, Iterable):
57
+ for item in value:
58
+ if isinstance(item, OpSpec):
59
+ specs.append(_ensure_spec_table(item, table))
60
+ elif isinstance(item, Mapping):
61
+ spec = _from_kwargs(item)
62
+ if spec:
63
+ specs.append(spec)
64
+ else:
65
+ spec = _from_kwargs(
66
+ {
67
+ "alias": getattr(value, "alias", None),
68
+ "target": getattr(value, "target", None),
69
+ }
70
+ )
71
+ if spec:
72
+ specs.append(spec)
73
+ return specs
74
+
75
+
76
+ def _generate_canonical(table: type) -> List[OpSpec]:
77
+ """Generate canonical CRUD specs based on model attributes."""
78
+ specs: List[OpSpec] = []
79
+ targets: List[Tuple[str, TargetOp]] = [
80
+ ("create", "create"),
81
+ ("read", "read"),
82
+ ("update", "update"),
83
+ # Include canonical "replace" so RPC callers get full CRUD semantics
84
+ # without opting into the Replaceable mixin.
85
+ ("replace", "replace"),
86
+ ("merge", "merge"),
87
+ ("delete", "delete"),
88
+ ("list", "list"),
89
+ ("clear", "clear"),
90
+ ("bulk_create", "bulk_create"),
91
+ ("bulk_update", "bulk_update"),
92
+ ("bulk_replace", "bulk_replace"),
93
+ ("bulk_merge", "bulk_merge"),
94
+ ("bulk_delete", "bulk_delete"),
95
+ ]
96
+ collection_targets = {
97
+ "create",
98
+ "list",
99
+ "clear",
100
+ "bulk_create",
101
+ "bulk_update",
102
+ "bulk_replace",
103
+ "bulk_merge",
104
+ "bulk_delete",
105
+ }
106
+ for alias, target in targets:
107
+ if not should_wire_canonical(table, target):
108
+ continue
109
+ specs.append(
110
+ OpSpec(
111
+ table=table,
112
+ alias=alias,
113
+ target=target,
114
+ arity="collection" if target in collection_targets else "member",
115
+ persist="default",
116
+ handler=None,
117
+ request_model=None,
118
+ response_model=None,
119
+ hooks=(),
120
+ status_code=None,
121
+ extra={},
122
+ )
123
+ )
124
+ return specs
125
+
126
+
127
+ def _collect_class_declared(model: type) -> List[OpSpec]:
128
+ out: List[OpSpec] = []
129
+ raw = getattr(model, TIGRBL_OPS_ATTR, None)
130
+ if isinstance(raw, Mapping) or isinstance(raw, Iterable):
131
+ out.extend(_as_specs(raw, model))
132
+ return out
133
+
134
+
135
+ def _collect_registry(model: type) -> List[OpSpec]:
136
+ return list(get_registered_ops(model))
137
+
138
+
139
+ def _dedupe(
140
+ existing: Dict[Tuple[str, str], OpSpec], incoming: Iterable[OpSpec]
141
+ ) -> None:
142
+ for sp in incoming:
143
+ if not isinstance(sp, OpSpec):
144
+ continue
145
+ if not sp.alias or not sp.target:
146
+ continue
147
+ existing[(sp.alias, sp.target)] = sp
148
+
149
+
150
+ def _apply_alias_ctx_to_canon(specs: List[OpSpec], model: type) -> List[OpSpec]:
151
+ aliases = mro_alias_map_for(model)
152
+ overrides: Mapping[str, Mapping[str, Any]] = (
153
+ getattr(model, "__tigrbl_alias_overrides__", {}) or {}
154
+ )
155
+
156
+ if not aliases and not overrides:
157
+ return specs
158
+
159
+ out: List[OpSpec] = []
160
+ for sp in specs:
161
+ canon = sp.target
162
+ new_alias = aliases.get(canon, sp.alias)
163
+ mutated = sp
164
+ if new_alias != sp.alias:
165
+ if not isinstance(new_alias, str) or not _ALIAS_RE.match(new_alias):
166
+ logger.warning(
167
+ "Invalid alias %r for verb %r on %s; keeping %r",
168
+ new_alias,
169
+ sp.target,
170
+ model.__name__,
171
+ sp.alias,
172
+ )
173
+ else:
174
+ mutated = replace(mutated, alias=new_alias, path_suffix="")
175
+
176
+ ov = overrides.get(canon)
177
+ if ov:
178
+ kwargs = {}
179
+ if "request_schema" in ov:
180
+ kwargs["request_model"] = ov["request_schema"]
181
+ if "response_schema" in ov:
182
+ kwargs["response_model"] = ov["response_schema"]
183
+ if "persist" in ov:
184
+ kwargs["persist"] = ov["persist"]
185
+ if "arity" in ov:
186
+ kwargs["arity"] = ov["arity"]
187
+ if "rest" in ov:
188
+ kwargs["expose_routes"] = bool(ov["rest"])
189
+ if "engine" in ov:
190
+ kwargs["engine"] = ov["engine"]
191
+ if kwargs:
192
+ mutated = replace(mutated, **kwargs)
193
+ out.append(mutated)
194
+ return out
195
+
196
+
197
+ def resolve(model: type) -> List[OpSpec]:
198
+ canon = _generate_canonical(model)
199
+ canon = _apply_alias_ctx_to_canon(canon, model)
200
+
201
+ class_specs = _collect_class_declared(model)
202
+ reg_specs = _collect_registry(model)
203
+
204
+ merged: Dict[Tuple[str, str], OpSpec] = {}
205
+ _dedupe(merged, canon)
206
+ _dedupe(merged, class_specs)
207
+ _dedupe(merged, reg_specs)
208
+
209
+ specs = list(merged.values())
210
+ specs = [_ensure_spec_table(sp, model) for sp in specs]
211
+
212
+ logger.debug("ops.resolver.resolve(%s): %d specs", model.__name__, len(specs))
213
+ return specs
214
+
215
+
216
+ __all__ = ["resolve"]