tigrbl-ops-oltp 0.1.0__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/PKG-INFO +52 -0
- tigrbl_ops_oltp-0.1.0/README.md +29 -0
- tigrbl_ops_oltp-0.1.0/pyproject.toml +46 -0
- tigrbl_ops_oltp-0.1.0/tigrbl_ops_oltp/__init__.py +22 -0
- tigrbl_ops_oltp-0.1.0/tigrbl_ops_oltp/crud/__init__.py +43 -0
- tigrbl_ops_oltp-0.1.0/tigrbl_ops_oltp/crud/bulk.py +167 -0
- tigrbl_ops_oltp-0.1.0/tigrbl_ops_oltp/crud/helpers/__init__.py +78 -0
- tigrbl_ops_oltp-0.1.0/tigrbl_ops_oltp/crud/helpers/db.py +105 -0
- tigrbl_ops_oltp-0.1.0/tigrbl_ops_oltp/crud/helpers/enum.py +81 -0
- tigrbl_ops_oltp-0.1.0/tigrbl_ops_oltp/crud/helpers/filters.py +161 -0
- tigrbl_ops_oltp-0.1.0/tigrbl_ops_oltp/crud/helpers/model.py +200 -0
- tigrbl_ops_oltp-0.1.0/tigrbl_ops_oltp/crud/helpers/normalize.py +98 -0
- tigrbl_ops_oltp-0.1.0/tigrbl_ops_oltp/crud/ops.py +357 -0
- tigrbl_ops_oltp-0.1.0/tigrbl_ops_oltp/crud/params.py +50 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tigrbl-ops-oltp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: OLTP operation implementations for Tigrbl, including CRUD and bulk handlers.
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
Keywords: tigrbl,sdk,standards,framework,oltp,crud
|
|
7
|
+
Author: Jacob Stewart
|
|
8
|
+
Author-email: jacob@swarmauri.com
|
|
9
|
+
Requires-Python: >=3.10,<3.13
|
|
10
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
11
|
+
Classifier: Development Status :: 1 - Planning
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Requires-Dist: tigrbl-base
|
|
19
|
+
Requires-Dist: tigrbl-core
|
|
20
|
+
Requires-Dist: tigrbl-orm
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+

|
|
24
|
+
|
|
25
|
+
# tigrbl-ops-oltp
|
|
26
|
+
|
|
27
|
+
    
