strapi-kit 0.0.1__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.
- strapi_kit/__init__.py +97 -0
- strapi_kit/__version__.py +15 -0
- strapi_kit/_version.py +34 -0
- strapi_kit/auth/__init__.py +7 -0
- strapi_kit/auth/api_token.py +48 -0
- strapi_kit/cache/__init__.py +5 -0
- strapi_kit/cache/schema_cache.py +211 -0
- strapi_kit/client/__init__.py +11 -0
- strapi_kit/client/async_client.py +1032 -0
- strapi_kit/client/base.py +460 -0
- strapi_kit/client/sync_client.py +980 -0
- strapi_kit/config_provider.py +368 -0
- strapi_kit/exceptions/__init__.py +37 -0
- strapi_kit/exceptions/errors.py +205 -0
- strapi_kit/export/__init__.py +10 -0
- strapi_kit/export/exporter.py +384 -0
- strapi_kit/export/importer.py +619 -0
- strapi_kit/export/media_handler.py +322 -0
- strapi_kit/export/relation_resolver.py +172 -0
- strapi_kit/models/__init__.py +104 -0
- strapi_kit/models/bulk.py +69 -0
- strapi_kit/models/config.py +174 -0
- strapi_kit/models/enums.py +97 -0
- strapi_kit/models/export_format.py +166 -0
- strapi_kit/models/import_options.py +142 -0
- strapi_kit/models/request/__init__.py +1 -0
- strapi_kit/models/request/fields.py +65 -0
- strapi_kit/models/request/filters.py +611 -0
- strapi_kit/models/request/pagination.py +168 -0
- strapi_kit/models/request/populate.py +281 -0
- strapi_kit/models/request/query.py +429 -0
- strapi_kit/models/request/sort.py +147 -0
- strapi_kit/models/response/__init__.py +1 -0
- strapi_kit/models/response/base.py +75 -0
- strapi_kit/models/response/component.py +67 -0
- strapi_kit/models/response/media.py +91 -0
- strapi_kit/models/response/meta.py +44 -0
- strapi_kit/models/response/normalized.py +168 -0
- strapi_kit/models/response/relation.py +48 -0
- strapi_kit/models/response/v4.py +70 -0
- strapi_kit/models/response/v5.py +57 -0
- strapi_kit/models/schema.py +93 -0
- strapi_kit/operations/__init__.py +16 -0
- strapi_kit/operations/media.py +226 -0
- strapi_kit/operations/streaming.py +144 -0
- strapi_kit/parsers/__init__.py +5 -0
- strapi_kit/parsers/version_detecting.py +171 -0
- strapi_kit/protocols.py +455 -0
- strapi_kit/utils/__init__.py +15 -0
- strapi_kit/utils/rate_limiter.py +201 -0
- strapi_kit/utils/uid.py +88 -0
- strapi_kit-0.0.1.dist-info/METADATA +1098 -0
- strapi_kit-0.0.1.dist-info/RECORD +55 -0
- strapi_kit-0.0.1.dist-info/WHEEL +4 -0
- strapi_kit-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
"""Main query builder combining all request parameters.
|
|
2
|
+
|
|
3
|
+
The StrapiQuery class provides a fluent API for building complete Strapi API queries
|
|
4
|
+
with filters, sorting, pagination, population, and field selection.
|
|
5
|
+
|
|
6
|
+
Examples:
|
|
7
|
+
Simple query:
|
|
8
|
+
>>> query = (StrapiQuery()
|
|
9
|
+
... .filter(FilterBuilder().eq("status", "published"))
|
|
10
|
+
... .sort_by("publishedAt", SortDirection.DESC)
|
|
11
|
+
... .paginate(page=1, page_size=25))
|
|
12
|
+
|
|
13
|
+
Complex query with relations:
|
|
14
|
+
>>> query = (StrapiQuery()
|
|
15
|
+
... .filter(FilterBuilder()
|
|
16
|
+
... .eq("status", "published")
|
|
17
|
+
... .gt("views", 100))
|
|
18
|
+
... .sort_by("views", SortDirection.DESC)
|
|
19
|
+
... .paginate(page=1, page_size=10)
|
|
20
|
+
... .populate_fields(["author", "category"])
|
|
21
|
+
... .select(["title", "description", "publishedAt"]))
|
|
22
|
+
|
|
23
|
+
Advanced query with nested population:
|
|
24
|
+
>>> query = (StrapiQuery()
|
|
25
|
+
... .filter(FilterBuilder().eq("featured", True))
|
|
26
|
+
... .populate(Populate()
|
|
27
|
+
... .add_field("author", fields=["name", "email"])
|
|
28
|
+
... .add_field("comments",
|
|
29
|
+
... filters=FilterBuilder().eq("approved", True),
|
|
30
|
+
... sort=Sort().by_field("createdAt", SortDirection.DESC)))
|
|
31
|
+
... .with_locale("fr"))
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import copy
|
|
37
|
+
from typing import Any
|
|
38
|
+
|
|
39
|
+
from strapi_kit.models.enums import PublicationState, SortDirection
|
|
40
|
+
from strapi_kit.models.request.fields import FieldSelection
|
|
41
|
+
from strapi_kit.models.request.filters import FilterBuilder
|
|
42
|
+
from strapi_kit.models.request.pagination import OffsetPagination, PagePagination, Pagination
|
|
43
|
+
from strapi_kit.models.request.populate import Populate
|
|
44
|
+
from strapi_kit.models.request.sort import Sort
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _flatten_dict(d: dict[str, Any], parent_key: str = "", sep: str = "") -> dict[str, Any]:
|
|
48
|
+
"""Flatten a nested dictionary into bracket notation for query parameters.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
d: Dictionary to flatten
|
|
52
|
+
parent_key: Parent key prefix
|
|
53
|
+
sep: Separator (empty for bracket notation)
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Flattened dictionary with bracket notation keys
|
|
57
|
+
|
|
58
|
+
Examples:
|
|
59
|
+
>>> _flatten_dict({"status": {"$eq": "published"}}, "filters")
|
|
60
|
+
{'filters[status][$eq]': 'published'}
|
|
61
|
+
|
|
62
|
+
>>> _flatten_dict({"$or": [{"views": {"$gt": 100}}]}, "filters")
|
|
63
|
+
{'filters[$or][0][views][$gt]': 100}
|
|
64
|
+
"""
|
|
65
|
+
items: list[tuple[str, Any]] = []
|
|
66
|
+
for k, v in d.items():
|
|
67
|
+
new_key = f"{parent_key}[{k}]" if parent_key else k
|
|
68
|
+
if isinstance(v, dict):
|
|
69
|
+
items.extend(_flatten_dict(v, new_key, sep).items())
|
|
70
|
+
elif isinstance(v, list):
|
|
71
|
+
# Check if list contains dicts (nested filters like $or, $and)
|
|
72
|
+
if v and isinstance(v[0], dict):
|
|
73
|
+
# Flatten each dict in the array with index
|
|
74
|
+
for i, item in enumerate(v):
|
|
75
|
+
if isinstance(item, dict):
|
|
76
|
+
indexed_key = f"{new_key}[{i}]"
|
|
77
|
+
items.extend(_flatten_dict(item, indexed_key, sep).items())
|
|
78
|
+
else:
|
|
79
|
+
items.append((f"{new_key}[{i}]", item))
|
|
80
|
+
else:
|
|
81
|
+
# Simple list (e.g., $in values) - keep as-is for httpx
|
|
82
|
+
items.append((new_key, v))
|
|
83
|
+
else:
|
|
84
|
+
items.append((new_key, v))
|
|
85
|
+
return dict(items)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class StrapiQuery:
|
|
89
|
+
"""Main query builder for Strapi API requests.
|
|
90
|
+
|
|
91
|
+
Combines filters, sorting, pagination, population, and field selection
|
|
92
|
+
into a complete query configuration.
|
|
93
|
+
|
|
94
|
+
Examples:
|
|
95
|
+
>>> # Basic query
|
|
96
|
+
>>> query = StrapiQuery().filter(FilterBuilder().eq("status", "published"))
|
|
97
|
+
|
|
98
|
+
>>> # Complete query
|
|
99
|
+
>>> query = (StrapiQuery()
|
|
100
|
+
... .filter(FilterBuilder().eq("status", "published"))
|
|
101
|
+
... .sort_by("publishedAt", SortDirection.DESC)
|
|
102
|
+
... .paginate(page=1, page_size=25)
|
|
103
|
+
... .populate_fields(["author", "category"])
|
|
104
|
+
... .select(["title", "description"]))
|
|
105
|
+
|
|
106
|
+
>>> # Convert to query parameters
|
|
107
|
+
>>> params = query.to_query_params()
|
|
108
|
+
>>> # params can be passed to httpx: client.get(url, params=params)
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
def __init__(self) -> None:
|
|
112
|
+
"""Initialize an empty query."""
|
|
113
|
+
self._filters: FilterBuilder | None = None
|
|
114
|
+
self._sort: Sort | None = None
|
|
115
|
+
self._pagination: Pagination | None = None
|
|
116
|
+
self._populate: Populate | None = None
|
|
117
|
+
self._fields: FieldSelection | None = None
|
|
118
|
+
self._locale: str | None = None
|
|
119
|
+
self._publication_state: PublicationState | None = None
|
|
120
|
+
|
|
121
|
+
def copy(self) -> StrapiQuery:
|
|
122
|
+
"""Create a deep copy of this query.
|
|
123
|
+
|
|
124
|
+
Useful for modifying a query without affecting the original,
|
|
125
|
+
especially in streaming operations that modify pagination.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Deep copy of this query
|
|
129
|
+
|
|
130
|
+
Examples:
|
|
131
|
+
>>> base_query = StrapiQuery().filter(FilterBuilder().eq("status", "published"))
|
|
132
|
+
>>> modified = base_query.copy().paginate(page=2)
|
|
133
|
+
>>> # base_query is unchanged
|
|
134
|
+
"""
|
|
135
|
+
new_query = StrapiQuery()
|
|
136
|
+
new_query._filters = copy.deepcopy(self._filters)
|
|
137
|
+
new_query._sort = copy.deepcopy(self._sort)
|
|
138
|
+
new_query._pagination = copy.deepcopy(self._pagination)
|
|
139
|
+
new_query._populate = copy.deepcopy(self._populate)
|
|
140
|
+
new_query._fields = copy.deepcopy(self._fields)
|
|
141
|
+
new_query._locale = self._locale
|
|
142
|
+
new_query._publication_state = self._publication_state
|
|
143
|
+
return new_query
|
|
144
|
+
|
|
145
|
+
def filter(self, filters: FilterBuilder) -> StrapiQuery:
|
|
146
|
+
"""Add filter conditions to the query.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
filters: FilterBuilder with filter conditions
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
Self for method chaining
|
|
153
|
+
|
|
154
|
+
Examples:
|
|
155
|
+
>>> query = StrapiQuery().filter(
|
|
156
|
+
... FilterBuilder()
|
|
157
|
+
... .eq("status", "published")
|
|
158
|
+
... .gt("views", 100)
|
|
159
|
+
... )
|
|
160
|
+
"""
|
|
161
|
+
self._filters = filters
|
|
162
|
+
return self
|
|
163
|
+
|
|
164
|
+
def sort_by(self, field: str, direction: SortDirection = SortDirection.ASC) -> StrapiQuery:
|
|
165
|
+
"""Add sorting to the query.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
field: Field name to sort by
|
|
169
|
+
direction: Sort direction (default: ASC)
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Self for method chaining
|
|
173
|
+
|
|
174
|
+
Examples:
|
|
175
|
+
>>> query = StrapiQuery().sort_by("publishedAt", SortDirection.DESC)
|
|
176
|
+
"""
|
|
177
|
+
if self._sort is None:
|
|
178
|
+
self._sort = Sort()
|
|
179
|
+
self._sort.by_field(field, direction)
|
|
180
|
+
return self
|
|
181
|
+
|
|
182
|
+
def then_sort_by(self, field: str, direction: SortDirection = SortDirection.ASC) -> StrapiQuery:
|
|
183
|
+
"""Add secondary sort field (alias for sort_by for readability).
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
field: Field name to sort by
|
|
187
|
+
direction: Sort direction (default: ASC)
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Self for method chaining
|
|
191
|
+
|
|
192
|
+
Examples:
|
|
193
|
+
>>> query = (StrapiQuery()
|
|
194
|
+
... .sort_by("status")
|
|
195
|
+
... .then_sort_by("publishedAt", SortDirection.DESC))
|
|
196
|
+
"""
|
|
197
|
+
return self.sort_by(field, direction)
|
|
198
|
+
|
|
199
|
+
def paginate(
|
|
200
|
+
self,
|
|
201
|
+
page: int | None = None,
|
|
202
|
+
page_size: int | None = None,
|
|
203
|
+
start: int | None = None,
|
|
204
|
+
limit: int | None = None,
|
|
205
|
+
with_count: bool = True,
|
|
206
|
+
) -> StrapiQuery:
|
|
207
|
+
"""Add pagination to the query.
|
|
208
|
+
|
|
209
|
+
Use either page-based (page + page_size) OR offset-based (start + limit).
|
|
210
|
+
Cannot mix both strategies.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
page: Page number (1-indexed) for page-based pagination
|
|
214
|
+
page_size: Items per page for page-based pagination
|
|
215
|
+
start: Start offset (0-indexed) for offset-based pagination
|
|
216
|
+
limit: Maximum items for offset-based pagination
|
|
217
|
+
with_count: Include total count in response (default: True)
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
Self for method chaining
|
|
221
|
+
|
|
222
|
+
Raises:
|
|
223
|
+
ValueError: If mixing page-based and offset-based parameters
|
|
224
|
+
|
|
225
|
+
Examples:
|
|
226
|
+
>>> # Page-based
|
|
227
|
+
>>> query = StrapiQuery().paginate(page=1, page_size=25)
|
|
228
|
+
|
|
229
|
+
>>> # Offset-based
|
|
230
|
+
>>> query = StrapiQuery().paginate(start=0, limit=25)
|
|
231
|
+
"""
|
|
232
|
+
# Detect pagination strategy
|
|
233
|
+
page_based = page is not None or page_size is not None
|
|
234
|
+
offset_based = start is not None or limit is not None
|
|
235
|
+
|
|
236
|
+
if page_based and offset_based:
|
|
237
|
+
raise ValueError("Cannot mix page-based and offset-based pagination")
|
|
238
|
+
|
|
239
|
+
if page_based:
|
|
240
|
+
# Validate page value if explicitly provided
|
|
241
|
+
if page is not None and page < 1:
|
|
242
|
+
raise ValueError("page must be >= 1")
|
|
243
|
+
# Validate page_size value if explicitly provided
|
|
244
|
+
if page_size is not None and page_size < 1:
|
|
245
|
+
raise ValueError("page_size must be >= 1")
|
|
246
|
+
self._pagination = PagePagination(
|
|
247
|
+
page=1 if page is None else page,
|
|
248
|
+
page_size=25 if page_size is None else page_size,
|
|
249
|
+
with_count=with_count,
|
|
250
|
+
)
|
|
251
|
+
elif offset_based:
|
|
252
|
+
# Validate start value if explicitly provided
|
|
253
|
+
if start is not None and start < 0:
|
|
254
|
+
raise ValueError("start must be >= 0")
|
|
255
|
+
# Validate limit value if explicitly provided
|
|
256
|
+
if limit is not None and limit < 1:
|
|
257
|
+
raise ValueError("limit must be >= 1")
|
|
258
|
+
self._pagination = OffsetPagination(
|
|
259
|
+
start=0 if start is None else start,
|
|
260
|
+
limit=25 if limit is None else limit,
|
|
261
|
+
with_count=with_count,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
return self
|
|
265
|
+
|
|
266
|
+
def populate(self, populate: Populate) -> StrapiQuery:
|
|
267
|
+
"""Add population configuration to the query.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
populate: Populate configuration
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
Self for method chaining
|
|
274
|
+
|
|
275
|
+
Examples:
|
|
276
|
+
>>> query = StrapiQuery().populate(
|
|
277
|
+
... Populate()
|
|
278
|
+
... .add_field("author", fields=["name", "email"])
|
|
279
|
+
... .add_field("category")
|
|
280
|
+
... )
|
|
281
|
+
"""
|
|
282
|
+
self._populate = populate
|
|
283
|
+
return self
|
|
284
|
+
|
|
285
|
+
def populate_all(self) -> StrapiQuery:
|
|
286
|
+
"""Populate all first-level relations.
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
Self for method chaining
|
|
290
|
+
|
|
291
|
+
Examples:
|
|
292
|
+
>>> query = StrapiQuery().populate_all()
|
|
293
|
+
"""
|
|
294
|
+
self._populate = Populate().all()
|
|
295
|
+
return self
|
|
296
|
+
|
|
297
|
+
def populate_fields(self, fields: list[str]) -> StrapiQuery:
|
|
298
|
+
"""Populate specific fields (simple list).
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
fields: List of field names to populate
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
Self for method chaining
|
|
305
|
+
|
|
306
|
+
Examples:
|
|
307
|
+
>>> query = StrapiQuery().populate_fields(["author", "category", "tags"])
|
|
308
|
+
"""
|
|
309
|
+
self._populate = Populate().fields_list(fields)
|
|
310
|
+
return self
|
|
311
|
+
|
|
312
|
+
def select(self, fields: list[str]) -> StrapiQuery:
|
|
313
|
+
"""Select specific fields to return.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
fields: List of field names to include in response
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
Self for method chaining
|
|
320
|
+
|
|
321
|
+
Examples:
|
|
322
|
+
>>> query = StrapiQuery().select(["title", "description", "publishedAt"])
|
|
323
|
+
"""
|
|
324
|
+
self._fields = FieldSelection(fields=fields)
|
|
325
|
+
return self
|
|
326
|
+
|
|
327
|
+
def with_locale(self, locale: str) -> StrapiQuery:
|
|
328
|
+
"""Set locale for i18n content.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
locale: Locale code (e.g., "en", "fr", "de")
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
Self for method chaining
|
|
335
|
+
|
|
336
|
+
Examples:
|
|
337
|
+
>>> query = StrapiQuery().with_locale("fr")
|
|
338
|
+
"""
|
|
339
|
+
self._locale = locale
|
|
340
|
+
return self
|
|
341
|
+
|
|
342
|
+
def with_publication_state(self, state: PublicationState) -> StrapiQuery:
|
|
343
|
+
"""Set publication state filter (draft & publish).
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
state: Publication state (LIVE or PREVIEW)
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
Self for method chaining
|
|
350
|
+
|
|
351
|
+
Examples:
|
|
352
|
+
>>> query = StrapiQuery().with_publication_state(PublicationState.LIVE)
|
|
353
|
+
"""
|
|
354
|
+
self._publication_state = state
|
|
355
|
+
return self
|
|
356
|
+
|
|
357
|
+
def to_query_params(self) -> dict[str, Any]:
|
|
358
|
+
"""Convert query to flat dictionary for HTTP query parameters.
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
Dictionary of query parameters ready for httpx
|
|
362
|
+
|
|
363
|
+
Examples:
|
|
364
|
+
>>> query = (StrapiQuery()
|
|
365
|
+
... .filter(FilterBuilder().eq("status", "published"))
|
|
366
|
+
... .sort_by("publishedAt", SortDirection.DESC)
|
|
367
|
+
... .paginate(page=1, page_size=10))
|
|
368
|
+
>>> params = query.to_query_params()
|
|
369
|
+
>>> # Use with httpx: client.get(url, params=params)
|
|
370
|
+
"""
|
|
371
|
+
params: dict[str, Any] = {}
|
|
372
|
+
|
|
373
|
+
# Add filters (flattened to bracket notation for Strapi)
|
|
374
|
+
if self._filters:
|
|
375
|
+
filter_dict = self._filters.to_query_dict()
|
|
376
|
+
if filter_dict:
|
|
377
|
+
# Flatten nested filter dict into bracket notation
|
|
378
|
+
# e.g., {"status": {"$eq": "published"}} -> {"filters[status][$eq]": "published"}
|
|
379
|
+
flattened = _flatten_dict(filter_dict, "filters")
|
|
380
|
+
params.update(flattened)
|
|
381
|
+
|
|
382
|
+
# Add sort
|
|
383
|
+
if self._sort:
|
|
384
|
+
sort_list = self._sort.to_query_list()
|
|
385
|
+
if sort_list:
|
|
386
|
+
params["sort"] = sort_list
|
|
387
|
+
|
|
388
|
+
# Add pagination
|
|
389
|
+
if self._pagination:
|
|
390
|
+
pagination_dict = self._pagination.to_query_dict()
|
|
391
|
+
params.update(pagination_dict)
|
|
392
|
+
|
|
393
|
+
# Add populate
|
|
394
|
+
if self._populate:
|
|
395
|
+
populate_dict = self._populate.to_query_dict()
|
|
396
|
+
if populate_dict:
|
|
397
|
+
params.update(populate_dict)
|
|
398
|
+
|
|
399
|
+
# Add fields
|
|
400
|
+
if self._fields:
|
|
401
|
+
fields_dict = self._fields.to_query_dict()
|
|
402
|
+
if fields_dict:
|
|
403
|
+
params.update(fields_dict)
|
|
404
|
+
|
|
405
|
+
# Add locale
|
|
406
|
+
if self._locale:
|
|
407
|
+
params["locale"] = self._locale
|
|
408
|
+
|
|
409
|
+
# Add publication state
|
|
410
|
+
if self._publication_state:
|
|
411
|
+
params["publicationState"] = self._publication_state.value
|
|
412
|
+
|
|
413
|
+
return params
|
|
414
|
+
|
|
415
|
+
def to_query_dict(self) -> dict[str, Any]:
|
|
416
|
+
"""Convert query to flattened dictionary for HTTP query parameters.
|
|
417
|
+
|
|
418
|
+
This is an alias for to_query_params() for consistency with other models.
|
|
419
|
+
Returns bracket-notation flattened params ready for httpx.
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
Dictionary of query parameters (flattened bracket notation)
|
|
423
|
+
|
|
424
|
+
Examples:
|
|
425
|
+
>>> query = StrapiQuery().filter(FilterBuilder().eq("status", "published"))
|
|
426
|
+
>>> query.to_query_dict()
|
|
427
|
+
{'filters[status][$eq]': 'published'}
|
|
428
|
+
"""
|
|
429
|
+
return self.to_query_params()
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Sort configuration for Strapi API queries.
|
|
2
|
+
|
|
3
|
+
Provides models and builders for sorting query results by one or more fields.
|
|
4
|
+
|
|
5
|
+
Examples:
|
|
6
|
+
Single field sort:
|
|
7
|
+
>>> sort = Sort().by_field("publishedAt", SortDirection.DESC)
|
|
8
|
+
>>> sort.to_query_list()
|
|
9
|
+
['publishedAt:desc']
|
|
10
|
+
|
|
11
|
+
Multi-field sort:
|
|
12
|
+
>>> sort = (Sort()
|
|
13
|
+
... .by_field("status", SortDirection.ASC)
|
|
14
|
+
... .then_by("publishedAt", SortDirection.DESC))
|
|
15
|
+
>>> sort.to_query_list()
|
|
16
|
+
['status:asc', 'publishedAt:desc']
|
|
17
|
+
|
|
18
|
+
Sort by nested relation:
|
|
19
|
+
>>> sort = Sort().by_field("author.name", SortDirection.ASC)
|
|
20
|
+
>>> sort.to_query_list()
|
|
21
|
+
['author.name:asc']
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
from pydantic import BaseModel, Field
|
|
27
|
+
|
|
28
|
+
from strapi_kit.models.enums import SortDirection
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SortField(BaseModel):
|
|
32
|
+
"""A single sort field with direction.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
field: Field name (supports dot notation for relations, e.g., "author.name")
|
|
36
|
+
direction: Sort direction (ASC or DESC)
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
field: str = Field(..., min_length=1, description="Field name to sort by")
|
|
40
|
+
direction: SortDirection = Field(
|
|
41
|
+
default=SortDirection.ASC, description="Sort direction (asc or desc)"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def to_string(self) -> str:
|
|
45
|
+
"""Convert to Strapi query string format.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
String in format "field:direction" (e.g., "publishedAt:desc")
|
|
49
|
+
|
|
50
|
+
Examples:
|
|
51
|
+
>>> SortField(field="publishedAt", direction=SortDirection.DESC).to_string()
|
|
52
|
+
'publishedAt:desc'
|
|
53
|
+
"""
|
|
54
|
+
return f"{self.field}:{self.direction.value}"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Sort:
|
|
58
|
+
"""Fluent API for building multi-field sort configurations.
|
|
59
|
+
|
|
60
|
+
Supports sorting by multiple fields with different directions.
|
|
61
|
+
Fields are applied in the order they are added.
|
|
62
|
+
|
|
63
|
+
Examples:
|
|
64
|
+
>>> # Single field
|
|
65
|
+
>>> sort = Sort().by_field("publishedAt", SortDirection.DESC)
|
|
66
|
+
|
|
67
|
+
>>> # Multiple fields
|
|
68
|
+
>>> sort = (Sort()
|
|
69
|
+
... .by_field("status", SortDirection.ASC)
|
|
70
|
+
... .then_by("publishedAt", SortDirection.DESC)
|
|
71
|
+
... .then_by("title", SortDirection.ASC))
|
|
72
|
+
|
|
73
|
+
>>> # Shorthand with default ASC
|
|
74
|
+
>>> sort = Sort().by_field("title") # Defaults to ASC
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def __init__(self) -> None:
|
|
78
|
+
"""Initialize an empty sort builder."""
|
|
79
|
+
self._fields: list[SortField] = []
|
|
80
|
+
|
|
81
|
+
def by_field(self, field: str, direction: SortDirection = SortDirection.ASC) -> "Sort":
|
|
82
|
+
"""Add a sort field (first field or when starting a new sort).
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
field: Field name to sort by
|
|
86
|
+
direction: Sort direction (default: ASC)
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Self for method chaining
|
|
90
|
+
|
|
91
|
+
Examples:
|
|
92
|
+
>>> Sort().by_field("publishedAt", SortDirection.DESC)
|
|
93
|
+
>>> Sort().by_field("title") # Defaults to ASC
|
|
94
|
+
"""
|
|
95
|
+
self._fields.append(SortField(field=field, direction=direction))
|
|
96
|
+
return self
|
|
97
|
+
|
|
98
|
+
def then_by(self, field: str, direction: SortDirection = SortDirection.ASC) -> "Sort":
|
|
99
|
+
"""Add a secondary sort field (alias for by_field for readability).
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
field: Field name to sort by
|
|
103
|
+
direction: Sort direction (default: ASC)
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Self for method chaining
|
|
107
|
+
|
|
108
|
+
Examples:
|
|
109
|
+
>>> (Sort()
|
|
110
|
+
... .by_field("status")
|
|
111
|
+
... .then_by("publishedAt", SortDirection.DESC))
|
|
112
|
+
"""
|
|
113
|
+
return self.by_field(field, direction)
|
|
114
|
+
|
|
115
|
+
def to_query_list(self) -> list[str]:
|
|
116
|
+
"""Convert sort configuration to list of query strings.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
List of strings in format "field:direction"
|
|
120
|
+
|
|
121
|
+
Examples:
|
|
122
|
+
>>> sort = Sort().by_field("publishedAt", SortDirection.DESC)
|
|
123
|
+
>>> sort.to_query_list()
|
|
124
|
+
['publishedAt:desc']
|
|
125
|
+
|
|
126
|
+
>>> sort = (Sort()
|
|
127
|
+
... .by_field("status")
|
|
128
|
+
... .then_by("publishedAt", SortDirection.DESC))
|
|
129
|
+
>>> sort.to_query_list()
|
|
130
|
+
['status:asc', 'publishedAt:desc']
|
|
131
|
+
"""
|
|
132
|
+
return [field.to_string() for field in self._fields]
|
|
133
|
+
|
|
134
|
+
def to_query_dict(self) -> dict[str, Any]:
|
|
135
|
+
"""Convert sort configuration to dictionary format for query parameters.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Dictionary with "sort" key containing list of sort strings
|
|
139
|
+
|
|
140
|
+
Examples:
|
|
141
|
+
>>> sort = Sort().by_field("publishedAt", SortDirection.DESC)
|
|
142
|
+
>>> sort.to_query_dict()
|
|
143
|
+
{'sort': ['publishedAt:desc']}
|
|
144
|
+
"""
|
|
145
|
+
if not self._fields:
|
|
146
|
+
return {}
|
|
147
|
+
return {"sort": self.to_query_list()}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Response models for parsing Strapi API responses."""
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Base response models for Strapi API.
|
|
2
|
+
|
|
3
|
+
Provides generic response containers for single and collection responses.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Generic, TypeVar
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
11
|
+
|
|
12
|
+
from strapi_kit.models.response.meta import ResponseMeta
|
|
13
|
+
|
|
14
|
+
# Generic type for entity data
|
|
15
|
+
T = TypeVar("T")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BaseStrapiResponse(BaseModel):
|
|
19
|
+
"""Base class for all Strapi responses.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
meta: Response metadata (pagination, locales, etc.)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
model_config = ConfigDict(extra="allow")
|
|
26
|
+
|
|
27
|
+
meta: ResponseMeta | None = Field(None, description="Response metadata")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class StrapiSingleResponse(BaseStrapiResponse, Generic[T]):
|
|
31
|
+
"""Response for a single entity.
|
|
32
|
+
|
|
33
|
+
Generic response container for GET /api/articles/1 endpoints.
|
|
34
|
+
|
|
35
|
+
Attributes:
|
|
36
|
+
data: The entity data (or None if not found)
|
|
37
|
+
meta: Response metadata
|
|
38
|
+
|
|
39
|
+
Examples:
|
|
40
|
+
>>> from pydantic import BaseModel
|
|
41
|
+
>>> class Article(BaseModel):
|
|
42
|
+
... id: int
|
|
43
|
+
... title: str
|
|
44
|
+
>>> response = StrapiSingleResponse[Article](
|
|
45
|
+
... data=Article(id=1, title="Test")
|
|
46
|
+
... )
|
|
47
|
+
>>> response.data.title
|
|
48
|
+
'Test'
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
data: T | None = Field(None, description="Single entity data")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class StrapiCollectionResponse(BaseStrapiResponse, Generic[T]):
|
|
55
|
+
"""Response for a collection of entities.
|
|
56
|
+
|
|
57
|
+
Generic response container for GET /api/articles endpoints.
|
|
58
|
+
|
|
59
|
+
Attributes:
|
|
60
|
+
data: List of entities
|
|
61
|
+
meta: Response metadata (includes pagination)
|
|
62
|
+
|
|
63
|
+
Examples:
|
|
64
|
+
>>> from pydantic import BaseModel
|
|
65
|
+
>>> class Article(BaseModel):
|
|
66
|
+
... id: int
|
|
67
|
+
... title: str
|
|
68
|
+
>>> response = StrapiCollectionResponse[Article](
|
|
69
|
+
... data=[Article(id=1, title="Test")]
|
|
70
|
+
... )
|
|
71
|
+
>>> len(response.data)
|
|
72
|
+
1
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
data: list[T] = Field(default_factory=list, description="Collection of entities")
|