strapi-py 0.1.1
The unofficial Python client for Strapi.
License: MIT
Requires Python: <4.0,>=3.10
Keywords: strapi, sdk, client, python

# Strapi Python Client

[![PyPI version](https://badge.fury.io/py/strapi-py.svg)](https://badge.fury.io/py/strapi-py)
[![Python](https://img.shields.io/pypi/pyversions/strapi-py.svg)](https://pypi.org/project/strapi-py/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

The unofficial Python client library to easily interface with Strapi from your Python project.

## Installation

```bash
pip install strapi-py
```

## Quick Start

```python
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)
```

## Authentication

### API Token Authentication

```python
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')
```

### Custom Headers

```python
client = strapi(
    base_url="http://localhost:1337/api",
    headers={
        "X-Custom-Header": "value",
        "Accept-Language": "en"
    }
)
```

## Working with Collection Types

Collection types represent multiple entries (e.g., articles, products, users).

### Find All Entries

```python
# Get all articles
articles = client.collection('articles')
response = articles.find()
print(response.json())
```

### Find with Filters and Sorting

```python
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': '*'
})
```

### Find One Entry

```python
articles = client.collection('articles')

# Get article with documentId j964065dnjrdr4u89weh79xl
response = articles.find_one("j964065dnjrdr4u89weh79xl", params={
    'populate': ['author', 'comments']
})
print(response.json())
```

>
> #### NOTE
>
>Strapi 5 replaces the numeric `id` used in Strapi 4 with a new 24-character alphanumeric identifier called `documentId`.
>When working with Strapi 5, use `documentId` as the primary resource identifier.
>This library continues to support the legacy `id` field for Strapi 4 projects.
>

### Create an Entry

```python
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']}")
```

### Update an Entry

```python
articles = client.collection('articles')

response = articles.update(1, data={
    'title': 'Updated Article Title',
    'content': 'Updated content'
})
```

### Delete an Entry

```python
articles = client.collection('articles')
response = articles.delete(1)
```

## Working with Single Types

Single types represent a single entry (e.g., homepage, about page, settings).

### Find Single Type

```python
homepage = client.single('homepage')
response = homepage.find(params={'populate': '*'})
print(response.json())
```

### Update Single Type

```python
homepage = client.single('homepage')
response = homepage.update(data={
    'title': 'Welcome to My Site',
    'description': 'A brief description'
})
```

### Delete Single Type

```python
homepage = client.single('homepage')
response = homepage.delete()
```

## Working with Plugins

### Users & Permissions Plugin

```python
# 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!'
})
```

### Custom Plugin with Prefix

```python
# 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()
```

### Custom Plugin without Prefix

```python
# Disable plugin prefix
custom_content = client.collection(
    resource='items',
    plugin={'name': 'custom-plugin', 'prefix': ''}
)
# This will make requests to /items
```

## File Management

### Upload a File

```python
# 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())
```

### Upload with File Object

```python
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'
    )
```

### List All Files

```python
# Get all files
response = client.files.find()
files = response.json()

# Filter files
response = client.files.find(params={
    'filters': {
        'mime': {'$contains': 'image'}
    },
    'sort': 'createdAt:desc'
})
```

### Get a Specific File

```python
response = client.files.find_one(file_id="clkgylmcc000008lcdd868feh")
file_data = response.json()
print(f"File URL: {file_data['url']}")
```

### Update File Metadata

```python
response = client.files.update(
    file_id="clkgylmcc000008lcdd868feh",
    file_info={
        'name': 'renamed-file.jpg',
        'alternativeText': 'Updated alt text',
        'caption': 'Updated caption'
    }
)
```

### Delete a File

```python
response = client.files.delete(file_id="clkgylmcc000008lcdd868feh")
```

## Custom Paths

You can specify custom API paths for content types:

```python
# 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
```

## Error Handling

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

### Basic Error Handling

```python
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}")
```

### Detailed Validation Errors

When you have validation errors (e.g., in dynamic zones or complex fields), the error message will show exactly which fields are problematic:

```python
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
```

### Accessing Response Details

All HTTP errors preserve the original response, allowing you to access additional details:

```python
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}")
```

### Available Error Types

- `StrapiError` - Base error class
- `StrapiValidationError` - Invalid input or configuration
- `StrapiHTTPError` - 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 Timeout
- `StrapiHTTPInternalServerError` - 500 Internal Server Error

## Advanced Usage

### Locale Support

```python
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'})
```

### Population

```python
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': '*'
    }
})
```

### Field Selection

```python
articles = client.collection('articles')

# Select specific fields only
response = articles.find(params={
    'fields': ['title', 'description', 'publishedAt']
})
```

## Requirements

- Python >= 3.10
- httpx >= 0.27.0

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

## License

MIT License - see LICENSE file for details.

## Links

- [GitHub Repository](https://github.com/Akinwalee/strapi-python)
- [Strapi Documentation](https://docs.strapi.io/)
- [Issue Tracker](https://github.com/Akinwalee/strapi-python/issues)