sera-2 1.21.2__tar.gz → 1.23.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.
- {sera_2-1.21.2 → sera_2-1.23.0}/PKG-INFO +2 -2
- {sera_2-1.21.2 → sera_2-1.23.0}/pyproject.toml +2 -2
- sera_2-1.23.0/sera/libs/api_helper.py +90 -0
- {sera_2-1.21.2 → sera_2-1.23.0}/sera/libs/api_test_helper.py +1 -1
- sera_2-1.23.0/sera/libs/base_service.py +264 -0
- sera_2-1.23.0/sera/libs/search_helper.py +359 -0
- {sera_2-1.21.2 → sera_2-1.23.0}/sera/make/make_python_api.py +65 -113
- {sera_2-1.21.2 → sera_2-1.23.0}/sera/make/make_python_model.py +184 -17
- {sera_2-1.21.2 → sera_2-1.23.0}/sera/make/make_python_services.py +3 -2
- {sera_2-1.21.2 → sera_2-1.23.0}/sera/make/make_typescript_model.py +21 -2
- {sera_2-1.21.2 → sera_2-1.23.0}/sera/misc/__init__.py +2 -0
- {sera_2-1.21.2 → sera_2-1.23.0}/sera/misc/_utils.py +17 -2
- {sera_2-1.21.2 → sera_2-1.23.0}/sera/models/_class.py +2 -2
- {sera_2-1.21.2 → sera_2-1.23.0}/sera/models/_collection.py +15 -11
- {sera_2-1.21.2 → sera_2-1.23.0}/sera/models/_constraints.py +1 -1
- {sera_2-1.21.2 → sera_2-1.23.0}/sera/models/_datatype.py +8 -29
- {sera_2-1.21.2 → sera_2-1.23.0}/sera/models/_enum.py +2 -2
- {sera_2-1.21.2 → sera_2-1.23.0}/sera/models/_module.py +7 -0
- sera_2-1.21.2/sera/libs/api_helper.py +0 -189
- sera_2-1.21.2/sera/libs/base_service.py +0 -176
- {sera_2-1.21.2 → sera_2-1.23.0}/README.md +0 -0
- {sera_2-1.21.2 → sera_2-1.23.0}/sera/__init__.py +0 -0
- {sera_2-1.21.2 → sera_2-1.23.0}/sera/constants.py +0 -0
- {sera_2-1.21.2 → sera_2-1.23.0}/sera/exports/__init__.py +0 -0
- {sera_2-1.21.2 → sera_2-1.23.0}/sera/exports/schema.py +0 -0
- {sera_2-1.21.2 → sera_2-1.23.0}/sera/exports/test.py +0 -0
- {sera_2-1.21.2 → sera_2-1.23.0}/sera/libs/__init__.py +0 -0
- {sera_2-1.21.2 → sera_2-1.23.0}/sera/libs/base_orm.py +0 -0
- {sera_2-1.21.2 → sera_2-1.23.0}/sera/libs/directed_computing_graph/__init__.py +0 -0
- {sera_2-1.21.2 → sera_2-1.23.0}/sera/libs/directed_computing_graph/_dcg.py +0 -0
- {sera_2-1.21.2 → sera_2-1.23.0}/sera/libs/directed_computing_graph/_edge.py +0 -0
- {sera_2-1.21.2 → sera_2-1.23.0}/sera/libs/directed_computing_graph/_flow.py +0 -0
- {sera_2-1.21.2 → sera_2-1.23.0}/sera/libs/directed_computing_graph/_fn_signature.py +0 -0
- {sera_2-1.21.2 → sera_2-1.23.0}/sera/libs/directed_computing_graph/_node.py +0 -0
- {sera_2-1.21.2 → sera_2-1.23.0}/sera/libs/directed_computing_graph/_runtime.py +0 -0
- {sera_2-1.21.2 → sera_2-1.23.0}/sera/libs/directed_computing_graph/_type_conversion.py +0 -0
- {sera_2-1.21.2 → sera_2-1.23.0}/sera/libs/middlewares/__init__.py +0 -0
- {sera_2-1.21.2 → sera_2-1.23.0}/sera/libs/middlewares/auth.py +0 -0
- {sera_2-1.21.2 → sera_2-1.23.0}/sera/libs/middlewares/uscp.py +0 -0
- {sera_2-1.21.2 → sera_2-1.23.0}/sera/make/__init__.py +0 -0
- {sera_2-1.21.2 → sera_2-1.23.0}/sera/make/__main__.py +0 -0
- {sera_2-1.21.2 → sera_2-1.23.0}/sera/make/make_app.py +0 -0
- {sera_2-1.21.2 → sera_2-1.23.0}/sera/misc/_formatter.py +0 -0
- {sera_2-1.21.2 → sera_2-1.23.0}/sera/models/__init__.py +0 -0
- {sera_2-1.21.2 → sera_2-1.23.0}/sera/models/_default.py +0 -0
- {sera_2-1.21.2 → sera_2-1.23.0}/sera/models/_multi_lingual_string.py +0 -0
- {sera_2-1.21.2 → sera_2-1.23.0}/sera/models/_parse.py +0 -0
- {sera_2-1.21.2 → sera_2-1.23.0}/sera/models/_property.py +0 -0
- {sera_2-1.21.2 → sera_2-1.23.0}/sera/models/_schema.py +0 -0
- {sera_2-1.21.2 → sera_2-1.23.0}/sera/typing.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: sera-2
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.23.0
|
4
4
|
Summary:
|
5
5
|
Author: Binh Vu
|
6
6
|
Author-email: bvu687@gmail.com
|
@@ -9,7 +9,7 @@ Classifier: Programming Language :: Python :: 3
|
|
9
9
|
Classifier: Programming Language :: Python :: 3.12
|
10
10
|
Classifier: Programming Language :: Python :: 3.13
|
11
11
|
Requires-Dist: black (==25.1.0)
|
12
|
-
Requires-Dist: codegen-2 (>=2.
|
12
|
+
Requires-Dist: codegen-2 (>=2.14.0,<3.0.0)
|
13
13
|
Requires-Dist: graph-wrapper (>=1.7.3,<2.0.0)
|
14
14
|
Requires-Dist: isort (==6.0.1)
|
15
15
|
Requires-Dist: litestar (>=2.15.1,<3.0.0)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[tool.poetry]
|
2
2
|
name = "sera-2"
|
3
|
-
version = "1.
|
3
|
+
version = "1.23.0"
|
4
4
|
description = ""
|
5
5
|
authors = ["Binh Vu <bvu687@gmail.com>"]
|
6
6
|
readme = "README.md"
|
@@ -9,7 +9,7 @@ repository = "https://github.com/binh-vu/sera"
|
|
9
9
|
|
10
10
|
[tool.poetry.dependencies]
|
11
11
|
python = "^3.12"
|
12
|
-
codegen-2 = "^2.
|
12
|
+
codegen-2 = "^2.14.0"
|
13
13
|
msgspec = "^0.19.0"
|
14
14
|
litestar = "^2.15.1"
|
15
15
|
loguru = "^0.7.0"
|
@@ -0,0 +1,90 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import Collection, Generic, TypeVar, cast
|
4
|
+
|
5
|
+
from litestar.connection import ASGIConnection
|
6
|
+
from litestar.dto import MsgspecDTO
|
7
|
+
from litestar.dto._backend import DTOBackend
|
8
|
+
from litestar.dto._codegen_backend import DTOCodegenBackend
|
9
|
+
from litestar.enums import RequestEncodingType
|
10
|
+
from litestar.serialization import decode_json, decode_msgpack
|
11
|
+
from litestar.typing import FieldDefinition
|
12
|
+
from msgspec import Struct
|
13
|
+
|
14
|
+
from sera.libs.middlewares.uscp import SKIP_UPDATE_SYSTEM_CONTROLLED_PROPS_KEY
|
15
|
+
|
16
|
+
S = TypeVar("S", bound=Struct)
|
17
|
+
|
18
|
+
|
19
|
+
class SingleAutoUSCP(MsgspecDTO[S], Generic[S]):
|
20
|
+
"""Auto Update System Controlled Property DTO"""
|
21
|
+
|
22
|
+
@classmethod
|
23
|
+
def create_for_field_definition(
|
24
|
+
cls,
|
25
|
+
field_definition: FieldDefinition,
|
26
|
+
handler_id: str,
|
27
|
+
backend_cls: type[DTOBackend] | None = None,
|
28
|
+
) -> None:
|
29
|
+
assert backend_cls is None, "Custom backend not supported"
|
30
|
+
super().create_for_field_definition(
|
31
|
+
field_definition, handler_id, FixedDTOBackend
|
32
|
+
)
|
33
|
+
|
34
|
+
def decode_bytes(self, value: bytes):
|
35
|
+
"""Decode a byte string into an object"""
|
36
|
+
backend = self._dto_backends[self.asgi_connection.route_handler.handler_id][
|
37
|
+
"data_backend"
|
38
|
+
] # pyright: ignore
|
39
|
+
obj = backend.populate_data_from_raw(value, self.asgi_connection)
|
40
|
+
if self.asgi_connection.scope["state"][SKIP_UPDATE_SYSTEM_CONTROLLED_PROPS_KEY]:
|
41
|
+
# Skip updating system-controlled properties
|
42
|
+
# TODO: dirty fix as this assumes every struct has _is_scp_updated property. find a
|
43
|
+
# better solution and fix me!
|
44
|
+
obj._is_scp_updated = True
|
45
|
+
return obj
|
46
|
+
|
47
|
+
obj.update_system_controlled_props(self.asgi_connection)
|
48
|
+
return obj
|
49
|
+
|
50
|
+
|
51
|
+
class FixedDTOBackend(DTOCodegenBackend):
|
52
|
+
def parse_raw(
|
53
|
+
self, raw: bytes, asgi_connection: ASGIConnection
|
54
|
+
) -> Struct | Collection[Struct]:
|
55
|
+
"""Parse raw bytes into transfer model type.
|
56
|
+
|
57
|
+
Note: instead of decoding into self.annotation, which I encounter this error: https://github.com/litestar-org/litestar/issues/4181; we have to use self.model_type, which is the original type.
|
58
|
+
|
59
|
+
Args:
|
60
|
+
raw: bytes
|
61
|
+
asgi_connection: The current ASGI Connection
|
62
|
+
|
63
|
+
Returns:
|
64
|
+
The raw bytes parsed into transfer model type.
|
65
|
+
"""
|
66
|
+
request_encoding = RequestEncodingType.JSON
|
67
|
+
|
68
|
+
if (content_type := getattr(asgi_connection, "content_type", None)) and (
|
69
|
+
media_type := content_type[0]
|
70
|
+
):
|
71
|
+
request_encoding = media_type
|
72
|
+
|
73
|
+
type_decoders = asgi_connection.route_handler.resolve_type_decoders()
|
74
|
+
|
75
|
+
if request_encoding == RequestEncodingType.MESSAGEPACK:
|
76
|
+
result = decode_msgpack(
|
77
|
+
value=raw,
|
78
|
+
target_type=self.model_type,
|
79
|
+
type_decoders=type_decoders,
|
80
|
+
strict=False,
|
81
|
+
)
|
82
|
+
else:
|
83
|
+
result = decode_json(
|
84
|
+
value=raw,
|
85
|
+
target_type=self.model_type,
|
86
|
+
type_decoders=type_decoders,
|
87
|
+
strict=False,
|
88
|
+
)
|
89
|
+
|
90
|
+
return cast("Struct | Collection[Struct]", result)
|
@@ -34,7 +34,7 @@ def test_get_by_id(
|
|
34
34
|
assert (
|
35
35
|
resp.status_code == 200
|
36
36
|
), f"Record {record} should exist but got {resp.status_code}"
|
37
|
-
assert resp.json() == data
|
37
|
+
assert resp.json() == data, (resp.json(), data)
|
38
38
|
|
39
39
|
for record in non_exist_records:
|
40
40
|
resp = client.get(f"{base_url}/{record}")
|
@@ -0,0 +1,264 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import Generic, NamedTuple, Optional, Sequence, TypeVar
|
4
|
+
|
5
|
+
from litestar.exceptions import HTTPException
|
6
|
+
from sqlalchemy import Result, Select, delete, exists, func, select
|
7
|
+
from sqlalchemy.exc import IntegrityError
|
8
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
9
|
+
from sqlalchemy.orm import contains_eager, load_only
|
10
|
+
|
11
|
+
from sera.libs.base_orm import BaseORM
|
12
|
+
from sera.libs.search_helper import Query, QueryOp
|
13
|
+
from sera.misc import assert_not_null, to_snake_case
|
14
|
+
from sera.models import Cardinality, Class, DataProperty, ObjectProperty
|
15
|
+
|
16
|
+
R = TypeVar("R", bound=BaseORM)
|
17
|
+
ID = TypeVar("ID") # ID of a class
|
18
|
+
SqlResult = TypeVar("SqlResult", bound=Result)
|
19
|
+
|
20
|
+
|
21
|
+
class QueryResult(NamedTuple, Generic[R]):
|
22
|
+
records: Sequence[R]
|
23
|
+
total: Optional[int]
|
24
|
+
|
25
|
+
|
26
|
+
class BaseAsyncService(Generic[ID, R]):
|
27
|
+
|
28
|
+
instance = None
|
29
|
+
|
30
|
+
def __init__(self, cls: Class, orm_classes: dict[str, type[R]]):
|
31
|
+
# schema of the class
|
32
|
+
self.cls = cls
|
33
|
+
self.orm_cls = orm_classes[cls.name]
|
34
|
+
self.id_prop = assert_not_null(cls.get_id_property())
|
35
|
+
|
36
|
+
self._cls_id_prop = getattr(self.orm_cls, self.id_prop.name)
|
37
|
+
self.is_id_auto_increment = assert_not_null(self.id_prop.db).is_auto_increment
|
38
|
+
|
39
|
+
self.prop2orm: dict[str, type] = {
|
40
|
+
prop.name: orm_classes[prop.target.name]
|
41
|
+
for prop in cls.properties.values()
|
42
|
+
if isinstance(prop, ObjectProperty) and prop.target.db is not None
|
43
|
+
}
|
44
|
+
|
45
|
+
# figure out the join clauses so we can join the tables
|
46
|
+
# for example, to join between User, UserGroup, and Group
|
47
|
+
# the query can look like this:
|
48
|
+
# select(User)
|
49
|
+
# .join(UserGroup, UserGroup.user_id == User.id)
|
50
|
+
# .join(Group, Group.id == UserGroup.group_id)
|
51
|
+
# .options(contains_eager(User.group).contains_eager(UserGroup.group))
|
52
|
+
self.join_clauses: dict[str, list[dict]] = {}
|
53
|
+
for prop in cls.properties.values():
|
54
|
+
if (
|
55
|
+
isinstance(prop, DataProperty)
|
56
|
+
and prop.db is not None
|
57
|
+
and prop.db.foreign_key is not None
|
58
|
+
):
|
59
|
+
target_tbl = orm_classes[prop.db.foreign_key.name]
|
60
|
+
target_cls = prop.db.foreign_key
|
61
|
+
source_fk = prop.name
|
62
|
+
# the property storing the SQLAlchemy relationship of the foreign key
|
63
|
+
source_relprop = prop.name + "_relobj"
|
64
|
+
cardinality = Cardinality.ONE_TO_ONE
|
65
|
+
elif isinstance(prop, ObjectProperty) and prop.target.db is not None:
|
66
|
+
target_tbl = orm_classes[prop.target.name]
|
67
|
+
target_cls = prop.target
|
68
|
+
source_fk = prop.name + "_id"
|
69
|
+
source_relprop = prop.name
|
70
|
+
cardinality = prop.cardinality
|
71
|
+
else:
|
72
|
+
continue
|
73
|
+
|
74
|
+
if cardinality == Cardinality.MANY_TO_MANY:
|
75
|
+
# for many-to-many, we need to import the association tables
|
76
|
+
assoc_tbl = orm_classes[f"{cls.name}{target_cls.name}"]
|
77
|
+
assoc_tbl_source_fk = to_snake_case(cls.name) + "_id"
|
78
|
+
assoc_tbl_target_fk = to_snake_case(target_cls.name) + "_id"
|
79
|
+
self.join_clauses[prop.name] = [
|
80
|
+
{
|
81
|
+
"class": assoc_tbl,
|
82
|
+
"condition": getattr(assoc_tbl, assoc_tbl_source_fk)
|
83
|
+
== getattr(self.orm_cls, self.id_prop.name),
|
84
|
+
"contains_eager": getattr(self.orm_cls, source_relprop),
|
85
|
+
},
|
86
|
+
{
|
87
|
+
"class": target_tbl,
|
88
|
+
"condition": getattr(assoc_tbl, assoc_tbl_target_fk)
|
89
|
+
== getattr(
|
90
|
+
target_tbl,
|
91
|
+
assert_not_null(target_cls.get_id_property()).name,
|
92
|
+
),
|
93
|
+
"contains_eager": getattr(
|
94
|
+
assoc_tbl, to_snake_case(target_cls.name)
|
95
|
+
),
|
96
|
+
},
|
97
|
+
]
|
98
|
+
elif cardinality == Cardinality.ONE_TO_MANY:
|
99
|
+
# A -> B is 1:N, A.id is stored in B, this does not supported in SERA yet so we do not need
|
100
|
+
# to implement it
|
101
|
+
raise NotImplementedError()
|
102
|
+
else:
|
103
|
+
# A -> B is either 1:1 or N:1, we will store the foreign key is in A
|
104
|
+
# .join(B, A.<foreign_key> == B.id)
|
105
|
+
self.join_clauses[prop.name] = [
|
106
|
+
{
|
107
|
+
"class": target_tbl,
|
108
|
+
"condition": getattr(
|
109
|
+
target_tbl,
|
110
|
+
assert_not_null(target_cls.get_id_property()).name,
|
111
|
+
)
|
112
|
+
== getattr(self.orm_cls, source_fk),
|
113
|
+
"contains_eager": getattr(self.orm_cls, source_relprop),
|
114
|
+
},
|
115
|
+
]
|
116
|
+
|
117
|
+
@classmethod
|
118
|
+
def get_instance(cls):
|
119
|
+
"""Get the singleton instance of the service."""
|
120
|
+
if cls.instance is None:
|
121
|
+
# assume that the subclass overrides the __init__ method
|
122
|
+
# so that we don't need to pass the class and orm_cls
|
123
|
+
cls.instance = cls() # type: ignore[call-arg]
|
124
|
+
return cls.instance
|
125
|
+
|
126
|
+
async def search(
|
127
|
+
self,
|
128
|
+
query: Query,
|
129
|
+
session: AsyncSession,
|
130
|
+
) -> QueryResult[R]:
|
131
|
+
"""Retrieving records matched a query.
|
132
|
+
|
133
|
+
Args:
|
134
|
+
query: The search query
|
135
|
+
session: The database session
|
136
|
+
"""
|
137
|
+
q = self._select()
|
138
|
+
|
139
|
+
if len(query.fields) > 0:
|
140
|
+
q = q.options(
|
141
|
+
load_only(*[getattr(self.orm_cls, field) for field in query.fields])
|
142
|
+
)
|
143
|
+
|
144
|
+
if query.unique:
|
145
|
+
q = q.distinct()
|
146
|
+
|
147
|
+
if len(query.sorted_by) > 0:
|
148
|
+
q = q.order_by(
|
149
|
+
*[
|
150
|
+
(
|
151
|
+
(
|
152
|
+
getattr(self.orm_cls, field.field).desc()
|
153
|
+
if field.order == "desc"
|
154
|
+
else getattr(self.orm_cls, field.field)
|
155
|
+
)
|
156
|
+
if field.prop is None
|
157
|
+
else (
|
158
|
+
getattr(self.prop2orm[field.prop], field.field).desc()
|
159
|
+
if field.order == "desc"
|
160
|
+
else getattr(self.prop2orm[field.prop], field.field)
|
161
|
+
)
|
162
|
+
)
|
163
|
+
for field in query.sorted_by
|
164
|
+
]
|
165
|
+
)
|
166
|
+
|
167
|
+
if len(query.group_by) > 0:
|
168
|
+
q = q.group_by(
|
169
|
+
*[
|
170
|
+
(
|
171
|
+
getattr(self.orm_cls, field.field)
|
172
|
+
if field.prop is None
|
173
|
+
else getattr(self.prop2orm[field.prop], field.field)
|
174
|
+
)
|
175
|
+
for field in query.group_by
|
176
|
+
]
|
177
|
+
)
|
178
|
+
|
179
|
+
for clause in query.conditions:
|
180
|
+
if clause.op == QueryOp.eq:
|
181
|
+
q = q.where(getattr(self.orm_cls, clause.field) == clause.value)
|
182
|
+
elif clause.op == QueryOp.ne:
|
183
|
+
q = q.where(getattr(self.orm_cls, clause.field) != clause.value)
|
184
|
+
elif clause.op == QueryOp.lt:
|
185
|
+
q = q.where(getattr(self.orm_cls, clause.field) < clause.value)
|
186
|
+
elif clause.op == QueryOp.lte:
|
187
|
+
q = q.where(getattr(self.orm_cls, clause.field) <= clause.value)
|
188
|
+
elif clause.op == QueryOp.gt:
|
189
|
+
q = q.where(getattr(self.orm_cls, clause.field) > clause.value)
|
190
|
+
elif clause.op == QueryOp.gte:
|
191
|
+
q = q.where(getattr(self.orm_cls, clause.field) >= clause.value)
|
192
|
+
elif clause.op == QueryOp.in_:
|
193
|
+
q = q.where(getattr(self.orm_cls, clause.field).in_(clause.value))
|
194
|
+
elif clause.op == QueryOp.not_in:
|
195
|
+
q = q.where(~getattr(self.orm_cls, clause.field).in_(clause.value))
|
196
|
+
else:
|
197
|
+
assert clause.op == QueryOp.fuzzy
|
198
|
+
# Assuming fuzzy search is implemented as a full-text search
|
199
|
+
q = q.where(
|
200
|
+
func.to_tsvector(getattr(self.orm_cls, clause.field)).match(
|
201
|
+
clause.value
|
202
|
+
)
|
203
|
+
)
|
204
|
+
|
205
|
+
for join_condition in query.join_conditions:
|
206
|
+
for join_clause in self.join_clauses[join_condition.prop]:
|
207
|
+
q = q.join(
|
208
|
+
join_clause["class"],
|
209
|
+
join_clause["condition"],
|
210
|
+
isouter=join_condition.join_type == "left",
|
211
|
+
full=join_condition.join_type == "full",
|
212
|
+
).options(contains_eager(join_clause["contains_eager"]))
|
213
|
+
|
214
|
+
print(">>>", join_clause)
|
215
|
+
|
216
|
+
cq = select(func.count()).select_from(q.subquery())
|
217
|
+
rq = q.limit(query.limit).offset(query.offset)
|
218
|
+
records = self._process_result(await session.execute(rq)).scalars().all()
|
219
|
+
if query.return_total:
|
220
|
+
total = (await session.execute(cq)).scalar_one()
|
221
|
+
else:
|
222
|
+
total = None
|
223
|
+
return QueryResult(records, total)
|
224
|
+
|
225
|
+
async def get_by_id(self, id: ID, session: AsyncSession) -> Optional[R]:
|
226
|
+
"""Retrieving a record by ID."""
|
227
|
+
q = self._select().where(self._cls_id_prop == id)
|
228
|
+
result = self._process_result(await session.execute(q)).scalar_one_or_none()
|
229
|
+
return result
|
230
|
+
|
231
|
+
async def has_id(self, id: ID, session: AsyncSession) -> bool:
|
232
|
+
"""Check whether we have a record with the given ID."""
|
233
|
+
q = exists().where(self._cls_id_prop == id).select()
|
234
|
+
result = (await session.execute(q)).scalar()
|
235
|
+
return bool(result)
|
236
|
+
|
237
|
+
async def create(self, record: R, session: AsyncSession) -> R:
|
238
|
+
"""Create a new record."""
|
239
|
+
if self.is_id_auto_increment:
|
240
|
+
setattr(record, self.id_prop.name, None)
|
241
|
+
|
242
|
+
try:
|
243
|
+
session.add(record)
|
244
|
+
await session.flush()
|
245
|
+
except IntegrityError:
|
246
|
+
raise HTTPException(detail="Invalid request", status_code=409)
|
247
|
+
return record
|
248
|
+
|
249
|
+
async def update(self, record: R, session: AsyncSession) -> R:
|
250
|
+
"""Update an existing record."""
|
251
|
+
await session.execute(record.get_update_query())
|
252
|
+
return record
|
253
|
+
|
254
|
+
def _select(self) -> Select:
|
255
|
+
"""Get the select statement for the class."""
|
256
|
+
return select(self.orm_cls)
|
257
|
+
|
258
|
+
def _process_result(self, result: SqlResult) -> SqlResult:
|
259
|
+
"""Process the result of a query."""
|
260
|
+
return result
|
261
|
+
|
262
|
+
async def truncate(self, session: AsyncSession) -> None:
|
263
|
+
"""Truncate the table."""
|
264
|
+
await session.execute(delete(self.orm_cls))
|