config.json handling

This commit is contained in:
Oli Passey
2025-06-30 21:23:13 +01:00
parent c1f2bfe5eb
commit 96932739d1
4 changed files with 458 additions and 6 deletions

View File

@@ -27,6 +27,8 @@
"enabled": false, "enabled": false,
"smtp_server": "smtp.gmail.com", "smtp_server": "smtp.gmail.com",
"smtp_port": 587, "smtp_port": 587,
"smtp_username": "",
"smtp_password": "",
"sender_email": "", "sender_email": "",
"sender_password": "", "sender_password": "",
"recipient_email": "" "recipient_email": ""

View File

@@ -4,26 +4,191 @@ Configuration management for the price tracker
import json import json
import os import os
import logging
from typing import Dict, Any, Optional from typing import Dict, Any, Optional
from pathlib import Path from pathlib import Path
class ConfigError(Exception):
"""Custom exception for configuration errors."""
pass
class Config: class Config:
"""Configuration manager for the price tracker application.""" """Configuration manager for the price tracker application."""
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_error = None
self._config = self._load_config() self._config = self._load_config()
self._apply_env_overrides() self._apply_env_overrides()
def _get_default_config(self) -> Dict[str, Any]:
"""Return a minimal default configuration."""
return {
"database": {
"path": "price_tracker.db"
},
"scraping": {
"delay_between_requests": 2,
"max_concurrent_requests": 1,
"timeout": 30,
"retry_attempts": 3,
"user_agents": [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
]
},
"notifications": {
"email": {
"enabled": False,
"smtp_server": "smtp.gmail.com",
"smtp_port": 587,
"smtp_username": "",
"smtp_password": "",
"sender_email": "",
"sender_password": "",
"recipient_email": ""
},
"webhook": {
"enabled": False,
"url": ""
}
},
"sites": {}
}
def _load_config(self) -> Dict[str, Any]: def _load_config(self) -> Dict[str, Any]:
"""Load configuration from JSON file.""" """Load configuration from JSON file with fallback to defaults."""
config_file = Path(self.config_path) config_file = Path(self.config_path)
if not config_file.exists():
raise FileNotFoundError(f"Config file not found: {self.config_path}")
with open(config_file, 'r') as f: # Check if file exists
return json.load(f) if not config_file.exists():
self._config_error = f"Configuration file not found: {self.config_path}"
logging.warning(f"Config file not found: {self.config_path}. Using default configuration.")
return self._get_default_config()
# Try to load and parse the file
try:
with open(config_file, 'r') as f:
content = f.read().strip()
if not content:
self._config_error = f"Configuration file is empty: {self.config_path}"
logging.warning(f"Config file is empty: {self.config_path}. Using default configuration.")
return self._get_default_config()
config = json.loads(content)
if not isinstance(config, dict):
self._config_error = f"Configuration file must contain a JSON object: {self.config_path}"
logging.warning(f"Invalid config structure in {self.config_path}. Using default configuration.")
return self._get_default_config()
return config
except json.JSONDecodeError as e:
self._config_error = f"Invalid JSON in configuration file {self.config_path}: {str(e)}"
logging.warning(f"JSON parsing error in {self.config_path}: {str(e)}. Using default configuration.")
return self._get_default_config()
except Exception as e:
self._config_error = f"Error reading configuration file {self.config_path}: {str(e)}"
logging.warning(f"Error reading {self.config_path}: {str(e)}. Using default configuration.")
return self._get_default_config()
def has_config_error(self) -> bool:
"""Check if there was an error loading the configuration."""
return self._config_error is not None
def get_config_error(self) -> Optional[str]:
"""Get the configuration error message if any."""
return self._config_error
def create_default_config_file(self) -> bool:
"""Create a default configuration file."""
try:
default_config = {
"database": {
"path": "price_tracker.db"
},
"scraping": {
"delay_between_requests": 2,
"max_concurrent_requests": 1,
"timeout": 30,
"retry_attempts": 3,
"special_pricing": {
"enabled": True,
"prefer_delivery_prices": True,
"detect_strikethrough": True,
"detect_was_now_patterns": True,
"detect_percentage_discounts": True,
"min_discount_threshold": 0.05,
"max_price_difference_ratio": 0.5
},
"user_agents": [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
]
},
"notifications": {
"email": {
"enabled": False,
"smtp_server": "smtp.gmail.com",
"smtp_port": 587,
"smtp_username": "",
"smtp_password": "",
"sender_email": "",
"sender_password": "",
"recipient_email": ""
},
"webhook": {
"enabled": False,
"url": ""
}
},
"sites": {
"jjfoodservice": {
"enabled": True,
"base_url": "https://www.jjfoodservice.com",
"selectors": {
"price": [".price-delivery", ".delivery-price", ".price"],
"delivery_price": [".price-delivery", ".delivery-price"],
"special_offer": [".special-offer", ".sale-price", ".offer-price"],
"title": ["h1"],
"availability": [".stock-status", ".availability"]
}
},
"atoz_catering": {
"enabled": True,
"base_url": "https://www.atoz-catering.co.uk",
"selectors": {
"price": [".my-price.price-offer", ".delivery-price", ".price"],
"delivery_price": [".delivery-price", ".price-delivery"],
"special_offer": [".my-price.price-offer", ".special-offer", ".sale-price"],
"title": ["h1"],
"availability": [".stock-status", ".availability"]
}
},
"amazon_uk": {
"enabled": True,
"base_url": "https://www.amazon.co.uk",
"selectors": {
"price": [".a-price-whole", ".a-price .a-offscreen", "#priceblock_ourprice"],
"special_offer": ["#priceblock_dealprice", ".a-price-strike .a-offscreen", ".a-price-was"],
"title": ["#productTitle"],
"availability": ["#availability span"]
}
}
}
}
with open(self.config_path, 'w') as f:
json.dump(default_config, f, indent=4)
logging.info(f"Created default configuration file: {self.config_path}")
return True
except Exception as e:
logging.error(f"Failed to create default config file: {str(e)}")
return False
def _apply_env_overrides(self): def _apply_env_overrides(self):
"""Apply environment variable overrides to configuration.""" """Apply environment variable overrides to configuration."""

