Task 11: Implement Deployment and Configuration
- Add production.py with security hardening and performance optimizations - Required environment variables for security (JWT, passwords, database) - Database connection pooling for PostgreSQL/MySQL - Security configurations and allowed hosts - Production logging and rotation settings - API rate limiting and performance tuning - Add development.py with relaxed settings for local development - Defaults for development (SQLite, debug logging, auto-reload) - Higher rate limits and longer session timeouts - Dev credentials for easy local setup - Development database defaults - Add environment configuration loader (__init__.py) - Automatic environment detection - Factory functions for lazy loading settings - Proper environment validation - Add startup scripts (start.sh) - Bash script for starting application in any environment - Conda environment validation - Automatic directory creation - Environment file generation - Database initialization - Development vs production startup modes - Add setup script (setup.py) - Python setup automation for environment initialization - Dependency installation - Environment file generation - Database initialization - Comprehensive validation and error handling - Update requirements.txt with psutil dependency All configurations follow project coding standards and include comprehensive documentation, type hints, and error handling.
This commit is contained in:
69
src/server/config/__init__.py
Normal file
69
src/server/config/__init__.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""
|
||||
Environment configuration loader for Aniworld application.
|
||||
|
||||
This module provides unified configuration loading based on the environment
|
||||
(development, production, or testing). It automatically selects the appropriate
|
||||
settings configuration based on the ENVIRONMENT variable.
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Union
|
||||
|
||||
from .development import DevelopmentSettings, get_development_settings
|
||||
from .production import ProductionSettings, get_production_settings
|
||||
|
||||
# Environment options
|
||||
ENVIRONMENT = os.getenv("ENVIRONMENT", "development").lower()
|
||||
|
||||
# Valid environment values
|
||||
VALID_ENVIRONMENTS = {"development", "production", "testing"}
|
||||
|
||||
if ENVIRONMENT not in VALID_ENVIRONMENTS:
|
||||
raise ValueError(
|
||||
f"Invalid ENVIRONMENT '{ENVIRONMENT}'. "
|
||||
f"Must be one of: {VALID_ENVIRONMENTS}"
|
||||
)
|
||||
|
||||
|
||||
def get_settings() -> Union[DevelopmentSettings, ProductionSettings]:
|
||||
"""
|
||||
Get environment-specific settings.
|
||||
|
||||
Returns:
|
||||
DevelopmentSettings: If ENVIRONMENT is 'development' or 'testing'
|
||||
ProductionSettings: If ENVIRONMENT is 'production'
|
||||
|
||||
Raises:
|
||||
ValueError: If ENVIRONMENT is not valid
|
||||
|
||||
Example:
|
||||
>>> settings = get_settings()
|
||||
>>> print(settings.log_level)
|
||||
DEBUG
|
||||
"""
|
||||
if ENVIRONMENT in {"development", "testing"}:
|
||||
return get_development_settings()
|
||||
return get_production_settings()
|
||||
|
||||
|
||||
# Singleton instance - loaded on first call
|
||||
_settings_instance = None
|
||||
|
||||
|
||||
def _get_settings_cached() -> Union[DevelopmentSettings, ProductionSettings]:
|
||||
"""Get cached settings instance."""
|
||||
global _settings_instance
|
||||
if _settings_instance is None:
|
||||
_settings_instance = get_settings()
|
||||
return _settings_instance
|
||||
|
||||
|
||||
# Re-export for convenience
|
||||
__all__ = [
|
||||
"get_settings",
|
||||
"ENVIRONMENT",
|
||||
"DevelopmentSettings",
|
||||
"ProductionSettings",
|
||||
"get_development_settings",
|
||||
"get_production_settings",
|
||||
]
|
||||
239
src/server/config/development.py
Normal file
239
src/server/config/development.py
Normal file
@@ -0,0 +1,239 @@
|
||||
"""
|
||||
Development environment configuration for Aniworld application.
|
||||
|
||||
This module provides development-specific settings including debugging,
|
||||
hot-reloading, and relaxed security for local development.
|
||||
|
||||
Environment Variables:
|
||||
JWT_SECRET_KEY: Secret key for JWT token signing (default: dev-secret)
|
||||
PASSWORD_SALT: Salt for password hashing (default: dev-salt)
|
||||
DATABASE_URL: Development database connection string (default: SQLite)
|
||||
LOG_LEVEL: Logging level (default: DEBUG)
|
||||
CORS_ORIGINS: Comma-separated list of allowed CORS origins
|
||||
API_RATE_LIMIT: API rate limit per minute (default: 1000)
|
||||
"""
|
||||
|
||||
from typing import List
|
||||
|
||||
from pydantic import Field, validator
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class DevelopmentSettings(BaseSettings):
|
||||
"""Development environment configuration settings."""
|
||||
|
||||
# ============================================================================
|
||||
# Security Settings (Relaxed for Development)
|
||||
# ============================================================================
|
||||
|
||||
jwt_secret_key: str = Field(
|
||||
default="dev-secret-key-change-in-production",
|
||||
env="JWT_SECRET_KEY"
|
||||
)
|
||||
"""JWT secret key (non-production value for development)."""
|
||||
|
||||
password_salt: str = Field(
|
||||
default="dev-salt-change-in-production",
|
||||
env="PASSWORD_SALT"
|
||||
)
|
||||
"""Password salt (non-production value for development)."""
|
||||
|
||||
master_password_hash: str = Field(
|
||||
default="$2b$12$wP0KBVbJKVAb8CdSSXw0NeGTKCk"
|
||||
"bw4fSAFXIqR2/wDqPSEBn9w7lS",
|
||||
env="MASTER_PASSWORD_HASH"
|
||||
)
|
||||
"""Hash of the master password (dev: 'password')."""
|
||||
|
||||
master_password: str = Field(default="password", env="MASTER_PASSWORD")
|
||||
"""Master password for development (NEVER use in production)."""
|
||||
|
||||
allowed_hosts: List[str] = Field(
|
||||
default=["localhost", "127.0.0.1", "*"], env="ALLOWED_HOSTS"
|
||||
)
|
||||
"""Allowed hosts (permissive for development)."""
|
||||
|
||||
cors_origins: str = Field(default="*", env="CORS_ORIGINS")
|
||||
"""CORS origins (allow all for development)."""
|
||||
|
||||
# ============================================================================
|
||||
# Database Settings
|
||||
# ============================================================================
|
||||
|
||||
database_url: str = Field(
|
||||
default="sqlite:///./data/aniworld_dev.db",
|
||||
env="DATABASE_URL"
|
||||
)
|
||||
"""Development database URL (SQLite by default)."""
|
||||
|
||||
database_pool_size: int = Field(default=5, env="DATABASE_POOL_SIZE")
|
||||
"""Database connection pool size."""
|
||||
|
||||
database_max_overflow: int = Field(default=10, env="DATABASE_MAX_OVERFLOW")
|
||||
"""Maximum overflow connections for database pool."""
|
||||
|
||||
database_pool_recycle: int = Field(
|
||||
default=3600, env="DATABASE_POOL_RECYCLE"
|
||||
)
|
||||
"""Recycle database connections every N seconds."""
|
||||
|
||||
# ============================================================================
|
||||
# API Settings
|
||||
# ============================================================================
|
||||
|
||||
api_rate_limit: int = Field(default=1000, env="API_RATE_LIMIT")
|
||||
"""API rate limit per minute (relaxed for development)."""
|
||||
|
||||
api_timeout: int = Field(default=60, env="API_TIMEOUT")
|
||||
"""API request timeout in seconds (longer for debugging)."""
|
||||
|
||||
# ============================================================================
|
||||
# Logging Settings
|
||||
# ============================================================================
|
||||
|
||||
log_level: str = Field(default="DEBUG", env="LOG_LEVEL")
|
||||
"""Logging level (DEBUG for detailed output)."""
|
||||
|
||||
log_file: str = Field(default="logs/development.log", env="LOG_FILE")
|
||||
"""Path to development log file."""
|
||||
|
||||
log_rotation_size: int = Field(default=5_242_880, env="LOG_ROTATION_SIZE")
|
||||
"""Log file rotation size in bytes (default: 5MB)."""
|
||||
|
||||
log_retention_days: int = Field(default=7, env="LOG_RETENTION_DAYS")
|
||||
"""Number of days to retain log files."""
|
||||
|
||||
# ============================================================================
|
||||
# Performance Settings
|
||||
# ============================================================================
|
||||
|
||||
workers: int = Field(default=1, env="WORKERS")
|
||||
"""Number of Uvicorn worker processes (single for development)."""
|
||||
|
||||
worker_timeout: int = Field(default=120, env="WORKER_TIMEOUT")
|
||||
"""Worker timeout in seconds."""
|
||||
|
||||
max_request_size: int = Field(default=104_857_600, env="MAX_REQUEST_SIZE")
|
||||
"""Maximum request body size in bytes (default: 100MB)."""
|
||||
|
||||
session_timeout_hours: int = Field(
|
||||
default=168, env="SESSION_TIMEOUT_HOURS"
|
||||
)
|
||||
"""Session timeout in hours (longer for development)."""
|
||||
|
||||
# ============================================================================
|
||||
# Provider Settings
|
||||
# ============================================================================
|
||||
|
||||
default_provider: str = Field(
|
||||
default="aniworld.to", env="DEFAULT_PROVIDER"
|
||||
)
|
||||
"""Default content provider."""
|
||||
|
||||
provider_timeout: int = Field(default=60, env="PROVIDER_TIMEOUT")
|
||||
"""Provider request timeout in seconds (longer for debugging)."""
|
||||
|
||||
provider_retries: int = Field(default=1, env="PROVIDER_RETRIES")
|
||||
"""Number of retry attempts for provider requests."""
|
||||
|
||||
# ============================================================================
|
||||
# Download Settings
|
||||
# ============================================================================
|
||||
|
||||
max_concurrent_downloads: int = Field(
|
||||
default=1, env="MAX_CONCURRENT_DOWNLOADS"
|
||||
)
|
||||
"""Maximum concurrent downloads (limited for development)."""
|
||||
|
||||
download_timeout: int = Field(default=7200, env="DOWNLOAD_TIMEOUT")
|
||||
"""Download timeout in seconds (default: 2 hours)."""
|
||||
|
||||
# ============================================================================
|
||||
# Application Paths
|
||||
# ============================================================================
|
||||
|
||||
anime_directory: str = Field(
|
||||
default="/tmp/aniworld_dev", env="ANIME_DIRECTORY"
|
||||
)
|
||||
"""Directory where anime is stored (development default)."""
|
||||
|
||||
temp_directory: str = Field(
|
||||
default="/tmp/aniworld_dev/temp", env="TEMP_DIRECTORY"
|
||||
)
|
||||
"""Temporary directory for downloads and cache."""
|
||||
|
||||
# ============================================================================
|
||||
# Validators
|
||||
# ============================================================================
|
||||
|
||||
@validator("log_level")
|
||||
@classmethod
|
||||
def validate_log_level(cls, v: str) -> str:
|
||||
"""Validate log level is valid."""
|
||||
valid_levels = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}
|
||||
if v.upper() not in valid_levels:
|
||||
raise ValueError(
|
||||
f"Invalid log level '{v}'. Must be one of: {valid_levels}"
|
||||
)
|
||||
return v.upper()
|
||||
|
||||
@validator("cors_origins")
|
||||
@classmethod
|
||||
def parse_cors_origins(cls, v: str) -> str:
|
||||
"""Parse comma-separated CORS origins."""
|
||||
if not v:
|
||||
return "http://localhost,http://127.0.0.1"
|
||||
return v
|
||||
|
||||
# ============================================================================
|
||||
# Configuration
|
||||
# ============================================================================
|
||||
|
||||
class Config:
|
||||
"""Pydantic config."""
|
||||
|
||||
env_file = ".env.development"
|
||||
extra = "ignore"
|
||||
case_sensitive = False
|
||||
|
||||
# ============================================================================
|
||||
# Properties
|
||||
# ============================================================================
|
||||
|
||||
@property
|
||||
def parsed_cors_origins(self) -> List[str]:
|
||||
"""Get parsed CORS origins as list."""
|
||||
if not self.cors_origins or self.cors_origins == "*":
|
||||
return ["*"]
|
||||
return [origin.strip() for origin in self.cors_origins.split(",")]
|
||||
|
||||
@property
|
||||
def is_production(self) -> bool:
|
||||
"""Check if running in production mode."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def debug_enabled(self) -> bool:
|
||||
"""Check if debug mode is enabled."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def reload_enabled(self) -> bool:
|
||||
"""Check if auto-reload is enabled."""
|
||||
return True
|
||||
|
||||
|
||||
def get_development_settings() -> DevelopmentSettings:
|
||||
"""
|
||||
Get development settings instance.
|
||||
|
||||
This is a factory function that should be called when settings are needed.
|
||||
|
||||
Returns:
|
||||
DevelopmentSettings instance configured from environment variables
|
||||
"""
|
||||
return DevelopmentSettings()
|
||||
|
||||
|
||||
# Export factory for backward compatibility
|
||||
development_settings = DevelopmentSettings()
|
||||
234
src/server/config/production.py
Normal file
234
src/server/config/production.py
Normal file
@@ -0,0 +1,234 @@
|
||||
"""
|
||||
Production environment configuration for Aniworld application.
|
||||
|
||||
This module provides production-specific settings including security hardening,
|
||||
performance optimizations, and operational configurations.
|
||||
|
||||
Environment Variables:
|
||||
JWT_SECRET_KEY: Secret key for JWT token signing (REQUIRED)
|
||||
PASSWORD_SALT: Salt for password hashing (REQUIRED)
|
||||
DATABASE_URL: Production database connection string
|
||||
LOG_LEVEL: Logging level (default: WARNING)
|
||||
CORS_ORIGINS: Comma-separated list of allowed CORS origins
|
||||
API_RATE_LIMIT: API rate limit per minute (default: 60)
|
||||
WORKERS: Number of Uvicorn worker processes (default: 4)
|
||||
WORKER_TIMEOUT: Worker timeout in seconds (default: 120)
|
||||
"""
|
||||
|
||||
from typing import List
|
||||
|
||||
from pydantic import Field, validator
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class ProductionSettings(BaseSettings):
|
||||
"""Production environment configuration settings."""
|
||||
|
||||
# ============================================================================
|
||||
# Security Settings
|
||||
# ============================================================================
|
||||
|
||||
jwt_secret_key: str = Field(..., env="JWT_SECRET_KEY")
|
||||
"""Secret key for JWT token signing. MUST be set in production."""
|
||||
|
||||
password_salt: str = Field(..., env="PASSWORD_SALT")
|
||||
"""Salt for password hashing. MUST be set in production."""
|
||||
|
||||
master_password_hash: str = Field(..., env="MASTER_PASSWORD_HASH")
|
||||
"""Hash of the master password for authentication."""
|
||||
|
||||
allowed_hosts: List[str] = Field(
|
||||
default=["*"], env="ALLOWED_HOSTS"
|
||||
)
|
||||
"""List of allowed hostnames for CORS and security checks."""
|
||||
|
||||
cors_origins: str = Field(default="", env="CORS_ORIGINS")
|
||||
"""Comma-separated list of allowed CORS origins."""
|
||||
|
||||
# ============================================================================
|
||||
# Database Settings
|
||||
# ============================================================================
|
||||
|
||||
database_url: str = Field(
|
||||
default="postgresql://user:password@localhost/aniworld",
|
||||
env="DATABASE_URL"
|
||||
)
|
||||
"""Database connection URL. Defaults to PostgreSQL for production."""
|
||||
|
||||
database_pool_size: int = Field(default=20, env="DATABASE_POOL_SIZE")
|
||||
"""Database connection pool size."""
|
||||
|
||||
database_max_overflow: int = Field(default=10, env="DATABASE_MAX_OVERFLOW")
|
||||
"""Maximum overflow connections for database pool."""
|
||||
|
||||
database_pool_recycle: int = Field(
|
||||
default=3600, env="DATABASE_POOL_RECYCLE"
|
||||
)
|
||||
"""Recycle database connections every N seconds."""
|
||||
|
||||
# ============================================================================
|
||||
# API Settings
|
||||
# ============================================================================
|
||||
|
||||
api_rate_limit: int = Field(default=60, env="API_RATE_LIMIT")
|
||||
"""API rate limit per minute per IP address."""
|
||||
|
||||
api_timeout: int = Field(default=30, env="API_TIMEOUT")
|
||||
"""API request timeout in seconds."""
|
||||
|
||||
# ============================================================================
|
||||
# Logging Settings
|
||||
# ============================================================================
|
||||
|
||||
log_level: str = Field(default="WARNING", env="LOG_LEVEL")
|
||||
"""Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)."""
|
||||
|
||||
log_file: str = Field(default="logs/production.log", env="LOG_FILE")
|
||||
"""Path to production log file."""
|
||||
|
||||
log_rotation_size: int = Field(default=10_485_760, env="LOG_ROTATION_SIZE")
|
||||
"""Log file rotation size in bytes (default: 10MB)."""
|
||||
|
||||
log_retention_days: int = Field(default=30, env="LOG_RETENTION_DAYS")
|
||||
"""Number of days to retain log files."""
|
||||
|
||||
# ============================================================================
|
||||
# Performance Settings
|
||||
# ============================================================================
|
||||
|
||||
workers: int = Field(default=4, env="WORKERS")
|
||||
"""Number of Uvicorn worker processes."""
|
||||
|
||||
worker_timeout: int = Field(default=120, env="WORKER_TIMEOUT")
|
||||
"""Worker timeout in seconds."""
|
||||
|
||||
max_request_size: int = Field(default=104_857_600, env="MAX_REQUEST_SIZE")
|
||||
"""Maximum request body size in bytes (default: 100MB)."""
|
||||
|
||||
session_timeout_hours: int = Field(default=24, env="SESSION_TIMEOUT_HOURS")
|
||||
"""Session timeout in hours."""
|
||||
|
||||
# ============================================================================
|
||||
# Provider Settings
|
||||
# ============================================================================
|
||||
|
||||
default_provider: str = Field(
|
||||
default="aniworld.to", env="DEFAULT_PROVIDER"
|
||||
)
|
||||
"""Default content provider."""
|
||||
|
||||
provider_timeout: int = Field(default=30, env="PROVIDER_TIMEOUT")
|
||||
"""Provider request timeout in seconds."""
|
||||
|
||||
provider_retries: int = Field(default=3, env="PROVIDER_RETRIES")
|
||||
"""Number of retry attempts for provider requests."""
|
||||
|
||||
# ============================================================================
|
||||
# Download Settings
|
||||
# ============================================================================
|
||||
|
||||
max_concurrent_downloads: int = Field(
|
||||
default=3, env="MAX_CONCURRENT_DOWNLOADS"
|
||||
)
|
||||
"""Maximum concurrent downloads."""
|
||||
|
||||
download_timeout: int = Field(default=3600, env="DOWNLOAD_TIMEOUT")
|
||||
"""Download timeout in seconds (default: 1 hour)."""
|
||||
|
||||
# ============================================================================
|
||||
# Application Paths
|
||||
# ============================================================================
|
||||
|
||||
anime_directory: str = Field(..., env="ANIME_DIRECTORY")
|
||||
"""Directory where anime is stored."""
|
||||
|
||||
temp_directory: str = Field(default="/tmp/aniworld", env="TEMP_DIRECTORY")
|
||||
"""Temporary directory for downloads and cache."""
|
||||
|
||||
# ============================================================================
|
||||
# Validators
|
||||
# ============================================================================
|
||||
|
||||
@validator("log_level")
|
||||
@classmethod
|
||||
def validate_log_level(cls, v: str) -> str:
|
||||
"""Validate log level is valid."""
|
||||
valid_levels = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}
|
||||
if v.upper() not in valid_levels:
|
||||
raise ValueError(
|
||||
f"Invalid log level '{v}'. Must be one of: {valid_levels}"
|
||||
)
|
||||
return v.upper()
|
||||
|
||||
@validator("database_url")
|
||||
@classmethod
|
||||
def validate_database_url(cls, v: str) -> str:
|
||||
"""Validate database URL is set and not SQLite."""
|
||||
if not v or v.startswith("sqlite"):
|
||||
raise ValueError(
|
||||
"Production database must not use SQLite. "
|
||||
"Use PostgreSQL or MySQL instead."
|
||||
)
|
||||
return v
|
||||
|
||||
@validator("cors_origins")
|
||||
@classmethod
|
||||
def parse_cors_origins(cls, v: str) -> str:
|
||||
"""Parse comma-separated CORS origins."""
|
||||
if not v:
|
||||
return ""
|
||||
return v
|
||||
|
||||
# ============================================================================
|
||||
# Configuration
|
||||
# ============================================================================
|
||||
|
||||
class Config:
|
||||
"""Pydantic config."""
|
||||
|
||||
env_file = ".env.production"
|
||||
extra = "ignore"
|
||||
case_sensitive = False
|
||||
|
||||
# ============================================================================
|
||||
# Properties
|
||||
# ============================================================================
|
||||
|
||||
@property
|
||||
def parsed_cors_origins(self) -> List[str]:
|
||||
"""Get parsed CORS origins as list."""
|
||||
if not self.cors_origins:
|
||||
return ["http://localhost", "http://127.0.0.1"]
|
||||
return [origin.strip() for origin in self.cors_origins.split(",")]
|
||||
|
||||
@property
|
||||
def is_production(self) -> bool:
|
||||
"""Check if running in production mode."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def debug_enabled(self) -> bool:
|
||||
"""Check if debug mode is enabled."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def reload_enabled(self) -> bool:
|
||||
"""Check if auto-reload is enabled."""
|
||||
return False
|
||||
|
||||
|
||||
def get_production_settings() -> ProductionSettings:
|
||||
"""
|
||||
Get production settings instance.
|
||||
|
||||
This is a factory function that should be called when settings are needed,
|
||||
rather than instantiating at module level to avoid requiring all
|
||||
environment variables at import time.
|
||||
|
||||
Returns:
|
||||
ProductionSettings instance configured from environment variables
|
||||
|
||||
Raises:
|
||||
ValidationError: If required environment variables are missing
|
||||
"""
|
||||
return ProductionSettings()
|
||||
Reference in New Issue
Block a user