- 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.
422 lines
12 KiB
Python
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)
|