tigrbl 0.0.1.dev1__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tigrbl/README.md +94 -0
- tigrbl/__init__.py +139 -14
- tigrbl/api/__init__.py +6 -0
- tigrbl/api/_api.py +97 -0
- tigrbl/api/api_spec.py +30 -0
- tigrbl/api/mro_collect.py +43 -0
- tigrbl/api/shortcuts.py +56 -0
- tigrbl/api/tigrbl_api.py +291 -0
- tigrbl/app/__init__.py +0 -0
- tigrbl/app/_app.py +86 -0
- tigrbl/app/_model_registry.py +41 -0
- tigrbl/app/app_spec.py +42 -0
- tigrbl/app/mro_collect.py +67 -0
- tigrbl/app/shortcuts.py +65 -0
- tigrbl/app/tigrbl_app.py +319 -0
- tigrbl/bindings/__init__.py +73 -0
- tigrbl/bindings/api/__init__.py +12 -0
- tigrbl/bindings/api/common.py +109 -0
- tigrbl/bindings/api/include.py +256 -0
- tigrbl/bindings/api/resource_proxy.py +149 -0
- tigrbl/bindings/api/rpc.py +111 -0
- tigrbl/bindings/columns.py +49 -0
- tigrbl/bindings/handlers/__init__.py +11 -0
- tigrbl/bindings/handlers/builder.py +119 -0
- tigrbl/bindings/handlers/ctx.py +74 -0
- tigrbl/bindings/handlers/identifiers.py +228 -0
- tigrbl/bindings/handlers/namespaces.py +51 -0
- tigrbl/bindings/handlers/steps.py +276 -0
- tigrbl/bindings/hooks.py +311 -0
- tigrbl/bindings/model.py +194 -0
- tigrbl/bindings/model_helpers.py +139 -0
- tigrbl/bindings/model_registry.py +77 -0
- tigrbl/bindings/rest/__init__.py +7 -0
- tigrbl/bindings/rest/attach.py +34 -0
- tigrbl/bindings/rest/collection.py +286 -0
- tigrbl/bindings/rest/common.py +120 -0
- tigrbl/bindings/rest/fastapi.py +76 -0
- tigrbl/bindings/rest/helpers.py +119 -0
- tigrbl/bindings/rest/io.py +317 -0
- tigrbl/bindings/rest/io_headers.py +49 -0
- tigrbl/bindings/rest/member.py +386 -0
- tigrbl/bindings/rest/router.py +296 -0
- tigrbl/bindings/rest/routing.py +153 -0
- tigrbl/bindings/rpc.py +364 -0
- tigrbl/bindings/schemas/__init__.py +11 -0
- tigrbl/bindings/schemas/builder.py +348 -0
- tigrbl/bindings/schemas/defaults.py +260 -0
- tigrbl/bindings/schemas/utils.py +193 -0
- tigrbl/column/README.md +62 -0
- tigrbl/column/__init__.py +72 -0
- tigrbl/column/_column.py +96 -0
- tigrbl/column/column_spec.py +40 -0
- tigrbl/column/field_spec.py +31 -0
- tigrbl/column/infer/__init__.py +25 -0
- tigrbl/column/infer/core.py +92 -0
- tigrbl/column/infer/jsonhints.py +44 -0
- tigrbl/column/infer/planning.py +133 -0
- tigrbl/column/infer/types.py +102 -0
- tigrbl/column/infer/utils.py +59 -0
- tigrbl/column/io_spec.py +136 -0
- tigrbl/column/mro_collect.py +59 -0
- tigrbl/column/shortcuts.py +89 -0
- tigrbl/column/storage_spec.py +65 -0
- tigrbl/config/__init__.py +19 -0
- tigrbl/config/constants.py +224 -0
- tigrbl/config/defaults.py +29 -0
- tigrbl/config/resolver.py +295 -0
- tigrbl/core/__init__.py +47 -0
- tigrbl/core/crud/__init__.py +36 -0
- tigrbl/core/crud/bulk.py +168 -0
- tigrbl/core/crud/helpers/__init__.py +76 -0
- tigrbl/core/crud/helpers/db.py +92 -0
- tigrbl/core/crud/helpers/enum.py +86 -0
- tigrbl/core/crud/helpers/filters.py +162 -0
- tigrbl/core/crud/helpers/model.py +123 -0
- tigrbl/core/crud/helpers/normalize.py +99 -0
- tigrbl/core/crud/ops.py +235 -0
- tigrbl/ddl/__init__.py +344 -0
- tigrbl/decorators.py +17 -0
- tigrbl/deps/__init__.py +20 -0
- tigrbl/deps/fastapi.py +45 -0
- tigrbl/deps/favicon.svg +4 -0
- tigrbl/deps/jinja.py +27 -0
- tigrbl/deps/pydantic.py +10 -0
- tigrbl/deps/sqlalchemy.py +94 -0
- tigrbl/deps/starlette.py +36 -0
- tigrbl/engine/__init__.py +45 -0
- tigrbl/engine/_engine.py +144 -0
- tigrbl/engine/bind.py +33 -0
- tigrbl/engine/builders.py +236 -0
- tigrbl/engine/capabilities.py +29 -0
- tigrbl/engine/collect.py +111 -0
- tigrbl/engine/decorators.py +110 -0
- tigrbl/engine/docs/PLUGINS.md +49 -0
- tigrbl/engine/engine_spec.py +355 -0
- tigrbl/engine/plugins.py +52 -0
- tigrbl/engine/registry.py +36 -0
- tigrbl/engine/resolver.py +224 -0
- tigrbl/engine/shortcuts.py +216 -0
- tigrbl/hook/__init__.py +21 -0
- tigrbl/hook/_hook.py +22 -0
- tigrbl/hook/decorators.py +28 -0
- tigrbl/hook/hook_spec.py +24 -0
- tigrbl/hook/mro_collect.py +98 -0
- tigrbl/hook/shortcuts.py +44 -0
- tigrbl/hook/types.py +76 -0
- tigrbl/op/__init__.py +50 -0
- tigrbl/op/_op.py +31 -0
- tigrbl/op/canonical.py +31 -0
- tigrbl/op/collect.py +11 -0
- tigrbl/op/decorators.py +238 -0
- tigrbl/op/model_registry.py +301 -0
- tigrbl/op/mro_collect.py +99 -0
- tigrbl/op/resolver.py +216 -0
- tigrbl/op/types.py +136 -0
- tigrbl/orm/__init__.py +1 -0
- tigrbl/orm/mixins/_RowBound.py +83 -0
- tigrbl/orm/mixins/__init__.py +95 -0
- tigrbl/orm/mixins/bootstrappable.py +113 -0
- tigrbl/orm/mixins/bound.py +47 -0
- tigrbl/orm/mixins/edges.py +40 -0
- tigrbl/orm/mixins/fields.py +165 -0
- tigrbl/orm/mixins/hierarchy.py +54 -0
- tigrbl/orm/mixins/key_digest.py +44 -0
- tigrbl/orm/mixins/lifecycle.py +115 -0
- tigrbl/orm/mixins/locks.py +51 -0
- tigrbl/orm/mixins/markers.py +16 -0
- tigrbl/orm/mixins/operations.py +57 -0
- tigrbl/orm/mixins/ownable.py +337 -0
- tigrbl/orm/mixins/principals.py +98 -0
- tigrbl/orm/mixins/tenant_bound.py +301 -0
- tigrbl/orm/mixins/upsertable.py +118 -0
- tigrbl/orm/mixins/utils.py +49 -0
- tigrbl/orm/tables/__init__.py +72 -0
- tigrbl/orm/tables/_base.py +8 -0
- tigrbl/orm/tables/audit.py +56 -0
- tigrbl/orm/tables/client.py +25 -0
- tigrbl/orm/tables/group.py +29 -0
- tigrbl/orm/tables/org.py +30 -0
- tigrbl/orm/tables/rbac.py +76 -0
- tigrbl/orm/tables/status.py +106 -0
- tigrbl/orm/tables/tenant.py +22 -0
- tigrbl/orm/tables/user.py +39 -0
- tigrbl/response/README.md +34 -0
- tigrbl/response/__init__.py +33 -0
- tigrbl/response/bind.py +12 -0
- tigrbl/response/decorators.py +37 -0
- tigrbl/response/resolver.py +83 -0
- tigrbl/response/shortcuts.py +171 -0
- tigrbl/response/types.py +49 -0
- tigrbl/rest/__init__.py +27 -0
- tigrbl/runtime/README.md +129 -0
- tigrbl/runtime/__init__.py +20 -0
- tigrbl/runtime/atoms/__init__.py +102 -0
- tigrbl/runtime/atoms/emit/__init__.py +42 -0
- tigrbl/runtime/atoms/emit/paired_post.py +158 -0
- tigrbl/runtime/atoms/emit/paired_pre.py +106 -0
- tigrbl/runtime/atoms/emit/readtime_alias.py +120 -0
- tigrbl/runtime/atoms/out/__init__.py +38 -0
- tigrbl/runtime/atoms/out/masking.py +135 -0
- tigrbl/runtime/atoms/refresh/__init__.py +38 -0
- tigrbl/runtime/atoms/refresh/demand.py +130 -0
- tigrbl/runtime/atoms/resolve/__init__.py +40 -0
- tigrbl/runtime/atoms/resolve/assemble.py +167 -0
- tigrbl/runtime/atoms/resolve/paired_gen.py +147 -0
- tigrbl/runtime/atoms/response/__init__.py +19 -0
- tigrbl/runtime/atoms/response/headers_from_payload.py +57 -0
- tigrbl/runtime/atoms/response/negotiate.py +30 -0
- tigrbl/runtime/atoms/response/negotiation.py +43 -0
- tigrbl/runtime/atoms/response/render.py +36 -0
- tigrbl/runtime/atoms/response/renderer.py +116 -0
- tigrbl/runtime/atoms/response/template.py +44 -0
- tigrbl/runtime/atoms/response/templates.py +88 -0
- tigrbl/runtime/atoms/schema/__init__.py +40 -0
- tigrbl/runtime/atoms/schema/collect_in.py +21 -0
- tigrbl/runtime/atoms/schema/collect_out.py +21 -0
- tigrbl/runtime/atoms/storage/__init__.py +38 -0
- tigrbl/runtime/atoms/storage/to_stored.py +167 -0
- tigrbl/runtime/atoms/wire/__init__.py +45 -0
- tigrbl/runtime/atoms/wire/build_in.py +166 -0
- tigrbl/runtime/atoms/wire/build_out.py +87 -0
- tigrbl/runtime/atoms/wire/dump.py +206 -0
- tigrbl/runtime/atoms/wire/validate_in.py +227 -0
- tigrbl/runtime/context.py +206 -0
- tigrbl/runtime/errors/__init__.py +61 -0
- tigrbl/runtime/errors/converters.py +214 -0
- tigrbl/runtime/errors/exceptions.py +124 -0
- tigrbl/runtime/errors/mappings.py +71 -0
- tigrbl/runtime/errors/utils.py +150 -0
- tigrbl/runtime/events.py +209 -0
- tigrbl/runtime/executor/__init__.py +6 -0
- tigrbl/runtime/executor/guards.py +132 -0
- tigrbl/runtime/executor/helpers.py +88 -0
- tigrbl/runtime/executor/invoke.py +150 -0
- tigrbl/runtime/executor/types.py +84 -0
- tigrbl/runtime/kernel.py +644 -0
- tigrbl/runtime/labels.py +353 -0
- tigrbl/runtime/opview.py +89 -0
- tigrbl/runtime/ordering.py +256 -0
- tigrbl/runtime/system.py +279 -0
- tigrbl/runtime/trace.py +330 -0
- tigrbl/schema/__init__.py +38 -0
- tigrbl/schema/_schema.py +27 -0
- tigrbl/schema/builder/__init__.py +17 -0
- tigrbl/schema/builder/build_schema.py +209 -0
- tigrbl/schema/builder/cache.py +24 -0
- tigrbl/schema/builder/compat.py +16 -0
- tigrbl/schema/builder/extras.py +85 -0
- tigrbl/schema/builder/helpers.py +51 -0
- tigrbl/schema/builder/list_params.py +117 -0
- tigrbl/schema/builder/strip_parent_fields.py +70 -0
- tigrbl/schema/collect.py +79 -0
- tigrbl/schema/decorators.py +68 -0
- tigrbl/schema/get_schema.py +86 -0
- tigrbl/schema/schema_spec.py +20 -0
- tigrbl/schema/shortcuts.py +42 -0
- tigrbl/schema/types.py +34 -0
- tigrbl/schema/utils.py +143 -0
- tigrbl/session/README.md +14 -0
- tigrbl/session/__init__.py +28 -0
- tigrbl/session/abc.py +76 -0
- tigrbl/session/base.py +151 -0
- tigrbl/session/decorators.py +43 -0
- tigrbl/session/default.py +118 -0
- tigrbl/session/shortcuts.py +50 -0
- tigrbl/session/spec.py +112 -0
- tigrbl/shortcuts.py +22 -0
- tigrbl/specs.py +44 -0
- tigrbl/system/__init__.py +13 -0
- tigrbl/system/diagnostics/__init__.py +24 -0
- tigrbl/system/diagnostics/compat.py +31 -0
- tigrbl/system/diagnostics/healthz.py +41 -0
- tigrbl/system/diagnostics/hookz.py +51 -0
- tigrbl/system/diagnostics/kernelz.py +20 -0
- tigrbl/system/diagnostics/methodz.py +43 -0
- tigrbl/system/diagnostics/router.py +73 -0
- tigrbl/system/diagnostics/utils.py +43 -0
- tigrbl/system/uvicorn.py +60 -0
- tigrbl/table/__init__.py +9 -0
- tigrbl/table/_base.py +260 -0
- tigrbl/table/_table.py +54 -0
- tigrbl/table/mro_collect.py +69 -0
- tigrbl/table/shortcuts.py +57 -0
- tigrbl/table/table_spec.py +28 -0
- tigrbl/transport/__init__.py +74 -0
- tigrbl/transport/jsonrpc/__init__.py +19 -0
- tigrbl/transport/jsonrpc/dispatcher.py +352 -0
- tigrbl/transport/jsonrpc/helpers.py +115 -0
- tigrbl/transport/jsonrpc/models.py +41 -0
- tigrbl/transport/rest/__init__.py +25 -0
- tigrbl/transport/rest/aggregator.py +132 -0
- tigrbl/types/__init__.py +170 -0
- tigrbl/types/allow_anon_provider.py +19 -0
- tigrbl/types/authn_abc.py +30 -0
- tigrbl/types/nested_path_provider.py +22 -0
- tigrbl/types/op.py +35 -0
- tigrbl/types/op_config_provider.py +17 -0
- tigrbl/types/op_verb_alias_provider.py +33 -0
- tigrbl/types/request_extras_provider.py +22 -0
- tigrbl/types/response_extras_provider.py +22 -0
- tigrbl/types/table_config_provider.py +13 -0
- tigrbl/types/uuid.py +55 -0
- tigrbl-0.3.0.dist-info/METADATA +516 -0
- tigrbl-0.3.0.dist-info/RECORD +266 -0
- {tigrbl-0.0.1.dev1.dist-info → tigrbl-0.3.0.dist-info}/WHEEL +1 -1
- tigrbl-0.3.0.dist-info/licenses/LICENSE +201 -0
- tigrbl/ExampleAgent.py +0 -1
- tigrbl-0.0.1.dev1.dist-info/METADATA +0 -18
- tigrbl-0.0.1.dev1.dist-info/RECORD +0 -5
tigrbl/runtime/labels.py
ADDED
|
@@ -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
|
+
]
|
tigrbl/runtime/opview.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Any, Mapping, Dict
|
|
3
|
+
from types import SimpleNamespace
|
|
4
|
+
|
|
5
|
+
from . import kernel as _kernel # 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 _kernel._default_kernel.get_opview(app, model, alias)
|
|
40
|
+
|
|
41
|
+
if alias:
|
|
42
|
+
specs = getattr(ctx, "specs", None)
|
|
43
|
+
if specs is not None:
|
|
44
|
+
return _kernel._default_kernel._compile_opview_from_specs(
|
|
45
|
+
specs, SimpleNamespace(alias=alias)
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
missing = []
|
|
49
|
+
if not alias:
|
|
50
|
+
missing.append("op")
|
|
51
|
+
if not app:
|
|
52
|
+
missing.append("app")
|
|
53
|
+
if not model:
|
|
54
|
+
missing.append("model")
|
|
55
|
+
# runtime-error policy: eject loudly; no skip
|
|
56
|
+
raise RuntimeError(f"ctx_missing:{','.join(missing)}")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def ensure_schema_in(ctx: Any, ov) -> Mapping[str, Any]:
|
|
60
|
+
"""
|
|
61
|
+
Load precompiled inbound schema from OpView into ctx.temp['schema_in'] if absent.
|
|
62
|
+
"""
|
|
63
|
+
temp = _ensure_temp(ctx)
|
|
64
|
+
if "schema_in" not in temp:
|
|
65
|
+
bf = ov.schema_in.by_field
|
|
66
|
+
req = tuple(n for n, e in bf.items() if e.get("required"))
|
|
67
|
+
temp["schema_in"] = {
|
|
68
|
+
"fields": ov.schema_in.fields,
|
|
69
|
+
"by_field": bf,
|
|
70
|
+
"required": req,
|
|
71
|
+
}
|
|
72
|
+
return temp["schema_in"]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def ensure_schema_out(ctx: Any, ov) -> Mapping[str, Any]:
|
|
76
|
+
"""
|
|
77
|
+
Load precompiled outbound schema from OpView into ctx.temp['schema_out'] if absent.
|
|
78
|
+
"""
|
|
79
|
+
temp = _ensure_temp(ctx)
|
|
80
|
+
if "schema_out" not in temp:
|
|
81
|
+
temp["schema_out"] = {
|
|
82
|
+
"fields": ov.schema_out.fields,
|
|
83
|
+
"by_field": ov.schema_out.by_field,
|
|
84
|
+
"expose": ov.schema_out.expose,
|
|
85
|
+
}
|
|
86
|
+
return temp["schema_out"]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
__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
|
+
]
|