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
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# tigrbl/tigrbl/v3/engine/resolver.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import inspect
|
|
6
|
+
import logging
|
|
7
|
+
import threading
|
|
8
|
+
from typing import Any, Callable, Optional
|
|
9
|
+
|
|
10
|
+
from ._engine import AsyncSession, Engine, Provider, Session
|
|
11
|
+
from .engine_spec import EngineSpec, EngineCfg
|
|
12
|
+
|
|
13
|
+
logging.getLogger("uvicorn").setLevel(logging.DEBUG)
|
|
14
|
+
logger = logging.getLogger("uvicorn")
|
|
15
|
+
|
|
16
|
+
# Registry with strict precedence: op > model > api > app
|
|
17
|
+
_LOCK = threading.RLock()
|
|
18
|
+
_DEFAULT: Optional[Provider] = None
|
|
19
|
+
_API: dict[int, Provider] = {}
|
|
20
|
+
_TAB: dict[Any, Provider] = {}
|
|
21
|
+
_OP: dict[tuple[Any, str], Provider] = {}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _with_class(obj: Any) -> list[Any]:
|
|
25
|
+
"""Return ``obj`` and its class when ``obj`` is an instance.
|
|
26
|
+
|
|
27
|
+
This allows resolution to honor providers registered on classes even when
|
|
28
|
+
an instance is supplied at lookup time.
|
|
29
|
+
"""
|
|
30
|
+
return [obj] if isinstance(obj, type) else [obj, type(obj)]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _coerce(ctx: Optional[EngineCfg]) -> Optional[Provider]:
|
|
34
|
+
"""
|
|
35
|
+
Promote an @engine_ctx value to a lazy Provider.
|
|
36
|
+
"""
|
|
37
|
+
logger.debug("_coerce called with ctx=%r", ctx)
|
|
38
|
+
if ctx is None:
|
|
39
|
+
logger.debug("_coerce: ctx is None")
|
|
40
|
+
return None
|
|
41
|
+
if isinstance(ctx, Provider):
|
|
42
|
+
logger.debug("_coerce: ctx is already a Provider")
|
|
43
|
+
return ctx
|
|
44
|
+
if isinstance(ctx, Engine):
|
|
45
|
+
logger.debug("_coerce: ctx is an Engine; returning provider")
|
|
46
|
+
return ctx.provider
|
|
47
|
+
if isinstance(ctx, EngineSpec):
|
|
48
|
+
logger.debug("_coerce: ctx is an EngineSpec; converting to provider")
|
|
49
|
+
return ctx.to_provider()
|
|
50
|
+
spec = EngineSpec.from_any(ctx)
|
|
51
|
+
logger.debug("_coerce: EngineSpec.from_any returned %r", spec)
|
|
52
|
+
return spec.to_provider() if spec else None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ---- registration -----------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def set_default(ctx: EngineCfg | None) -> None:
|
|
59
|
+
"""
|
|
60
|
+
Register the app-level default Provider used when no API/table/op binds.
|
|
61
|
+
"""
|
|
62
|
+
global _DEFAULT
|
|
63
|
+
prov = _coerce(ctx)
|
|
64
|
+
logger.debug("set_default: setting default provider to %r", prov)
|
|
65
|
+
with _LOCK:
|
|
66
|
+
_DEFAULT = prov
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def register_api(api: Any, ctx: EngineCfg | None) -> None:
|
|
70
|
+
"""
|
|
71
|
+
Register an API-level Provider.
|
|
72
|
+
"""
|
|
73
|
+
prov = _coerce(ctx)
|
|
74
|
+
logger.debug("register_api: api=%r coerced provider=%r", api, prov)
|
|
75
|
+
if prov is None:
|
|
76
|
+
logger.debug("register_api: no provider; skipping registration")
|
|
77
|
+
return
|
|
78
|
+
with _LOCK:
|
|
79
|
+
_API[id(api)] = prov
|
|
80
|
+
logger.debug("register_api: registered provider for api id %s", id(api))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def register_table(model: Any, ctx: EngineCfg | None) -> None:
|
|
84
|
+
"""
|
|
85
|
+
Register a table/model-level Provider.
|
|
86
|
+
"""
|
|
87
|
+
prov = _coerce(ctx)
|
|
88
|
+
logger.debug("register_table: model=%r coerced provider=%r", model, prov)
|
|
89
|
+
if prov is None:
|
|
90
|
+
logger.debug("register_table: no provider; skipping registration")
|
|
91
|
+
return
|
|
92
|
+
with _LOCK:
|
|
93
|
+
_TAB[model] = prov
|
|
94
|
+
logger.debug("register_table: registered provider for model %r", model)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def register_op(model: Any, alias: str, ctx: EngineCfg | None) -> None:
|
|
98
|
+
"""
|
|
99
|
+
Register an op-level Provider for (model, alias).
|
|
100
|
+
"""
|
|
101
|
+
prov = _coerce(ctx)
|
|
102
|
+
logger.debug(
|
|
103
|
+
"register_op: model=%r alias=%r coerced provider=%r", model, alias, prov
|
|
104
|
+
)
|
|
105
|
+
if prov is None:
|
|
106
|
+
logger.debug("register_op: no provider; skipping registration")
|
|
107
|
+
return
|
|
108
|
+
with _LOCK:
|
|
109
|
+
_OP[(model, alias)] = prov
|
|
110
|
+
logger.debug(
|
|
111
|
+
"register_op: registered provider for model %r alias %s", model, alias
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ---- resolution -------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def resolve_provider(
|
|
119
|
+
*,
|
|
120
|
+
api: Any = None,
|
|
121
|
+
model: Any = None,
|
|
122
|
+
op_alias: str | None = None,
|
|
123
|
+
) -> Optional[Provider]:
|
|
124
|
+
"""
|
|
125
|
+
Resolve the effective Provider using precedence:
|
|
126
|
+
op > model > api > app(default)
|
|
127
|
+
"""
|
|
128
|
+
logger.debug(
|
|
129
|
+
"resolve_provider called with api=%r model=%r op_alias=%r",
|
|
130
|
+
api,
|
|
131
|
+
model,
|
|
132
|
+
op_alias,
|
|
133
|
+
)
|
|
134
|
+
with _LOCK:
|
|
135
|
+
if model is not None and op_alias is not None:
|
|
136
|
+
logger.debug("resolve_provider: checking op-level provider")
|
|
137
|
+
for m in _with_class(model):
|
|
138
|
+
logger.debug(
|
|
139
|
+
"resolve_provider: looking for op provider for %r alias %s",
|
|
140
|
+
m,
|
|
141
|
+
op_alias,
|
|
142
|
+
)
|
|
143
|
+
p = _OP.get((m, op_alias))
|
|
144
|
+
if p:
|
|
145
|
+
logger.debug("resolve_provider: found op-level provider %r", p)
|
|
146
|
+
return p
|
|
147
|
+
if model is not None:
|
|
148
|
+
logger.debug("resolve_provider: checking model-level provider")
|
|
149
|
+
for m in _with_class(model):
|
|
150
|
+
logger.debug("resolve_provider: looking for model provider %r", m)
|
|
151
|
+
p = _TAB.get(m)
|
|
152
|
+
if p:
|
|
153
|
+
logger.debug("resolve_provider: found model-level provider %r", p)
|
|
154
|
+
return p
|
|
155
|
+
if api is not None:
|
|
156
|
+
logger.debug("resolve_provider: checking api-level provider")
|
|
157
|
+
for a in _with_class(api):
|
|
158
|
+
logger.debug("resolve_provider: looking for api provider %r", a)
|
|
159
|
+
# APIs are keyed by ``id`` to avoid relying on ``__hash__``
|
|
160
|
+
p = _API.get(id(a))
|
|
161
|
+
if p:
|
|
162
|
+
logger.debug("resolve_provider: found api-level provider %r", p)
|
|
163
|
+
return p
|
|
164
|
+
logger.debug("resolve_provider: returning default provider %r", _DEFAULT)
|
|
165
|
+
return _DEFAULT
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
SessionT = Session | AsyncSession
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def acquire(
|
|
172
|
+
*,
|
|
173
|
+
api: Any = None,
|
|
174
|
+
model: Any = None,
|
|
175
|
+
op_alias: str | None = None,
|
|
176
|
+
) -> tuple[SessionT, Callable[[], None]]:
|
|
177
|
+
"""
|
|
178
|
+
Acquire a DB session from the resolved Provider.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
(session, release_fn)
|
|
182
|
+
|
|
183
|
+
Raises:
|
|
184
|
+
RuntimeError: if no Provider can be resolved and no default is set.
|
|
185
|
+
"""
|
|
186
|
+
logger.debug(
|
|
187
|
+
"acquire called with api=%r model=%r op_alias=%r", api, model, op_alias
|
|
188
|
+
)
|
|
189
|
+
p = resolve_provider(api=api, model=model, op_alias=op_alias)
|
|
190
|
+
if p is None:
|
|
191
|
+
logger.debug("acquire: no provider resolved; raising error")
|
|
192
|
+
raise RuntimeError(
|
|
193
|
+
f"No database provider configured for op={op_alias} "
|
|
194
|
+
f"model={getattr(model, '__name__', model)} "
|
|
195
|
+
f"api={type(api).__name__ if api else None} and no default"
|
|
196
|
+
)
|
|
197
|
+
db: SessionT = p.session()
|
|
198
|
+
logger.debug("acquire: session %r acquired from provider %r", db, p)
|
|
199
|
+
|
|
200
|
+
def _release() -> None:
|
|
201
|
+
logger.debug("_release: attempting to release session %r", db)
|
|
202
|
+
close = getattr(db, "close", None)
|
|
203
|
+
if callable(close):
|
|
204
|
+
try:
|
|
205
|
+
rv = close()
|
|
206
|
+
logger.debug("_release: close returned %r", rv)
|
|
207
|
+
if inspect.isawaitable(rv):
|
|
208
|
+
logger.debug("_release: awaiting asynchronous close")
|
|
209
|
+
try:
|
|
210
|
+
loop = asyncio.get_running_loop()
|
|
211
|
+
except RuntimeError:
|
|
212
|
+
logger.debug("_release: no running loop; using asyncio.run")
|
|
213
|
+
asyncio.run(rv)
|
|
214
|
+
else:
|
|
215
|
+
logger.debug("_release: scheduling close on running loop")
|
|
216
|
+
loop.create_task(rv)
|
|
217
|
+
# If close is sync, it has already executed
|
|
218
|
+
except Exception:
|
|
219
|
+
logger.debug("_release: error during close", exc_info=True)
|
|
220
|
+
# best-effort close; swallow to avoid masking handler errors
|
|
221
|
+
pass
|
|
222
|
+
logger.debug("_release: release complete for session %r", db)
|
|
223
|
+
|
|
224
|
+
return db, _release
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# tigrbl/tigrbl/v3/engine/shortcuts.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, Mapping, Optional, Union
|
|
5
|
+
|
|
6
|
+
from .engine_spec import EngineSpec
|
|
7
|
+
from ._engine import Provider, Engine
|
|
8
|
+
|
|
9
|
+
EngineCfg = Union[str, Mapping[str, object]] # DSN string or structured mapping
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# ---------------------------------------------------------------------------
|
|
13
|
+
# EngineSpec / Provider / Engine helpers (ctx builder collapsed into
|
|
14
|
+
# engine_spec)
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def engine_spec(
|
|
19
|
+
spec: Union[EngineCfg, Mapping[str, Any], str, None] = None, **kw: Any
|
|
20
|
+
) -> EngineSpec:
|
|
21
|
+
"""Build an :class:`EngineSpec` from a DSN string, mapping, or keyword fields."""
|
|
22
|
+
if spec is None and kw:
|
|
23
|
+
# Inline the former ctx builder behavior (no double wrap)
|
|
24
|
+
dsn: Optional[str] = kw.get("dsn")
|
|
25
|
+
if dsn:
|
|
26
|
+
spec = dsn
|
|
27
|
+
else:
|
|
28
|
+
kind = kw.get("kind")
|
|
29
|
+
if not kind:
|
|
30
|
+
raise ValueError(
|
|
31
|
+
"Provide spec=<DSN|mapping> or kind=('sqlite'|'postgres') with appropriate fields"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
async_kw = kw.get("async_")
|
|
35
|
+
if async_kw is None:
|
|
36
|
+
async_kw = kw.get("async")
|
|
37
|
+
|
|
38
|
+
if kind == "sqlite":
|
|
39
|
+
path = kw.get("path")
|
|
40
|
+
mode = kw.get("mode")
|
|
41
|
+
memory_flag = kw.get("memory")
|
|
42
|
+
memory = (
|
|
43
|
+
(mode == "memory")
|
|
44
|
+
or (memory_flag is True)
|
|
45
|
+
or (not path and mode != "file")
|
|
46
|
+
)
|
|
47
|
+
async_default = True if async_kw is None and memory else False
|
|
48
|
+
async_ = bool(async_kw) if async_kw is not None else async_default
|
|
49
|
+
if memory:
|
|
50
|
+
spec = {"kind": "sqlite", "async": async_, "mode": "memory"}
|
|
51
|
+
else:
|
|
52
|
+
if not path:
|
|
53
|
+
raise ValueError("sqlite file requires 'path'")
|
|
54
|
+
spec = {"kind": "sqlite", "async": async_, "path": path}
|
|
55
|
+
|
|
56
|
+
elif kind == "postgres":
|
|
57
|
+
async_ = bool(async_kw) if async_kw is not None else False
|
|
58
|
+
spec = {
|
|
59
|
+
"kind": "postgres",
|
|
60
|
+
"async": async_,
|
|
61
|
+
"user": kw.get("user", "app"),
|
|
62
|
+
"pwd": kw.get("pwd", "secret"),
|
|
63
|
+
"host": kw.get("host", "localhost"),
|
|
64
|
+
"port": kw.get("port", 5432),
|
|
65
|
+
"db": kw.get("name", kw.get("db", "app_db")),
|
|
66
|
+
"pool_size": kw.get("pool_size", 10),
|
|
67
|
+
"max": kw.get("max", 20),
|
|
68
|
+
}
|
|
69
|
+
else:
|
|
70
|
+
raise ValueError("kind must be 'sqlite' or 'postgres'")
|
|
71
|
+
|
|
72
|
+
return EngineSpec.from_any(spec)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def prov(
|
|
76
|
+
spec: Union[EngineSpec, EngineCfg, Mapping[str, Any], str, None] = None, **kw: Any
|
|
77
|
+
) -> Provider:
|
|
78
|
+
"""
|
|
79
|
+
Get a lazy Provider (engine+sessionmaker).
|
|
80
|
+
Accepts EngineSpec, EngineCfg (mapping/DSN), or kw fields (collapsed former ctxS).
|
|
81
|
+
"""
|
|
82
|
+
if isinstance(spec, EngineSpec):
|
|
83
|
+
return spec.to_provider()
|
|
84
|
+
return engine_spec(spec, **kw).to_provider()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def engine(
|
|
88
|
+
spec: Union[EngineSpec, EngineCfg, Mapping[str, Any], str, None] = None, **kw: Any
|
|
89
|
+
) -> Engine:
|
|
90
|
+
"""Return an Engine façade for convenience in ad-hoc flows."""
|
|
91
|
+
if isinstance(spec, EngineSpec):
|
|
92
|
+
return Engine(spec)
|
|
93
|
+
return Engine(engine_spec(spec, **kw))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
# Convenience helpers (construct EngineCfg mappings directly; no ctxS needed)
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def sqlite_cfg(
|
|
102
|
+
path: Optional[str] = None, *, async_: bool = True, memory: Optional[bool] = None
|
|
103
|
+
) -> EngineCfg:
|
|
104
|
+
return (
|
|
105
|
+
{"kind": "sqlite", "async": async_, "mode": "memory"}
|
|
106
|
+
if (memory or path is None)
|
|
107
|
+
else {"kind": "sqlite", "async": async_, "path": path}
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def pg_cfg(
|
|
112
|
+
*,
|
|
113
|
+
async_: bool = False,
|
|
114
|
+
user: str = "app",
|
|
115
|
+
pwd: str = "secret",
|
|
116
|
+
host: str = "localhost",
|
|
117
|
+
port: int = 5432,
|
|
118
|
+
name: str = "app_db",
|
|
119
|
+
pool_size: int = 10,
|
|
120
|
+
max: int = 20,
|
|
121
|
+
) -> EngineCfg:
|
|
122
|
+
return {
|
|
123
|
+
"kind": "postgres",
|
|
124
|
+
"async": async_,
|
|
125
|
+
"user": user,
|
|
126
|
+
"pwd": pwd,
|
|
127
|
+
"host": host,
|
|
128
|
+
"port": port,
|
|
129
|
+
"db": name,
|
|
130
|
+
"pool_size": pool_size,
|
|
131
|
+
"max": max,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def mem(async_: bool = True) -> EngineCfg:
|
|
136
|
+
"""SQLite in-memory (StaticPool) EngineCfg mapping."""
|
|
137
|
+
return {"kind": "sqlite", "async": async_, "mode": "memory"}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def sqlitef(path: str, *, async_: bool = False) -> EngineCfg:
|
|
141
|
+
"""SQLite file EngineCfg mapping."""
|
|
142
|
+
return {"kind": "sqlite", "async": async_, "path": path}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def pg(**kw: Any) -> EngineCfg:
|
|
146
|
+
"""Postgres EngineCfg; set async_=True for asyncpg."""
|
|
147
|
+
return pg_cfg(**kw)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def pga(**kw: Any) -> EngineCfg:
|
|
151
|
+
"""Async Postgres EngineCfg (asyncpg)."""
|
|
152
|
+
kw.setdefault("async_", True)
|
|
153
|
+
return pg_cfg(**kw)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def pgs(**kw: Any) -> EngineCfg:
|
|
157
|
+
"""Sync Postgres EngineCfg (psycopg/pg8000 depending on your builders)."""
|
|
158
|
+
kw.setdefault("async_", False)
|
|
159
|
+
return pg_cfg(**kw)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# ---------------------------------------------------------------------------
|
|
163
|
+
# Provider one-liners
|
|
164
|
+
# ---------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def provider_sqlite_memory(async_: bool = True) -> Provider:
|
|
168
|
+
return engine_spec(kind="sqlite", mode="memory", async_=async_).to_provider()
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def provider_sqlite_file(path: str, async_: bool = False) -> Provider:
|
|
172
|
+
return engine_spec(kind="sqlite", path=path, async_=async_).to_provider()
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def provider_postgres(
|
|
176
|
+
*,
|
|
177
|
+
async_: bool = False,
|
|
178
|
+
user: str = "app",
|
|
179
|
+
pwd: str = "secret",
|
|
180
|
+
host: str = "localhost",
|
|
181
|
+
port: int = 5432,
|
|
182
|
+
name: str = "app_db",
|
|
183
|
+
pool_size: int = 10,
|
|
184
|
+
max: int = 20,
|
|
185
|
+
) -> Provider:
|
|
186
|
+
return engine_spec(
|
|
187
|
+
kind="postgres",
|
|
188
|
+
async_=async_,
|
|
189
|
+
user=user,
|
|
190
|
+
pwd=pwd,
|
|
191
|
+
host=host,
|
|
192
|
+
port=port,
|
|
193
|
+
name=name,
|
|
194
|
+
pool_size=pool_size,
|
|
195
|
+
max=max,
|
|
196
|
+
).to_provider()
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
__all__ = [
|
|
200
|
+
# EngineSpec / Provider / Engine helpers
|
|
201
|
+
"engine_spec",
|
|
202
|
+
"prov",
|
|
203
|
+
"engine",
|
|
204
|
+
# convenience EngineCfg helpers
|
|
205
|
+
"sqlite_cfg",
|
|
206
|
+
"pg_cfg",
|
|
207
|
+
"mem",
|
|
208
|
+
"sqlitef",
|
|
209
|
+
"pg",
|
|
210
|
+
"pga",
|
|
211
|
+
"pgs",
|
|
212
|
+
# direct providers
|
|
213
|
+
"provider_sqlite_memory",
|
|
214
|
+
"provider_sqlite_file",
|
|
215
|
+
"provider_postgres",
|
|
216
|
+
]
|
tigrbl/hook/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from ..config.constants import HOOK_DECLS_ATTR
|
|
2
|
+
from .decorators import hook_ctx
|
|
3
|
+
from .types import PHASE, HookPhase, PHASES, Ctx, StepFn, HookPredicate
|
|
4
|
+
from .shortcuts import hook, hook_spec
|
|
5
|
+
from ._hook import Hook
|
|
6
|
+
from .hook_spec import HookSpec
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"hook_ctx",
|
|
10
|
+
"HOOK_DECLS_ATTR",
|
|
11
|
+
"Hook",
|
|
12
|
+
"PHASE",
|
|
13
|
+
"HookPhase",
|
|
14
|
+
"PHASES",
|
|
15
|
+
"Ctx",
|
|
16
|
+
"StepFn",
|
|
17
|
+
"HookPredicate",
|
|
18
|
+
"hook",
|
|
19
|
+
"hook_spec",
|
|
20
|
+
"HookSpec",
|
|
21
|
+
]
|
tigrbl/hook/_hook.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Runtime hook wrapper for Tigrbl v3."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Iterable, Optional, Union
|
|
7
|
+
|
|
8
|
+
from .types import HookPhase, StepFn
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True, slots=True)
|
|
12
|
+
class Hook:
|
|
13
|
+
"""Concrete hook bound to a phase and one or more ops."""
|
|
14
|
+
|
|
15
|
+
phase: HookPhase
|
|
16
|
+
fn: StepFn
|
|
17
|
+
ops: Union[str, Iterable[str]]
|
|
18
|
+
name: Optional[str] = None
|
|
19
|
+
description: Optional[str] = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
__all__ = ["Hook"]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Hook-related decorators for Tigrbl v3."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Iterable, Union
|
|
6
|
+
|
|
7
|
+
from ..config.constants import HOOK_DECLS_ATTR
|
|
8
|
+
from ._hook import Hook
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def hook_ctx(ops: Union[str, Iterable[str]], *, phase: str):
|
|
12
|
+
"""Declare a ctx-only hook for one/many ops at a given phase."""
|
|
13
|
+
|
|
14
|
+
def deco(fn):
|
|
15
|
+
from ..op.decorators import _ensure_cm, _unwrap
|
|
16
|
+
|
|
17
|
+
cm = _ensure_cm(fn)
|
|
18
|
+
f = _unwrap(cm)
|
|
19
|
+
f.__tigrbl_ctx_only__ = True
|
|
20
|
+
lst = getattr(f, HOOK_DECLS_ATTR, [])
|
|
21
|
+
lst.append(Hook(phase=phase, fn=f, ops=ops))
|
|
22
|
+
setattr(f, HOOK_DECLS_ATTR, lst)
|
|
23
|
+
return cm
|
|
24
|
+
|
|
25
|
+
return deco
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
__all__ = ["hook_ctx", "HOOK_DECLS_ATTR"]
|
tigrbl/hook/hook_spec.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Hook specification for Tigrbl v3."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from .types import HookPhase, StepFn, HookPredicate
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True, slots=True)
|
|
12
|
+
class HookSpec:
|
|
13
|
+
phase: HookPhase
|
|
14
|
+
fn: StepFn
|
|
15
|
+
order: int = 0
|
|
16
|
+
when: Optional[HookPredicate] = None
|
|
17
|
+
name: Optional[str] = None
|
|
18
|
+
description: Optional[str] = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Backwards compatibility alias
|
|
22
|
+
OpHook = HookSpec
|
|
23
|
+
|
|
24
|
+
__all__ = ["HookSpec", "OpHook"]
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Helpers for collecting ctx-only hooks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from functools import lru_cache
|
|
7
|
+
from typing import Any, Callable, Dict, Iterable, Union
|
|
8
|
+
|
|
9
|
+
from ..runtime.executor import _Ctx
|
|
10
|
+
from ..op.collect import apply_alias
|
|
11
|
+
from ..op.mro_collect import mro_alias_map_for
|
|
12
|
+
from ..op.decorators import _maybe_await, _unwrap
|
|
13
|
+
from .decorators import HOOK_DECLS_ATTR, Hook
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger("uvicorn")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _phase_io_key(phase: str) -> str | None:
|
|
19
|
+
p = str(phase)
|
|
20
|
+
if p.startswith("PRE_"):
|
|
21
|
+
return "payload"
|
|
22
|
+
if p.startswith("POST_"):
|
|
23
|
+
return "result"
|
|
24
|
+
if p.startswith("ON_"):
|
|
25
|
+
return "error"
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _wrap_ctx_hook(
|
|
30
|
+
table: type, func: Callable[..., Any], phase: str
|
|
31
|
+
) -> Callable[..., Any]:
|
|
32
|
+
io_key = _phase_io_key(phase)
|
|
33
|
+
|
|
34
|
+
async def hook(
|
|
35
|
+
value=None, *, db=None, request=None, ctx: Dict[str, Any] | None = None
|
|
36
|
+
):
|
|
37
|
+
ctx = _Ctx.ensure(request=request, db=db, seed=ctx)
|
|
38
|
+
if io_key is not None and value is not None:
|
|
39
|
+
ctx[io_key] = value
|
|
40
|
+
bound = func.__get__(table, table)
|
|
41
|
+
_ = await _maybe_await(bound(ctx))
|
|
42
|
+
if io_key is None:
|
|
43
|
+
return None
|
|
44
|
+
return ctx.get(io_key, value)
|
|
45
|
+
|
|
46
|
+
hook.__name__ = getattr(func, "__name__", "hook")
|
|
47
|
+
hook.__qualname__ = getattr(func, "__qualname__", hook.__name__)
|
|
48
|
+
return hook
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@lru_cache(maxsize=None)
|
|
52
|
+
def _mro_collect_decorated_hooks_cached(
|
|
53
|
+
table: type, visible_aliases_fs: frozenset[str]
|
|
54
|
+
) -> Dict[str, Dict[str, list[Callable[..., Any]]]]:
|
|
55
|
+
"""Cached helper for :func:`mro_collect_decorated_hooks`."""
|
|
56
|
+
|
|
57
|
+
visible_aliases = set(visible_aliases_fs)
|
|
58
|
+
logger.info("Collecting hooks for %s", table.__name__)
|
|
59
|
+
mapping: Dict[str, Dict[str, list[Callable[..., Any]]]] = {}
|
|
60
|
+
aliases = mro_alias_map_for(table)
|
|
61
|
+
|
|
62
|
+
def _resolve_ops(spec: Union[str, Iterable[str]]) -> Iterable[str]:
|
|
63
|
+
if spec == "*":
|
|
64
|
+
return visible_aliases
|
|
65
|
+
if isinstance(spec, str):
|
|
66
|
+
return [spec if spec in visible_aliases else apply_alias(spec, aliases)]
|
|
67
|
+
out: list[str] = []
|
|
68
|
+
for x in spec:
|
|
69
|
+
out.append(x if x in visible_aliases else apply_alias(x, aliases))
|
|
70
|
+
return out
|
|
71
|
+
|
|
72
|
+
for base in reversed(table.__mro__):
|
|
73
|
+
for name, attr in base.__dict__.items():
|
|
74
|
+
func = _unwrap(attr)
|
|
75
|
+
decls: list[Hook] | None = getattr(func, HOOK_DECLS_ATTR, None)
|
|
76
|
+
if not decls:
|
|
77
|
+
continue
|
|
78
|
+
for d in decls:
|
|
79
|
+
for op in _resolve_ops(d.ops):
|
|
80
|
+
if op not in visible_aliases:
|
|
81
|
+
continue
|
|
82
|
+
ph = d.phase
|
|
83
|
+
mapping.setdefault(op, {}).setdefault(ph, []).append(
|
|
84
|
+
_wrap_ctx_hook(table, d.fn, ph)
|
|
85
|
+
)
|
|
86
|
+
logger.debug("Collected hooks for aliases: %s", list(mapping.keys()))
|
|
87
|
+
return mapping
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def mro_collect_decorated_hooks(
|
|
91
|
+
table: type, *, visible_aliases: set[str]
|
|
92
|
+
) -> Dict[str, Dict[str, list[Callable[..., Any]]]]:
|
|
93
|
+
"""Collect alias→phase→[hook] declarations across a table's MRO."""
|
|
94
|
+
|
|
95
|
+
return _mro_collect_decorated_hooks_cached(table, frozenset(visible_aliases))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
__all__ = ["mro_collect_decorated_hooks"]
|
tigrbl/hook/shortcuts.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Shortcut helpers for building Hook specs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Iterable, Union
|
|
6
|
+
|
|
7
|
+
from .types import HookPhase, HookPredicate, StepFn
|
|
8
|
+
from ._hook import Hook
|
|
9
|
+
from .hook_spec import HookSpec
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def hook(
|
|
13
|
+
phase: HookPhase,
|
|
14
|
+
ops: Union[str, Iterable[str]],
|
|
15
|
+
fn: StepFn,
|
|
16
|
+
*,
|
|
17
|
+
name: str | None = None,
|
|
18
|
+
description: str | None = None,
|
|
19
|
+
) -> Hook:
|
|
20
|
+
"""Build a :class:`Hook` instance."""
|
|
21
|
+
return Hook(phase=phase, fn=fn, ops=ops, name=name, description=description)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def hook_spec(
|
|
25
|
+
phase: HookPhase,
|
|
26
|
+
fn: StepFn,
|
|
27
|
+
*,
|
|
28
|
+
order: int = 0,
|
|
29
|
+
when: HookPredicate | None = None,
|
|
30
|
+
name: str | None = None,
|
|
31
|
+
description: str | None = None,
|
|
32
|
+
) -> HookSpec:
|
|
33
|
+
"""Build a :class:`HookSpec` instance."""
|
|
34
|
+
return HookSpec(
|
|
35
|
+
phase=phase,
|
|
36
|
+
fn=fn,
|
|
37
|
+
order=order,
|
|
38
|
+
when=when,
|
|
39
|
+
name=name,
|
|
40
|
+
description=description,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
__all__ = ["hook", "hook_spec"]
|