|
|
28
|
+
|
|
29
|
+
## Features
|
|
30
|
+
|
|
31
|
+
- Transactional OLTP operation implementations for Tigrbl.
|
|
32
|
+
- Includes canonical CRUD and bulk operation executors.
|
|
33
|
+
- Supports Python 3.10 through 3.12.
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
### uv
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
uv add tigrbl-ops-oltp
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### pip
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install tigrbl-ops-oltp
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Usage
|
|
50
|
+
|
|
51
|
+
Import operation callables or parameter markers from `tigrbl_ops_oltp.crud`.
|
|
52
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
# tigrbl-ops-oltp
|
|
4
|
+
|
|
5
|
+
    
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Transactional OLTP operation implementations for Tigrbl.
|
|
10
|
+
- Includes canonical CRUD and bulk operation executors.
|
|
11
|
+
- Supports Python 3.10 through 3.12.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
### uv
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
uv add tigrbl-ops-oltp
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### pip
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install tigrbl-ops-oltp
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
Import operation callables or parameter markers from `tigrbl_ops_oltp.crud`.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "tigrbl-ops-oltp"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "OLTP operation implementations for Tigrbl, including CRUD and bulk handlers."
|
|
5
|
+
license = "Apache-2.0"
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
repository = "http://github.com/swarmauri/swarmauri-sdk"
|
|
8
|
+
requires-python = ">=3.10,<3.13"
|
|
9
|
+
classifiers = [
|
|
10
|
+
"License :: OSI Approved :: Apache Software License",
|
|
11
|
+
"Development Status :: 1 - Planning",
|
|
12
|
+
"Programming Language :: Python :: 3.10",
|
|
13
|
+
"Programming Language :: Python :: 3.11",
|
|
14
|
+
"Programming Language :: Python :: 3.12",
|
|
15
|
+
"Programming Language :: Python",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
18
|
+
]
|
|
19
|
+
authors = [{ name = "Jacob Stewart", email = "jacob@swarmauri.com" }]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"tigrbl-orm",
|
|
22
|
+
"tigrbl-base",
|
|
23
|
+
"tigrbl-core",
|
|
24
|
+
]
|
|
25
|
+
keywords = ["tigrbl", "sdk", "standards", "framework", "oltp", "crud"]
|
|
26
|
+
|
|
27
|
+
[tool.uv.sources]
|
|
28
|
+
"tigrbl-orm" = { workspace = true }
|
|
29
|
+
"tigrbl-base" = { workspace = true }
|
|
30
|
+
"tigrbl-core" = { workspace = true }
|
|
31
|
+
|
|
32
|
+
[build-system]
|
|
33
|
+
requires = ["poetry-core>=1.0.0"]
|
|
34
|
+
build-backend = "poetry.core.masonry.api"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
[tool.poetry]
|
|
38
|
+
packages = [
|
|
39
|
+
{ include = "tigrbl_ops_oltp" },
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[dependency-groups]
|
|
43
|
+
dev = [
|
|
44
|
+
"pytest>=8.0",
|
|
45
|
+
"ruff>=0.9",
|
|
46
|
+
]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""OLTP operation implementations for Tigrbl."""
|
|
2
|
+
|
|
3
|
+
from .crud import ( # noqa: F401
|
|
4
|
+
Body,
|
|
5
|
+
Header,
|
|
6
|
+
Param,
|
|
7
|
+
Path,
|
|
8
|
+
Query,
|
|
9
|
+
bulk_create,
|
|
10
|
+
bulk_delete,
|
|
11
|
+
bulk_merge,
|
|
12
|
+
bulk_replace,
|
|
13
|
+
bulk_update,
|
|
14
|
+
clear,
|
|
15
|
+
create,
|
|
16
|
+
delete,
|
|
17
|
+
list,
|
|
18
|
+
merge,
|
|
19
|
+
read,
|
|
20
|
+
replace,
|
|
21
|
+
update,
|
|
22
|
+
)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from .ops import (
|
|
2
|
+
create,
|
|
3
|
+
read,
|
|
4
|
+
update,
|
|
5
|
+
replace,
|
|
6
|
+
merge,
|
|
7
|
+
delete,
|
|
8
|
+
list as _list,
|
|
9
|
+
clear,
|
|
10
|
+
)
|
|
11
|
+
from .bulk import (
|
|
12
|
+
bulk_create,
|
|
13
|
+
bulk_update,
|
|
14
|
+
bulk_replace,
|
|
15
|
+
bulk_merge,
|
|
16
|
+
bulk_delete,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from .params import Body, Header, Param, Path, Query
|
|
20
|
+
|
|
21
|
+
# Public alias named exactly `list` to preserve API surface
|
|
22
|
+
list = _list # noqa: A001 - intentional shadow of built-in for public API
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"Header",
|
|
26
|
+
"Path",
|
|
27
|
+
"Query",
|
|
28
|
+
"Body",
|
|
29
|
+
"Param",
|
|
30
|
+
"create",
|
|
31
|
+
"read",
|
|
32
|
+
"update",
|
|
33
|
+
"replace",
|
|
34
|
+
"merge",
|
|
35
|
+
"delete",
|
|
36
|
+
"list",
|
|
37
|
+
"clear",
|
|
38
|
+
"bulk_create",
|
|
39
|
+
"bulk_update",
|
|
40
|
+
"bulk_replace",
|
|
41
|
+
"bulk_merge",
|
|
42
|
+
"bulk_delete",
|
|
43
|
+
]
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Iterable, List, Mapping, Union
|
|
4
|
+
|
|
5
|
+
import builtins as _builtins
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
from .helpers import (
|
|
9
|
+
AsyncSession,
|
|
10
|
+
Session,
|
|
11
|
+
sa_delete,
|
|
12
|
+
_coerce_pk_value,
|
|
13
|
+
_immutable_columns,
|
|
14
|
+
_maybe_delete,
|
|
15
|
+
_maybe_execute,
|
|
16
|
+
_maybe_flush,
|
|
17
|
+
_maybe_get,
|
|
18
|
+
_set_attrs,
|
|
19
|
+
_single_pk_name,
|
|
20
|
+
_validate_enum_values,
|
|
21
|
+
)
|
|
22
|
+
from .ops import merge, read
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger("uvicorn")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def bulk_create(
|
|
28
|
+
model: type, rows: Iterable[Mapping[str, Any]], db: Union[Session, AsyncSession]
|
|
29
|
+
) -> List[Any]:
|
|
30
|
+
"""
|
|
31
|
+
Insert many rows. Returns the list of persisted instances.
|
|
32
|
+
Flush-only.
|
|
33
|
+
"""
|
|
34
|
+
logger.debug("bulk_create called with model=%s rows=%s", model, rows)
|
|
35
|
+
items_data = [dict(r) for r in (rows or ())]
|
|
36
|
+
for r in items_data:
|
|
37
|
+
_validate_enum_values(model, r)
|
|
38
|
+
items = [model(**r) for r in items_data]
|
|
39
|
+
if not items:
|
|
40
|
+
logger.debug("bulk_create no items to create")
|
|
41
|
+
return []
|
|
42
|
+
if hasattr(db, "add_all"):
|
|
43
|
+
db.add_all(items) # type: ignore[attr-defined]
|
|
44
|
+
else:
|
|
45
|
+
for it in items:
|
|
46
|
+
db.add(it) # type: ignore[attr-defined]
|
|
47
|
+
await _maybe_flush(db)
|
|
48
|
+
logger.debug("bulk_create persisted %d items", len(items))
|
|
49
|
+
return items
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
async def bulk_update(
|
|
53
|
+
model: type, rows: Iterable[Mapping[str, Any]], db: Union[Session, AsyncSession]
|
|
54
|
+
) -> List[Any]:
|
|
55
|
+
"""
|
|
56
|
+
Update many rows by PK. Each row must include the PK field.
|
|
57
|
+
Returns the list of updated instances. Flush-only.
|
|
58
|
+
"""
|
|
59
|
+
logger.debug("bulk_update called with model=%s rows=%s", model, rows)
|
|
60
|
+
pk = _single_pk_name(model)
|
|
61
|
+
skip = _immutable_columns(model, "update")
|
|
62
|
+
updated: List[Any] = []
|
|
63
|
+
for r in rows or ():
|
|
64
|
+
r = dict(r)
|
|
65
|
+
_validate_enum_values(model, r)
|
|
66
|
+
ident = r.get(pk)
|
|
67
|
+
if ident is None:
|
|
68
|
+
raise ValueError(f"bulk_update requires '{pk}' in each row")
|
|
69
|
+
obj = await read(model, ident, db)
|
|
70
|
+
data = {k: v for k, v in r.items() if k != pk}
|
|
71
|
+
_set_attrs(obj, data, allow_missing=True, skip=skip)
|
|
72
|
+
updated.append(obj)
|
|
73
|
+
if updated:
|
|
74
|
+
await _maybe_flush(db)
|
|
75
|
+
logger.debug("bulk_update updated %d items", len(updated))
|
|
76
|
+
return updated
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
async def bulk_replace(
|
|
80
|
+
model: type, rows: Iterable[Mapping[str, Any]], db: Union[Session, AsyncSession]
|
|
81
|
+
) -> List[Any]:
|
|
82
|
+
"""
|
|
83
|
+
Replace many rows by PK. Each row must include the PK field.
|
|
84
|
+
Missing attributes are nulled (except PK). Flush-only.
|
|
85
|
+
"""
|
|
86
|
+
logger.debug("bulk_replace called with model=%s rows=%s", model, rows)
|
|
87
|
+
pk = _single_pk_name(model)
|
|
88
|
+
skip = _immutable_columns(model, "replace")
|
|
89
|
+
replaced: List[Any] = []
|
|
90
|
+
for r in rows or ():
|
|
91
|
+
r = dict(r)
|
|
92
|
+
_validate_enum_values(model, r)
|
|
93
|
+
ident = r.get(pk)
|
|
94
|
+
if ident is None:
|
|
95
|
+
raise ValueError(f"bulk_replace requires '{pk}' in each row")
|
|
96
|
+
obj = await read(model, ident, db)
|
|
97
|
+
data = {k: v for k, v in r.items() if k != pk}
|
|
98
|
+
_set_attrs(obj, data, allow_missing=False, skip=skip)
|
|
99
|
+
replaced.append(obj)
|
|
100
|
+
if replaced:
|
|
101
|
+
await _maybe_flush(db)
|
|
102
|
+
logger.debug("bulk_replace replaced %d items", len(replaced))
|
|
103
|
+
return replaced
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
async def bulk_merge(
|
|
107
|
+
model: type, rows: Iterable[Mapping[str, Any]], db: Union[Session, AsyncSession]
|
|
108
|
+
) -> List[Any]:
|
|
109
|
+
"""Merge many rows by primary key with upsert semantics."""
|
|
110
|
+
logger.debug("bulk_merge called with model=%s rows=%s", model, rows)
|
|
111
|
+
pk = _single_pk_name(model)
|
|
112
|
+
results: List[Any] = []
|
|
113
|
+
to_create: List[Mapping[str, Any]] = []
|
|
114
|
+
for r in rows or ():
|
|
115
|
+
r = dict(r)
|
|
116
|
+
ident = _coerce_pk_value(model, r.get(pk))
|
|
117
|
+
if ident is not None:
|
|
118
|
+
existing = await _maybe_get(db, model, ident)
|
|
119
|
+
if existing is not None:
|
|
120
|
+
data = {k: v for k, v in r.items() if k != pk}
|
|
121
|
+
merged = await merge(model, ident, data, db=db)
|
|
122
|
+
results.append(merged)
|
|
123
|
+
continue
|
|
124
|
+
r[pk] = ident
|
|
125
|
+
to_create.append(r)
|
|
126
|
+
if to_create:
|
|
127
|
+
created = await bulk_create(model, to_create, db)
|
|
128
|
+
results.extend(created)
|
|
129
|
+
logger.debug(
|
|
130
|
+
"bulk_merge returning %d results (%d created)",
|
|
131
|
+
len(results),
|
|
132
|
+
len(to_create),
|
|
133
|
+
)
|
|
134
|
+
return results
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
async def bulk_delete(
|
|
138
|
+
model: type, idents: Iterable[Any], db: Union[Session, AsyncSession]
|
|
139
|
+
) -> Dict[str, int]:
|
|
140
|
+
"""
|
|
141
|
+
Delete many rows by a sequence of PK values. Returns {"deleted": N}.
|
|
142
|
+
Flush-only.
|
|
143
|
+
"""
|
|
144
|
+
logger.debug("bulk_delete called with model=%s idents=%s", model, idents)
|
|
145
|
+
pk_name = _single_pk_name(model)
|
|
146
|
+
id_seq = _builtins.list(idents or ())
|
|
147
|
+
if not id_seq:
|
|
148
|
+
logger.debug("bulk_delete no ids supplied")
|
|
149
|
+
return {"deleted": 0}
|
|
150
|
+
|
|
151
|
+
if sa_delete is not None:
|
|
152
|
+
col = getattr(model, pk_name)
|
|
153
|
+
stmt = sa_delete(model).where(col.in_(id_seq)) # type: ignore[attr-defined]
|
|
154
|
+
res = await _maybe_execute(db, stmt)
|
|
155
|
+
await _maybe_flush(db)
|
|
156
|
+
n = int(getattr(res, "rowcount", 0) or 0)
|
|
157
|
+
logger.debug("bulk_delete removed %d rows via stmt", n)
|
|
158
|
+
return {"deleted": n}
|
|
159
|
+
|
|
160
|
+
n = 0
|
|
161
|
+
for ident in id_seq:
|
|
162
|
+
obj = await read(model, ident, db)
|
|
163
|
+
await _maybe_delete(db, obj)
|
|
164
|
+
n += 1
|
|
165
|
+
await _maybe_flush(db)
|
|
166
|
+
logger.debug("bulk_delete removed %d rows individually", n)
|
|
167
|
+
return {"deleted": n}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
from sqlalchemy import select, delete as sa_delete, and_, asc, desc, Enum as SAEnum
|
|
5
|
+
from sqlalchemy.orm import Session
|
|
6
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
7
|
+
from sqlalchemy.orm.exc import NoResultFound # type: ignore
|
|
8
|
+
except Exception: # pragma: no cover
|
|
9
|
+
select = sa_delete = and_ = asc = desc = None # type: ignore
|
|
10
|
+
SAEnum = None # type: ignore
|
|
11
|
+
Session = object # type: ignore
|
|
12
|
+
AsyncSession = object # type: ignore
|
|
13
|
+
|
|
14
|
+
class NoResultFound(LookupError): # type: ignore
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from .model import (
|
|
19
|
+
_pk_columns,
|
|
20
|
+
_single_pk_name,
|
|
21
|
+
_coerce_pk_value,
|
|
22
|
+
_model_columns,
|
|
23
|
+
_colspecs,
|
|
24
|
+
_filter_in_values,
|
|
25
|
+
_immutable_columns,
|
|
26
|
+
)
|
|
27
|
+
from .filters import _CANON_OPS, _coerce_filters, _apply_filters, _apply_sort
|
|
28
|
+
from .db import (
|
|
29
|
+
_is_async_db,
|
|
30
|
+
_maybe_get,
|
|
31
|
+
_maybe_execute,
|
|
32
|
+
_maybe_flush,
|
|
33
|
+
_maybe_rollback,
|
|
34
|
+
_maybe_delete,
|
|
35
|
+
_set_attrs,
|
|
36
|
+
)
|
|
37
|
+
from .enum import _validate_enum_values
|
|
38
|
+
from .normalize import (
|
|
39
|
+
_normalize_list_call,
|
|
40
|
+
_pop_bound_self,
|
|
41
|
+
_extract_db,
|
|
42
|
+
_as_pos_int,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
"AsyncSession",
|
|
47
|
+
"Session",
|
|
48
|
+
"NoResultFound",
|
|
49
|
+
"select",
|
|
50
|
+
"sa_delete",
|
|
51
|
+
"_apply_filters",
|
|
52
|
+
"_apply_sort",
|
|
53
|
+
"_CANON_OPS",
|
|
54
|
+
"_coerce_filters",
|
|
55
|
+
"_coerce_pk_value",
|
|
56
|
+
"_colspecs",
|
|
57
|
+
"_filter_in_values",
|
|
58
|
+
"_immutable_columns",
|
|
59
|
+
"_is_async_db",
|
|
60
|
+
"_maybe_delete",
|
|
61
|
+
"_maybe_execute",
|
|
62
|
+
"_maybe_flush",
|
|
63
|
+
"_maybe_rollback",
|
|
64
|
+
"_maybe_get",
|
|
65
|
+
"_model_columns",
|
|
66
|
+
"_normalize_list_call",
|
|
67
|
+
"_pop_bound_self",
|
|
68
|
+
"_extract_db",
|
|
69
|
+
"_as_pos_int",
|
|
70
|
+
"_pk_columns",
|
|
71
|
+
"_set_attrs",
|
|
72
|
+
"_single_pk_name",
|
|
73
|
+
"_validate_enum_values",
|
|
74
|
+
"SAEnum",
|
|
75
|
+
"asc",
|
|
76
|
+
"desc",
|
|
77
|
+
"and_",
|
|
78
|
+
]
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
from typing import Any, Mapping, Sequence, Union
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
from . import AsyncSession, Session
|
|
10
|
+
from .model import _model_columns, _single_pk_name
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger("uvicorn")
|
|
13
|
+
|
|
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
|
+
|
|
20
|
+
def _is_async_db(db: Any) -> bool:
|
|
21
|
+
logger.debug("_is_async_db called with db=%s", db)
|
|
22
|
+
result = _is_async_db_type(type(db))
|
|
23
|
+
logger.debug("_is_async_db returning %s", result)
|
|
24
|
+
return result
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def _maybe_get(db: Union[Session, AsyncSession], model: type, pk_value: Any):
|
|
28
|
+
logger.debug("_maybe_get model=%s pk_value=%s", model, pk_value)
|
|
29
|
+
result = db.get(model, pk_value) # type: ignore[attr-defined]
|
|
30
|
+
if inspect.isawaitable(result):
|
|
31
|
+
result = await result
|
|
32
|
+
logger.debug("_maybe_get returning %s", result)
|
|
33
|
+
return result
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async def _maybe_execute(db: Union[Session, AsyncSession], stmt: Any):
|
|
37
|
+
logger.debug("_maybe_execute stmt=%s", stmt)
|
|
38
|
+
result = db.execute(stmt) # type: ignore[attr-defined]
|
|
39
|
+
if inspect.isawaitable(result):
|
|
40
|
+
result = await result
|
|
41
|
+
logger.debug("_maybe_execute returning %s", result)
|
|
42
|
+
return result
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def _maybe_flush(db: Union[Session, AsyncSession]) -> None:
|
|
46
|
+
logger.debug("_maybe_flush called")
|
|
47
|
+
result = db.flush() # type: ignore[attr-defined]
|
|
48
|
+
if inspect.isawaitable(result):
|
|
49
|
+
await result
|
|
50
|
+
logger.debug("_maybe_flush completed")
|
|
51
|
+
|
|
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
|
+
|
|
64
|
+
async def _maybe_delete(db: Union[Session, AsyncSession], obj: Any) -> None:
|
|
65
|
+
logger.debug("_maybe_delete called with obj=%s", obj)
|
|
66
|
+
if not hasattr(db, "delete"):
|
|
67
|
+
logger.debug("_maybe_delete skipping delete; no attribute")
|
|
68
|
+
return
|
|
69
|
+
result = db.delete(obj) # type: ignore[attr-defined]
|
|
70
|
+
if inspect.isawaitable(result):
|
|
71
|
+
await result
|
|
72
|
+
logger.debug("_maybe_delete completed for obj=%s", obj)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _set_attrs(
|
|
76
|
+
obj: Any,
|
|
77
|
+
values: Mapping[str, Any],
|
|
78
|
+
*,
|
|
79
|
+
allow_missing: bool = True,
|
|
80
|
+
skip: Sequence[str] = (),
|
|
81
|
+
) -> None:
|
|
82
|
+
logger.debug(
|
|
83
|
+
"_set_attrs called on obj=%s values=%s allow_missing=%s skip=%s",
|
|
84
|
+
obj,
|
|
85
|
+
values,
|
|
86
|
+
allow_missing,
|
|
87
|
+
skip,
|
|
88
|
+
)
|
|
89
|
+
cols = set(_model_columns(type(obj)))
|
|
90
|
+
pk = _single_pk_name(type(obj))
|
|
91
|
+
skip_set = set(skip) | {pk}
|
|
92
|
+
|
|
93
|
+
if allow_missing:
|
|
94
|
+
for k, v in values.items():
|
|
95
|
+
if k in cols and k not in skip_set:
|
|
96
|
+
setattr(obj, k, v)
|
|
97
|
+
else:
|
|
98
|
+
for c in cols:
|
|
99
|
+
if c in skip_set:
|
|
100
|
+
continue
|
|
101
|
+
if c in values:
|
|
102
|
+
setattr(obj, c, values[c])
|
|
103
|
+
else:
|
|
104
|
+
setattr(obj, c, None)
|
|
105
|
+
logger.debug("_set_attrs completed for obj=%s", obj)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import enum as _enum
|
|
4
|
+
from typing import Any, Mapping
|
|
5
|
+
import builtins as _builtins
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
from . import SAEnum
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger("uvicorn")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _validate_enum_values(model: type, values: Mapping[str, Any]) -> None:
|
|
14
|
+
logger.debug("_validate_enum_values called with model=%s values=%s", model, values)
|
|
15
|
+
if not values or SAEnum is None:
|
|
16
|
+
logger.debug("_validate_enum_values no validation needed")
|
|
17
|
+
return
|
|
18
|
+
|
|
19
|
+
table = getattr(model, "__table__", None)
|
|
20
|
+
if table is None:
|
|
21
|
+
return
|
|
22
|
+
|
|
23
|
+
get = getattr(table.c, "get", None)
|
|
24
|
+
|
|
25
|
+
for key, v in values.items():
|
|
26
|
+
col = get(key) if get else None
|
|
27
|
+
if col is None:
|
|
28
|
+
try:
|
|
29
|
+
col = table.c[key] # type: ignore[index]
|
|
30
|
+
except Exception:
|
|
31
|
+
col = None
|
|
32
|
+
if col is None:
|
|
33
|
+
continue
|
|
34
|
+
|
|
35
|
+
col_type = getattr(col, "type", None)
|
|
36
|
+
if col_type is None or not isinstance(col_type, SAEnum):
|
|
37
|
+
continue
|
|
38
|
+
|
|
39
|
+
if v is None:
|
|
40
|
+
continue
|
|
41
|
+
|
|
42
|
+
enum_cls = getattr(col_type, "enum_class", None)
|
|
43
|
+
if enum_cls is not None:
|
|
44
|
+
if isinstance(v, _enum.Enum):
|
|
45
|
+
if isinstance(v, enum_cls):
|
|
46
|
+
continue
|
|
47
|
+
logger.debug(
|
|
48
|
+
"_validate_enum_values invalid value %s for enum %s", v, enum_cls
|
|
49
|
+
)
|
|
50
|
+
raise LookupError(
|
|
51
|
+
f"{v!r} is not among the defined enum values. "
|
|
52
|
+
f"Enum name: {enum_cls.__name__}. "
|
|
53
|
+
f"Possible values: {', '.join([e.value for e in enum_cls])}"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
allowed_values = [e.value for e in enum_cls]
|
|
57
|
+
allowed_names = [e.name for e in enum_cls]
|
|
58
|
+
if isinstance(v, str) and (v in allowed_values or v in allowed_names):
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
logger.debug(
|
|
62
|
+
"_validate_enum_values invalid value %s for enum %s", v, enum_cls
|
|
63
|
+
)
|
|
64
|
+
raise LookupError(
|
|
65
|
+
f"{v!r} is not among the defined enum values. "
|
|
66
|
+
f"Enum name: {enum_cls.__name__}. "
|
|
67
|
+
f"Possible values: {', '.join(allowed_values)}"
|
|
68
|
+
)
|
|
69
|
+
else:
|
|
70
|
+
allowed = _builtins.list(getattr(col_type, "enums", []) or [])
|
|
71
|
+
if isinstance(v, str) and v in allowed:
|
|
72
|
+
continue
|
|
73
|
+
logger.debug(
|
|
74
|
+
"_validate_enum_values invalid value %s for enum %s", v, col_type
|
|
75
|
+
)
|
|
76
|
+
raise LookupError(
|
|
77
|
+
f"{v!r} is not among the defined enum values. "
|
|
78
|
+
f"Enum name: {getattr(col_type, 'name', 'Enum')}. "
|
|
79
|
+
f"Possible values: {', '.join(allowed) if allowed else '(none)'}"
|
|
80
|
+
)
|
|
81
|
+
logger.debug("_validate_enum_values completed")
|