tigrbl-ops-oltp 0.4.5.dev1__tar.gz → 0.4.5.dev6__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.4.5.dev1 → tigrbl_ops_oltp-0.4.5.dev6}/PKG-INFO +7 -7
- {tigrbl_ops_oltp-0.4.5.dev1 → tigrbl_ops_oltp-0.4.5.dev6}/README.md +5 -5
- {tigrbl_ops_oltp-0.4.5.dev1 → tigrbl_ops_oltp-0.4.5.dev6}/pyproject.toml +2 -2
- {tigrbl_ops_oltp-0.4.5.dev1 → tigrbl_ops_oltp-0.4.5.dev6}/tigrbl_ops_oltp/crud/bulk.py +1 -2
- {tigrbl_ops_oltp-0.4.5.dev1 → tigrbl_ops_oltp-0.4.5.dev6}/tigrbl_ops_oltp/crud/helpers/__init__.py +2 -4
- {tigrbl_ops_oltp-0.4.5.dev1 → tigrbl_ops_oltp-0.4.5.dev6}/tigrbl_ops_oltp/crud/helpers/filters.py +18 -12
- {tigrbl_ops_oltp-0.4.5.dev1 → tigrbl_ops_oltp-0.4.5.dev6}/tigrbl_ops_oltp/crud/helpers/model.py +5 -56
- {tigrbl_ops_oltp-0.4.5.dev1 → tigrbl_ops_oltp-0.4.5.dev6}/tigrbl_ops_oltp/crud/ops.py +68 -16
- {tigrbl_ops_oltp-0.4.5.dev1 → tigrbl_ops_oltp-0.4.5.dev6}/LICENSE +0 -0
- {tigrbl_ops_oltp-0.4.5.dev1 → tigrbl_ops_oltp-0.4.5.dev6}/NOTICE +0 -0
- {tigrbl_ops_oltp-0.4.5.dev1 → tigrbl_ops_oltp-0.4.5.dev6}/tigrbl_ops_oltp/__init__.py +0 -0
- {tigrbl_ops_oltp-0.4.5.dev1 → tigrbl_ops_oltp-0.4.5.dev6}/tigrbl_ops_oltp/crud/__init__.py +0 -0
- {tigrbl_ops_oltp-0.4.5.dev1 → tigrbl_ops_oltp-0.4.5.dev6}/tigrbl_ops_oltp/crud/helpers/db.py +0 -0
- {tigrbl_ops_oltp-0.4.5.dev1 → tigrbl_ops_oltp-0.4.5.dev6}/tigrbl_ops_oltp/crud/helpers/enum.py +0 -0
- {tigrbl_ops_oltp-0.4.5.dev1 → tigrbl_ops_oltp-0.4.5.dev6}/tigrbl_ops_oltp/crud/helpers/normalize.py +0 -0
- {tigrbl_ops_oltp-0.4.5.dev1 → tigrbl_ops_oltp-0.4.5.dev6}/tigrbl_ops_oltp/crud/params.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tigrbl-ops-oltp
|
|
3
|
-
Version: 0.4.5.
|
|
3
|
+
Version: 0.4.5.dev6
|
|
4
4
|
Summary: Transactional OLTP operation handlers for Tigrbl CRUD, bulk, REST, JSON-RPC, and database-backed workloads.
|
|
5
5
|
License: Apache License
|
|
6
6
|
Version 2.0, January 2004
|
|
@@ -232,7 +232,7 @@ Project-URL: Discord, https://discord.gg/K4YTAPapjR
|
|
|
232
232
|
Project-URL: Homepage, https://github.com/tigrbl/tigrbl
|
|
233
233
|
Project-URL: Issues, https://github.com/tigrbl/tigrbl/issues
|
|
234
234
|
Project-URL: Organization, https://github.com/tigrbl
|
|
235
|
-
Project-URL: Repository, https://github.com/tigrbl/tigrbl/tree/master/pkgs/
|
|
235
|
+
Project-URL: Repository, https://github.com/tigrbl/tigrbl/tree/master/pkgs/60_ops/tigrbl_ops_oltp
|
|
236
236
|
Description-Content-Type: text/markdown
|
|
237
237
|
|
|
238
238
|
<div align="center">
|
|
@@ -242,7 +242,7 @@ Description-Content-Type: text/markdown
|
|
|
242
242
|
<a href="https://pypi.org/project/tigrbl-ops-oltp/"><img src="https://img.shields.io/pypi/v/tigrbl-ops-oltp?label=PyPI" alt="PyPI version for tigrbl-ops-oltp"/></a>
|
|
243
243
|
<a href="https://pypi.org/project/tigrbl-ops-oltp/"><img src="https://static.pepy.tech/badge/tigrbl-ops-oltp" alt="Downloads for tigrbl-ops-oltp"/></a>
|
|
244
244
|
<a href="https://discord.gg/K4YTAPapjR"><img src="https://img.shields.io/badge/Discord-Join%20chat-5865F2?logo=discord&logoColor=white" alt="Discord community for tigrbl-ops-oltp"/></a>
|
|
245
|
-
<a href="https://github.com/tigrbl/tigrbl/blob/master/pkgs/
|
|
245
|
+
<a href="https://github.com/tigrbl/tigrbl/blob/master/pkgs/60_ops/tigrbl_ops_oltp/README.md"><img src="https://hits.sh/github.com/tigrbl/tigrbl/blob/master/pkgs/60_ops/tigrbl_ops_oltp/README.md.svg?label=hits" alt="Repository hits for tigrbl-ops-oltp README"/></a>
|
|
246
246
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache%202.0-525252" alt="Apache 2.0 license"/></a>
|
|
247
247
|
<a href="pyproject.toml"><img src="https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-3776ab" alt="Python versions 3.10 | 3.11 | 3.12 | 3.13 | 3.14 for tigrbl-ops-oltp"/></a>
|
|
248
248
|
<a href="https://github.com/tigrbl/tigrbl/blob/master/docs/README.md"><img src="https://img.shields.io/badge/workspace-core-1f6feb" alt="Workspace group for tigrbl-ops-oltp"/></a>
|
|
@@ -266,7 +266,7 @@ Framework maintainers and extension authors working near Tigrbl operation dispat
|
|
|
266
266
|
|
|
267
267
|
## Where does tigrbl-ops-oltp fit?
|
|
268
268
|
|
|
269
|
-
`tigrbl-ops-oltp` lives at `pkgs/
|
|
269
|
+
`tigrbl-ops-oltp` lives at `pkgs/60_ops/tigrbl_ops_oltp` and serves focused operation dispatch behavior inside the Tigrbl runtime model.
|
|
270
270
|
|
|
271
271
|
## How does tigrbl-ops-oltp work?
|
|
272
272
|
|
|
@@ -288,7 +288,7 @@ pip install tigrbl-ops-oltp
|
|
|
288
288
|
| Surface | Value |
|
|
289
289
|
|---|---|
|
|
290
290
|
| PyPI package | [`tigrbl-ops-oltp`](https://pypi.org/project/tigrbl-ops-oltp/) |
|
|
291
|
-
| Repository path | [`pkgs/
|
|
291
|
+
| Repository path | [`pkgs/60_ops/tigrbl_ops_oltp`](https://github.com/tigrbl/tigrbl/tree/master/pkgs/60_ops/tigrbl_ops_oltp) |
|
|
292
292
|
| Python import root | `tigrbl_ops_oltp` |
|
|
293
293
|
| Console scripts | none declared |
|
|
294
294
|
| Entry points | none declared |
|
|
@@ -375,7 +375,7 @@ Choose `tigrbl-ops-oltp` when the quick-answer table matches your use case. Choo
|
|
|
375
375
|
|
|
376
376
|
- Community: [Discord](https://discord.gg/K4YTAPapjR).
|
|
377
377
|
- Issues: [GitHub Issues](https://github.com/tigrbl/tigrbl/issues).
|
|
378
|
-
- Repository: [pkgs/
|
|
378
|
+
- Repository: [pkgs/60_ops/tigrbl_ops_oltp](https://github.com/tigrbl/tigrbl/tree/master/pkgs/60_ops/tigrbl_ops_oltp).
|
|
379
379
|
|
|
380
380
|
## Package-local Boundary
|
|
381
381
|
|
|
@@ -386,7 +386,7 @@ This README is the package-local distribution entry point for `tigrbl-ops-oltp`.
|
|
|
386
386
|
- Package status: governed package in the `tigrbl/tigrbl` workspace.
|
|
387
387
|
- Governance source: [SSOT registry](https://github.com/tigrbl/tigrbl/blob/master/.ssot/registry.json).
|
|
388
388
|
- Release evidence: [publish workflow](https://github.com/tigrbl/tigrbl/actions/workflows/publish.yml) validates package builds, tests, GitHub release assets, and PyPI publication for managed packages.
|
|
389
|
-
- Local certification guard: `pkgs/
|
|
389
|
+
- Local certification guard: `pkgs/97_tests/tigrbl_tests/tests/unit/test_package_badges_and_notices.py` verifies every package README keeps the Discord badge, Apache 2.0 badge, explicit Python-version badge, `LICENSE`, and `NOTICE`.
|
|
390
390
|
- Scope note: this README documents the package boundary. Runtime feature support remains governed by `.ssot/` entities and the conformance docs linked below.
|
|
391
391
|
|
|
392
392
|
## License
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<a href="https://pypi.org/project/tigrbl-ops-oltp/"><img src="https://img.shields.io/pypi/v/tigrbl-ops-oltp?label=PyPI" alt="PyPI version for tigrbl-ops-oltp"/></a>
|
|
6
6
|
<a href="https://pypi.org/project/tigrbl-ops-oltp/"><img src="https://static.pepy.tech/badge/tigrbl-ops-oltp" alt="Downloads for tigrbl-ops-oltp"/></a>
|
|
7
7
|
<a href="https://discord.gg/K4YTAPapjR"><img src="https://img.shields.io/badge/Discord-Join%20chat-5865F2?logo=discord&logoColor=white" alt="Discord community for tigrbl-ops-oltp"/></a>
|
|
8
|
-
<a href="https://github.com/tigrbl/tigrbl/blob/master/pkgs/
|
|
8
|
+
<a href="https://github.com/tigrbl/tigrbl/blob/master/pkgs/60_ops/tigrbl_ops_oltp/README.md"><img src="https://hits.sh/github.com/tigrbl/tigrbl/blob/master/pkgs/60_ops/tigrbl_ops_oltp/README.md.svg?label=hits" alt="Repository hits for tigrbl-ops-oltp README"/></a>
|
|
9
9
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache%202.0-525252" alt="Apache 2.0 license"/></a>
|
|
10
10
|
<a href="pyproject.toml"><img src="https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-3776ab" alt="Python versions 3.10 | 3.11 | 3.12 | 3.13 | 3.14 for tigrbl-ops-oltp"/></a>
|
|
11
11
|
<a href="https://github.com/tigrbl/tigrbl/blob/master/docs/README.md"><img src="https://img.shields.io/badge/workspace-core-1f6feb" alt="Workspace group for tigrbl-ops-oltp"/></a>
|
|
@@ -29,7 +29,7 @@ Framework maintainers and extension authors working near Tigrbl operation dispat
|
|
|
29
29
|
|
|
30
30
|
## Where does tigrbl-ops-oltp fit?
|
|
31
31
|
|
|
32
|
-
`tigrbl-ops-oltp` lives at `pkgs/
|
|
32
|
+
`tigrbl-ops-oltp` lives at `pkgs/60_ops/tigrbl_ops_oltp` and serves focused operation dispatch behavior inside the Tigrbl runtime model.
|
|
33
33
|
|
|
34
34
|
## How does tigrbl-ops-oltp work?
|
|
35
35
|
|
|
@@ -51,7 +51,7 @@ pip install tigrbl-ops-oltp
|
|
|
51
51
|
| Surface | Value |
|
|
52
52
|
|---|---|
|
|
53
53
|
| PyPI package | [`tigrbl-ops-oltp`](https://pypi.org/project/tigrbl-ops-oltp/) |
|
|
54
|
-
| Repository path | [`pkgs/
|
|
54
|
+
| Repository path | [`pkgs/60_ops/tigrbl_ops_oltp`](https://github.com/tigrbl/tigrbl/tree/master/pkgs/60_ops/tigrbl_ops_oltp) |
|
|
55
55
|
| Python import root | `tigrbl_ops_oltp` |
|
|
56
56
|
| Console scripts | none declared |
|
|
57
57
|
| Entry points | none declared |
|
|
@@ -138,7 +138,7 @@ Choose `tigrbl-ops-oltp` when the quick-answer table matches your use case. Choo
|
|
|
138
138
|
|
|
139
139
|
- Community: [Discord](https://discord.gg/K4YTAPapjR).
|
|
140
140
|
- Issues: [GitHub Issues](https://github.com/tigrbl/tigrbl/issues).
|
|
141
|
-
- Repository: [pkgs/
|
|
141
|
+
- Repository: [pkgs/60_ops/tigrbl_ops_oltp](https://github.com/tigrbl/tigrbl/tree/master/pkgs/60_ops/tigrbl_ops_oltp).
|
|
142
142
|
|
|
143
143
|
## Package-local Boundary
|
|
144
144
|
|
|
@@ -149,7 +149,7 @@ This README is the package-local distribution entry point for `tigrbl-ops-oltp`.
|
|
|
149
149
|
- Package status: governed package in the `tigrbl/tigrbl` workspace.
|
|
150
150
|
- Governance source: [SSOT registry](https://github.com/tigrbl/tigrbl/blob/master/.ssot/registry.json).
|
|
151
151
|
- Release evidence: [publish workflow](https://github.com/tigrbl/tigrbl/actions/workflows/publish.yml) validates package builds, tests, GitHub release assets, and PyPI publication for managed packages.
|
|
152
|
-
- Local certification guard: `pkgs/
|
|
152
|
+
- Local certification guard: `pkgs/97_tests/tigrbl_tests/tests/unit/test_package_badges_and_notices.py` verifies every package README keeps the Discord badge, Apache 2.0 badge, explicit Python-version badge, `LICENSE`, and `NOTICE`.
|
|
153
153
|
- Scope note: this README documents the package boundary. Runtime feature support remains governed by `.ssot/` entities and the conformance docs linked below.
|
|
154
154
|
|
|
155
155
|
## License
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tigrbl-ops-oltp"
|
|
3
|
-
version = "0.4.5.
|
|
3
|
+
version = "0.4.5.dev6"
|
|
4
4
|
description = "Transactional OLTP operation handlers for Tigrbl CRUD, bulk, REST, JSON-RPC, and database-backed workloads."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { file = "LICENSE" }
|
|
@@ -52,7 +52,7 @@ dependencies = [
|
|
|
52
52
|
Organization = "https://github.com/tigrbl"
|
|
53
53
|
Discord = "https://discord.gg/K4YTAPapjR"
|
|
54
54
|
Homepage = "https://github.com/tigrbl/tigrbl"
|
|
55
|
-
Repository = "https://github.com/tigrbl/tigrbl/tree/master/pkgs/
|
|
55
|
+
Repository = "https://github.com/tigrbl/tigrbl/tree/master/pkgs/60_ops/tigrbl_ops_oltp"
|
|
56
56
|
Issues = "https://github.com/tigrbl/tigrbl/issues"
|
|
57
57
|
|
|
58
58
|
[tool.uv.sources]
|
|
@@ -9,7 +9,6 @@ from .helpers import (
|
|
|
9
9
|
AsyncSession,
|
|
10
10
|
Session,
|
|
11
11
|
sa_delete,
|
|
12
|
-
_coerce_pk_value,
|
|
13
12
|
_immutable_columns,
|
|
14
13
|
_maybe_delete,
|
|
15
14
|
_maybe_execute,
|
|
@@ -113,7 +112,7 @@ async def bulk_merge(
|
|
|
113
112
|
to_create: List[Mapping[str, Any]] = []
|
|
114
113
|
for r in rows or ():
|
|
115
114
|
r = dict(r)
|
|
116
|
-
ident =
|
|
115
|
+
ident = r.get(pk)
|
|
117
116
|
if ident is not None:
|
|
118
117
|
existing = await _maybe_get(db, model, ident)
|
|
119
118
|
if existing is not None:
|
{tigrbl_ops_oltp-0.4.5.dev1 → tigrbl_ops_oltp-0.4.5.dev6}/tigrbl_ops_oltp/crud/helpers/__init__.py
RENAMED
|
@@ -18,13 +18,12 @@ except Exception: # pragma: no cover
|
|
|
18
18
|
from .model import (
|
|
19
19
|
_pk_columns,
|
|
20
20
|
_single_pk_name,
|
|
21
|
-
_coerce_pk_value,
|
|
22
21
|
_model_columns,
|
|
23
22
|
_colspecs,
|
|
24
23
|
_filter_in_values,
|
|
25
24
|
_immutable_columns,
|
|
26
25
|
)
|
|
27
|
-
from .filters import _CANON_OPS,
|
|
26
|
+
from .filters import _CANON_OPS, _authorize_filters, _apply_filters, _apply_sort
|
|
28
27
|
from .db import (
|
|
29
28
|
_is_async_db,
|
|
30
29
|
_maybe_get,
|
|
@@ -51,8 +50,7 @@ __all__ = [
|
|
|
51
50
|
"_apply_filters",
|
|
52
51
|
"_apply_sort",
|
|
53
52
|
"_CANON_OPS",
|
|
54
|
-
"
|
|
55
|
-
"_coerce_pk_value",
|
|
53
|
+
"_authorize_filters",
|
|
56
54
|
"_colspecs",
|
|
57
55
|
"_filter_in_values",
|
|
58
56
|
"_immutable_columns",
|
{tigrbl_ops_oltp-0.4.5.dev1 → tigrbl_ops_oltp-0.4.5.dev6}/tigrbl_ops_oltp/crud/helpers/filters.py
RENAMED
|
@@ -4,6 +4,8 @@ from typing import Any, Dict, Iterable, Mapping, Optional, Sequence
|
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
6
|
|
|
7
|
+
from tigrbl_typing.status.exceptions import HTTPException
|
|
8
|
+
|
|
7
9
|
from . import select, and_, asc, desc
|
|
8
10
|
from .model import _model_columns, _colspecs
|
|
9
11
|
|
|
@@ -36,29 +38,33 @@ _CANON_OPS = {
|
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
|
|
39
|
-
def
|
|
40
|
-
model: type, filters: Optional[Mapping[str, Any]]
|
|
41
|
+
def _authorize_filters(
|
|
42
|
+
model: type, filters: Optional[Mapping[str, Any]], *, strict: bool = False
|
|
41
43
|
) -> Dict[str, Any]:
|
|
42
|
-
logger.debug("
|
|
44
|
+
logger.debug("_authorize_filters called with filters=%s", filters)
|
|
43
45
|
cols = set(_model_columns(model))
|
|
44
46
|
specs = _colspecs(model)
|
|
45
47
|
raw = dict(filters or {})
|
|
46
48
|
out: Dict[str, Any] = {}
|
|
47
49
|
for k, v in raw.items():
|
|
48
50
|
name, op = k.split("__", 1) if "__" in k else (k, "eq")
|
|
49
|
-
if name not in cols:
|
|
50
|
-
continue
|
|
51
51
|
canon = _CANON_OPS.get(op, op)
|
|
52
52
|
sp = specs.get(name)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
io = getattr(sp, "io", None)
|
|
54
|
+
ops = {_CANON_OPS.get(o, o) for o in getattr(io, "filter_ops", ()) or ()}
|
|
55
|
+
if (
|
|
56
|
+
name not in cols
|
|
57
|
+
or (sp is not None and canon not in ops)
|
|
58
|
+
or (strict and op not in _CANON_OPS)
|
|
59
|
+
):
|
|
60
|
+
if strict:
|
|
61
|
+
raise HTTPException(
|
|
62
|
+
status_code=422, detail=f"Unsupported clear filter: {k}"
|
|
63
|
+
)
|
|
64
|
+
continue
|
|
59
65
|
key = name if canon == "eq" else f"{name}__{canon}"
|
|
60
66
|
out[key] = v
|
|
61
|
-
logger.debug("
|
|
67
|
+
logger.debug("_authorize_filters returning %s", out)
|
|
62
68
|
return out
|
|
63
69
|
|
|
64
70
|
|
{tigrbl_ops_oltp-0.4.5.dev1 → tigrbl_ops_oltp-0.4.5.dev6}/tigrbl_ops_oltp/crud/helpers/model.py
RENAMED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import datetime as dt
|
|
4
3
|
from functools import lru_cache
|
|
5
4
|
from typing import Any, Dict, Mapping, Tuple
|
|
6
5
|
|
|
@@ -61,11 +60,11 @@ def _filter_plan_cache_token(model: type, verb: str) -> tuple[int, int, int, int
|
|
|
61
60
|
@lru_cache(maxsize=512)
|
|
62
61
|
def _filter_plan_cached(
|
|
63
62
|
model: type, verb: str, cache_token: tuple[int, int, int, int, int]
|
|
64
|
-
) -> tuple[Mapping[str, bool], frozenset[str]
|
|
63
|
+
) -> tuple[Mapping[str, bool], frozenset[str]]:
|
|
65
64
|
del cache_token
|
|
66
65
|
specs = _colspecs(model)
|
|
67
66
|
if not specs:
|
|
68
|
-
return {}, frozenset()
|
|
67
|
+
return {}, frozenset()
|
|
69
68
|
|
|
70
69
|
allowed_by_field: dict[str, bool] = {}
|
|
71
70
|
for name, sp in specs.items():
|
|
@@ -80,28 +79,7 @@ def _filter_plan_cached(
|
|
|
80
79
|
)
|
|
81
80
|
allowed_by_field[name] = allowed
|
|
82
81
|
|
|
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
|
|
82
|
+
return allowed_by_field, _excluded_schema_fields(model, verb)
|
|
105
83
|
|
|
106
84
|
|
|
107
85
|
def _pk_columns(model: type) -> Tuple[Any, ...]:
|
|
@@ -124,23 +102,6 @@ def _single_pk_name(model: type) -> str:
|
|
|
124
102
|
return name
|
|
125
103
|
|
|
126
104
|
|
|
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
105
|
def _model_columns(model: type) -> Tuple[str, ...]:
|
|
145
106
|
table = getattr(model, "__table__", None)
|
|
146
107
|
if table is None:
|
|
@@ -156,7 +117,7 @@ def _colspecs(model: type) -> Mapping[str, Any]:
|
|
|
156
117
|
def _filter_in_values(
|
|
157
118
|
model: type, data: Mapping[str, Any], verb: str
|
|
158
119
|
) -> Dict[str, Any]:
|
|
159
|
-
allowed_by_field, excluded_fields
|
|
120
|
+
allowed_by_field, excluded_fields = _filter_plan_cached(
|
|
160
121
|
model, verb, _filter_plan_cache_token(model, verb)
|
|
161
122
|
)
|
|
162
123
|
if not allowed_by_field and not excluded_fields:
|
|
@@ -171,19 +132,7 @@ def _filter_in_values(
|
|
|
171
132
|
out[k] = v
|
|
172
133
|
continue
|
|
173
134
|
if allowed:
|
|
174
|
-
|
|
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
|
|
135
|
+
out[k] = v
|
|
187
136
|
return out
|
|
188
137
|
|
|
189
138
|
|
|
@@ -21,8 +21,7 @@ from .helpers import (
|
|
|
21
21
|
sa_delete,
|
|
22
22
|
_apply_filters,
|
|
23
23
|
_apply_sort,
|
|
24
|
-
|
|
25
|
-
_coerce_pk_value,
|
|
24
|
+
_authorize_filters,
|
|
26
25
|
_filter_in_values,
|
|
27
26
|
_immutable_columns,
|
|
28
27
|
_maybe_delete,
|
|
@@ -38,6 +37,20 @@ from .helpers import (
|
|
|
38
37
|
logger = logging.getLogger("uvicorn")
|
|
39
38
|
|
|
40
39
|
_MAPPED_MODELS: "WeakSet[type]" = WeakSet()
|
|
40
|
+
_NO_NATIVE_OPERATION = object()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
async def _call_native_operation(
|
|
44
|
+
db: Any,
|
|
45
|
+
name: str,
|
|
46
|
+
*args: Any,
|
|
47
|
+
**kwargs: Any,
|
|
48
|
+
) -> Any:
|
|
49
|
+
operation = getattr(db, name, None)
|
|
50
|
+
if not callable(operation):
|
|
51
|
+
return _NO_NATIVE_OPERATION
|
|
52
|
+
result = operation(*args, **kwargs)
|
|
53
|
+
return await result if inspect.isawaitable(result) else result
|
|
41
54
|
|
|
42
55
|
|
|
43
56
|
@lru_cache(maxsize=512)
|
|
@@ -127,9 +140,12 @@ async def create(
|
|
|
127
140
|
Flush-only (commit happens later in TX_COMMIT).
|
|
128
141
|
"""
|
|
129
142
|
logger.debug("create called with model=%s data=%s", model, data)
|
|
130
|
-
_ensure_model_mapped(model)
|
|
131
143
|
data = _filter_in_values(model, data or {}, "create")
|
|
132
144
|
_validate_enum_values(model, data)
|
|
145
|
+
native = await _call_native_operation(db, "create_record", model, data)
|
|
146
|
+
if native is not _NO_NATIVE_OPERATION:
|
|
147
|
+
return native
|
|
148
|
+
_ensure_model_mapped(model)
|
|
133
149
|
obj = model(**data)
|
|
134
150
|
if hasattr(db, "add"):
|
|
135
151
|
try:
|
|
@@ -170,6 +186,9 @@ async def read(model: type, ident: Any, db: Union[Session, AsyncSession]) -> Any
|
|
|
170
186
|
Load a single row by primary key. Raises NoResultFound if not found.
|
|
171
187
|
"""
|
|
172
188
|
logger.debug("read called with model=%s ident=%s", model, ident)
|
|
189
|
+
native = await _call_native_operation(db, "read_record", model, ident)
|
|
190
|
+
if native is not _NO_NATIVE_OPERATION:
|
|
191
|
+
return native
|
|
173
192
|
_ensure_model_mapped(model)
|
|
174
193
|
obj = await _maybe_get(db, model, ident)
|
|
175
194
|
if obj is None:
|
|
@@ -233,7 +252,6 @@ async def merge(
|
|
|
233
252
|
logger.debug("merge called with model=%s ident=%s data=%s", model, ident, data)
|
|
234
253
|
_ensure_model_mapped(model)
|
|
235
254
|
pk = _single_pk_name(model)
|
|
236
|
-
ident = _coerce_pk_value(model, ident)
|
|
237
255
|
obj = await _maybe_get(db, model, ident)
|
|
238
256
|
|
|
239
257
|
verb = "update" if obj is not None else "create"
|
|
@@ -260,6 +278,9 @@ async def delete(
|
|
|
260
278
|
Flush-only.
|
|
261
279
|
"""
|
|
262
280
|
logger.debug("delete called with model=%s ident=%s", model, ident)
|
|
281
|
+
native = await _call_native_operation(db, "delete_record", model, ident)
|
|
282
|
+
if native is not _NO_NATIVE_OPERATION:
|
|
283
|
+
return native
|
|
263
284
|
_ensure_model_mapped(model)
|
|
264
285
|
obj = await read(model, ident, db)
|
|
265
286
|
await _maybe_delete(db, obj)
|
|
@@ -280,13 +301,23 @@ async def list(*_args: Any, **_kwargs: Any) -> List[Any]: # noqa: A001 (shadow
|
|
|
280
301
|
"""
|
|
281
302
|
logger.debug("list called with args=%s kwargs=%s", _args, _kwargs)
|
|
282
303
|
model, params = _normalize_list_call(_args, _kwargs)
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
filters: Mapping[str, Any] = _coerce_filters(model, params["filters"])
|
|
304
|
+
filters: Mapping[str, Any] = _authorize_filters(model, params["filters"])
|
|
286
305
|
skip: Optional[int] = params["skip"]
|
|
287
306
|
limit: Optional[int] = params["limit"]
|
|
288
307
|
db: Union[Session, AsyncSession] = params["db"]
|
|
289
308
|
sort = params["sort"]
|
|
309
|
+
native = await _call_native_operation(
|
|
310
|
+
db,
|
|
311
|
+
"list_records",
|
|
312
|
+
model,
|
|
313
|
+
filters=filters,
|
|
314
|
+
skip=skip,
|
|
315
|
+
limit=limit,
|
|
316
|
+
sort=sort,
|
|
317
|
+
)
|
|
318
|
+
if native is not _NO_NATIVE_OPERATION:
|
|
319
|
+
return native
|
|
320
|
+
_ensure_model_mapped(model)
|
|
290
321
|
|
|
291
322
|
if select is None: # pragma: no cover
|
|
292
323
|
# Fallback: legacy query API
|
|
@@ -331,13 +362,23 @@ async def clear(
|
|
|
331
362
|
# Reuse normalizer to accept the same shapes
|
|
332
363
|
logger.debug("clear called with args=%s kwargs=%s", args, kwargs)
|
|
333
364
|
model, params = _normalize_list_call(args, kwargs)
|
|
334
|
-
|
|
335
|
-
|
|
365
|
+
filters: Mapping[str, Any] = _authorize_filters(
|
|
366
|
+
model, params["filters"], strict=True
|
|
367
|
+
)
|
|
336
368
|
db: Union[Session, AsyncSession] = params["db"]
|
|
369
|
+
native = await _call_native_operation(
|
|
370
|
+
db,
|
|
371
|
+
"clear_records",
|
|
372
|
+
model,
|
|
373
|
+
filters=filters,
|
|
374
|
+
)
|
|
375
|
+
if native is not _NO_NATIVE_OPERATION:
|
|
376
|
+
return native
|
|
377
|
+
_ensure_model_mapped(model)
|
|
337
378
|
|
|
338
379
|
if sa_delete is None: # pragma: no cover
|
|
339
380
|
# Fallback path: manual iteration
|
|
340
|
-
items = await list(model,
|
|
381
|
+
items = await list(model, filters, db=db)
|
|
341
382
|
n = 0
|
|
342
383
|
for obj in items:
|
|
343
384
|
await _maybe_delete(db, obj)
|
|
@@ -345,8 +386,7 @@ async def clear(
|
|
|
345
386
|
await _maybe_flush(db)
|
|
346
387
|
return {"deleted": n}
|
|
347
388
|
|
|
348
|
-
|
|
349
|
-
where = _apply_filters(model, filt)
|
|
389
|
+
where = _apply_filters(model, filters)
|
|
350
390
|
stmt = sa_delete(model)
|
|
351
391
|
if where is not None:
|
|
352
392
|
stmt = stmt.where(where)
|
|
@@ -361,10 +401,17 @@ async def clear(
|
|
|
361
401
|
async def count(*args: Any, **kwargs: Any) -> Dict[str, int]:
|
|
362
402
|
"""Count rows matching equality filters."""
|
|
363
403
|
model, params = _normalize_list_call(args, kwargs)
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
filters: Mapping[str, Any] = _coerce_filters(model, params["filters"])
|
|
404
|
+
filters: Mapping[str, Any] = _authorize_filters(model, params["filters"])
|
|
367
405
|
db: Union[Session, AsyncSession] = params["db"]
|
|
406
|
+
native = await _call_native_operation(
|
|
407
|
+
db,
|
|
408
|
+
"count_records",
|
|
409
|
+
model,
|
|
410
|
+
filters=filters,
|
|
411
|
+
)
|
|
412
|
+
if native is not _NO_NATIVE_OPERATION:
|
|
413
|
+
return native
|
|
414
|
+
_ensure_model_mapped(model)
|
|
368
415
|
stmt = select(func.count()).select_from(model)
|
|
369
416
|
where = _apply_filters(model, filters)
|
|
370
417
|
if where is not None:
|
|
@@ -374,8 +421,13 @@ async def count(*args: Any, **kwargs: Any) -> Dict[str, int]:
|
|
|
374
421
|
return {"count": int(value or 0)}
|
|
375
422
|
|
|
376
423
|
|
|
377
|
-
async def exists(
|
|
424
|
+
async def exists(
|
|
425
|
+
model: type, ident: Any, db: Union[Session, AsyncSession]
|
|
426
|
+
) -> Dict[str, bool]:
|
|
378
427
|
"""Check whether a row exists by primary key."""
|
|
428
|
+
native = await _call_native_operation(db, "exists_record", model, ident)
|
|
429
|
+
if native is not _NO_NATIVE_OPERATION:
|
|
430
|
+
return native
|
|
379
431
|
_ensure_model_mapped(model)
|
|
380
432
|
obj = await _maybe_get(db, model, ident)
|
|
381
433
|
return {"exists": obj is not None}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tigrbl_ops_oltp-0.4.5.dev1 → tigrbl_ops_oltp-0.4.5.dev6}/tigrbl_ops_oltp/crud/helpers/db.py
RENAMED
|
File without changes
|
{tigrbl_ops_oltp-0.4.5.dev1 → tigrbl_ops_oltp-0.4.5.dev6}/tigrbl_ops_oltp/crud/helpers/enum.py
RENAMED
|
File without changes
|
{tigrbl_ops_oltp-0.4.5.dev1 → tigrbl_ops_oltp-0.4.5.dev6}/tigrbl_ops_oltp/crud/helpers/normalize.py
RENAMED
|
File without changes
|
|
File without changes
|