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,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.