# PunimTag API Standards ## Overview This document defines the standards for designing and implementing API endpoints in PunimTag. ## Response Format ### Success Response ```json { "success": true, "data": { // Response data here }, "message": "Optional success message" } ``` ### Error Response ```json { "success": false, "error": "Descriptive error message", "code": "ERROR_CODE_OPTIONAL" } ``` ### Paginated Response ```json { "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 ```python # 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 ```python # 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 ```python @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 ```python 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 ```python 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 ```python # 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 ```python 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 ```python 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 ```python 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 ```python 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 ```python @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 ```python 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 ```python @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 ```