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.
Files changed (55) hide show
  1. strapi_kit/__init__.py +97 -0
  2. strapi_kit/__version__.py +15 -0
  3. strapi_kit/_version.py +34 -0
  4. strapi_kit/auth/__init__.py +7 -0
  5. strapi_kit/auth/api_token.py +48 -0
  6. strapi_kit/cache/__init__.py +5 -0
  7. strapi_kit/cache/schema_cache.py +211 -0
  8. strapi_kit/client/__init__.py +11 -0
  9. strapi_kit/client/async_client.py +1032 -0
  10. strapi_kit/client/base.py +460 -0
  11. strapi_kit/client/sync_client.py +980 -0
  12. strapi_kit/config_provider.py +368 -0
  13. strapi_kit/exceptions/__init__.py +37 -0
  14. strapi_kit/exceptions/errors.py +205 -0
  15. strapi_kit/export/__init__.py +10 -0
  16. strapi_kit/export/exporter.py +384 -0
  17. strapi_kit/export/importer.py +619 -0
  18. strapi_kit/export/media_handler.py +322 -0
  19. strapi_kit/export/relation_resolver.py +172 -0
  20. strapi_kit/models/__init__.py +104 -0
  21. strapi_kit/models/bulk.py +69 -0
  22. strapi_kit/models/config.py +174 -0
  23. strapi_kit/models/enums.py +97 -0
  24. strapi_kit/models/export_format.py +166 -0
  25. strapi_kit/models/import_options.py +142 -0
  26. strapi_kit/models/request/__init__.py +1 -0
  27. strapi_kit/models/request/fields.py +65 -0
  28. strapi_kit/models/request/filters.py +611 -0
  29. strapi_kit/models/request/pagination.py +168 -0
  30. strapi_kit/models/request/populate.py +281 -0
  31. strapi_kit/models/request/query.py +429 -0
  32. strapi_kit/models/request/sort.py +147 -0
  33. strapi_kit/models/response/__init__.py +1 -0
  34. strapi_kit/models/response/base.py +75 -0
  35. strapi_kit/models/response/component.py +67 -0
  36. strapi_kit/models/response/media.py +91 -0
  37. strapi_kit/models/response/meta.py +44 -0
  38. strapi_kit/models/response/normalized.py +168 -0
  39. strapi_kit/models/response/relation.py +48 -0
  40. strapi_kit/models/response/v4.py +70 -0
  41. strapi_kit/models/response/v5.py +57 -0
  42. strapi_kit/models/schema.py +93 -0
  43. strapi_kit/operations/__init__.py +16 -0
  44. strapi_kit/operations/media.py +226 -0
  45. strapi_kit/operations/streaming.py +144 -0
  46. strapi_kit/parsers/__init__.py +5 -0
  47. strapi_kit/parsers/version_detecting.py +171 -0
  48. strapi_kit/protocols.py +455 -0
  49. strapi_kit/utils/__init__.py +15 -0
  50. strapi_kit/utils/rate_limiter.py +201 -0
  51. strapi_kit/utils/uid.py +88 -0
  52. strapi_kit-0.0.1.dist-info/METADATA +1098 -0
  53. strapi_kit-0.0.1.dist-info/RECORD +55 -0
  54. strapi_kit-0.0.1.dist-info/WHEEL +4 -0
  55. 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")