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.
- strapi_kit-0.0.4/AGENTS.md +54 -0
- strapi_kit-0.0.4/CHANGELOG.md +81 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/CLAUDE.md +8 -1
- strapi_kit-0.0.4/LLM.md +502 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/PKG-INFO +204 -10
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/README.md +203 -9
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/docs/changelog.md +20 -1
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/examples/basic_crud.py +13 -6
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/examples/full_migration_v5.py +90 -33
- strapi_kit-0.0.4/examples/simple_migration.py +223 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/_version.py +2 -2
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/client/async_client.py +87 -1
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/client/base.py +99 -3
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/client/sync_client.py +87 -1
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/config_provider.py +1 -16
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/exceptions/__init__.py +2 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/exceptions/errors.py +13 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/export/exporter.py +6 -5
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/models/__init__.py +8 -0
- strapi_kit-0.0.4/src/strapi_kit/models/content_type.py +148 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/models/export_format.py +5 -3
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/models/request/query.py +9 -6
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/operations/media.py +13 -6
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/operations/streaming.py +5 -4
- strapi_kit-0.0.4/src/strapi_kit/utils/__init__.py +34 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/utils/rate_limiter.py +4 -2
- strapi_kit-0.0.4/src/strapi_kit/utils/seo.py +294 -0
- strapi_kit-0.0.4/src/strapi_kit/utils/uid.py +217 -0
- strapi_kit-0.0.2/examples/simple_migration.py +0 -90
- strapi_kit-0.0.2/src/strapi_kit/utils/__init__.py +0 -15
- strapi_kit-0.0.2/src/strapi_kit/utils/uid.py +0 -88
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/.coderabbit.yaml +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/.env.example +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/.github/dependabot.yml +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/.github/pull_request_template.md +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/.github/workflows/ci.yml +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/.github/workflows/dev-release.yml +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/.github/workflows/document.yml +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/.github/workflows/guard-main-origin.yml +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/.github/workflows/release.yml +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/.gitignore +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/.pre-commit-config.yaml +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/.secrets.baseline +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/LICENSE +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/Makefile +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/codecov.yml +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/docs/configuration.md +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/docs/development/architecture.md +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/docs/development/contributing.md +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/docs/development/release-process.md +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/docs/development/testing.md +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/docs/export-import.md +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/docs/index.md +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/docs/installation.md +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/docs/media.md +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/docs/models.md +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/docs/quickstart.md +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/docs/stylesheets/extra.css +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/examples/MIGRATION_GUIDE.md +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/examples/async_operations.py +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/examples/config_di_demo.py +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/examples/export_import_with_media.py +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/examples/export_import_with_schemas.py +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/examples/verify_installation.py +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/mkdocs.yml +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/pyproject.toml +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/__init__.py +1 -1
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/__version__.py +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/auth/__init__.py +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/auth/api_token.py +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/cache/__init__.py +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/cache/schema_cache.py +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/client/__init__.py +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/export/__init__.py +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/export/importer.py +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/export/media_handler.py +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/export/relation_resolver.py +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/models/bulk.py +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/models/config.py +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/models/enums.py +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/models/import_options.py +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/models/request/__init__.py +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/models/request/fields.py +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/models/request/filters.py +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/models/request/pagination.py +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/models/request/populate.py +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/models/request/sort.py +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/models/response/__init__.py +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/models/response/base.py +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/models/response/component.py +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/models/response/media.py +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/models/response/meta.py +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/models/response/normalized.py +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/models/response/relation.py +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/models/response/v4.py +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/models/response/v5.py +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/models/schema.py +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/operations/__init__.py +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/parsers/__init__.py +0 -0
- {strapi_kit-0.0.2 → strapi_kit-0.0.4}/src/strapi_kit/parsers/version_detecting.py +0 -0
- {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
|
strapi_kit-0.0.4/LLM.md
ADDED
|
@@ -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
|