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,132 @@
|
|
|
1
|
+
# tigrbl/v3/transport/rest/aggregator.py
|
|
2
|
+
"""
|
|
3
|
+
Aggregates per-model REST routers into a single Router.
|
|
4
|
+
|
|
5
|
+
This does not build endpoints by itself — it simply collects the routers that
|
|
6
|
+
`tigrbl.bindings.rest` attached to each model at `model.rest.router`.
|
|
7
|
+
|
|
8
|
+
Recommended workflow:
|
|
9
|
+
1) Include models with `mount_router=False` so you don't double-mount:
|
|
10
|
+
api.include_model(User, mount_router=False)
|
|
11
|
+
api.include_model(Team, mount_router=False)
|
|
12
|
+
2) Aggregate and mount once:
|
|
13
|
+
app.include_router(build_rest_router(api, base_prefix="/api"))
|
|
14
|
+
or:
|
|
15
|
+
mount_rest(api, app, base_prefix="/api")
|
|
16
|
+
|
|
17
|
+
Notes:
|
|
18
|
+
• Router paths already include `/{resource}`; we only add `base_prefix`.
|
|
19
|
+
• Model-level auth/db deps and extra REST deps are already attached to each
|
|
20
|
+
model router by `bindings.rest`; this wrapper can add *additional* top-level
|
|
21
|
+
dependencies if you pass them in.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from typing import Any, Mapping, Optional, Sequence
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
from ...types import Router, Depends
|
|
30
|
+
except Exception: # pragma: no cover
|
|
31
|
+
# Minimal shim to keep importable without FastAPI
|
|
32
|
+
class Router: # type: ignore
|
|
33
|
+
def __init__(self, *a, dependencies: Optional[Sequence[Any]] = None, **kw):
|
|
34
|
+
self.routes = []
|
|
35
|
+
self.includes = []
|
|
36
|
+
self.dependencies = list(dependencies or [])
|
|
37
|
+
|
|
38
|
+
def add_api_route(self, path: str, endpoint, methods: Sequence[str], **opts):
|
|
39
|
+
self.routes.append((path, methods, endpoint, opts))
|
|
40
|
+
|
|
41
|
+
def include_router(self, router: "Router", *, prefix: str = "", **opts):
|
|
42
|
+
self.includes.append((router, prefix, opts))
|
|
43
|
+
|
|
44
|
+
def Depends(fn): # type: ignore
|
|
45
|
+
return fn
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _norm_prefix(p: Optional[str]) -> str:
|
|
49
|
+
if not p:
|
|
50
|
+
return ""
|
|
51
|
+
if not p.startswith("/"):
|
|
52
|
+
p = "/" + p
|
|
53
|
+
# Avoid double trailing slashes; FastAPI is lenient but keep it clean
|
|
54
|
+
return p.rstrip("/")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _normalize_deps(deps: Optional[Sequence[Any]]) -> list:
|
|
58
|
+
"""Accept either Depends(...) objects or plain callables."""
|
|
59
|
+
out = []
|
|
60
|
+
for d in deps or ():
|
|
61
|
+
try:
|
|
62
|
+
is_dep_obj = hasattr(d, "dependency")
|
|
63
|
+
except Exception:
|
|
64
|
+
is_dep_obj = False
|
|
65
|
+
out.append(d if is_dep_obj else Depends(d))
|
|
66
|
+
return out
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _iter_models(api: Any, only: Optional[Sequence[type]] = None) -> Sequence[type]:
|
|
70
|
+
if only:
|
|
71
|
+
return list(only)
|
|
72
|
+
models: Mapping[str, type] = getattr(api, "models", {}) or {}
|
|
73
|
+
# deterministic iteration
|
|
74
|
+
return [models[k] for k in sorted(models.keys())]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def build_rest_router(
|
|
78
|
+
api: Any,
|
|
79
|
+
*,
|
|
80
|
+
models: Optional[Sequence[type]] = None,
|
|
81
|
+
base_prefix: str = "",
|
|
82
|
+
dependencies: Optional[Sequence[Any]] = None,
|
|
83
|
+
) -> Router:
|
|
84
|
+
"""
|
|
85
|
+
Build a top-level Router that includes each model's router under `base_prefix`.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
api: your Tigrbl facade (or any object with `.models` dict).
|
|
89
|
+
models: optional subset of models to include; defaults to all bound models.
|
|
90
|
+
base_prefix: prefix applied once for all included routers (e.g., "/api").
|
|
91
|
+
dependencies: additional router-level dependencies (Depends(...) or callables).
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Router ready to be mounted on your FastAPI app.
|
|
95
|
+
"""
|
|
96
|
+
root = Router(dependencies=_normalize_deps(dependencies))
|
|
97
|
+
prefix = _norm_prefix(base_prefix)
|
|
98
|
+
|
|
99
|
+
for model in _iter_models(api, models):
|
|
100
|
+
rest_ns = getattr(model, "rest", None)
|
|
101
|
+
router = getattr(rest_ns, "router", None) if rest_ns is not None else None
|
|
102
|
+
if router is None:
|
|
103
|
+
# Nothing to include for this model (not bound or no routes)
|
|
104
|
+
continue
|
|
105
|
+
# Include with only the base prefix; the model router already has /{resource} in its paths
|
|
106
|
+
root.include_router(router, prefix=prefix or "")
|
|
107
|
+
return root
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def mount_rest(
|
|
111
|
+
api: Any,
|
|
112
|
+
app: Any,
|
|
113
|
+
*,
|
|
114
|
+
models: Optional[Sequence[type]] = None,
|
|
115
|
+
base_prefix: str = "",
|
|
116
|
+
dependencies: Optional[Sequence[Any]] = None,
|
|
117
|
+
) -> Router:
|
|
118
|
+
"""
|
|
119
|
+
Convenience helper: build the aggregated router and include it on `app`.
|
|
120
|
+
|
|
121
|
+
Returns the created router so you can keep a reference if desired.
|
|
122
|
+
"""
|
|
123
|
+
router = build_rest_router(
|
|
124
|
+
api, models=models, base_prefix=base_prefix, dependencies=dependencies
|
|
125
|
+
)
|
|
126
|
+
include = getattr(app, "include_router", None)
|
|
127
|
+
if callable(include):
|
|
128
|
+
include(router)
|
|
129
|
+
return router
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
__all__ = ["build_rest_router", "mount_rest"]
|
tigrbl/types/__init__.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# ── Standard Library ─────────────────────────────────────────────────────
|
|
2
|
+
from types import MethodType, SimpleNamespace
|
|
3
|
+
from uuid import uuid4, UUID
|
|
4
|
+
|
|
5
|
+
# ── Third-party Dependencies (via deps module) ───────────────────────────
|
|
6
|
+
from ..deps.sqlalchemy import (
|
|
7
|
+
# Core SQLAlchemy
|
|
8
|
+
Boolean,
|
|
9
|
+
Column,
|
|
10
|
+
_DateTime,
|
|
11
|
+
SAEnum,
|
|
12
|
+
Text,
|
|
13
|
+
ForeignKey,
|
|
14
|
+
Index,
|
|
15
|
+
Integer,
|
|
16
|
+
JSON,
|
|
17
|
+
Numeric,
|
|
18
|
+
String,
|
|
19
|
+
LargeBinary,
|
|
20
|
+
UniqueConstraint,
|
|
21
|
+
CheckConstraint,
|
|
22
|
+
create_engine,
|
|
23
|
+
event,
|
|
24
|
+
# PostgreSQL dialect
|
|
25
|
+
ARRAY,
|
|
26
|
+
PgEnum,
|
|
27
|
+
JSONB,
|
|
28
|
+
TSVECTOR,
|
|
29
|
+
# ORM
|
|
30
|
+
Mapped,
|
|
31
|
+
declarative_mixin,
|
|
32
|
+
declared_attr,
|
|
33
|
+
foreign,
|
|
34
|
+
mapped_column,
|
|
35
|
+
relationship,
|
|
36
|
+
remote,
|
|
37
|
+
column_property,
|
|
38
|
+
Session,
|
|
39
|
+
sessionmaker,
|
|
40
|
+
InstrumentedAttribute,
|
|
41
|
+
# Extensions
|
|
42
|
+
MutableDict,
|
|
43
|
+
MutableList,
|
|
44
|
+
hybrid_property,
|
|
45
|
+
StaticPool,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
from ..deps.pydantic import (
|
|
49
|
+
BaseModel,
|
|
50
|
+
Field,
|
|
51
|
+
ValidationError,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
from ..deps.fastapi import (
|
|
55
|
+
APIRouter,
|
|
56
|
+
Router,
|
|
57
|
+
Security,
|
|
58
|
+
Depends,
|
|
59
|
+
Request,
|
|
60
|
+
Response,
|
|
61
|
+
Path,
|
|
62
|
+
Body,
|
|
63
|
+
HTTPException,
|
|
64
|
+
App,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# ── Local Package ─────────────────────────────────────────────────────────
|
|
68
|
+
from .op import _Op, _SchemaVerb
|
|
69
|
+
from .uuid import PgUUID, SqliteUUID
|
|
70
|
+
from .authn_abc import AuthNProvider
|
|
71
|
+
from .table_config_provider import TableConfigProvider
|
|
72
|
+
from .nested_path_provider import NestedPathProvider
|
|
73
|
+
from .allow_anon_provider import AllowAnonProvider
|
|
74
|
+
from .request_extras_provider import (
|
|
75
|
+
RequestExtrasProvider,
|
|
76
|
+
list_request_extras_providers,
|
|
77
|
+
)
|
|
78
|
+
from .response_extras_provider import (
|
|
79
|
+
ResponseExtrasProvider,
|
|
80
|
+
list_response_extras_providers,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
from .op_verb_alias_provider import OpVerbAliasProvider, list_verb_alias_providers
|
|
84
|
+
from .op_config_provider import OpConfigProvider
|
|
85
|
+
|
|
86
|
+
# ── Generics / Extensions ─────────────────────────────────────────────────
|
|
87
|
+
DateTime = _DateTime(timezone=False)
|
|
88
|
+
TZDateTime = _DateTime(timezone=True)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ── Public Re-exports (Backwards Compatibility) ──────────────────────────
|
|
92
|
+
__all__: list[str] = [
|
|
93
|
+
# local
|
|
94
|
+
"_Op",
|
|
95
|
+
"_SchemaVerb",
|
|
96
|
+
"AuthNProvider",
|
|
97
|
+
"TableConfigProvider",
|
|
98
|
+
"NestedPathProvider",
|
|
99
|
+
"AllowAnonProvider",
|
|
100
|
+
"RequestExtrasProvider",
|
|
101
|
+
"ResponseExtrasProvider",
|
|
102
|
+
"OpVerbAliasProvider",
|
|
103
|
+
"list_verb_alias_providers",
|
|
104
|
+
"list_request_extras_providers",
|
|
105
|
+
"list_response_extras_providers",
|
|
106
|
+
"OpConfigProvider",
|
|
107
|
+
# add ons
|
|
108
|
+
"SqliteUUID",
|
|
109
|
+
# builtin types
|
|
110
|
+
"MethodType",
|
|
111
|
+
"SimpleNamespace",
|
|
112
|
+
"uuid4",
|
|
113
|
+
"UUID",
|
|
114
|
+
# sqlalchemy core (from deps.sqlalchemy)
|
|
115
|
+
"Boolean",
|
|
116
|
+
"Column",
|
|
117
|
+
"DateTime",
|
|
118
|
+
"TZDateTime",
|
|
119
|
+
"Text",
|
|
120
|
+
"SAEnum",
|
|
121
|
+
"ForeignKey",
|
|
122
|
+
"Index",
|
|
123
|
+
"Integer",
|
|
124
|
+
"JSON",
|
|
125
|
+
"Numeric",
|
|
126
|
+
"String",
|
|
127
|
+
"LargeBinary",
|
|
128
|
+
"UniqueConstraint",
|
|
129
|
+
"CheckConstraint",
|
|
130
|
+
"create_engine",
|
|
131
|
+
"event",
|
|
132
|
+
# sqlalchemy.dialects.postgresql (from deps.sqlalchemy)
|
|
133
|
+
"ARRAY",
|
|
134
|
+
"PgEnum",
|
|
135
|
+
"JSONB",
|
|
136
|
+
"PgUUID",
|
|
137
|
+
"TSVECTOR",
|
|
138
|
+
# sqlalchemy.orm (from deps.sqlalchemy)
|
|
139
|
+
"Mapped",
|
|
140
|
+
"declarative_mixin",
|
|
141
|
+
"declared_attr",
|
|
142
|
+
"foreign",
|
|
143
|
+
"mapped_column",
|
|
144
|
+
"column_property",
|
|
145
|
+
"hybrid_property",
|
|
146
|
+
"relationship",
|
|
147
|
+
"remote",
|
|
148
|
+
"Session",
|
|
149
|
+
"sessionmaker",
|
|
150
|
+
"InstrumentedAttribute",
|
|
151
|
+
# sqlalchemy.ext.mutable (from deps.sqlalchemy)
|
|
152
|
+
"MutableDict",
|
|
153
|
+
"MutableList",
|
|
154
|
+
"StaticPool",
|
|
155
|
+
# pydantic schema support (from deps.pydantic)
|
|
156
|
+
"BaseModel",
|
|
157
|
+
"Field",
|
|
158
|
+
"ValidationError",
|
|
159
|
+
# fastapi support (from deps.fastapi)
|
|
160
|
+
"Request",
|
|
161
|
+
"Response",
|
|
162
|
+
"APIRouter",
|
|
163
|
+
"Router",
|
|
164
|
+
"App",
|
|
165
|
+
"Security",
|
|
166
|
+
"Depends",
|
|
167
|
+
"Path",
|
|
168
|
+
"Body",
|
|
169
|
+
"HTTPException",
|
|
170
|
+
]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from typing import Callable, ClassVar, Iterable
|
|
2
|
+
|
|
3
|
+
from .table_config_provider import TableConfigProvider
|
|
4
|
+
|
|
5
|
+
_ALLOW_ANON_PROVIDERS: set[type] = set()
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AllowAnonProvider(TableConfigProvider):
|
|
9
|
+
"""Models that expose operations without authentication."""
|
|
10
|
+
|
|
11
|
+
__tigrbl_allow_anon__: ClassVar[Iterable[str] | Callable[[], Iterable[str]]] = ()
|
|
12
|
+
|
|
13
|
+
def __init_subclass__(cls, **kw):
|
|
14
|
+
super().__init_subclass__(**kw)
|
|
15
|
+
_ALLOW_ANON_PROVIDERS.add(cls)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def list_allow_anon_providers():
|
|
19
|
+
return sorted(_ALLOW_ANON_PROVIDERS, key=lambda c: c.__name__)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# tigrbl/v3/types/authn_abc.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from fastapi import Request
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AuthNProvider(ABC):
|
|
8
|
+
"""
|
|
9
|
+
Marker‑interface that any AuthN extension must implement
|
|
10
|
+
so that Tigrbl can plug itself in at run‑time.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
# ---------- FastAPI dependency ----------
|
|
14
|
+
@abstractmethod
|
|
15
|
+
async def get_principal(self, request: Request): # -> dict[str, str]
|
|
16
|
+
"""Return {"sub": user_id, "tid": tenant_id, ...} or raise HTTP 401."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
__all__ = ["AuthNProvider"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
for _name in list(globals()):
|
|
23
|
+
if _name not in __all__ and not _name.startswith("__"):
|
|
24
|
+
del globals()[_name]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def __dir__():
|
|
28
|
+
"""Tighten ``dir()`` output for interactive sessions."""
|
|
29
|
+
|
|
30
|
+
return sorted(__all__)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from abc import abstractmethod
|
|
2
|
+
|
|
3
|
+
from .table_config_provider import TableConfigProvider
|
|
4
|
+
|
|
5
|
+
_NESTED_PATH_PROVIDERS: set[type] = set()
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class NestedPathProvider(TableConfigProvider):
|
|
9
|
+
"""Models that supply nested route prefixes."""
|
|
10
|
+
|
|
11
|
+
@classmethod
|
|
12
|
+
@abstractmethod
|
|
13
|
+
def __tigrbl_nested_paths__(cls) -> str | None:
|
|
14
|
+
"""Return hierarchical path prefix for nested routes."""
|
|
15
|
+
|
|
16
|
+
def __init_subclass__(cls, **kw):
|
|
17
|
+
super().__init_subclass__(**kw)
|
|
18
|
+
_NESTED_PATH_PROVIDERS.add(cls)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def list_nested_path_providers():
|
|
22
|
+
return sorted(_NESTED_PATH_PROVIDERS, key=lambda c: c.__name__)
|
tigrbl/types/op.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tigrbl/v3/types/_op.py
|
|
3
|
+
Pure structural helpers"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Callable, NamedTuple, Type, Literal, TypeAlias
|
|
6
|
+
|
|
7
|
+
_SchemaVerb: TypeAlias = Literal[
|
|
8
|
+
"create",
|
|
9
|
+
"read",
|
|
10
|
+
"update",
|
|
11
|
+
"replace",
|
|
12
|
+
"merge",
|
|
13
|
+
"delete",
|
|
14
|
+
"list",
|
|
15
|
+
"clear",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
# need to add clear
|
|
19
|
+
# need to add support for bulk create, update, delete
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class _Op(NamedTuple):
|
|
23
|
+
"""
|
|
24
|
+
Metadata for one REST/RPC operation registered by Tigrbl.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
verb: str # e.g. "create", "list"
|
|
28
|
+
http: str # "POST" | "GET" | "PATCH" | …
|
|
29
|
+
path: str # URL suffix, e.g. "/{item_id}"
|
|
30
|
+
In: Type | None # Pydantic input model (or None)
|
|
31
|
+
Out: Type # Pydantic output model
|
|
32
|
+
core: Callable[..., Any] # The actual implementation
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
__all__ = ["_Op", "_SchemaVerb"]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Iterable, Literal
|
|
3
|
+
from .table_config_provider import TableConfigProvider
|
|
4
|
+
from ..op import OpSpec
|
|
5
|
+
from ..op.canonical import ( # noqa: F401
|
|
6
|
+
DEFAULT_CANON_VERBS,
|
|
7
|
+
should_wire_canonical,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class OpConfigProvider(TableConfigProvider):
|
|
12
|
+
__tigrbl_ops__: Iterable[OpSpec] = ()
|
|
13
|
+
|
|
14
|
+
# Default canonical verbs wiring policy
|
|
15
|
+
__tigrbl_defaults_mode__: Literal["all", "none", "some"] = "all"
|
|
16
|
+
__tigrbl_defaults_include__: set[str] = set()
|
|
17
|
+
__tigrbl_defaults_exclude__: set[str] = set()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from typing import Callable, ClassVar, Mapping, Literal
|
|
2
|
+
|
|
3
|
+
from .table_config_provider import TableConfigProvider
|
|
4
|
+
|
|
5
|
+
_VERB_ALIAS_PROVIDERS: set[type] = set()
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class OpVerbAliasProvider(TableConfigProvider):
|
|
9
|
+
"""
|
|
10
|
+
Table-level config provider to rename operation verbs for RPC exposure.
|
|
11
|
+
Does not change REST routes or internal canonical semantics.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
# Map canonical → alias (e.g., {"create": "register"})
|
|
15
|
+
__tigrbl_verb_aliases__: ClassVar[
|
|
16
|
+
Mapping[str, str] | Callable[[], Mapping[str, str]]
|
|
17
|
+
] = {}
|
|
18
|
+
|
|
19
|
+
# How to expose names:
|
|
20
|
+
# - "both" (default): expose canonical & alias
|
|
21
|
+
# - "alias_only": expose alias only
|
|
22
|
+
# - "canonical_only": ignore aliases, keep canonical only
|
|
23
|
+
__tigrbl_verb_alias_policy__: ClassVar[
|
|
24
|
+
Literal["both", "alias_only", "canonical_only"]
|
|
25
|
+
] = "both"
|
|
26
|
+
|
|
27
|
+
def __init_subclass__(cls, **kw):
|
|
28
|
+
super().__init_subclass__(**kw)
|
|
29
|
+
_VERB_ALIAS_PROVIDERS.add(cls)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def list_verb_alias_providers():
|
|
33
|
+
return sorted(_VERB_ALIAS_PROVIDERS, key=lambda c: c.__name__)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from typing import Callable, ClassVar, Mapping
|
|
2
|
+
|
|
3
|
+
from .table_config_provider import TableConfigProvider
|
|
4
|
+
|
|
5
|
+
_REQUEST_EXTRAS_PROVIDERS: set[type] = set()
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RequestExtrasProvider(TableConfigProvider):
|
|
9
|
+
"""Models that expose request-only virtual fields."""
|
|
10
|
+
|
|
11
|
+
__tigrbl_request_extras__: ClassVar[
|
|
12
|
+
Mapping[str, Mapping[str, object]]
|
|
13
|
+
| Callable[[], Mapping[str, Mapping[str, object]]]
|
|
14
|
+
] = {}
|
|
15
|
+
|
|
16
|
+
def __init_subclass__(cls, **kw):
|
|
17
|
+
super().__init_subclass__(**kw)
|
|
18
|
+
_REQUEST_EXTRAS_PROVIDERS.add(cls)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def list_request_extras_providers():
|
|
22
|
+
return sorted(_REQUEST_EXTRAS_PROVIDERS, key=lambda c: c.__name__)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from typing import Callable, ClassVar, Mapping
|
|
2
|
+
|
|
3
|
+
from .table_config_provider import TableConfigProvider
|
|
4
|
+
|
|
5
|
+
_RESPONSE_EXTRAS_PROVIDERS: set[type] = set()
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ResponseExtrasProvider(TableConfigProvider):
|
|
9
|
+
"""Models that expose response-only virtual fields."""
|
|
10
|
+
|
|
11
|
+
__tigrbl_response_extras__: ClassVar[
|
|
12
|
+
Mapping[str, Mapping[str, object]]
|
|
13
|
+
| Callable[[], Mapping[str, Mapping[str, object]]]
|
|
14
|
+
] = {}
|
|
15
|
+
|
|
16
|
+
def __init_subclass__(cls, **kw):
|
|
17
|
+
super().__init_subclass__(**kw)
|
|
18
|
+
_RESPONSE_EXTRAS_PROVIDERS.add(cls)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def list_response_extras_providers():
|
|
22
|
+
return sorted(_RESPONSE_EXTRAS_PROVIDERS, key=lambda c: c.__name__)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
_TABLE_CONFIG_PROVIDERS: set[type] = set()
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TableConfigProvider:
|
|
5
|
+
"""Marker base for table-level configuration providers."""
|
|
6
|
+
|
|
7
|
+
def __init_subclass__(cls, **kw):
|
|
8
|
+
super().__init_subclass__(**kw)
|
|
9
|
+
_TABLE_CONFIG_PROVIDERS.add(cls)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def list_table_config_providers():
|
|
13
|
+
return sorted(_TABLE_CONFIG_PROVIDERS, key=lambda c: c.__name__)
|
tigrbl/types/uuid.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# ── Standard Library ─────────────────────────────────────────────────────
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any
|
|
5
|
+
import uuid
|
|
6
|
+
|
|
7
|
+
# ── Third-party Dependencies ────────────────────────────────────────────
|
|
8
|
+
from sqlalchemy.types import TypeDecorator
|
|
9
|
+
|
|
10
|
+
# ── Local Package ───────────────────────────────────────────────────────
|
|
11
|
+
from ..deps.sqlalchemy import String, _PgUUID
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PgUUID(_PgUUID):
|
|
15
|
+
@property
|
|
16
|
+
def hex(self):
|
|
17
|
+
return self.as_uuid.hex
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SqliteUUID(TypeDecorator):
|
|
21
|
+
"""UUID type that stores hyphenated strings on SQLite to avoid numeric coercion."""
|
|
22
|
+
|
|
23
|
+
impl = String(36)
|
|
24
|
+
cache_ok = True
|
|
25
|
+
|
|
26
|
+
def __init__(self, as_uuid: bool = True):
|
|
27
|
+
super().__init__()
|
|
28
|
+
self.as_uuid = as_uuid
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def python_type(self) -> type:
|
|
32
|
+
return uuid.UUID if self.as_uuid else str
|
|
33
|
+
|
|
34
|
+
def load_dialect_impl(self, dialect) -> Any:
|
|
35
|
+
if dialect.name == "postgresql":
|
|
36
|
+
return dialect.type_descriptor(PgUUID(as_uuid=self.as_uuid))
|
|
37
|
+
return dialect.type_descriptor(String(36))
|
|
38
|
+
|
|
39
|
+
def process_bind_param(self, value: Any, dialect) -> Any:
|
|
40
|
+
if value is None:
|
|
41
|
+
return None
|
|
42
|
+
if self.as_uuid:
|
|
43
|
+
if not isinstance(value, uuid.UUID):
|
|
44
|
+
value = uuid.UUID(str(value))
|
|
45
|
+
return value if dialect.name == "postgresql" else str(value)
|
|
46
|
+
return str(value)
|
|
47
|
+
|
|
48
|
+
def process_result_value(self, value: Any, dialect) -> Any:
|
|
49
|
+
if value is None:
|
|
50
|
+
return None
|
|
51
|
+
if self.as_uuid:
|
|
52
|
+
if isinstance(value, uuid.UUID):
|
|
53
|
+
return value
|
|
54
|
+
return uuid.UUID(str(value))
|
|
55
|
+
return str(value)
|