View File

@@ -30,8 +30,41 @@ def create_app():
app = Flask(__name__, template_folder=template_dir) app = Flask(__name__, template_folder=template_dir)
app.config['SECRET_KEY'] = 'your-secret-key-change-this' app.config['SECRET_KEY'] = 'your-secret-key-change-this'
# Initialize components # Initialize configuration with error handling
config = Config() config = Config()
# Check for configuration errors
if config.has_config_error():
@app.route('/')
def setup_required():
"""Show setup page when configuration is missing or invalid."""
return render_template('setup.html',
error=config.get_config_error(),
config_path=config.config_path)
@app.route('/create-config', methods=['POST'])
def create_config():
"""Create a default configuration file."""
success = config.create_default_config_file()
if success:
return jsonify({
'success': True,
'message': 'Default configuration file created successfully. Please restart the application.'
})
else:
return jsonify({
'success': False,
'message': 'Failed to create configuration file. Check file permissions.'
})
@app.route('/health')
def health_check():
"""Health check endpoint."""
return jsonify({'status': 'error', 'message': 'Configuration required'})
return app
# Initialize other components only if config is valid
db_manager = DatabaseManager(config.database_path) db_manager = DatabaseManager(config.database_path)
scraper_manager = ScraperManager(config) scraper_manager = ScraperManager(config)
notification_manager = NotificationManager(config) notification_manager = NotificationManager(config)

252
templates/setup.html Normal file
View File

