fastapi-fsp 0.2.3__py3-none-any.whl → 0.4.0__py3-none-any.whl

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.
fastapi_fsp/__init__.py CHANGED
@@ -1,9 +1,52 @@
1
1
  """fastapi-fsp: Filter, Sort, and Paginate utilities for FastAPI + SQLModel."""
2
2
 
3
3
  from . import models as models # noqa: F401
4
+ from .builder import FieldBuilder, FilterBuilder # noqa: F401
5
+ from .config import FSPConfig, FSPPresets # noqa: F401
6
+ from .filters import FILTER_STRATEGIES, FilterEngine # noqa: F401
4
7
  from .fsp import FSPManager # noqa: F401
8
+ from .models import ( # noqa: F401
9
+ Filter,
10
+ FilterOperator,
11
+ Links,
12
+ Meta,
13
+ PaginatedResponse,
14
+ Pagination,
15
+ PaginationQuery,
16
+ SortingOrder,
17
+ SortingQuery,
18
+ )
19
+ from .pagination import PaginationEngine # noqa: F401
20
+ from .presets import CommonFilters # noqa: F401
21
+ from .sorting import SortEngine # noqa: F401
5
22
 
6
23
  __all__ = [
24
+ # Main class
7
25
  "FSPManager",
26
+ # Engines
27
+ "FilterEngine",
28
+ "SortEngine",
29
+ "PaginationEngine",
30
+ # Strategy registry
31
+ "FILTER_STRATEGIES",
32
+ # Builder
33
+ "FilterBuilder",
34
+ "FieldBuilder",
35
+ # Configuration
36
+ "FSPConfig",
37
+ "FSPPresets",
38
+ # Presets
39
+ "CommonFilters",
40
+ # Models
41
+ "Filter",
42
+ "FilterOperator",
43
+ "SortingOrder",
44
+ "SortingQuery",
45
+ "PaginationQuery",
46
+ "Pagination",
47
+ "Meta",
48
+ "Links",
49
+ "PaginatedResponse",
50
+ # Module
8
51
  "models",
9
52
  ]
