tigrbl 0.0.1.dev1__py3-none-any.whl → 0.3.0.dev3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tigrbl/README.md +94 -0
- tigrbl/__init__.py +139 -14
- tigrbl/api/__init__.py +6 -0
- tigrbl/api/_api.py +72 -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 +286 -0
- tigrbl/app/__init__.py +0 -0
- tigrbl/app/_app.py +61 -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 +314 -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 +265 -0
- tigrbl/bindings/rest/common.py +116 -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/member.py +367 -0
- tigrbl/bindings/rest/router.py +292 -0
- tigrbl/bindings/rest/routing.py +133 -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 +133 -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 +26 -0
- tigrbl/engine/_engine.py +130 -0
- tigrbl/engine/bind.py +33 -0
- tigrbl/engine/builders.py +236 -0
- tigrbl/engine/collect.py +111 -0
- tigrbl/engine/decorators.py +108 -0
- tigrbl/engine/engine_spec.py +261 -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 +111 -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 +144 -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 +17 -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 +628 -0
- tigrbl/runtime/labels.py +353 -0
- tigrbl/runtime/opview.py +87 -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 +55 -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/shortcuts.py +22 -0
- tigrbl/specs.py +44 -0
- tigrbl/system/__init__.py +12 -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/table/__init__.py +9 -0
- tigrbl/table/_base.py +237 -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 +174 -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-0.3.0.dev3.dist-info/LICENSE +201 -0
- tigrbl-0.3.0.dev3.dist-info/METADATA +501 -0
- tigrbl-0.3.0.dev3.dist-info/RECORD +249 -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-0.0.1.dev1.dist-info → tigrbl-0.3.0.dev3.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# tigrbl/v3/schema/decorators.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Dict, Optional
|
|
7
|
+
|
|
8
|
+
from ..config.constants import TIGRBL_SCHEMA_DECLS_ATTR
|
|
9
|
+
|
|
10
|
+
logging.getLogger("uvicorn").setLevel(logging.DEBUG)
|
|
11
|
+
logger = logging.getLogger("uvicorn")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class _SchemaDecl:
|
|
16
|
+
alias: str # name under model.schemas.<alias>
|
|
17
|
+
kind: str # "in" | "out"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _register_schema_decl(
|
|
21
|
+
target_model: type, alias: str, kind: str, schema_cls: type
|
|
22
|
+
) -> None:
|
|
23
|
+
"""Store schema declarations on the model for later binding."""
|
|
24
|
+
logger.debug(
|
|
25
|
+
"Registering schema decl for %s alias=%s kind=%s",
|
|
26
|
+
target_model.__name__,
|
|
27
|
+
alias,
|
|
28
|
+
kind,
|
|
29
|
+
)
|
|
30
|
+
if kind not in ("in", "out"):
|
|
31
|
+
logger.debug("Invalid kind '%s' for schema decl", kind)
|
|
32
|
+
raise ValueError("schema_ctx(kind=...) must be 'in' or 'out'")
|
|
33
|
+
mapping: Dict[str, Dict[str, type]] = (
|
|
34
|
+
getattr(target_model, TIGRBL_SCHEMA_DECLS_ATTR, None) or {}
|
|
35
|
+
)
|
|
36
|
+
bucket = dict(mapping.get(alias, {}))
|
|
37
|
+
bucket[kind] = schema_cls
|
|
38
|
+
mapping[alias] = bucket
|
|
39
|
+
setattr(target_model, TIGRBL_SCHEMA_DECLS_ATTR, mapping)
|
|
40
|
+
logger.debug("Registered schema %s for alias '%s'", schema_cls, alias)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def schema_ctx(*, alias: str, kind: str = "out", for_: Optional[type] = None):
|
|
44
|
+
"""Register a named schema for a model."""
|
|
45
|
+
|
|
46
|
+
def deco(schema_cls: type):
|
|
47
|
+
if not isinstance(schema_cls, type):
|
|
48
|
+
logger.debug("schema_ctx applied to non-class %r", schema_cls)
|
|
49
|
+
raise TypeError("@schema_ctx must decorate a class")
|
|
50
|
+
if for_ is not None:
|
|
51
|
+
logger.debug(
|
|
52
|
+
"Registering schema %s for model %s via decorator",
|
|
53
|
+
schema_cls,
|
|
54
|
+
for_.__name__,
|
|
55
|
+
)
|
|
56
|
+
_register_schema_decl(for_, alias, kind, schema_cls)
|
|
57
|
+
setattr(
|
|
58
|
+
schema_cls, "__tigrbl_schema_decl__", _SchemaDecl(alias=alias, kind=kind)
|
|
59
|
+
)
|
|
60
|
+
logger.debug(
|
|
61
|
+
"Attached schema decl to %s: alias=%s kind=%s", schema_cls, alias, kind
|
|
62
|
+
)
|
|
63
|
+
return schema_cls
|
|
64
|
+
|
|
65
|
+
return deco
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
__all__ = ["schema_ctx"]
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Literal, Optional, Type
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
logging.getLogger("uvicorn").setLevel(logging.DEBUG)
|
|
9
|
+
logger = logging.getLogger("uvicorn")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_schema(
|
|
13
|
+
orm_cls: type,
|
|
14
|
+
op: str,
|
|
15
|
+
*,
|
|
16
|
+
kind: Literal["in", "out"] = "out",
|
|
17
|
+
) -> Optional[Type[BaseModel]]:
|
|
18
|
+
"""Return the bound schema for ``orm_cls``.
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
orm_cls:
|
|
23
|
+
ORM model that has been bound via :func:`tigrbl.bindings.build_schemas`.
|
|
24
|
+
op:
|
|
25
|
+
Operation alias whose schema should be returned.
|
|
26
|
+
kind:
|
|
27
|
+
Either ``"in"`` for request schemas or ``"out"`` for response schemas.
|
|
28
|
+
|
|
29
|
+
Returns
|
|
30
|
+
-------
|
|
31
|
+
Optional[Type[BaseModel]]
|
|
32
|
+
The requested schema class if available, otherwise ``None`` when the
|
|
33
|
+
operation uses raw payloads.
|
|
34
|
+
|
|
35
|
+
Raises
|
|
36
|
+
------
|
|
37
|
+
KeyError
|
|
38
|
+
If the model has not been bound or the operation/kind is unknown.
|
|
39
|
+
"""
|
|
40
|
+
logger.debug(
|
|
41
|
+
"Resolving schema for model=%s op=%s kind=%s", orm_cls.__name__, op, kind
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
ns = getattr(orm_cls, "schemas", None)
|
|
45
|
+
if ns is None:
|
|
46
|
+
logger.debug("Model %s has no schemas namespace", orm_cls.__name__)
|
|
47
|
+
raise KeyError(
|
|
48
|
+
f"{orm_cls.__name__} has no bound schemas; did you include the model?",
|
|
49
|
+
)
|
|
50
|
+
logger.debug("Found schemas namespace for model %s", orm_cls.__name__)
|
|
51
|
+
|
|
52
|
+
alias_ns = getattr(ns, op, None)
|
|
53
|
+
if alias_ns is None:
|
|
54
|
+
logger.debug("Unknown operation '%s' for model %s", op, orm_cls.__name__)
|
|
55
|
+
raise KeyError(f"Unknown op '{op}' for {orm_cls.__name__}")
|
|
56
|
+
logger.debug("Found operation '%s' for model %s", op, orm_cls.__name__)
|
|
57
|
+
|
|
58
|
+
kind = kind.lower()
|
|
59
|
+
if kind not in {"in", "out"}:
|
|
60
|
+
logger.debug("Invalid schema kind '%s' requested", kind)
|
|
61
|
+
raise ValueError("kind must be 'in' or 'out'")
|
|
62
|
+
logger.debug("Using schema kind '%s'", kind)
|
|
63
|
+
|
|
64
|
+
attr = "in_" if kind == "in" else "out"
|
|
65
|
+
if not hasattr(alias_ns, attr):
|
|
66
|
+
logger.debug(
|
|
67
|
+
"Schema kind '%s' not found for op '%s' on %s", kind, op, orm_cls.__name__
|
|
68
|
+
)
|
|
69
|
+
raise KeyError(
|
|
70
|
+
f"Schema kind '{kind}' not found for op '{op}' on {orm_cls.__name__}",
|
|
71
|
+
)
|
|
72
|
+
logger.debug(
|
|
73
|
+
"Found schema attribute '%s' for op '%s' on %s", attr, op, orm_cls.__name__
|
|
74
|
+
)
|
|
75
|
+
schema = getattr(alias_ns, attr)
|
|
76
|
+
logger.debug(
|
|
77
|
+
"Resolved schema %s for model %s op=%s kind=%s",
|
|
78
|
+
schema,
|
|
79
|
+
orm_cls.__name__,
|
|
80
|
+
op,
|
|
81
|
+
kind,
|
|
82
|
+
)
|
|
83
|
+
return schema
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
__all__ = ["get_schema"]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# tigrbl/v3/schema/schema_spec.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from .types import SchemaArg, SchemaKind
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True, slots=True)
|
|
11
|
+
class SchemaSpec:
|
|
12
|
+
"""Declarative description of a schema for a model."""
|
|
13
|
+
|
|
14
|
+
alias: str
|
|
15
|
+
kind: SchemaKind = "out"
|
|
16
|
+
for_: Optional[type] = None
|
|
17
|
+
schema: SchemaArg | None = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__all__ = ["SchemaSpec"]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# tigrbl/v3/schema/shortcuts.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Optional, Type
|
|
5
|
+
|
|
6
|
+
try: # pragma: no cover
|
|
7
|
+
from pydantic import BaseModel # type: ignore
|
|
8
|
+
except Exception: # pragma: no cover
|
|
9
|
+
|
|
10
|
+
class BaseModel: # minimal stub for typing only
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
from ._schema import Schema
|
|
15
|
+
from .schema_spec import SchemaSpec
|
|
16
|
+
from .types import SchemaArg, SchemaKind
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def schema_spec(
|
|
20
|
+
alias: str,
|
|
21
|
+
*,
|
|
22
|
+
kind: SchemaKind = "out",
|
|
23
|
+
for_: Optional[type] = None,
|
|
24
|
+
schema: SchemaArg | None = None,
|
|
25
|
+
) -> SchemaSpec:
|
|
26
|
+
"""Factory for :class:`SchemaSpec`."""
|
|
27
|
+
|
|
28
|
+
return SchemaSpec(alias=alias, kind=kind, for_=for_, schema=schema)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def schema(
|
|
32
|
+
model: Type[BaseModel],
|
|
33
|
+
*,
|
|
34
|
+
kind: SchemaKind = "out",
|
|
35
|
+
alias: str | None = None,
|
|
36
|
+
) -> Schema:
|
|
37
|
+
"""Factory for :class:`Schema`."""
|
|
38
|
+
|
|
39
|
+
return Schema(model=model, kind=kind, alias=alias)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
__all__ = ["schema_spec", "schema"]
|
tigrbl/schema/types.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# tigrbl/v3/schema/types.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Callable, Type, Union, Literal
|
|
6
|
+
|
|
7
|
+
try: # pragma: no cover
|
|
8
|
+
from pydantic import BaseModel # type: ignore
|
|
9
|
+
except Exception: # pragma: no cover
|
|
10
|
+
|
|
11
|
+
class BaseModel: # minimal stub for typing only
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
SchemaKind = Literal["in", "out"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True, slots=True)
|
|
19
|
+
class SchemaRef:
|
|
20
|
+
"""Lazy reference to ``model.schemas.<alias>.(in_|out)``."""
|
|
21
|
+
|
|
22
|
+
alias: str
|
|
23
|
+
kind: SchemaKind = "in"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
SchemaArg = Union[
|
|
27
|
+
Type[BaseModel], # direct Pydantic model
|
|
28
|
+
SchemaRef, # cross-op reference
|
|
29
|
+
str, # "alias.in" | "alias.out"
|
|
30
|
+
Callable[[type], Type[BaseModel]], # lambda cls: cls.schemas.create.in_
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
__all__ = ["SchemaKind", "SchemaRef", "SchemaArg"]
|
tigrbl/schema/utils.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any, Dict, List, Type
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, Field, RootModel, create_model
|
|
7
|
+
|
|
8
|
+
logging.getLogger("uvicorn").setLevel(logging.DEBUG)
|
|
9
|
+
logger = logging.getLogger("uvicorn")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def namely_model(model: Type[BaseModel], *, name: str, doc: str) -> Type[BaseModel]:
|
|
13
|
+
"""Assign a unique name and docstring to a Pydantic model class."""
|
|
14
|
+
logger.debug("Renaming model %s to %s", model.__name__, name)
|
|
15
|
+
model.__name__ = name
|
|
16
|
+
model.__qualname__ = name
|
|
17
|
+
model.__doc__ = doc
|
|
18
|
+
|
|
19
|
+
# Rebuild the model so Pydantic updates internal references (e.g., for OpenAPI titles).
|
|
20
|
+
|
|
21
|
+
model.model_rebuild(force=True)
|
|
22
|
+
logger.debug("Model %s rebuilt", name)
|
|
23
|
+
return model
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _camel(s: str) -> str:
|
|
27
|
+
rv = "".join(p.capitalize() or "_" for p in s.split("_"))
|
|
28
|
+
logger.debug("Camel-cased '%s' → '%s'", s, rv)
|
|
29
|
+
return rv
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _extract_example(schema: Type[BaseModel]) -> Dict[str, Any]:
|
|
33
|
+
"""Build a simple example object from field examples if available."""
|
|
34
|
+
try:
|
|
35
|
+
js = schema.model_json_schema()
|
|
36
|
+
except Exception as exc:
|
|
37
|
+
logger.debug("Failed to build JSON schema for %s: %s", schema, exc)
|
|
38
|
+
return {}
|
|
39
|
+
out: Dict[str, Any] = {}
|
|
40
|
+
for name, prop in (js.get("properties") or {}).items():
|
|
41
|
+
examples = prop.get("examples")
|
|
42
|
+
if examples:
|
|
43
|
+
out[name] = examples[0]
|
|
44
|
+
logger.debug("Extracted example for %s: %s", schema, out)
|
|
45
|
+
return out
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _make_bulk_rows_model(
|
|
49
|
+
model: type, verb: str, item_schema: Type[BaseModel]
|
|
50
|
+
) -> Type[BaseModel]:
|
|
51
|
+
"""Build a root model representing ``List[item_schema]``."""
|
|
52
|
+
name = f"{model.__name__}{_camel(verb)}Request"
|
|
53
|
+
example = _extract_example(item_schema)
|
|
54
|
+
examples = [[example]] if example else []
|
|
55
|
+
|
|
56
|
+
class _BulkModel(RootModel[List[item_schema]]): # type: ignore[misc]
|
|
57
|
+
model_config = ConfigDict(json_schema_extra={"examples": examples})
|
|
58
|
+
|
|
59
|
+
logger.debug("Built bulk rows model %s with examples=%s", name, examples)
|
|
60
|
+
return namely_model(
|
|
61
|
+
_BulkModel,
|
|
62
|
+
name=name,
|
|
63
|
+
doc=f"{verb} request schema for {model.__name__}",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _make_bulk_rows_response_model(
|
|
68
|
+
model: type, verb: str, item_schema: Type[BaseModel]
|
|
69
|
+
) -> Type[BaseModel]:
|
|
70
|
+
"""Build a root model representing ``List[item_schema]`` for responses."""
|
|
71
|
+
name = f"{model.__name__}{_camel(verb)}Response"
|
|
72
|
+
example = _extract_example(item_schema)
|
|
73
|
+
examples = [[example]] if example else []
|
|
74
|
+
|
|
75
|
+
class _BulkModel(RootModel[List[item_schema]]): # type: ignore[misc]
|
|
76
|
+
model_config = ConfigDict(json_schema_extra={"examples": examples})
|
|
77
|
+
|
|
78
|
+
logger.debug("Built bulk rows response model %s with examples=%s", name, examples)
|
|
79
|
+
return namely_model(
|
|
80
|
+
_BulkModel,
|
|
81
|
+
name=name,
|
|
82
|
+
doc=f"{verb} response schema for {model.__name__}",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _make_bulk_ids_model(
|
|
87
|
+
model: type, verb: str, pk_type: type | Any
|
|
88
|
+
) -> Type[BaseModel]:
|
|
89
|
+
"""Build a wrapper schema with an ``ids: List[pk_type]`` field."""
|
|
90
|
+
name = f"{model.__name__}{_camel(verb)}Request"
|
|
91
|
+
schema = create_model( # type: ignore[call-arg]
|
|
92
|
+
name,
|
|
93
|
+
ids=(List[pk_type], Field(...)), # type: ignore[name-defined]
|
|
94
|
+
)
|
|
95
|
+
logger.debug("Built bulk ids model %s", name)
|
|
96
|
+
return namely_model(
|
|
97
|
+
schema,
|
|
98
|
+
name=name,
|
|
99
|
+
doc=f"{verb} request schema for {model.__name__}",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _make_deleted_response_model(model: type, verb: str) -> Type[BaseModel]:
|
|
104
|
+
"""Build a response schema with a ``deleted`` count."""
|
|
105
|
+
name = f"{model.__name__}{_camel(verb)}Response"
|
|
106
|
+
schema = create_model( # type: ignore[call-arg]
|
|
107
|
+
name,
|
|
108
|
+
deleted=(int, Field(..., examples=[0])),
|
|
109
|
+
__config__=ConfigDict(json_schema_extra={"examples": [{"deleted": 0}]}),
|
|
110
|
+
)
|
|
111
|
+
logger.debug("Built deleted response model %s", name)
|
|
112
|
+
return namely_model(
|
|
113
|
+
schema,
|
|
114
|
+
name=name,
|
|
115
|
+
doc=f"{verb} response schema for {model.__name__}",
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _make_pk_model(
|
|
120
|
+
model: type, verb: str, pk_name: str, pk_type: type | Any
|
|
121
|
+
) -> Type[BaseModel]:
|
|
122
|
+
"""Build a wrapper schema with a single primary-key field."""
|
|
123
|
+
name = f"{model.__name__}{_camel(verb)}Request"
|
|
124
|
+
schema = create_model( # type: ignore[call-arg]
|
|
125
|
+
name,
|
|
126
|
+
**{pk_name: (pk_type, Field(...))}, # type: ignore[name-defined]
|
|
127
|
+
)
|
|
128
|
+
logger.debug("Built pk model %s for field %s", name, pk_name)
|
|
129
|
+
return namely_model(
|
|
130
|
+
schema,
|
|
131
|
+
name=name,
|
|
132
|
+
doc=f"{verb} request schema for {model.__name__}",
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
__all__ = [
|
|
137
|
+
"namely_model",
|
|
138
|
+
"_make_bulk_rows_model",
|
|
139
|
+
"_make_bulk_rows_response_model",
|
|
140
|
+
"_make_bulk_ids_model",
|
|
141
|
+
"_make_deleted_response_model",
|
|
142
|
+
"_make_pk_model",
|
|
143
|
+
]
|
tigrbl/shortcuts.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# tigrbl/tigrbl/v3/shortcuts.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from .app import shortcuts as app_shortcuts
|
|
5
|
+
from .api import shortcuts as api_shortcuts
|
|
6
|
+
from .engine import shortcuts as engine_shortcuts
|
|
7
|
+
from .specs import shortcuts as specs_shortcuts
|
|
8
|
+
from .table import shortcuts as table_shortcuts
|
|
9
|
+
|
|
10
|
+
from .app.shortcuts import * # noqa: F401,F403
|
|
11
|
+
from .api.shortcuts import * # noqa: F401,F403
|
|
12
|
+
from .engine.shortcuts import * # noqa: F401,F403
|
|
13
|
+
from .specs.shortcuts import * # noqa: F401,F403
|
|
14
|
+
from .table.shortcuts import * # noqa: F401,F403
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
*app_shortcuts.__all__,
|
|
18
|
+
*api_shortcuts.__all__,
|
|
19
|
+
*engine_shortcuts.__all__,
|
|
20
|
+
*specs_shortcuts.__all__,
|
|
21
|
+
*table_shortcuts.__all__,
|
|
22
|
+
]
|
tigrbl/specs.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# tigrbl/v3/specs.py
|
|
2
|
+
"""Compatibility layer that re-exports column specs.
|
|
3
|
+
|
|
4
|
+
Importing from ``tigrbl.specs`` remains supported but the
|
|
5
|
+
implementation now lives under :mod:`tigrbl.column`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
from .column import * # noqa: F401,F403
|
|
13
|
+
from .column import (
|
|
14
|
+
__all__ as _all,
|
|
15
|
+
column_spec as _column_spec,
|
|
16
|
+
field_spec as _field_spec,
|
|
17
|
+
infer as _infer,
|
|
18
|
+
io_spec as _io_spec,
|
|
19
|
+
shortcuts as _shortcuts,
|
|
20
|
+
storage_spec as _storage_spec,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = list(_all)
|
|
24
|
+
|
|
25
|
+
# Allow submodule imports like ``tigrbl.specs.storage_spec``
|
|
26
|
+
__path__: list[str] = []
|
|
27
|
+
_mod = sys.modules[__name__]
|
|
28
|
+
_mod.column_spec = _column_spec
|
|
29
|
+
_mod.field_spec = _field_spec
|
|
30
|
+
_mod.infer = _infer
|
|
31
|
+
_mod.io_spec = _io_spec
|
|
32
|
+
_mod.shortcuts = _shortcuts
|
|
33
|
+
_mod.storage_spec = _storage_spec
|
|
34
|
+
|
|
35
|
+
sys.modules[f"{__name__}.column_spec"] = _column_spec
|
|
36
|
+
sys.modules[f"{__name__}.field_spec"] = _field_spec
|
|
37
|
+
sys.modules[f"{__name__}.infer"] = _infer
|
|
38
|
+
sys.modules[f"{__name__}.io_spec"] = _io_spec
|
|
39
|
+
sys.modules[f"{__name__}.shortcuts"] = _shortcuts
|
|
40
|
+
sys.modules[f"{__name__}.storage_spec"] = _storage_spec
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def __dir__() -> list[str]:
|
|
44
|
+
return sorted(__all__)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# tigrbl/v3/system/__init__.py
|
|
2
|
+
"""
|
|
3
|
+
Tigrbl v3 – System/Diagnostics helpers.
|
|
4
|
+
|
|
5
|
+
- mount_diagnostics(api, *, get_db=None) -> Router
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from .diagnostics import mount_diagnostics
|
|
11
|
+
|
|
12
|
+
__all__ = ["mount_diagnostics"]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from ...runtime.kernel import _default_kernel, build_phase_chains
|
|
2
|
+
from .router import mount_diagnostics
|
|
3
|
+
from .methodz import build_methodz_endpoint as _build_methodz_endpoint
|
|
4
|
+
from .hookz import build_hookz_endpoint as _build_hookz_endpoint
|
|
5
|
+
from .kernelz import build_kernelz_endpoint as _build_kernelz_endpoint
|
|
6
|
+
from .utils import (
|
|
7
|
+
model_iter as _model_iter,
|
|
8
|
+
opspecs as _opspecs,
|
|
9
|
+
label_callable as _label_callable,
|
|
10
|
+
label_hook as _label_hook,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"mount_diagnostics",
|
|
15
|
+
"_build_methodz_endpoint",
|
|
16
|
+
"_build_hookz_endpoint",
|
|
17
|
+
"_build_kernelz_endpoint",
|
|
18
|
+
"_model_iter",
|
|
19
|
+
"_opspecs",
|
|
20
|
+
"_label_callable",
|
|
21
|
+
"_label_hook",
|
|
22
|
+
"build_phase_chains",
|
|
23
|
+
"_default_kernel",
|
|
24
|
+
]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, Sequence
|
|
4
|
+
from types import SimpleNamespace
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
from ...types import Router, Request, Depends
|
|
8
|
+
from fastapi.responses import JSONResponse
|
|
9
|
+
except Exception: # pragma: no cover
|
|
10
|
+
|
|
11
|
+
class Router: # type: ignore
|
|
12
|
+
def __init__(self, *a, **kw):
|
|
13
|
+
self.routes = []
|
|
14
|
+
|
|
15
|
+
def add_api_route(
|
|
16
|
+
self, path: str, endpoint: Callable, methods: Sequence[str], **opts
|
|
17
|
+
):
|
|
18
|
+
self.routes.append((path, methods, endpoint, opts))
|
|
19
|
+
|
|
20
|
+
class Request: # type: ignore
|
|
21
|
+
def __init__(self, scope=None):
|
|
22
|
+
self.scope = scope or {}
|
|
23
|
+
self.state = SimpleNamespace()
|
|
24
|
+
self.query_params = {}
|
|
25
|
+
|
|
26
|
+
class JSONResponse(dict): # type: ignore
|
|
27
|
+
def __init__(self, content: Any, status_code: int = 200):
|
|
28
|
+
super().__init__(content=content, status_code=status_code)
|
|
29
|
+
|
|
30
|
+
def Depends(fn: Callable[..., Any]): # type: ignore
|
|
31
|
+
return fn
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any, Callable, Optional
|
|
5
|
+
|
|
6
|
+
from .compat import Depends, JSONResponse, Request
|
|
7
|
+
from .utils import maybe_execute
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def build_healthz_endpoint(dep: Optional[Callable[..., Any]]):
|
|
13
|
+
"""
|
|
14
|
+
Returns a FastAPI endpoint function for /healthz.
|
|
15
|
+
If `dep` is provided, it's used as a dependency to supply `db`.
|
|
16
|
+
Otherwise, we try request.state.db.
|
|
17
|
+
"""
|
|
18
|
+
if dep is not None:
|
|
19
|
+
|
|
20
|
+
async def _healthz(db: Any = Depends(dep)):
|
|
21
|
+
try:
|
|
22
|
+
await maybe_execute(db, "SELECT 1")
|
|
23
|
+
return {"ok": True}
|
|
24
|
+
except Exception as e: # pragma: no cover
|
|
25
|
+
logger.exception("/healthz failed")
|
|
26
|
+
return JSONResponse({"ok": False, "error": str(e)}, status_code=500)
|
|
27
|
+
|
|
28
|
+
return _healthz
|
|
29
|
+
|
|
30
|
+
async def _healthz(request: Request):
|
|
31
|
+
db = getattr(request.state, "db", None)
|
|
32
|
+
if db is None:
|
|
33
|
+
return {"ok": True, "warning": "no-db"}
|
|
34
|
+
try:
|
|
35
|
+
await maybe_execute(db, "SELECT 1")
|
|
36
|
+
return {"ok": True}
|
|
37
|
+
except Exception as e: # pragma: no cover
|
|
38
|
+
logger.exception("/healthz failed")
|
|
39
|
+
return JSONResponse({"ok": False, "error": str(e)}, status_code=500)
|
|
40
|
+
|
|
41
|
+
return _healthz
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List, Optional
|
|
4
|
+
from types import SimpleNamespace
|
|
5
|
+
|
|
6
|
+
from ...op.types import PHASES
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def build_hookz_endpoint(api: Any):
|
|
10
|
+
cache: Optional[Dict[str, Dict[str, Dict[str, List[str]]]]] = None
|
|
11
|
+
|
|
12
|
+
async def _hookz():
|
|
13
|
+
nonlocal cache
|
|
14
|
+
"""
|
|
15
|
+
Expose hook execution order for each method.
|
|
16
|
+
|
|
17
|
+
Phases appear in runner order; error phases trail.
|
|
18
|
+
Within each phase, hooks are listed in execution order: global (None) hooks,
|
|
19
|
+
then method-specific hooks.
|
|
20
|
+
"""
|
|
21
|
+
if cache is not None:
|
|
22
|
+
return cache
|
|
23
|
+
|
|
24
|
+
from . import _model_iter, _opspecs, _label_callable
|
|
25
|
+
|
|
26
|
+
out: Dict[str, Dict[str, Dict[str, List[str]]]] = {}
|
|
27
|
+
for model in _model_iter(api):
|
|
28
|
+
mname = getattr(model, "__name__", "Model")
|
|
29
|
+
hooks_root = getattr(model, "hooks", SimpleNamespace())
|
|
30
|
+
alias_sources = set()
|
|
31
|
+
rpc_ns = getattr(model, "rpc", SimpleNamespace())
|
|
32
|
+
alias_sources.update(getattr(rpc_ns, "__dict__", {}).keys())
|
|
33
|
+
for sp in _opspecs(model):
|
|
34
|
+
alias_sources.add(sp.alias)
|
|
35
|
+
|
|
36
|
+
model_map: Dict[str, Dict[str, List[str]]] = {}
|
|
37
|
+
for alias in sorted(alias_sources):
|
|
38
|
+
alias_ns = getattr(hooks_root, alias, None) or SimpleNamespace()
|
|
39
|
+
phase_map: Dict[str, List[str]] = {}
|
|
40
|
+
for ph in PHASES:
|
|
41
|
+
steps = list(getattr(alias_ns, ph, []) or [])
|
|
42
|
+
if steps:
|
|
43
|
+
phase_map[ph] = [_label_callable(fn) for fn in steps]
|
|
44
|
+
if phase_map:
|
|
45
|
+
model_map[alias] = phase_map
|
|
46
|
+
if model_map:
|
|
47
|
+
out[mname] = model_map
|
|
48
|
+
cache = out
|
|
49
|
+
return cache
|
|
50
|
+
|
|
51
|
+
return _hookz
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Diagnostic endpoint exposing kernel phase plans."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ...runtime.kernel import _default_kernel as K
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def build_kernelz_endpoint(api: Any):
|
|
11
|
+
"""Return an async handler that serves the Kernel's cached plan."""
|
|
12
|
+
|
|
13
|
+
async def _kernelz():
|
|
14
|
+
K.ensure_primed(api)
|
|
15
|
+
return K.kernelz_payload(api)
|
|
16
|
+
|
|
17
|
+
return _kernelz
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__all__ = ["build_kernelz_endpoint"]
|