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,73 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, Optional
|
|
4
|
+
|
|
5
|
+
from .compat import Router
|
|
6
|
+
from .healthz import build_healthz_endpoint
|
|
7
|
+
from .methodz import build_methodz_endpoint
|
|
8
|
+
from .hookz import build_hookz_endpoint
|
|
9
|
+
from .kernelz import build_kernelz_endpoint
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def mount_diagnostics(
|
|
13
|
+
api: Any,
|
|
14
|
+
*,
|
|
15
|
+
get_db: Optional[Callable[..., Any]] = None,
|
|
16
|
+
) -> Router:
|
|
17
|
+
"""
|
|
18
|
+
Create & return a Router that exposes:
|
|
19
|
+
GET /healthz
|
|
20
|
+
GET /methodz
|
|
21
|
+
GET /hookz
|
|
22
|
+
GET /kernelz
|
|
23
|
+
"""
|
|
24
|
+
router = Router()
|
|
25
|
+
|
|
26
|
+
dep = get_db
|
|
27
|
+
|
|
28
|
+
router.add_api_route(
|
|
29
|
+
"/healthz",
|
|
30
|
+
build_healthz_endpoint(dep),
|
|
31
|
+
methods=["GET"],
|
|
32
|
+
name="healthz",
|
|
33
|
+
tags=["system"],
|
|
34
|
+
summary="Health",
|
|
35
|
+
description="Database connectivity check.",
|
|
36
|
+
)
|
|
37
|
+
router.add_api_route(
|
|
38
|
+
"/methodz",
|
|
39
|
+
build_methodz_endpoint(api),
|
|
40
|
+
methods=["GET"],
|
|
41
|
+
name="methodz",
|
|
42
|
+
tags=["system"],
|
|
43
|
+
summary="Methods",
|
|
44
|
+
description="Ordered, canonical operation list.",
|
|
45
|
+
)
|
|
46
|
+
router.add_api_route(
|
|
47
|
+
"/hookz",
|
|
48
|
+
build_hookz_endpoint(api),
|
|
49
|
+
methods=["GET"],
|
|
50
|
+
name="hookz",
|
|
51
|
+
tags=["system"],
|
|
52
|
+
summary="Hooks",
|
|
53
|
+
description=(
|
|
54
|
+
"Expose hook execution order for each method.\n\n"
|
|
55
|
+
"Phases appear in runner order; error phases trail.\n"
|
|
56
|
+
"Within each phase, hooks are listed in execution order: "
|
|
57
|
+
"global (None) hooks, then method-specific hooks."
|
|
58
|
+
),
|
|
59
|
+
)
|
|
60
|
+
router.add_api_route(
|
|
61
|
+
"/kernelz",
|
|
62
|
+
build_kernelz_endpoint(api),
|
|
63
|
+
methods=["GET"],
|
|
64
|
+
name="kernelz",
|
|
65
|
+
tags=["system"],
|
|
66
|
+
summary="Kernel Plan",
|
|
67
|
+
description="Phase-chain plan as built by the kernel per operation.",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
return router
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
__all__ = ["mount_diagnostics"]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from types import SimpleNamespace
|
|
5
|
+
from typing import Any, Iterable
|
|
6
|
+
|
|
7
|
+
from sqlalchemy import text
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def model_iter(api: Any) -> Iterable[type]:
|
|
11
|
+
models = getattr(api, "models", {}) or {}
|
|
12
|
+
return models.values()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def opspecs(model: type):
|
|
16
|
+
return getattr(getattr(model, "opspecs", SimpleNamespace()), "all", ()) or ()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def label_callable(fn: Any) -> str:
|
|
20
|
+
n = getattr(fn, "__qualname__", getattr(fn, "__name__", repr(fn)))
|
|
21
|
+
m = getattr(fn, "__module__", None)
|
|
22
|
+
return f"{m}.{n}" if m else n
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def label_hook(fn: Any, phase: str) -> str:
|
|
26
|
+
label = getattr(fn, "__tigrbl_label", None)
|
|
27
|
+
if isinstance(label, str):
|
|
28
|
+
return label
|
|
29
|
+
subj = label_callable(fn).replace(".", ":")
|
|
30
|
+
return f"hook:wire:{subj}@{phase}"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def maybe_execute(db: Any, stmt: str):
|
|
34
|
+
try:
|
|
35
|
+
rv = db.execute(text(stmt)) # type: ignore[attr-defined]
|
|
36
|
+
if inspect.isawaitable(rv):
|
|
37
|
+
return await rv
|
|
38
|
+
return rv
|
|
39
|
+
except Exception:
|
|
40
|
+
rv = db.execute(text("select 1")) # type: ignore[attr-defined]
|
|
41
|
+
if inspect.isawaitable(rv):
|
|
42
|
+
return await rv
|
|
43
|
+
return rv
|
tigrbl/system/uvicorn.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Utilities for running uvicorn during tests or tooling."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
|
|
7
|
+
import uvicorn
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def _cancel_task(task: asyncio.Task) -> None:
|
|
11
|
+
if task.done():
|
|
12
|
+
return
|
|
13
|
+
task.cancel()
|
|
14
|
+
try:
|
|
15
|
+
await task
|
|
16
|
+
except asyncio.CancelledError:
|
|
17
|
+
return
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def _close_servers(server: uvicorn.Server) -> None:
|
|
21
|
+
servers = []
|
|
22
|
+
primary = getattr(server, "server", None)
|
|
23
|
+
if primary is not None:
|
|
24
|
+
servers.append(primary)
|
|
25
|
+
extra = getattr(server, "servers", None)
|
|
26
|
+
if extra:
|
|
27
|
+
servers.extend(extra)
|
|
28
|
+
for srv in servers:
|
|
29
|
+
close = getattr(srv, "close", None)
|
|
30
|
+
if callable(close):
|
|
31
|
+
close()
|
|
32
|
+
wait_closed = getattr(srv, "wait_closed", None)
|
|
33
|
+
if callable(wait_closed):
|
|
34
|
+
await wait_closed()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
async def stop_uvicorn_server(
|
|
38
|
+
server: uvicorn.Server,
|
|
39
|
+
task: asyncio.Task,
|
|
40
|
+
*,
|
|
41
|
+
timeout: float = 5.0,
|
|
42
|
+
) -> None:
|
|
43
|
+
"""Request uvicorn shutdown and ensure the task exits."""
|
|
44
|
+
if task.done():
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
server.should_exit = True
|
|
48
|
+
try:
|
|
49
|
+
await asyncio.wait_for(task, timeout=timeout)
|
|
50
|
+
return
|
|
51
|
+
except asyncio.TimeoutError:
|
|
52
|
+
server.force_exit = True
|
|
53
|
+
shutdown = getattr(server, "shutdown", None)
|
|
54
|
+
if callable(shutdown):
|
|
55
|
+
try:
|
|
56
|
+
await asyncio.wait_for(shutdown(), timeout=timeout)
|
|
57
|
+
except asyncio.TimeoutError:
|
|
58
|
+
pass
|
|
59
|
+
await _close_servers(server)
|
|
60
|
+
await _cancel_task(task)
|
tigrbl/table/__init__.py
ADDED
tigrbl/table/_base.py
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# tigrbl/tigrbl/v3/table/_base.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, Optional, Union, get_args, get_origin
|
|
5
|
+
from enum import Enum as PyEnum
|
|
6
|
+
|
|
7
|
+
from sqlalchemy.orm import DeclarativeBase, declared_attr, mapped_column
|
|
8
|
+
from sqlalchemy import CheckConstraint, ForeignKey, MetaData
|
|
9
|
+
from sqlalchemy.types import Enum as SAEnum, String
|
|
10
|
+
|
|
11
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
12
|
+
# Helpers – type inference & SA type instantiation
|
|
13
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _unwrap_optional(t: Any) -> Any:
|
|
17
|
+
"""Optional[T] / Union[T, None] → T"""
|
|
18
|
+
if get_origin(t) is Union:
|
|
19
|
+
args = [a for a in get_args(t) if a is not type(None)]
|
|
20
|
+
return args[0] if args else t
|
|
21
|
+
return t
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _infer_py_type(cls, name: str, spec: Any) -> Optional[type]:
|
|
25
|
+
"""
|
|
26
|
+
Prefer FieldSpec.py_type if provided; otherwise unwrap Mapped[...] / Optional[...]
|
|
27
|
+
from the class' annotation to get the real Python type for the column.
|
|
28
|
+
"""
|
|
29
|
+
fld = getattr(spec, "field", None)
|
|
30
|
+
py = getattr(fld, "py_type", None)
|
|
31
|
+
if isinstance(py, type):
|
|
32
|
+
return py
|
|
33
|
+
|
|
34
|
+
ann = getattr(cls, "__annotations__", {}).get(name)
|
|
35
|
+
if ann is None:
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
# Mapped[T] → T (then unwrap Optional)
|
|
39
|
+
try:
|
|
40
|
+
from ..types import Mapped
|
|
41
|
+
|
|
42
|
+
if get_origin(ann) is Mapped:
|
|
43
|
+
inner = get_args(ann)[0]
|
|
44
|
+
return _unwrap_optional(inner)
|
|
45
|
+
except Exception:
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
# Optional[T]/Union[T, None] → T
|
|
49
|
+
return _unwrap_optional(ann)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _instantiate_dtype(
|
|
53
|
+
dtype: Any, py_type: Any, spec: Any, cls_name: str, col_name: str
|
|
54
|
+
):
|
|
55
|
+
"""
|
|
56
|
+
Create a SQLAlchemy TypeEngine instance from either a type CLASS or an instance.
|
|
57
|
+
- SAEnum: instantiate from the actual Enum class with a stable name
|
|
58
|
+
- String: honor FieldSpec.constraints['max_length'] if present
|
|
59
|
+
- UUID (PG): prefer as_uuid=True when available
|
|
60
|
+
"""
|
|
61
|
+
# Already an instance? keep it.
|
|
62
|
+
try:
|
|
63
|
+
from sqlalchemy.sql.type_api import TypeEngine
|
|
64
|
+
|
|
65
|
+
if isinstance(dtype, TypeEngine):
|
|
66
|
+
return dtype
|
|
67
|
+
except Exception:
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
# SAEnum from a Python Enum class
|
|
71
|
+
if dtype is SAEnum and isinstance(py_type, type) and issubclass(py_type, PyEnum):
|
|
72
|
+
enum_name = f"{cls_name.lower()}_{col_name.lower()}"
|
|
73
|
+
return SAEnum(py_type, name=enum_name, native_enum=True, validate_strings=True)
|
|
74
|
+
|
|
75
|
+
# String – pick up max_length from FieldSpec
|
|
76
|
+
if dtype is String:
|
|
77
|
+
max_len = getattr(getattr(spec, "field", None), "constraints", {}).get(
|
|
78
|
+
"max_length"
|
|
79
|
+
)
|
|
80
|
+
return String(max_len) if max_len else String()
|
|
81
|
+
|
|
82
|
+
# PostgreSQL UUID (or similar) – try as_uuid=True first
|
|
83
|
+
try:
|
|
84
|
+
return dtype(as_uuid=True) # e.g., PG UUID
|
|
85
|
+
except TypeError:
|
|
86
|
+
try:
|
|
87
|
+
return dtype()
|
|
88
|
+
except TypeError:
|
|
89
|
+
# As a last resort, return the class; SQLA will raise clearly if unusable
|
|
90
|
+
return dtype
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _materialize_colspecs_to_sqla(cls) -> None:
|
|
94
|
+
"""
|
|
95
|
+
Replace ColumnSpec attributes with sqlalchemy.orm.mapped_column(...) BEFORE mapping.
|
|
96
|
+
Keep the original specs in __tigrbl_cols__ for downstream builders.
|
|
97
|
+
"""
|
|
98
|
+
try:
|
|
99
|
+
from tigrbl.column.column_spec import ColumnSpec
|
|
100
|
+
except Exception:
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
# Prefer explicit registry if present; otherwise collect specs from the
|
|
104
|
+
# entire MRO so mixins contribute their ColumnSpec definitions.
|
|
105
|
+
specs: dict[str, ColumnSpec] = {}
|
|
106
|
+
for base in reversed(cls.__mro__):
|
|
107
|
+
base_specs = getattr(base, "__tigrbl_cols__", None)
|
|
108
|
+
if isinstance(base_specs, dict) and base_specs:
|
|
109
|
+
specs.update(base_specs)
|
|
110
|
+
continue
|
|
111
|
+
for name, attr in getattr(base, "__dict__", {}).items():
|
|
112
|
+
if isinstance(attr, ColumnSpec):
|
|
113
|
+
specs.setdefault(name, attr)
|
|
114
|
+
|
|
115
|
+
if not specs:
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
# Ensure downstream code can find the spec map
|
|
119
|
+
setattr(cls, "__tigrbl_cols__", dict(specs))
|
|
120
|
+
|
|
121
|
+
for name, spec in specs.items():
|
|
122
|
+
storage = getattr(spec, "storage", None)
|
|
123
|
+
if not storage:
|
|
124
|
+
# Virtual (wire-only) column – no DB column
|
|
125
|
+
continue
|
|
126
|
+
|
|
127
|
+
dtype = getattr(storage, "type_", None)
|
|
128
|
+
if not dtype:
|
|
129
|
+
# No SA dtype specified – cannot materialize
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
py_type = _infer_py_type(cls, name, spec)
|
|
133
|
+
dtype_inst = _instantiate_dtype(dtype, py_type, spec, cls.__name__, name)
|
|
134
|
+
|
|
135
|
+
# Foreign key (if any)
|
|
136
|
+
fk = getattr(storage, "fk", None)
|
|
137
|
+
fk_arg = None
|
|
138
|
+
if fk is not None:
|
|
139
|
+
# ForeignKeySpec: target="table(col)", on_delete/on_update: "CASCADE"/...
|
|
140
|
+
fk_arg = ForeignKey(fk.target, ondelete=fk.on_delete, onupdate=fk.on_update)
|
|
141
|
+
|
|
142
|
+
check = getattr(storage, "check", None)
|
|
143
|
+
args: list[Any] = []
|
|
144
|
+
if fk_arg is not None:
|
|
145
|
+
args.append(fk_arg)
|
|
146
|
+
if check is not None:
|
|
147
|
+
cname = f"ck_{cls.__name__.lower()}_{name}"
|
|
148
|
+
args.append(CheckConstraint(check, name=cname))
|
|
149
|
+
|
|
150
|
+
# Build mapped_column from StorageSpec flags
|
|
151
|
+
mc = mapped_column(
|
|
152
|
+
dtype_inst,
|
|
153
|
+
*args,
|
|
154
|
+
primary_key=getattr(storage, "primary_key", False),
|
|
155
|
+
nullable=getattr(storage, "nullable", True),
|
|
156
|
+
unique=getattr(storage, "unique", False),
|
|
157
|
+
index=getattr(storage, "index", False),
|
|
158
|
+
default=getattr(storage, "default", None),
|
|
159
|
+
onupdate=getattr(storage, "onupdate", None),
|
|
160
|
+
server_default=getattr(storage, "server_default", None),
|
|
161
|
+
comment=getattr(storage, "comment", None),
|
|
162
|
+
autoincrement=getattr(storage, "autoincrement", None),
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
setattr(cls, name, mc)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
169
|
+
# Declarative Base
|
|
170
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class Base(DeclarativeBase):
|
|
174
|
+
__allow_unmapped__ = True
|
|
175
|
+
|
|
176
|
+
def __init_subclass__(cls, **kw):
|
|
177
|
+
# 0) Remove any previously registered class with the same module path.
|
|
178
|
+
try:
|
|
179
|
+
reg = Base.registry._class_registry
|
|
180
|
+
key = f"{cls.__module__}.{cls.__name__}"
|
|
181
|
+
existing = reg.get(key)
|
|
182
|
+
if existing is not None:
|
|
183
|
+
try:
|
|
184
|
+
Base.registry._dispose_cls(existing)
|
|
185
|
+
except Exception:
|
|
186
|
+
pass
|
|
187
|
+
reg.pop(key, None)
|
|
188
|
+
if reg.get(cls.__name__) is existing:
|
|
189
|
+
reg.pop(cls.__name__, None)
|
|
190
|
+
except Exception:
|
|
191
|
+
pass
|
|
192
|
+
|
|
193
|
+
# 0.5) If a table with the same name already exists, allow this class
|
|
194
|
+
# to extend it instead of raising duplicate-table errors.
|
|
195
|
+
try:
|
|
196
|
+
table_name = getattr(cls, "__tablename__", None)
|
|
197
|
+
if table_name and table_name in Base.metadata.tables:
|
|
198
|
+
table_args = getattr(cls, "__table_args__", None)
|
|
199
|
+
if table_args is None:
|
|
200
|
+
cls.__table_args__ = {"extend_existing": True}
|
|
201
|
+
elif isinstance(table_args, dict):
|
|
202
|
+
table_args = dict(table_args)
|
|
203
|
+
table_args["extend_existing"] = True
|
|
204
|
+
cls.__table_args__ = table_args
|
|
205
|
+
elif isinstance(table_args, tuple):
|
|
206
|
+
if table_args and isinstance(table_args[-1], dict):
|
|
207
|
+
table_dict = dict(table_args[-1])
|
|
208
|
+
table_dict["extend_existing"] = True
|
|
209
|
+
cls.__table_args__ = (*table_args[:-1], table_dict)
|
|
210
|
+
else:
|
|
211
|
+
cls.__table_args__ = (*table_args, {"extend_existing": True})
|
|
212
|
+
except Exception:
|
|
213
|
+
pass
|
|
214
|
+
|
|
215
|
+
# 1) BEFORE SQLAlchemy maps: turn ColumnSpecs into real mapped_column(...)
|
|
216
|
+
_materialize_colspecs_to_sqla(cls)
|
|
217
|
+
|
|
218
|
+
# 2) Let SQLAlchemy map the class (PK now exists)
|
|
219
|
+
super().__init_subclass__(**kw)
|
|
220
|
+
|
|
221
|
+
# 3) Seed model namespaces / index specs (ops/hooks/etc.) – idempotent
|
|
222
|
+
try:
|
|
223
|
+
from tigrbl.bindings import model as _model_bind
|
|
224
|
+
|
|
225
|
+
_model_bind.bind(cls)
|
|
226
|
+
except Exception:
|
|
227
|
+
pass
|
|
228
|
+
|
|
229
|
+
# 3) AUTO-BUILD CRUD schemas from ColumnSpecs so /docs has them
|
|
230
|
+
try:
|
|
231
|
+
from tigrbl.schema.build import build_for_model as _build_schemas
|
|
232
|
+
|
|
233
|
+
_build_schemas(
|
|
234
|
+
cls
|
|
235
|
+
) # attaches request/response models to the model/registry
|
|
236
|
+
except Exception:
|
|
237
|
+
# Surface during development if needed:
|
|
238
|
+
# raise
|
|
239
|
+
pass
|
|
240
|
+
|
|
241
|
+
metadata = MetaData(
|
|
242
|
+
naming_convention={
|
|
243
|
+
"pk": "pk_%(table_name)s",
|
|
244
|
+
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
|
|
245
|
+
"ix": "ix_%(table_name)s_%(column_0_name)s",
|
|
246
|
+
"uq": "uq_%(table_name)s_%(column_0_name)s",
|
|
247
|
+
"ck": "ck_%(table_name)s_%(column_0_name)s_%(constraint_type)s",
|
|
248
|
+
}
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
@declared_attr.directive
|
|
252
|
+
def __tablename__(cls) -> str: # noqa: N805
|
|
253
|
+
return cls.__name__.lower()
|
|
254
|
+
|
|
255
|
+
def __getitem__(self, key: str) -> Any:
|
|
256
|
+
"""Allow dict-style access to model attributes."""
|
|
257
|
+
return getattr(self, key)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
__all__ = ["Base"]
|
tigrbl/table/_table.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# tigrbl/tigrbl/v3/table/_table.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from types import SimpleNamespace
|
|
5
|
+
from typing import Any, Callable
|
|
6
|
+
|
|
7
|
+
from ..engine._engine import AsyncSession, Session
|
|
8
|
+
from ..engine import install_from_objects # reuse the collector
|
|
9
|
+
from ..engine import resolver as _resolver
|
|
10
|
+
from ..ddl import initialize as _ddl_initialize
|
|
11
|
+
from ._base import Base
|
|
12
|
+
from .table_spec import TableSpec
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Table(Base, TableSpec):
|
|
16
|
+
"""Declarative ORM table base.
|
|
17
|
+
|
|
18
|
+
This class now integrates :class:`Base` so ORM models and tables share
|
|
19
|
+
the same type. Column specifications are exposed via ``columns`` for
|
|
20
|
+
convenience.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
__abstract__ = True
|
|
24
|
+
columns: SimpleNamespace = SimpleNamespace()
|
|
25
|
+
|
|
26
|
+
def __init__(self, **kw: Any) -> None: # pragma: no cover - SQLA sets attrs
|
|
27
|
+
for k, v in kw.items():
|
|
28
|
+
setattr(self, k, v)
|
|
29
|
+
|
|
30
|
+
def __init_subclass__(cls, **kw: Any) -> None: # noqa: D401
|
|
31
|
+
super().__init_subclass__(**kw)
|
|
32
|
+
|
|
33
|
+
# expose ColumnSpecs under `columns` namespace
|
|
34
|
+
specs = getattr(cls, "__tigrbl_cols__", {})
|
|
35
|
+
cls.columns = SimpleNamespace(**specs)
|
|
36
|
+
|
|
37
|
+
# auto-register table-level bindings if declared
|
|
38
|
+
try:
|
|
39
|
+
install_from_objects(models=[cls])
|
|
40
|
+
except Exception: # pragma: no cover - best effort
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def install_engines(cls, *, api: Any | None = None) -> None:
|
|
45
|
+
install_from_objects(api=api, models=[cls])
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def acquire(
|
|
49
|
+
cls, *, op_alias: str | None = None
|
|
50
|
+
) -> tuple[Session | AsyncSession, Callable[[], None]]:
|
|
51
|
+
db, release = _resolver.acquire(model=cls, op_alias=op_alias)
|
|
52
|
+
return db, release
|
|
53
|
+
|
|
54
|
+
initialize = classmethod(_ddl_initialize)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
from typing import Any, Mapping, Tuple
|
|
6
|
+
|
|
7
|
+
from .table_spec import TableSpec
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger("uvicorn")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _merge_seq_attr(model: type, attr: str) -> Tuple[Any, ...]:
|
|
13
|
+
values: list[Any] = []
|
|
14
|
+
for base in model.__mro__:
|
|
15
|
+
seq = base.__dict__.get(attr, ()) or ()
|
|
16
|
+
try:
|
|
17
|
+
values.extend(seq)
|
|
18
|
+
except TypeError: # pragma: no cover - non-iterable
|
|
19
|
+
values.append(seq)
|
|
20
|
+
return tuple(values)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@lru_cache(maxsize=None)
|
|
24
|
+
def mro_collect_table_spec(model: type) -> TableSpec:
|
|
25
|
+
"""Collect TableSpec-like declarations across the model's MRO.
|
|
26
|
+
|
|
27
|
+
Merges common spec attributes (OPS, COLUMNS, SCHEMAS, HOOKS, SECURITY_DEPS,
|
|
28
|
+
DEPS) declared on the class or any mixins. Engine bindings declared via
|
|
29
|
+
``table_config`` use the same precedence: later classes in the MRO override
|
|
30
|
+
earlier ones.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
logger.info("Collecting table spec for %s", model.__name__)
|
|
34
|
+
|
|
35
|
+
engine: Any | None = None
|
|
36
|
+
for base in model.__mro__:
|
|
37
|
+
cfg = base.__dict__.get("table_config")
|
|
38
|
+
if isinstance(cfg, Mapping):
|
|
39
|
+
eng = (
|
|
40
|
+
cfg.get("engine")
|
|
41
|
+
or cfg.get("db")
|
|
42
|
+
or cfg.get("database")
|
|
43
|
+
or cfg.get("engine_provider")
|
|
44
|
+
or cfg.get("db_provider")
|
|
45
|
+
)
|
|
46
|
+
if eng is not None:
|
|
47
|
+
engine = eng
|
|
48
|
+
|
|
49
|
+
spec = TableSpec(
|
|
50
|
+
model=model,
|
|
51
|
+
engine=engine,
|
|
52
|
+
ops=_merge_seq_attr(model, "OPS"),
|
|
53
|
+
columns=_merge_seq_attr(model, "COLUMNS"),
|
|
54
|
+
schemas=_merge_seq_attr(model, "SCHEMAS"),
|
|
55
|
+
hooks=_merge_seq_attr(model, "HOOKS"),
|
|
56
|
+
security_deps=_merge_seq_attr(model, "SECURITY_DEPS"),
|
|
57
|
+
deps=_merge_seq_attr(model, "DEPS"),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
logger.debug(
|
|
61
|
+
"Collected table spec for %s: %d ops, %d columns",
|
|
62
|
+
model.__name__,
|
|
63
|
+
len(spec.ops),
|
|
64
|
+
len(spec.columns),
|
|
65
|
+
)
|
|
66
|
+
return spec
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
__all__ = ["mro_collect_table_spec"]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# tigrbl/tigrbl/v3/table/shortcuts.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, Sequence, Type
|
|
5
|
+
|
|
6
|
+
from .table_spec import TableSpec
|
|
7
|
+
from ._table import Table
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def defineTableSpec(
|
|
11
|
+
*,
|
|
12
|
+
# engine binding
|
|
13
|
+
engine: Any = None,
|
|
14
|
+
# composition
|
|
15
|
+
ops: Sequence[Any] = (),
|
|
16
|
+
columns: Sequence[Any] = (),
|
|
17
|
+
schemas: Sequence[Any] = (),
|
|
18
|
+
hooks: Sequence[Any] = (),
|
|
19
|
+
# dependency stacks
|
|
20
|
+
security_deps: Sequence[Any] = (),
|
|
21
|
+
deps: Sequence[Any] = (),
|
|
22
|
+
) -> Type[TableSpec]:
|
|
23
|
+
"""
|
|
24
|
+
Build a Table-spec class with class attributes only (no instances).
|
|
25
|
+
Use directly in your ORM class MRO:
|
|
26
|
+
|
|
27
|
+
class User(defineTableSpec(engine=..., ops=(...)), Base, Table):
|
|
28
|
+
__tablename__ = "users"
|
|
29
|
+
|
|
30
|
+
or pass it to `deriveTable(Model, ...)` to get a configured subclass.
|
|
31
|
+
"""
|
|
32
|
+
attrs = {
|
|
33
|
+
# top-level mirrors read by collectors
|
|
34
|
+
"OPS": tuple(ops or ()),
|
|
35
|
+
"COLUMNS": tuple(columns or ()),
|
|
36
|
+
"SCHEMAS": tuple(schemas or ()),
|
|
37
|
+
"HOOKS": tuple(hooks or ()),
|
|
38
|
+
"SECURITY_DEPS": tuple(security_deps or ()),
|
|
39
|
+
"DEPS": tuple(deps or ()),
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# Engine binding is conventionally stored under table_config["engine"]
|
|
43
|
+
# (and legacy "db" for backward compatibility) so collectors can find it.
|
|
44
|
+
if engine is not None:
|
|
45
|
+
attrs["table_config"] = {"engine": engine, "db": engine}
|
|
46
|
+
|
|
47
|
+
return type("TableSpec", (TableSpec,), attrs)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def deriveTable(model: Type[Table], **kw: Any) -> Type[Table]:
|
|
51
|
+
"""Produce a concrete ORM subclass that inherits the spec."""
|
|
52
|
+
Spec = defineTableSpec(**kw)
|
|
53
|
+
name = f"{model.__name__}WithSpec"
|
|
54
|
+
return type(name, (Spec, model), {})
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
__all__ = ["defineTableSpec", "deriveTable"]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# tigrbl/tigrbl/v3/table/table_spec.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any, Callable, Optional, Sequence
|
|
5
|
+
|
|
6
|
+
from ..engine.engine_spec import EngineCfg
|
|
7
|
+
from ..response.types import ResponseSpec
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class TableSpec:
|
|
12
|
+
"""
|
|
13
|
+
Declarative enrichments for an ORM class (model == table).
|
|
14
|
+
This does not construct an instance; it decorates/produces a class.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
model: Any # ORM class
|
|
18
|
+
engine: Optional[EngineCfg] = None
|
|
19
|
+
|
|
20
|
+
# NEW
|
|
21
|
+
ops: Sequence[Any] = field(default_factory=tuple) # OpSpec or shorthands
|
|
22
|
+
columns: Sequence[Any] = field(default_factory=tuple) # ColumnSpec or shorthands
|
|
23
|
+
schemas: Sequence[Any] = field(default_factory=tuple)
|
|
24
|
+
hooks: Sequence[Callable[..., Any]] = field(default_factory=tuple)
|
|
25
|
+
security_deps: Sequence[Callable[..., Any]] = field(default_factory=tuple)
|
|
26
|
+
deps: Sequence[Callable[..., Any]] = field(default_factory=tuple)
|
|
27
|
+
|
|
28
|
+
response: Optional[ResponseSpec] = None
|