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,1098 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: strapi-kit
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A modern Python client for Strapi CMS with import/export capabilities
|
|
5
|
+
Project-URL: Homepage, https://github.com/mehdizare/strapi-kit
|
|
6
|
+
Project-URL: Documentation, https://mehdizare.github.io/strapi-kit/
|
|
7
|
+
Project-URL: Repository, https://github.com/mehdizare/strapi-kit
|
|
8
|
+
Project-URL: Issues, https://github.com/mehdizare/strapi-kit/issues
|
|
9
|
+
Author: Mehdi Zare
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: api,client,cms,export,import,strapi
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.12
|
|
21
|
+
Requires-Dist: httpx>=0.28.1
|
|
22
|
+
Requires-Dist: orjson>=3.11.0
|
|
23
|
+
Requires-Dist: pydantic-settings<3.0,>=2.7.0
|
|
24
|
+
Requires-Dist: pydantic<3.0,>=2.10.0
|
|
25
|
+
Requires-Dist: python-dateutil>=2.9.0
|
|
26
|
+
Requires-Dist: tenacity>=9.0.0
|
|
27
|
+
Requires-Dist: typing-extensions>=4.15.0
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: bandit[toml]>=1.9.3; extra == 'dev'
|
|
30
|
+
Requires-Dist: detect-secrets>=1.5.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: mypy>=1.19.1; extra == 'dev'
|
|
32
|
+
Requires-Dist: pre-commit>=4.5.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest-asyncio>=0.24.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: pytest-cov>=6.0.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: pytest-mock>=3.15.0; extra == 'dev'
|
|
36
|
+
Requires-Dist: pytest>=8.3.0; extra == 'dev'
|
|
37
|
+
Requires-Dist: respx>=0.22.0; extra == 'dev'
|
|
38
|
+
Requires-Dist: ruff>=0.14.14; extra == 'dev'
|
|
39
|
+
Requires-Dist: safety<4.0.0,>=2.3.0; extra == 'dev'
|
|
40
|
+
Provides-Extra: docs
|
|
41
|
+
Requires-Dist: mkdocs-material>=9.7.0; extra == 'docs'
|
|
42
|
+
Requires-Dist: mkdocs>=1.6.1; extra == 'docs'
|
|
43
|
+
Requires-Dist: mkdocstrings[python]>=1.0.0; extra == 'docs'
|
|
44
|
+
Description-Content-Type: text/markdown
|
|
45
|
+
|
|
46
|
+
# strapi-kit
|
|
47
|
+
|
|
48
|
+
**PyPI Package**: `strapi-kit`
|
|
49
|
+
|
|
50
|
+
A modern Python client for Strapi CMS with comprehensive import/export capabilities.
|
|
51
|
+
|
|
52
|
+
## Features
|
|
53
|
+
|
|
54
|
+
- 🚀 **Full Strapi Support**: Works with both v4 and v5 APIs with automatic version detection
|
|
55
|
+
- ⚡ **Async & Sync**: Choose between synchronous and asynchronous clients based on your needs
|
|
56
|
+
- 🔒 **Type Safe**: Built with Pydantic for robust data validation and type safety
|
|
57
|
+
- 🔄 **Import/Export**: Comprehensive backup/restore and data migration tools
|
|
58
|
+
- 🔁 **Smart Retry**: Automatic retry with exponential backoff for transient failures
|
|
59
|
+
- 📦 **Modern Python**: Built for Python 3.12+ with full type hints
|
|
60
|
+
|
|
61
|
+
## Installation
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install strapi-kit
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Or with uv (recommended for faster installs):
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
uv pip install strapi-kit
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
For development:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# With pip
|
|
77
|
+
pip install -e ".[dev]"
|
|
78
|
+
|
|
79
|
+
# With uv (recommended)
|
|
80
|
+
uv pip install -e ".[dev]"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Quick Start
|
|
84
|
+
|
|
85
|
+
### Type-Safe API (Recommended)
|
|
86
|
+
|
|
87
|
+
The typed API provides full type safety, IDE autocomplete, and automatic v4/v5 normalization:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from strapi_kit import SyncClient, StrapiConfig
|
|
91
|
+
from strapi_kit.models import StrapiQuery, FilterBuilder, SortDirection
|
|
92
|
+
|
|
93
|
+
config = StrapiConfig(
|
|
94
|
+
base_url="http://localhost:1337",
|
|
95
|
+
api_token="your-api-token"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
with SyncClient(config) as client:
|
|
99
|
+
# Build a type-safe query
|
|
100
|
+
query = (StrapiQuery()
|
|
101
|
+
.filter(FilterBuilder()
|
|
102
|
+
.eq("status", "published")
|
|
103
|
+
.gt("views", 100))
|
|
104
|
+
.sort_by("publishedAt", SortDirection.DESC)
|
|
105
|
+
.paginate(page=1, page_size=25)
|
|
106
|
+
.populate_fields(["author", "category"]))
|
|
107
|
+
|
|
108
|
+
# Get normalized, type-safe response
|
|
109
|
+
response = client.get_many("articles", query=query)
|
|
110
|
+
|
|
111
|
+
# Works with both v4 and v5 automatically!
|
|
112
|
+
for article in response.data:
|
|
113
|
+
print(f"{article.id}: {article.attributes['title']}")
|
|
114
|
+
print(f"Published: {article.published_at}")
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Raw API (Backward Compatible)
|
|
118
|
+
|
|
119
|
+
The raw API returns dictionaries directly from Strapi:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from strapi_kit import SyncClient, StrapiConfig
|
|
123
|
+
|
|
124
|
+
config = StrapiConfig(
|
|
125
|
+
base_url="http://localhost:1337",
|
|
126
|
+
api_token="your-api-token"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
with SyncClient(config) as client:
|
|
130
|
+
# Get raw JSON response
|
|
131
|
+
response = client.get("articles")
|
|
132
|
+
print(response) # dict
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Asynchronous Usage
|
|
136
|
+
|
|
137
|
+
Both typed and raw APIs work with async:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
import asyncio
|
|
141
|
+
from strapi_kit import AsyncClient, StrapiConfig
|
|
142
|
+
from strapi_kit.models import StrapiQuery, FilterBuilder
|
|
143
|
+
|
|
144
|
+
async def main():
|
|
145
|
+
config = StrapiConfig(
|
|
146
|
+
base_url="http://localhost:1337",
|
|
147
|
+
api_token="your-api-token"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
async with AsyncClient(config) as client:
|
|
151
|
+
# Typed API
|
|
152
|
+
query = StrapiQuery().filter(FilterBuilder().eq("status", "published"))
|
|
153
|
+
response = await client.get_many("articles", query=query)
|
|
154
|
+
|
|
155
|
+
for article in response.data:
|
|
156
|
+
print(article.attributes["title"])
|
|
157
|
+
|
|
158
|
+
asyncio.run(main())
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Configuration
|
|
162
|
+
|
|
163
|
+
strapi-kit provides flexible configuration options through dependency injection:
|
|
164
|
+
|
|
165
|
+
### 1. Using .env Files (Recommended for Development)
|
|
166
|
+
|
|
167
|
+
Create a `.env` file in your project root:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
# .env
|
|
171
|
+
STRAPI_BASE_URL=http://localhost:1337
|
|
172
|
+
STRAPI_API_TOKEN=your-api-token-here
|
|
173
|
+
STRAPI_TIMEOUT=30.0
|
|
174
|
+
STRAPI_MAX_CONNECTIONS=10
|
|
175
|
+
STRAPI_RETRY_MAX_ATTEMPTS=3
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Then load it automatically:
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
from strapi_kit import load_config, SyncClient
|
|
182
|
+
|
|
183
|
+
# Automatically searches for .env, .env.local, or ~/.config/strapi/.env
|
|
184
|
+
config = load_config()
|
|
185
|
+
|
|
186
|
+
with SyncClient(config) as client:
|
|
187
|
+
response = client.get("articles")
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### 2. Using Environment Variables (Recommended for Production)
|
|
191
|
+
|
|
192
|
+
Perfect for containerized deployments (Docker, Kubernetes):
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
export STRAPI_BASE_URL=https://api.production.com
|
|
196
|
+
export STRAPI_API_TOKEN=production-secret-token
|
|
197
|
+
export STRAPI_TIMEOUT=120.0
|
|
198
|
+
export STRAPI_MAX_CONNECTIONS=100
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
from strapi_kit import ConfigFactory, SyncClient
|
|
203
|
+
|
|
204
|
+
# Load from environment variables only (no .env files)
|
|
205
|
+
config = ConfigFactory.from_environment_only()
|
|
206
|
+
|
|
207
|
+
with SyncClient(config) as client:
|
|
208
|
+
response = client.get("articles")
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### 3. Explicit Configuration (Recommended for Testing)
|
|
212
|
+
|
|
213
|
+
Create configuration programmatically:
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
from strapi_kit import create_config, SyncClient
|
|
217
|
+
|
|
218
|
+
config = create_config(
|
|
219
|
+
base_url="http://localhost:1337",
|
|
220
|
+
api_token="your-token",
|
|
221
|
+
timeout=60.0,
|
|
222
|
+
max_connections=50,
|
|
223
|
+
verify_ssl=True
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
with SyncClient(config) as client:
|
|
227
|
+
response = client.get("articles")
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### 4. Advanced Configuration Patterns
|
|
231
|
+
|
|
232
|
+
#### Custom .env File Location
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
from strapi_kit import ConfigFactory
|
|
236
|
+
|
|
237
|
+
# Load from specific file
|
|
238
|
+
config = ConfigFactory.from_env_file("/path/to/custom.env")
|
|
239
|
+
|
|
240
|
+
# Search multiple locations
|
|
241
|
+
config = ConfigFactory.from_env(
|
|
242
|
+
search_paths=[
|
|
243
|
+
".env.local", # Local overrides (highest priority)
|
|
244
|
+
".env", # Base config
|
|
245
|
+
"~/.strapi/.env" # User config (lowest priority)
|
|
246
|
+
]
|
|
247
|
+
)
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
#### Layered Configuration (Development → Production)
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
from strapi_kit import ConfigFactory
|
|
254
|
+
|
|
255
|
+
# Base configuration from .env file
|
|
256
|
+
base_config = ConfigFactory.from_env_file(".env")
|
|
257
|
+
|
|
258
|
+
# Override specific values for production
|
|
259
|
+
production_overrides = ConfigFactory.from_dict({
|
|
260
|
+
"base_url": "https://api.production.com",
|
|
261
|
+
"api_token": "production-token",
|
|
262
|
+
"timeout": 120.0,
|
|
263
|
+
"max_connections": 100
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
# Merge configs (later configs override earlier ones)
|
|
267
|
+
final_config = ConfigFactory.merge(base_config, production_overrides)
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
#### Retry Configuration
|
|
271
|
+
|
|
272
|
+
Configure automatic retry behavior:
|
|
273
|
+
|
|
274
|
+
```python
|
|
275
|
+
from strapi_kit import StrapiConfig, RetryConfig
|
|
276
|
+
|
|
277
|
+
config = StrapiConfig(
|
|
278
|
+
base_url="http://localhost:1337",
|
|
279
|
+
api_token="your-token",
|
|
280
|
+
retry=RetryConfig(
|
|
281
|
+
max_attempts=5, # Retry up to 5 times
|
|
282
|
+
initial_wait=2.0, # Wait 2 seconds before first retry
|
|
283
|
+
max_wait=120.0, # Maximum 2 minutes between retries
|
|
284
|
+
exponential_base=3.0, # Faster backoff growth
|
|
285
|
+
retry_on_status={500, 502, 503, 504, 408} # Retry on these status codes
|
|
286
|
+
)
|
|
287
|
+
)
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Or via environment variables:
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
STRAPI_RETRY_MAX_ATTEMPTS=5
|
|
294
|
+
STRAPI_RETRY_INITIAL_WAIT=2.0
|
|
295
|
+
STRAPI_RETRY_MAX_WAIT=120.0
|
|
296
|
+
STRAPI_RETRY_EXPONENTIAL_BASE=3.0
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Configuration Reference
|
|
300
|
+
|
|
301
|
+
All available options:
|
|
302
|
+
|
|
303
|
+
| Option | Type | Default | Description |
|
|
304
|
+
|--------|------|---------|-------------|
|
|
305
|
+
| `base_url` | `str` | **Required** | Strapi instance URL |
|
|
306
|
+
| `api_token` | `str` | **Required** | API authentication token |
|
|
307
|
+
| `api_version` | `"v4" \| "v5" \| "auto"` | `"auto"` | API version (auto-detect or explicit) |
|
|
308
|
+
| `timeout` | `float` | `30.0` | Request timeout in seconds |
|
|
309
|
+
| `max_connections` | `int` | `10` | Maximum concurrent connections |
|
|
310
|
+
| `verify_ssl` | `bool` | `True` | Verify SSL certificates |
|
|
311
|
+
| `rate_limit_per_second` | `float \| None` | `None` | Rate limiting (None = unlimited) |
|
|
312
|
+
| `retry.max_attempts` | `int` | `3` | Maximum retry attempts (1-10) |
|
|
313
|
+
| `retry.initial_wait` | `float` | `1.0` | Initial retry wait time (seconds) |
|
|
314
|
+
| `retry.max_wait` | `float` | `60.0` | Maximum retry wait time (seconds) |
|
|
315
|
+
| `retry.exponential_base` | `float` | `2.0` | Exponential backoff multiplier |
|
|
316
|
+
|
|
317
|
+
## Usage Examples
|
|
318
|
+
|
|
319
|
+
### Filtering
|
|
320
|
+
|
|
321
|
+
Use the `FilterBuilder` to create complex filters with 24 operators:
|
|
322
|
+
|
|
323
|
+
```python
|
|
324
|
+
from strapi_kit.models import StrapiQuery, FilterBuilder
|
|
325
|
+
|
|
326
|
+
# Simple equality
|
|
327
|
+
query = StrapiQuery().filter(FilterBuilder().eq("status", "published"))
|
|
328
|
+
|
|
329
|
+
# Comparison operators
|
|
330
|
+
query = StrapiQuery().filter(
|
|
331
|
+
FilterBuilder()
|
|
332
|
+
.gt("views", 100)
|
|
333
|
+
.lte("price", 50)
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
# String matching
|
|
337
|
+
query = StrapiQuery().filter(
|
|
338
|
+
FilterBuilder()
|
|
339
|
+
.contains("title", "Python")
|
|
340
|
+
.starts_with("slug", "blog-")
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
# Array operators
|
|
344
|
+
query = StrapiQuery().filter(
|
|
345
|
+
FilterBuilder().in_("category", ["tech", "science"])
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
# Logical operators (AND, OR, NOT)
|
|
349
|
+
query = StrapiQuery().filter(
|
|
350
|
+
FilterBuilder()
|
|
351
|
+
.eq("status", "published")
|
|
352
|
+
.or_group(
|
|
353
|
+
FilterBuilder().gt("views", 1000),
|
|
354
|
+
FilterBuilder().gt("likes", 500)
|
|
355
|
+
)
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
# Deep relation filtering
|
|
359
|
+
query = StrapiQuery().filter(
|
|
360
|
+
FilterBuilder()
|
|
361
|
+
.eq("author.name", "John Doe")
|
|
362
|
+
.eq("author.country", "USA")
|
|
363
|
+
)
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Sorting
|
|
367
|
+
|
|
368
|
+
Sort by one or multiple fields:
|
|
369
|
+
|
|
370
|
+
```python
|
|
371
|
+
from strapi_kit.models import StrapiQuery, SortDirection
|
|
372
|
+
|
|
373
|
+
# Single field
|
|
374
|
+
query = StrapiQuery().sort_by("publishedAt", SortDirection.DESC)
|
|
375
|
+
|
|
376
|
+
# Multiple fields
|
|
377
|
+
query = (StrapiQuery()
|
|
378
|
+
.sort_by("status", SortDirection.ASC)
|
|
379
|
+
.then_sort_by("publishedAt", SortDirection.DESC)
|
|
380
|
+
.then_sort_by("title", SortDirection.ASC))
|
|
381
|
+
|
|
382
|
+
# Sort by relation field
|
|
383
|
+
query = StrapiQuery().sort_by("author.name", SortDirection.ASC)
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
### Pagination
|
|
387
|
+
|
|
388
|
+
Choose between page-based or offset-based pagination:
|
|
389
|
+
|
|
390
|
+
```python
|
|
391
|
+
from strapi_kit.models import StrapiQuery
|
|
392
|
+
|
|
393
|
+
# Page-based pagination
|
|
394
|
+
query = StrapiQuery().paginate(page=1, page_size=25)
|
|
395
|
+
|
|
396
|
+
# Offset-based pagination
|
|
397
|
+
query = StrapiQuery().paginate(start=0, limit=50)
|
|
398
|
+
|
|
399
|
+
# Disable count for performance
|
|
400
|
+
query = StrapiQuery().paginate(page=1, page_size=100, with_count=False)
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### Population (Relations)
|
|
404
|
+
|
|
405
|
+
Expand relations, components, and dynamic zones:
|
|
406
|
+
|
|
407
|
+
```python
|
|
408
|
+
from strapi_kit.models import StrapiQuery, Populate, FilterBuilder, SortDirection
|
|
409
|
+
|
|
410
|
+
# Populate all relations
|
|
411
|
+
query = StrapiQuery().populate_all()
|
|
412
|
+
|
|
413
|
+
# Populate specific fields
|
|
414
|
+
query = StrapiQuery().populate_fields(["author", "category", "tags"])
|
|
415
|
+
|
|
416
|
+
# Advanced population with filtering and field selection
|
|
417
|
+
query = StrapiQuery().populate(
|
|
418
|
+
Populate()
|
|
419
|
+
.add_field("author", fields=["name", "email", "avatar"])
|
|
420
|
+
.add_field("category")
|
|
421
|
+
.add_field("comments",
|
|
422
|
+
filters=FilterBuilder().eq("approved", True),
|
|
423
|
+
sort=Sort().by_field("createdAt", SortDirection.DESC),
|
|
424
|
+
fields=["content", "author"])
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
# Nested population
|
|
428
|
+
query = StrapiQuery().populate(
|
|
429
|
+
Populate().add_field(
|
|
430
|
+
"author",
|
|
431
|
+
nested=Populate().add_field("profile")
|
|
432
|
+
)
|
|
433
|
+
)
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
### Field Selection
|
|
437
|
+
|
|
438
|
+
Select specific fields to reduce payload size:
|
|
439
|
+
|
|
440
|
+
```python
|
|
441
|
+
from strapi_kit.models import StrapiQuery
|
|
442
|
+
|
|
443
|
+
query = StrapiQuery().select(["title", "description", "publishedAt"])
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
### Locale & Publication State
|
|
447
|
+
|
|
448
|
+
For i18n and draft/publish workflows:
|
|
449
|
+
|
|
450
|
+
```python
|
|
451
|
+
from strapi_kit.models import StrapiQuery, PublicationState
|
|
452
|
+
|
|
453
|
+
# Set locale
|
|
454
|
+
query = StrapiQuery().with_locale("fr")
|
|
455
|
+
|
|
456
|
+
# Set publication state
|
|
457
|
+
query = StrapiQuery().with_publication_state(PublicationState.LIVE)
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
### Complete Example
|
|
461
|
+
|
|
462
|
+
Combine all features for complex queries:
|
|
463
|
+
|
|
464
|
+
```python
|
|
465
|
+
from strapi_kit import SyncClient, StrapiConfig
|
|
466
|
+
from strapi_kit.models import (
|
|
467
|
+
StrapiQuery,
|
|
468
|
+
FilterBuilder,
|
|
469
|
+
SortDirection,
|
|
470
|
+
Populate,
|
|
471
|
+
PublicationState,
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
config = StrapiConfig(
|
|
475
|
+
base_url="http://localhost:1337",
|
|
476
|
+
api_token="your-token"
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
with SyncClient(config) as client:
|
|
480
|
+
# Build complex query
|
|
481
|
+
query = (StrapiQuery()
|
|
482
|
+
# Filters
|
|
483
|
+
.filter(FilterBuilder()
|
|
484
|
+
.eq("status", "published")
|
|
485
|
+
.gte("publishedAt", "2024-01-01")
|
|
486
|
+
.null("deletedAt")
|
|
487
|
+
.or_group(
|
|
488
|
+
FilterBuilder().contains("title", "Python"),
|
|
489
|
+
FilterBuilder().contains("title", "Django")
|
|
490
|
+
))
|
|
491
|
+
# Sorting
|
|
492
|
+
.sort_by("publishedAt", SortDirection.DESC)
|
|
493
|
+
.then_sort_by("views", SortDirection.DESC)
|
|
494
|
+
# Pagination
|
|
495
|
+
.paginate(page=1, page_size=20)
|
|
496
|
+
# Population
|
|
497
|
+
.populate(Populate()
|
|
498
|
+
.add_field("author", fields=["name", "avatar", "bio"])
|
|
499
|
+
.add_field("category")
|
|
500
|
+
.add_field("comments",
|
|
501
|
+
filters=FilterBuilder().eq("approved", True)))
|
|
502
|
+
# Field selection
|
|
503
|
+
.select(["title", "slug", "excerpt", "coverImage", "publishedAt"])
|
|
504
|
+
# Locale & publication
|
|
505
|
+
.with_locale("en")
|
|
506
|
+
.with_publication_state(PublicationState.LIVE))
|
|
507
|
+
|
|
508
|
+
# Execute query with type-safe response
|
|
509
|
+
response = client.get_many("articles", query=query)
|
|
510
|
+
|
|
511
|
+
# Access normalized data (works with both v4 and v5!)
|
|
512
|
+
print(f"Total articles: {response.meta.pagination.total}")
|
|
513
|
+
print(f"Page {response.meta.pagination.page} of {response.meta.pagination.page_count}")
|
|
514
|
+
|
|
515
|
+
for article in response.data:
|
|
516
|
+
# All responses are normalized to the same structure
|
|
517
|
+
print(f"ID: {article.id}")
|
|
518
|
+
print(f"Document ID: {article.document_id}") # v5 only, None for v4
|
|
519
|
+
print(f"Title: {article.attributes['title']}")
|
|
520
|
+
print(f"Published: {article.published_at}")
|
|
521
|
+
print("---")
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
### CRUD Operations
|
|
525
|
+
|
|
526
|
+
Create, read, update, and delete entities:
|
|
527
|
+
|
|
528
|
+
```python
|
|
529
|
+
from strapi_kit import SyncClient, StrapiConfig
|
|
530
|
+
|
|
531
|
+
config = StrapiConfig(base_url="http://localhost:1337", api_token="your-token")
|
|
532
|
+
|
|
533
|
+
with SyncClient(config) as client:
|
|
534
|
+
# Create
|
|
535
|
+
data = {"title": "New Article", "content": "Article body"}
|
|
536
|
+
response = client.create("articles", data)
|
|
537
|
+
created_id = response.data.id
|
|
538
|
+
|
|
539
|
+
# Read one
|
|
540
|
+
response = client.get_one(f"articles/{created_id}")
|
|
541
|
+
article = response.data
|
|
542
|
+
|
|
543
|
+
# Read many
|
|
544
|
+
response = client.get_many("articles")
|
|
545
|
+
all_articles = response.data
|
|
546
|
+
|
|
547
|
+
# Update
|
|
548
|
+
data = {"title": "Updated Title"}
|
|
549
|
+
response = client.update(f"articles/{created_id}", data)
|
|
550
|
+
|
|
551
|
+
# Delete
|
|
552
|
+
response = client.remove(f"articles/{created_id}")
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
### Media Upload/Download
|
|
556
|
+
|
|
557
|
+
Upload, download, and manage media files in Strapi's media library:
|
|
558
|
+
|
|
559
|
+
```python
|
|
560
|
+
from strapi_kit import SyncClient, StrapiConfig
|
|
561
|
+
from strapi_kit.models import StrapiQuery, FilterBuilder
|
|
562
|
+
|
|
563
|
+
config = StrapiConfig(base_url="http://localhost:1337", api_token="your-token")
|
|
564
|
+
|
|
565
|
+
with SyncClient(config) as client:
|
|
566
|
+
# Upload a file
|
|
567
|
+
media = client.upload_file(
|
|
568
|
+
"hero-image.jpg",
|
|
569
|
+
alternative_text="Hero image",
|
|
570
|
+
caption="Main article hero image"
|
|
571
|
+
)
|
|
572
|
+
print(f"Uploaded: {media.name} (ID: {media.id})")
|
|
573
|
+
print(f"URL: {media.url}")
|
|
574
|
+
|
|
575
|
+
# Upload and attach to an entity
|
|
576
|
+
cover = client.upload_file(
|
|
577
|
+
"cover.jpg",
|
|
578
|
+
ref="api::article.article",
|
|
579
|
+
ref_id="abc123", # Article documentId or numeric ID
|
|
580
|
+
field="cover"
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
# Upload multiple files
|
|
584
|
+
files = ["image1.jpg", "image2.jpg", "image3.jpg"]
|
|
585
|
+
media_list = client.upload_files(files, folder="gallery")
|
|
586
|
+
print(f"Uploaded {len(media_list)} files")
|
|
587
|
+
|
|
588
|
+
# List media library
|
|
589
|
+
response = client.list_media()
|
|
590
|
+
for item in response.data:
|
|
591
|
+
print(f"{item.attributes['name']}: {item.attributes['url']}")
|
|
592
|
+
|
|
593
|
+
# List with filters
|
|
594
|
+
query = (StrapiQuery()
|
|
595
|
+
.filter(FilterBuilder().eq("mime", "image/jpeg"))
|
|
596
|
+
.paginate(page=1, page_size=10))
|
|
597
|
+
response = client.list_media(query)
|
|
598
|
+
|
|
599
|
+
# Get specific media details
|
|
600
|
+
media = client.get_media(42)
|
|
601
|
+
print(f"Name: {media.name}, Size: {media.size} KB")
|
|
602
|
+
|
|
603
|
+
# Download a file
|
|
604
|
+
content = client.download_file(media.url)
|
|
605
|
+
print(f"Downloaded {len(content)} bytes")
|
|
606
|
+
|
|
607
|
+
# Download and save
|
|
608
|
+
client.download_file(
|
|
609
|
+
media.url,
|
|
610
|
+
save_path="downloaded_image.jpg"
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
# Update media metadata
|
|
614
|
+
updated = client.update_media(
|
|
615
|
+
42,
|
|
616
|
+
alternative_text="Updated alt text",
|
|
617
|
+
caption="Updated caption"
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
# Delete media
|
|
621
|
+
client.delete_media(42)
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
**Async version:**
|
|
625
|
+
|
|
626
|
+
```python
|
|
627
|
+
import asyncio
|
|
628
|
+
from strapi_kit import AsyncClient, StrapiConfig
|
|
629
|
+
|
|
630
|
+
async def main():
|
|
631
|
+
config = StrapiConfig(base_url="http://localhost:1337", api_token="your-token")
|
|
632
|
+
|
|
633
|
+
async with AsyncClient(config) as client:
|
|
634
|
+
# All methods have async equivalents
|
|
635
|
+
media = await client.upload_file("image.jpg")
|
|
636
|
+
content = await client.download_file(media.url)
|
|
637
|
+
await client.delete_media(media.id)
|
|
638
|
+
|
|
639
|
+
asyncio.run(main())
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
**Media Features:**
|
|
643
|
+
|
|
644
|
+
- Upload single or multiple files
|
|
645
|
+
- Attach uploads to specific entities (articles, pages, etc.)
|
|
646
|
+
- Set metadata (alt text, captions)
|
|
647
|
+
- Download with streaming for large files
|
|
648
|
+
- Query media library with filters
|
|
649
|
+
- Update metadata without re-uploading
|
|
650
|
+
- Full support for both sync and async
|
|
651
|
+
|
|
652
|
+
### Export/Import with Relation Resolution
|
|
653
|
+
|
|
654
|
+
strapi-kit provides comprehensive export/import functionality with automatic relation resolution for migrating content between Strapi instances.
|
|
655
|
+
|
|
656
|
+
```python
|
|
657
|
+
from strapi_kit import StrapiConfig, StrapiExporter, StrapiImporter, SyncClient
|
|
658
|
+
|
|
659
|
+
# Export from source instance
|
|
660
|
+
source_config = StrapiConfig(
|
|
661
|
+
base_url="http://localhost:1337",
|
|
662
|
+
api_token="source-token"
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
with SyncClient(source_config) as client:
|
|
666
|
+
exporter = StrapiExporter(client)
|
|
667
|
+
|
|
668
|
+
# Export content types with schemas for relation resolution
|
|
669
|
+
export_data = exporter.export_content_types([
|
|
670
|
+
"api::article.article",
|
|
671
|
+
"api::author.author",
|
|
672
|
+
"api::category.category"
|
|
673
|
+
])
|
|
674
|
+
|
|
675
|
+
# Save to file
|
|
676
|
+
exporter.save_to_file(export_data, "migration.json")
|
|
677
|
+
|
|
678
|
+
# Import to target instance
|
|
679
|
+
target_config = StrapiConfig(
|
|
680
|
+
base_url="http://localhost:1338",
|
|
681
|
+
api_token="target-token"
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
with SyncClient(target_config) as client:
|
|
685
|
+
importer = StrapiImporter(client)
|
|
686
|
+
|
|
687
|
+
# Load export
|
|
688
|
+
export_data = StrapiExporter.load_from_file("migration.json")
|
|
689
|
+
|
|
690
|
+
# Import with automatic relation resolution
|
|
691
|
+
result = importer.import_data(export_data)
|
|
692
|
+
|
|
693
|
+
print(f"Imported {result.entities_imported} entities")
|
|
694
|
+
print(f"ID mapping: {result.id_mapping}")
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
**Export/Import Features:**
|
|
698
|
+
|
|
699
|
+
- **Automatic Relation Resolution**: Relations are automatically mapped using content type schemas
|
|
700
|
+
- **Schema Caching**: Content type metadata cached for fast relation lookups
|
|
701
|
+
- **ID Mapping**: Old IDs automatically mapped to new IDs during import
|
|
702
|
+
- **Media Support**: Export and import media files with content
|
|
703
|
+
- **Progress Tracking**: Optional callbacks for monitoring long operations
|
|
704
|
+
- **Dry Run Mode**: Test imports before executing
|
|
705
|
+
- **Conflict Resolution**: Configurable strategies for handling existing entities
|
|
706
|
+
|
|
707
|
+
**How Relation Resolution Works:**
|
|
708
|
+
|
|
709
|
+
1. During export, content type schemas are fetched from the Content-Type Builder API
|
|
710
|
+
2. Schemas include relation metadata (field types, targets)
|
|
711
|
+
3. During import, relations are resolved by looking up target content types from schemas
|
|
712
|
+
4. Old IDs are mapped to new IDs using the ID mapping table
|
|
713
|
+
|
|
714
|
+
For example, when importing an article with `{"author": [5]}`, the system:
|
|
715
|
+
- Looks up the schema to find that `author` targets `"api::author.author"`
|
|
716
|
+
- Maps old author ID 5 to the new ID in the target instance
|
|
717
|
+
- Updates the article with the resolved relation
|
|
718
|
+
|
|
719
|
+
See the [Export/Import Guide](docs/export-import.md) for complete documentation.
|
|
720
|
+
|
|
721
|
+
### Complete Migration Examples
|
|
722
|
+
|
|
723
|
+
We provide two complete migration examples for different use cases:
|
|
724
|
+
|
|
725
|
+
#### Simple Migration (Quick Start)
|
|
726
|
+
|
|
727
|
+
Perfect for straightforward migrations with known content types:
|
|
728
|
+
|
|
729
|
+
```bash
|
|
730
|
+
# 1. Edit examples/simple_migration.py with your configuration
|
|
731
|
+
# 2. Run the migration
|
|
732
|
+
python examples/simple_migration.py
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
Features:
|
|
736
|
+
- ✅ Single-file, easy to understand
|
|
737
|
+
- ✅ Migrates specific content types
|
|
738
|
+
- ✅ Includes media files
|
|
739
|
+
- ✅ Automatic relation resolution
|
|
740
|
+
- ✅ Saves backup to JSON
|
|
741
|
+
|
|
742
|
+
#### Full Migration (Production-Ready)
|
|
743
|
+
|
|
744
|
+
Comprehensive migration tool with auto-discovery and verification:
|
|
745
|
+
|
|
746
|
+
```bash
|
|
747
|
+
# Export all content from source
|
|
748
|
+
python examples/full_migration_v5.py export
|
|
749
|
+
|
|
750
|
+
# Import to target
|
|
751
|
+
python examples/full_migration_v5.py import
|
|
752
|
+
|
|
753
|
+
# Or do both in one command
|
|
754
|
+
python examples/full_migration_v5.py migrate
|
|
755
|
+
|
|
756
|
+
# Verify migration success
|
|
757
|
+
python examples/full_migration_v5.py verify
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
Features:
|
|
761
|
+
- ✅ **Auto-discovers all content types** (no manual configuration needed)
|
|
762
|
+
- ✅ Progress bars for long operations
|
|
763
|
+
- ✅ Detailed migration reports
|
|
764
|
+
- ✅ Entity count verification
|
|
765
|
+
- ✅ Error reporting and recovery
|
|
766
|
+
- ✅ Batch processing for large datasets
|
|
767
|
+
- ✅ ID mapping with detailed logs
|
|
768
|
+
- ✅ Media file handling with progress tracking
|
|
769
|
+
|
|
770
|
+
**Full Migration Example Output:**
|
|
771
|
+
|
|
772
|
+
```
|
|
773
|
+
🔍 Discovering content types...
|
|
774
|
+
Found 12 content types:
|
|
775
|
+
- api::article.article
|
|
776
|
+
- api::author.author
|
|
777
|
+
- api::category.category
|
|
778
|
+
...
|
|
779
|
+
|
|
780
|
+
📥 Exporting 12 content types...
|
|
781
|
+
[████████████████████████████████████████] 100% | Processing articles
|
|
782
|
+
|
|
783
|
+
✅ EXPORT COMPLETE
|
|
784
|
+
Content types exported: 12
|
|
785
|
+
Total entities exported: 1,847
|
|
786
|
+
Media files downloaded: 234
|
|
787
|
+
Total export size: 45.3 MB
|
|
788
|
+
|
|
789
|
+
📤 Importing 1,847 entities...
|
|
790
|
+
[████████████████████████████████████████] 100% | Importing articles
|
|
791
|
+
|
|
792
|
+
✅ IMPORT COMPLETE
|
|
793
|
+
Entities imported: 1,847
|
|
794
|
+
Media files imported: 234
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
Both examples include:
|
|
798
|
+
- SecretStr for secure token handling
|
|
799
|
+
- Proper error handling and reporting
|
|
800
|
+
- Progress tracking
|
|
801
|
+
- Automatic relation resolution using schemas
|
|
802
|
+
- Media file download/upload
|
|
803
|
+
- ID mapping for relations
|
|
804
|
+
|
|
805
|
+
## Dependency Injection
|
|
806
|
+
|
|
807
|
+
strapi-kit supports full dependency injection for testability and customization. All dependencies have sensible defaults but can be overridden.
|
|
808
|
+
|
|
809
|
+
### Why DI?
|
|
810
|
+
|
|
811
|
+
- **Testability**: Inject mocks for unit testing without HTTP calls
|
|
812
|
+
- **Customization**: Provide custom parsers, auth handlers, or HTTP clients
|
|
813
|
+
- **Flexibility**: Share HTTP clients across multiple Strapi instances
|
|
814
|
+
- **Control**: Manage lifecycles of shared resources
|
|
815
|
+
|
|
816
|
+
### Basic DI Example
|
|
817
|
+
|
|
818
|
+
```python
|
|
819
|
+
from strapi_kit import SyncClient, StrapiConfig
|
|
820
|
+
import httpx
|
|
821
|
+
|
|
822
|
+
config = StrapiConfig(
|
|
823
|
+
base_url="http://localhost:1337",
|
|
824
|
+
api_token="your-token"
|
|
825
|
+
)
|
|
826
|
+
|
|
827
|
+
# Simple usage - all dependencies created automatically
|
|
828
|
+
with SyncClient(config) as client:
|
|
829
|
+
response = client.get_many("articles")
|
|
830
|
+
|
|
831
|
+
# Advanced usage - inject custom HTTP client
|
|
832
|
+
shared_http = httpx.Client()
|
|
833
|
+
client1 = SyncClient(config, http_client=shared_http)
|
|
834
|
+
client2 = SyncClient(config, http_client=shared_http)
|
|
835
|
+
# Both share the same connection pool
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
### Injectable Dependencies
|
|
839
|
+
|
|
840
|
+
```python
|
|
841
|
+
from strapi_kit import (
|
|
842
|
+
SyncClient,
|
|
843
|
+
AsyncClient,
|
|
844
|
+
StrapiConfig,
|
|
845
|
+
AuthProvider,
|
|
846
|
+
HTTPClient,
|
|
847
|
+
AsyncHTTPClient,
|
|
848
|
+
ResponseParser,
|
|
849
|
+
VersionDetectingParser,
|
|
850
|
+
)
|
|
851
|
+
|
|
852
|
+
# Custom authentication
|
|
853
|
+
class CustomAuth:
|
|
854
|
+
def get_headers(self) -> dict[str, str]:
|
|
855
|
+
return {"Authorization": "Custom token"}
|
|
856
|
+
|
|
857
|
+
def validate_token(self) -> bool:
|
|
858
|
+
return True
|
|
859
|
+
|
|
860
|
+
# Custom response parser
|
|
861
|
+
class CustomParser:
|
|
862
|
+
def parse_single(self, response_data):
|
|
863
|
+
# Custom parsing logic
|
|
864
|
+
...
|
|
865
|
+
|
|
866
|
+
def parse_collection(self, response_data):
|
|
867
|
+
# Custom parsing logic
|
|
868
|
+
...
|
|
869
|
+
|
|
870
|
+
# Inject custom dependencies
|
|
871
|
+
client = SyncClient(
|
|
872
|
+
config,
|
|
873
|
+
http_client=custom_http, # Custom HTTP client
|
|
874
|
+
auth=custom_auth, # Custom auth provider
|
|
875
|
+
parser=custom_parser # Custom response parser
|
|
876
|
+
)
|
|
877
|
+
```
|
|
878
|
+
|
|
879
|
+
### Testing with DI
|
|
880
|
+
|
|
881
|
+
```python
|
|
882
|
+
from unittest.mock import Mock
|
|
883
|
+
|
|
884
|
+
# Create mock HTTP client for testing (no actual HTTP calls)
|
|
885
|
+
class MockHTTPClient:
|
|
886
|
+
def __init__(self):
|
|
887
|
+
self.requests = []
|
|
888
|
+
|
|
889
|
+
def request(self, method, url, **kwargs):
|
|
890
|
+
self.requests.append((method, url))
|
|
891
|
+
# Return mock response
|
|
892
|
+
mock_response = Mock()
|
|
893
|
+
mock_response.is_success = True
|
|
894
|
+
mock_response.json.return_value = {"data": []}
|
|
895
|
+
return mock_response
|
|
896
|
+
|
|
897
|
+
def close(self):
|
|
898
|
+
pass
|
|
899
|
+
|
|
900
|
+
# Use mock in tests
|
|
901
|
+
mock_http = MockHTTPClient()
|
|
902
|
+
client = SyncClient(config, http_client=mock_http)
|
|
903
|
+
|
|
904
|
+
# Make requests (no actual HTTP)
|
|
905
|
+
client.get("articles")
|
|
906
|
+
|
|
907
|
+
# Verify mock was called
|
|
908
|
+
assert len(mock_http.requests) == 1
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
### Protocols (Type Interfaces)
|
|
912
|
+
|
|
913
|
+
strapi-kit uses Python protocols for dependency interfaces:
|
|
914
|
+
|
|
915
|
+
- **`ConfigProvider`**: Configuration interface
|
|
916
|
+
- **`AuthProvider`**: Authentication interface
|
|
917
|
+
- **`HTTPClient`**: Sync HTTP client interface
|
|
918
|
+
- **`AsyncHTTPClient`**: Async HTTP client interface
|
|
919
|
+
- **`ResponseParser`**: Response parsing interface
|
|
920
|
+
|
|
921
|
+
All implementations satisfy these protocols and are type-checked with mypy.
|
|
922
|
+
|
|
923
|
+
**Example - Custom config from database**:
|
|
924
|
+
```python
|
|
925
|
+
class DatabaseConfig:
|
|
926
|
+
"""Load config from database."""
|
|
927
|
+
|
|
928
|
+
def __init__(self, db):
|
|
929
|
+
self.db = db
|
|
930
|
+
|
|
931
|
+
def get_base_url(self) -> str:
|
|
932
|
+
return self.db.query("SELECT url FROM config")[0]
|
|
933
|
+
|
|
934
|
+
def get_api_token(self) -> str:
|
|
935
|
+
return self.db.query("SELECT token FROM secrets")[0]
|
|
936
|
+
|
|
937
|
+
# ... other properties
|
|
938
|
+
|
|
939
|
+
# Use database config
|
|
940
|
+
db_config = DatabaseConfig(db_connection)
|
|
941
|
+
client = SyncClient(db_config)
|
|
942
|
+
```
|
|
943
|
+
|
|
944
|
+
## Development
|
|
945
|
+
|
|
946
|
+
### Setup
|
|
947
|
+
|
|
948
|
+
```bash
|
|
949
|
+
# Clone the repository
|
|
950
|
+
git clone https://github.com/mehdizare/strapi-kit.git
|
|
951
|
+
cd strapi-kit
|
|
952
|
+
|
|
953
|
+
# Create virtual environment
|
|
954
|
+
python -m venv .venv
|
|
955
|
+
source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
|
956
|
+
|
|
957
|
+
# Install dependencies (uv is recommended for faster installs)
|
|
958
|
+
uv pip install -e ".[dev]"
|
|
959
|
+
# Or with pip
|
|
960
|
+
pip install -e ".[dev]"
|
|
961
|
+
|
|
962
|
+
# Install pre-commit hooks (one-time setup)
|
|
963
|
+
make install-hooks
|
|
964
|
+
# Or manually:
|
|
965
|
+
pre-commit install
|
|
966
|
+
```
|
|
967
|
+
|
|
968
|
+
### Pre-commit Hooks
|
|
969
|
+
|
|
970
|
+
This project uses pre-commit hooks to ensure code quality:
|
|
971
|
+
|
|
972
|
+
```bash
|
|
973
|
+
# Install pre-commit hooks (one-time setup)
|
|
974
|
+
make install-hooks
|
|
975
|
+
|
|
976
|
+
# Or manually:
|
|
977
|
+
pre-commit install
|
|
978
|
+
|
|
979
|
+
# Run hooks manually on all files
|
|
980
|
+
make run-hooks
|
|
981
|
+
|
|
982
|
+
# Update hooks to latest versions
|
|
983
|
+
make update-hooks
|
|
984
|
+
```
|
|
985
|
+
|
|
986
|
+
**What the hooks check:**
|
|
987
|
+
- ✅ Code formatting (ruff format)
|
|
988
|
+
- ✅ Linting (ruff check)
|
|
989
|
+
- ✅ Type checking (mypy strict mode)
|
|
990
|
+
- ✅ Security issues (bandit)
|
|
991
|
+
- ✅ Secrets detection (detect-secrets)
|
|
992
|
+
- ✅ File consistency (trailing whitespace, EOF, etc.)
|
|
993
|
+
|
|
994
|
+
**Skip hooks temporarily** (not recommended):
|
|
995
|
+
```bash
|
|
996
|
+
git commit --no-verify
|
|
997
|
+
```
|
|
998
|
+
|
|
999
|
+
### Testing
|
|
1000
|
+
|
|
1001
|
+
```bash
|
|
1002
|
+
# Run all tests
|
|
1003
|
+
pytest
|
|
1004
|
+
|
|
1005
|
+
# Run with coverage
|
|
1006
|
+
pytest --cov=strapi_kit --cov-report=html
|
|
1007
|
+
|
|
1008
|
+
# Run specific test file
|
|
1009
|
+
pytest tests/unit/test_client.py -v
|
|
1010
|
+
```
|
|
1011
|
+
|
|
1012
|
+
### Code Quality
|
|
1013
|
+
|
|
1014
|
+
```bash
|
|
1015
|
+
# Format code
|
|
1016
|
+
ruff format src/ tests/
|
|
1017
|
+
|
|
1018
|
+
# Lint code
|
|
1019
|
+
ruff check src/ tests/
|
|
1020
|
+
|
|
1021
|
+
# Type checking
|
|
1022
|
+
mypy src/strapi_kit/
|
|
1023
|
+
|
|
1024
|
+
# Security checks
|
|
1025
|
+
make security
|
|
1026
|
+
|
|
1027
|
+
# Run all quality checks
|
|
1028
|
+
make quality
|
|
1029
|
+
```
|
|
1030
|
+
|
|
1031
|
+
## Project Status
|
|
1032
|
+
|
|
1033
|
+
This project is in active development. Currently implemented:
|
|
1034
|
+
|
|
1035
|
+
### ✅ Phase 1: Core Infrastructure (Complete)
|
|
1036
|
+
- HTTP clients (sync and async)
|
|
1037
|
+
- Configuration with Pydantic
|
|
1038
|
+
- Authentication (API tokens)
|
|
1039
|
+
- Exception hierarchy
|
|
1040
|
+
- API version detection (v4/v5)
|
|
1041
|
+
|
|
1042
|
+
### ✅ Phase 2: Type-Safe Query Builder (Complete)
|
|
1043
|
+
- **Request Models**: Filters (24 operators), sorting, pagination, population, field selection
|
|
1044
|
+
- **Response Models**: V4/V5 parsing with automatic normalization
|
|
1045
|
+
- **Query Builder**: `StrapiQuery` fluent API with full type safety
|
|
1046
|
+
- **Typed Client Methods**: `get_one()`, `get_many()`, `create()`, `update()`, `remove()`
|
|
1047
|
+
- **Dependency Injection**: Full DI support with protocols for testability
|
|
1048
|
+
- **93% test coverage** with 196 passing tests
|
|
1049
|
+
|
|
1050
|
+
### ✅ Phase 3: Media Operations (Complete)
|
|
1051
|
+
- **Media Upload**: Single and batch file uploads with metadata
|
|
1052
|
+
- **Media Download**: Streaming downloads for large files
|
|
1053
|
+
- **Media Management**: List, get, update, and delete media
|
|
1054
|
+
- **Entity Attachment**: Link media to specific content types
|
|
1055
|
+
- **Full async support** for all media operations
|
|
1056
|
+
- **100% test coverage** on media operations
|
|
1057
|
+
|
|
1058
|
+
### ✅ Phase 4: Export/Import (Complete)
|
|
1059
|
+
- **Content Export**: Export content types with all entities
|
|
1060
|
+
- **Automatic Relation Resolution**: Schema-based relation mapping
|
|
1061
|
+
- **Media Export**: Download and package media files
|
|
1062
|
+
- **Content Import**: Import with ID mapping and relation resolution
|
|
1063
|
+
- **Schema Caching**: Efficient content type metadata handling
|
|
1064
|
+
- **89% overall test coverage** with 355 passing tests
|
|
1065
|
+
|
|
1066
|
+
### 🚧 Phase 5-6: Advanced Features (Planned)
|
|
1067
|
+
- Bulk operations with streaming
|
|
1068
|
+
- Content type introspection
|
|
1069
|
+
- Advanced retry strategies
|
|
1070
|
+
- Rate limiting
|
|
1071
|
+
|
|
1072
|
+
### Key Features
|
|
1073
|
+
- **Type-Safe**: Full Pydantic validation and mypy strict mode compliance
|
|
1074
|
+
- **Version Agnostic**: Works with both Strapi v4 and v5 seamlessly
|
|
1075
|
+
- **24 Filter Operators**: Complete filtering support (eq, gt, contains, in, null, between, etc.)
|
|
1076
|
+
- **Normalized Responses**: Consistent interface regardless of Strapi version
|
|
1077
|
+
- **Dependency Injection**: Protocol-based DI for testability and customization
|
|
1078
|
+
- **IDE Autocomplete**: Full type hints for excellent developer experience
|
|
1079
|
+
- **Dual API**: Use typed methods for safety or raw methods for flexibility
|
|
1080
|
+
|
|
1081
|
+
## License
|
|
1082
|
+
|
|
1083
|
+
MIT License - see LICENSE file for details.
|
|
1084
|
+
|
|
1085
|
+
## Contributing
|
|
1086
|
+
|
|
1087
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
1088
|
+
|
|
1089
|
+
### Development Process
|
|
1090
|
+
|
|
1091
|
+
1. Fork the repository
|
|
1092
|
+
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
|
1093
|
+
3. Make your changes and add tests
|
|
1094
|
+
4. Run quality checks: `make pre-commit`
|
|
1095
|
+
5. Commit your changes with conventional commits format
|
|
1096
|
+
6. Push to your fork and submit a Pull Request
|
|
1097
|
+
|
|
1098
|
+
**Automated Reviews:** All PRs are automatically reviewed by CodeRabbit AI for code quality, security, and best practices.
|