tigrbl 0.0.1.dev1__py3-none-any.whl → 0.3.0.dev3__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.dev3.dist-info/LICENSE +201 -0
  247. tigrbl-0.3.0.dev3.dist-info/METADATA +501 -0
  248. tigrbl-0.3.0.dev3.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.dev3.dist-info}/WHEEL +0 -0
@@ -0,0 +1,353 @@
1
+ # tigrbl/v3/runtime/labels.py
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass, replace
5
+ from typing import Dict, Optional, Tuple, Literal, Iterable, Set
6
+ import re as _re
7
+
8
+ from . import events as _ev
9
+
10
+ # ──────────────────────────────────────────────────────────────────────────────
11
+ # Grammar
12
+ # Canonical (display) form:
13
+ # - secdep: <name> → "secdep:<name>"
14
+ # - dep: <name> → "dep:<name>"
15
+ # - sys: <subject>@<PHASE> → "sys:txn:begin@START_TX"
16
+ # - atom: <domain>:<subject>@<anchor>[#field]
17
+ # - hook: <domain>:<subject>@<anchor>[#field]
18
+ #
19
+ # Notes:
20
+ # - step_kind ∈ {secdep, dep, sys, atom, hook}
21
+ # - domains are restricted to: {emit, out, refresh, resolve, response, schema, storage, wire}
22
+ # - anchors for atom/hook MUST be canonical events from runtime/events.py
23
+ # - sys anchors MUST be one of PHASES (typically START_TX, HANDLER, END_TX)
24
+ # ──────────────────────────────────────────────────────────────────────────────
25
+
26
+ STEP_KINDS: Tuple[str, ...] = ("secdep", "dep", "sys", "atom", "hook")
27
+ StepKind = Literal["secdep", "dep", "sys", "atom", "hook"]
28
+ DOMAINS: Tuple[str, ...] = (
29
+ "emit",
30
+ "out",
31
+ "refresh",
32
+ "resolve",
33
+ "response",
34
+ "schema",
35
+ "storage",
36
+ "wire",
37
+ "sys",
38
+ )
39
+
40
+ # minimal token rules (tight but readable)
41
+ # - domain/subject: letters, digits, underscore, dash; subject may contain colon to support composite subjects like "txn:begin"
42
+ # - field: letters, digits, underscore, dash, dot
43
+
44
+ _RE_NAME = _re.compile(r"^[A-Za-z0-9_.:-]+$") # secdep/dep name (tolerant)
45
+ _RE_SUBJECT = _re.compile(
46
+ r"^[A-Za-z0-9_:-]+$"
47
+ ) # allow ":" inside subject (e.g., "txn:begin")
48
+ _RE_FIELD = _re.compile(r"^[A-Za-z0-9_.-]+$") # instance suffix
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class Label:
53
+ kind: StepKind
54
+ subject: str
55
+ domain: Optional[str] = None
56
+ anchor: Optional[str] = None
57
+ field: Optional[str] = None
58
+
59
+ # ── renderers ──────────────────────────────────────────────────────────────
60
+ def render(self, *, pretty: bool = True) -> str:
61
+ """
62
+ Pretty = human-facing (short secdep/dep). False = always canonicalized shape when possible.
63
+ """
64
+ if self.kind in ("secdep", "dep"):
65
+ return f"{self.kind}:{self.subject}"
66
+ if self.kind == "sys":
67
+ return f"sys:{self.subject}@{self.anchor}"
68
+ # atom / hook
69
+ base = f"{self.kind}:{self.domain}:{self.subject}@{self.anchor}"
70
+ return f"{base}#{self.field}" if self.field else base
71
+
72
+ __str__ = render
73
+
74
+ # ── convenience ───────────────────────────────────────────────────────────
75
+ def with_field(self, field: Optional[str]) -> "Label":
76
+ _validate_field(field)
77
+ return replace(self, field=field)
78
+
79
+ def clear_field(self) -> "Label":
80
+ return replace(self, field=None)
81
+
82
+ # ── predicates ────────────────────────────────────────────────────────────
83
+ @property
84
+ def is_secdep(self) -> bool:
85
+ return self.kind == "secdep"
86
+
87
+ @property
88
+ def is_dep(self) -> bool:
89
+ return self.kind == "dep"
90
+
91
+ @property
92
+ def is_sys(self) -> bool:
93
+ return self.kind == "sys"
94
+
95
+ @property
96
+ def is_atom(self) -> bool:
97
+ return self.kind == "atom"
98
+
99
+ @property
100
+ def is_hook(self) -> bool:
101
+ return self.kind == "hook"
102
+
103
+
104
+ # ──────────────────────────────────────────────────────────────────────────────
105
+ # Builders (typed helpers)
106
+ # ──────────────────────────────────────────────────────────────────────────────
107
+
108
+
109
+ def make_dep(name: str) -> Label:
110
+ _require(_RE_NAME.match(name), f"Invalid dep name {name!r}")
111
+ return Label(kind="dep", subject=name)
112
+
113
+
114
+ def make_secdep(name: str) -> Label:
115
+ _require(_RE_NAME.match(name), f"Invalid secdep name {name!r}")
116
+ return Label(kind="secdep", subject=name)
117
+
118
+
119
+ def make_sys(subject: str, phase: _ev.Phase) -> Label:
120
+ _require(subject and _RE_SUBJECT.match(subject), f"Invalid sys subject {subject!r}")
121
+ _require(phase in _ev.PHASES, f"Invalid sys phase {phase!r}")
122
+ return Label(kind="sys", subject=subject, anchor=phase)
123
+
124
+
125
+ def make_atom(
126
+ domain: str, subject: str, anchor: str, field: Optional[str] = None
127
+ ) -> Label:
128
+ _validate_domain(domain)
129
+ _validate_subject(subject)
130
+ _validate_anchor(anchor)
131
+ _validate_field(field)
132
+ return Label(
133
+ kind="atom", domain=domain, subject=subject, anchor=anchor, field=field
134
+ )
135
+
136
+
137
+ def make_hook(
138
+ domain: str, subject: str, anchor: str, field: Optional[str] = None
139
+ ) -> Label:
140
+ _validate_domain(domain)
141
+ _validate_subject(subject)
142
+ _validate_anchor(anchor)
143
+ _validate_field(field)
144
+ return Label(
145
+ kind="hook", domain=domain, subject=subject, anchor=anchor, field=field
146
+ )
147
+
148
+
149
+ # ──────────────────────────────────────────────────────────────────────────────
150
+ # Parse / validate
151
+ # ──────────────────────────────────────────────────────────────────────────────
152
+
153
+
154
+ def parse(s: str) -> Label:
155
+ """
156
+ Parse a label string into a Label object. Raises ValueError on any mismatch.
157
+ Accepts the canonical display forms described in the header.
158
+ """
159
+ if not isinstance(s, str) or ":" not in s:
160
+ raise ValueError(f"Not a label: {s!r}")
161
+
162
+ # secdep:name / dep:name
163
+ if s.startswith("secdep:"):
164
+ name = s[len("secdep:") :]
165
+ _require(_RE_NAME.match(name), f"Invalid secdep name {name!r}")
166
+ return Label(kind="secdep", subject=name)
167
+ if s.startswith("dep:"):
168
+ name = s[len("dep:") :]
169
+ _require(_RE_NAME.match(name), f"Invalid dep name {name!r}")
170
+ return Label(kind="dep", subject=name)
171
+
172
+ # sys:subject@PHASE
173
+ if s.startswith("sys:"):
174
+ rest = s[len("sys:") :]
175
+ try:
176
+ subject, phase = rest.split("@", 1)
177
+ except ValueError as e:
178
+ raise ValueError("System label must be 'sys:<subject>@<PHASE>'") from e
179
+ _require(_RE_SUBJECT.match(subject), f"Invalid sys subject {subject!r}")
180
+ _require(phase in _ev.PHASES, f"Invalid sys phase {phase!r}")
181
+ return Label(kind="sys", subject=subject, anchor=phase)
182
+
183
+ # atom:/hook:
184
+ if s.startswith("atom:") or s.startswith("hook:"):
185
+ kind: StepKind = "atom" if s.startswith("atom:") else "hook"
186
+ rest = s[len("atom:") :] if kind == "atom" else s[len("hook:") :]
187
+
188
+ # Split domain:subject@anchor[#field]
189
+ try:
190
+ dom, rest2 = rest.split(":", 1)
191
+ except ValueError as e:
192
+ raise ValueError(f"{kind} label must start with '<domain>:...'") from e
193
+
194
+ try:
195
+ subj, rest3 = rest2.split("@", 1)
196
+ except ValueError as e:
197
+ raise ValueError(f"{kind} label must include '@<anchor>'") from e
198
+
199
+ anchor, field = _split_anchor_field(rest3)
200
+
201
+ _validate_domain(dom)
202
+
203
+ _validate_subject(subj)
204
+ _validate_anchor(anchor)
205
+ _validate_field(field)
206
+
207
+ return Label(kind=kind, domain=dom, subject=subj, anchor=anchor, field=field)
208
+
209
+ raise ValueError(f"Unknown step kind in label {s!r}")
210
+
211
+
212
+ def validate(label: Label) -> None:
213
+ """Raise ValueError if the label violates the grammar or constraints."""
214
+ k = label.kind
215
+ if k in ("secdep", "dep"):
216
+ _require(
217
+ label.subject and _RE_NAME.match(label.subject),
218
+ f"Invalid {k} name {label.subject!r}",
219
+ )
220
+ _require(
221
+ label.domain is None and label.anchor is None,
222
+ f"{k} cannot carry domain/anchor",
223
+ )
224
+ _validate_field(label.field)
225
+ return
226
+
227
+ if k == "sys":
228
+ _require(
229
+ label.subject and _RE_SUBJECT.match(label.subject),
230
+ f"Invalid sys subject {label.subject!r}",
231
+ )
232
+ _require(label.anchor in _ev.PHASES, f"Invalid sys phase {label.anchor!r}")
233
+ _require(label.domain is None, "sys cannot carry a domain")
234
+ _validate_field(label.field)
235
+ return
236
+
237
+ if k == "atom":
238
+ _validate_domain(label.domain)
239
+ _validate_subject(label.subject)
240
+ _validate_anchor(label.anchor)
241
+ _validate_field(label.field)
242
+ return
243
+
244
+ if k == "hook":
245
+ _validate_domain(label.domain)
246
+ _validate_subject(label.subject)
247
+ _validate_anchor(label.anchor)
248
+ _validate_field(label.field)
249
+ return
250
+
251
+ raise ValueError(f"Unknown label kind {k!r}")
252
+
253
+
254
+ # ──────────────────────────────────────────────────────────────────────────────
255
+ # Helpers / Legend
256
+ # ──────────────────────────────────────────────────────────────────────────────
257
+
258
+
259
+ def _split_anchor_field(s: str) -> Tuple[str, Optional[str]]:
260
+ """Split 'anchor[#field]' into (anchor, field?)."""
261
+ if "#" in s:
262
+ anchor, field = s.split("#", 1)
263
+ return anchor, (field or None)
264
+ return s, None
265
+
266
+
267
+ def _validate_domain(domain: Optional[str]) -> None:
268
+ _require(
269
+ domain is not None and domain in DOMAINS,
270
+ f"Invalid domain {domain!r}; expected one of {list(DOMAINS)}",
271
+ )
272
+
273
+
274
+ def _validate_subject(subj: Optional[str]) -> None:
275
+ _require(subj is not None and _RE_SUBJECT.match(subj), f"Invalid subject {subj!r}")
276
+
277
+
278
+ def _validate_anchor(anchor: Optional[str]) -> None:
279
+ _require(
280
+ anchor is not None and (_ev.is_valid_event(anchor) or anchor in _ev.PHASES),
281
+ f"Invalid or unknown anchor {anchor!r}",
282
+ )
283
+
284
+
285
+ def _validate_field(field: Optional[str]) -> None:
286
+ if field is None:
287
+ return
288
+ _require(_RE_FIELD.match(field), f"Invalid field instance suffix {field!r}")
289
+
290
+
291
+ def _require(cond: bool, msg: str) -> None:
292
+ if not cond:
293
+ raise ValueError(msg)
294
+
295
+
296
+ def legend() -> Dict[str, object]:
297
+ """
298
+ Return a stable dictionary suitable for a /diagnostics/labels/legend endpoint.
299
+ Includes step kinds, atom domains, sys phases, and ordered anchors.
300
+ """
301
+ return {
302
+ "step_kinds": STEP_KINDS,
303
+ "atom_domains": DOMAINS,
304
+ "sys_phases": _ev.PHASES,
305
+ "anchors": _ev.all_events_ordered(),
306
+ "notes": {
307
+ "secdep/dep": "Run before any anchor; shape is 'secdep:<name>' / 'dep:<name>'.",
308
+ "sys": "Subject describes the system op; anchor is a PHASE.",
309
+ "atom/hook": "Use '<domain>:<subject>@<anchor>#field' (field optional).",
310
+ },
311
+ }
312
+
313
+
314
+ # ──────────────────────────────────────────────────────────────────────────────
315
+ # Bulk utilities (nice-to-haves for planner/trace)
316
+ # ──────────────────────────────────────────────────────────────────────────────
317
+
318
+
319
+ def ensure_all_valid(labels: Iterable[Label]) -> None:
320
+ for lbl in labels:
321
+ validate(lbl)
322
+
323
+
324
+ def only_atoms(labels: Iterable[Label]) -> Tuple[Label, ...]:
325
+ return tuple(label for label in labels if label.kind == "atom")
326
+
327
+
328
+ def only_hooks(labels: Iterable[Label]) -> Tuple[Label, ...]:
329
+ return tuple(label for label in labels if label.kind == "hook")
330
+
331
+
332
+ def fields_used(labels: Iterable[Label]) -> Set[str]:
333
+ return {label.field for label in labels if label.field}
334
+
335
+
336
+ __all__ = [
337
+ "STEP_KINDS",
338
+ "StepKind",
339
+ "DOMAINS",
340
+ "Label",
341
+ "make_dep",
342
+ "make_secdep",
343
+ "make_sys",
344
+ "make_atom",
345
+ "make_hook",
346
+ "parse",
347
+ "validate",
348
+ "legend",
349
+ "ensure_all_valid",
350
+ "only_atoms",
351
+ "only_hooks",
352
+ "fields_used",
353
+ ]
@@ -0,0 +1,87 @@
1
+ from __future__ import annotations
2
+ from typing import Any, Mapping, Dict
3
+ from types import SimpleNamespace
4
+
5
+ from .kernel import _default_kernel as K # single, app-scoped kernel
6
+
7
+
8
+ def _ensure_temp(ctx: Any) -> Dict[str, Any]:
9
+ tmp = getattr(ctx, "temp", None)
10
+ if not isinstance(tmp, dict):
11
+ tmp = {}
12
+ setattr(ctx, "temp", tmp)
13
+ return tmp
14
+
15
+
16
+ def opview_from_ctx(ctx: Any):
17
+ """
18
+ Resolve the ``OpView`` for this request context or raise a runtime error.
19
+
20
+ Preferred resolution path is via ``ctx.opview`` which should be attached by
21
+ the caller. Falling back to kernel lookups requires ``ctx.app`` (or
22
+ ``ctx.api``), ``ctx.model`` (or derivable from ``ctx.obj``), and ``ctx.op``
23
+ (or ``ctx.method``).
24
+ """
25
+ ov = getattr(ctx, "opview", None)
26
+ if ov is not None:
27
+ return ov
28
+
29
+ app = getattr(ctx, "app", None) or getattr(ctx, "api", None)
30
+ model = getattr(ctx, "model", None)
31
+ if model is None:
32
+ obj = getattr(ctx, "obj", None)
33
+ if obj is not None:
34
+ model = type(obj)
35
+ alias = getattr(ctx, "op", None) or getattr(ctx, "method", None)
36
+
37
+ if app and model and alias:
38
+ # One-kernel-per-app, prime once; raises if not compiled
39
+ return K.get_opview(app, model, alias)
40
+
41
+ if alias:
42
+ specs = getattr(ctx, "specs", None)
43
+ if specs is not None:
44
+ return K._compile_opview_from_specs(specs, SimpleNamespace(alias=alias))
45
+
46
+ missing = []
47
+ if not alias:
48
+ missing.append("op")
49
+ if not app:
50
+ missing.append("app")
51
+ if not model:
52
+ missing.append("model")
53
+ # runtime-error policy: eject loudly; no skip
54
+ raise RuntimeError(f"ctx_missing:{','.join(missing)}")
55
+
56
+
57
+ def ensure_schema_in(ctx: Any, ov) -> Mapping[str, Any]:
58
+ """
59
+ Load precompiled inbound schema from OpView into ctx.temp['schema_in'] if absent.
60
+ """
61
+ temp = _ensure_temp(ctx)
62
+ if "schema_in" not in temp:
63
+ bf = ov.schema_in.by_field
64
+ req = tuple(n for n, e in bf.items() if e.get("required"))
65
+ temp["schema_in"] = {
66
+ "fields": ov.schema_in.fields,
67
+ "by_field": bf,
68
+ "required": req,
69
+ }
70
+ return temp["schema_in"]
71
+
72
+
73
+ def ensure_schema_out(ctx: Any, ov) -> Mapping[str, Any]:
74
+ """
75
+ Load precompiled outbound schema from OpView into ctx.temp['schema_out'] if absent.
76
+ """
77
+ temp = _ensure_temp(ctx)
78
+ if "schema_out" not in temp:
79
+ temp["schema_out"] = {
80
+ "fields": ov.schema_out.fields,
81
+ "by_field": ov.schema_out.by_field,
82
+ "expose": ov.schema_out.expose,
83
+ }
84
+ return temp["schema_out"]
85
+
86
+
87
+ __all__ = ["opview_from_ctx", "ensure_schema_in", "ensure_schema_out", "_ensure_temp"]
@@ -0,0 +1,256 @@
1
+ # tigrbl/v3/runtime/ordering.py
2
+ from __future__ import annotations
3
+
4
+ from collections import defaultdict
5
+ from dataclasses import dataclass
6
+ from typing import Dict, Iterable, List, Mapping, Optional, Sequence, Tuple
7
+
8
+ from . import events as _ev
9
+ from .labels import Label
10
+
11
+ # ──────────────────────────────────────────────────────────────────────────────
12
+ # Default in-anchor preferences (safe, minimal)
13
+ # - Each entry is a list of "domain:subject" tokens in desired order.
14
+ # - DEFAULT_EDGES are derived from these (u -> v).
15
+ # ──────────────────────────────────────────────────────────────────────────────
16
+
17
+ # tokens: "domain:subject"
18
+ _PREF: Dict[str, Tuple[str, ...]] = {
19
+ _ev.SCHEMA_COLLECT_IN: ("schema:collect_in",),
20
+ _ev.IN_VALIDATE: ("wire:build_in", "wire:validate_in"),
21
+ _ev.RESOLVE_VALUES: ("resolve:assemble", "resolve:paired_gen"),
22
+ _ev.PRE_FLUSH: ("storage:to_stored",),
23
+ _ev.EMIT_ALIASES_PRE: ("emit:paired_pre",),
24
+ _ev.POST_FLUSH: ("refresh:demand",),
25
+ _ev.EMIT_ALIASES_POST: ("emit:paired_post",),
26
+ _ev.SCHEMA_COLLECT_OUT: ("schema:collect_out",),
27
+ _ev.OUT_BUILD: ("wire:build_out",),
28
+ _ev.EMIT_ALIASES_READ: ("emit:readtime_alias",),
29
+ _ev.OUT_DUMP: (
30
+ "wire:dump",
31
+ "out:masking",
32
+ "response:negotiate",
33
+ "response:render",
34
+ "response:template",
35
+ ),
36
+ }
37
+
38
+
39
+ def _derive_edges(
40
+ pref: Mapping[str, Sequence[str]],
41
+ ) -> Dict[str, Tuple[Tuple[str, str], ...]]:
42
+ out: Dict[str, Tuple[Tuple[str, str], ...]] = {}
43
+ for anchor, seq in pref.items():
44
+ edges: List[Tuple[str, str]] = []
45
+ for i in range(len(seq) - 1):
46
+ edges.append((seq[i], seq[i + 1]))
47
+ out[anchor] = tuple(edges)
48
+ return out
49
+
50
+
51
+ _DEFAULT_EDGES: Dict[str, Tuple[Tuple[str, str], ...]] = _derive_edges(_PREF)
52
+
53
+
54
+ # ──────────────────────────────────────────────────────────────────────────────
55
+ # Public API
56
+ # ──────────────────────────────────────────────────────────────────────────────
57
+
58
+
59
+ @dataclass(frozen=True)
60
+ class AnchorPolicy:
61
+ """
62
+ Extra ordering rules for a specific anchor.
63
+ - edges: (u, v) means "u before v" where u/v are "domain:subject" tokens.
64
+ - prefer: stable tie-break priority list of tokens.
65
+ """
66
+
67
+ edges: Tuple[Tuple[str, str], ...] = ()
68
+ prefer: Tuple[str, ...] = ()
69
+
70
+
71
+ def flatten(
72
+ labels: Iterable[Label],
73
+ *,
74
+ persist: bool,
75
+ anchor_policies: Optional[Mapping[str, AnchorPolicy]] = None,
76
+ ) -> List[Label]:
77
+ """
78
+ Produce a flattened order of labels across all anchors.
79
+
80
+ Rules:
81
+ - secdep → dep → PRE_HANDLER anchors → POST_HANDLER anchors → POST_RESPONSE anchors
82
+ - persist-tied anchors are pruned when persist=False (see events.is_persist_tied).
83
+ - Within each anchor, perform a topo sort using DEFAULT_EDGES + anchor_policies[anchor].edges,
84
+ with stable tie-breaks using preferences + (kind, domain, subject, field).
85
+ """
86
+ # Partition by kind/anchor
87
+ secdeps: List[Label] = []
88
+ deps: List[Label] = []
89
+ by_anchor: Dict[str, List[Label]] = defaultdict(list)
90
+
91
+ for lbl in labels:
92
+ if lbl.kind == "secdep":
93
+ secdeps.append(lbl)
94
+ elif lbl.kind == "dep":
95
+ deps.append(lbl)
96
+ else:
97
+ if not lbl.anchor:
98
+ raise ValueError(f"Label missing anchor: {lbl}")
99
+ by_anchor[lbl.anchor].append(lbl)
100
+
101
+ anchors_present = tuple(by_anchor.keys())
102
+ anchors = _ev.order_events(anchors_present)
103
+ if not persist:
104
+ anchors = _ev.prune_events_for_persist(anchors, persist=False)
105
+
106
+ out: List[Label] = []
107
+
108
+ out.extend(secdeps)
109
+ out.extend(deps)
110
+
111
+ _append_anchor_block(
112
+ out,
113
+ by_anchor,
114
+ anchors,
115
+ target_phase="PRE_HANDLER",
116
+ anchor_policies=anchor_policies,
117
+ persist=persist,
118
+ )
119
+ _append_anchor_block(
120
+ out,
121
+ by_anchor,
122
+ anchors,
123
+ target_phase="POST_HANDLER",
124
+ anchor_policies=anchor_policies,
125
+ persist=persist,
126
+ )
127
+ _append_anchor_block(
128
+ out,
129
+ by_anchor,
130
+ anchors,
131
+ target_phase="POST_RESPONSE",
132
+ anchor_policies=anchor_policies,
133
+ persist=persist,
134
+ )
135
+
136
+ return out
137
+
138
+
139
+ # ──────────────────────────────────────────────────────────────────────────────
140
+ # Anchor block assembly
141
+ # ──────────────────────────────────────────────────────────────────────────────
142
+
143
+
144
+ def _append_anchor_block(
145
+ out: List[Label],
146
+ by_anchor: Mapping[str, Sequence[Label]],
147
+ anchors: Sequence[str],
148
+ *,
149
+ target_phase: _ev.Phase,
150
+ anchor_policies: Optional[Mapping[str, AnchorPolicy]],
151
+ persist: bool,
152
+ ) -> None:
153
+ """Append all labels for anchors in a given phase in canonical anchor order."""
154
+ for anchor in anchors:
155
+ if _ev.phase_for_event(anchor) != target_phase:
156
+ continue
157
+ if not persist and _ev.is_persist_tied(anchor):
158
+ continue
159
+ group = list(by_anchor.get(anchor, ()))
160
+ if not group:
161
+ continue
162
+ policy = anchor_policies.get(anchor) if anchor_policies else None
163
+ ordered = order_within_anchor(anchor, group, policy)
164
+ out.extend(ordered)
165
+
166
+
167
+ # ──────────────────────────────────────────────────────────────────────────────
168
+ # In-anchor ordering (topological, deterministic)
169
+ # ──────────────────────────────────────────────────────────────────────────────
170
+
171
+
172
+ def order_within_anchor(
173
+ anchor: str,
174
+ labels: Sequence[Label],
175
+ policy: Optional[AnchorPolicy] = None,
176
+ ) -> List[Label]:
177
+ """
178
+ Topologically sort labels within an anchor.
179
+
180
+ Nodes are individual labels; edges are lifted from token-level rules where
181
+ token = "domain:subject". If multiple labels share the same token (e.g., per-field),
182
+ edges fan-out to all matching nodes.
183
+ """
184
+ # Build token index
185
+ tokens: Dict[Label, str] = {}
186
+ by_token: Dict[str, List[Label]] = defaultdict(list)
187
+ for label in labels:
188
+ if not label.domain:
189
+ # hooks always have domain; atoms must have domain by grammar
190
+ raise ValueError(f"In-anchor item missing domain: {label}")
191
+ t = f"{label.domain}:{label.subject}"
192
+ tokens[label] = t
193
+ by_token[t].append(label)
194
+
195
+ # Collect edges: defaults + policy
196
+ edges = list(_DEFAULT_EDGES.get(anchor, ()))
197
+ if policy and policy.edges:
198
+ edges.extend(policy.edges)
199
+
200
+ # Build adjacency on label nodes (fan-out pairwise where tokens exist)
201
+ adj: Dict[Label, List[Label]] = {label: [] for label in labels}
202
+ indeg: Dict[Label, int] = {label: 0 for label in labels}
203
+
204
+ def _present(tok: str) -> bool:
205
+ return tok in by_token
206
+
207
+ for u_tok, v_tok in edges:
208
+ if not (_present(u_tok) and _present(v_tok)):
209
+ continue
210
+ for u in by_token[u_tok]:
211
+ for v in by_token[v_tok]:
212
+ if v not in adj[u]:
213
+ adj[u].append(v)
214
+ indeg[v] += 1
215
+
216
+ # Kahn topo with deterministic tie-breaks
217
+ # Priority: policy.prefer index → DEFAULT preference index → kind (atom<hook) → domain → subject → field
218
+ prefer = tuple(policy.prefer) if policy and policy.prefer else ()
219
+ pref_index: Dict[str, int] = {t: i for i, t in enumerate(prefer)}
220
+ def_pref = _PREF.get(anchor, ())
221
+ def_index: Dict[str, int] = {t: i for i, t in enumerate(def_pref)}
222
+
223
+ def _rank(label: Label) -> Tuple[int, int, int, str, str, str]:
224
+ t = tokens[label]
225
+ p1 = pref_index.get(t, 10_000)
226
+ p2 = def_index.get(t, 10_000)
227
+ k = 0 if label.kind == "atom" else 1 # atoms before hooks by default
228
+ return (p1, p2, k, label.domain or "", label.subject, label.field or "")
229
+
230
+ q: List[Label] = [n for n, d in indeg.items() if d == 0]
231
+ q.sort(key=_rank)
232
+
233
+ out: List[Label] = []
234
+ while q:
235
+ n = q.pop(0)
236
+ out.append(n)
237
+ for v in adj[n]:
238
+ indeg[v] -= 1
239
+ if indeg[v] == 0:
240
+ q.append(v)
241
+ q.sort(key=_rank) # keep deterministic
242
+
243
+ if len(out) != len(labels):
244
+ # Cycle detected — fall back to total order by rank for remaining nodes
245
+ remaining = [n for n in labels if n not in out]
246
+ remaining.sort(key=_rank)
247
+ out.extend(remaining)
248
+
249
+ return out
250
+
251
+
252
+ __all__ = [
253
+ "AnchorPolicy",
254
+ "flatten",
255
+ "order_within_anchor",
256
+ ]