From c1f2bfe5eba4899c35c50e54e42d7de78dba397c Mon Sep 17 00:00:00 2001 From: Oli Passey Date: Mon, 30 Jun 2025 21:11:36 +0100 Subject: [PATCH] variables fix --- .env.example | 0 .gitignore | 1 + DOCKER.md | 88 ++++++++++++++++++++++++++++++++-- Dockerfile | 8 ++++ docker-compose.yml | 21 +++++++++ src/config.py | 84 +++++++++++++++++++++++++++++++++ test_env_config.py | 115 +++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 314 insertions(+), 3 deletions(-) create mode 100644 .env.example create mode 100644 test_env_config.py diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e69de29 diff --git a/.gitignore b/.gitignore index 96ac300..071cd9d 100644 --- a/.gitignore +++ b/.gitignore @@ -134,3 +134,4 @@ price_tracker.db config-local.json .vscode/ .idea/ +config.json diff --git a/DOCKER.md b/DOCKER.md index d1734f1..cf9f3f8 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -17,7 +17,17 @@ This guide covers how to build, deploy, and run the Price Tracker application us ./build.sh latest your-registry.com ``` -### 2. Run with Docker Compose (Recommended) +### 2. Configure Environment Variables (Optional) + +```bash +# Copy the example environment file +cp .env.example .env + +# Edit the .env file with your settings +nano .env +``` + +### 3. Run with Docker Compose (Recommended) ```bash # Start the application @@ -30,13 +40,13 @@ docker-compose logs -f docker-compose down ``` -### 3. Manual Docker Run +### 4. Manual Docker Run ```bash # Create directories for persistence mkdir -p data logs -# Run the container +# Run with environment variables docker run -d \ --name price-tracker \ --restart unless-stopped \ @@ -45,9 +55,81 @@ docker run -d \ -v $(pwd)/logs:/app/logs \ -v $(pwd)/config.json:/app/config.json:ro \ -e FLASK_ENV=production \ + -e DATABASE_PATH=/app/data/price_tracker.db \ + -e EMAIL_ENABLED=true \ + -e SMTP_SERVER=smtp.gmail.com \ + -e SENDER_EMAIL=your-email@gmail.com \ + -e SENDER_PASSWORD=your-app-password \ + -e RECIPIENT_EMAIL=alerts@yourdomain.com \ price-tracker:latest ``` +## Environment Variable Configuration + +The application supports environment variable overrides for flexible deployment configurations. This allows you to customize settings without modifying the `config.json` file. + +### Available Environment Variables + +#### Database Configuration +- `DATABASE_PATH` - Path to SQLite database file (default: `/app/data/price_tracker.db`) + +#### Scraping Configuration +- `DELAY_BETWEEN_REQUESTS` - Delay between requests in seconds (default: `2`) +- `MAX_CONCURRENT_REQUESTS` - Maximum concurrent requests (default: `1`) +- `REQUEST_TIMEOUT` - Request timeout in seconds (default: `30`) +- `RETRY_ATTEMPTS` - Number of retry attempts (default: `3`) + +#### Email Notifications +- `EMAIL_ENABLED` - Enable email notifications (`true`/`false`) +- `SMTP_SERVER` - SMTP server hostname (e.g., `smtp.gmail.com`) +- `SMTP_PORT` - SMTP server port (e.g., `587`) +- `SENDER_EMAIL` - Email address to send from +- `SENDER_PASSWORD` - Email password or app password +- `RECIPIENT_EMAIL` - Email address to send alerts to + +#### Webhook Notifications +- `WEBHOOK_ENABLED` - Enable webhook notifications (`true`/`false`) +- `WEBHOOK_URL` - Webhook URL for sending alerts + +### Configuration Priority + +Environment variables take precedence over `config.json` settings: +1. **Environment Variables** (highest priority) +2. **config.json** file (fallback) + +### Using .env Files + +Create a `.env` file from the example: + +```bash +cp .env.example .env +``` + +Edit the `.env` file with your settings: + +```env +# Database +DATABASE_PATH=/app/data/price_tracker.db + +# Email notifications +EMAIL_ENABLED=true +SMTP_SERVER=smtp.gmail.com +SMTP_PORT=587 +SENDER_EMAIL=alerts@mydomain.com +SENDER_PASSWORD=my-app-password +RECIPIENT_EMAIL=me@mydomain.com + +# Scraping +DELAY_BETWEEN_REQUESTS=3 +MAX_CONCURRENT_REQUESTS=2 +``` + +Docker Compose will automatically load the `.env` file: + +```bash +docker-compose up -d +``` + ## Registry Deployment ### Push to Registry diff --git a/Dockerfile b/Dockerfile index e5e8a3f..210e1f2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,6 +10,14 @@ ENV PYTHONDONTWRITEBYTECODE=1 \ FLASK_APP=main.py \ FLASK_ENV=production +# Optional: Set default configuration via environment variables +# These can be overridden when running the container +ENV DATABASE_PATH=/app/data/price_tracker.db \ + DELAY_BETWEEN_REQUESTS=2 \ + MAX_CONCURRENT_REQUESTS=1 \ + REQUEST_TIMEOUT=30 \ + RETRY_ATTEMPTS=3 + # Install system dependencies RUN apt-get update && apt-get install -y \ gcc \ diff --git a/docker-compose.yml b/docker-compose.yml index dc1dd54..6b8c02c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,11 +5,32 @@ services: build: . container_name: price-tracker restart: unless-stopped + env_file: + - .env # Load environment variables from .env file (optional) ports: - "5000:5000" environment: - FLASK_ENV=production - PYTHONUNBUFFERED=1 + # Database configuration + - DATABASE_PATH=/app/data/price_tracker.db + # Scraping configuration + - DELAY_BETWEEN_REQUESTS=2 + - MAX_CONCURRENT_REQUESTS=1 + - REQUEST_TIMEOUT=30 + - RETRY_ATTEMPTS=3 + # Email notifications (uncomment and set values to enable) + # - EMAIL_ENABLED=true + # - SMTP_SERVER=smtp.gmail.com + # - SMTP_PORT=587 + # - SMTP_USERNAME=your-smtp-username # May be same as sender email + # - SMTP_PASSWORD=your-smtp-password + # - SENDER_EMAIL=your-email@gmail.com + # - SENDER_PASSWORD=your-app-password + # - RECIPIENT_EMAIL=alerts@yourdomain.com + # Webhook notifications (uncomment and set URL to enable) + # - WEBHOOK_ENABLED=true + # - WEBHOOK_URL=https://your-webhook-url.com/notify volumes: # Mount database and logs for persistence - ./data:/app/data diff --git a/src/config.py b/src/config.py index 30dbfab..e10cd15 100644 --- a/src/config.py +++ b/src/config.py @@ -14,6 +14,7 @@ class Config: def __init__(self, config_path: Optional[str] = None): self.config_path = config_path or "config.json" self._config = self._load_config() + self._apply_env_overrides() def _load_config(self) -> Dict[str, Any]: """Load configuration from JSON file.""" @@ -24,6 +25,89 @@ class Config: with open(config_file, 'r') as f: return json.load(f) + def _apply_env_overrides(self): + """Apply environment variable overrides to configuration.""" + # Database path override + if os.getenv('DATABASE_PATH'): + if 'database' not in self._config: + self._config['database'] = {} + self._config['database']['path'] = os.getenv('DATABASE_PATH') + + # Email notification overrides + email_env_vars = { + 'SMTP_SERVER': ['notifications', 'email', 'smtp_server'], + 'SMTP_PORT': ['notifications', 'email', 'smtp_port'], + 'SMTP_USERNAME': ['notifications', 'email', 'smtp_username'], + 'SMTP_PASSWORD': ['notifications', 'email', 'smtp_password'], + 'SENDER_EMAIL': ['notifications', 'email', 'sender_email'], + 'SENDER_PASSWORD': ['notifications', 'email', 'sender_password'], + 'RECIPIENT_EMAIL': ['notifications', 'email', 'recipient_email'], + 'EMAIL_ENABLED': ['notifications', 'email', 'enabled'] + } + + for env_var, config_path in email_env_vars.items(): + env_value = os.getenv(env_var) + if env_value is not None: + self._set_nested_config(config_path, env_value) + + # Webhook notification overrides + webhook_env_vars = { + 'WEBHOOK_URL': ['notifications', 'webhook', 'url'], + 'WEBHOOK_ENABLED': ['notifications', 'webhook', 'enabled'] + } + + for env_var, config_path in webhook_env_vars.items(): + env_value = os.getenv(env_var) + if env_value is not None: + self._set_nested_config(config_path, env_value) + + # Scraping configuration overrides + scraping_env_vars = { + 'DELAY_BETWEEN_REQUESTS': ['scraping', 'delay_between_requests'], + 'MAX_CONCURRENT_REQUESTS': ['scraping', 'max_concurrent_requests'], + 'REQUEST_TIMEOUT': ['scraping', 'timeout'], + 'RETRY_ATTEMPTS': ['scraping', 'retry_attempts'] + } + + for env_var, config_path in scraping_env_vars.items(): + env_value = os.getenv(env_var) + if env_value is not None: + self._set_nested_config(config_path, env_value) + + def _set_nested_config(self, path: list, value: str): + """Set a nested configuration value from environment variable.""" + # Convert string values to appropriate types + converted_value = self._convert_env_value(value) + + # Navigate to the correct nested location + current = self._config + for key in path[:-1]: + if key not in current: + current[key] = {} + current = current[key] + + current[path[-1]] = converted_value + + def _convert_env_value(self, value: str): + """Convert environment variable string to appropriate type.""" + # Handle boolean values + if value.lower() in ('true', 'false'): + return value.lower() == 'true' + + # Handle integer values + if value.isdigit(): + return int(value) + + # Handle float values + try: + if '.' in value: + return float(value) + except ValueError: + pass + + # Return as string + return value + @property def database_path(self) -> str: """Get database file path.""" diff --git a/test_env_config.py b/test_env_config.py new file mode 100644 index 0000000..5b52243 --- /dev/null +++ b/test_env_config.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +""" +Test script to verify environment variable configuration overrides +""" + +import os +import sys +import tempfile +import json +from pathlib import Path + +# Add src to path +sys.path.insert(0, 'src') + +from config import Config + +def test_env_overrides(): + """Test that environment variables properly override config.json settings.""" + + # Create a temporary config file + config_data = { + "database": {"path": "default.db"}, + "scraping": { + "delay_between_requests": 1, + "max_concurrent_requests": 5, + "timeout": 10, + "retry_attempts": 1 + }, + "notifications": { + "email": { + "enabled": False, + "smtp_server": "default.smtp.com", + "smtp_port": 25, + "sender_email": "default@example.com", + "sender_password": "default_pass", + "recipient_email": "default_recipient@example.com" + }, + "webhook": { + "enabled": False, + "url": "http://default.webhook.com" + } + } + } + + # Create temporary config file + with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f: + json.dump(config_data, f) + temp_config_path = f.name + + try: + print("Testing environment variable overrides...") + + # Set environment variables + os.environ['DATABASE_PATH'] = '/custom/database.db' + os.environ['DELAY_BETWEEN_REQUESTS'] = '5' + os.environ['MAX_CONCURRENT_REQUESTS'] = '10' + os.environ['REQUEST_TIMEOUT'] = '60' + os.environ['RETRY_ATTEMPTS'] = '5' + os.environ['EMAIL_ENABLED'] = 'true' + os.environ['SMTP_SERVER'] = 'custom.smtp.com' + os.environ['SMTP_PORT'] = '587' + os.environ['SENDER_EMAIL'] = 'custom@example.com' + os.environ['SENDER_PASSWORD'] = 'custom_pass' + os.environ['RECIPIENT_EMAIL'] = 'custom_recipient@example.com' + os.environ['WEBHOOK_ENABLED'] = 'true' + os.environ['WEBHOOK_URL'] = 'http://custom.webhook.com' + + # Load configuration + config = Config(temp_config_path) + + # Test database path + assert config.database_path == '/custom/database.db', f"Expected '/custom/database.db', got '{config.database_path}'" + print("āœ“ Database path override works") + + # Test scraping config + assert config.delay_between_requests == 5, f"Expected 5, got {config.delay_between_requests}" + assert config.max_concurrent_requests == 10, f"Expected 10, got {config.max_concurrent_requests}" + assert config.timeout == 60, f"Expected 60, got {config.timeout}" + assert config.retry_attempts == 5, f"Expected 5, got {config.retry_attempts}" + print("āœ“ Scraping configuration overrides work") + + # Test email config + email_config = config.notification_config['email'] + assert email_config['enabled'] == True, f"Expected True, got {email_config['enabled']}" + assert email_config['smtp_server'] == 'custom.smtp.com', f"Expected 'custom.smtp.com', got '{email_config['smtp_server']}'" + assert email_config['smtp_port'] == 587, f"Expected 587, got {email_config['smtp_port']}" + assert email_config['sender_email'] == 'custom@example.com', f"Expected 'custom@example.com', got '{email_config['sender_email']}'" + assert email_config['sender_password'] == 'custom_pass', f"Expected 'custom_pass', got '{email_config['sender_password']}'" + assert email_config['recipient_email'] == 'custom_recipient@example.com', f"Expected 'custom_recipient@example.com', got '{email_config['recipient_email']}'" + print("āœ“ Email configuration overrides work") + + # Test webhook config + webhook_config = config.notification_config['webhook'] + assert webhook_config['enabled'] == True, f"Expected True, got {webhook_config['enabled']}" + assert webhook_config['url'] == 'http://custom.webhook.com', f"Expected 'http://custom.webhook.com', got '{webhook_config['url']}'" + print("āœ“ Webhook configuration overrides work") + + print("\nšŸŽ‰ All environment variable overrides are working correctly!") + + finally: + # Clean up + Path(temp_config_path).unlink() + + # Clean up environment variables + env_vars = ['DATABASE_PATH', 'DELAY_BETWEEN_REQUESTS', 'MAX_CONCURRENT_REQUESTS', + 'REQUEST_TIMEOUT', 'RETRY_ATTEMPTS', 'EMAIL_ENABLED', 'SMTP_SERVER', + 'SMTP_PORT', 'SENDER_EMAIL', 'SENDER_PASSWORD', 'RECIPIENT_EMAIL', + 'WEBHOOK_ENABLED', 'WEBHOOK_URL'] + + for var in env_vars: + if var in os.environ: + del os.environ[var] + +if __name__ == '__main__': + test_env_overrides()