tigrbl-ops-oltp 0.1.0.dev1__tar.gz → 0.1.0.dev7__tar.gz
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_ops_oltp-0.1.0.dev1 → tigrbl_ops_oltp-0.1.0.dev7}/PKG-INFO +1 -1
- {tigrbl_ops_oltp-0.1.0.dev1 → tigrbl_ops_oltp-0.1.0.dev7}/pyproject.toml +1 -1
- {tigrbl_ops_oltp-0.1.0.dev1 → tigrbl_ops_oltp-0.1.0.dev7}/tigrbl_ops_oltp/crud/helpers/__init__.py +2 -0
- {tigrbl_ops_oltp-0.1.0.dev1 → tigrbl_ops_oltp-0.1.0.dev7}/tigrbl_ops_oltp/crud/helpers/db.py +31 -17
- {tigrbl_ops_oltp-0.1.0.dev1 → tigrbl_ops_oltp-0.1.0.dev7}/tigrbl_ops_oltp/crud/helpers/enum.py +2 -6
- tigrbl_ops_oltp-0.1.0.dev7/tigrbl_ops_oltp/crud/helpers/model.py +200 -0
- {tigrbl_ops_oltp-0.1.0.dev1 → tigrbl_ops_oltp-0.1.0.dev7}/tigrbl_ops_oltp/crud/ops.py +61 -2
- tigrbl_ops_oltp-0.1.0.dev1/tigrbl_ops_oltp/crud/helpers/model.py +0 -142
- {tigrbl_ops_oltp-0.1.0.dev1 → tigrbl_ops_oltp-0.1.0.dev7}/README.md +0 -0
- {tigrbl_ops_oltp-0.1.0.dev1 → tigrbl_ops_oltp-0.1.0.dev7}/tigrbl_ops_oltp/__init__.py +0 -0
- {tigrbl_ops_oltp-0.1.0.dev1 → tigrbl_ops_oltp-0.1.0.dev7}/tigrbl_ops_oltp/crud/__init__.py +0 -0
- {tigrbl_ops_oltp-0.1.0.dev1 → tigrbl_ops_oltp-0.1.0.dev7}/tigrbl_ops_oltp/crud/bulk.py +0 -0
- {tigrbl_ops_oltp-0.1.0.dev1 → tigrbl_ops_oltp-0.1.0.dev7}/tigrbl_ops_oltp/crud/helpers/filters.py +0 -0
- {tigrbl_ops_oltp-0.1.0.dev1 → tigrbl_ops_oltp-0.1.0.dev7}/tigrbl_ops_oltp/crud/helpers/normalize.py +0 -0
- {tigrbl_ops_oltp-0.1.0.dev1 → tigrbl_ops_oltp-0.1.0.dev7}/tigrbl_ops_oltp/crud/params.py +0 -0
{tigrbl_ops_oltp-0.1.0.dev1 → tigrbl_ops_oltp-0.1.0.dev7}/tigrbl_ops_oltp/crud/helpers/__init__.py
RENAMED
|
@@ -30,6 +30,7 @@ from .db import (
|
|
|
30
30
|
_maybe_get,
|
|
31
31
|
_maybe_execute,
|
|
32
32
|
_maybe_flush,
|
|
33
|
+
_maybe_rollback,
|
|
33
34
|
_maybe_delete,
|
|
34
35
|
_set_attrs,
|
|
35
36
|
)
|
|
@@ -59,6 +60,7 @@ __all__ = [
|
|
|
59
60
|
"_maybe_delete",
|
|
60
61
|
"_maybe_execute",
|
|
61
62
|
"_maybe_flush",
|
|
63
|
+
"_maybe_rollback",
|
|
62
64
|
"_maybe_get",
|
|
63
65
|
"_model_columns",
|
|
64
66
|
"_normalize_list_call",
|
{tigrbl_ops_oltp-0.1.0.dev1 → tigrbl_ops_oltp-0.1.0.dev7}/tigrbl_ops_oltp/crud/helpers/db.py
RENAMED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import inspect
|
|
4
|
+
from functools import lru_cache
|
|
3
5
|
from typing import Any, Mapping, Sequence, Union
|
|
4
6
|
|
|
5
7
|
import logging
|
|
@@ -10,51 +12,63 @@ from .model import _model_columns, _single_pk_name
|
|
|
10
12
|
logger = logging.getLogger("uvicorn")
|
|
11
13
|
|
|
12
14
|
|
|
15
|
+
@lru_cache(maxsize=512)
|
|
16
|
+
def _is_async_db_type(db_type: type[Any]) -> bool:
|
|
17
|
+
return db_type.__name__ == "AsyncSession" or hasattr(db_type, "run_sync")
|
|
18
|
+
|
|
19
|
+
|
|
13
20
|
def _is_async_db(db: Any) -> bool:
|
|
14
21
|
logger.debug("_is_async_db called with db=%s", db)
|
|
15
|
-
result =
|
|
22
|
+
result = _is_async_db_type(type(db))
|
|
16
23
|
logger.debug("_is_async_db returning %s", result)
|
|
17
24
|
return result
|
|
18
25
|
|
|
19
26
|
|
|
20
27
|
async def _maybe_get(db: Union[Session, AsyncSession], model: type, pk_value: Any):
|
|
21
28
|
logger.debug("_maybe_get model=%s pk_value=%s", model, pk_value)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
result = db.get(model, pk_value) # type: ignore[attr-defined]
|
|
29
|
+
result = db.get(model, pk_value) # type: ignore[attr-defined]
|
|
30
|
+
if inspect.isawaitable(result):
|
|
31
|
+
result = await result
|
|
26
32
|
logger.debug("_maybe_get returning %s", result)
|
|
27
33
|
return result
|
|
28
34
|
|
|
29
35
|
|
|
30
36
|
async def _maybe_execute(db: Union[Session, AsyncSession], stmt: Any):
|
|
31
37
|
logger.debug("_maybe_execute stmt=%s", stmt)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
result = db.execute(stmt) # type: ignore[attr-defined]
|
|
38
|
+
result = db.execute(stmt) # type: ignore[attr-defined]
|
|
39
|
+
if inspect.isawaitable(result):
|
|
40
|
+
result = await result
|
|
36
41
|
logger.debug("_maybe_execute returning %s", result)
|
|
37
42
|
return result
|
|
38
43
|
|
|
39
44
|
|
|
40
45
|
async def _maybe_flush(db: Union[Session, AsyncSession]) -> None:
|
|
41
46
|
logger.debug("_maybe_flush called")
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
db.flush() # type: ignore[attr-defined]
|
|
47
|
+
result = db.flush() # type: ignore[attr-defined]
|
|
48
|
+
if inspect.isawaitable(result):
|
|
49
|
+
await result
|
|
46
50
|
logger.debug("_maybe_flush completed")
|
|
47
51
|
|
|
48
52
|
|
|
53
|
+
async def _maybe_rollback(db: Union[Session, AsyncSession]) -> None:
|
|
54
|
+
logger.debug("_maybe_rollback called")
|
|
55
|
+
if not hasattr(db, "rollback"):
|
|
56
|
+
logger.debug("_maybe_rollback skipping rollback; no attribute")
|
|
57
|
+
return
|
|
58
|
+
result = db.rollback() # type: ignore[attr-defined]
|
|
59
|
+
if inspect.isawaitable(result):
|
|
60
|
+
await result
|
|
61
|
+
logger.debug("_maybe_rollback completed")
|
|
62
|
+
|
|
63
|
+
|
|
49
64
|
async def _maybe_delete(db: Union[Session, AsyncSession], obj: Any) -> None:
|
|
50
65
|
logger.debug("_maybe_delete called with obj=%s", obj)
|
|
51
66
|
if not hasattr(db, "delete"):
|
|
52
67
|
logger.debug("_maybe_delete skipping delete; no attribute")
|
|
53
68
|
return
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
db.delete(obj) # type: ignore[attr-defined]
|
|
69
|
+
result = db.delete(obj) # type: ignore[attr-defined]
|
|
70
|
+
if inspect.isawaitable(result):
|
|
71
|
+
await result
|
|
58
72
|
logger.debug("_maybe_delete completed for obj=%s", obj)
|
|
59
73
|
|
|
60
74
|
|
{tigrbl_ops_oltp-0.1.0.dev1 → tigrbl_ops_oltp-0.1.0.dev7}/tigrbl_ops_oltp/crud/helpers/enum.py
RENAMED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import enum as _enum
|
|
3
4
|
from typing import Any, Mapping
|
|
4
5
|
import builtins as _builtins
|
|
5
6
|
import logging
|
|
@@ -40,12 +41,7 @@ def _validate_enum_values(model: type, values: Mapping[str, Any]) -> None:
|
|
|
40
41
|
|
|
41
42
|
enum_cls = getattr(col_type, "enum_class", None)
|
|
42
43
|
if enum_cls is not None:
|
|
43
|
-
|
|
44
|
-
import enum as _enum
|
|
45
|
-
except Exception: # pragma: no cover
|
|
46
|
-
_enum = None
|
|
47
|
-
|
|
48
|
-
if _enum is not None and isinstance(v, _enum.Enum):
|
|
44
|
+
if isinstance(v, _enum.Enum):
|
|
49
45
|
if isinstance(v, enum_cls):
|
|
50
46
|
continue
|
|
51
47
|
logger.debug(
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import datetime as dt
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
from typing import Any, Dict, Mapping, Tuple
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from tigrbl_core._spec.column_spec import mro_collect_columns
|
|
9
|
+
except Exception: # pragma: no cover
|
|
10
|
+
|
|
11
|
+
def mro_collect_columns(_model: type, _cache_bust: int | None = None):
|
|
12
|
+
return {}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _colspecs_cache_token(model: type) -> tuple[int, int, int, int]:
|
|
16
|
+
table = getattr(model, "__table__", None)
|
|
17
|
+
cols = getattr(table, "columns", None) if table is not None else None
|
|
18
|
+
try:
|
|
19
|
+
col_count = len(tuple(cols)) if cols is not None else 0
|
|
20
|
+
except Exception:
|
|
21
|
+
col_count = 0
|
|
22
|
+
return (
|
|
23
|
+
id(getattr(model, "__tigrbl_colspecs__", None)),
|
|
24
|
+
id(getattr(model, "__tigrbl_cols__", None)),
|
|
25
|
+
id(table),
|
|
26
|
+
col_count,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@lru_cache(maxsize=256)
|
|
31
|
+
def _colspecs_cached(
|
|
32
|
+
model: type, cache_token: tuple[int, int, int, int]
|
|
33
|
+
) -> Mapping[str, Any]:
|
|
34
|
+
del cache_token
|
|
35
|
+
return mro_collect_columns(model, _cache_bust=hash(_colspecs_cache_token(model)))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@lru_cache(maxsize=512)
|
|
39
|
+
def _excluded_schema_fields(model: type, verb: str) -> frozenset[str]:
|
|
40
|
+
schema_cls = getattr(getattr(model, "schemas", None), verb, None)
|
|
41
|
+
schema_in = getattr(schema_cls, "in_", None) if schema_cls else None
|
|
42
|
+
model_fields = (
|
|
43
|
+
getattr(schema_in, "model_fields", None) if schema_in is not None else None
|
|
44
|
+
)
|
|
45
|
+
if not isinstance(model_fields, Mapping):
|
|
46
|
+
return frozenset()
|
|
47
|
+
return frozenset(
|
|
48
|
+
name
|
|
49
|
+
for name, field_info in model_fields.items()
|
|
50
|
+
if getattr(field_info, "exclude", False)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _filter_plan_cache_token(model: type, verb: str) -> tuple[int, int, int, int, int]:
|
|
55
|
+
return (
|
|
56
|
+
*_colspecs_cache_token(model),
|
|
57
|
+
id(getattr(getattr(model, "schemas", None), verb, None)),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@lru_cache(maxsize=512)
|
|
62
|
+
def _filter_plan_cached(
|
|
63
|
+
model: type, verb: str, cache_token: tuple[int, int, int, int, int]
|
|
64
|
+
) -> tuple[Mapping[str, bool], frozenset[str], Mapping[str, Any]]:
|
|
65
|
+
del cache_token
|
|
66
|
+
specs = _colspecs(model)
|
|
67
|
+
if not specs:
|
|
68
|
+
return {}, frozenset(), _table_python_types(model)
|
|
69
|
+
|
|
70
|
+
allowed_by_field: dict[str, bool] = {}
|
|
71
|
+
for name, sp in specs.items():
|
|
72
|
+
io = getattr(sp, "io", None)
|
|
73
|
+
if io is None:
|
|
74
|
+
allowed_by_field[name] = True
|
|
75
|
+
continue
|
|
76
|
+
in_verbs = getattr(io, "in_verbs", ())
|
|
77
|
+
mutable = getattr(io, "mutable_verbs", ())
|
|
78
|
+
allowed = (not in_verbs or verb in in_verbs) and (
|
|
79
|
+
not mutable or verb in mutable
|
|
80
|
+
)
|
|
81
|
+
allowed_by_field[name] = allowed
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
allowed_by_field,
|
|
85
|
+
_excluded_schema_fields(model, verb),
|
|
86
|
+
_table_python_types(model),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@lru_cache(maxsize=256)
|
|
91
|
+
def _table_python_types(model: type) -> Mapping[str, Any]:
|
|
92
|
+
table = getattr(model, "__table__", None)
|
|
93
|
+
columns = getattr(table, "columns", None)
|
|
94
|
+
if columns is None:
|
|
95
|
+
return {}
|
|
96
|
+
resolved: dict[str, Any] = {}
|
|
97
|
+
for col in columns:
|
|
98
|
+
try:
|
|
99
|
+
py_t = getattr(getattr(col, "type", None), "python_type", None)
|
|
100
|
+
except Exception:
|
|
101
|
+
py_t = None
|
|
102
|
+
if py_t is not None:
|
|
103
|
+
resolved[getattr(col, "name", "")] = py_t
|
|
104
|
+
return resolved
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _pk_columns(model: type) -> Tuple[Any, ...]:
|
|
108
|
+
table = getattr(model, "__table__", None)
|
|
109
|
+
if table is None:
|
|
110
|
+
raise ValueError(f"{model.__name__} has no __table__")
|
|
111
|
+
pks = tuple(table.primary_key.columns) # type: ignore[attr-defined]
|
|
112
|
+
if not pks:
|
|
113
|
+
raise ValueError(f"{model.__name__} has no primary key")
|
|
114
|
+
return pks
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _single_pk_name(model: type) -> str:
|
|
118
|
+
pks = _pk_columns(model)
|
|
119
|
+
if len(pks) != 1:
|
|
120
|
+
raise NotImplementedError(
|
|
121
|
+
f"{model.__name__} has composite PK; not supported by default core"
|
|
122
|
+
)
|
|
123
|
+
name = pks[0].name
|
|
124
|
+
return name
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _coerce_pk_value(model: type, value: Any) -> Any:
|
|
128
|
+
if value is None:
|
|
129
|
+
return None
|
|
130
|
+
try:
|
|
131
|
+
col = _pk_columns(model)[0]
|
|
132
|
+
py_type = col.type.python_type # type: ignore[attr-defined]
|
|
133
|
+
except Exception: # pragma: no cover - best effort
|
|
134
|
+
return value
|
|
135
|
+
if isinstance(value, py_type):
|
|
136
|
+
return value
|
|
137
|
+
try:
|
|
138
|
+
coerced = py_type(value)
|
|
139
|
+
return coerced
|
|
140
|
+
except Exception: # pragma: no cover - fallback to original
|
|
141
|
+
return value
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _model_columns(model: type) -> Tuple[str, ...]:
|
|
145
|
+
table = getattr(model, "__table__", None)
|
|
146
|
+
if table is None:
|
|
147
|
+
return ()
|
|
148
|
+
cols = tuple(c.name for c in table.columns)
|
|
149
|
+
return cols
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _colspecs(model: type) -> Mapping[str, Any]:
|
|
153
|
+
return _colspecs_cached(model, _colspecs_cache_token(model))
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _filter_in_values(
|
|
157
|
+
model: type, data: Mapping[str, Any], verb: str
|
|
158
|
+
) -> Dict[str, Any]:
|
|
159
|
+
allowed_by_field, excluded_fields, column_types = _filter_plan_cached(
|
|
160
|
+
model, verb, _filter_plan_cache_token(model, verb)
|
|
161
|
+
)
|
|
162
|
+
if not allowed_by_field and not excluded_fields:
|
|
163
|
+
return dict(data)
|
|
164
|
+
|
|
165
|
+
out: Dict[str, Any] = {}
|
|
166
|
+
for k, v in data.items():
|
|
167
|
+
allowed = allowed_by_field.get(k)
|
|
168
|
+
if allowed is None:
|
|
169
|
+
if k in excluded_fields:
|
|
170
|
+
continue
|
|
171
|
+
out[k] = v
|
|
172
|
+
continue
|
|
173
|
+
if allowed:
|
|
174
|
+
try:
|
|
175
|
+
py_t = column_types.get(k)
|
|
176
|
+
if py_t is not None and v is not None and not isinstance(v, py_t):
|
|
177
|
+
if py_t in (dt.datetime, dt.date) and isinstance(v, str):
|
|
178
|
+
parsed = py_t.fromisoformat(v)
|
|
179
|
+
out[k] = parsed
|
|
180
|
+
else:
|
|
181
|
+
out[k] = py_t(v)
|
|
182
|
+
else:
|
|
183
|
+
out[k] = v
|
|
184
|
+
except Exception:
|
|
185
|
+
# Best effort coercion only; preserve original value on failure.
|
|
186
|
+
out[k] = v
|
|
187
|
+
return out
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _immutable_columns(model: type, verb: str) -> set[str]:
|
|
191
|
+
specs = _colspecs(model)
|
|
192
|
+
if not specs:
|
|
193
|
+
return set()
|
|
194
|
+
imm: set[str] = set()
|
|
195
|
+
for name, sp in specs.items():
|
|
196
|
+
io = getattr(sp, "io", None)
|
|
197
|
+
mutable = getattr(io, "mutable_verbs", ()) if io else ()
|
|
198
|
+
if mutable and verb not in mutable:
|
|
199
|
+
imm.add(name)
|
|
200
|
+
return imm
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from functools import lru_cache
|
|
3
4
|
from typing import Any, Dict, List, Mapping, Optional, Union
|
|
5
|
+
import inspect
|
|
6
|
+
from weakref import WeakSet
|
|
4
7
|
|
|
5
8
|
import builtins as _builtins
|
|
6
9
|
import logging
|
|
@@ -33,10 +36,57 @@ from .helpers import (
|
|
|
33
36
|
|
|
34
37
|
logger = logging.getLogger("uvicorn")
|
|
35
38
|
|
|
39
|
+
_MAPPED_MODELS: "WeakSet[type]" = WeakSet()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@lru_cache(maxsize=512)
|
|
43
|
+
def _create_flush_requirements(model: type) -> tuple[str | None, tuple[str, ...]]:
|
|
44
|
+
"""Precompute model attributes that may need a create-time flush."""
|
|
45
|
+
table = getattr(model, "__table__", None)
|
|
46
|
+
pk_name: str | None = None
|
|
47
|
+
server_default_names: list[str] = []
|
|
48
|
+
|
|
49
|
+
if table is None:
|
|
50
|
+
return pk_name, ()
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
pk_cols = tuple(getattr(table, "primary_key", ()).columns)
|
|
54
|
+
except Exception:
|
|
55
|
+
pk_cols = ()
|
|
56
|
+
if len(pk_cols) == 1:
|
|
57
|
+
name = getattr(pk_cols[0], "name", None)
|
|
58
|
+
if isinstance(name, str) and name:
|
|
59
|
+
pk_name = name
|
|
60
|
+
|
|
61
|
+
for col in getattr(table, "columns", ()):
|
|
62
|
+
name = getattr(col, "name", None)
|
|
63
|
+
if not isinstance(name, str) or not name:
|
|
64
|
+
continue
|
|
65
|
+
if getattr(col, "server_default", None) is not None:
|
|
66
|
+
server_default_names.append(name)
|
|
67
|
+
|
|
68
|
+
return pk_name, tuple(server_default_names)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _requires_create_flush(model: type, obj: Any) -> bool:
|
|
72
|
+
"""Flush only when we need database-generated values immediately."""
|
|
73
|
+
pk_name, server_default_names = _create_flush_requirements(model)
|
|
74
|
+
|
|
75
|
+
if pk_name is not None and getattr(obj, pk_name, None) is None:
|
|
76
|
+
return True
|
|
77
|
+
|
|
78
|
+
for name in server_default_names:
|
|
79
|
+
if getattr(obj, name, None) is None:
|
|
80
|
+
return True
|
|
81
|
+
return False
|
|
82
|
+
|
|
36
83
|
|
|
37
84
|
def _ensure_model_mapped(model: type) -> None:
|
|
85
|
+
if model in _MAPPED_MODELS:
|
|
86
|
+
return
|
|
38
87
|
try:
|
|
39
88
|
_sa_inspect(model)
|
|
89
|
+
_MAPPED_MODELS.add(model)
|
|
40
90
|
return
|
|
41
91
|
except NoInspectionAvailable:
|
|
42
92
|
pass
|
|
@@ -46,6 +96,7 @@ def _ensure_model_mapped(model: type) -> None:
|
|
|
46
96
|
|
|
47
97
|
_materialize_colspecs_to_sqla(model)
|
|
48
98
|
TableBase.registry.map_declaratively(model)
|
|
99
|
+
_MAPPED_MODELS.add(model)
|
|
49
100
|
|
|
50
101
|
|
|
51
102
|
def _ensure_model_table(model: type, db: Union[Session, AsyncSession]) -> None:
|
|
@@ -92,15 +143,23 @@ async def create(
|
|
|
92
143
|
TableBase.registry.map_declaratively(model)
|
|
93
144
|
obj = model(**data)
|
|
94
145
|
db.add(obj)
|
|
146
|
+
needs_flush = _requires_create_flush(model, obj)
|
|
147
|
+
|
|
95
148
|
try:
|
|
96
|
-
|
|
149
|
+
if needs_flush:
|
|
150
|
+
await _maybe_flush(db)
|
|
97
151
|
except OperationalError as exc:
|
|
98
152
|
if "no such table" not in str(exc).lower():
|
|
99
153
|
raise
|
|
154
|
+
if hasattr(db, "rollback"):
|
|
155
|
+
rollback_result = db.rollback()
|
|
156
|
+
if inspect.isawaitable(rollback_result):
|
|
157
|
+
await rollback_result
|
|
100
158
|
_ensure_model_table(model, db)
|
|
101
159
|
obj = model(**data)
|
|
102
160
|
db.add(obj)
|
|
103
|
-
|
|
161
|
+
if needs_flush:
|
|
162
|
+
await _maybe_flush(db)
|
|
104
163
|
logger.debug("create persisted obj=%s", obj)
|
|
105
164
|
return obj
|
|
106
165
|
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import datetime as dt
|
|
4
|
-
from typing import Any, Dict, Mapping, Tuple
|
|
5
|
-
|
|
6
|
-
import logging
|
|
7
|
-
|
|
8
|
-
try:
|
|
9
|
-
from tigrbl_canon.mapping.column_mro_collect import mro_collect_columns
|
|
10
|
-
except Exception: # pragma: no cover
|
|
11
|
-
|
|
12
|
-
def mro_collect_columns(_model: type, _cache_bust: int | None = None):
|
|
13
|
-
return {}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
logger = logging.getLogger("uvicorn")
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def _pk_columns(model: type) -> Tuple[Any, ...]:
|
|
20
|
-
logger.debug("_pk_columns called with model=%s", model)
|
|
21
|
-
table = getattr(model, "__table__", None)
|
|
22
|
-
if table is None:
|
|
23
|
-
raise ValueError(f"{model.__name__} has no __table__")
|
|
24
|
-
pks = tuple(table.primary_key.columns) # type: ignore[attr-defined]
|
|
25
|
-
if not pks:
|
|
26
|
-
raise ValueError(f"{model.__name__} has no primary key")
|
|
27
|
-
logger.debug("_pk_columns returning %s", pks)
|
|
28
|
-
return pks
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def _single_pk_name(model: type) -> str:
|
|
32
|
-
logger.debug("_single_pk_name called with model=%s", model)
|
|
33
|
-
pks = _pk_columns(model)
|
|
34
|
-
if len(pks) != 1:
|
|
35
|
-
raise NotImplementedError(
|
|
36
|
-
f"{model.__name__} has composite PK; not supported by default core"
|
|
37
|
-
)
|
|
38
|
-
name = pks[0].name
|
|
39
|
-
logger.debug("_single_pk_name returning %s", name)
|
|
40
|
-
return name
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def _coerce_pk_value(model: type, value: Any) -> Any:
|
|
44
|
-
logger.debug("_coerce_pk_value called with model=%s value=%s", model, value)
|
|
45
|
-
if value is None:
|
|
46
|
-
return None
|
|
47
|
-
try:
|
|
48
|
-
col = _pk_columns(model)[0]
|
|
49
|
-
py_type = col.type.python_type # type: ignore[attr-defined]
|
|
50
|
-
except Exception: # pragma: no cover - best effort
|
|
51
|
-
logger.debug("_coerce_pk_value returning original value %s", value)
|
|
52
|
-
return value
|
|
53
|
-
if isinstance(value, py_type):
|
|
54
|
-
return value
|
|
55
|
-
try:
|
|
56
|
-
coerced = py_type(value)
|
|
57
|
-
logger.debug("_coerce_pk_value coerced %s to %s", value, coerced)
|
|
58
|
-
return coerced
|
|
59
|
-
except Exception: # pragma: no cover - fallback to original
|
|
60
|
-
logger.debug("_coerce_pk_value failed to coerce %s", value)
|
|
61
|
-
return value
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def _model_columns(model: type) -> Tuple[str, ...]:
|
|
65
|
-
logger.debug("_model_columns called with model=%s", model)
|
|
66
|
-
table = getattr(model, "__table__", None)
|
|
67
|
-
if table is None:
|
|
68
|
-
return ()
|
|
69
|
-
cols = tuple(c.name for c in table.columns)
|
|
70
|
-
logger.debug("_model_columns returning %s", cols)
|
|
71
|
-
return cols
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def _colspecs(model: type) -> Mapping[str, Any]:
|
|
75
|
-
logger.info("_colspecs called with model=%s", model)
|
|
76
|
-
cache_bust = hash(
|
|
77
|
-
(
|
|
78
|
-
id(getattr(model, "__tigrbl_colspecs__", None)),
|
|
79
|
-
id(getattr(model, "__tigrbl_cols__", None)),
|
|
80
|
-
)
|
|
81
|
-
)
|
|
82
|
-
specs = mro_collect_columns(model, _cache_bust=cache_bust)
|
|
83
|
-
logger.info("_colspecs returning %s", specs)
|
|
84
|
-
return specs
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def _filter_in_values(
|
|
88
|
-
model: type, data: Mapping[str, Any], verb: str
|
|
89
|
-
) -> Dict[str, Any]:
|
|
90
|
-
logger.info("_filter_in_values called with data=%s verb=%s", data, verb)
|
|
91
|
-
specs = _colspecs(model)
|
|
92
|
-
if not specs:
|
|
93
|
-
result = dict(data)
|
|
94
|
-
logger.debug("_filter_in_values returning %s", result)
|
|
95
|
-
return result
|
|
96
|
-
out: Dict[str, Any] = {}
|
|
97
|
-
for k, v in data.items():
|
|
98
|
-
sp = specs.get(k)
|
|
99
|
-
if sp is None:
|
|
100
|
-
out[k] = v
|
|
101
|
-
continue
|
|
102
|
-
io = getattr(sp, "io", None)
|
|
103
|
-
allowed = True
|
|
104
|
-
if io is not None:
|
|
105
|
-
in_verbs = getattr(io, "in_verbs", ())
|
|
106
|
-
mutable = getattr(io, "mutable_verbs", ())
|
|
107
|
-
if in_verbs and verb not in in_verbs:
|
|
108
|
-
allowed = False
|
|
109
|
-
if mutable and verb not in mutable:
|
|
110
|
-
allowed = False
|
|
111
|
-
if allowed:
|
|
112
|
-
try:
|
|
113
|
-
col = getattr(getattr(model, "__table__", None), "columns", {}).get(k)
|
|
114
|
-
py_t = getattr(getattr(col, "type", None), "python_type", None)
|
|
115
|
-
if py_t is not None and v is not None and not isinstance(v, py_t):
|
|
116
|
-
if py_t in (dt.datetime, dt.date) and isinstance(v, str):
|
|
117
|
-
parsed = py_t.fromisoformat(v)
|
|
118
|
-
out[k] = parsed
|
|
119
|
-
else:
|
|
120
|
-
out[k] = py_t(v)
|
|
121
|
-
else:
|
|
122
|
-
out[k] = v
|
|
123
|
-
except Exception:
|
|
124
|
-
# Best effort coercion only; preserve original value on failure.
|
|
125
|
-
out[k] = v
|
|
126
|
-
logger.info("_filter_in_values returning %s", out)
|
|
127
|
-
return out
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
def _immutable_columns(model: type, verb: str) -> set[str]:
|
|
131
|
-
logger.info("_immutable_columns called with model=%s verb=%s", model, verb)
|
|
132
|
-
specs = _colspecs(model)
|
|
133
|
-
if not specs:
|
|
134
|
-
return set()
|
|
135
|
-
imm: set[str] = set()
|
|
136
|
-
for name, sp in specs.items():
|
|
137
|
-
io = getattr(sp, "io", None)
|
|
138
|
-
mutable = getattr(io, "mutable_verbs", ()) if io else ()
|
|
139
|
-
if mutable and verb not in mutable:
|
|
140
|
-
imm.add(name)
|
|
141
|
-
logger.info("_immutable_columns returning %s", imm)
|
|
142
|
-
return imm
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tigrbl_ops_oltp-0.1.0.dev1 → tigrbl_ops_oltp-0.1.0.dev7}/tigrbl_ops_oltp/crud/helpers/filters.py
RENAMED
|
File without changes
|
{tigrbl_ops_oltp-0.1.0.dev1 → tigrbl_ops_oltp-0.1.0.dev7}/tigrbl_ops_oltp/crud/helpers/normalize.py
RENAMED
|
File without changes
|
|
File without changes
|