strapi-kit 0.0.2__tar.gz → 0.0.4__tar.gz

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 (101) hide show
  1. strapi_kit-0.0.4/AGENTS.md +54 -0
  2. strapi_kit-0.0.4/CHANGELOG.md +81 -0
  3. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/CLAUDE.md +8 -1
  4. strapi_kit-0.0.4/LLM.md +502 -0
  5. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/PKG-INFO +204 -10
  6. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/README.md +203 -9
  7. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/docs/changelog.md +20 -1
  8. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/examples/basic_crud.py +13 -6
  9. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/examples/full_migration_v5.py +90 -33
  10. strapi_kit-0.0.4/examples/simple_migration.py +223 -0
  11. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/_version.py +2 -2
  12. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/client/async_client.py +87 -1
  13. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/client/base.py +99 -3
  14. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/client/sync_client.py +87 -1
  15. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/config_provider.py +1 -16
  16. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/exceptions/__init__.py +2 -0
  17. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/exceptions/errors.py +13 -0
  18. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/export/exporter.py +6 -5
  19. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/models/__init__.py +8 -0
  20. strapi_kit-0.0.4/src/strapi_kit/models/content_type.py +148 -0
  21. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/models/export_format.py +5 -3
  22. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/models/request/query.py +9 -6
  23. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/operations/media.py +13 -6
  24. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/operations/streaming.py +5 -4
  25. strapi_kit-0.0.4/src/strapi_kit/utils/__init__.py +34 -0
  26. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/utils/rate_limiter.py +4 -2
  27. strapi_kit-0.0.4/src/strapi_kit/utils/seo.py +294 -0
  28. strapi_kit-0.0.4/src/strapi_kit/utils/uid.py +217 -0
  29. strapi_kit-0.0.2/examples/simple_migration.py +0 -90
  30. strapi_kit-0.0.2/src/strapi_kit/utils/__init__.py +0 -15
  31. strapi_kit-0.0.2/src/strapi_kit/utils/uid.py +0 -88
  32. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/.coderabbit.yaml +0 -0
  33. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/.env.example +0 -0
  34. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/.github/dependabot.yml +0 -0
  35. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/.github/pull_request_template.md +0 -0
  36. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/.github/workflows/ci.yml +0 -0
  37. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/.github/workflows/dev-release.yml +0 -0
  38. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/.github/workflows/document.yml +0 -0
  39. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/.github/workflows/guard-main-origin.yml +0 -0
  40. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/.github/workflows/release.yml +0 -0
  41. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/.gitignore +0 -0
  42. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/.pre-commit-config.yaml +0 -0
  43. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/.secrets.baseline +0 -0
  44. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/LICENSE +0 -0
  45. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/Makefile +0 -0
  46. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/codecov.yml +0 -0
  47. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/docs/configuration.md +0 -0
  48. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/docs/development/architecture.md +0 -0
  49. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/docs/development/contributing.md +0 -0
  50. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/docs/development/release-process.md +0 -0
  51. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/docs/development/testing.md +0 -0
  52. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/docs/export-import.md +0 -0
  53. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/docs/index.md +0 -0
  54. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/docs/installation.md +0 -0
  55. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/docs/media.md +0 -0
  56. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/docs/models.md +0 -0
  57. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/docs/quickstart.md +0 -0
  58. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/docs/stylesheets/extra.css +0 -0
  59. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/examples/MIGRATION_GUIDE.md +0 -0
  60. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/examples/async_operations.py +0 -0
  61. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/examples/config_di_demo.py +0 -0
  62. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/examples/export_import_with_media.py +0 -0
  63. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/examples/export_import_with_schemas.py +0 -0
  64. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/examples/verify_installation.py +0 -0
  65. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/mkdocs.yml +0 -0
  66. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/pyproject.toml +0 -0
  67. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/__init__.py +1 -1
  68. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/__version__.py +0 -0
  69. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/auth/__init__.py +0 -0
  70. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/auth/api_token.py +0 -0
  71. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/cache/__init__.py +0 -0
  72. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/cache/schema_cache.py +0 -0
  73. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/client/__init__.py +0 -0
  74. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/export/__init__.py +0 -0
  75. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/export/importer.py +0 -0
  76. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/export/media_handler.py +0 -0
  77. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/export/relation_resolver.py +0 -0
  78. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/models/bulk.py +0 -0
  79. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/models/config.py +0 -0
  80. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/models/enums.py +0 -0
  81. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/models/import_options.py +0 -0
  82. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/models/request/__init__.py +0 -0
  83. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/models/request/fields.py +0 -0
  84. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/models/request/filters.py +0 -0
  85. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/models/request/pagination.py +0 -0
  86. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/models/request/populate.py +0 -0
  87. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/models/request/sort.py +0 -0
  88. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/models/response/__init__.py +0 -0
  89. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/models/response/base.py +0 -0
  90. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/models/response/component.py +0 -0
  91. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/models/response/media.py +0 -0
  92. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/models/response/meta.py +0 -0
  93. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/models/response/normalized.py +0 -0
  94. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/models/response/relation.py +0 -0
  95. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/models/response/v4.py +0 -0
  96. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/models/response/v5.py +0 -0
  97. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/models/schema.py +0 -0
  98. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/operations/__init__.py +0 -0
  99. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/parsers/__init__.py +0 -0
  100. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/parsers/version_detecting.py +0 -0
  101. {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/protocols.py +0 -0
@@ -0,0 +1,54 @@
1
+ # Repository Guidelines
2
+
3
+ ## Project Structure & Module Organization
4
+ - `src/strapi_kit/`: library source (clients, models, query builder, migrations, etc.).
5
+ - `tests/`: automated tests organized by scope: `unit/`, `integration/`, `e2e/`.
6
+ - `docs/`: MkDocs documentation source; `site/` is generated output.
7
+ - `examples/`: runnable usage samples and migration scripts.
8
+
9
+ ## Build, Test, and Development Commands
10
+ ```bash
11
+ # Install (uv preferred)
12
+ uv pip install -e ".[dev]" # or: make install-dev
13
+
14
+ # Quality and tests
15
+ make test # pytest
16
+ make coverage # coverage report (htmlcov/)
17
+ make lint # ruff check
18
+ make format # ruff format
19
+ make type-check # mypy strict
20
+ make pre-commit # format + lint-fix + type-check + test
21
+
22
+ # Docs
23
+ make docs-serve # local docs at http://127.0.0.1:8000
24
+
25
+ # E2E (requires Strapi via Docker)
26
+ make e2e-setup # start Strapi
27
+ make e2e # run e2e tests
28
+ make e2e-teardown # stop Strapi
29
+ ```
30
+
31
+ ## Coding Style & Naming Conventions
32
+ - Python 3.12+, PEP 8, line length 100 (enforced by Ruff).
33
+ - Use `ruff format` for formatting and `ruff check` for linting.
34
+ - `mypy` runs in strict mode on `src/strapi_kit/`; type hints are expected for all functions.
35
+ - Prefer f-strings and Google-style docstrings.
36
+ - Use project exception types (e.g., from `strapi_kit.exceptions`) and chain errors with `raise ... from e`.
37
+
38
+ ## Testing Guidelines
39
+ - Frameworks: `pytest`, `pytest-asyncio` (auto mode), `respx` for HTTP mocking.
40
+ - Naming: `test_*.py`, `Test*` classes, `test_*` functions (per pytest config).
41
+ - Organize by scope: unit tests are fast/isolated; integration tests require a real Strapi instance; e2e tests are Docker-backed.
42
+ - Coverage target: 85%+ overall.
43
+
44
+ ## Commit & Pull Request Guidelines
45
+ - Recent history uses descriptive sentence-style messages (e.g., “Add…”, “Refactor…”), but the contributing guide requests **Conventional Commits**.
46
+ - Use conventional commit format in commits and PR titles, e.g. `feat(client): add retry logic`.
47
+ - PRs should describe what/why, link issues, note breaking changes, and include screenshots for UI changes.
48
+ - Expect automated review by CodeRabbit; ensure `pytest`, `mypy`, and `ruff` pass.
49
+
50
+ ## Security & Configuration
51
+ - Run security checks with `make security` (Bandit). Use `make security-baseline` to refresh `.secrets.baseline` when needed.
52
+
53
+ ## Agent-Specific Instructions
54
+ - If you are an AI assistant, consult `CLAUDE.md` and `LLM.md` for workflow conventions and architecture notes.
@@ -0,0 +1,81 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ### Fixed
11
+
12
+ - **Exception handling improvements** ([#23](https://github.com/MehdiZare/strapi-kit/pull/23))
13
+ - Use `StrapiError` instead of bare `Exception` in examples for precise error handling
14
+ - Catch `PydanticValidationError` specifically in Content-Type Builder parsing
15
+ - Add proper exception chaining when re-raising validation errors
16
+ - Fix docstring to document `ConfigurationError` instead of `ValueError`
17
+
18
+ - **Singularization bug fix** ([#23](https://github.com/MehdiZare/strapi-kit/pull/23))
19
+ - Fix `api_id_to_singular()` for `-zzes` endings: `quizzes` → `quiz`, `buzzes` → `buzz`
20
+ - Use length-based heuristic to distinguish single-z doubled vs double-z base words
21
+
22
+ ### Added
23
+
24
+ - **Content-Type Builder API** ([#15](https://github.com/MehdiZare/strapi-kit/issues/15))
25
+ - `get_content_types(include_plugins=False)` - List all content types from Strapi
26
+ - `get_components()` - List all components
27
+ - `get_content_type_schema(uid)` - Get full schema for a content type
28
+ - New models: `ContentTypeListItem`, `ComponentListItem`, `CTBContentTypeSchema`, `CTBContentTypeInfo`
29
+ - Schema helper methods: `get_field_type()`, `is_relation_field()`, `is_component_field()`, `get_relation_target()`, `get_component_uid()`
30
+ - Full async support for all methods
31
+
32
+ - **UID Conversion Utilities** ([#16](https://github.com/MehdiZare/strapi-kit/issues/16))
33
+ - `api_id_to_singular()` - Convert plural API IDs to singular form (handles irregular plurals like people→person, children→child)
34
+ - `uid_to_admin_url()` - Build Strapi admin panel URLs from content type UIDs
35
+ - `uid_to_api_id` - Alias for `uid_to_endpoint` for clarity
36
+ - Export of existing utilities: `extract_model_name()`, `is_api_content_type()`
37
+
38
+ - **SEO Configuration Detection** ([#17](https://github.com/MehdiZare/strapi-kit/issues/17))
39
+ - `detect_seo_configuration()` - Detect SEO setup in content type schemas
40
+ - `SEOConfiguration` dataclass for structured detection results
41
+ - Support for component-based SEO (shared.seo, meta, metadata)
42
+ - Support for flat SEO fields (metaTitle, meta_description, ogTitle, etc.)
43
+ - Case-insensitive matching for field names and component UIDs
44
+
45
+ ### Changed
46
+
47
+ - Test coverage increased from 85% to 86% (528 passing tests)
48
+ - Added 68 new tests for Content-Type Builder, UID utilities, and SEO detection
49
+
50
+ ## [0.0.2] - 2025-01-XX
51
+
52
+ ### Added
53
+
54
+ - Export/Import functionality with automatic relation resolution
55
+ - Schema caching for efficient content type metadata handling
56
+ - Media file export/download support
57
+ - Full migration examples (simple and production-ready)
58
+
59
+ ### Changed
60
+
61
+ - Improved test coverage to 85%
62
+
63
+ ## [0.0.1] - 2025-01-XX
64
+
65
+ ### Added
66
+
67
+ - Initial release
68
+ - HTTP clients (sync and async)
69
+ - Configuration with Pydantic and environment variable support
70
+ - Authentication (API tokens)
71
+ - Exception hierarchy with semantic error types
72
+ - API version detection (v4/v5)
73
+ - Type-safe query builder with 24 filter operators
74
+ - Response normalization for both Strapi v4 and v5
75
+ - Media upload/download operations
76
+ - Dependency injection support with protocols
77
+ - Full type hints and mypy strict mode compliance
78
+
79
+ [Unreleased]: https://github.com/MehdiZare/strapi-kit/compare/v0.0.2...HEAD
80
+ [0.0.2]: https://github.com/MehdiZare/strapi-kit/compare/v0.0.1...v0.0.2
81
+ [0.0.1]: https://github.com/MehdiZare/strapi-kit/releases/tag/v0.0.1
@@ -284,10 +284,11 @@ assert len(mock_http.requests) == 1
284
284
 
285
285
  ```
286
286
  StrapiError (base)
287
+ ├─ ConfigurationError (invalid config, missing token, bad URL)
287
288
  ├─ AuthenticationError (401)
288
289
  ├─ AuthorizationError (403)
289
290
  ├─ NotFoundError (404)
290
- ├─ ValidationError (400)
291
+ ├─ ValidationError (400, input validation)
291
292
  ├─ ConflictError (409)
292
293
  ├─ ServerError (5xx)
293
294
  ├─ NetworkError (connection issues)
@@ -302,6 +303,12 @@ StrapiError (base)
302
303
 
303
304
  **Usage pattern**: Always catch specific exceptions before generic ones. All exceptions carry optional `details: dict[str, Any]` for context.
304
305
 
306
+ **Non-HTTP exceptions**:
307
+ - `ConfigurationError`: Invalid API token, missing config, bad base URL
308
+ - `ValidationError`: Invalid query parameters (pagination, filters), invalid function arguments
309
+ - `FormatError`: Invalid export data format, path traversal prevention
310
+ - `MediaError`: File not found, upload/download failures, context manager misuse
311
+
305
312
  **HTTP mapping** (client/base.py:_handle_error_response):
306
313
  - 401 → AuthenticationError
307
314
  - 403 → AuthorizationError
@@ -0,0 +1,502 @@
1
+ # LLM Guide for strapi-kit
2
+
3
+ This guide helps AI coding agents work effectively with the strapi-kit Python package for Strapi CMS integration.
4
+
5
+ ## Reporting Issues
6
+
7
+ **If you encounter bugs, unexpected behavior, or errors while using strapi-kit, please file an issue:**
8
+
9
+ - **GitHub Issues**: https://github.com/MehdiZare/strapi-kit/issues
10
+
11
+ When filing an issue, include:
12
+ 1. Python version and strapi-kit version
13
+ 2. Strapi version (v4 or v5)
14
+ 3. Minimal code to reproduce the error
15
+ 4. Full error traceback
16
+ 5. Expected vs actual behavior
17
+
18
+ Filing issues helps improve the library for all users. Don't hesitate to report problems, even if you're unsure whether it's a bug or user error.
19
+
20
+ ## Package Overview
21
+
22
+ **strapi-kit** is a type-safe Python client for Strapi CMS with:
23
+ - Sync and async clients
24
+ - Automatic Strapi v4/v5 detection
25
+ - Type-safe query building with 24 filter operators
26
+ - Media upload/download
27
+ - Content export/import with relation resolution
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ pip install strapi-kit
33
+ ```
34
+
35
+ ## Quick Reference
36
+
37
+ ### Imports
38
+
39
+ ```python
40
+ # Core clients and config
41
+ from strapi_kit import SyncClient, AsyncClient, StrapiConfig
42
+
43
+ # Query building
44
+ from strapi_kit.models import StrapiQuery, FilterBuilder, SortDirection, Populate
45
+
46
+ # For SecretStr (API tokens)
47
+ from pydantic import SecretStr
48
+ ```
49
+
50
+ ### Basic Client Setup
51
+
52
+ ```python
53
+ from strapi_kit import SyncClient, StrapiConfig
54
+ from pydantic import SecretStr
55
+
56
+ config = StrapiConfig(
57
+ base_url="http://localhost:1337",
58
+ api_token=SecretStr("your-api-token"),
59
+ )
60
+
61
+ # Always use context manager
62
+ with SyncClient(config) as client:
63
+ response = client.get_many("articles")
64
+ ```
65
+
66
+ ### Async Client
67
+
68
+ ```python
69
+ from strapi_kit import AsyncClient, StrapiConfig
70
+
71
+ async with AsyncClient(config) as client:
72
+ response = await client.get_many("articles")
73
+ ```
74
+
75
+ ## CRUD Operations
76
+
77
+ ### Read
78
+
79
+ ```python
80
+ # Get many (returns NormalizedCollectionResponse)
81
+ response = client.get_many("articles")
82
+ for article in response.data:
83
+ print(article.id, article.attributes["title"])
84
+
85
+ # Get one (returns NormalizedSingleResponse)
86
+ response = client.get_one("articles/1")
87
+ article = response.data
88
+ print(article.attributes["title"])
89
+
90
+ # Raw API (returns dict)
91
+ response = client.get("articles") # dict
92
+ ```
93
+
94
+ ### Create
95
+
96
+ ```python
97
+ data = {"title": "New Article", "content": "Body text"}
98
+ response = client.create("articles", data)
99
+ new_id = response.data.id
100
+ ```
101
+
102
+ ### Update
103
+
104
+ ```python
105
+ data = {"title": "Updated Title"}
106
+ response = client.update("articles/1", data)
107
+ ```
108
+
109
+ ### Delete
110
+
111
+ ```python
112
+ response = client.remove("articles/1")
113
+ ```
114
+
115
+ ## Query Building
116
+
117
+ ### Filters
118
+
119
+ ```python
120
+ from strapi_kit.models import StrapiQuery, FilterBuilder
121
+
122
+ # Basic equality
123
+ query = StrapiQuery().filter(FilterBuilder().eq("status", "published"))
124
+
125
+ # Multiple conditions (AND)
126
+ query = StrapiQuery().filter(
127
+ FilterBuilder()
128
+ .eq("status", "published")
129
+ .gt("views", 100)
130
+ .contains("title", "Python")
131
+ )
132
+
133
+ # OR conditions
134
+ query = StrapiQuery().filter(
135
+ FilterBuilder()
136
+ .eq("status", "published")
137
+ .or_group(
138
+ FilterBuilder().gt("views", 1000),
139
+ FilterBuilder().gt("likes", 500)
140
+ )
141
+ )
142
+
143
+ # Available operators
144
+ # eq, ne, gt, gte, lt, lte, contains, not_contains,
145
+ # starts_with, ends_with, in_, not_in, null, not_null,
146
+ # between, is_empty, is_not_empty
147
+ ```
148
+
149
+ ### Sorting
150
+
151
+ ```python
152
+ from strapi_kit.models import StrapiQuery, SortDirection
153
+
154
+ query = (StrapiQuery()
155
+ .sort_by("publishedAt", SortDirection.DESC)
156
+ .then_sort_by("title", SortDirection.ASC))
157
+ ```
158
+
159
+ ### Pagination
160
+
161
+ ```python
162
+ # Page-based
163
+ query = StrapiQuery().paginate(page=1, page_size=25)
164
+
165
+ # Offset-based
166
+ query = StrapiQuery().paginate(start=0, limit=50)
167
+ ```
168
+
169
+ ### Population (Relations)
170
+
171
+ ```python
172
+ from strapi_kit.models import StrapiQuery, Populate
173
+
174
+ # Populate all
175
+ query = StrapiQuery().populate_all()
176
+
177
+ # Specific fields
178
+ query = StrapiQuery().populate_fields(["author", "category"])
179
+
180
+ # Advanced with nested
181
+ query = StrapiQuery().populate(
182
+ Populate()
183
+ .add_field("author", fields=["name", "email"])
184
+ .add_field("comments", nested=Populate().add_field("author"))
185
+ )
186
+ ```
187
+
188
+ ### Complete Query Example
189
+
190
+ ```python
191
+ query = (StrapiQuery()
192
+ .filter(FilterBuilder().eq("status", "published").gt("views", 100))
193
+ .sort_by("publishedAt", SortDirection.DESC)
194
+ .paginate(page=1, page_size=20)
195
+ .populate_fields(["author", "category"])
196
+ .select(["title", "slug", "publishedAt"]))
197
+
198
+ response = client.get_many("articles", query=query)
199
+ ```
200
+
201
+ ## Media Operations
202
+
203
+ ### Upload
204
+
205
+ ```python
206
+ # Single file
207
+ media = client.upload_file(
208
+ "image.jpg",
209
+ alternative_text="Alt text",
210
+ caption="Caption"
211
+ )
212
+ print(media.id, media.url)
213
+
214
+ # Attach to entity
215
+ media = client.upload_file(
216
+ "cover.jpg",
217
+ ref="api::article.article",
218
+ ref_id="abc123",
219
+ field="cover"
220
+ )
221
+
222
+ # Multiple files
223
+ media_list = client.upload_files(["img1.jpg", "img2.jpg"])
224
+ ```
225
+
226
+ ### Download
227
+
228
+ ```python
229
+ # Get bytes
230
+ content = client.download_file("/uploads/image.jpg")
231
+
232
+ # Save to file
233
+ client.download_file("/uploads/image.jpg", save_path="local.jpg")
234
+ ```
235
+
236
+ ### Manage
237
+
238
+ ```python
239
+ # List media
240
+ response = client.list_media()
241
+
242
+ # Get specific
243
+ media = client.get_media(42)
244
+
245
+ # Update metadata
246
+ client.update_media(42, alternative_text="New alt text")
247
+
248
+ # Delete
249
+ client.delete_media(42)
250
+ ```
251
+
252
+ ## Export/Import
253
+
254
+ ```python
255
+ from strapi_kit import StrapiExporter, StrapiImporter
256
+
257
+ # Export
258
+ with SyncClient(source_config) as client:
259
+ exporter = StrapiExporter(client)
260
+ export_data = exporter.export_content_types([
261
+ "api::article.article",
262
+ "api::author.author"
263
+ ])
264
+ exporter.save_to_file(export_data, "backup.json")
265
+
266
+ # Import
267
+ with SyncClient(target_config) as client:
268
+ importer = StrapiImporter(client)
269
+ export_data = StrapiExporter.load_from_file("backup.json")
270
+ result = importer.import_data(export_data)
271
+ print(f"Imported {result.entities_imported} entities")
272
+ ```
273
+
274
+ ## Error Handling
275
+
276
+ ```python
277
+ from strapi_kit.exceptions import (
278
+ StrapiError, # Base exception
279
+ AuthenticationError, # 401
280
+ AuthorizationError, # 403
281
+ NotFoundError, # 404
282
+ ValidationError, # 400
283
+ ServerError, # 5xx
284
+ NetworkError, # Connection issues
285
+ )
286
+
287
+ try:
288
+ response = client.get_one("articles/999")
289
+ except NotFoundError:
290
+ print("Article not found")
291
+ except AuthenticationError:
292
+ print("Invalid API token")
293
+ except StrapiError as e:
294
+ print(f"Strapi error: {e}")
295
+ ```
296
+
297
+ ## Response Structure
298
+
299
+ ### NormalizedEntity (from get_one, get_many)
300
+
301
+ ```python
302
+ response = client.get_one("articles/1")
303
+ entity = response.data
304
+
305
+ entity.id # int - Entity ID
306
+ entity.document_id # str | None - v5 documentId (None for v4)
307
+ entity.attributes # dict - Custom fields {"title": "...", "content": "..."}
308
+ entity.published_at # datetime | None
309
+ entity.created_at # datetime | None
310
+ entity.updated_at # datetime | None
311
+ entity.locale # str | None
312
+ ```
313
+
314
+ ### Pagination Metadata
315
+
316
+ ```python
317
+ response = client.get_many("articles", query)
318
+
319
+ response.meta.pagination.page # Current page
320
+ response.meta.pagination.page_size # Items per page
321
+ response.meta.pagination.page_count # Total pages
322
+ response.meta.pagination.total # Total items
323
+ ```
324
+
325
+ ## Configuration Options
326
+
327
+ ```python
328
+ from strapi_kit import StrapiConfig, RetryConfig
329
+
330
+ config = StrapiConfig(
331
+ base_url="http://localhost:1337", # Required
332
+ api_token=SecretStr("token"), # Required
333
+ api_version="auto", # "auto" | "v4" | "v5"
334
+ timeout=30.0, # Request timeout (seconds)
335
+ max_connections=10, # Connection pool size
336
+ verify_ssl=True, # SSL verification
337
+ retry=RetryConfig(
338
+ max_attempts=3, # Retry count
339
+ initial_wait=1.0, # First retry delay
340
+ max_wait=60.0, # Max retry delay
341
+ retry_on_status={500, 502, 503, 504},
342
+ ),
343
+ )
344
+ ```
345
+
346
+ ## Environment Variables
347
+
348
+ ```bash
349
+ STRAPI_BASE_URL=http://localhost:1337
350
+ STRAPI_API_TOKEN=your-token
351
+ STRAPI_TIMEOUT=30.0
352
+ STRAPI_MAX_CONNECTIONS=10
353
+ ```
354
+
355
+ ```python
356
+ from strapi_kit import load_config
357
+
358
+ config = load_config() # Auto-loads from .env or environment
359
+ ```
360
+
361
+ ## Common Patterns
362
+
363
+ ### Content Type UID to Endpoint
364
+
365
+ ```python
366
+ def uid_to_endpoint(uid: str) -> str:
367
+ """Convert 'api::article.article' to 'articles'.
368
+
369
+ Also handles plugin UIDs like 'plugin::users-permissions.user' -> 'users'.
370
+ """
371
+ parts = uid.split("::")
372
+ if len(parts) == 2:
373
+ # Extract content name from after the dot
374
+ name_parts = parts[1].split(".")
375
+ name = name_parts[1] if len(name_parts) > 1 else name_parts[0]
376
+ # Pluralize
377
+ if name.endswith("y") and not name.endswith(("ay", "ey", "oy", "uy")):
378
+ return name[:-1] + "ies"
379
+ if name.endswith(("s", "x", "z", "ch", "sh")):
380
+ return name + "es"
381
+ if not name.endswith("s"):
382
+ return name + "s"
383
+ return name
384
+ return uid
385
+ ```
386
+
387
+ ### Iterate All Pages
388
+
389
+ ```python
390
+ page = 1
391
+ while True:
392
+ query = StrapiQuery().paginate(page=page, page_size=100)
393
+ response = client.get_many("articles", query=query)
394
+
395
+ for item in response.data:
396
+ process(item)
397
+
398
+ if page >= response.meta.pagination.page_count:
399
+ break
400
+ page += 1
401
+ ```
402
+
403
+ ### Check API Version
404
+
405
+ ```python
406
+ with SyncClient(config) as client:
407
+ # Make a request first to trigger detection
408
+ client.get_many("articles", query=StrapiQuery().paginate(1, 1))
409
+
410
+ if client.api_version == "v5":
411
+ # Use documentId
412
+ pass
413
+ else:
414
+ # Use numeric id
415
+ pass
416
+ ```
417
+
418
+ ## Key Files in Codebase
419
+
420
+ | Path | Purpose |
421
+ |------|---------|
422
+ | `src/strapi_kit/client/sync_client.py` | Synchronous client |
423
+ | `src/strapi_kit/client/async_client.py` | Async client |
424
+ | `src/strapi_kit/client/base.py` | Shared client logic |
425
+ | `src/strapi_kit/models/request/query.py` | StrapiQuery builder |
426
+ | `src/strapi_kit/models/request/filters.py` | FilterBuilder |
427
+ | `src/strapi_kit/models/response/normalized.py` | Response models |
428
+ | `src/strapi_kit/operations/media.py` | Media utilities |
429
+ | `src/strapi_kit/exceptions/errors.py` | Exception hierarchy |
430
+ | `src/strapi_kit/models/config.py` | Configuration models |
431
+
432
+ ## Testing
433
+
434
+ ```python
435
+ import pytest
436
+ from strapi_kit import SyncClient, StrapiConfig
437
+ from pydantic import SecretStr
438
+
439
+ @pytest.fixture
440
+ def config():
441
+ return StrapiConfig(
442
+ base_url="http://localhost:1337",
443
+ api_token=SecretStr("test-token"),
444
+ )
445
+
446
+ # Use respx for HTTP mocking
447
+ import respx
448
+ import httpx
449
+
450
+ @respx.mock
451
+ def test_get_articles(config):
452
+ respx.get("http://localhost:1337/api/articles").mock(
453
+ return_value=httpx.Response(200, json={
454
+ "data": [{"id": 1, "attributes": {"title": "Test"}}],
455
+ "meta": {"pagination": {"page": 1, "pageSize": 25, "total": 1}}
456
+ })
457
+ )
458
+
459
+ with SyncClient(config) as client:
460
+ response = client.get_many("articles")
461
+ assert len(response.data) == 1
462
+ ```
463
+
464
+ ## Examples
465
+
466
+ Complete working examples are available in the `examples/` directory:
467
+
468
+ | File | Description |
469
+ |------|-------------|
470
+ | [`examples/basic_crud.py`](examples/basic_crud.py) | Basic CRUD operations (create, read, update, delete) |
471
+ | [`examples/simple_migration.py`](examples/simple_migration.py) | Simple content migration between Strapi instances |
472
+ | [`examples/full_migration_v5.py`](examples/full_migration_v5.py) | Production-ready migration with auto-discovery |
473
+ | [`examples/MIGRATION_GUIDE.md`](examples/MIGRATION_GUIDE.md) | Comprehensive migration documentation |
474
+
475
+ ### Running Examples
476
+
477
+ ```bash
478
+ # Set environment variables for migration examples
479
+ export SOURCE_STRAPI_TOKEN='your-source-token'
480
+ export TARGET_STRAPI_TOKEN='your-target-token'
481
+
482
+ # Run basic CRUD
483
+ python examples/basic_crud.py
484
+
485
+ # Run simple migration
486
+ python examples/simple_migration.py
487
+
488
+ # Run full migration
489
+ python examples/full_migration_v5.py migrate
490
+ ```
491
+
492
+ ## Tips for LLM Agents
493
+
494
+ 1. **Always use context managers** (`with` / `async with`) for clients
495
+ 2. **Use typed methods** (`get_many`, `create`) over raw methods (`get`, `post`)
496
+ 3. **Build queries incrementally** - chain methods on `StrapiQuery()`
497
+ 4. **Handle errors specifically** - catch `NotFoundError` before `StrapiError`
498
+ 5. **Check response.data** - it's `None` for 404 on `get_one`
499
+ 6. **API prefix is automatic** - use `"articles"` not `"/api/articles"`
500
+ 7. **SecretStr for tokens** - always wrap API tokens in `SecretStr`
501
+ 8. **v4 vs v5** - use `document_id` for v5, `id` works for both
502
+ 9. **File issues** - if something doesn't work as expected, file an issue at https://github.com/MehdiZare/strapi-kit/issues