1
1
#!/usr/bin/env python3
2
2
# -*- coding: utf-8 -*-
3
- from typing import Any , Generic , Iterable , Sequence , Type
3
+ from typing import Any , Generic , Iterable , Sequence
4
4
5
5
from sqlalchemy import (
6
6
Column ,
7
7
ColumnExpressionArgument ,
8
- Row ,
9
- RowMapping ,
10
8
Select ,
11
9
delete ,
12
10
func ,
16
14
)
17
15
from sqlalchemy .ext .asyncio import AsyncSession
18
16
19
- from sqlalchemy_crud_plus .errors import CompositePrimaryKeysError , MultipleResultsError
17
+ from sqlalchemy_crud_plus .errors import CompositePrimaryKeysError , ModelColumnError , MultipleResultsError
20
18
from sqlalchemy_crud_plus .types import CreateSchema , Model , UpdateSchema
21
19
from sqlalchemy_crud_plus .utils import apply_sorting , parse_filters
22
20
23
21
24
22
class CRUDPlus (Generic [Model ]):
25
- def __init__ (self , model : Type [Model ]):
23
+ def __init__ (self , model : type [Model ]):
26
24
self .model = model
27
25
self .primary_key = self ._get_primary_key ()
28
26
@@ -37,11 +35,11 @@ def _get_primary_key(self) -> Column | list[Column]:
37
35
else :
38
36
return list (primary_key )
39
37
40
- def _get_pk_filter (self , pk : Any | Sequence [Any ]) -> list [bool ]:
38
+ def _get_pk_filter (self , pk : Any | Sequence [Any ]) -> list [ColumnExpressionArgument [ bool ] ]:
41
39
"""
42
40
Get the primary key filter(s).
43
41
44
- :param pk: Single value for simple primary key, or tuple for composite primary key.
42
+ :param pk: Single value for simple primary key, or tuple for composite primary key
45
43
:return:
46
44
"""
47
45
if isinstance (self .primary_key , list ):
@@ -60,17 +58,20 @@ async def create_model(
60
58
** kwargs ,
61
59
) -> Model :
62
60
"""
63
- Create a new instance of a model
61
+ Create a new instance of a model.
64
62
65
- :param session: The SQLAlchemy async session.
66
- :param obj: The Pydantic schema containing data to be saved.
67
- :param flush: If `True`, flush all object changes to the database. Default is `False`.
68
- :param commit: If `True`, commits the transaction immediately. Default is `False`.
69
- :param kwargs: Additional model data not included in the pydantic schema.
63
+ :param session: The SQLAlchemy async session
64
+ :param obj: The Pydantic schema containing data to be saved
65
+ :param flush: If `True`, flush all object changes to the database
66
+ :param commit: If `True`, commits the transaction immediately
67
+ :param kwargs: Additional model data not included in the pydantic schema
70
68
:return:
71
69
"""
72
- ins = self .model (** obj .model_dump ()) if not kwargs else self .model (** obj .model_dump (), ** kwargs )
70
+ obj_data = obj .model_dump ()
71
+ if kwargs :
72
+ obj_data .update (kwargs )
73
73
74
+ ins = self .model (** obj_data )
74
75
session .add (ins )
75
76
76
77
if flush :
@@ -89,18 +90,21 @@ async def create_models(
89
90
** kwargs ,
90
91
) -> list [Model ]:
91
92
"""
92
- Create new instances of a model
93
+ Create new instances of a model.
93
94
94
- :param session: The SQLAlchemy async session.
95
- :param objs: The Pydantic schema list containing data to be saved.
96
- :param flush: If `True`, flush all object changes to the database. Default is `False`.
97
- :param commit: If `True`, commits the transaction immediately. Default is `False`.
98
- :param kwargs: Additional model data not included in the pydantic schema.
95
+ :param session: The SQLAlchemy async session
96
+ :param objs: The Pydantic schema list containing data to be saved
97
+ :param flush: If `True`, flush all object changes to the database
98
+ :param commit: If `True`, commits the transaction immediately
99
+ :param kwargs: Additional model data not included in the pydantic schema
99
100
:return:
100
101
"""
101
102
ins_list = []
102
103
for obj in objs :
103
- ins = self .model (** obj .model_dump ()) if not kwargs else self .model (** obj .model_dump (), ** kwargs )
104
+ obj_data = obj .model_dump ()
105
+ if kwargs :
106
+ obj_data .update (kwargs )
107
+ ins = self .model (** obj_data )
104
108
ins_list .append (ins )
105
109
106
110
session .add_all (ins_list )
@@ -119,19 +123,22 @@ async def count(
119
123
** kwargs ,
120
124
) -> int :
121
125
"""
122
- Counts records that match specified filters.
126
+ Count records that match specified filters.
123
127
124
- :param session: The sqlalchemy session to use for the operation.
125
- :param whereclause: The WHERE clauses to apply to the query.
126
- :param kwargs: Query expressions.
128
+ :param session: The SQLAlchemy async session
129
+ :param whereclause: Additional WHERE clauses to apply to the query
130
+ :param kwargs: Filter expressions using field__operator=value syntax
127
131
:return:
128
132
"""
129
133
filters = list (whereclause )
130
134
131
135
if kwargs :
132
136
filters .extend (parse_filters (self .model , ** kwargs ))
133
137
134
- stmt = select (func .count ()).select_from (self .model ).where (* filters )
138
+ stmt = select (func .count ()).select_from (self .model )
139
+ if filters :
140
+ stmt = stmt .where (* filters )
141
+
135
142
query = await session .execute (stmt )
136
143
total_count = query .scalar ()
137
144
return total_count if total_count is not None else 0
@@ -143,11 +150,11 @@ async def exists(
143
150
** kwargs ,
144
151
) -> bool :
145
152
"""
146
- Whether the records that match the specified filter exist.
153
+ Check whether records that match the specified filters exist.
147
154
148
- :param session: The sqlalchemy session to use for the operation.
149
- :param whereclause: The WHERE clauses to apply to the query.
150
- :param kwargs: Query expressions.
155
+ :param session: The SQLAlchemy async session
156
+ :param whereclause: Additional WHERE clauses to apply to the query
157
+ :param kwargs: Filter expressions using field__operator=value syntax
151
158
:return:
152
159
"""
153
160
filter_list = list (whereclause )
@@ -174,7 +181,7 @@ async def select_model(
174
181
:return:
175
182
"""
176
183
filters = self ._get_pk_filter (pk )
177
- filters + list (whereclause )
184
+ filters . extend ( list (whereclause ) )
178
185
stmt = select (self .model ).where (* filters )
179
186
query = await session .execute (stmt )
180
187
return query .scalars ().first ()
@@ -235,13 +242,13 @@ async def select_models(
235
242
session : AsyncSession ,
236
243
* whereclause : ColumnExpressionArgument [bool ],
237
244
** kwargs ,
238
- ) -> Sequence [Row [ Any ] | RowMapping | Any ]:
245
+ ) -> Sequence [Model ]:
239
246
"""
240
- Query all rows
247
+ Query all rows that match the specified filters.
241
248
242
- :param session: The SQLAlchemy async session.
243
- :param whereclause: The WHERE clauses to apply to the query.
244
- :param kwargs: Query expressions.
249
+ :param session: The SQLAlchemy async session
250
+ :param whereclause: Additional WHERE clauses to apply to the query
251
+ :param kwargs: Filter expressions using field__operator=value syntax
245
252
:return:
246
253
"""
247
254
stmt = await self .select (* whereclause , ** kwargs )
@@ -255,15 +262,15 @@ async def select_models_order(
255
262
sort_orders : str | list [str ] | None = None ,
256
263
* whereclause : ColumnExpressionArgument [bool ],
257
264
** kwargs ,
258
- ) -> Sequence [Row | RowMapping | Any ] | None :
265
+ ) -> Sequence [Model ] :
259
266
"""
260
- Query all rows and sort by columns
267
+ Query all rows that match the specified filters and sort by columns.
261
268
262
- :param session: The SQLAlchemy async session.
263
- :param sort_columns: more details see apply_sorting
264
- :param sort_orders: more details see apply_sorting
265
- :param whereclause: The WHERE clauses to apply to the query.
266
- :param kwargs: Query expressions.
269
+ :param session: The SQLAlchemy async session
270
+ :param sort_columns: Column name(s) to sort by
271
+ :param sort_orders: Sort order(s) ('asc' or 'desc')
272
+ :param whereclause: Additional WHERE clauses to apply to the query
273
+ :param kwargs: Filter expressions using field__operator=value syntax
267
274
:return:
268
275
"""
269
276
stmt = await self .select_order (sort_columns , sort_orders , * whereclause , ** kwargs )
@@ -313,21 +320,25 @@ async def update_model_by_column(
313
320
** kwargs ,
314
321
) -> int :
315
322
"""
316
- Update an instance by model column
323
+ Update records by model column filters.
317
324
318
- :param session: The SQLAlchemy async session.
319
- :param obj: A pydantic schema or dictionary containing the update data
320
- :param allow_multiple: If `True`, allows updating multiple records that match the filters.
321
- :param flush: If `True`, flush all object changes to the database. Default is `False`.
322
- :param commit: If `True`, commits the transaction immediately. Default is `False`.
323
- :param kwargs: Query expressions.
325
+ :param session: The SQLAlchemy async session
326
+ :param obj: A Pydantic schema or dictionary containing the update data
327
+ :param allow_multiple: If `True`, allows updating multiple records that match the filters
328
+ :param flush: If `True`, flush all object changes to the database
329
+ :param commit: If `True`, commits the transaction immediately
330
+ :param kwargs: Filter expressions using field__operator=value syntax
324
331
:return:
325
332
"""
326
333
filters = parse_filters (self .model , ** kwargs )
327
334
328
- total_count = await self .count (session , * filters )
329
- if not allow_multiple and total_count > 1 :
330
- raise MultipleResultsError (f'Only one record is expected to be update, found { total_count } records.' )
335
+ if not filters :
336
+ raise ValueError ('At least one filter condition must be provided for update operation' )
337
+
338
+ if not allow_multiple :
339
+ total_count = await self .count (session , * filters )
340
+ if total_count > 1 :
341
+ raise MultipleResultsError (f'Only one record is expected to be updated, found { total_count } records.' )
331
342
332
343
instance_data = obj if isinstance (obj , dict ) else obj .model_dump (exclude_unset = True )
333
344
stmt = update (self .model ).where (* filters ).values (** instance_data )
@@ -379,22 +390,30 @@ async def delete_model_by_column(
379
390
** kwargs ,
380
391
) -> int :
381
392
"""
382
- Delete an instance by model column
393
+ Delete records by model column filters.
383
394
384
- :param session: The SQLAlchemy async session.
385
- :param allow_multiple: If `True`, allows deleting multiple records that match the filters.
395
+ :param session: The SQLAlchemy async session
396
+ :param allow_multiple: If `True`, allows deleting multiple records that match the filters
386
397
:param logical_deletion: If `True`, enable logical deletion instead of physical deletion
387
- :param deleted_flag_column: Specify the flag column for logical deletion
388
- :param flush: If `True`, flush all object changes to the database. Default is `False`.
389
- :param commit: If `True`, commits the transaction immediately. Default is `False`.
390
- :param kwargs: Query expressions.
398
+ :param deleted_flag_column: Column name for logical deletion flag
399
+ :param flush: If `True`, flush all object changes to the database
400
+ :param commit: If `True`, commits the transaction immediately
401
+ :param kwargs: Filter expressions using field__operator=value syntax
391
402
:return:
392
403
"""
404
+ if logical_deletion :
405
+ if not hasattr (self .model , deleted_flag_column ):
406
+ raise ModelColumnError (f'Column { deleted_flag_column } is not found in { self .model } ' )
407
+
393
408
filters = parse_filters (self .model , ** kwargs )
394
409
395
- total_count = await self .count (session , * filters )
396
- if not allow_multiple and total_count > 1 :
397
- raise MultipleResultsError (f'Only one record is expected to be delete, found { total_count } records.' )
410
+ if not filters :
411
+ raise ValueError ('At least one filter condition must be provided for delete operation' )
412
+
413
+ if not allow_multiple :
414
+ total_count = await self .count (session , * filters )
415
+ if total_count > 1 :
416
+ raise MultipleResultsError (f'Only one record is expected to be deleted, found { total_count } records.' )
398
417
399
418
stmt = (
400
419
update (self .model ).where (* filters ).values (** {deleted_flag_column : True })
0 commit comments