A modern Python client for Strapi CMS with comprehensive import/export capabilities.
- 🚀 Full Strapi Support: Works with both v4 and v5 APIs with automatic version detection
- ⚡ Async & Sync: Choose between synchronous and asynchronous clients based on your needs
- 🔒 Type Safe: Built with Pydantic for robust data validation and type safety
- 🔄 Import/Export: Comprehensive backup/restore and data migration tools
- 🔁 Smart Retry: Automatic retry with exponential backoff for transient failures
- 🔍 Schema Introspection: Content-Type Builder API support for schema discovery
- 📦 Modern Python: Built for Python 3.12+ with full type hints
pip install strapi-kitOr with uv (recommended for faster installs):
uv pip install strapi-kitFor development:
# With pip
pip install -e ".[dev]"
# With uv (recommended)
uv pip install -e ".[dev]"The typed API provides full type safety, IDE autocomplete, and automatic v4/v5 normalization:
from strapi_kit import SyncClient, StrapiConfig
from strapi_kit.models import StrapiQuery, FilterBuilder, SortDirection
config = StrapiConfig(
base_url="http://localhost:1337",
api_token="your-api-token"
)
with SyncClient(config) as client:
# Build a type-safe query
query = (StrapiQuery()
.filter(FilterBuilder()
.eq("status", "published")
.gt("views", 100))
.sort_by("publishedAt", SortDirection.DESC)
.paginate(page=1, page_size=25)
.populate_fields(["author", "category"]))
# Get normalized, type-safe response
response = client.get_many("articles", query=query)
# Works with both v4 and v5 automatically!
for article in response.data:
print(f"{article.id}: {article.attributes['title']}")
print(f"Published: {article.published_at}")The raw API returns dictionaries directly from Strapi:
from strapi_kit import SyncClient, StrapiConfig
config = StrapiConfig(
base_url="http://localhost:1337",
api_token="your-api-token"
)
with SyncClient(config) as client:
# Get raw JSON response
response = client.get("articles")
print(response) # dictBoth typed and raw APIs work with async:
import asyncio
from strapi_kit import AsyncClient, StrapiConfig
from strapi_kit.models import StrapiQuery, FilterBuilder
async def main():
config = StrapiConfig(
base_url="http://localhost:1337",
api_token="your-api-token"
)
async with AsyncClient(config) as client:
# Typed API
query = StrapiQuery().filter(FilterBuilder().eq("status", "published"))
response = await client.get_many("articles", query=query)
for article in response.data:
print(article.attributes["title"])
asyncio.run(main())strapi-kit provides flexible configuration options through dependency injection:
Create a .env file in your project root:
# .env
STRAPI_BASE_URL=http://localhost:1337
STRAPI_API_TOKEN=your-api-token-here
STRAPI_TIMEOUT=30.0
STRAPI_MAX_CONNECTIONS=10
STRAPI_RETRY_MAX_ATTEMPTS=3Then load it automatically:
from strapi_kit import load_config, SyncClient
# Automatically searches for .env, .env.local, or ~/.config/strapi/.env
config = load_config()
with SyncClient(config) as client:
response = client.get("articles")Perfect for containerized deployments (Docker, Kubernetes):
export STRAPI_BASE_URL=https://api.production.com
export STRAPI_API_TOKEN=production-secret-token
export STRAPI_TIMEOUT=120.0
export STRAPI_MAX_CONNECTIONS=100from strapi_kit import ConfigFactory, SyncClient
# Load from environment variables only (no .env files)
config = ConfigFactory.from_environment_only()
with SyncClient(config) as client:
response = client.get("articles")Create configuration programmatically:
from strapi_kit import create_config, SyncClient
config = create_config(
base_url="http://localhost:1337",
api_token="your-token",
timeout=60.0,
max_connections=50,
verify_ssl=True
)
with SyncClient(config) as client:
response = client.get("articles")from strapi_kit import ConfigFactory
# Load from specific file
config = ConfigFactory.from_env_file("/path/to/custom.env")
# Search multiple locations
config = ConfigFactory.from_env(
search_paths=[
".env.local", # Local overrides (highest priority)
".env", # Base config
"~/.strapi/.env" # User config (lowest priority)
]
)from strapi_kit import ConfigFactory
# Base configuration from .env file
base_config = ConfigFactory.from_env_file(".env")
# Override specific values for production
production_overrides = ConfigFactory.from_dict({
"base_url": "https://api.production.com",
"api_token": "production-token",
"timeout": 120.0,
"max_connections": 100
})
# Merge configs (later configs override earlier ones)
final_config = ConfigFactory.merge(base_config, production_overrides)Configure automatic retry behavior:
from strapi_kit import StrapiConfig, RetryConfig
config = StrapiConfig(
base_url="http://localhost:1337",
api_token="your-token",
retry=RetryConfig(
max_attempts=5, # Retry up to 5 times
initial_wait=2.0, # Wait 2 seconds before first retry
max_wait=120.0, # Maximum 2 minutes between retries
exponential_base=3.0, # Faster backoff growth
retry_on_status={500, 502, 503, 504, 408} # Retry on these status codes
)
)Or via environment variables:
STRAPI_RETRY_MAX_ATTEMPTS=5
STRAPI_RETRY_INITIAL_WAIT=2.0
STRAPI_RETRY_MAX_WAIT=120.0
STRAPI_RETRY_EXPONENTIAL_BASE=3.0All available options:
| Option | Type | Default | Description |
|---|---|---|---|
base_url |
str |
Required | Strapi instance URL |
api_token |
str |
Required | API authentication token |
api_version |
"v4" | "v5" | "auto" |
"auto" |
API version (auto-detect or explicit) |
timeout |
float |
30.0 |
Request timeout in seconds |
max_connections |
int |
10 |
Maximum concurrent connections |
verify_ssl |
bool |
True |
Verify SSL certificates |
rate_limit_per_second |
float | None |
None |
Rate limiting (None = unlimited) |
retry.max_attempts |
int |
3 |
Maximum retry attempts (1-10) |
retry.initial_wait |
float |
1.0 |
Initial retry wait time (seconds) |
retry.max_wait |
float |
60.0 |
Maximum retry wait time (seconds) |
retry.exponential_base |
float |
2.0 |
Exponential backoff multiplier |
Use the FilterBuilder to create complex filters with 24 operators:
from strapi_kit.models import StrapiQuery, FilterBuilder
# Simple equality
query = StrapiQuery().filter(FilterBuilder().eq("status", "published"))
# Comparison operators
query = StrapiQuery().filter(
FilterBuilder()
.gt("views", 100)
.lte("price", 50)
)
# String matching
query = StrapiQuery().filter(
FilterBuilder()
.contains("title", "Python")
.starts_with("slug", "blog-")
)
# Array operators
query = StrapiQuery().filter(
FilterBuilder().in_("category", ["tech", "science"])
)
# Logical operators (AND, OR, NOT)
query = StrapiQuery().filter(
FilterBuilder()
.eq("status", "published")
.or_group(
FilterBuilder().gt("views", 1000),
FilterBuilder().gt("likes", 500)
)
)
# Deep relation filtering
query = StrapiQuery().filter(
FilterBuilder()
.eq("author.name", "John Doe")
.eq("author.country", "USA")
)Sort by one or multiple fields:
from strapi_kit.models import StrapiQuery, SortDirection
# Single field
query = StrapiQuery().sort_by("publishedAt", SortDirection.DESC)
# Multiple fields
query = (StrapiQuery()
.sort_by("status", SortDirection.ASC)
.then_sort_by("publishedAt", SortDirection.DESC)
.then_sort_by("title", SortDirection.ASC))
# Sort by relation field
query = StrapiQuery().sort_by("author.name", SortDirection.ASC)Choose between page-based or offset-based pagination:
from strapi_kit.models import StrapiQuery
# Page-based pagination
query = StrapiQuery().paginate(page=1, page_size=25)
# Offset-based pagination
query = StrapiQuery().paginate(start=0, limit=50)
# Disable count for performance
query = StrapiQuery().paginate(page=1, page_size=100, with_count=False)Expand relations, components, and dynamic zones:
from strapi_kit.models import StrapiQuery, Populate, FilterBuilder, SortDirection
# Populate all relations
query = StrapiQuery().populate_all()
# Populate specific fields
query = StrapiQuery().populate_fields(["author", "category", "tags"])
# Advanced population with filtering and field selection
query = StrapiQuery().populate(
Populate()
.add_field("author", fields=["name", "email", "avatar"])
.add_field("category")
.add_field("comments",
filters=FilterBuilder().eq("approved", True),
sort=Sort().by_field("createdAt", SortDirection.DESC),
fields=["content", "author"])
)
# Nested population
query = StrapiQuery().populate(
Populate().add_field(
"author",
nested=Populate().add_field("profile")
)
)Select specific fields to reduce payload size:
from strapi_kit.models import StrapiQuery
query = StrapiQuery().select(["title", "description", "publishedAt"])For i18n and draft/publish workflows:
from strapi_kit.models import StrapiQuery, PublicationState
# Set locale
query = StrapiQuery().with_locale("fr")
# Set publication state
query = StrapiQuery().with_publication_state(PublicationState.LIVE)Combine all features for complex queries:
from strapi_kit import SyncClient, StrapiConfig
from strapi_kit.models import (
StrapiQuery,
FilterBuilder,
SortDirection,
Populate,
PublicationState,
)
config = StrapiConfig(
base_url="http://localhost:1337",
api_token="your-token"
)
with SyncClient(config) as client:
# Build complex query
query = (StrapiQuery()
# Filters
.filter(FilterBuilder()
.eq("status", "published")
.gte("publishedAt", "2024-01-01")
.null("deletedAt")
.or_group(
FilterBuilder().contains("title", "Python"),
FilterBuilder().contains("title", "Django")
))
# Sorting
.sort_by("publishedAt", SortDirection.DESC)
.then_sort_by("views", SortDirection.DESC)
# Pagination
.paginate(page=1, page_size=20)
# Population
.populate(Populate()
.add_field("author", fields=["name", "avatar", "bio"])
.add_field("category")
.add_field("comments",
filters=FilterBuilder().eq("approved", True)))
# Field selection
.select(["title", "slug", "excerpt", "coverImage", "publishedAt"])
# Locale & publication
.with_locale("en")
.with_publication_state(PublicationState.LIVE))
# Execute query with type-safe response
response = client.get_many("articles", query=query)
# Access normalized data (works with both v4 and v5!)
print(f"Total articles: {response.meta.pagination.total}")
print(f"Page {response.meta.pagination.page} of {response.meta.pagination.page_count}")
for article in response.data:
# All responses are normalized to the same structure
print(f"ID: {article.id}")
print(f"Document ID: {article.document_id}") # v5 only, None for v4
print(f"Title: {article.attributes['title']}")
print(f"Published: {article.published_at}")
print("---")Create, read, update, and delete entities:
from strapi_kit import SyncClient, StrapiConfig
config = StrapiConfig(base_url="http://localhost:1337", api_token="your-token")
with SyncClient(config) as client:
# Create
data = {"title": "New Article", "content": "Article body"}
response = client.create("articles", data)
created_id = response.data.id
# Read one
response = client.get_one(f"articles/{created_id}")
article = response.data
# Read many
response = client.get_many("articles")
all_articles = response.data
# Update
data = {"title": "Updated Title"}
response = client.update(f"articles/{created_id}", data)
# Delete
response = client.remove(f"articles/{created_id}")Upload, download, and manage media files in Strapi's media library:
from strapi_kit import SyncClient, StrapiConfig
from strapi_kit.models import StrapiQuery, FilterBuilder
config = StrapiConfig(base_url="http://localhost:1337", api_token="your-token")
with SyncClient(config) as client:
# Upload a file
media = client.upload_file(
"hero-image.jpg",
alternative_text="Hero image",
caption="Main article hero image"
)
print(f"Uploaded: {media.name} (ID: {media.id})")
print(f"URL: {media.url}")
# Upload and attach to an entity
cover = client.upload_file(
"cover.jpg",
ref="api::article.article",
ref_id="abc123", # Article documentId or numeric ID
field="cover"
)
# Upload multiple files
files = ["image1.jpg", "image2.jpg", "image3.jpg"]
media_list = client.upload_files(files, folder="gallery")
print(f"Uploaded {len(media_list)} files")
# List media library
response = client.list_media()
for item in response.data:
print(f"{item.attributes['name']}: {item.attributes['url']}")
# List with filters
query = (StrapiQuery()
.filter(FilterBuilder().eq("mime", "image/jpeg"))
.paginate(page=1, page_size=10))
response = client.list_media(query)
# Get specific media details
media = client.get_media(42)
print(f"Name: {media.name}, Size: {media.size} KB")
# Download a file
content = client.download_file(media.url)
print(f"Downloaded {len(content)} bytes")
# Download and save
client.download_file(
media.url,
save_path="downloaded_image.jpg"
)
# Update media metadata
updated = client.update_media(
42,
alternative_text="Updated alt text",
caption="Updated caption"
)
# Delete media
client.delete_media(42)Async version:
import asyncio
from strapi_kit import AsyncClient, StrapiConfig
async def main():
config = StrapiConfig(base_url="http://localhost:1337", api_token="your-token")
async with AsyncClient(config) as client:
# All methods have async equivalents
media = await client.upload_file("image.jpg")
content = await client.download_file(media.url)
await client.delete_media(media.id)
asyncio.run(main())Media Features:
- Upload single or multiple files
- Attach uploads to specific entities (articles, pages, etc.)
- Set metadata (alt text, captions)
- Download with streaming for large files
- Query media library with filters
- Update metadata without re-uploading
- Full support for both sync and async
Query Strapi's Content-Type Builder to discover schemas, content types, and components:
from strapi_kit import SyncClient, StrapiConfig
config = StrapiConfig(base_url="http://localhost:1337", api_token="your-token")
with SyncClient(config) as client:
# List all content types (excludes plugins by default)
content_types = client.get_content_types()
for ct in content_types:
print(f"{ct.uid}: {ct.info.display_name}")
# api::article.article: Article
# api::category.category: Category
# Include plugin content types
all_types = client.get_content_types(include_plugins=True)
# List all components
components = client.get_components()
for comp in components:
print(f"{comp.category}/{comp.uid}: {comp.info.display_name}")
# shared/shared.seo: SEO
# blocks/blocks.hero: Hero Section
# Get full schema for a content type
schema = client.get_content_type_schema("api::article.article")
print(f"Display name: {schema.display_name}")
print(f"Plural name: {schema.plural_name}")
# Check field types
print(schema.get_field_type("title")) # "string"
print(schema.is_relation_field("author")) # True
print(schema.get_relation_target("author")) # "api::author.author"
# Check for components
print(schema.is_component_field("seo")) # True
print(schema.get_component_uid("seo")) # "shared.seo"Async version:
async with AsyncClient(config) as client:
content_types = await client.get_content_types()
components = await client.get_components()
schema = await client.get_content_type_schema("api::article.article")Utility functions for working with Strapi content type UIDs:
from strapi_kit.utils import (
uid_to_endpoint,
uid_to_api_id, # Alias for uid_to_endpoint
api_id_to_singular,
uid_to_admin_url,
extract_model_name,
is_api_content_type,
)
# Convert UID to API endpoint (pluralized)
uid_to_endpoint("api::article.article") # "articles"
uid_to_endpoint("api::category.category") # "categories"
uid_to_endpoint("api::class.class") # "classes"
# Convert plural API ID to singular
api_id_to_singular("articles") # "article"
api_id_to_singular("categories") # "category"
api_id_to_singular("quizzes") # "quiz" (handles -zzes endings)
api_id_to_singular("people") # "person" (handles irregular plurals)
api_id_to_singular("children") # "child"
# Build admin panel URL
uid_to_admin_url("api::article.article", "http://localhost:1337")
# "http://localhost:1337/admin/content-manager/collection-types/api::article.article"
uid_to_admin_url("api::homepage.homepage", "http://localhost:1337", kind="singleType")
# "http://localhost:1337/admin/content-manager/single-types/api::homepage.homepage"
# Extract model name from UID
extract_model_name("api::article.article") # "article"
extract_model_name("plugin::users-permissions.user") # "user"
# Check if UID is an API content type
is_api_content_type("api::article.article") # True
is_api_content_type("plugin::users-permissions.user") # FalseDetect SEO configuration patterns in content type schemas:
from strapi_kit.utils import detect_seo_configuration, SEOConfiguration
# Detect SEO in a schema dict
schema = {
"uid": "api::article.article",
"attributes": {
"title": {"type": "string"},
"seo": {"type": "component", "component": "shared.seo"},
}
}
config = detect_seo_configuration(schema)
print(config.has_seo) # True
print(config.seo_type) # "component"
print(config.seo_field_name) # "seo"
print(config.seo_component_uid) # "shared.seo"
print(config.fields) # {"title": "seo.metaTitle", "description": "seo.metaDescription", ...}
# Also detects flat SEO fields
schema_flat = {
"uid": "api::page.page",
"attributes": {
"metaTitle": {"type": "string"},
"metaDescription": {"type": "text"},
"ogImage": {"type": "media"},
}
}
config = detect_seo_configuration(schema_flat)
print(config.has_seo) # True
print(config.seo_type) # "flat"
print(config.fields) # {"title": "metaTitle", "description": "metaDescription", "og_image": "ogImage"}Supported SEO patterns:
- Component-based: Fields named
seo,meta,metadatawith typecomponent - Component UIDs: Components with
seoin the UID (e.g.,shared.seo,custom.page-seo) - Flat fields:
metaTitle,meta_title,seoTitle,metaDescription,ogTitle,canonicalUrl,noIndex, etc.
strapi-kit provides comprehensive export/import functionality with automatic relation resolution for migrating content between Strapi instances.
from strapi_kit import StrapiConfig, StrapiExporter, StrapiImporter, SyncClient
# Export from source instance
source_config = StrapiConfig(
base_url="http://localhost:1337",
api_token="source-token"
)
with SyncClient(source_config) as client:
exporter = StrapiExporter(client)
# Export content types with schemas for relation resolution
export_data = exporter.export_content_types([
"api::article.article",
"api::author.author",
"api::category.category"
])
# Save to file
exporter.save_to_file(export_data, "migration.json")
# Import to target instance
target_config = StrapiConfig(
base_url="http://localhost:1338",
api_token="target-token"
)
with SyncClient(target_config) as client:
importer = StrapiImporter(client)
# Load export
export_data = StrapiExporter.load_from_file("migration.json")
# Import with automatic relation resolution
result = importer.import_data(export_data)
print(f"Imported {result.entities_imported} entities")
print(f"ID mapping: {result.id_mapping}")Export/Import Features:
- Automatic Relation Resolution: Relations are automatically mapped using content type schemas
- Schema Caching: Content type metadata cached for fast relation lookups
- ID Mapping: Old IDs automatically mapped to new IDs during import
- Media Support: Export and import media files with content
- Progress Tracking: Optional callbacks for monitoring long operations
- Dry Run Mode: Test imports before executing
- Conflict Resolution: Configurable strategies for handling existing entities
How Relation Resolution Works:
- During export, content type schemas are fetched from the Content-Type Builder API
- Schemas include relation metadata (field types, targets)
- During import, relations are resolved by looking up target content types from schemas
- Old IDs are mapped to new IDs using the ID mapping table
For example, when importing an article with {"author": [5]}, the system:
- Looks up the schema to find that
authortargets"api::author.author" - Maps old author ID 5 to the new ID in the target instance
- Updates the article with the resolved relation
See the Export/Import Guide for complete documentation.
We provide two complete migration examples for different use cases:
Perfect for straightforward migrations with known content types:
# Set environment variables (or edit the script)
export SOURCE_STRAPI_TOKEN='your-source-token'
export TARGET_STRAPI_TOKEN='your-target-token'
# Run the migration
python examples/simple_migration.pyFeatures:
- ✅ Single-file, easy to understand
- ✅ Environment variable support for credentials
- ✅ Configuration validation before migration
- ✅ Connection verification for both instances
- ✅ Timestamped backup files to prevent overwrites
- ✅ Comprehensive error handling
- ✅ Automatic relation resolution
- ✅ Includes media files
Comprehensive migration tool with auto-discovery and verification:
# Set environment variables (required)
export SOURCE_STRAPI_URL="http://localhost:1337"
export SOURCE_STRAPI_TOKEN="your-source-api-token"
export TARGET_STRAPI_URL="http://localhost:1338"
export TARGET_STRAPI_TOKEN="your-target-api-token"
# Export all content from source
python examples/full_migration_v5.py export
# Import to target
python examples/full_migration_v5.py import
# Or do both in one command
python examples/full_migration_v5.py migrate
# Verify migration success
python examples/full_migration_v5.py verifyFeatures:
- ✅ Environment variable configuration (no hardcoded credentials)
- ✅ Auto-discovers all content types (no manual configuration needed)
- ✅ Progress bars for long operations
- ✅ Detailed migration reports
- ✅ Entity count verification
- ✅ Error reporting and recovery
- ✅ Batch processing for large datasets
- ✅ ID mapping with detailed logs
- ✅ Media file handling with progress tracking
Full Migration Example Output:
🔍 Discovering content types...
Found 12 content types:
- api::article.article
- api::author.author
- api::category.category
...
📥 Exporting 12 content types...
[████████████████████████████████████████] 100% | Processing articles
✅ EXPORT COMPLETE
Content types exported: 12
Total entities exported: 1,847
Media files downloaded: 234
Total export size: 45.3 MB
📤 Importing 1,847 entities...
[████████████████████████████████████████] 100% | Importing articles
✅ IMPORT COMPLETE
Entities imported: 1,847
Media files imported: 234
Both examples include:
- SecretStr for secure token handling
- Proper error handling and reporting
- Progress tracking
- Automatic relation resolution using schemas
- Media file download/upload
- ID mapping for relations
strapi-kit supports full dependency injection for testability and customization. All dependencies have sensible defaults but can be overridden.
- Testability: Inject mocks for unit testing without HTTP calls
- Customization: Provide custom parsers, auth handlers, or HTTP clients
- Flexibility: Share HTTP clients across multiple Strapi instances
- Control: Manage lifecycles of shared resources
from strapi_kit import SyncClient, StrapiConfig
import httpx
config = StrapiConfig(
base_url="http://localhost:1337",
api_token="your-token"
)
# Simple usage - all dependencies created automatically
with SyncClient(config) as client:
response = client.get_many("articles")
# Advanced usage - inject custom HTTP client
shared_http = httpx.Client()
client1 = SyncClient(config, http_client=shared_http)
client2 = SyncClient(config, http_client=shared_http)
# Both share the same connection poolfrom strapi_kit import (
SyncClient,
AsyncClient,
StrapiConfig,
AuthProvider,
HTTPClient,
AsyncHTTPClient,
ResponseParser,
VersionDetectingParser,
)
# Custom authentication
class CustomAuth:
def get_headers(self) -> dict[str, str]:
return {"Authorization": "Custom token"}
def validate_token(self) -> bool:
return True
# Custom response parser
class CustomParser:
def parse_single(self, response_data):
# Custom parsing logic
...
def parse_collection(self, response_data):
# Custom parsing logic
...
# Inject custom dependencies
client = SyncClient(
config,
http_client=custom_http, # Custom HTTP client
auth=custom_auth, # Custom auth provider
parser=custom_parser # Custom response parser
)from unittest.mock import Mock
# Create mock HTTP client for testing (no actual HTTP calls)
class MockHTTPClient:
def __init__(self):
self.requests = []
def request(self, method, url, **kwargs):
self.requests.append((method, url))
# Return mock response
mock_response = Mock()
mock_response.is_success = True
mock_response.json.return_value = {"data": []}
return mock_response
def close(self):
pass
# Use mock in tests
mock_http = MockHTTPClient()
client = SyncClient(config, http_client=mock_http)
# Make requests (no actual HTTP)
client.get("articles")
# Verify mock was called
assert len(mock_http.requests) == 1strapi-kit uses Python protocols for dependency interfaces:
ConfigProvider: Configuration interfaceAuthProvider: Authentication interfaceHTTPClient: Sync HTTP client interfaceAsyncHTTPClient: Async HTTP client interfaceResponseParser: Response parsing interface
All implementations satisfy these protocols and are type-checked with mypy.
Example - Custom config from database:
class DatabaseConfig:
"""Load config from database."""
def __init__(self, db):
self.db = db
def get_base_url(self) -> str:
return self.db.query("SELECT url FROM config")[0]
def get_api_token(self) -> str:
return self.db.query("SELECT token FROM secrets")[0]
# ... other properties
# Use database config
db_config = DatabaseConfig(db_connection)
client = SyncClient(db_config)strapi-kit provides a rich exception hierarchy for precise error handling:
from strapi_kit import (
StrapiError, # Base for all errors
ConfigurationError, # Invalid config (missing token, bad URL)
ValidationError, # Invalid input/query params
AuthenticationError, # HTTP 401
AuthorizationError, # HTTP 403
NotFoundError, # HTTP 404
ConflictError, # HTTP 409
ServerError, # HTTP 5xx
NetworkError, # Connection issues (base)
RateLimitError, # HTTP 429
ImportExportError, # Data operations (base)
FormatError, # Invalid data format
MediaError, # Media upload/download errors
)
try:
with SyncClient(config) as client:
response = client.get_many("articles")
except ConfigurationError as e:
print(f"Config issue: {e}")
except ValidationError as e:
print(f"Invalid query: {e}")
except NotFoundError as e:
print(f"Not found: {e}")
except StrapiError as e:
print(f"Strapi error: {e}")All exceptions inherit from StrapiError, making it easy to catch all package-specific errors while still allowing precise handling of specific error types.
# Clone the repository
git clone https://github.com/mehdizare/strapi-kit.git
cd strapi-kit
# Create virtual environment
python -m venv .venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
# Install dependencies including pre-commit (uv recommended)
uv pip install -e ".[dev]"
# Or with pip
pip install -e ".[dev]"
# Install pre-commit hooks (one-time setup)
pre-commit installThis project uses pre-commit hooks to ensure code quality. The pre-commit package is included in [dev] dependencies.
# Install hooks (one-time setup, after installing dev dependencies)
pre-commit install
# Run hooks manually on all files
pre-commit run --all-files
# Update hooks to latest versions
pre-commit autoupdateWhat the hooks check:
- ✅ Code formatting (ruff format)
- ✅ Linting (ruff check)
- ✅ Type checking (mypy strict mode)
- ✅ Security issues (ruff S rules)
- ✅ Secrets detection (detect-secrets)
- ✅ File consistency (trailing whitespace, EOF, etc.)
Skip hooks temporarily (not recommended):
git commit --no-verify# Run all tests
pytest
# Run with coverage
pytest --cov=strapi_kit --cov-report=html
# Run specific test file
pytest tests/unit/test_client.py -v# Format code
ruff format src/ tests/
# Lint code
ruff check src/ tests/
# Type checking
mypy src/strapi_kit/
# Security checks
make security
# Run all quality checks
make qualityThis project is in active development. Currently implemented:
- HTTP clients (sync and async)
- Configuration with Pydantic
- Authentication (API tokens)
- Exception hierarchy
- API version detection (v4/v5)
- Request Models: Filters (24 operators), sorting, pagination, population, field selection
- Response Models: V4/V5 parsing with automatic normalization
- Query Builder:
StrapiQueryfluent API with full type safety - Typed Client Methods:
get_one(),get_many(),create(),update(),remove() - Dependency Injection: Full DI support with protocols for testability
- Full test coverage with type-safe query building
- Media Upload: Single and batch file uploads with metadata
- Media Download: Streaming downloads for large files
- Media Management: List, get, update, and delete media
- Entity Attachment: Link media to specific content types
- Full async support for all media operations
- 100% test coverage on media operations
- Content Export: Export content types with all entities
- Automatic Relation Resolution: Schema-based relation mapping
- Media Export: Download and package media files
- Content Import: Import with ID mapping and relation resolution
- Schema Caching: Efficient content type metadata handling
- 85% overall test coverage with 460 passing tests
- Content-Type Builder API: List content types, components, and full schemas
- UID Utilities: Convert UIDs to endpoints, singularize, build admin URLs
- SEO Detection: Detect SEO configuration patterns in schemas
- 86% overall test coverage with 528 passing tests
- Bulk operations with streaming
- Advanced retry strategies
- Rate limiting
- GraphQL support
- Type-Safe: Full Pydantic validation and mypy strict mode compliance
- Version Agnostic: Works with both Strapi v4 and v5 seamlessly
- 24 Filter Operators: Complete filtering support (eq, gt, contains, in, null, between, etc.)
- Normalized Responses: Consistent interface regardless of Strapi version
- Dependency Injection: Protocol-based DI for testability and customization
- IDE Autocomplete: Full type hints for excellent developer experience
- Dual API: Use typed methods for safety or raw methods for flexibility
MIT License - see LICENSE file for details.
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Make your changes and add tests
- Run quality checks:
make pre-commit - Commit your changes with conventional commits format
- Push to your fork and submit a Pull Request
Automated Reviews: All PRs are automatically reviewed by CodeRabbit AI for code quality, security, and best practices.