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,49 @@
|
|
|
1
|
+
|
|
2
|
+
# Tigrbl Engine Plugins
|
|
3
|
+
|
|
4
|
+
Tigrbl supports external engine kinds via an entry-point group: `tigrbl.engine`.
|
|
5
|
+
|
|
6
|
+
An external package registers itself by exposing a `register()` function and
|
|
7
|
+
declaring an entry point:
|
|
8
|
+
|
|
9
|
+
```toml
|
|
10
|
+
[project.entry-points."tigrbl.engine"]
|
|
11
|
+
duckdb = "tigrbl_engine_duckdb.plugin:register"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Inside `register()` call:
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from tigrbl.engine.registry import register_engine
|
|
18
|
+
from .builder import duckdb_engine, duckdb_capabilities
|
|
19
|
+
|
|
20
|
+
def register():
|
|
21
|
+
register_engine("duckdb", duckdb_engine, duckdb_capabilities)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
At runtime, `EngineSpec(kind="duckdb")` will look up the registration and use
|
|
25
|
+
the external builder or raise a helpful `RuntimeError` if the plugin is not
|
|
26
|
+
installed.
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
## Capabilities / supports()
|
|
30
|
+
|
|
31
|
+
External engines should expose a capabilities callable when registering:
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from tigrbl.engine.registry import register_engine
|
|
35
|
+
|
|
36
|
+
def my_engine_builder(...): ...
|
|
37
|
+
def my_engine_capabilities(**kw):
|
|
38
|
+
# Return a dict describing what the engine supports
|
|
39
|
+
return {
|
|
40
|
+
"transactional": True,
|
|
41
|
+
"isolation_levels": {"read_committed","serializable"},
|
|
42
|
+
"read_only_enforced": True,
|
|
43
|
+
"async_native": False,
|
|
44
|
+
"engine": "myengine",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
def register():
|
|
48
|
+
register_engine("myengine", my_engine_builder, capabilities=my_engine_capabilities)
|
|
49
|
+
```
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
# tigrbl/v3/engine/engine_spec.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Optional, Mapping, Union, Any, Tuple
|
|
6
|
+
from urllib.parse import urlsplit, urlunsplit
|
|
7
|
+
|
|
8
|
+
from ._engine import Engine, Provider, SessionFactory
|
|
9
|
+
from .builders import (
|
|
10
|
+
async_postgres_engine,
|
|
11
|
+
async_sqlite_engine,
|
|
12
|
+
blocking_postgres_engine,
|
|
13
|
+
blocking_sqlite_engine,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
# The value stored by @engine_ctx on App/API/Table/Op.
|
|
17
|
+
EngineCfg = Union[str, Mapping[str, object], "EngineSpec", Provider, Engine]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class EngineSpec:
|
|
22
|
+
"""
|
|
23
|
+
Canonical, normalized engine spec → Provider factory.
|
|
24
|
+
|
|
25
|
+
Input comes from @engine_ctx attached to an App/API/Table/Op:
|
|
26
|
+
• DSN string:
|
|
27
|
+
"sqlite://:memory:" ,
|
|
28
|
+
"sqlite:///./file.db" ,
|
|
29
|
+
"sqlite+aiosqlite:///./file.db" ,
|
|
30
|
+
"postgresql://user:pwd@host:5432/db" ,
|
|
31
|
+
"postgresql+asyncpg://user:pwd@host:5432/db"
|
|
32
|
+
• Mapping (recommended for clarity/portability):
|
|
33
|
+
{"kind":"sqlite","async":True,"path":"./file.db"}
|
|
34
|
+
{"kind":"postgres","async":True,"host":"db","db":"app_db",...}
|
|
35
|
+
{<external kind> ...} # for plugin engines
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
# normalized
|
|
39
|
+
kind: Optional[str] = None # "sqlite" | "postgres" | <external>
|
|
40
|
+
async_: bool = False
|
|
41
|
+
|
|
42
|
+
# canonical DSN (optional) and raw mapping (for external engines)
|
|
43
|
+
dsn: Optional[str] = None
|
|
44
|
+
mapping: Optional[Mapping[str, object]] = None
|
|
45
|
+
|
|
46
|
+
# sqlite
|
|
47
|
+
path: Optional[str] = None # file path (None → memory)
|
|
48
|
+
memory: bool = False
|
|
49
|
+
|
|
50
|
+
# postgres
|
|
51
|
+
user: Optional[str] = None
|
|
52
|
+
pwd: Optional[str] = field(default=None, repr=False)
|
|
53
|
+
host: Optional[str] = None
|
|
54
|
+
port: Optional[int] = None
|
|
55
|
+
name: Optional[str] = None
|
|
56
|
+
pool_size: int = 10
|
|
57
|
+
max: int = 20 # max_overflow (sync) or max_size (async)
|
|
58
|
+
|
|
59
|
+
# ---------- parsing ----------
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def from_any(x: EngineCfg | None) -> Optional["EngineSpec"]:
|
|
63
|
+
"""Parse DSN/Mapping/Provider/Engine into an :class:`EngineSpec`."""
|
|
64
|
+
if x is None:
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
if isinstance(x, EngineSpec):
|
|
68
|
+
return x
|
|
69
|
+
|
|
70
|
+
if isinstance(x, Provider):
|
|
71
|
+
return x.spec
|
|
72
|
+
|
|
73
|
+
if isinstance(x, Engine):
|
|
74
|
+
return x.spec
|
|
75
|
+
|
|
76
|
+
# DSN string
|
|
77
|
+
if isinstance(x, str):
|
|
78
|
+
s = x.strip()
|
|
79
|
+
# sqlite async
|
|
80
|
+
if s.startswith("sqlite+aiosqlite://") or s.startswith("sqlite+aiosqlite:"):
|
|
81
|
+
path = urlsplit(s).path or ""
|
|
82
|
+
if s.startswith("sqlite+aiosqlite:////"):
|
|
83
|
+
if path.startswith("//"):
|
|
84
|
+
path = path[1:]
|
|
85
|
+
path = path or None
|
|
86
|
+
else:
|
|
87
|
+
path = path.lstrip("/") or None
|
|
88
|
+
mem = path in {None, ":memory:", "/:memory:"} or s.endswith(":memory:")
|
|
89
|
+
return EngineSpec(
|
|
90
|
+
kind="sqlite", async_=True, path=path, memory=mem, dsn=s
|
|
91
|
+
)
|
|
92
|
+
# sqlite sync
|
|
93
|
+
if s.startswith("sqlite://") or s.startswith("sqlite:"):
|
|
94
|
+
# handle sqlite://:memory: and sqlite:///file.db
|
|
95
|
+
if s.startswith("sqlite://:memory:") or s.endswith(":memory:"):
|
|
96
|
+
return EngineSpec(
|
|
97
|
+
kind="sqlite", async_=False, path=None, memory=True, dsn=s
|
|
98
|
+
)
|
|
99
|
+
# Take the path part after scheme; urlsplit handles both sqlite:// and sqlite:/// forms
|
|
100
|
+
p = urlsplit(s).path or ""
|
|
101
|
+
if s.startswith("sqlite:////"):
|
|
102
|
+
if p.startswith("//"):
|
|
103
|
+
p = p[1:]
|
|
104
|
+
p = p or None
|
|
105
|
+
else:
|
|
106
|
+
p = p.lstrip("/") or None
|
|
107
|
+
mem = p is None
|
|
108
|
+
return EngineSpec(
|
|
109
|
+
kind="sqlite", async_=False, path=p, memory=mem, dsn=s
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# postgres async
|
|
113
|
+
if s.startswith("postgresql+asyncpg://") or s.startswith(
|
|
114
|
+
"postgres+asyncpg://"
|
|
115
|
+
):
|
|
116
|
+
return EngineSpec(kind="postgres", async_=True, dsn=s)
|
|
117
|
+
# postgres sync
|
|
118
|
+
if s.startswith("postgresql://") or s.startswith("postgres://"):
|
|
119
|
+
return EngineSpec(kind="postgres", async_=False, dsn=s)
|
|
120
|
+
|
|
121
|
+
raise ValueError(f"Unsupported DSN: {s}")
|
|
122
|
+
|
|
123
|
+
# Mapping
|
|
124
|
+
m = x # type: ignore[assignment]
|
|
125
|
+
|
|
126
|
+
# Helpers
|
|
127
|
+
def _get_bool(key: str, *aliases: str, default: bool = False) -> bool:
|
|
128
|
+
for k in (key, *aliases):
|
|
129
|
+
if k in m:
|
|
130
|
+
return bool(m[k]) # type: ignore[index]
|
|
131
|
+
return default
|
|
132
|
+
|
|
133
|
+
def _get_str(
|
|
134
|
+
key: str, *aliases: str, default: Optional[str] = None
|
|
135
|
+
) -> Optional[str]:
|
|
136
|
+
for k in (key, *aliases):
|
|
137
|
+
if k in m and m[k] is not None:
|
|
138
|
+
return str(m[k]) # type: ignore[index]
|
|
139
|
+
return default
|
|
140
|
+
|
|
141
|
+
def _get_int(
|
|
142
|
+
key: str, *aliases: str, default: Optional[int] = None
|
|
143
|
+
) -> Optional[int]:
|
|
144
|
+
for k in (key, *aliases):
|
|
145
|
+
if k in m and m[k] is not None:
|
|
146
|
+
try:
|
|
147
|
+
return int(m[k]) # type: ignore[index]
|
|
148
|
+
except Exception:
|
|
149
|
+
return default
|
|
150
|
+
return default
|
|
151
|
+
|
|
152
|
+
k = str(m.get("kind", m.get("engine", ""))).lower() # type: ignore[index]
|
|
153
|
+
if k == "sqlite":
|
|
154
|
+
async_ = _get_bool("async", "async_", default=False)
|
|
155
|
+
path = _get_str("path")
|
|
156
|
+
memory = (
|
|
157
|
+
_get_bool("memory", default=False)
|
|
158
|
+
or (str(m.get("mode", "")).lower() == "memory")
|
|
159
|
+
or (path is None)
|
|
160
|
+
)
|
|
161
|
+
return EngineSpec(
|
|
162
|
+
kind="sqlite",
|
|
163
|
+
async_=async_,
|
|
164
|
+
path=path,
|
|
165
|
+
memory=memory,
|
|
166
|
+
dsn=_get_str("dsn", "url"),
|
|
167
|
+
mapping=m,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
if k == "postgres":
|
|
171
|
+
async_ = _get_bool("async", "async_", default=False)
|
|
172
|
+
return EngineSpec(
|
|
173
|
+
kind="postgres",
|
|
174
|
+
async_=async_,
|
|
175
|
+
user=_get_str("user"),
|
|
176
|
+
pwd=_get_str("pwd", "password"),
|
|
177
|
+
host=_get_str("host"),
|
|
178
|
+
port=_get_int("port"),
|
|
179
|
+
name=_get_str("db", "name"),
|
|
180
|
+
pool_size=_get_int("pool_size", default=10) or 10,
|
|
181
|
+
max=_get_int("max", "max_overflow", "max_size", default=20) or 20,
|
|
182
|
+
dsn=_get_str("dsn", "url"),
|
|
183
|
+
mapping=m,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# External / unknown kinds – keep mapping and defer to registry at build()
|
|
187
|
+
return EngineSpec(
|
|
188
|
+
kind=k or None,
|
|
189
|
+
async_=_get_bool("async", "async_", default=False),
|
|
190
|
+
dsn=_get_str("dsn", "url"),
|
|
191
|
+
mapping=m,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# ---------- realization ----------
|
|
195
|
+
|
|
196
|
+
def build(self) -> Tuple[Any, SessionFactory]:
|
|
197
|
+
"""Construct the engine and sessionmaker for this spec."""
|
|
198
|
+
if self.kind == "sqlite":
|
|
199
|
+
if self.memory:
|
|
200
|
+
if self.async_:
|
|
201
|
+
return async_sqlite_engine(path=None)
|
|
202
|
+
return blocking_sqlite_engine(path=None)
|
|
203
|
+
if not self.path:
|
|
204
|
+
raise ValueError("sqlite file requires 'path'")
|
|
205
|
+
if self.async_:
|
|
206
|
+
return async_sqlite_engine(path=self.path)
|
|
207
|
+
return blocking_sqlite_engine(path=self.path)
|
|
208
|
+
|
|
209
|
+
if self.kind == "postgres":
|
|
210
|
+
if self.dsn:
|
|
211
|
+
if self.async_:
|
|
212
|
+
return async_postgres_engine(dsn=self.dsn)
|
|
213
|
+
return blocking_postgres_engine(dsn=self.dsn)
|
|
214
|
+
# keyword build
|
|
215
|
+
kwargs: dict[str, Any] = {
|
|
216
|
+
"user": self.user or "app",
|
|
217
|
+
"host": self.host or "localhost",
|
|
218
|
+
"port": self.port or 5432,
|
|
219
|
+
"db": self.name or "app_db",
|
|
220
|
+
"pool_size": int(self.pool_size or 10),
|
|
221
|
+
}
|
|
222
|
+
if self.pwd is not None:
|
|
223
|
+
kwargs["pwd"] = self.pwd
|
|
224
|
+
if self.async_:
|
|
225
|
+
kwargs["max_size"] = int(self.max or 20)
|
|
226
|
+
return async_postgres_engine(**kwargs)
|
|
227
|
+
else:
|
|
228
|
+
kwargs["max_overflow"] = int(self.max or 20)
|
|
229
|
+
return blocking_postgres_engine(**kwargs)
|
|
230
|
+
|
|
231
|
+
# External/registered engines
|
|
232
|
+
try:
|
|
233
|
+
from .plugins import load_engine_plugins
|
|
234
|
+
from .registry import get_engine_registration, known_engine_kinds
|
|
235
|
+
|
|
236
|
+
load_engine_plugins()
|
|
237
|
+
reg = get_engine_registration(self.kind or "")
|
|
238
|
+
except Exception:
|
|
239
|
+
reg = None
|
|
240
|
+
if reg:
|
|
241
|
+
mapping = self.mapping or {}
|
|
242
|
+
return reg.build(mapping=mapping, spec=self, dsn=self.dsn)
|
|
243
|
+
|
|
244
|
+
# No registration found: helpful error
|
|
245
|
+
try:
|
|
246
|
+
from .registry import known_engine_kinds # re-import defensive
|
|
247
|
+
|
|
248
|
+
kinds = ", ".join(known_engine_kinds()) or "(none)"
|
|
249
|
+
except Exception:
|
|
250
|
+
kinds = "(unknown)"
|
|
251
|
+
raise RuntimeError(
|
|
252
|
+
f"Unknown or unavailable engine kind '{self.kind}'. Installed engine kinds: {kinds}. "
|
|
253
|
+
f"If this is an optional extension, install its package (e.g., 'pip install tigrbl_engine_{self.kind}')."
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
def supports(self) -> dict[str, Any]:
|
|
257
|
+
"""Return capability dictionary for this engine spec.
|
|
258
|
+
For external kinds, consult the plugin registry if available.
|
|
259
|
+
"""
|
|
260
|
+
# Built-ins
|
|
261
|
+
if self.kind == "sqlite":
|
|
262
|
+
try:
|
|
263
|
+
from .capabilities import sqlite_capabilities
|
|
264
|
+
|
|
265
|
+
return sqlite_capabilities(async_=self.async_, memory=self.memory)
|
|
266
|
+
except Exception:
|
|
267
|
+
pass
|
|
268
|
+
if self.kind == "postgres":
|
|
269
|
+
try:
|
|
270
|
+
from .capabilities import postgres_capabilities
|
|
271
|
+
|
|
272
|
+
return postgres_capabilities(async_=self.async_)
|
|
273
|
+
except Exception:
|
|
274
|
+
pass
|
|
275
|
+
# External/registered engines
|
|
276
|
+
try:
|
|
277
|
+
from .plugins import load_engine_plugins
|
|
278
|
+
from .registry import get_engine_registration
|
|
279
|
+
|
|
280
|
+
load_engine_plugins()
|
|
281
|
+
reg = get_engine_registration(self.kind or "")
|
|
282
|
+
except Exception:
|
|
283
|
+
reg = None
|
|
284
|
+
if reg and getattr(reg, "capabilities", None):
|
|
285
|
+
try:
|
|
286
|
+
# Try flexible signature: capabilities(spec=..., mapping=...)
|
|
287
|
+
return reg.capabilities(spec=self, mapping=self.mapping)
|
|
288
|
+
except TypeError:
|
|
289
|
+
try:
|
|
290
|
+
return reg.capabilities()
|
|
291
|
+
except Exception:
|
|
292
|
+
pass
|
|
293
|
+
except Exception:
|
|
294
|
+
pass
|
|
295
|
+
# Fallback minimal shape
|
|
296
|
+
return {
|
|
297
|
+
"transactional": False,
|
|
298
|
+
"async_native": bool(self.async_),
|
|
299
|
+
"isolation_levels": set(),
|
|
300
|
+
"read_only_enforced": False,
|
|
301
|
+
"engine": self.kind or "unknown",
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
def to_provider(self) -> Provider:
|
|
305
|
+
"""Materialize a lazy :class:`Provider` for this spec."""
|
|
306
|
+
return Provider(self)
|
|
307
|
+
|
|
308
|
+
def __repr__(self) -> str: # pragma: no cover - deterministic output
|
|
309
|
+
def _redact_dsn(dsn: Optional[str]) -> Optional[str]:
|
|
310
|
+
if not dsn:
|
|
311
|
+
return dsn
|
|
312
|
+
try:
|
|
313
|
+
parts = urlsplit(dsn)
|
|
314
|
+
except Exception:
|
|
315
|
+
return dsn
|
|
316
|
+
if not parts.scheme or parts.password is None:
|
|
317
|
+
return dsn
|
|
318
|
+
user = parts.username or ""
|
|
319
|
+
userinfo = f"{user}:***" if user else "***"
|
|
320
|
+
host = parts.hostname or ""
|
|
321
|
+
netloc = f"{userinfo}@{host}" if host else userinfo
|
|
322
|
+
if parts.port is not None:
|
|
323
|
+
netloc = f"{netloc}:{parts.port}"
|
|
324
|
+
return urlunsplit(
|
|
325
|
+
(parts.scheme, netloc, parts.path, parts.query, parts.fragment)
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
def _redact_mapping(
|
|
329
|
+
mapping: Optional[Mapping[str, object]],
|
|
330
|
+
) -> Optional[dict[str, object]]:
|
|
331
|
+
if mapping is None:
|
|
332
|
+
return None
|
|
333
|
+
redacted: dict[str, object] = {}
|
|
334
|
+
for key, value in mapping.items():
|
|
335
|
+
if str(key).lower() in {"pwd", "password", "pass", "secret"}:
|
|
336
|
+
redacted[key] = "***"
|
|
337
|
+
else:
|
|
338
|
+
redacted[key] = value
|
|
339
|
+
return redacted
|
|
340
|
+
|
|
341
|
+
fields = [
|
|
342
|
+
("kind", self.kind),
|
|
343
|
+
("async_", self.async_),
|
|
344
|
+
("dsn", _redact_dsn(self.dsn)),
|
|
345
|
+
("mapping", _redact_mapping(self.mapping)),
|
|
346
|
+
("path", self.path),
|
|
347
|
+
("memory", self.memory),
|
|
348
|
+
("user", self.user),
|
|
349
|
+
("host", self.host),
|
|
350
|
+
("port", self.port),
|
|
351
|
+
("name", self.name),
|
|
352
|
+
("pool_size", self.pool_size),
|
|
353
|
+
("max", self.max),
|
|
354
|
+
]
|
|
355
|
+
return "EngineSpec(" + ", ".join(f"{k}={v!r}" for k, v in fields) + ")"
|
tigrbl/engine/plugins.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
_loaded = False
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def load_engine_plugins() -> None:
|
|
8
|
+
"""Discover and load external engine plugins via entry points.
|
|
9
|
+
Safe and idempotent; does nothing if already loaded.
|
|
10
|
+
"""
|
|
11
|
+
global _loaded
|
|
12
|
+
if _loaded:
|
|
13
|
+
return
|
|
14
|
+
|
|
15
|
+
# importlib.metadata API differs across Python versions; support both.
|
|
16
|
+
eps = None
|
|
17
|
+
try:
|
|
18
|
+
from importlib.metadata import entry_points # Python >= 3.10
|
|
19
|
+
|
|
20
|
+
eps = entry_points()
|
|
21
|
+
# New API: .select(group="tigrbl.engine")
|
|
22
|
+
selected = (
|
|
23
|
+
eps.select(group="tigrbl.engine")
|
|
24
|
+
if hasattr(eps, "select")
|
|
25
|
+
else eps.get("tigrbl.engine", [])
|
|
26
|
+
)
|
|
27
|
+
except Exception:
|
|
28
|
+
try:
|
|
29
|
+
from importlib_metadata import entry_points as entry_points_backport
|
|
30
|
+
|
|
31
|
+
eps = entry_points_backport()
|
|
32
|
+
selected = (
|
|
33
|
+
eps.select(group="tigrbl.engine")
|
|
34
|
+
if hasattr(eps, "select")
|
|
35
|
+
else eps.get("tigrbl.engine", [])
|
|
36
|
+
)
|
|
37
|
+
except Exception:
|
|
38
|
+
selected = []
|
|
39
|
+
|
|
40
|
+
for ep in selected or []:
|
|
41
|
+
try:
|
|
42
|
+
fn = ep.load()
|
|
43
|
+
except Exception:
|
|
44
|
+
# Ignore broken entry points; the engine remains unavailable.
|
|
45
|
+
continue
|
|
46
|
+
try:
|
|
47
|
+
fn() # call plugin's register() to register_engine(kind, build, ...)
|
|
48
|
+
except Exception:
|
|
49
|
+
# Defensive: a broken plugin must not crash core import
|
|
50
|
+
continue
|
|
51
|
+
|
|
52
|
+
_loaded = True
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Any, Callable, Optional, Tuple, Dict
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# A registration for an engine kind provided by an external (or built-in) package.
|
|
7
|
+
@dataclass
|
|
8
|
+
class EngineRegistration:
|
|
9
|
+
build: Callable[..., Tuple[Any, Callable[[], Any]]]
|
|
10
|
+
capabilities: Optional[Callable[[], Any]] = None
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
_registry: Dict[str, EngineRegistration] = {}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def register_engine(
|
|
17
|
+
kind: str,
|
|
18
|
+
build: Callable[..., Tuple[Any, Callable[[], Any]]],
|
|
19
|
+
capabilities: Optional[Callable[[], Any]] = None,
|
|
20
|
+
) -> None:
|
|
21
|
+
"""Register an engine kind → (builder, capabilities). Idempotent."""
|
|
22
|
+
k = (kind or "").strip().lower()
|
|
23
|
+
if not k:
|
|
24
|
+
raise ValueError("engine kind must be a non-empty string")
|
|
25
|
+
if k in _registry:
|
|
26
|
+
# idempotent registration
|
|
27
|
+
return
|
|
28
|
+
_registry[k] = EngineRegistration(build=build, capabilities=capabilities)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_engine_registration(kind: str) -> Optional[EngineRegistration]:
|
|
32
|
+
return _registry.get((kind or "").strip().lower())
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def known_engine_kinds() -> list[str]:
|
|
36
|
+
return sorted(_registry.keys())
|