7.0 KiB
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