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,168 @@
1
+ """Pagination configuration for Strapi API queries.
2
+
3
+ Strapi supports two pagination strategies:
4
+ 1. Page-based: Use page number and page size
5
+ 2. Offset-based: Use start offset and limit
6
+
7
+ IMPORTANT: Cannot mix pagination strategies in the same query.
8
+
9
+ Examples:
10
+ Page-based pagination:
11
+ >>> pagination = PagePagination(page=1, page_size=25)
12
+ >>> pagination.to_query_dict()
13
+ {'pagination[page]': 1, 'pagination[pageSize]': 25, 'pagination[withCount]': True}
14
+
15
+ Offset-based pagination:
16
+ >>> pagination = OffsetPagination(start=0, limit=25)
17
+ >>> pagination.to_query_dict()
18
+ {'pagination[start]': 0, 'pagination[limit]': 25, 'pagination[withCount]': True}
19
+
20
+ Disable count (performance optimization):
21
+ >>> pagination = PagePagination(page=1, page_size=100, with_count=False)
22
+ """
23
+
24
+ from typing import Any
25
+
26
+ from pydantic import BaseModel, Field, field_validator
27
+
28
+
29
+ class PagePagination(BaseModel):
30
+ """Page-based pagination configuration.
31
+
32
+ Uses page number and page size for pagination. This is the most
33
+ user-friendly approach for displaying results across multiple pages.
34
+
35
+ Attributes:
36
+ page: Page number (1-indexed, must be >= 1)
37
+ page_size: Number of items per page (must be between 1 and 100)
38
+ with_count: Include total count in response (default: True)
39
+ """
40
+
41
+ page: int = Field(default=1, ge=1, description="Page number (1-indexed)")
42
+ page_size: int = Field(default=25, ge=1, le=100, description="Items per page")
43
+ with_count: bool = Field(default=True, description="Include total count in response metadata")
44
+
45
+ @field_validator("page")
46
+ @classmethod
47
+ def validate_page(cls, v: int) -> int:
48
+ """Validate page number is positive.
49
+
50
+ Args:
51
+ v: Page number to validate
52
+
53
+ Returns:
54
+ Validated page number
55
+
56
+ Raises:
57
+ ValueError: If page number is less than 1
58
+ """
59
+ if v < 1:
60
+ raise ValueError("Page number must be >= 1")
61
+ return v
62
+
63
+ @field_validator("page_size")
64
+ @classmethod
65
+ def validate_page_size(cls, v: int) -> int:
66
+ """Validate page size is within allowed range.
67
+
68
+ Args:
69
+ v: Page size to validate
70
+
71
+ Returns:
72
+ Validated page size
73
+
74
+ Raises:
75
+ ValueError: If page size is out of range [1, 100]
76
+ """
77
+ if v < 1 or v > 100:
78
+ raise ValueError("Page size must be between 1 and 100")
79
+ return v
80
+
81
+ def to_query_dict(self) -> dict[str, Any]:
82
+ """Convert to query parameters dictionary.
83
+
84
+ Returns:
85
+ Dictionary with pagination parameters in Strapi format
86
+
87
+ Examples:
88
+ >>> PagePagination(page=2, page_size=50).to_query_dict()
89
+ {'pagination[page]': 2, 'pagination[pageSize]': 50, 'pagination[withCount]': True}
90
+ """
91
+ return {
92
+ "pagination[page]": self.page,
93
+ "pagination[pageSize]": self.page_size,
94
+ "pagination[withCount]": self.with_count,
95
+ }
96
+
97
+
98
+ class OffsetPagination(BaseModel):
99
+ """Offset-based pagination configuration.
100
+
101
+ Uses start offset and limit for pagination. This approach is more
102
+ flexible but requires manual calculation of offsets.
103
+
104
+ Attributes:
105
+ start: Starting offset (0-indexed, must be >= 0)
106
+ limit: Maximum number of items to return (must be between 1 and 100)
107
+ with_count: Include total count in response (default: True)
108
+ """
109
+
110
+ start: int = Field(default=0, ge=0, description="Starting offset (0-indexed)")
111
+ limit: int = Field(default=25, ge=1, le=100, description="Maximum items to return")
112
+ with_count: bool = Field(default=True, description="Include total count in response metadata")
113
+
114
+ @field_validator("start")
115
+ @classmethod
116
+ def validate_start(cls, v: int) -> int:
117
+ """Validate start offset is non-negative.
118
+
119
+ Args:
120
+ v: Start offset to validate
121
+
122
+ Returns:
123
+ Validated start offset
124
+
125
+ Raises:
126
+ ValueError: If start is negative
127
+ """
128
+ if v < 0:
129
+ raise ValueError("Start offset must be >= 0")
130
+ return v
131
+
132
+ @field_validator("limit")
133
+ @classmethod
134
+ def validate_limit(cls, v: int) -> int:
135
+ """Validate limit is within allowed range.
136
+
137
+ Args:
138
+ v: Limit to validate
139
+
140
+ Returns:
141
+ Validated limit
142
+
143
+ Raises:
144
+ ValueError: If limit is out of range [1, 100]
145
+ """
146
+ if v < 1 or v > 100:
147
+ raise ValueError("Limit must be between 1 and 100")
148
+ return v
149
+
150
+ def to_query_dict(self) -> dict[str, Any]:
151
+ """Convert to query parameters dictionary.
152
+
153
+ Returns:
154
+ Dictionary with pagination parameters in Strapi format
155
+
156
+ Examples:
157
+ >>> OffsetPagination(start=50, limit=25).to_query_dict()
158
+ {'pagination[start]': 50, 'pagination[limit]': 25, 'pagination[withCount]': True}
159
+ """
160
+ return {
161
+ "pagination[start]": self.start,
162
+ "pagination[limit]": self.limit,
163
+ "pagination[withCount]": self.with_count,
164
+ }
165
+
166
+
167
+ # Type alias for either pagination strategy
168
+ Pagination = PagePagination | OffsetPagination
@@ -0,0 +1,281 @@
1
+ """Population (relation expansion) for Strapi API queries.
2
+
3
+ Strapi supports populating (expanding) relations, components, and dynamic zones.
4
+ This module provides a fluent API for building complex population configurations.
5
+
6
+ Examples:
7
+ Simple population:
8
+ >>> populate = Populate().fields_list(["author", "category"])
9
+ >>> populate.to_query_dict()
10
+ {'populate': ['author', 'category']}
11
+
12
+ Populate all relations:
13
+ >>> populate = Populate().all()
14
+ >>> populate.to_query_dict()
15
+ {'populate': '*'}
16
+
17
+ Nested population with filtering:
18
+ >>> from strapi_kit.models.request.filters import FilterBuilder
19
+ >>> populate = Populate().add_field(
20
+ ... "author",
21
+ ... fields=["name", "email"],
22
+ ... filters=FilterBuilder().eq("active", True)
23
+ ... )
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ from typing import Any
29
+
30
+ from pydantic import BaseModel, ConfigDict, Field
31
+
32
+ from strapi_kit.models.request.filters import FilterBuilder
33
+ from strapi_kit.models.request.sort import Sort
34
+
35
+
36
+ class PopulateField(BaseModel):
37
+ """Configuration for populating a single field.
38
+
39
+ Attributes:
40
+ field: Field name to populate
41
+ nested: Nested population configuration
42
+ filters: Filter conditions for the populated data
43
+ fields: Specific fields to select from the relation
44
+ sort: Sort configuration for the populated data
45
+ """
46
+
47
+ model_config = ConfigDict(arbitrary_types_allowed=True)
48
+
49
+ field: str = Field(..., min_length=1, description="Field name to populate")
50
+ nested: Populate | None = Field(None, description="Nested population")
51
+ filters: FilterBuilder | None = Field(None, description="Filter populated data")
52
+ fields: list[str] | None = Field(None, description="Fields to select")
53
+ sort: Sort | None = Field(None, description="Sort configuration")
54
+
55
+ def to_dict(self, _depth: int = 0, _max_depth: int = 10) -> dict[str, Any]:
56
+ """Convert to dictionary format for query parameters.
57
+
58
+ Args:
59
+ _depth: Current recursion depth (internal use)
60
+ _max_depth: Maximum allowed recursion depth (default: 10)
61
+
62
+ Returns:
63
+ Dictionary with field name as key and configuration as value
64
+
65
+ Raises:
66
+ RecursionError: If nesting exceeds max_depth
67
+
68
+ Examples:
69
+ >>> # Simple populate
70
+ >>> pf = PopulateField(field="author")
71
+ >>> pf.to_dict()
72
+ {'author': {'populate': '*'}}
73
+
74
+ >>> # With field selection
75
+ >>> pf = PopulateField(field="author", fields=["name", "email"])
76
+ >>> pf.to_dict()
77
+ {'author': {'fields': ['name', 'email'], 'populate': '*'}}
78
+ """
79
+ if _depth > _max_depth:
80
+ raise RecursionError(
81
+ f"Populate nesting exceeded maximum depth of {_max_depth}. "
82
+ "This may indicate circular references in your populate configuration."
83
+ )
84
+
85
+ config: dict[str, Any] = {}
86
+
87
+ # Add field selection
88
+ if self.fields:
89
+ config["fields"] = self.fields
90
+
91
+ # Add filters
92
+ if self.filters:
93
+ filter_dict = self.filters.to_query_dict()
94
+ if filter_dict:
95
+ config["filters"] = filter_dict
96
+
97
+ # Add sort
98
+ if self.sort:
99
+ sort_list = self.sort.to_query_list()
100
+ if sort_list:
101
+ config["sort"] = sort_list
102
+
103
+ # Add nested population
104
+ if self.nested:
105
+ nested_dict = self.nested.to_query_dict(_depth=_depth + 1, _max_depth=_max_depth)
106
+ if "populate" in nested_dict:
107
+ config["populate"] = nested_dict["populate"]
108
+ else:
109
+ # Default: populate all fields of this relation
110
+ config["populate"] = "*"
111
+
112
+ return {self.field: config}
113
+
114
+
115
+ class Populate:
116
+ """Fluent API for building population configurations.
117
+
118
+ Strapi population allows expanding relations, components, and dynamic zones.
119
+ This class provides methods to configure simple and complex population scenarios.
120
+
121
+ Examples:
122
+ >>> # Populate all relations
123
+ >>> populate = Populate().all()
124
+
125
+ >>> # Populate specific fields
126
+ >>> populate = Populate().fields_list(["author", "category"])
127
+
128
+ >>> # Populate with nested relations
129
+ >>> populate = (Populate()
130
+ ... .add_field("author")
131
+ ... .add_field("posts", nested=Populate().add_field("comments")))
132
+
133
+ >>> # Populate with filtering
134
+ >>> populate = Populate().add_field(
135
+ ... "author",
136
+ ... filters=FilterBuilder().eq("active", True),
137
+ ... fields=["name", "email"]
138
+ ... )
139
+ """
140
+
141
+ def __init__(self) -> None:
142
+ """Initialize an empty populate configuration."""
143
+ self._populate_all: bool = False
144
+ self._fields: list[PopulateField] = []
145
+
146
+ def all(self) -> Populate:
147
+ """Populate all first-level relations.
148
+
149
+ Returns:
150
+ Self for method chaining
151
+
152
+ Examples:
153
+ >>> populate = Populate().all()
154
+ >>> populate.to_query_dict()
155
+ {'populate': '*'}
156
+ """
157
+ self._populate_all = True
158
+ return self
159
+
160
+ def fields_list(self, fields: list[str]) -> Populate:
161
+ """Populate specific fields (simple list).
162
+
163
+ Args:
164
+ fields: List of field names to populate
165
+
166
+ Returns:
167
+ Self for method chaining
168
+
169
+ Examples:
170
+ >>> populate = Populate().fields_list(["author", "category", "tags"])
171
+ >>> populate.to_query_dict()
172
+ {'populate': ['author', 'category', 'tags']}
173
+ """
174
+ # Convert simple field names to PopulateField objects
175
+ for field_name in fields:
176
+ self._fields.append(
177
+ PopulateField(field=field_name, nested=None, filters=None, fields=None, sort=None)
178
+ )
179
+ return self
180
+
181
+ def add_field(
182
+ self,
183
+ field: str,
184
+ nested: Populate | None = None,
185
+ filters: FilterBuilder | None = None,
186
+ fields: list[str] | None = None,
187
+ sort: Sort | None = None,
188
+ ) -> Populate:
189
+ """Add a field to populate with advanced configuration.
190
+
191
+ Args:
192
+ field: Field name to populate
193
+ nested: Nested population configuration
194
+ filters: Filter conditions for the populated data
195
+ fields: Specific fields to select from the relation
196
+ sort: Sort configuration for the populated data
197
+
198
+ Returns:
199
+ Self for method chaining
200
+
201
+ Examples:
202
+ >>> # Simple field
203
+ >>> populate = Populate().add_field("author")
204
+
205
+ >>> # With field selection
206
+ >>> populate = Populate().add_field("author", fields=["name", "email"])
207
+
208
+ >>> # With filtering
209
+ >>> populate = Populate().add_field(
210
+ ... "comments",
211
+ ... filters=FilterBuilder().eq("approved", True),
212
+ ... sort=Sort().by_field("createdAt", SortDirection.DESC)
213
+ ... )
214
+
215
+ >>> # Nested population
216
+ >>> populate = Populate().add_field(
217
+ ... "author",
218
+ ... nested=Populate().add_field("profile")
219
+ ... )
220
+ """
221
+ self._fields.append(
222
+ PopulateField(field=field, nested=nested, filters=filters, fields=fields, sort=sort)
223
+ )
224
+ return self
225
+
226
+ def to_query_dict(self, _depth: int = 0, _max_depth: int = 10) -> dict[str, Any]:
227
+ """Convert to dictionary format for query parameters.
228
+
229
+ Args:
230
+ _depth: Current recursion depth (internal use)
231
+ _max_depth: Maximum allowed recursion depth (default: 10)
232
+
233
+ Returns:
234
+ Dictionary with 'populate' key
235
+
236
+ Raises:
237
+ RecursionError: If nesting exceeds max_depth
238
+
239
+ Examples:
240
+ >>> # Populate all
241
+ >>> Populate().all().to_query_dict()
242
+ {'populate': '*'}
243
+
244
+ >>> # Simple list
245
+ >>> Populate().fields_list(["author", "category"]).to_query_dict()
246
+ {'populate': ['author', 'category']}
247
+
248
+ >>> # Complex with configuration
249
+ >>> populate = Populate().add_field("author", fields=["name"])
250
+ >>> # Returns nested structure
251
+ """
252
+ # Check depth limit at Populate level
253
+ if _depth > _max_depth:
254
+ raise RecursionError(
255
+ f"Populate nesting exceeded maximum depth of {_max_depth}. "
256
+ "This may indicate circular references in your populate configuration."
257
+ )
258
+
259
+ if not self._populate_all and not self._fields:
260
+ return {}
261
+
262
+ # Populate all
263
+ if self._populate_all:
264
+ return {"populate": "*"}
265
+
266
+ # Check if all fields are simple (no config)
267
+ all_simple = all(
268
+ not f.nested and not f.filters and not f.fields and not f.sort for f in self._fields
269
+ )
270
+
271
+ if all_simple:
272
+ # Simple array format
273
+ return {"populate": [f.field for f in self._fields]}
274
+
275
+ # Complex object format
276
+ result: dict[str, Any] = {}
277
+ for field_config in self._fields:
278
+ field_dict = field_config.to_dict(_depth=_depth, _max_depth=_max_depth)
279
+ result.update(field_dict)
280
+
281
+ return {"populate": result}