mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-01-25 22:19:53 -06:00
This commit implements a complete architectural transformation of the TimeTracker application, introducing modern design patterns and comprehensive feature set. ## Architecture Improvements ### Service Layer (18 Services) - TimeTrackingService: Time entry management with timer functionality - ProjectService: Project operations and lifecycle management - InvoiceService: Invoice creation, management, and status tracking - TaskService: Task management and workflow - ExpenseService: Expense tracking and categorization - ClientService: Client relationship management - PaymentService: Payment processing and invoice reconciliation - CommentService: Comment system for projects, tasks, and quotes - UserService: User management and role operations - NotificationService: Notification delivery system - ReportingService: Report generation and analytics - AnalyticsService: Event tracking and analytics - ExportService: CSV export functionality - ImportService: CSV import with validation - EmailService: Email operations and invoice delivery - PermissionService: Role-based permission management - BackupService: Database backup operations - HealthService: System health checks and monitoring ### Repository Layer (9 Repositories) - BaseRepository: Generic CRUD operations - TimeEntryRepository: Time entry data access - ProjectRepository: Project data access with filtering - InvoiceRepository: Invoice queries and status management - TaskRepository: Task data access - ExpenseRepository: Expense data access - ClientRepository: Client data access - UserRepository: User data access - PaymentRepository: Payment data access - CommentRepository: Comment data access ### Schema Layer (9 Schemas) - Marshmallow schemas for validation and serialization - Create, update, and full schemas for all entities - Input validation and data transformation ### Utility Modules (15 Utilities) - api_responses: Standardized API response helpers - validation: Input validation utilities - query_optimization: N+1 query prevention and eager loading - error_handlers: Centralized error handling - cache: Caching foundation (Redis-ready) - transactions: Transaction management decorators - event_bus: Domain event system - performance: Performance monitoring decorators - logger: Enhanced structured logging - pagination: Pagination utilities - file_upload: Secure file upload handling - search: Full-text search utilities - rate_limiting: Rate limiting helpers - config_manager: Configuration management - datetime_utils: Enhanced date/time utilities ## Database Improvements - Performance indexes migration (15+ indexes) - Query optimization utilities - N+1 query prevention patterns ## Testing Infrastructure - Comprehensive test fixtures (conftest.py) - Service layer unit tests - Repository layer unit tests - Integration test examples ## CI/CD Pipeline - GitHub Actions workflow - Automated linting (Black, Flake8, Pylint) - Security scanning (Bandit, Safety, Semgrep) - Automated testing with coverage - Docker image builds ## Documentation - Architecture migration guide - Quick start guide - API enhancements documentation - Implementation summaries - Refactored route examples ## Key Benefits - Separation of concerns: Business logic decoupled from routes - Testability: Services and repositories can be tested in isolation - Maintainability: Consistent patterns across codebase - Performance: Database indexes and query optimization - Security: Input validation and security scanning - Scalability: Event-driven architecture and health checks ## Statistics - 70+ new files created - 8,000+ lines of code - 18 services, 9 repositories, 9 schemas - 15 utility modules - 5 test files with examples This transformation establishes a solid foundation for future development and follows industry best practices for maintainable, scalable applications.
258 lines
5.5 KiB
Python
258 lines
5.5 KiB
Python
"""
|
|
Consistent API response helpers.
|
|
Provides standardized response formats for all API endpoints.
|
|
"""
|
|
|
|
from typing import Any, Dict, Optional, List
|
|
from flask import jsonify, Response
|
|
from marshmallow import ValidationError
|
|
|
|
|
|
def success_response(
|
|
data: Any = None,
|
|
message: Optional[str] = None,
|
|
status_code: int = 200,
|
|
meta: Optional[Dict[str, Any]] = None
|
|
) -> Response:
|
|
"""
|
|
Create a successful API response.
|
|
|
|
Args:
|
|
data: Response data
|
|
message: Optional success message
|
|
status_code: HTTP status code
|
|
meta: Optional metadata
|
|
|
|
Returns:
|
|
Flask JSON response
|
|
"""
|
|
response = {
|
|
'success': True,
|
|
}
|
|
|
|
if message:
|
|
response['message'] = message
|
|
|
|
if data is not None:
|
|
response['data'] = data
|
|
|
|
if meta:
|
|
response['meta'] = meta
|
|
|
|
return jsonify(response), status_code
|
|
|
|
|
|
def error_response(
|
|
message: str,
|
|
error_code: Optional[str] = None,
|
|
status_code: int = 400,
|
|
errors: Optional[Dict[str, List[str]]] = None,
|
|
details: Optional[Dict[str, Any]] = None
|
|
) -> Response:
|
|
"""
|
|
Create an error API response.
|
|
|
|
Args:
|
|
message: Error message
|
|
error_code: Optional error code
|
|
status_code: HTTP status code
|
|
errors: Optional field-specific errors
|
|
details: Optional additional error details
|
|
|
|
Returns:
|
|
Flask JSON response
|
|
"""
|
|
response = {
|
|
'success': False,
|
|
'error': error_code or 'error',
|
|
'message': message
|
|
}
|
|
|
|
if errors:
|
|
response['errors'] = errors
|
|
|
|
if details:
|
|
response['details'] = details
|
|
|
|
return jsonify(response), status_code
|
|
|
|
|
|
def validation_error_response(
|
|
errors: Dict[str, List[str]],
|
|
message: str = "Validation failed"
|
|
) -> Response:
|
|
"""
|
|
Create a validation error response.
|
|
|
|
Args:
|
|
errors: Field-specific validation errors
|
|
message: Error message
|
|
|
|
Returns:
|
|
Flask JSON response
|
|
"""
|
|
return error_response(
|
|
message=message,
|
|
error_code='validation_error',
|
|
status_code=400,
|
|
errors=errors
|
|
)
|
|
|
|
|
|
def not_found_response(
|
|
resource: str = "Resource",
|
|
resource_id: Optional[Any] = None
|
|
) -> Response:
|
|
"""
|
|
Create a not found error response.
|
|
|
|
Args:
|
|
resource: Resource type name
|
|
resource_id: Optional resource ID
|
|
|
|
Returns:
|
|
Flask JSON response
|
|
"""
|
|
message = f"{resource} not found"
|
|
if resource_id is not None:
|
|
message = f"{resource} with ID {resource_id} not found"
|
|
|
|
return error_response(
|
|
message=message,
|
|
error_code='not_found',
|
|
status_code=404
|
|
)
|
|
|
|
|
|
def unauthorized_response(message: str = "Authentication required") -> Response:
|
|
"""
|
|
Create an unauthorized error response.
|
|
|
|
Args:
|
|
message: Error message
|
|
|
|
Returns:
|
|
Flask JSON response
|
|
"""
|
|
return error_response(
|
|
message=message,
|
|
error_code='unauthorized',
|
|
status_code=401
|
|
)
|
|
|
|
|
|
def forbidden_response(message: str = "Insufficient permissions") -> Response:
|
|
"""
|
|
Create a forbidden error response.
|
|
|
|
Args:
|
|
message: Error message
|
|
|
|
Returns:
|
|
Flask JSON response
|
|
"""
|
|
return error_response(
|
|
message=message,
|
|
error_code='forbidden',
|
|
status_code=403
|
|
)
|
|
|
|
|
|
def paginated_response(
|
|
items: List[Any],
|
|
page: int,
|
|
per_page: int,
|
|
total: int,
|
|
message: Optional[str] = None
|
|
) -> Response:
|
|
"""
|
|
Create a paginated response.
|
|
|
|
Args:
|
|
items: List of items for current page
|
|
page: Current page number
|
|
per_page: Items per page
|
|
total: Total number of items
|
|
message: Optional message
|
|
|
|
Returns:
|
|
Flask JSON response
|
|
"""
|
|
pages = (total + per_page - 1) // per_page if total > 0 else 0
|
|
|
|
pagination = {
|
|
'page': page,
|
|
'per_page': per_page,
|
|
'total': total,
|
|
'pages': pages,
|
|
'has_next': page < pages,
|
|
'has_prev': page > 1,
|
|
'next_page': page + 1 if page < pages else None,
|
|
'prev_page': page - 1 if page > 1 else None
|
|
}
|
|
|
|
return success_response(
|
|
data=items,
|
|
message=message,
|
|
meta={'pagination': pagination}
|
|
)
|
|
|
|
|
|
def handle_validation_error(error: ValidationError) -> Response:
|
|
"""
|
|
Handle Marshmallow validation errors.
|
|
|
|
Args:
|
|
error: ValidationError instance
|
|
|
|
Returns:
|
|
Flask JSON response
|
|
"""
|
|
errors = {}
|
|
if isinstance(error.messages, dict):
|
|
errors = error.messages
|
|
elif isinstance(error.messages, list):
|
|
errors = {'_general': error.messages}
|
|
|
|
return validation_error_response(errors=errors)
|
|
|
|
|
|
def created_response(
|
|
data: Any,
|
|
message: Optional[str] = None,
|
|
location: Optional[str] = None
|
|
) -> Response:
|
|
"""
|
|
Create a 201 Created response.
|
|
|
|
Args:
|
|
data: Created resource data
|
|
message: Optional success message
|
|
location: Optional resource location URL
|
|
|
|
Returns:
|
|
Flask JSON response
|
|
"""
|
|
response_data = {'data': data}
|
|
if message:
|
|
response_data['message'] = message
|
|
|
|
response = jsonify(response_data)
|
|
response.status_code = 201
|
|
|
|
if location:
|
|
response.headers['Location'] = location
|
|
|
|
return response
|
|
|
|
|
|
def no_content_response() -> Response:
|
|
"""
|
|
Create a 204 No Content response.
|
|
|
|
Returns:
|
|
Flask response
|
|
"""
|
|
return '', 204
|
|
|