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 +43 -0
- fastapi_fsp/builder.py +339 -0
- fastapi_fsp/config.py +158 -0
- fastapi_fsp/filters.py +372 -0
- fastapi_fsp/fsp.py +192 -298
- fastapi_fsp/pagination.py +324 -0
- fastapi_fsp/presets.py +267 -0
- fastapi_fsp/sorting.py +71 -0
- {fastapi_fsp-0.2.3.dist-info → fastapi_fsp-0.4.0.dist-info}/METADATA +159 -1
- fastapi_fsp-0.4.0.dist-info/RECORD +13 -0
- fastapi_fsp-0.2.3.dist-info/RECORD +0 -7
- {fastapi_fsp-0.2.3.dist-info → fastapi_fsp-0.4.0.dist-info}/WHEEL +0 -0
- {fastapi_fsp-0.2.3.dist-info → fastapi_fsp-0.4.0.dist-info}/licenses/LICENSE +0 -0
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
|
+
)
|