variables fix
This commit is contained in:
0
.env.example
Normal file
0
.env.example
Normal file
1
.gitignore
vendored
1
.gitignore
vendored
@@ -134,3 +134,4 @@ price_tracker.db
|
|||||||
config-local.json
|
config-local.json
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
|
config.json
|
||||||
|
|||||||
88
DOCKER.md
88
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
|
./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
|
```bash
|
||||||
# Start the application
|
# Start the application
|
||||||
@@ -30,13 +40,13 @@ docker-compose logs -f
|
|||||||
docker-compose down
|
docker-compose down
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Manual Docker Run
|
### 4. Manual Docker Run
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Create directories for persistence
|
# Create directories for persistence
|
||||||
mkdir -p data logs
|
mkdir -p data logs
|
||||||
|
|
||||||
# Run the container
|
# Run with environment variables
|
||||||
docker run -d \
|
docker run -d \
|
||||||
--name price-tracker \
|
--name price-tracker \
|
||||||
--restart unless-stopped \
|
--restart unless-stopped \
|
||||||
@@ -45,9 +55,81 @@ docker run -d \
|
|||||||
-v $(pwd)/logs:/app/logs \
|
-v $(pwd)/logs:/app/logs \
|
||||||
-v $(pwd)/config.json:/app/config.json:ro \
|
-v $(pwd)/config.json:/app/config.json:ro \
|
||||||
-e FLASK_ENV=production \
|
-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
|
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
|
## Registry Deployment
|
||||||
|
|
||||||
### Push to Registry
|
### Push to Registry
|
||||||
|
|||||||
@@ -10,6 +10,14 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
|
|||||||
FLASK_APP=main.py \
|
FLASK_APP=main.py \
|
||||||
FLASK_ENV=production
|
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
|
# Install system dependencies
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
gcc \
|
gcc \
|
||||||
|
|||||||
@@ -5,11 +5,32 @@ services:
|
|||||||
build: .
|
build: .
|
||||||
container_name: price-tracker
|
container_name: price-tracker
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- .env # Load environment variables from .env file (optional)
|
||||||
ports:
|
ports:
|
||||||
- "5000:5000"
|
- "5000:5000"
|
||||||
environment:
|
environment:
|
||||||
- FLASK_ENV=production
|
- FLASK_ENV=production
|
||||||
- PYTHONUNBUFFERED=1
|
- 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:
|
volumes:
|
||||||
# Mount database and logs for persistence
|
# Mount database and logs for persistence
|
||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class Config:
|
|||||||
def __init__(self, config_path: Optional[str] = None):
|
def __init__(self, config_path: Optional[str] = None):
|
||||||
self.config_path = config_path or "config.json"
|
self.config_path = config_path or "config.json"
|
||||||
self._config = self._load_config()
|
self._config = self._load_config()
|
||||||
|
self._apply_env_overrides()
|
||||||
|
|
||||||
def _load_config(self) -> Dict[str, Any]:
|
def _load_config(self) -> Dict[str, Any]:
|
||||||
"""Load configuration from JSON file."""
|
"""Load configuration from JSON file."""
|
||||||
@@ -24,6 +25,89 @@ class Config:
|
|||||||
with open(config_file, 'r') as f:
|
with open(config_file, 'r') as f:
|
||||||
return json.load(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
|
@property
|
||||||
def database_path(self) -> str:
|
def database_path(self) -> str:
|
||||||
"""Get database file path."""
|
"""Get database file path."""
|
||||||
|
|||||||
115
test_env_config.py
Normal file
115
test_env_config.py
Normal file
@@ -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()
|
||||||
Reference in New Issue
Block a user