fastapi_fsp/builder.py ADDED
@@ -0,0 +1,339 @@
1
+ """FilterBuilder API for creating filters with a fluent interface."""
2
+
3
+ from datetime import date, datetime
4
+ from typing import List, Optional, Union
5
+
6
+ from fastapi_fsp.models import Filter, FilterOperator
7
+
8
+
9
+ class FieldBuilder:
10
+ """
11
+ Builder for a single field's filter conditions.
12
+
13
+ Provides a fluent interface for building filter conditions on a specific field.
14
+ """
15
+
16
+ def __init__(self, filter_builder: "FilterBuilder", field: str):
17
+ """
18
+ Initialize FieldBuilder.
19
+
20
+ Args:
21
+ filter_builder: Parent FilterBuilder instance
22
+ field: Field name to build filters for
23
+ """
24
+ self._filter_builder = filter_builder
25
+ self._field = field
26
+
27
+ def _add_filter(self, operator: FilterOperator, value: str) -> "FilterBuilder":
28
+ """Add a filter and return the parent builder."""
29
+ self._filter_builder._filters.append(
30
+ Filter(field=self._field, operator=operator, value=value)
31
+ )
32
+ return self._filter_builder
33
+
34
+ @staticmethod
35
+ def _to_str(value: Union[str, int, float, bool, date, datetime]) -> str:
36
+ """Convert a value to string representation."""
37
+ if isinstance(value, bool):
38
+ return "true" if value else "false"
39
+ if isinstance(value, datetime):
40
+ return value.isoformat()
41
+ if isinstance(value, date):
42
+ return value.isoformat()
43
+ return str(value)
44
+
45
+ def eq(self, value: Union[str, int, float, bool, date, datetime]) -> "FilterBuilder":
46
+ """
47
+ Equal to (=).
48
+
49
+ Args:
50
+ value: Value to compare against
51
+
52
+ Returns:
53
+ FilterBuilder: Parent builder for chaining
54
+ """
55
+ return self._add_filter(FilterOperator.EQ, self._to_str(value))
56
+
57
+ def ne(self, value: Union[str, int, float, bool, date, datetime]) -> "FilterBuilder":
58
+ """
59
+ Not equal to (!=).
60
+
61
+ Args:
62
+ value: Value to compare against
63
+
64
+ Returns:
65
+ FilterBuilder: Parent builder for chaining
66
+ """
67
+ return self._add_filter(FilterOperator.NE, self._to_str(value))
68
+
69
+ def gt(self, value: Union[str, int, float, date, datetime]) -> "FilterBuilder":
70
+ """
71
+ Greater than (>).
72
+
73
+ Args:
74
+ value: Value to compare against
75
+
76
+ Returns:
77
+ FilterBuilder: Parent builder for chaining
78
+ """
79
+ return self._add_filter(FilterOperator.GT, self._to_str(value))
80
+
81
+ def gte(self, value: Union[str, int, float, date, datetime]) -> "FilterBuilder":
82
+ """
83
+ Greater than or equal to (>=).
84
+
85
+ Args:
86
+ value: Value to compare against
87
+
88
+ Returns:
89
+ FilterBuilder: Parent builder for chaining
90
+ """
91
+ return self._add_filter(FilterOperator.GTE, self._to_str(value))
92
+
93
+ def lt(self, value: Union[str, int, float, date, datetime]) -> "FilterBuilder":
94
+ """
95
+ Less than (<).
96
+
97
+ Args:
98
+ value: Value to compare against
99
+
100
+ Returns:
101
+ FilterBuilder: Parent builder for chaining
102
+ """
103
+ return self._add_filter(FilterOperator.LT, self._to_str(value))
104
+
105
+ def lte(self, value: Union[str, int, float, date, datetime]) -> "FilterBuilder":
106
+ """
107
+ Less than or equal to (<=).
108
+
109
+ Args:
110
+ value: Value to compare against
111
+
112
+ Returns:
113
+ FilterBuilder: Parent builder for chaining
114
+ """
115
+ return self._add_filter(FilterOperator.LTE, self._to_str(value))
116
+
117
+ def like(self, pattern: str) -> "FilterBuilder":
118
+ """
119
+ Case-sensitive LIKE pattern match.
120
+
121
+ Args:
122
+ pattern: LIKE pattern (use % for wildcards)
123
+
124
+ Returns:
125
+ FilterBuilder: Parent builder for chaining
126
+ """
127
+ return self._add_filter(FilterOperator.LIKE, pattern)
128
+
129
+ def not_like(self, pattern: str) -> "FilterBuilder":
130
+ """
131
+ Case-sensitive NOT LIKE pattern match.
132
+
133
+ Args:
134
+ pattern: LIKE pattern (use % for wildcards)
135
+
136
+ Returns:
137
+ FilterBuilder: Parent builder for chaining
138
+ """
139
+ return self._add_filter(FilterOperator.NOT_LIKE, pattern)
140
+
141
+ def ilike(self, pattern: str) -> "FilterBuilder":
142
+ """
143
+ Case-insensitive LIKE pattern match.
144
+
145
+ Args:
146
+ pattern: LIKE pattern (use % for wildcards)
147
+
148
+ Returns:
149
+ FilterBuilder: Parent builder for chaining
150
+ """
151
+ return self._add_filter(FilterOperator.ILIKE, pattern)
152
+
153
+ def not_ilike(self, pattern: str) -> "FilterBuilder":
154
+ """
155
+ Case-insensitive NOT LIKE pattern match.
156
+
157
+ Args:
158
+ pattern: LIKE pattern (use % for wildcards)
159
+
160
+ Returns:
161
+ FilterBuilder: Parent builder for chaining
162
+ """
163
+ return self._add_filter(FilterOperator.NOT_ILIKE, pattern)
164
+
165
+ def in_(self, values: List[Union[str, int, float, bool, date, datetime]]) -> "FilterBuilder":
166
+ """
167
+ IN list of values.
168
+
169
+ Args:
170
+ values: List of values to match against
171
+
172
+ Returns:
173
+ FilterBuilder: Parent builder for chaining
174
+ """
175
+ str_values = ",".join(self._to_str(v) for v in values)
176
+ return self._add_filter(FilterOperator.IN, str_values)
177
+
178
+ def not_in(self, values: List[Union[str, int, float, bool, date, datetime]]) -> "FilterBuilder":
179
+ """
180
+ NOT IN list of values.
181
+
182
+ Args:
183
+ values: List of values to exclude
184
+
185
+ Returns:
186
+ FilterBuilder: Parent builder for chaining
187
+ """
188
+ str_values = ",".join(self._to_str(v) for v in values)
189
+ return self._add_filter(FilterOperator.NOT_IN, str_values)
190
+
191
+ def between(
192
+ self,
193
+ low: Union[str, int, float, date, datetime],
194
+ high: Union[str, int, float, date, datetime],
195
+ ) -> "FilterBuilder":
196
+ """
197
+ BETWEEN low AND high (inclusive).
198
+
199
+ Args:
200
+ low: Lower bound
201
+ high: Upper bound
202
+
203
+ Returns:
204
+ FilterBuilder: Parent builder for chaining
205
+ """
206
+ value = f"{self._to_str(low)},{self._to_str(high)}"
207
+ return self._add_filter(FilterOperator.BETWEEN, value)
208
+
209
+ def is_null(self) -> "FilterBuilder":
210
+ """
211
+ IS NULL check.
212
+
213
+ Returns:
214
+ FilterBuilder: Parent builder for chaining
215
+ """
216
+ return self._add_filter(FilterOperator.IS_NULL, "")
217
+
218
+ def is_not_null(self) -> "FilterBuilder":
219
+ """
220
+ IS NOT NULL check.
221
+
222
+ Returns:
223
+ FilterBuilder: Parent builder for chaining
224
+ """
225
+ return self._add_filter(FilterOperator.IS_NOT_NULL, "")
226
+
227
+ def starts_with(self, prefix: str) -> "FilterBuilder":
228
+ """
229
+ Starts with prefix (case-insensitive).
230
+
231
+ Args:
232
+ prefix: String prefix to match
233
+
234
+ Returns:
235
+ FilterBuilder: Parent builder for chaining
236
+ """
237
+ return self._add_filter(FilterOperator.STARTS_WITH, prefix)
238
+
239
+ def ends_with(self, suffix: str) -> "FilterBuilder":
240
+ """
241
+ Ends with suffix (case-insensitive).
242
+
243
+ Args:
244
+ suffix: String suffix to match
245
+
246
+ Returns:
247
+ FilterBuilder: Parent builder for chaining
248
+ """
249
+ return self._add_filter(FilterOperator.ENDS_WITH, suffix)
250
+
251
+ def contains(self, substring: str) -> "FilterBuilder":
252
+ """
253
+ Contains substring (case-insensitive).
254
+
255
+ Args:
256
+ substring: String to search for
257
+
258
+ Returns:
259
+ FilterBuilder: Parent builder for chaining
260
+ """
261
+ return self._add_filter(FilterOperator.CONTAINS, substring)
262
+
263
+
264
+ class FilterBuilder:
265
+ """
266
+ Fluent builder for creating filter lists.
267
+
268
+ Example usage:
269
+ filters = (
270
+ FilterBuilder()
271
+ .where("age").gte(30)
272
+ .where("city").eq("Chicago")
273
+ .where("deleted").eq(False)
274
+ .build()
275
+ )
276
+
277
+ This creates a list of Filter objects that can be used with FSPManager.
278
+ """
279
+
280
+ def __init__(self):
281
+ """Initialize an empty FilterBuilder."""
282
+ self._filters: List[Filter] = []
283
+
284
+ def where(self, field: str) -> FieldBuilder:
285
+ """
286
+ Start building a filter for a field.
287
+
288
+ Args:
289
+ field: Name of the field to filter on
290
+
291
+ Returns:
292
+ FieldBuilder: Builder for the field's filter condition
293
+ """
294
+ return FieldBuilder(self, field)
295
+
296
+ def add_filter(self, field: str, operator: FilterOperator, value: str) -> "FilterBuilder":
297
+ """
298
+ Add a filter directly.
299
+
300
+ Args:
301
+ field: Field name
302
+ operator: Filter operator
303
+ value: Filter value as string
304
+
305
+ Returns:
306
+ FilterBuilder: Self for chaining
307
+ """
308
+ self._filters.append(Filter(field=field, operator=operator, value=value))
309
+ return self
310
+
311
+ def add_filters(self, filters: List[Filter]) -> "FilterBuilder":
312
+ """
313
+ Add multiple filters at once.
314
+
315
+ Args:
316
+ filters: List of Filter objects to add
317
+
318
+ Returns:
319
+ FilterBuilder: Self for chaining
320
+ """
321
+ self._filters.extend(filters)
322
+ return self
323
+
324
+ def build(self) -> Optional[List[Filter]]:
325
+ """
326
+ Build and return the list of filters.
327
+
328
+ Returns:
329
+ Optional[List[Filter]]: List of filters, or None if empty
330
+ """
331
+ return self._filters if self._filters else None
332
+
333
+ def __len__(self) -> int:
334
+ """Return the number of filters."""
335
+ return len(self._filters)
336
+
337
+ def __bool__(self) -> bool:
338
+ """Return True if there are any filters."""
339
+ return bool(self._filters)
fastapi_fsp/config.py ADDED
@@ -0,0 +1,158 @@
1
+ """Configuration classes for fastapi-fsp."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Optional
5
+
6
+
7
+ @dataclass
8
+ class FSPConfig:
9
+ """
10
+ Configuration for FSP behavior.
11
+
12
+ This class centralizes all configuration options for the FSPManager,
13
+ making it easier to customize behavior across your application.
14
+
15
+ Attributes:
16
+ max_per_page: Maximum allowed items per page (default: 100)
17
+ default_per_page: Default items per page when not specified (default: 10)
18
+ default_page: Default page number when not specified (default: 1)
19
+ strict_mode: If True, raise errors for unknown fields (default: False)
20
+ allow_deep_pagination: If True, allow pagination to any page (default: True)
21
+ max_page: Maximum allowed page number, None for unlimited (default: None)
22
+ min_per_page: Minimum allowed items per page (default: 1)
23
+
24
+ Example:
25
+ # Create a strict configuration
26
+ config = FSPConfig(
27
+ strict_mode=True,
28
+ max_per_page=50,
29
+ default_per_page=20
30
+ )
31
+
32
+ # Use with FSPManager
33
+ def get_fsp_config():
34
+ return FSPConfig(strict_mode=True)
35
+
36
+ @app.get("/items/")
37
+ def read_items(
38
+ fsp: FSPManager = Depends(FSPManager),
39
+ config: FSPConfig = Depends(get_fsp_config)
40
+ ):
41
+ fsp.apply_config(config)
42
+ ...
43
+ """
44
+
45
+ # Pagination settings
46
+ max_per_page: int = 100
47
+ default_per_page: int = 10
48
+ default_page: int = 1
49
+ min_per_page: int = 1
50
+
51
+ # Validation settings
52
+ strict_mode: bool = False
53
+
54
+ # Deep pagination settings
55
+ allow_deep_pagination: bool = True
56
+ max_page: Optional[int] = None
57
+
58
+ # Reserved for future features
59
+ _extra: dict = field(default_factory=dict)
60
+
61
+ def __post_init__(self):
62
+ """Validate configuration values."""
63
+ if self.max_per_page < 1:
64
+ raise ValueError("max_per_page must be >= 1")
65
+ if self.default_per_page < 1:
66
+ raise ValueError("default_per_page must be >= 1")
67
+ if self.default_per_page > self.max_per_page:
68
+ raise ValueError("default_per_page cannot exceed max_per_page")
69
+ if self.min_per_page < 1:
70
+ raise ValueError("min_per_page must be >= 1")
71
+ if self.min_per_page > self.max_per_page:
72
+ raise ValueError("min_per_page cannot exceed max_per_page")
73
+ if self.default_page < 1:
74
+ raise ValueError("default_page must be >= 1")
75
+ if self.max_page is not None and self.max_page < 1:
76
+ raise ValueError("max_page must be >= 1 or None")
77
+
78
+ def validate_page(self, page: int) -> int:
79
+ """
80
+ Validate and constrain a page number.
81
+
82
+ Args:
83
+ page: Requested page number
84
+
85
+ Returns:
86
+ int: Valid page number
87
+
88
+ Raises:
89
+ ValueError: If page exceeds max_page and allow_deep_pagination is False
90
+ """
91
+ if page < 1:
92
+ return self.default_page
93
+
94
+ if not self.allow_deep_pagination and self.max_page is not None:
95
+ if page > self.max_page:
96
+ raise ValueError(f"Page {page} exceeds maximum allowed page {self.max_page}")
97
+
98
+ return page
99
+
100
+ def validate_per_page(self, per_page: int) -> int:
101
+ """
102
+ Validate and constrain items per page.
103
+
104
+ Args:
105
+ per_page: Requested items per page
106
+
107
+ Returns:
108
+ int: Valid per_page value, constrained to min/max bounds
109
+ """
110
+ if per_page < self.min_per_page:
111
+ return self.min_per_page
112
+ if per_page > self.max_per_page:
113
+ return self.max_per_page
114
+ return per_page
115
+
116
+
117
+ # Pre-defined configurations for common use cases
118
+ class FSPPresets:
119
+ """Pre-defined FSPConfig presets for common use cases."""
120
+
121
+ @staticmethod
122
+ def default() -> FSPConfig:
123
+ """Default configuration with sensible defaults."""
124
+ return FSPConfig()
125
+
126
+ @staticmethod
127
+ def strict() -> FSPConfig:
128
+ """Strict mode configuration - raises errors for unknown fields."""
129
+ return FSPConfig(strict_mode=True)
130
+
131
+ @staticmethod
132
+ def limited_pagination(max_page: int = 100, max_per_page: int = 50) -> FSPConfig:
133
+ """
134
+ Configuration that limits deep pagination.
135
+
136
+ Args:
137
+ max_page: Maximum allowed page number
138
+ max_per_page: Maximum items per page
139
+ """
140
+ return FSPConfig(
141
+ max_page=max_page,
142
+ max_per_page=max_per_page,
143
+ allow_deep_pagination=False,
144
+ )
145
+
146
+ @staticmethod
147
+ def high_volume(max_per_page: int = 500, default_per_page: int = 100) -> FSPConfig:
148
+ """
149
+ Configuration for high-volume APIs.
150
+
151
+ Args:
152
+ max_per_page: Maximum items per page
153
+ default_per_page: Default items per page
154
+ """
155
+ return FSPConfig(
156
+ max_per_page=max_per_page,
157
+ default_per_page=default_per_page,
158
+ )