punimtag/docs/api-standards.md
2025-08-15 00:57:39 -08:00

7.0 KiB

PunimTag API Standards

Overview

This document defines the standards for designing and implementing API endpoints in PunimTag.

Response Format

Success Response

{
  "success": true,
  "data": {
    // Response data here
  },
  "message": "Optional success message"
}

Error Response

{
  "success": false,
  "error": "Descriptive error message",
  "code": "ERROR_CODE_OPTIONAL"
}

Paginated Response

{
  "success": true,
  "data": {
    "items": [...],
    "pagination": {
      "page": 1,
      "per_page": 20,
      "total": 150,
      "pages": 8
    }
  }
}

HTTP Status Codes

Success Codes

  • 200 OK: Request successful
  • 201 Created: Resource created successfully
  • 204 No Content: Request successful, no content to return

Client Error Codes

  • 400 Bad Request: Invalid request data
  • 401 Unauthorized: Authentication required
  • 403 Forbidden: Access denied
  • 404 Not Found: Resource not found
  • 409 Conflict: Resource conflict
  • 422 Unprocessable Entity: Validation error

Server Error Codes

  • 500 Internal Server Error: Server error
  • 503 Service Unavailable: Service temporarily unavailable

Endpoint Naming Conventions

RESTful Patterns

  • GET /photos: List photos
  • GET /photos/{id}: Get specific photo
  • POST /photos: Create new photo
  • PUT /photos/{id}: Update photo
  • DELETE /photos/{id}: Delete photo

Custom Actions

  • POST /photos/{id}/identify: Identify faces in photo
  • POST /photos/{id}/duplicates: Find duplicates
  • GET /photos/{id}/faces: Get faces in photo

Request Parameters

Query Parameters

# Standard pagination
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 20, type=int)

# Filtering
filter_name = request.args.get('filter', '')
sort_by = request.args.get('sort', 'date_taken')
sort_order = request.args.get('order', 'desc')

JSON Body Parameters

# Validate required fields
data = request.get_json()
if not data:
    return jsonify({'success': False, 'error': 'No JSON data provided'}), 400

required_fields = ['name', 'email']
for field in required_fields:
    if field not in data:
        return jsonify({'success': False, 'error': f'Missing required field: {field}'}), 400

Error Handling

Standard Error Handler

@app.errorhandler(404)
def not_found(error):
    return jsonify({
        'success': False,
        'error': 'Resource not found',
        'code': 'NOT_FOUND'
    }), 404

@app.errorhandler(500)
def internal_error(error):
    return jsonify({
        'success': False,
        'error': 'Internal server error',
        'code': 'INTERNAL_ERROR'
    }), 500

Validation Errors

def validate_photo_data(data):
    errors = []

    if 'filename' not in data:
        errors.append('filename is required')

    if 'path' in data and not os.path.exists(data['path']):
        errors.append('file path does not exist')

    return errors

# Usage in endpoint
errors = validate_photo_data(data)
if errors:
    return jsonify({
        'success': False,
        'error': 'Validation failed',
        'details': errors
    }), 422

Database Operations

Connection Management

def get_db_connection():
    conn = sqlite3.connect('punimtag_simple.db')
    conn.row_factory = sqlite3.Row  # Enable dict-like access
    return conn

# Usage in endpoint
try:
    conn = get_db_connection()
    cursor = conn.cursor()
    # Database operations
    conn.commit()
except Exception as e:
    conn.rollback()
    return jsonify({'success': False, 'error': str(e)}), 500
finally:
    conn.close()

Parameterized Queries

# Always use parameterized queries to prevent SQL injection
cursor.execute('SELECT * FROM images WHERE id = ?', (image_id,))
cursor.execute('INSERT INTO photos (name, path) VALUES (?, ?)', (name, path))

Rate Limiting

Basic Rate Limiting

from functools import wraps
import time

def rate_limit(requests_per_minute=60):
    def decorator(f):
        @wraps(f)
        def wrapped(*args, **kwargs):
            # Implement rate limiting logic here
            return f(*args, **kwargs)
        return wrapped
    return decorator

# Usage
@app.route('/api/photos')
@rate_limit(requests_per_minute=30)
def get_photos():
    # Endpoint implementation
    pass

Caching

Response Caching

from functools import wraps
import hashlib
import json

def cache_response(ttl_seconds=300):
    def decorator(f):
        @wraps(f)
        def wrapped(*args, **kwargs):
            # Implement caching logic here
            return f(*args, **kwargs)
        return wrapped
    return decorator

# Usage
@app.route('/api/photos')
@cache_response(ttl_seconds=60)
def get_photos():
    # Endpoint implementation
    pass

Logging

Request Logging

import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

@app.before_request
def log_request():
    logger.info(f'{request.method} {request.path} - {request.remote_addr}')

@app.after_request
def log_response(response):
    logger.info(f'Response: {response.status_code}')
    return response

Security

Input Sanitization

import re

def sanitize_filename(filename):
    # Remove dangerous characters
    filename = re.sub(r'[<>:"/\\|?*]', '', filename)
    # Limit length
    return filename[:255]

def validate_file_type(filename):
    allowed_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.bmp'}
    ext = os.path.splitext(filename)[1].lower()
    return ext in allowed_extensions

CORS Headers

@app.after_request
def add_cors_headers(response):
    response.headers['Access-Control-Allow-Origin'] = '*'
    response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
    response.headers['Access-Control-Allow-Headers'] = 'Content-Type'
    return response

Testing

Endpoint Testing

def test_get_photos():
    response = app.test_client().get('/api/photos')
    assert response.status_code == 200
    data = json.loads(response.data)
    assert data['success'] == True
    assert 'data' in data

def test_create_photo():
    response = app.test_client().post('/api/photos',
                                    json={'filename': 'test.jpg', 'path': '/test/path'})
    assert response.status_code == 201
    data = json.loads(response.data)
    assert data['success'] == True

Documentation

Endpoint Documentation

@app.route('/api/photos', methods=['GET'])
def get_photos():
    """
    Get a list of photos with optional filtering and pagination.

    Query Parameters:
        page (int): Page number (default: 1)
        per_page (int): Items per page (default: 20)
        filter (str): Filter by name or tags
        sort (str): Sort field (default: date_taken)
        order (str): Sort order (asc/desc, default: desc)

    Returns:
        JSON response with photos and pagination info
    """
    # Implementation
    pass