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
tigrbl/ddl/__init__.py
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import inspect
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict, Iterable, Mapping, Optional, Sequence
|
|
7
|
+
from types import SimpleNamespace
|
|
8
|
+
|
|
9
|
+
from ..engine import resolver as _resolver
|
|
10
|
+
|
|
11
|
+
try: # pragma: no cover
|
|
12
|
+
from sqlalchemy import event, text
|
|
13
|
+
from sqlalchemy.engine import Engine
|
|
14
|
+
from sqlalchemy.schema import CreateSchema # type: ignore
|
|
15
|
+
except Exception: # pragma: no cover
|
|
16
|
+
event = text = CreateSchema = None # type: ignore
|
|
17
|
+
Engine = object # type: ignore
|
|
18
|
+
|
|
19
|
+
from ..config.constants import __SAFE_IDENT__
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"register_sqlite_attach",
|
|
23
|
+
"ensure_schemas",
|
|
24
|
+
"bootstrap_dbschema",
|
|
25
|
+
"sqlite_default_attach_map",
|
|
26
|
+
"initialize",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
# SQLite helpers
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _quote_ident_sqlite(name: str) -> str:
|
|
36
|
+
if __SAFE_IDENT__.match(name or ""):
|
|
37
|
+
return name
|
|
38
|
+
return '"' + (name or "").replace('"', '""') + '"'
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _attached_names_sqlite(dbapi_conn: Any) -> set[str]:
|
|
42
|
+
names: set[str] = set()
|
|
43
|
+
cur = None
|
|
44
|
+
try:
|
|
45
|
+
cur = dbapi_conn.cursor()
|
|
46
|
+
cur.execute("PRAGMA database_list")
|
|
47
|
+
for row in cur.fetchall():
|
|
48
|
+
names.add(str(row[1]))
|
|
49
|
+
finally:
|
|
50
|
+
try:
|
|
51
|
+
cur and cur.close()
|
|
52
|
+
except Exception:
|
|
53
|
+
pass
|
|
54
|
+
return names
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _attach_sqlite_dbapi(dbapi_conn: Any, attachments: Mapping[str, str]) -> None:
|
|
58
|
+
cur = None
|
|
59
|
+
try:
|
|
60
|
+
existing = _attached_names_sqlite(dbapi_conn)
|
|
61
|
+
cur = dbapi_conn.cursor()
|
|
62
|
+
try:
|
|
63
|
+
cur.execute("PRAGMA foreign_keys=ON")
|
|
64
|
+
except Exception:
|
|
65
|
+
pass
|
|
66
|
+
for schema, path in (attachments or {}).items():
|
|
67
|
+
if not path or schema in existing:
|
|
68
|
+
continue
|
|
69
|
+
ident = _quote_ident_sqlite(schema)
|
|
70
|
+
try:
|
|
71
|
+
cur.execute(f"ATTACH DATABASE ? AS {ident}", (path,))
|
|
72
|
+
except Exception:
|
|
73
|
+
cur.execute(f"ATTACH DATABASE '{path}' AS {ident}") # nosec: application-provided paths
|
|
74
|
+
finally:
|
|
75
|
+
try:
|
|
76
|
+
cur and cur.close()
|
|
77
|
+
except Exception:
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
# Public helpers
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def sqlite_default_attach_map(engine: Engine, schemas: Iterable[str]) -> Dict[str, str]:
|
|
87
|
+
"""Return a deterministic SQLite ATTACH map for ``schemas``."""
|
|
88
|
+
db = getattr(getattr(engine, "url", None), "database", None) or ":memory:"
|
|
89
|
+
if db == ":memory:" or str(db).startswith("file::memory:"):
|
|
90
|
+
return {s: ":memory:" for s in schemas}
|
|
91
|
+
p = Path(db)
|
|
92
|
+
suffix = p.suffix if p.suffix else ".db"
|
|
93
|
+
return {s: str(p.with_name(f"{p.stem}__{s}{suffix}")) for s in schemas}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def register_sqlite_attach(engine: Engine, attachments: Mapping[str, str]) -> Any:
|
|
97
|
+
if (
|
|
98
|
+
not hasattr(engine, "dialect")
|
|
99
|
+
or getattr(engine.dialect, "name", "") != "sqlite"
|
|
100
|
+
):
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
def _connect_listener(dbapi_conn, _): # type: ignore[override]
|
|
104
|
+
try:
|
|
105
|
+
_attach_sqlite_dbapi(dbapi_conn, attachments)
|
|
106
|
+
except Exception:
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
event.listen(engine, "connect", _connect_listener)
|
|
110
|
+
return _connect_listener
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def ensure_schemas(engine: Engine, schemas: Iterable[str]) -> Sequence[str]:
|
|
114
|
+
if not schemas:
|
|
115
|
+
return tuple()
|
|
116
|
+
if not hasattr(engine, "dialect"):
|
|
117
|
+
return tuple(schemas)
|
|
118
|
+
|
|
119
|
+
dialect = getattr(engine.dialect, "name", "")
|
|
120
|
+
attempted: list[str] = []
|
|
121
|
+
|
|
122
|
+
if dialect == "sqlite":
|
|
123
|
+
return tuple()
|
|
124
|
+
|
|
125
|
+
if text is None: # pragma: no cover
|
|
126
|
+
raise RuntimeError("SQLAlchemy is required for ensure_schemas")
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
with engine.begin() as conn:
|
|
130
|
+
for name in dict.fromkeys(schemas).keys():
|
|
131
|
+
if not name:
|
|
132
|
+
continue
|
|
133
|
+
attempted.append(name)
|
|
134
|
+
try:
|
|
135
|
+
conn.execute(CreateSchema(name)) # type: ignore[arg-type]
|
|
136
|
+
except Exception:
|
|
137
|
+
try:
|
|
138
|
+
if dialect in ("postgresql", "redshift"):
|
|
139
|
+
conn.execute(text(f'CREATE SCHEMA IF NOT EXISTS "{name}"'))
|
|
140
|
+
elif dialect in ("mysql", "mariadb"):
|
|
141
|
+
conn.execute(text(f"CREATE SCHEMA IF NOT EXISTS `{name}`"))
|
|
142
|
+
elif dialect in ("mssql", "sqlserver"):
|
|
143
|
+
conn.execute(
|
|
144
|
+
text(
|
|
145
|
+
"IF NOT EXISTS (SELECT * FROM sys.schemas WHERE name = :n) "
|
|
146
|
+
"EXEC('CREATE SCHEMA ' + QUOTENAME(:n))"
|
|
147
|
+
),
|
|
148
|
+
{"n": name},
|
|
149
|
+
)
|
|
150
|
+
else:
|
|
151
|
+
conn.execute(text(f"CREATE SCHEMA IF NOT EXISTS {name}"))
|
|
152
|
+
except Exception:
|
|
153
|
+
pass
|
|
154
|
+
except Exception:
|
|
155
|
+
pass
|
|
156
|
+
|
|
157
|
+
return tuple(attempted)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def bootstrap_dbschema(
|
|
161
|
+
engine: Engine,
|
|
162
|
+
*,
|
|
163
|
+
schemas: Optional[Iterable[str]] = None,
|
|
164
|
+
sqlite_attachments: Optional[Mapping[str, str]] = None,
|
|
165
|
+
immediate: bool = True,
|
|
166
|
+
) -> Dict[str, Any]:
|
|
167
|
+
attempted = tuple()
|
|
168
|
+
if schemas:
|
|
169
|
+
attempted = ensure_schemas(engine, schemas)
|
|
170
|
+
|
|
171
|
+
listener = None
|
|
172
|
+
if sqlite_attachments:
|
|
173
|
+
listener = register_sqlite_attach(engine, sqlite_attachments)
|
|
174
|
+
if immediate and getattr(engine.dialect, "name", "") == "sqlite":
|
|
175
|
+
try:
|
|
176
|
+
with engine.connect() as conn:
|
|
177
|
+
dbapi = getattr(conn, "connection", None)
|
|
178
|
+
if dbapi is not None:
|
|
179
|
+
_attach_sqlite_dbapi(dbapi, sqlite_attachments)
|
|
180
|
+
except Exception:
|
|
181
|
+
pass
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
"attempted_schemas": attempted,
|
|
185
|
+
"sqlite_attachments": dict(sqlite_attachments or {}),
|
|
186
|
+
"listener": listener,
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# ---------------------------------------------------------------------------
|
|
191
|
+
# Internal creation helper
|
|
192
|
+
# ---------------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _create_all_on_bind(
|
|
196
|
+
bind,
|
|
197
|
+
*,
|
|
198
|
+
schemas: Iterable[str] | None = None,
|
|
199
|
+
sqlite_attachments: Mapping[str, str] | None = None,
|
|
200
|
+
tables: Iterable[Any] | None = None,
|
|
201
|
+
) -> None:
|
|
202
|
+
engine = getattr(bind, "engine", bind)
|
|
203
|
+
tables = list(tables or [])
|
|
204
|
+
|
|
205
|
+
schema_names = set(schemas or [])
|
|
206
|
+
for t in tables:
|
|
207
|
+
if getattr(t, "schema", None):
|
|
208
|
+
schema_names.add(t.schema)
|
|
209
|
+
|
|
210
|
+
attachments = sqlite_attachments
|
|
211
|
+
if attachments is None and getattr(engine.dialect, "name", "") == "sqlite":
|
|
212
|
+
if schema_names:
|
|
213
|
+
attachments = sqlite_default_attach_map(engine, schema_names)
|
|
214
|
+
|
|
215
|
+
if attachments:
|
|
216
|
+
bootstrap_dbschema(
|
|
217
|
+
engine,
|
|
218
|
+
schemas=schema_names,
|
|
219
|
+
sqlite_attachments=attachments,
|
|
220
|
+
immediate=True,
|
|
221
|
+
)
|
|
222
|
+
else:
|
|
223
|
+
ensure_schemas(engine, schema_names)
|
|
224
|
+
|
|
225
|
+
by_meta: dict[Any, list[Any]] = {}
|
|
226
|
+
for t in tables:
|
|
227
|
+
by_meta.setdefault(t.metadata, []).append(t)
|
|
228
|
+
for md, group in by_meta.items():
|
|
229
|
+
md.create_all(bind=bind, checkfirst=True, tables=group)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
# ---------------------------------------------------------------------------
|
|
233
|
+
# Public initialize
|
|
234
|
+
# ---------------------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def initialize(
|
|
238
|
+
obj: Any,
|
|
239
|
+
*,
|
|
240
|
+
schemas: Iterable[str] | None = None,
|
|
241
|
+
sqlite_attachments: Mapping[str, str] | None = None,
|
|
242
|
+
tables: Iterable[Any] | None = None,
|
|
243
|
+
):
|
|
244
|
+
if getattr(obj, "_ddl_executed", False):
|
|
245
|
+
return
|
|
246
|
+
|
|
247
|
+
ts = list(tables or [])
|
|
248
|
+
if not ts:
|
|
249
|
+
if hasattr(obj, "_collect_tables"):
|
|
250
|
+
ts = list(obj._collect_tables()) # type: ignore[attr-defined]
|
|
251
|
+
elif hasattr(obj, "__table__"):
|
|
252
|
+
ts = [obj.__table__] # type: ignore[attr-defined]
|
|
253
|
+
|
|
254
|
+
kwargs: Dict[str, Any] = {}
|
|
255
|
+
if hasattr(obj, "_collect_tables"):
|
|
256
|
+
kwargs["api"] = obj
|
|
257
|
+
elif hasattr(obj, "__table__"):
|
|
258
|
+
kwargs["model"] = obj
|
|
259
|
+
|
|
260
|
+
prov = _resolver.resolve_provider(**kwargs)
|
|
261
|
+
if prov is None:
|
|
262
|
+
raise ValueError("Engine provider is not configured")
|
|
263
|
+
|
|
264
|
+
def _bootstrap(db):
|
|
265
|
+
bind = db.get_bind() if hasattr(db, "get_bind") else db
|
|
266
|
+
_create_all_on_bind(
|
|
267
|
+
bind,
|
|
268
|
+
schemas=schemas,
|
|
269
|
+
sqlite_attachments=sqlite_attachments,
|
|
270
|
+
tables=ts,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
if hasattr(obj, "models"):
|
|
274
|
+
tables_map = {
|
|
275
|
+
name: getattr(m, "__table__", None)
|
|
276
|
+
for name, m in getattr(obj, "models").items()
|
|
277
|
+
if hasattr(m, "__table__")
|
|
278
|
+
}
|
|
279
|
+
existing = getattr(obj, "tables", None)
|
|
280
|
+
if isinstance(existing, dict):
|
|
281
|
+
existing.update(tables_map)
|
|
282
|
+
elif existing is not None:
|
|
283
|
+
for k, v in tables_map.items():
|
|
284
|
+
setattr(existing, k, v)
|
|
285
|
+
else:
|
|
286
|
+
setattr(obj, "tables", SimpleNamespace(**tables_map))
|
|
287
|
+
|
|
288
|
+
try:
|
|
289
|
+
asyncio.get_running_loop()
|
|
290
|
+
except RuntimeError:
|
|
291
|
+
# No running event loop; fall back to fully synchronous bootstrap
|
|
292
|
+
with next(prov.get_db()) as db:
|
|
293
|
+
_bootstrap(db)
|
|
294
|
+
setattr(obj, "_ddl_executed", True)
|
|
295
|
+
return
|
|
296
|
+
else:
|
|
297
|
+
# If we're already inside an event loop but the provider is synchronous
|
|
298
|
+
# (i.e. ``get_db`` is neither coroutine nor async generator), we can
|
|
299
|
+
# bootstrap synchronously as well. This mirrors previous "initialize_sync"
|
|
300
|
+
# behaviour and allows ``initialize()`` to be invoked without ``await``
|
|
301
|
+
# from async contexts when using sync engines.
|
|
302
|
+
if not inspect.iscoroutinefunction(
|
|
303
|
+
prov.get_db
|
|
304
|
+
) and not inspect.isasyncgenfunction(prov.get_db):
|
|
305
|
+
with next(prov.get_db()) as db:
|
|
306
|
+
_bootstrap(db)
|
|
307
|
+
setattr(obj, "_ddl_executed", True)
|
|
308
|
+
|
|
309
|
+
class _Completed:
|
|
310
|
+
def __await__(self): # pragma: no cover - trivial
|
|
311
|
+
if False:
|
|
312
|
+
yield None
|
|
313
|
+
return None
|
|
314
|
+
|
|
315
|
+
return _Completed()
|
|
316
|
+
|
|
317
|
+
async def _inner():
|
|
318
|
+
if inspect.isasyncgenfunction(prov.get_db):
|
|
319
|
+
async for adb in prov.get_db():
|
|
320
|
+
await adb.run_sync(_bootstrap)
|
|
321
|
+
break
|
|
322
|
+
else:
|
|
323
|
+
gen = prov.get_db()
|
|
324
|
+
db = next(gen)
|
|
325
|
+
try:
|
|
326
|
+
if hasattr(db, "run_sync"):
|
|
327
|
+
await db.run_sync(_bootstrap)
|
|
328
|
+
else:
|
|
329
|
+
bind = db.get_bind()
|
|
330
|
+
await asyncio.to_thread(
|
|
331
|
+
_create_all_on_bind,
|
|
332
|
+
bind,
|
|
333
|
+
schemas=schemas,
|
|
334
|
+
sqlite_attachments=sqlite_attachments,
|
|
335
|
+
tables=ts,
|
|
336
|
+
)
|
|
337
|
+
finally:
|
|
338
|
+
try:
|
|
339
|
+
next(gen)
|
|
340
|
+
except StopIteration:
|
|
341
|
+
pass
|
|
342
|
+
setattr(obj, "_ddl_executed", True)
|
|
343
|
+
|
|
344
|
+
return _inner()
|
tigrbl/decorators.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Compatibility layer for decorator imports."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .op.decorators import * # noqa: F401,F403
|
|
6
|
+
from .hook.decorators import * # noqa: F401,F403
|
|
7
|
+
from .schema.decorators import schema_ctx
|
|
8
|
+
from .engine.decorators import engine_ctx
|
|
9
|
+
from .op.decorators import __all__ as _op_all
|
|
10
|
+
from .hook.decorators import __all__ as _hook_all
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
*_op_all,
|
|
14
|
+
*_hook_all,
|
|
15
|
+
"schema_ctx",
|
|
16
|
+
"engine_ctx",
|
|
17
|
+
]
|
tigrbl/deps/__init__.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# ── Third-party Dependencies Re-exports ─────────────────────────────────
|
|
2
|
+
"""
|
|
3
|
+
Centralized third-party dependency imports for tigrbl.
|
|
4
|
+
|
|
5
|
+
This module provides a single location for all third-party dependencies,
|
|
6
|
+
making it easier to manage versions and potential replacements.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
# Re-export all SQLAlchemy dependencies
|
|
10
|
+
from .sqlalchemy import relationship # noqa: F401
|
|
11
|
+
from .sqlalchemy import * # noqa: F403, F401
|
|
12
|
+
|
|
13
|
+
# Re-export all Pydantic dependencies
|
|
14
|
+
from .pydantic import * # noqa: F403, F401
|
|
15
|
+
|
|
16
|
+
# Re-export all FastAPI dependencies
|
|
17
|
+
from .fastapi import * # noqa: F403, F401
|
|
18
|
+
|
|
19
|
+
# Note: starlette.py is reserved for future Starlette-specific imports
|
|
20
|
+
# if we need to import directly from Starlette rather than through FastAPI
|
tigrbl/deps/fastapi.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# ── FastAPI Imports ─────────────────────────────────────────────────────
|
|
2
|
+
from fastapi import (
|
|
3
|
+
APIRouter,
|
|
4
|
+
FastAPI,
|
|
5
|
+
Security,
|
|
6
|
+
Depends,
|
|
7
|
+
Request,
|
|
8
|
+
Response,
|
|
9
|
+
Path as FastAPIPath,
|
|
10
|
+
Body,
|
|
11
|
+
HTTPException,
|
|
12
|
+
)
|
|
13
|
+
from fastapi.responses import FileResponse
|
|
14
|
+
from pathlib import Path as FilePath
|
|
15
|
+
|
|
16
|
+
Router = APIRouter
|
|
17
|
+
Path = FastAPIPath
|
|
18
|
+
|
|
19
|
+
FAVICON_PATH = FilePath(__file__).with_name("favicon.svg")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def App(*args, **kwargs):
|
|
23
|
+
app = FastAPI(*args, **kwargs)
|
|
24
|
+
|
|
25
|
+
@app.get("/favicon.ico", include_in_schema=False)
|
|
26
|
+
async def favicon() -> FileResponse: # pragma: no cover - simple static route
|
|
27
|
+
return FileResponse(FAVICON_PATH, media_type="image/svg+xml")
|
|
28
|
+
|
|
29
|
+
return app
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ── Public Exports ───────────────────────────────────────────────────────
|
|
33
|
+
__all__ = [
|
|
34
|
+
"APIRouter",
|
|
35
|
+
"Router",
|
|
36
|
+
"FastAPI",
|
|
37
|
+
"Security",
|
|
38
|
+
"Depends",
|
|
39
|
+
"Request",
|
|
40
|
+
"Response",
|
|
41
|
+
"Path",
|
|
42
|
+
"Body",
|
|
43
|
+
"HTTPException",
|
|
44
|
+
"App",
|
|
45
|
+
]
|
tigrbl/deps/favicon.svg
ADDED
tigrbl/deps/jinja.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
try: # pragma: no cover - optional dependency
|
|
4
|
+
from jinja2 import (
|
|
5
|
+
Environment,
|
|
6
|
+
FileSystemLoader,
|
|
7
|
+
PackageLoader,
|
|
8
|
+
ChoiceLoader,
|
|
9
|
+
select_autoescape,
|
|
10
|
+
TemplateNotFound,
|
|
11
|
+
)
|
|
12
|
+
except Exception: # pragma: no cover
|
|
13
|
+
Environment = None # type: ignore
|
|
14
|
+
FileSystemLoader = None # type: ignore
|
|
15
|
+
PackageLoader = None # type: ignore
|
|
16
|
+
ChoiceLoader = None # type: ignore
|
|
17
|
+
select_autoescape = None # type: ignore
|
|
18
|
+
TemplateNotFound = None # type: ignore
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"Environment",
|
|
22
|
+
"FileSystemLoader",
|
|
23
|
+
"PackageLoader",
|
|
24
|
+
"ChoiceLoader",
|
|
25
|
+
"select_autoescape",
|
|
26
|
+
"TemplateNotFound",
|
|
27
|
+
]
|
tigrbl/deps/pydantic.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# ── Pydantic Imports ────────────────────────────────────────────────────
|
|
2
|
+
from pydantic import BaseModel, Field, ValidationError
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
# ── Public Exports ───────────────────────────────────────────────────────
|
|
6
|
+
__all__ = [
|
|
7
|
+
"BaseModel",
|
|
8
|
+
"Field",
|
|
9
|
+
"ValidationError",
|
|
10
|
+
]
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# ── SQLAlchemy Core ──────────────────────────────────────────────────────
|
|
2
|
+
from sqlalchemy import (
|
|
3
|
+
Boolean,
|
|
4
|
+
Column,
|
|
5
|
+
DateTime as _DateTime,
|
|
6
|
+
Enum as SAEnum,
|
|
7
|
+
Text,
|
|
8
|
+
ForeignKey,
|
|
9
|
+
Index,
|
|
10
|
+
Integer,
|
|
11
|
+
JSON,
|
|
12
|
+
Numeric,
|
|
13
|
+
String,
|
|
14
|
+
LargeBinary,
|
|
15
|
+
UniqueConstraint,
|
|
16
|
+
CheckConstraint,
|
|
17
|
+
create_engine,
|
|
18
|
+
event,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# ── SQLAlchemy PostgreSQL Dialect ────────────────────────────────────────
|
|
22
|
+
from sqlalchemy.dialects.postgresql import (
|
|
23
|
+
ARRAY,
|
|
24
|
+
ENUM as PgEnum,
|
|
25
|
+
JSONB,
|
|
26
|
+
UUID as _PgUUID,
|
|
27
|
+
TSVECTOR,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# ── SQLAlchemy ORM ───────────────────────────────────────────────────────
|
|
31
|
+
from sqlalchemy.orm import (
|
|
32
|
+
Mapped,
|
|
33
|
+
declarative_mixin,
|
|
34
|
+
declared_attr,
|
|
35
|
+
foreign,
|
|
36
|
+
mapped_column,
|
|
37
|
+
relationship,
|
|
38
|
+
remote,
|
|
39
|
+
column_property,
|
|
40
|
+
Session,
|
|
41
|
+
sessionmaker,
|
|
42
|
+
InstrumentedAttribute,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
from sqlalchemy.pool import StaticPool
|
|
46
|
+
|
|
47
|
+
# ── SQLAlchemy Extensions ────────────────────────────────────────────────
|
|
48
|
+
from sqlalchemy.ext.mutable import MutableDict, MutableList
|
|
49
|
+
from sqlalchemy.ext.hybrid import hybrid_property
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ── Public Exports ───────────────────────────────────────────────────────
|
|
53
|
+
__all__ = [
|
|
54
|
+
# Core types
|
|
55
|
+
"Boolean",
|
|
56
|
+
"Column",
|
|
57
|
+
"_DateTime",
|
|
58
|
+
"SAEnum",
|
|
59
|
+
"Text",
|
|
60
|
+
"ForeignKey",
|
|
61
|
+
"Index",
|
|
62
|
+
"Integer",
|
|
63
|
+
"JSON",
|
|
64
|
+
"Numeric",
|
|
65
|
+
"String",
|
|
66
|
+
"LargeBinary",
|
|
67
|
+
"UniqueConstraint",
|
|
68
|
+
"CheckConstraint",
|
|
69
|
+
"create_engine",
|
|
70
|
+
"StaticPool",
|
|
71
|
+
"event",
|
|
72
|
+
# PostgreSQL dialect
|
|
73
|
+
"ARRAY",
|
|
74
|
+
"PgEnum",
|
|
75
|
+
"JSONB",
|
|
76
|
+
"_PgUUID",
|
|
77
|
+
"TSVECTOR",
|
|
78
|
+
# ORM
|
|
79
|
+
"Mapped",
|
|
80
|
+
"declarative_mixin",
|
|
81
|
+
"declared_attr",
|
|
82
|
+
"foreign",
|
|
83
|
+
"mapped_column",
|
|
84
|
+
"relationship",
|
|
85
|
+
"remote",
|
|
86
|
+
"column_property",
|
|
87
|
+
"Session",
|
|
88
|
+
"sessionmaker",
|
|
89
|
+
"InstrumentedAttribute",
|
|
90
|
+
# Extensions
|
|
91
|
+
"MutableDict",
|
|
92
|
+
"MutableList",
|
|
93
|
+
"hybrid_property",
|
|
94
|
+
]
|
tigrbl/deps/starlette.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
try: # pragma: no cover - optional runtime dependency
|
|
4
|
+
from starlette.background import BackgroundTask
|
|
5
|
+
from starlette.requests import Request
|
|
6
|
+
from starlette.responses import (
|
|
7
|
+
Response,
|
|
8
|
+
JSONResponse,
|
|
9
|
+
HTMLResponse,
|
|
10
|
+
PlainTextResponse,
|
|
11
|
+
StreamingResponse,
|
|
12
|
+
FileResponse,
|
|
13
|
+
RedirectResponse,
|
|
14
|
+
)
|
|
15
|
+
except Exception: # pragma: no cover
|
|
16
|
+
BackgroundTask = None # type: ignore
|
|
17
|
+
Request = None # type: ignore
|
|
18
|
+
Response = None # type: ignore
|
|
19
|
+
JSONResponse = None # type: ignore
|
|
20
|
+
HTMLResponse = None # type: ignore
|
|
21
|
+
PlainTextResponse = None # type: ignore
|
|
22
|
+
StreamingResponse = None # type: ignore
|
|
23
|
+
FileResponse = None # type: ignore
|
|
24
|
+
RedirectResponse = None # type: ignore
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"BackgroundTask",
|
|
28
|
+
"Request",
|
|
29
|
+
"Response",
|
|
30
|
+
"JSONResponse",
|
|
31
|
+
"HTMLResponse",
|
|
32
|
+
"PlainTextResponse",
|
|
33
|
+
"StreamingResponse",
|
|
34
|
+
"FileResponse",
|
|
35
|
+
"RedirectResponse",
|
|
36
|
+
]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Engine utilities for collecting and binding database providers."""
|
|
2
|
+
|
|
3
|
+
from .bind import bind, install_from_objects
|
|
4
|
+
from .collect import collect_engine_config
|
|
5
|
+
from .builders import (
|
|
6
|
+
async_postgres_engine,
|
|
7
|
+
async_sqlite_engine,
|
|
8
|
+
blocking_postgres_engine,
|
|
9
|
+
blocking_sqlite_engine,
|
|
10
|
+
HybridSession,
|
|
11
|
+
)
|
|
12
|
+
from ._engine import Engine
|
|
13
|
+
from .shortcuts import engine
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"collect_engine_config",
|
|
17
|
+
"bind",
|
|
18
|
+
"install_from_objects",
|
|
19
|
+
"blocking_sqlite_engine",
|
|
20
|
+
"blocking_postgres_engine",
|
|
21
|
+
"async_sqlite_engine",
|
|
22
|
+
"async_postgres_engine",
|
|
23
|
+
"HybridSession",
|
|
24
|
+
"Engine",
|
|
25
|
+
"engine",
|
|
26
|
+
]
|