@@ -0,0 +1,252 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Price Tracker - Setup Required</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
.setup-container {
max-width: 800px;
margin: 50px auto;
padding: 20px;
}
.alert-custom {
border-radius: 10px;
border: none;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.card-custom {
border-radius: 15px;
border: none;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
}
.btn-custom {
border-radius: 25px;
padding: 10px 30px;
font-weight: 500;
}
.code-block {
background-color: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 15px;
font-family: 'Courier New', monospace;
font-size: 14px;
overflow-x: auto;
}
</style>
</head>
<body class="bg-light">
<div class="container setup-container">
<!-- Header -->
<div class="text-center mb-5">
<i class="fas fa-cog fa-4x text-primary mb-3"></i>
<h1 class="display-4 text-dark">Price Tracker Setup</h1>
<p class="lead text-muted">Configuration required to get started</p>
</div>
<!-- Error Alert -->
<div class="alert alert-warning alert-custom mb-4" role="alert">
<div class="d-flex align-items-center">
<i class="fas fa-exclamation-triangle fa-2x me-3"></i>
<div>
<h5 class="alert-heading mb-1">Configuration Issue</h5>
<p class="mb-0">{{ error }}</p>
</div>
</div>
</div>
<!-- Setup Options -->
<div class="row">
<!-- Option 1: Create Default Config -->
<div class="col-md-6 mb-4">
<div class="card card-custom h-100">
<div class="card-body text-center">
<i class="fas fa-magic fa-3x text-success mb-3"></i>
<h4 class="card-title">Quick Setup</h4>
<p class="card-text">Create a default configuration file automatically. This will set up basic settings to get you started quickly.</p>
<button class="btn btn-success btn-custom" onclick="createDefaultConfig()">
<i class="fas fa-wand-magic-sparkles me-2"></i>Create Default Config
</button>
</div>
</div>
</div>
<!-- Option 2: Manual Setup -->
<div class="col-md-6 mb-4">
<div class="card card-custom h-100">
<div class="card-body text-center">
<i class="fas fa-file-code fa-3x text-primary mb-3"></i>
<h4 class="card-title">Manual Setup</h4>
<p class="card-text">Create your own configuration file with custom settings. This gives you full control over the application settings.</p>
<button class="btn btn-primary btn-custom" data-bs-toggle="collapse" data-bs-target="#manualSetup">
<i class="fas fa-code me-2"></i>Manual Setup
</button>
</div>
</div>
</div>
</div>
<!-- Manual Setup Instructions (Collapsed) -->
<div class="collapse mt-4" id="manualSetup">
<div class="card card-custom">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="fas fa-book me-2"></i>Manual Configuration Instructions</h5>
</div>
<div class="card-body">
<h6>1. Create the configuration file:</h6>
<p>Create a file named <code>{{ config_path }}</code> in the root directory of the application.</p>
<h6>2. Add the following basic configuration:</h6>
<div class="code-block">
{
"database": {
"path": "price_tracker.db"
},
"scraping": {
"delay_between_requests": 2,
"max_concurrent_requests": 1,
"timeout": 30,
"retry_attempts": 3
},
"notifications": {
"email": {
"enabled": false,
"smtp_server": "smtp.gmail.com",
"smtp_port": 587,
"smtp_username": "",
"smtp_password": "",
"sender_email": "",
"recipient_email": ""
}
},
"sites": {
"jjfoodservice": {
"enabled": true,
"base_url": "https://www.jjfoodservice.com"
}
}
}
</div>
<h6 class="mt-3">3. Restart the application:</h6>
<p>After creating the configuration file, restart the Price Tracker application.</p>
<div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i>
<strong>Tip:</strong> You can also use environment variables to override configuration settings when running in Docker. Check the documentation for available environment variables.
</div>
</div>
</div>
</div>
<!-- Docker Environment Variables -->
<div class="card card-custom mt-4">
<div class="card-header bg-info text-white">
<h5 class="mb-0"><i class="fab fa-docker me-2"></i>Running with Docker?</h5>
</div>
<div class="card-body">
<p>If you're running this application in Docker, you can configure it using environment variables instead of a config file:</p>
<div class="code-block">
docker run -e DATABASE_PATH=/app/data/tracker.db \
-e EMAIL_ENABLED=true \
-e SMTP_SERVER=smtp.gmail.com \
-e SENDER_EMAIL=your-email@gmail.com \
-p 5000:5000 price-tracker
</div>
<p class="mt-2 mb-0">
<small class="text-muted">
<i class="fas fa-info-circle me-1"></i>
Available environment variables: DATABASE_PATH, EMAIL_ENABLED, SMTP_SERVER, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD, SENDER_EMAIL, RECIPIENT_EMAIL, WEBHOOK_ENABLED, WEBHOOK_URL
</small>
</p>
</div>
</div>
</div>
<!-- Loading Modal -->
<div class="modal fade" id="loadingModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-body text-center p-4">
<div class="spinner-border text-primary mb-3" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<h5>Creating Configuration...</h5>
<p class="mb-0 text-muted">Please wait while we set up your application.</p>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
function createDefaultConfig() {
// Show loading modal
const loadingModal = new bootstrap.Modal(document.getElementById('loadingModal'));
loadingModal.show();
// Make request to create config
fetch('/create-config', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
loadingModal.hide();
if (data.success) {
// Show success message
const alertHtml = `
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="fas fa-check-circle me-2"></i>
<strong>Success!</strong> ${data.message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
`;
document.querySelector('.setup-container').insertAdjacentHTML('afterbegin', alertHtml);
// Add refresh button
setTimeout(() => {
const refreshBtn = `
<div class="text-center mt-3">
<button class="btn btn-success btn-lg" onclick="window.location.reload()">
<i class="fas fa-refresh me-2"></i>Restart Application
</button>
</div>
`;
document.querySelector('.setup-container').insertAdjacentHTML('beforeend', refreshBtn);
}, 1000);
} else {
// Show error message
const alertHtml = `
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="fas fa-exclamation-circle me-2"></i>
<strong>Error!</strong> ${data.message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
`;
document.querySelector('.setup-container').insertAdjacentHTML('afterbegin', alertHtml);
}
})
.catch(error => {
loadingModal.hide();
console.error('Error:', error);
const alertHtml = `
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="fas fa-exclamation-circle me-2"></i>
<strong>Error!</strong> Failed to create configuration file. Please try manual setup.
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
`;
document.querySelector('.setup-container').insertAdjacentHTML('afterbegin', alertHtml);
});
}
</script>
</body>
</html>