2020 InCollectionFilter ,
2121 LimitOffsetFilter ,
2222 NotInCollectionFilter ,
23+ NotNullFilter ,
24+ NullFilter ,
2325 OrderByFilter ,
2426 SearchFilter ,
2527)
@@ -91,6 +93,10 @@ class FilterConfig(TypedDict):
9193 updated_at : NotRequired [bool ]
9294 not_in_fields : NotRequired [FieldNameType | set [FieldNameType ] | list [str | FieldNameType ]]
9395 in_fields : NotRequired [FieldNameType | set [FieldNameType ] | list [str | FieldNameType ]]
96+ null_fields : NotRequired [str | set [str ] | list [str ]]
97+ """Fields that support IS NULL filtering."""
98+ not_null_fields : NotRequired [str | set [str ] | list [str ]]
99+ """Fields that support IS NOT NULL filtering."""
94100
95101
96102class DependencyCache (metaclass = SingletonMeta ):
@@ -152,7 +158,7 @@ def _make_hashable(value: Any) -> HashableType:
152158 return str (value )
153159
154160
155- def _create_statement_filters (
161+ def _create_statement_filters ( # noqa: C901
156162 config : FilterConfig , dep_defaults : DependencyDefaults = DEPENDENCY_DEFAULTS
157163) -> dict [str , Provide ]:
158164 """Create filter dependencies based on configuration.
@@ -294,6 +300,40 @@ def provide_in_filter( # pyright: ignore
294300 provider = create_in_filter_provider (field_def ) # type: ignore
295301 filters [f"{ field_def .name } _in_filter" ] = Provide (provider , sync_to_thread = False ) # pyright: ignore
296302
303+ if null_fields := config .get ("null_fields" ):
304+ null_fields = {null_fields } if isinstance (null_fields , str ) else set (null_fields )
305+
306+ for field_name in null_fields :
307+
308+ def create_null_filter_provider (fname : str ) -> Callable [..., NullFilter | None ]:
309+ def provide_null_filter (
310+ is_null : bool | None = Parameter (query = camelize (f"{ fname } _is_null" ), default = None , required = False ),
311+ ) -> NullFilter | None :
312+ return NullFilter (field_name = fname ) if is_null else None
313+
314+ return provide_null_filter
315+
316+ null_provider = create_null_filter_provider (field_name )
317+ filters [f"{ field_name } _null_filter" ] = Provide (null_provider , sync_to_thread = False )
318+
319+ if not_null_fields := config .get ("not_null_fields" ):
320+ not_null_fields = {not_null_fields } if isinstance (not_null_fields , str ) else set (not_null_fields )
321+
322+ for field_name in not_null_fields :
323+
324+ def create_not_null_filter_provider (fname : str ) -> Callable [..., NotNullFilter | None ]:
325+ def provide_not_null_filter (
326+ is_not_null : bool | None = Parameter (
327+ query = camelize (f"{ fname } _is_not_null" ), default = None , required = False
328+ ),
329+ ) -> NotNullFilter | None :
330+ return NotNullFilter (field_name = fname ) if is_not_null else None
331+
332+ return provide_not_null_filter
333+
334+ not_null_provider = create_not_null_filter_provider (field_name )
335+ filters [f"{ field_name } _not_null_filter" ] = Provide (not_null_provider , sync_to_thread = False )
336+
297337 if filters :
298338 filters [dep_defaults .FILTERS_DEPENDENCY_KEY ] = Provide (
299339 _create_filter_aggregate_function (config ), sync_to_thread = False
@@ -302,7 +342,7 @@ def provide_in_filter( # pyright: ignore
302342 return filters
303343
304344
305- def _create_filter_aggregate_function (config : FilterConfig ) -> Callable [..., list [FilterTypes ]]:
345+ def _create_filter_aggregate_function (config : FilterConfig ) -> Callable [..., list [FilterTypes ]]: # noqa: C901
306346 """Create filter aggregation function based on configuration.
307347
308348 Args:
@@ -391,6 +431,28 @@ def _create_filter_aggregate_function(config: FilterConfig) -> Callable[..., lis
391431 )
392432 annotations [f"{ field_def .name } _in_filter" ] = InCollectionFilter [field_def .type_hint ] # type: ignore
393433
434+ if null_fields := config .get ("null_fields" ):
435+ null_fields = {null_fields } if isinstance (null_fields , str ) else set (null_fields )
436+ for field_name in null_fields :
437+ parameters [f"{ field_name } _null_filter" ] = inspect .Parameter (
438+ name = f"{ field_name } _null_filter" ,
439+ kind = inspect .Parameter .POSITIONAL_OR_KEYWORD ,
440+ default = Dependency (skip_validation = True ),
441+ annotation = NullFilter | None ,
442+ )
443+ annotations [f"{ field_name } _null_filter" ] = NullFilter | None
444+
445+ if not_null_fields := config .get ("not_null_fields" ):
446+ not_null_fields = {not_null_fields } if isinstance (not_null_fields , str ) else set (not_null_fields )
447+ for field_name in not_null_fields :
448+ parameters [f"{ field_name } _not_null_filter" ] = inspect .Parameter (
449+ name = f"{ field_name } _not_null_filter" ,
450+ kind = inspect .Parameter .POSITIONAL_OR_KEYWORD ,
451+ default = Dependency (skip_validation = True ),
452+ annotation = NotNullFilter | None ,
453+ )
454+ annotations [f"{ field_name } _not_null_filter" ] = NotNullFilter | None
455+
394456 def provide_filters (** kwargs : FilterTypes ) -> list [FilterTypes ]:
395457 """Aggregate filter dependencies based on configuration.
396458
@@ -438,6 +500,21 @@ def provide_filters(**kwargs: FilterTypes) -> list[FilterTypes]:
438500 filter_ = kwargs .get (f"{ field_def .name } _in_filter" )
439501 if filter_ is not None :
440502 filters .append (filter_ )
503+
504+ if null_fields := config .get ("null_fields" ):
505+ null_fields = {null_fields } if isinstance (null_fields , str ) else set (null_fields )
506+ for field_name in null_fields :
507+ filter_ = kwargs .get (f"{ field_name } _null_filter" )
508+ if filter_ is not None :
509+ filters .append (filter_ )
510+
511+ if not_null_fields := config .get ("not_null_fields" ):
512+ not_null_fields = {not_null_fields } if isinstance (not_null_fields , str ) else set (not_null_fields )
513+ for field_name in not_null_fields :
514+ filter_ = kwargs .get (f"{ field_name } _not_null_filter" )
515+ if filter_ is not None :
516+ filters .append (filter_ )
517+
441518 return filters
442519
443520 provide_filters .__signature__ = inspect .Signature ( # type: ignore
0 commit comments