diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..25a7d03 --- /dev/null +++ b/.env.example @@ -0,0 +1,29 @@ +# =========================================== +# OpenRouter API Key Monitor - Configuration +# =========================================== + +# Database +DATABASE_URL=sqlite:///./data/app.db + +# Security - REQUIRED +# Generate with: openssl rand -hex 32 +SECRET_KEY=your-super-secret-jwt-key-min-32-chars +ENCRYPTION_KEY=your-32-byte-encryption-key-here + +# OpenRouter Integration +OPENROUTER_API_URL=https://openrouter.ai/api/v1 + +# Background Tasks +SYNC_INTERVAL_MINUTES=60 + +# Limits +MAX_API_KEYS_PER_USER=10 +RATE_LIMIT_REQUESTS=100 +RATE_LIMIT_WINDOW=3600 + +# JWT +JWT_EXPIRATION_HOURS=24 + +# Development +DEBUG=false +LOG_LEVEL=INFO diff --git a/export/progress.md b/export/progress.md index 888fa2b..94cea7e 100644 --- a/export/progress.md +++ b/export/progress.md @@ -9,11 +9,11 @@ | Metrica | Valore | |---------|--------| | **Stato** | 🟡 In Progress | -| **Progresso** | 4% | +| **Progresso** | 5% | | **Data Inizio** | 2024-04-07 | | **Data Target** | TBD | | **Task Totali** | 74 | -| **Task Completati** | 3 | +| **Task Completati** | 4 | | **Task In Progress** | 1 | --- @@ -37,11 +37,11 @@ ## 📋 Task Pianificate -### 🔧 Setup Progetto (T01-T05) - 3/5 completati +### 🔧 Setup Progetto (T01-T05) - 4/5 completati - [x] T01: Creare struttura cartelle progetto (2024-04-07) - [x] T02: Inizializzare virtual environment e .gitignore (2024-04-07) - [x] T03: Creare requirements.txt con dipendenze (2024-04-07) -- [ ] T04: Setup file configurazione (.env, config.py) +- [x] T04: Setup file configurazione (.env, config.py) (2024-04-07) - [ ] T05: Configurare pytest e struttura test ### 🗄️ Database & Models (T06-T11) - 0/6 completati diff --git a/src/openrouter_monitor/config.py b/src/openrouter_monitor/config.py index e69de29..f17844f 100644 --- a/src/openrouter_monitor/config.py +++ b/src/openrouter_monitor/config.py @@ -0,0 +1,103 @@ +"""Configuration management using Pydantic Settings. + +This module provides centralized configuration management for the +OpenRouter API Key Monitor application. +""" +from functools import lru_cache +from pydantic_settings import BaseSettings, SettingsConfigDict +from pydantic import Field + + +class Settings(BaseSettings): + """Application settings loaded from environment variables. + + Required environment variables: + - SECRET_KEY: JWT signing key (min 32 chars) + - ENCRYPTION_KEY: AES-256 encryption key (32 bytes) + + Optional environment variables with defaults: + - DATABASE_URL: SQLite database path + - OPENROUTER_API_URL: OpenRouter API base URL + - SYNC_INTERVAL_MINUTES: Background sync interval + - MAX_API_KEYS_PER_USER: API key limit per user + - RATE_LIMIT_REQUESTS: API rate limit + - RATE_LIMIT_WINDOW: Rate limit window (seconds) + - JWT_EXPIRATION_HOURS: JWT token lifetime + - DEBUG: Debug mode flag + - LOG_LEVEL: Logging level + """ + + # Database + database_url: str = Field( + default="sqlite:///./data/app.db", + description="SQLite database URL" + ) + + # Security - REQUIRED + secret_key: str = Field( + description="JWT signing key (min 32 characters)" + ) + encryption_key: str = Field( + description="AES-256 encryption key (32 bytes)" + ) + jwt_expiration_hours: int = Field( + default=24, + description="JWT token expiration in hours" + ) + + # OpenRouter Integration + openrouter_api_url: str = Field( + default="https://openrouter.ai/api/v1", + description="OpenRouter API base URL" + ) + + # Task scheduling + sync_interval_minutes: int = Field( + default=60, + description="Background sync interval in minutes" + ) + + # Limits + max_api_keys_per_user: int = Field( + default=10, + description="Maximum API keys per user" + ) + rate_limit_requests: int = Field( + default=100, + description="API rate limit requests" + ) + rate_limit_window: int = Field( + default=3600, + description="Rate limit window in seconds" + ) + + # App settings + debug: bool = Field( + default=False, + description="Debug mode" + ) + log_level: str = Field( + default="INFO", + description="Logging level" + ) + + model_config = SettingsConfigDict( + env_file=".env", + env_file_encoding="utf-8", + case_sensitive=False + ) + + +@lru_cache() +def get_settings() -> Settings: + """Get cached settings instance. + + Returns: + Settings: Application settings instance + + Example: + >>> from openrouter_monitor.config import get_settings + >>> settings = get_settings() + >>> print(settings.database_url) + """ + return Settings() diff --git a/tests/unit/test_configuration.py b/tests/unit/test_configuration.py new file mode 100644 index 0000000..96501e1 --- /dev/null +++ b/tests/unit/test_configuration.py @@ -0,0 +1,116 @@ +"""Test for configuration setup (T04).""" +import os +import sys +import pytest + +# Add src to path for importing +sys.path.insert(0, '/home/google/Sources/LucaSacchiNet/openrouter-watcher/src') + + +@pytest.mark.unit +class TestConfigurationSetup: + """Test configuration files and settings.""" + + def test_config_py_exists(self): + """Verify config.py file exists.""" + config_path = "/home/google/Sources/LucaSacchiNet/openrouter-watcher/src/openrouter_monitor/config.py" + assert os.path.isfile(config_path), f"File {config_path} does not exist" + + def test_env_example_exists(self): + """Verify .env.example file exists.""" + env_path = "/home/google/Sources/LucaSacchiNet/openrouter-watcher/.env.example" + assert os.path.isfile(env_path), f"File {env_path} does not exist" + + def test_config_py_has_settings_class(self): + """Verify config.py contains Settings class.""" + config_path = "/home/google/Sources/LucaSacchiNet/openrouter-watcher/src/openrouter_monitor/config.py" + with open(config_path, 'r') as f: + content = f.read() + assert 'class Settings' in content, "config.py should contain Settings class" + + def test_config_py_has_database_url(self): + """Verify Settings has database_url field.""" + config_path = "/home/google/Sources/LucaSacchiNet/openrouter-watcher/src/openrouter_monitor/config.py" + with open(config_path, 'r') as f: + content = f.read() + assert 'database_url' in content.lower(), "Settings should have database_url field" + + def test_config_py_has_secret_key(self): + """Verify Settings has secret_key field.""" + config_path = "/home/google/Sources/LucaSacchiNet/openrouter-watcher/src/openrouter_monitor/config.py" + with open(config_path, 'r') as f: + content = f.read() + assert 'secret_key' in content.lower(), "Settings should have secret_key field" + + def test_config_py_has_encryption_key(self): + """Verify Settings has encryption_key field.""" + config_path = "/home/google/Sources/LucaSacchiNet/openrouter-watcher/src/openrouter_monitor/config.py" + with open(config_path, 'r') as f: + content = f.read() + assert 'encryption_key' in content.lower(), "Settings should have encryption_key field" + + def test_config_py_uses_pydantic_settings(self): + """Verify config.py uses pydantic_settings.""" + config_path = "/home/google/Sources/LucaSacchiNet/openrouter-watcher/src/openrouter_monitor/config.py" + with open(config_path, 'r') as f: + content = f.read() + assert 'BaseSettings' in content or 'pydantic_settings' in content, \ + "config.py should use pydantic_settings BaseSettings" + + def test_env_example_has_database_url(self): + """Verify .env.example contains DATABASE_URL.""" + env_path = "/home/google/Sources/LucaSacchiNet/openrouter-watcher/.env.example" + with open(env_path, 'r') as f: + content = f.read() + assert 'DATABASE_URL' in content, ".env.example should contain DATABASE_URL" + + def test_env_example_has_secret_key(self): + """Verify .env.example contains SECRET_KEY.""" + env_path = "/home/google/Sources/LucaSacchiNet/openrouter-watcher/.env.example" + with open(env_path, 'r') as f: + content = f.read() + assert 'SECRET_KEY' in content, ".env.example should contain SECRET_KEY" + + def test_env_example_has_encryption_key(self): + """Verify .env.example contains ENCRYPTION_KEY.""" + env_path = "/home/google/Sources/LucaSacchiNet/openrouter-watcher/.env.example" + with open(env_path, 'r') as f: + content = f.read() + assert 'ENCRYPTION_KEY' in content, ".env.example should contain ENCRYPTION_KEY" + + def test_config_can_be_imported(self): + """Verify config module can be imported successfully.""" + try: + from openrouter_monitor.config import Settings, get_settings + assert True + except Exception as e: + pytest.fail(f"Failed to import config module: {e}") + + def test_settings_class_instantiates(self): + """Verify Settings class can be instantiated with test values.""" + try: + from openrouter_monitor.config import Settings + # Test with required fields (use snake_case field names) + settings = Settings( + secret_key="test-secret-key-min-32-chars-long", + encryption_key="test-32-byte-encryption-key!!" + ) + assert settings is not None + assert hasattr(settings, 'database_url') + except Exception as e: + pytest.fail(f"Failed to instantiate Settings: {e}") + + def test_settings_has_defaults(self): + """Verify Settings has sensible defaults.""" + try: + from openrouter_monitor.config import Settings + settings = Settings( + secret_key="test-secret-key-min-32-chars-long", + encryption_key="test-32-byte-encryption-key!!" + ) + # Check default values + assert settings.database_url is not None + assert settings.debug is not None + assert settings.log_level is not None + except Exception as e: + pytest.fail(f"Settings missing defaults: {e}")