Aniworld/scripts/setup.py
Lukas 1637835fe6 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.
2025-10-22 10:28:37 +02:00

422 lines
12 KiB
Python

"""
Aniworld Application Setup Script
This script handles initial setup, dependency installation, database
initialization, and configuration for the Aniworld application.
Usage:
python setup.py [--environment {development|production}] [--no-deps]
python setup.py --help
"""
import argparse
import asyncio
import os
import subprocess
import sys
from pathlib import Path
class SetupManager:
"""Manages application setup and initialization."""
def __init__(
self,
environment: str = "development",
skip_deps: bool = False
):
"""
Initialize setup manager.
Args:
environment: Environment mode (development or production)
skip_deps: Skip dependency installation
"""
self.environment = environment
self.skip_deps = skip_deps
self.project_root = Path(__file__).parent.parent
self.conda_env = "AniWorld"
# ============================================================================
# Logging
# ============================================================================
@staticmethod
def log_info(message: str) -> None:
"""Log info message."""
print(f"\033[34m[INFO]\033[0m {message}")
@staticmethod
def log_success(message: str) -> None:
"""Log success message."""
print(f"\033[32m[SUCCESS]\033[0m {message}")
@staticmethod
def log_warning(message: str) -> None:
"""Log warning message."""
print(f"\033[33m[WARNING]\033[0m {message}")
@staticmethod
def log_error(message: str) -> None:
"""Log error message."""
print(f"\033[31m[ERROR]\033[0m {message}")
# ============================================================================
# Validation
# ============================================================================
def validate_environment(self) -> bool:
"""
Validate environment parameter.
Returns:
True if valid, False otherwise
"""
valid_envs = {"development", "production", "testing"}
if self.environment not in valid_envs:
self.log_error(
f"Invalid environment: {self.environment}. "
f"Must be one of: {valid_envs}"
)
return False
self.log_success(f"Environment: {self.environment}")
return True
def check_conda_env(self) -> bool:
"""
Check if conda environment exists.
Returns:
True if exists, False otherwise
"""
result = subprocess.run(
["conda", "env", "list"],
capture_output=True,
text=True
)
if self.conda_env in result.stdout:
self.log_success(f"Conda environment '{self.conda_env}' found")
return True
self.log_error(
f"Conda environment '{self.conda_env}' not found. "
f"Create with: conda create -n {self.conda_env} python=3.11"
)
return False
def check_python_version(self) -> bool:
"""
Check Python version.
Returns:
True if version >= 3.9, False otherwise
"""
if sys.version_info < (3, 9):
self.log_error(
f"Python 3.9+ required. Current: {sys.version_info.major}."
f"{sys.version_info.minor}"
)
return False
self.log_success(
f"Python version: {sys.version_info.major}."
f"{sys.version_info.minor}"
)
return True
# ============================================================================
# Directory Setup
# ============================================================================
def create_directories(self) -> bool:
"""
Create necessary directories.
Returns:
True if successful, False otherwise
"""
try:
directories = [
"logs",
"data",
"data/config_backups",
"Temp",
"tests",
"scripts",
]
self.log_info("Creating directories...")
for directory in directories:
dir_path = self.project_root / directory
dir_path.mkdir(parents=True, exist_ok=True)
self.log_success("Directories created")
return True
except Exception as e:
self.log_error(f"Failed to create directories: {e}")
return False
# ============================================================================
# Dependency Installation
# ============================================================================
def install_dependencies(self) -> bool:
"""
Install Python dependencies.
Returns:
True if successful, False otherwise
"""
if self.skip_deps:
self.log_warning("Skipping dependency installation")
return True
try:
requirements_file = self.project_root / "requirements.txt"
if not requirements_file.exists():
self.log_error(
f"requirements.txt not found at {requirements_file}"
)
return False
self.log_info("Installing dependencies...")
subprocess.run(
["conda", "run", "-n", self.conda_env,
"pip", "install", "-q", "-r", str(requirements_file)],
check=True
)
self.log_success("Dependencies installed")
return True
except subprocess.CalledProcessError as e:
self.log_error(f"Failed to install dependencies: {e}")
return False
# ============================================================================
# Environment Configuration
# ============================================================================
def create_env_files(self) -> bool:
"""
Create environment configuration files.
Returns:
True if successful, False otherwise
"""
try:
self.log_info("Creating environment configuration files...")
env_file = self.project_root / f".env.{self.environment}"
if env_file.exists():
self.log_warning(f"{env_file.name} already exists")
return True
# Create environment file with defaults
env_content = self._get_env_template()
env_file.write_text(env_content)
self.log_success(f"Created {env_file.name}")
return True
except Exception as e:
self.log_error(f"Failed to create env files: {e}")
return False
def _get_env_template(self) -> str:
"""
Get environment file template.
Returns:
Environment file content
"""
if self.environment == "production":
return """# Aniworld Production Configuration
# IMPORTANT: Set these values before running in production
# Security (REQUIRED - generate new values)
JWT_SECRET_KEY=change-this-to-a-secure-random-key
PASSWORD_SALT=change-this-to-a-secure-random-salt
MASTER_PASSWORD_HASH=change-this-to-hashed-password
# Database (REQUIRED - use PostgreSQL or MySQL in production)
DATABASE_URL=postgresql://user:password@localhost/aniworld
DATABASE_POOL_SIZE=20
DATABASE_MAX_OVERFLOW=10
# Application
ENVIRONMENT=production
ANIME_DIRECTORY=/var/lib/aniworld
TEMP_DIRECTORY=/tmp/aniworld
# Server
HOST=0.0.0.0
PORT=8000
WORKERS=4
# Security
CORS_ORIGINS=https://yourdomain.com
ALLOWED_HOSTS=yourdomain.com
# Logging
LOG_LEVEL=WARNING
LOG_FILE=logs/production.log
LOG_ROTATION_SIZE=10485760
LOG_RETENTION_DAYS=30
# Performance
API_RATE_LIMIT=60
SESSION_TIMEOUT_HOURS=24
MAX_CONCURRENT_DOWNLOADS=3
"""
else: # development
return """# Aniworld Development Configuration
# Security (Development defaults - NOT for production)
JWT_SECRET_KEY=dev-secret-key-change-in-production
PASSWORD_SALT=dev-salt-change-in-production
MASTER_PASSWORD_HASH=$2b$12$wP0KBVbJKVAb8CdSSXw0NeGTKCkbw4fSAFXIqR2/wDqPSEBn9w7lS
MASTER_PASSWORD=password
# Database
DATABASE_URL=sqlite:///./data/aniworld_dev.db
# Application
ENVIRONMENT=development
ANIME_DIRECTORY=/tmp/aniworld_dev
TEMP_DIRECTORY=/tmp/aniworld_dev/temp
# Server
HOST=127.0.0.1
PORT=8000
WORKERS=1
# Security
CORS_ORIGINS=*
# Logging
LOG_LEVEL=DEBUG
LOG_FILE=logs/development.log
# Performance
API_RATE_LIMIT=1000
SESSION_TIMEOUT_HOURS=168
MAX_CONCURRENT_DOWNLOADS=1
"""
# ============================================================================
# Database Initialization
# ============================================================================
async def init_database(self) -> bool:
"""
Initialize database.
Returns:
True if successful, False otherwise
"""
try:
self.log_info("Initializing database...")
# Import and run database initialization
os.chdir(self.project_root)
from src.server.database import init_db
await init_db()
self.log_success("Database initialized")
return True
except Exception as e:
self.log_error(f"Failed to initialize database: {e}")
return False
# ============================================================================
# Summary
# ============================================================================
def print_summary(self) -> None:
"""Print setup summary."""
self.log_info("=" * 50)
self.log_info("Setup Summary")
self.log_info("=" * 50)
self.log_info(f"Environment: {self.environment}")
self.log_info(f"Conda Environment: {self.conda_env}")
self.log_info(f"Project Root: {self.project_root}")
self.log_info("")
self.log_success("Setup complete!")
self.log_info("")
self.log_info("Next steps:")
self.log_info("1. Configure .env files with your settings")
if self.environment == "production":
self.log_info("2. Set up database (PostgreSQL/MySQL)")
self.log_info("3. Configure security settings")
self.log_info("4. Run: ./scripts/start.sh production")
else:
self.log_info("2. Run: ./scripts/start.sh development")
self.log_info("")
# ============================================================================
# Main Setup
# ============================================================================
async def run(self) -> int:
"""
Run setup process.
Returns:
0 if successful, 1 otherwise
"""
print("\033[34m" + "=" * 50 + "\033[0m")
print("\033[34mAniworld Application Setup\033[0m")
print("\033[34m" + "=" * 50 + "\033[0m")
print()
# Validation
if not self.validate_environment():
return 1
if not self.check_python_version():
return 1
if not self.check_conda_env():
return 1
# Setup
if not self.create_directories():
return 1
if not self.create_env_files():
return 1
if not self.install_dependencies():
return 1
# Initialize database
if not await self.init_database():
return 1
# Summary
self.print_summary()
return 0
async def main() -> int:
"""
Main entry point.
Returns:
Exit code
"""
parser = argparse.ArgumentParser(
description="Aniworld Application Setup"
)
parser.add_argument(
"--environment",
choices=["development", "production", "testing"],
default="development",
help="Environment to set up (default: development)"
)
parser.add_argument(
"--no-deps",
action="store_true",
help="Skip dependency installation"
)
args = parser.parse_args()
setup = SetupManager(
environment=args.environment,
skip_deps=args.no_deps
)
return await setup.run()
if __name__ == "__main__":
exit_code = asyncio.run(main())
sys.exit(exit_code)