tigrbl 0.3.0.dev4__py3-none-any.whl → 0.3.2__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/api/_api.py +26 -1
- tigrbl/api/tigrbl_api.py +6 -1
- tigrbl/app/_app.py +26 -1
- tigrbl/app/_model_registry.py +41 -0
- tigrbl/app/tigrbl_app.py +6 -1
- tigrbl/bindings/api/__init__.py +0 -1
- tigrbl/bindings/api/common.py +0 -1
- tigrbl/bindings/api/include.py +14 -2
- tigrbl/bindings/api/resource_proxy.py +0 -1
- tigrbl/bindings/api/rpc.py +0 -1
- tigrbl/bindings/handlers/__init__.py +0 -1
- tigrbl/bindings/handlers/builder.py +0 -1
- tigrbl/bindings/handlers/ctx.py +0 -1
- tigrbl/bindings/handlers/identifiers.py +0 -1
- tigrbl/bindings/handlers/namespaces.py +0 -1
- tigrbl/bindings/handlers/steps.py +0 -1
- tigrbl/bindings/rest/collection.py +24 -3
- tigrbl/bindings/rest/common.py +4 -0
- tigrbl/bindings/rest/io_headers.py +49 -0
- tigrbl/bindings/rest/member.py +19 -0
- tigrbl/bindings/rest/router.py +4 -0
- tigrbl/bindings/rest/routing.py +21 -1
- tigrbl/bindings/schemas/__init__.py +0 -1
- tigrbl/bindings/schemas/builder.py +0 -1
- tigrbl/bindings/schemas/defaults.py +0 -1
- tigrbl/bindings/schemas/utils.py +0 -1
- tigrbl/column/io_spec.py +3 -0
- tigrbl/core/crud/bulk.py +0 -1
- tigrbl/core/crud/helpers/db.py +0 -1
- tigrbl/core/crud/helpers/enum.py +0 -1
- tigrbl/core/crud/helpers/filters.py +0 -1
- tigrbl/core/crud/helpers/model.py +0 -1
- tigrbl/core/crud/helpers/normalize.py +0 -1
- tigrbl/core/crud/ops.py +0 -1
- tigrbl/docs/verbosity.md +35 -0
- tigrbl/engine/__init__.py +19 -0
- tigrbl/engine/_engine.py +14 -0
- tigrbl/engine/builders.py +0 -1
- tigrbl/engine/capabilities.py +29 -0
- tigrbl/engine/decorators.py +3 -1
- tigrbl/engine/docs/PLUGINS.md +49 -0
- tigrbl/engine/engine_spec.py +197 -103
- tigrbl/engine/plugins.py +52 -0
- tigrbl/engine/registry.py +36 -0
- tigrbl/engine/resolver.py +0 -1
- tigrbl/orm/mixins/upsertable.py +7 -0
- tigrbl/response/shortcuts.py +31 -4
- tigrbl/runtime/atoms/__init__.py +0 -1
- tigrbl/runtime/atoms/response/__init__.py +2 -0
- tigrbl/runtime/atoms/response/headers_from_payload.py +57 -0
- tigrbl/runtime/kernel.py +27 -11
- tigrbl/runtime/opview.py +5 -3
- tigrbl/schema/collect.py +26 -3
- tigrbl/schema/decorators.py +0 -1
- tigrbl/schema/get_schema.py +0 -1
- tigrbl/schema/utils.py +0 -1
- 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/system/__init__.py +2 -1
- tigrbl/system/uvicorn.py +60 -0
- tigrbl/table/_base.py +39 -5
- tigrbl/types/__init__.py +3 -7
- tigrbl/types/uuid.py +55 -0
- {tigrbl-0.3.0.dev4.dist-info → tigrbl-0.3.2.dist-info}/METADATA +19 -4
- {tigrbl-0.3.0.dev4.dist-info → tigrbl-0.3.2.dist-info}/RECORD +73 -55
- {tigrbl-0.3.0.dev4.dist-info → tigrbl-0.3.2.dist-info}/WHEEL +1 -1
- {tigrbl-0.3.0.dev4.dist-info → tigrbl-0.3.2.dist-info/licenses}/LICENSE +0 -0
tigrbl/core/crud/helpers/db.py
CHANGED
tigrbl/core/crud/helpers/enum.py
CHANGED
tigrbl/core/crud/ops.py
CHANGED
tigrbl/docs/verbosity.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Tigrbl verbosity and uvicorn logging
|
|
2
|
+
|
|
3
|
+
Tigrbl logs through the `uvicorn` logger. This keeps its output aligned with the
|
|
4
|
+
server's log configuration, so debug statements only appear when uvicorn is
|
|
5
|
+
started in verbose mode.
|
|
6
|
+
|
|
7
|
+
## Default behavior
|
|
8
|
+
|
|
9
|
+
By default, uvicorn runs at `INFO` level and suppresses debug output. Tigrbl does
|
|
10
|
+
not override that setting, so debug messages stay hidden unless you opt in.
|
|
11
|
+
|
|
12
|
+
## Enabling verbose output
|
|
13
|
+
|
|
14
|
+
Use uvicorn's log-level to opt into debug output:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
uvicorn your_module:app --log-level debug
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Or configure it programmatically:
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
import uvicorn
|
|
24
|
+
|
|
25
|
+
config = uvicorn.Config("your_module:app", log_level="debug")
|
|
26
|
+
server = uvicorn.Server(config)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Disabling verbose output
|
|
30
|
+
|
|
31
|
+
Set the log level back to `info` (or higher) to suppress debug messages:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
uvicorn your_module:app --log-level info
|
|
35
|
+
```
|
tigrbl/engine/__init__.py
CHANGED
|
@@ -24,3 +24,22 @@ __all__ = [
|
|
|
24
24
|
"Engine",
|
|
25
25
|
"engine",
|
|
26
26
|
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Optional engine plugin support
|
|
30
|
+
from .plugins import load_engine_plugins
|
|
31
|
+
from .registry import register_engine, known_engine_kinds, get_engine_registration
|
|
32
|
+
|
|
33
|
+
# Load external engines automatically on import (idempotent)
|
|
34
|
+
try:
|
|
35
|
+
load_engine_plugins()
|
|
36
|
+
except Exception:
|
|
37
|
+
# Import-time plugin load should never fail the package import
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
__all__ += [
|
|
41
|
+
"load_engine_plugins",
|
|
42
|
+
"register_engine",
|
|
43
|
+
"known_engine_kinds",
|
|
44
|
+
"get_engine_registration",
|
|
45
|
+
]
|
tigrbl/engine/_engine.py
CHANGED
|
@@ -27,6 +27,13 @@ if TYPE_CHECKING: # pragma: no cover - for type checkers only
|
|
|
27
27
|
|
|
28
28
|
@dataclass(frozen=True)
|
|
29
29
|
class Provider:
|
|
30
|
+
# supports() exposes engine capabilities for compatibility checks
|
|
31
|
+
def supports(self) -> dict:
|
|
32
|
+
try:
|
|
33
|
+
return self.spec.supports()
|
|
34
|
+
except Exception:
|
|
35
|
+
return {"engine": self.spec.kind or "unknown"}
|
|
36
|
+
|
|
30
37
|
"""Lazily builds an engine + sessionmaker from an :class:`EngineSpec`."""
|
|
31
38
|
|
|
32
39
|
spec: "EngineSpec"
|
|
@@ -87,6 +94,13 @@ class Provider:
|
|
|
87
94
|
|
|
88
95
|
@dataclass
|
|
89
96
|
class Engine:
|
|
97
|
+
# Delegate to provider/spec for capability reporting
|
|
98
|
+
def supports(self) -> dict:
|
|
99
|
+
try:
|
|
100
|
+
return self.provider.supports()
|
|
101
|
+
except Exception:
|
|
102
|
+
return {"engine": self.spec.kind or "unknown"}
|
|
103
|
+
|
|
90
104
|
"""Thin façade over an :class:`EngineSpec` with convenient (a)context managers."""
|
|
91
105
|
|
|
92
106
|
spec: "EngineSpec"
|
tigrbl/engine/builders.py
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Any, Dict
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def sqlite_capabilities(*, async_: bool, memory: bool) -> Dict[str, Any]:
|
|
6
|
+
# SQLite supports transactional semantics; isolation semantics differ from server DBs.
|
|
7
|
+
# We expose a conservative set that higher layers can reason about.
|
|
8
|
+
return {
|
|
9
|
+
"transactional": True,
|
|
10
|
+
"async_native": async_, # async layer is provided by aiosqlite/SQLAlchemy AsyncEngine
|
|
11
|
+
"isolation_levels": {"read_committed", "serializable"},
|
|
12
|
+
"read_only_enforced": False, # app layer should enforce; file-open RO may be used externally
|
|
13
|
+
"supports_timeouts": True,
|
|
14
|
+
"supports_ddl": True,
|
|
15
|
+
"engine": "sqlite",
|
|
16
|
+
"memory": bool(memory),
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def postgres_capabilities(*, async_: bool) -> Dict[str, Any]:
|
|
21
|
+
return {
|
|
22
|
+
"transactional": True,
|
|
23
|
+
"async_native": async_,
|
|
24
|
+
"isolation_levels": {"read_committed", "repeatable_read", "serializable"},
|
|
25
|
+
"read_only_enforced": True,
|
|
26
|
+
"supports_timeouts": True,
|
|
27
|
+
"supports_ddl": True,
|
|
28
|
+
"engine": "postgres",
|
|
29
|
+
}
|
tigrbl/engine/decorators.py
CHANGED
|
@@ -56,7 +56,9 @@ def _normalize(ctx: Optional[EngineCfg] = None, **kw: Any) -> EngineCfg:
|
|
|
56
56
|
if k in kw:
|
|
57
57
|
m[k] = kw[k]
|
|
58
58
|
else:
|
|
59
|
-
|
|
59
|
+
# Allow external engine kinds; pass mapping through unchanged.
|
|
60
|
+
# Keep provided keys as-is so external builders can interpret them.
|
|
61
|
+
m.update({k: v for k, v in kw.items() if k not in m})
|
|
60
62
|
|
|
61
63
|
return m
|
|
62
64
|
|
|
@@ -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
|
+
```
|
tigrbl/engine/engine_spec.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# tigrbl/v3/engine/engine_spec.py
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
-
from dataclasses import dataclass, field
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
5
|
from typing import Optional, Mapping, Union, Any, Tuple
|
|
6
6
|
from urllib.parse import urlsplit, urlunsplit
|
|
7
7
|
|
|
@@ -14,7 +14,6 @@ from .builders import (
|
|
|
14
14
|
)
|
|
15
15
|
|
|
16
16
|
# The value stored by @engine_ctx on App/API/Table/Op.
|
|
17
|
-
# Accept either a DSN string, structured mapping, or pre-built objects.
|
|
18
17
|
EngineCfg = Union[str, Mapping[str, object], "EngineSpec", Provider, Engine]
|
|
19
18
|
|
|
20
19
|
|
|
@@ -31,28 +30,21 @@ class EngineSpec:
|
|
|
31
30
|
"postgresql://user:pwd@host:5432/db" ,
|
|
32
31
|
"postgresql+asyncpg://user:pwd@host:5432/db"
|
|
33
32
|
• Mapping (recommended for clarity/portability):
|
|
34
|
-
{
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
# sqlite:
|
|
38
|
-
"path": "./file.db", # file-backed
|
|
39
|
-
"mode": "memory" | None, # memory uses StaticPool
|
|
40
|
-
# postgres:
|
|
41
|
-
"user": "app", "pwd": "secret",
|
|
42
|
-
"host": "localhost", "port": 5432, "db": "app_db",
|
|
43
|
-
"pool_size": 10, "max": 20 # max_overflow (sync) or max_size (async)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
This class *does not* open connections; call .to_provider() to obtain
|
|
47
|
-
a lazy Provider that builds (engine, sessionmaker) on first use.
|
|
33
|
+
{"kind":"sqlite","async":True,"path":"./file.db"}
|
|
34
|
+
{"kind":"postgres","async":True,"host":"db","db":"app_db",...}
|
|
35
|
+
{<external kind> ...} # for plugin engines
|
|
48
36
|
"""
|
|
49
37
|
|
|
50
|
-
# normalized
|
|
51
|
-
kind: Optional[str] = None # "sqlite" | "postgres"
|
|
38
|
+
# normalized
|
|
39
|
+
kind: Optional[str] = None # "sqlite" | "postgres" | <external>
|
|
52
40
|
async_: bool = False
|
|
53
41
|
|
|
42
|
+
# canonical DSN (optional) and raw mapping (for external engines)
|
|
43
|
+
dsn: Optional[str] = None
|
|
44
|
+
mapping: Optional[Mapping[str, object]] = None
|
|
45
|
+
|
|
54
46
|
# sqlite
|
|
55
|
-
path: Optional[str] = None # file path (None
|
|
47
|
+
path: Optional[str] = None # file path (None → memory)
|
|
56
48
|
memory: bool = False
|
|
57
49
|
|
|
58
50
|
# postgres
|
|
@@ -64,44 +56,11 @@ class EngineSpec:
|
|
|
64
56
|
pool_size: int = 10
|
|
65
57
|
max: int = 20 # max_overflow (sync) or max_size (async)
|
|
66
58
|
|
|
67
|
-
#
|
|
68
|
-
dsn: Optional[str] = None
|
|
69
|
-
mapping: Optional[Mapping[str, object]] = field(default=None, repr=False)
|
|
70
|
-
|
|
71
|
-
def __repr__(self) -> str: # pragma: no cover - representation logic
|
|
72
|
-
parts = []
|
|
73
|
-
for f in fields(self):
|
|
74
|
-
if not f.repr:
|
|
75
|
-
continue
|
|
76
|
-
value = getattr(self, f.name)
|
|
77
|
-
if f.name == "dsn":
|
|
78
|
-
value = self._redact_dsn(value)
|
|
79
|
-
if value is not None:
|
|
80
|
-
parts.append(f"{f.name}={value!r}")
|
|
81
|
-
return f"EngineSpec({', '.join(parts)})"
|
|
82
|
-
|
|
83
|
-
@staticmethod
|
|
84
|
-
def _redact_dsn(dsn: Optional[str]) -> Optional[str]:
|
|
85
|
-
if not dsn:
|
|
86
|
-
return dsn
|
|
87
|
-
try:
|
|
88
|
-
parts = urlsplit(dsn)
|
|
89
|
-
if parts.password:
|
|
90
|
-
netloc = parts.netloc.replace(parts.password, "***")
|
|
91
|
-
return urlunsplit(
|
|
92
|
-
(parts.scheme, netloc, parts.path, parts.query, parts.fragment)
|
|
93
|
-
)
|
|
94
|
-
except Exception:
|
|
95
|
-
pass
|
|
96
|
-
return dsn
|
|
97
|
-
|
|
98
|
-
# ---------- parsing / normalization ----------
|
|
59
|
+
# ---------- parsing ----------
|
|
99
60
|
|
|
100
61
|
@staticmethod
|
|
101
62
|
def from_any(x: EngineCfg | None) -> Optional["EngineSpec"]:
|
|
102
|
-
"""
|
|
103
|
-
Parse a DSN or mapping (as attached by @engine_ctx) into an EngineSpec.
|
|
104
|
-
"""
|
|
63
|
+
"""Parse DSN/Mapping/Provider/Engine into an :class:`EngineSpec`."""
|
|
105
64
|
if x is None:
|
|
106
65
|
return None
|
|
107
66
|
|
|
@@ -114,39 +73,57 @@ class EngineSpec:
|
|
|
114
73
|
if isinstance(x, Engine):
|
|
115
74
|
return x.spec
|
|
116
75
|
|
|
117
|
-
#
|
|
76
|
+
# DSN string
|
|
118
77
|
if isinstance(x, str):
|
|
119
78
|
s = x.strip()
|
|
120
|
-
# sqlite
|
|
121
|
-
if s
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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:")
|
|
130
89
|
return EngineSpec(
|
|
131
|
-
kind="sqlite", async_=True, path=
|
|
90
|
+
kind="sqlite", async_=True, path=path, memory=mem, dsn=s
|
|
132
91
|
)
|
|
133
|
-
# sqlite sync
|
|
134
|
-
if s.startswith("sqlite
|
|
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
|
|
135
108
|
return EngineSpec(
|
|
136
|
-
kind="sqlite", async_=False, path=
|
|
109
|
+
kind="sqlite", async_=False, path=p, memory=mem, dsn=s
|
|
137
110
|
)
|
|
111
|
+
|
|
138
112
|
# postgres async
|
|
139
|
-
if s.startswith("postgresql+asyncpg://")
|
|
113
|
+
if s.startswith("postgresql+asyncpg://") or s.startswith(
|
|
114
|
+
"postgres+asyncpg://"
|
|
115
|
+
):
|
|
140
116
|
return EngineSpec(kind="postgres", async_=True, dsn=s)
|
|
141
117
|
# postgres sync
|
|
142
|
-
if s.startswith("postgresql://"):
|
|
118
|
+
if s.startswith("postgresql://") or s.startswith("postgres://"):
|
|
143
119
|
return EngineSpec(kind="postgres", async_=False, dsn=s)
|
|
120
|
+
|
|
144
121
|
raise ValueError(f"Unsupported DSN: {s}")
|
|
145
122
|
|
|
146
123
|
# Mapping
|
|
147
124
|
m = x # type: ignore[assignment]
|
|
148
125
|
|
|
149
|
-
#
|
|
126
|
+
# Helpers
|
|
150
127
|
def _get_bool(key: str, *aliases: str, default: bool = False) -> bool:
|
|
151
128
|
for k in (key, *aliases):
|
|
152
129
|
if k in m:
|
|
@@ -166,14 +143,16 @@ class EngineSpec:
|
|
|
166
143
|
) -> Optional[int]:
|
|
167
144
|
for k in (key, *aliases):
|
|
168
145
|
if k in m and m[k] is not None:
|
|
169
|
-
|
|
146
|
+
try:
|
|
147
|
+
return int(m[k]) # type: ignore[index]
|
|
148
|
+
except Exception:
|
|
149
|
+
return default
|
|
170
150
|
return default
|
|
171
151
|
|
|
172
152
|
k = str(m.get("kind", m.get("engine", ""))).lower() # type: ignore[index]
|
|
173
153
|
if k == "sqlite":
|
|
174
154
|
async_ = _get_bool("async", "async_", default=False)
|
|
175
155
|
path = _get_str("path")
|
|
176
|
-
# support either {"mode": "memory"} or {"memory": True} or no path
|
|
177
156
|
memory = (
|
|
178
157
|
_get_bool("memory", default=False)
|
|
179
158
|
or (str(m.get("mode", "")).lower() == "memory")
|
|
@@ -182,8 +161,9 @@ class EngineSpec:
|
|
|
182
161
|
return EngineSpec(
|
|
183
162
|
kind="sqlite",
|
|
184
163
|
async_=async_,
|
|
185
|
-
path=
|
|
164
|
+
path=path,
|
|
186
165
|
memory=memory,
|
|
166
|
+
dsn=_get_str("dsn", "url"),
|
|
187
167
|
mapping=m,
|
|
188
168
|
)
|
|
189
169
|
|
|
@@ -199,10 +179,17 @@ class EngineSpec:
|
|
|
199
179
|
name=_get_str("db", "name"),
|
|
200
180
|
pool_size=_get_int("pool_size", default=10) or 10,
|
|
201
181
|
max=_get_int("max", "max_overflow", "max_size", default=20) or 20,
|
|
182
|
+
dsn=_get_str("dsn", "url"),
|
|
202
183
|
mapping=m,
|
|
203
184
|
)
|
|
204
185
|
|
|
205
|
-
|
|
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
|
+
)
|
|
206
193
|
|
|
207
194
|
# ---------- realization ----------
|
|
208
195
|
|
|
@@ -222,40 +209,147 @@ class EngineSpec:
|
|
|
222
209
|
if self.kind == "postgres":
|
|
223
210
|
if self.dsn:
|
|
224
211
|
if self.async_:
|
|
225
|
-
return async_postgres_engine(
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
)
|
|
235
|
-
|
|
236
|
-
kwargs = {
|
|
237
|
-
"pool_size": self.pool_size,
|
|
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),
|
|
238
221
|
}
|
|
239
|
-
if self.async_:
|
|
240
|
-
kwargs["max_size"] = self.max
|
|
241
|
-
else:
|
|
242
|
-
kwargs["max_overflow"] = self.max
|
|
243
|
-
if self.user is not None:
|
|
244
|
-
kwargs["user"] = self.user
|
|
245
222
|
if self.pwd is not None:
|
|
246
223
|
kwargs["pwd"] = self.pwd
|
|
247
|
-
if self.host is not None:
|
|
248
|
-
kwargs["host"] = self.host
|
|
249
|
-
if self.port is not None:
|
|
250
|
-
kwargs["port"] = self.port
|
|
251
|
-
if self.name is not None:
|
|
252
|
-
kwargs["db"] = self.name
|
|
253
224
|
if self.async_:
|
|
225
|
+
kwargs["max_size"] = int(self.max or 20)
|
|
254
226
|
return async_postgres_engine(**kwargs)
|
|
255
|
-
|
|
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
|
|
256
279
|
|
|
257
|
-
|
|
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
|
+
}
|
|
258
303
|
|
|
259
304
|
def to_provider(self) -> Provider:
|
|
260
305
|
"""Materialize a lazy :class:`Provider` for this spec."""
|
|
261
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) + ")"
|