1
- from typing import Any , Generic , Union , Optional , Callable
1
+ from typing import Any , Generic , Union , Optional , Callable , cast
2
2
from datetime import datetime , timezone
3
3
4
4
from pydantic import ValidationError
15
15
desc ,
16
16
or_ ,
17
17
column ,
18
+ not_ ,
19
+ Column ,
18
20
)
19
21
from sqlalchemy .exc import ArgumentError , MultipleResultsFound , NoResultFound
20
22
from sqlalchemy .sql import Join
47
49
48
50
from ..endpoint .helper import _get_primary_keys
49
51
52
+ FilterCallable = Callable [[Column [Any ]], Callable [..., ColumnElement [bool ]]]
50
53
51
54
class FastCRUD (
52
55
Generic [
@@ -453,6 +456,7 @@ class Comment(Base):
453
456
"""
454
457
455
458
_SUPPORTED_FILTERS = {
459
+ "eq" : lambda column : column .__eq__ ,
456
460
"gt" : lambda column : column .__gt__ ,
457
461
"lt" : lambda column : column .__lt__ ,
458
462
"gte" : lambda column : column .__ge__ ,
@@ -471,6 +475,8 @@ class Comment(Base):
471
475
"between" : lambda column : column .between ,
472
476
"in" : lambda column : column .in_ ,
473
477
"not_in" : lambda column : column .not_in ,
478
+ "or" : lambda column : column .or_ ,
479
+ "not" : lambda column : column .not_ ,
474
480
}
475
481
476
482
def __init__ (
@@ -491,51 +497,131 @@ def _get_sqlalchemy_filter(
491
497
self ,
492
498
operator : str ,
493
499
value : Any ,
494
- ) -> Optional [Callable [[ str ], Callable ] ]:
500
+ ) -> Optional [FilterCallable ]:
495
501
if operator in {"in" , "not_in" , "between" }:
496
502
if not isinstance (value , (tuple , list , set )):
497
503
raise ValueError (f"<{ operator } > filter must be tuple, list or set" )
498
- return self ._SUPPORTED_FILTERS .get (operator )
504
+ return cast ( Optional [ FilterCallable ], self ._SUPPORTED_FILTERS .get (operator ) )
499
505
500
506
def _parse_filters (
501
- self , model : Optional [Union [type [ModelType ], AliasedClass ]] = None , ** kwargs
507
+ self ,
508
+ model : Optional [Union [type [ModelType ], AliasedClass ]] = None ,
509
+ ** kwargs
502
510
) -> list [ColumnElement ]:
511
+ """Parse and convert filter arguments into SQLAlchemy filter conditions.
512
+
513
+ Args:
514
+ model: The model to apply filters to. Defaults to self.model
515
+ **kwargs: Filter arguments in the format field_name__operator=value
516
+
517
+ Returns:
518
+ List of SQLAlchemy filter conditions
519
+ """
503
520
model = model or self .model
504
521
filters = []
505
522
506
523
for key , value in kwargs .items ():
507
- if "__" in key :
508
- field_name , op = key .rsplit ("__" , 1 )
509
- column = getattr (model , field_name , None )
510
- if column is None :
511
- raise ValueError (f"Invalid filter column: { field_name } " )
512
- if op == "or" :
513
- or_filters = [
514
- sqlalchemy_filter (column )(or_value )
515
- for or_key , or_value in value .items ()
516
- if (
517
- sqlalchemy_filter := self ._get_sqlalchemy_filter (
518
- or_key , or_value
519
- )
520
- )
521
- is not None
522
- ]
523
- filters .append (or_ (* or_filters ))
524
- else :
525
- sqlalchemy_filter = self ._get_sqlalchemy_filter (op , value )
526
- if sqlalchemy_filter :
527
- filters .append (
528
- sqlalchemy_filter (column )(value )
529
- if op != "between"
530
- else sqlalchemy_filter (column )(* value )
531
- )
524
+ if "__" not in key :
525
+ filters .extend (self ._handle_simple_filter (model , key , value ))
526
+ continue
527
+
528
+ field_name , operator = key .rsplit ("__" , 1 )
529
+ model_column = self ._get_column (model , field_name )
530
+
531
+ if operator == "or" :
532
+ filters .extend (self ._handle_or_filter (model_column , value ))
533
+ elif operator == "not" :
534
+ filters .extend (self ._handle_not_filter (model_column , value ))
532
535
else :
533
- column = getattr (model , key , None )
534
- if column is not None :
535
- filters .append (column == value )
536
+ filters .extend (self ._handle_standard_filter (model_column , operator , value ))
536
537
537
538
return filters
538
539
540
+ def _handle_simple_filter (
541
+ self ,
542
+ model : Union [type [ModelType ], AliasedClass ],
543
+ key : str ,
544
+ value : Any
545
+ ) -> list [ColumnElement ]:
546
+ """Handle simple equality filters (e.g., name='John')."""
547
+ col = getattr (model , key , None )
548
+ return [col == value ] if col is not None else []
549
+
550
+ def _handle_or_filter (
551
+ self ,
552
+ col : Column ,
553
+ value : dict
554
+ ) -> list [ColumnElement ]:
555
+ """Handle OR conditions (e.g., age__or={'gt': 18, 'lt': 65})."""
556
+ if not isinstance (value , dict ):
557
+ raise ValueError ("OR filter value must be a dictionary" )
558
+
559
+ or_conditions = []
560
+ for or_op , or_value in value .items ():
561
+ sqlalchemy_filter = self ._get_sqlalchemy_filter (or_op , or_value )
562
+ if sqlalchemy_filter :
563
+ condition = (
564
+ sqlalchemy_filter (col )(* or_value )
565
+ if or_op == "between"
566
+ else sqlalchemy_filter (col )(or_value )
567
+ )
568
+ or_conditions .append (condition )
569
+
570
+ return [or_ (* or_conditions )] if or_conditions else []
571
+
572
+ def _handle_not_filter (
573
+ self ,
574
+ col : Column ,
575
+ value : dict
576
+ ) -> list [ColumnElement [bool ]]:
577
+ """Handle NOT conditions (e.g., age__not={'eq': 20, 'between': (30, 40)})."""
578
+ if not isinstance (value , dict ):
579
+ raise ValueError ("NOT filter value must be a dictionary" )
580
+
581
+ not_conditions = []
582
+ for not_op , not_value in value .items ():
583
+ sqlalchemy_filter = self ._get_sqlalchemy_filter (not_op , not_value )
584
+ if sqlalchemy_filter is None :
585
+ continue
586
+
587
+ condition = (
588
+ sqlalchemy_filter (col )(* not_value )
589
+ if not_op == "between"
590
+ else sqlalchemy_filter (col )(not_value )
591
+ )
592
+ not_conditions .append (condition )
593
+
594
+ return [and_ (* (not_ (cond ) for cond in not_conditions ))] if not_conditions else []
595
+
596
+ def _handle_standard_filter (
597
+ self ,
598
+ col : Column [Any ],
599
+ operator : str ,
600
+ value : Any
601
+ ) -> list [ColumnElement [bool ]]:
602
+ """Handle standard comparison operators (e.g., age__gt=18)."""
603
+ sqlalchemy_filter = self ._get_sqlalchemy_filter (operator , value )
604
+ if sqlalchemy_filter is None :
605
+ return []
606
+
607
+ condition = (
608
+ sqlalchemy_filter (col )(* value )
609
+ if operator == "between"
610
+ else sqlalchemy_filter (col )(value )
611
+ )
612
+ return [condition ]
613
+
614
+ def _get_column (
615
+ self ,
616
+ model : Union [type [ModelType ], AliasedClass ],
617
+ field_name : str
618
+ ) -> Column [Any ]:
619
+ """Get column from model, raising ValueError if not found."""
620
+ model_column = getattr (model , field_name , None )
621
+ if model_column is None :
622
+ raise ValueError (f"Invalid filter column: { field_name } " )
623
+ return cast (Column [Any ], model_column )
624
+
539
625
def _apply_sorting (
540
626
self ,
541
627
stmt : Select ,
@@ -2080,11 +2166,9 @@ async def get_multi_by_cursor(
2080
2166
db,
2081
2167
limit=10,
2082
2168
sort_column='registration_date',
2083
- sort_order='desc',
2084
2169
)
2085
2170
2086
2171
# Fetch the next set of records using the cursor from the first page
2087
- next_cursor = first_page['next_cursor']
2088
2172
second_page = await user_crud.get_multi_by_cursor(
2089
2173
db,
2090
2174
cursor=next_cursor,
@@ -2102,14 +2186,12 @@ async def get_multi_by_cursor(
2102
2186
limit=10,
2103
2187
sort_column='age',
2104
2188
sort_order='asc',
2105
- age__gt=30,
2106
2189
)
2107
2190
```
2108
2191
2109
2192
Fetch records excluding a specific username using cursor-based pagination:
2110
2193
2111
2194
```python
2112
- first_page = await user_crud.get_multi_by_cursor(
2113
2195
db,
2114
2196
limit=10,
2115
2197
sort_column='username',
@@ -2121,7 +2203,6 @@ async def get_multi_by_cursor(
2121
2203
Note:
2122
2204
This method is designed for efficient pagination in large datasets and is ideal for infinite scrolling features.
2123
2205
Make sure the column used for cursor pagination is indexed for performance.
2124
- This method assumes that your records can be ordered by a unique, sequential field (like `id` or `created_at`).
2125
2206
"""
2126
2207
if limit == 0 :
2127
2208
return {"data" : [], "next_cursor" : None }
0 commit comments