PyPI version Python License: MIT
The unofficial Python client library to easily interface with Strapi from your Python project.
pip install strapi-py
from strapi_client import strapi # Create a client instance client = strapi(base_url="http://localhost:1337/api") # Fetch data from a custom endpoint response = client.fetch('/articles') data = response.json() print(data)
from strapi_client import strapi # Initialize the client with yor API token client = strapi( base_url="http://localhost:1337/api", auth="your-api-token-here" ) # All requests will include the Authorization header response = client.fetch('/articles')
client = strapi( base_url="http://localhost:1337/api", headers={ "X-Custom-Header": "value", "Accept-Language": "en" } )
Collection types represent multiple entries (e.g., articles, products, users).
# Get all articles articles = client.collection('articles') response = articles.find() print(response.json())
articles = client.collection('articles') # Filter and sort response = articles.find(params={ 'filters': { 'title': { '$contains': 'Python' }, 'publishedAt': { '$notNull': True } }, 'sort': ['createdAt:desc'], 'pagination': { 'page': 1, 'pageSize': 10 }, 'populate': '*' })
articles = client.collection('articles') # Get article with documentId j964065dnjrdr4u89weh79xl response = articles.find_one("j964065dnjrdr4u89weh79xl", params={ 'populate': ['author', 'comments'] }) print(response.json())
Strapi 5 replaces the numeric
idused in Strapi 4 with a new 24-character alphanumeric identifier calleddocumentId. When working with Strapi 5, usedocumentIdas the primary resource identifier. This library continues to support the legacyidfield for Strapi 4 projects.
articles = client.collection('articles') response = articles.create(data={ 'title': 'My New Article', 'content': 'This is the article content', 'publishedAt': '2024-01-01T00:00:00.000Z' }) created_article = response.json() print(f"Created article with ID: {created_article['data']['documentId']}")
articles = client.collection('articles') response = articles.update(1, data={ 'title': 'Updated Article Title', 'content': 'Updated content' })
articles = client.collection('articles') response = articles.delete(1)
Single types represent a single entry (e.g., homepage, about page, settings).
homepage = client.single('homepage') response = homepage.find(params={'populate': '*'}) print(response.json())
homepage = client.single('homepage') response = homepage.update(data={ 'title': 'Welcome to My Site', 'description': 'A brief description' })
homepage = client.single('homepage') response = homepage.delete()
# Access users endpoint users = client.collection( resource='users', plugin={'name': 'users-permissions', 'prefix': ''} ) # Create a new user response = users.create(data={ 'username': 'johndoe', 'email': 'john@example.com', 'password': 'SecurePassword123!' })
# Access a custom plugin endpoint blog_posts = client.collection( resource='posts', plugin={'name': 'blog', 'prefix': 'blog'} ) # This will make requests to /blog/posts response = blog_posts.find()
# Disable plugin prefix custom_content = client.collection( resource='items', plugin={'name': 'custom-plugin', 'prefix': ''} ) # This will make requests to /items
# Upload from bytes with open('image.jpg', 'rb') as f: file_data = f.read() response = client.files.upload( file_data=file_data, filename='image.jpg', mimetype='image/jpeg', file_info={ 'alternativeText': 'A sample image', 'caption': 'My caption' } ) print(response.json())
import io # Upload from file-like object with open('document.pdf', 'rb') as f: response = client.files.upload( file_data=f, filename='document.pdf', mimetype='application/pdf' )
# Get all files response = client.files.find() files = response.json() # Filter files response = client.files.find(params={ 'filters': { 'mime': {'$contains': 'image'} }, 'sort': 'createdAt:desc' })
response = client.files.find_one(file_id="clkgylmcc000008lcdd868feh") file_data = response.json() print(f"File URL: {file_data['url']}")
response = client.files.update( file_id="clkgylmcc000008lcdd868feh", file_info={ 'name': 'renamed-file.jpg', 'alternativeText': 'Updated alt text', 'caption': 'Updated caption' } )
response = client.files.delete(file_id="clkgylmcc000008lcdd868feh")
You can specify custom API paths for content types:
# Use a custom path instead of the default /articles custom_articles = client.collection( resource='articles', path='/v2/custom-articles' ) response = custom_articles.find() # Makes request to /v2/custom-articles
The library provides detailed error messages from Strapi, making debugging much easier. When an error occurs, you'll see:
- Error type/name (e.g., ValidationError, ApplicationError)
- Clear error message from Strapi
- Detailed field-level validation errors with paths
- Access to the original response for debugging
from strapi_client import ( strapi, StrapiHTTPError, StrapiHTTPNotFoundError, StrapiHTTPUnauthorizedError, StrapiHTTPBadRequestError, StrapiValidationError ) try: client = strapi(base_url="http://localhost:1337/api") articles = client.collection('articles') response = articles.find_one(999) except StrapiHTTPNotFoundError as e: print(f"Article not found: {e}") print(f"Status code: {e.response.status_code}") except StrapiHTTPUnauthorizedError as e: print(f"Authentication failed: {e}") except StrapiHTTPBadRequestError as e: print(f"Bad request: {e}") print(f"Full error: {e.response.json()}") except StrapiHTTPError as e: print(f"HTTP error occurred: {e}") print(f"Response: {e.response.text}") except StrapiValidationError as e: print(f"Validation error: {e}")
When you have validation errors (e.g., in dynamic zones or complex fields), the error message will show exactly which fields are problematic:
try: blog = client.collection('blogs') response = blog.create(data={ 'title': '', # Invalid: too short 'blocks': [ { # Missing __component field 'body': 'Some content' } ] }) except StrapiHTTPBadRequestError as e: print(e) # Output: # Strapi API Error (400): [ValidationError] Invalid data provided # Validation errors: # - title: title must be at least 1 characters # - blocks.0.__component: component is required
All HTTP errors preserve the original response, allowing you to access additional details:
try: response = client.collection('articles').create(data={...}) except StrapiHTTPBadRequestError as e: # Get status code print(f"Status: {e.response.status_code}") # Get full error details error_details = e.response.json() print(f"Error name: {error_details['error']['name']}") print(f"Error details: {error_details['error']['details']}") # Get request Url print(f"Request URL: {e.response.request.url}")
StrapiError- Base error classStrapiValidationError- Invalid input or configurationStrapiHTTPError- Base HTTP error (non-2xx responses)StrapiHTTPBadRequestError- 400 Bad Request (validation errors, malformed requests)StrapiHTTPUnauthorizedError- 401 Unauthorized (authentication required)StrapiHTTPForbiddenError- 403 Forbidden (insufficient permissions)StrapiHTTPNotFoundError- 404 Not Found (resource doesn't exist)StrapiHTTPTimeoutError- 408 Request TimeoutStrapiHTTPInternalServerError- 500 Internal Server Error
articles = client.collection('articles') # Fetch French content response = articles.find(params={'locale': 'fr'}) # Fetch specific entry in Spanish response = articles.find_one(1, params={'locale': 'es'})
articles = client.collection('articles') # Populate all relations response = articles.find(params={'populate': '*'}) # Populate specific relations response = articles.find(params={ 'populate': ['author', 'categories', 'cover'] }) # Deep population response = articles.find(params={ 'populate': { 'author': { 'populate': ['avatar'] }, 'categories': '*' } })
articles = client.collection('articles') # Select specific fields only response = articles.find(params={ 'fields': ['title', 'description', 'publishedAt'] })
- Python >= 3.10
- httpx >= 0.27.0
Contributions are welcome! Please feel free to submit a Pull Request.
MIT License - see LICENSE file for details.