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,
"smtp_server": "smtp.gmail.com",
"smtp_port": 587,
"smtp_username": "",
"smtp_password": "",
"sender_email": "",
"sender_password": "",
"recipient_email": ""

View File

@@ -4,26 +4,191 @@ Configuration management for the price tracker
import json
import os
import logging
from typing import Dict, Any, Optional
from pathlib import Path
class ConfigError(Exception):
"""Custom exception for configuration errors."""
pass
class Config:
"""Configuration manager for the price tracker application."""
def __init__(self, config_path: Optional[str] = None):
self.config_path = config_path or "config.json"
self._config_error = None
self._config = self._load_config()
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]:
"""Load configuration from JSON file."""
"""Load configuration from JSON file with fallback to defaults."""
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:
return json.load(f)
# Check if file exists
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):
"""Apply environment variable overrides to configuration."""

View File

@@ -30,8 +30,41 @@ def create_app():
app = Flask(__name__, template_folder=template_dir)
app.config['SECRET_KEY'] = 'your-secret-key-change-this'
# Initialize components
# Initialize configuration with error handling
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)
scraper_manager = ScraperManager(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>