# Creatopy API Documentation

## Table of Contents
1. [Introduction](#introduction)
2. [Authentication](#authentication)
3. [Base URLs](#base-urls)
4. [Rate Limits](#rate-limits)
5. [Python SDK](#python-sdk)
6. [Resources & Examples](#resources)
7. [Error Handling](#error-handling)
8. [Webhooks](#webhooks)
9. [Best Practices](#best-practices)

## Introduction

The Creatopy API allows you to programmatically create, manage, and manipulate creative assets through RESTful endpoints. This documentation focuses on Python implementation.

### API Versioning

Current Version: `v1`
Base URL: `https://api.creatopy.com/v1`

## Authentication

### Getting API Keys

1. Log in to your Creatopy account
2. Navigate to Settings → API Keys
3. Generate a new API key

### Python Authentication Setup

```python
import requests
from typing import Dict, List, Optional, Union
import json
import os
from datetime import datetime

class CreatopyAuth:
    def __init__(self, api_key: str):
        self.api_key = api_key
        self.base_url = "https://api.creatopy.com/v1"
        self.session = requests.Session()
        self.session.headers.update({
            "Authorization": f"Bearer {self.api_key}",
            "Content-Type": "application/json"
        })

    def validate_auth(self) -> bool:
        """Validate authentication credentials"""
        try:
            response = self.session.get(f"{self.base_url}/auth/validate")
            return response.status_code == 200
        except requests.exceptions.RequestException:
            return False
```

## Python SDK

### Installation and Setup

```bash
pip install creatopy-sdk
```

### Basic Client Configuration

```python
from creatopy import CreatopyClient
from creatopy.exceptions import CreatopyError

class CreatopyManager:
    def __init__(self, api_key: str):
        self.client = CreatopyClient(api_key=api_key)
        self.validate_connection()

    def validate_connection(self) -> None:
        try:
            self.client.ping()
        except CreatopyError as e:
            raise ConnectionError(f"Failed to connect to Creatopy: {str(e)}")
```

### Advanced Configuration

```python
class CreatopyConfig:
    def __init__(
        self,
        api_key: str,
        timeout: int = 30,
        max_retries: int = 3,
        retry_delay: int = 1
    ):
        self.api_key = api_key
        self.timeout = timeout
        self.max_retries = max_retries
        self.retry_delay = retry_delay
        
        self.session = requests.Session()
        self.session.headers.update({
            "Authorization": f"Bearer {self.api_key}",
            "Content-Type": "application/json"
        })
        
        # Configure retry strategy
        adapter = requests.adapters.HTTPAdapter(
            max_retries=self.max_retries,
            pool_connections=100,
            pool_maxsize=100
        )
        self.session.mount("https://", adapter)
```

## Resources & Examples

### Design Management

#### Design Client Class

```python
from dataclasses import dataclass
from typing import Optional, List, Dict, Any

@dataclass
class DesignDimensions:
    width: int
    height: int

@dataclass
class DesignElement:
    type: str
    content: str
    position: Dict[str, int]
    style: Optional[Dict[str, Any]] = None

class DesignClient:
    def __init__(self, auth_client: CreatopyAuth):
        self.auth = auth_client
        self.base_url = f"{auth_client.base_url}/designs"

    def list_designs(
        self,
        page: int = 1,
        limit: int = 10,
        sort: str = "created_at",
        filter_params: Optional[Dict] = None
    ) -> Dict:
        """
        List designs with pagination and filtering options
        """
        params = {
            "page": page,
            "limit": limit,
            "sort": sort
        }
        if filter_params:
            params.update(filter_params)

        response = self.auth.session.get(self.base_url, params=params)
        response.raise_for_status()
        return response.json()

    def create_design(
        self,
        name: str,
        template_id: str,
        dimensions: DesignDimensions,
        elements: List[DesignElement]
    ) -> Dict:
        """
        Create a new design with specified elements
        """
        payload = {
            "name": name,
            "template_id": template_id,
            "dimensions": dimensions.__dict__,
            "elements": [element.__dict__ for element in elements]
        }

        response = self.auth.session.post(self.base_url, json=payload)
        response.raise_for_status()
        return response.json()

    def update_design(
        self,
        design_id: str,
        updates: Dict[str, Any]
    ) -> Dict:
        """
        Update an existing design
        """
        response = self.auth.session.patch(
            f"{self.base_url}/{design_id}",
            json=updates
        )
        response.raise_for_status()
        return response.json()

    def delete_design(self, design_id: str) -> None:
        """
        Delete a design
        """
        response = self.auth.session.delete(f"{self.base_url}/{design_id}")
        response.raise_for_status()
```

### Asset Management

```python
class AssetClient:
    def __init__(self, auth_client: CreatopyAuth):
        self.auth = auth_client
        self.base_url = f"{auth_client.base_url}/assets"

    def upload_asset(
        self,
        file_path: str,
        asset_type: str,
        name: Optional[str] = None
    ) -> Dict:
        """
        Upload an asset file to Creatopy
        
        Args:
            file_path: Path to the file
            asset_type: Type of asset (image, video, font)
            name: Optional name for the asset
        """
        if not os.path.exists(file_path):
            raise FileNotFoundError(f"File not found: {file_path}")

        file_name = name or os.path.basename(file_path)
        
        with open(file_path, 'rb') as file:
            files = {
                'file': (file_name, file, self._get_mime_type(file_path))
            }
            data = {
                'type': asset_type,
                'name': file_name
            }
            
            response = self.auth.session.post(
                f"{self.base_url}/upload",
                files=files,
                data=data
            )
            response.raise_for_status()
            return response.json()

    def list_assets(
        self,
        asset_type: Optional[str] = None,
        page: int = 1,
        limit: int = 50
    ) -> Dict:
        """
        List assets with optional filtering
        """
        params = {
            "page": page,
            "limit": limit
        }
        if asset_type:
            params["type"] = asset_type

        response = self.auth.session.get(self.base_url, params=params)
        response.raise_for_status()
        return response.json()

    def get_asset(self, asset_id: str) -> Dict:
        """Get asset details"""
        response = self.auth.session.get(f"{self.base_url}/{asset_id}")
        response.raise_for_status()
        return response.json()

    def delete_asset(self, asset_id: str) -> None:
        """Delete an asset"""
        response = self.auth.session.delete(f"{self.base_url}/{asset_id}")
        response.raise_for_status()

    @staticmethod
    def _get_mime_type(file_path: str) -> str:
        """Helper method to determine MIME type"""
        import mimetypes
        mime_type, _ = mimetypes.guess_type(file_path)
        return mime_type or 'application/octet-stream'
```

### Template Management

```python
class TemplateClient:
    def __init__(self, auth_client: CreatopyAuth):
        self.auth = auth_client
        self.base_url = f"{auth_client.base_url}/templates"

    def list_templates(
        self,
        category: Optional[str] = None,
        page: int = 1,
        limit: int = 20,
        search: Optional[str] = None
    ) -> Dict:
        """
        List available templates with filtering options
        """
        params = {
            "page": page,
            "limit": limit
        }
        if category:
            params["category"] = category
        if search:
            params["search"] = search

        response = self.auth.session.get(self.base_url, params=params)
        response.raise_for_status()
        return response.json()

    def get_template(self, template_id: str) -> Dict:
        """
        Get detailed information about a specific template
        """
        response = self.auth.session.get(f"{self.base_url}/{template_id}")
        response.raise_for_status()
        return response.json()

    def create_from_template(
        self,
        template_id: str,
        name: str,
        modifications: Optional[Dict] = None
    ) -> Dict:
        """
        Create a new design from a template
        """
        payload = {
            "template_id": template_id,
            "name": name
        }
        if modifications:
            payload["modifications"] = modifications

        response = self.auth.session.post(
            f"{self.base_url}/{template_id}/create",
            json=payload
        )
        response.raise_for_status()
        return response.json()
```

### Usage Examples

```python
# Complete workflow example
def create_campaign_design():
    # Initialize clients
    auth = CreatopyAuth(api_key="your_api_key")
    design_client = DesignClient(auth)
    asset_client = AssetClient(auth)
    template_client = TemplateClient(auth)

    try:
        # 1. Upload campaign assets
        logo = asset_client.upload_asset(
            "path/to/logo.png",
            "image",
            "company_logo"
        )
        
        # 2. Find suitable template
        templates = template_client.list_templates(
            category="social_media",
            search="promotional"
        )
        
        template_id = templates['data'][0]['id']
        
        # 3. Create design elements
        elements = [
            DesignElement(
                type="image",
                content=logo['url'],
                position={"x": 100, "y": 100},
                style={"width": 200, "height": 100}
            ),
            DesignElement(
                type="text",
                content="Summer Sale!",
                position={"x": 150, "y": 250},
                style={
                    "fontSize": 48,
                    "fontFamily": "Arial",
                    "color": "#FF0000"
                }
            )
        ]
        
        # 4. Create the design
        design = design_client.create_design(
            name="Summer Campaign 2024",
            template_id=template_id,
            dimensions=DesignDimensions(width=1200, height=628),
            elements=elements
        )
        
        return design
        
    except requests.exceptions.RequestException as e:
        print(f"Error creating campaign: {str(e)}")
        raise
```

## Error Handling

### Custom Exception Classes

```python
class CreatopyException(Exception):
    """Base exception for Creatopy API errors"""
    pass

class AuthenticationError(CreatopyException):
    """Raised when authentication fails"""
    pass

class RateLimitError(CreatopyException):
    """Raised when API rate limit is exceeded"""
    pass

class ResourceNotFoundError(CreatopyException):
    """Raised when requested resource is not found"""
    pass

class ValidationError(CreatopyException):
    """Raised when request validation fails"""
    pass

class APIError(CreatopyException):
    """Generic API error"""
    def __init__(self, message: str, status_code: int, response_body: Dict):
        self.status_code = status_code
        self.response_body = response_body
        super().__init__(message)

class ErrorHandler:
    @staticmethod
    def handle_response(response: requests.Response) -> None:
        """
        Handle API response errors
        """
        if response.status_code >= 400:
            try:
                error_data = response.json()
            except ValueError:
                error_data = {"message": response.text}

            if response.status_code == 401:
                raise AuthenticationError("Authentication failed")
            elif response.status_code == 404:
                raise ResourceNotFoundError(f"Resource not found: {error_data.get('message')}")
            elif response.status_code == 429:
                raise RateLimitError("Rate limit exceeded")
            elif response.status_code == 400:
                raise ValidationError(error_data.get('message', 'Validation failed'))
            else:
                raise APIError(
                    message=error_data.get('message', 'Unknown error'),
                    status_code=response.status_code,
                    response_body=error_data
                )
```

### Retry Handler

```python
import time
from functools import wraps

def retry_handler(max_retries: int = 3, delay: int = 1):
    """
    Decorator for handling retries on API calls
    """
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            retries = 0
            while retries < max_retries:
                try:
                    return func(*args, **kwargs)
                except (RateLimitError, requests.exceptions.RequestException) as e:
                    retries += 1
                    if retries == max_retries:
                        raise
                    time.sleep(delay * (2 ** (retries - 1)))  # Exponential backoff
            return None
        return wrapper
    return decorator

# Usage example
@retry_handler(max_retries=3, delay=1)
def safe_api_call(client, *args, **kwargs):
    return client.make_request(*args, **kwargs)
```

## Best Practices

### Python-Specific Best Practices

1. **Session Management**
```python
# Use context managers for session handling
with CreatopyClient(api_key="your_key") as client:
    designs = client.designs.list()
```

2. **Batch Operations**
```python
def batch_process_designs(design_ids: List[str], operation: callable):
    """Process designs in batches to avoid rate limits"""
    batch_size = 10
    for i in range(0, len(design_ids), batch_size):
        batch = design_ids[i:i + batch_size]
        for design_id in batch:
            try:
                operation(design_id)
            except CreatopyException as e:
                print(f"Error processing design {design_id}: {str(e)}")
        time.sleep(1)  # Rate limiting consideration
```
3. **Caching Implementation**
```python
from functools import lru_cache
from datetime import datetime, timedelta

class CachedCreatopyClient:
    def __init__(self, auth_client: CreatopyAuth, cache_ttl: int = 300):
        self.auth = auth_client
        self.cache_ttl = cache_ttl
        self._cache = {}

    @lru_cache(maxsize=100)
    def get_template(self, template_id: str) -> Dict:
        """Cached template retrieval"""
        return self.auth.session.get(f"{self.auth.base_url}/templates/{template_id}").json()

    def get_design_with_cache(self, design_id: str) -> Dict:
        """Get design with time-based cache"""
        cache_key = f"design_{design_id}"
        now = datetime.now()
        
        if cache_key in self._cache:
            data, timestamp = self._cache[cache_key]
            if now - timestamp < timedelta(seconds=self.cache_ttl):
                return data

        # Cache miss or expired, fetch fresh data
        data = self.auth.session.get(f"{self.auth.base_url}/designs/{design_id}").json()
        self._cache[cache_key] = (data, now)
        return data

    def clear_cache(self):
        """Clear all cached data"""
        self._cache.clear()
        self.get_template.cache_clear()
```

4. **Async Implementation**
```python
import asyncio
import aiohttp
from typing import List, Dict, Any

class AsyncCreatopyClient:
    def __init__(self, api_key: str):
        self.api_key = api_key
        self.base_url = "https://api.creatopy.com/v1"
        self.session = None

    async def __aenter__(self):
        self.session = aiohttp.ClientSession(headers={
            "Authorization": f"Bearer {self.api_key}",
            "Content-Type": "application/json"
        })
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        if self.session:
            await self.session.close()

    async def get_designs(self, design_ids: List[str]) -> List[Dict[str, Any]]:
        """Fetch multiple designs concurrently"""
        async def fetch_design(design_id: str) -> Dict[str, Any]:
            async with self.session.get(
                f"{self.base_url}/designs/{design_id}"
            ) as response:
                return await response.json()

        tasks = [fetch_design(design_id) for design_id in design_ids]
        return await asyncio.gather(*tasks)

    async def bulk_create_designs(
        self,
        designs: List[Dict[str, Any]]
    ) -> List[Dict[str, Any]]:
        """Create multiple designs concurrently"""
        async def create_design(design_data: Dict[str, Any]) -> Dict[str, Any]:
            async with self.session.post(
                f"{self.base_url}/designs",
                json=design_data
            ) as response:
                return await response.json()

        tasks = [create_design(design) for design in designs]
        return await asyncio.gather(*tasks)

# Usage example
async def main():
    async with AsyncCreatopyClient(api_key="your_key") as client:
        designs = await client.get_designs(["design1", "design2", "design3"])
        print(designs)

# Run async code
if __name__ == "__main__":
    asyncio.run(main())
```

5. **Logging Implementation**
```python
import logging
from typing import Optional
from datetime import datetime

class CreatopyLogger:
    def __init__(self, log_file: Optional[str] = None, level=logging.INFO):
        self.logger = logging.getLogger("creatopy")
        self.logger.setLevel(level)
        
        formatter = logging.Formatter(
            '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
        )

        # Console handler
        console_handler = logging.StreamHandler()
        console_handler.setFormatter(formatter)
        self.logger.addHandler(console_handler)

        # File handler if log_file specified
        if log_file:
            file_handler = logging.FileHandler(log_file)
            file_handler.setFormatter(formatter)
            self.logger.addHandler(file_handler)

class LoggedCreatopyClient:
    def __init__(self, auth_client: CreatopyAuth, logger: CreatopyLogger):
        self.auth = auth_client
        self.logger = logger.logger

    def create_design(self, design_data: Dict[str, Any]) -> Dict[str, Any]:
        """Create design with logging"""
        try:
            self.logger.info(f"Creating design: {design_data.get('name')}")
            response = self.auth.session.post(
                f"{self.auth.base_url}/designs",
                json=design_data
            )
            response.raise_for_status()
            result = response.json()
            self.logger.info(f"Design created successfully: {result.get('id')}")
            return result
        except Exception as e:
            self.logger.error(f"Error creating design: {str(e)}")
            raise
```

6. **Rate Limiting Handler**
```python
class RateLimitHandler:
    def __init__(self, max_requests: int = 60, time_window: int = 60):
        self.max_requests = max_requests
        self.time_window = time_window
        self.requests = []

    def wait_if_needed(self):
        """Wait if rate limit is about to be exceeded"""
        now = datetime.now()
        self.requests = [
            timestamp for timestamp in self.requests 
            if (now - timestamp).total_seconds() < self.time_window
        ]

        if len(self.requests) >= self.max_requests:
            oldest_request = self.requests[0]
            sleep_time = self.time_window - (now - oldest_request).total_seconds()
            if sleep_time > 0:
                time.sleep(sleep_time)
            self.requests = self.requests[1:]

        self.requests.append(now)

class RateLimitedCreatopyClient:
    def __init__(self, auth_client: CreatopyAuth):
        self.auth = auth_client
        self.rate_limiter = RateLimitHandler()

    def make_request(self, method: str, endpoint: str, **kwargs) -> Dict:
        """Make rate-limited request"""
        self.rate_limiter.wait_if_needed()
        response = self.auth.session.request(method, endpoint, **kwargs)
        response.raise_for_status()
        return response.json()
```

7. **Validation Utilities**
```python
from dataclasses import dataclass
from typing import Optional, List, Union
import re

@dataclass
class DesignValidation:
    name: str
    width: int
    height: int
    elements: List[Dict]

    def validate(self) -> List[str]:
        """Validate design parameters"""
        errors = []
        
        # Name validation
        if not self.name or len(self.name) < 3:
            errors.append("Name must be at least 3 characters long")
        
        # Dimension validation
        if not (100 <= self.width <= 8000):
            errors.append("Width must be between 100 and 8000 pixels")
        if not (100 <= self.height <= 8000):
            errors.append("Height must be between 100 and 8000 pixels")
        
        # Elements validation
        if not self.elements:
            errors.append("At least one element is required")
        
        for element in self.elements:
            if 'type' not in element:
                errors.append("Element must have a type")
            if 'position' not in element:
                errors.append("Element must have a position")
        
        return errors

class InputValidator:
    @staticmethod
    def validate_hex_color(color: str) -> bool:
        """Validate hex color format"""
        return bool(re.match(r'^#(?:[0-9a-fA-F]{3}){1,2}$', color))

    @staticmethod
    def validate_dimensions(width: int, height: int) -> bool:
        """Validate image dimensions"""
        return 100 <= width <= 8000 and 100 <= height <= 8000

    @staticmethod
    def validate_file_type(filename: str, allowed_types: List[str]) -> bool:
        """Validate file type"""
        return any(filename.lower().endswith(ext) for ext in allowed_types)
```

8. **Testing Utilities**
```python
import pytest
from unittest.mock import Mock, patch

class CreatopyTestClient:
    def __init__(self):
        self.mock_session = Mock()
        self.mock_responses = {}

    def add_mock_response(
        self,
        endpoint: str,
        response_data: Dict,
        status_code: int = 200
    ):
        """Add mock response for endpoint"""
        mock_response = Mock()
        mock_response.json.return_value = response_data
        mock_response.status_code = status_code
        self.mock_responses[endpoint] = mock_response

    def get_mock_response(self, endpoint: str):
        """Get mock response for endpoint"""
        return self.mock_responses.get(endpoint)

# Example test cases
def test_create_design():
    test_client = CreatopyTestClient()
    test_client.add_mock_response(
        "/designs",
        {"id": "test_id", "name": "Test Design"}
    )

    with patch('requests.Session') as mock_session:
        mock_session.return_value = test_client.mock_session
        client = DesignClient(CreatopyAuth("test_key"))
        
        result = client.create_design(
            name="Test Design",
            template_id="template_id",
            dimensions=DesignDimensions(width=800, height=600),
            elements=[]
        )
        
        assert result["id"] == "test_id"
        assert result["name"] == "Test Design"
```