697 lines
34 KiB
Python
697 lines
34 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Web API Server for On-Demand Report Generation
|
|
|
|
Provides REST API endpoints to trigger report generation on demand.
|
|
"""
|
|
|
|
import logging
|
|
from pathlib import Path
|
|
from typing import Optional, List, Dict
|
|
import json
|
|
from datetime import datetime
|
|
import shutil
|
|
import os
|
|
|
|
try:
|
|
from flask import Flask, jsonify, request, send_from_directory
|
|
from flask_cors import CORS
|
|
from werkzeug.utils import secure_filename
|
|
FLASK_AVAILABLE = True
|
|
except ImportError:
|
|
FLASK_AVAILABLE = False
|
|
logging.warning("Flask not installed. API server features disabled.")
|
|
|
|
from config import load_config
|
|
from report_generator import generate_report
|
|
from sharepoint_downloader import download_from_sharepoint
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
app = None
|
|
config = None
|
|
|
|
|
|
def cleanup_old_reports(output_dir: Path, reports_dir: Path, max_reports: int = 10):
|
|
"""
|
|
Cleanup old reports and Excel files, keeping only the last max_reports.
|
|
|
|
Args:
|
|
output_dir: Directory containing report HTML/JSON files
|
|
reports_dir: Directory containing Excel files
|
|
max_reports: Maximum number of reports to keep
|
|
"""
|
|
try:
|
|
# Get all report HTML files sorted by modification time (newest first)
|
|
html_files = sorted(output_dir.glob('report-*.html'), key=lambda p: p.stat().st_mtime, reverse=True)
|
|
|
|
if len(html_files) <= max_reports:
|
|
return # No cleanup needed
|
|
|
|
# Get reports to delete (oldest ones)
|
|
reports_to_delete = html_files[max_reports:]
|
|
|
|
deleted_count = 0
|
|
for html_file in reports_to_delete:
|
|
report_id = html_file.stem
|
|
|
|
# Delete HTML file
|
|
try:
|
|
html_file.unlink()
|
|
logger.info(f"Deleted old report HTML: {html_file.name}")
|
|
deleted_count += 1
|
|
except Exception as e:
|
|
logger.warning(f"Failed to delete {html_file.name}: {e}")
|
|
|
|
# Delete corresponding JSON file
|
|
json_file = output_dir / f"{report_id}.json"
|
|
if json_file.exists():
|
|
try:
|
|
json_file.unlink()
|
|
logger.info(f"Deleted old report JSON: {json_file.name}")
|
|
except Exception as e:
|
|
logger.warning(f"Failed to delete {json_file.name}: {e}")
|
|
|
|
# Cleanup Excel files - keep only files associated with remaining reports
|
|
if reports_dir.exists():
|
|
excel_files = list(reports_dir.glob('*.xlsx')) + list(reports_dir.glob('*.xls'))
|
|
|
|
if len(excel_files) > max_reports:
|
|
# Sort by modification time and delete oldest
|
|
excel_files_sorted = sorted(excel_files, key=lambda p: p.stat().st_mtime, reverse=True)
|
|
excel_to_delete = excel_files_sorted[max_reports:]
|
|
|
|
for excel_file in excel_to_delete:
|
|
try:
|
|
excel_file.unlink()
|
|
logger.info(f"Deleted old Excel file: {excel_file.name}")
|
|
except Exception as e:
|
|
logger.warning(f"Failed to delete {excel_file.name}: {e}")
|
|
|
|
logger.info(f"Cleanup completed: deleted {deleted_count} old report(s)")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error during cleanup: {e}", exc_info=True)
|
|
|
|
|
|
def create_app(config_path: Optional[str] = None):
|
|
"""Create and configure Flask app."""
|
|
global app, config
|
|
|
|
if not FLASK_AVAILABLE:
|
|
raise ImportError(
|
|
"Flask is required for API server. "
|
|
"Install it with: pip install flask flask-cors"
|
|
)
|
|
|
|
app = Flask(__name__)
|
|
CORS(app) # Enable CORS for all routes
|
|
|
|
config = load_config(config_path)
|
|
api_config = config.get('api', {})
|
|
sharepoint_config = config.get('sharepoint', {})
|
|
report_config = config.get('report', {})
|
|
|
|
# Resolve paths relative to script location, not current working directory
|
|
script_dir = Path(__file__).parent.absolute()
|
|
|
|
# Convert relative paths to absolute paths relative to script directory
|
|
if 'output_dir' in report_config and report_config['output_dir']:
|
|
output_dir = Path(report_config['output_dir'])
|
|
if not output_dir.is_absolute():
|
|
report_config['output_dir'] = str(script_dir / output_dir)
|
|
|
|
if 'reports_dir' in report_config and report_config['reports_dir']:
|
|
reports_dir = Path(report_config['reports_dir'])
|
|
if not reports_dir.is_absolute():
|
|
report_config['reports_dir'] = str(script_dir / reports_dir)
|
|
|
|
# Store config in app context
|
|
app.config['API_KEY'] = api_config.get('api_key')
|
|
app.config['SHAREPOINT_CONFIG'] = sharepoint_config
|
|
app.config['REPORT_CONFIG'] = report_config
|
|
|
|
@app.route('/health', methods=['GET'])
|
|
def health():
|
|
"""Health check endpoint."""
|
|
return jsonify({
|
|
'status': 'healthy',
|
|
'service': 'vendor-report-generator'
|
|
})
|
|
|
|
@app.route('/api/generate', methods=['POST'])
|
|
def generate_report_endpoint():
|
|
"""
|
|
Generate report on demand.
|
|
|
|
Request body (optional):
|
|
{
|
|
"download_from_sharepoint": true,
|
|
"reports_dir": "reports",
|
|
"output_file": "output/report.json"
|
|
}
|
|
"""
|
|
# Check API key if configured
|
|
api_key = app.config.get('API_KEY')
|
|
if api_key:
|
|
provided_key = request.headers.get('X-API-Key') or request.json.get('api_key') if request.json else None
|
|
if provided_key != api_key:
|
|
return jsonify({'error': 'Invalid API key'}), 401
|
|
|
|
try:
|
|
request_data = request.json or {}
|
|
download_from_sp = request_data.get('download_from_sharepoint', True) # Default to True for backward compatibility
|
|
downloaded_files = [] # Initialize here for scope
|
|
|
|
# Get report config early - needed for error handling
|
|
report_config = app.config['REPORT_CONFIG']
|
|
|
|
# Download from SharePoint if requested AND no manual upload happened
|
|
# If download_from_sharepoint is False, it means manual upload was used
|
|
if download_from_sp:
|
|
sp_config = app.config['SHAREPOINT_CONFIG']
|
|
if not sp_config.get('enabled'):
|
|
return jsonify({
|
|
'error': 'SharePoint is not enabled in configuration'
|
|
}), 400
|
|
|
|
logger.info("Downloading files from SharePoint...")
|
|
try:
|
|
downloaded = download_from_sharepoint(
|
|
site_url=sp_config['site_url'],
|
|
folder_path=sp_config.get('folder_path'),
|
|
file_path=sp_config.get('file_path'),
|
|
local_dir=sp_config.get('local_dir', 'reports'),
|
|
tenant_id=sp_config.get('tenant_id'),
|
|
client_id=sp_config.get('client_id'),
|
|
client_secret=sp_config.get('client_secret'),
|
|
use_app_authentication=sp_config.get('use_app_authentication', True),
|
|
file_pattern=sp_config.get('file_pattern'),
|
|
overwrite=sp_config.get('overwrite', True)
|
|
)
|
|
downloaded_files = downloaded if downloaded else []
|
|
logger.info(f"Downloaded {len(downloaded_files)} file(s) from SharePoint: {downloaded_files}")
|
|
|
|
# If SharePoint download failed (no files downloaded), check if we have existing files
|
|
if len(downloaded_files) == 0:
|
|
logger.warning("SharePoint download returned 0 files. This could mean:")
|
|
logger.warning("1. SharePoint permissions issue (401/403 error)")
|
|
logger.warning("2. No files found in the specified folder")
|
|
logger.warning("3. Site access not granted (Resource-Specific Consent needed)")
|
|
logger.warning("Checking if existing files are available in reports directory...")
|
|
|
|
# Check if there are existing files we can use
|
|
reports_dir_path = Path(report_config.get('reports_dir', 'reports'))
|
|
if not reports_dir_path.is_absolute():
|
|
script_dir = Path(__file__).parent.absolute()
|
|
reports_dir_path = script_dir / reports_dir_path
|
|
|
|
if reports_dir_path.exists():
|
|
existing_files = list(reports_dir_path.glob('*.xlsx')) + list(reports_dir_path.glob('*.xls'))
|
|
if existing_files:
|
|
logger.warning(f"Found {len(existing_files)} existing file(s) in reports directory. Will use these instead.")
|
|
logger.warning("NOTE: These may be old files. Consider using manual upload for fresh data.")
|
|
else:
|
|
logger.error("No files available - neither from SharePoint nor existing files.")
|
|
return jsonify({
|
|
'error': 'SharePoint download failed and no existing files found',
|
|
'details': 'SharePoint access may require Resource-Specific Consent (RSC). Please use manual file upload or fix SharePoint permissions.',
|
|
'sharepoint_error': True
|
|
}), 500
|
|
except Exception as e:
|
|
logger.error(f"Failed to download from SharePoint: {e}", exc_info=True)
|
|
# Check if we have existing files as fallback
|
|
reports_dir_path = Path(report_config.get('reports_dir', 'reports'))
|
|
if not reports_dir_path.is_absolute():
|
|
script_dir = Path(__file__).parent.absolute()
|
|
reports_dir_path = script_dir / reports_dir_path
|
|
|
|
if reports_dir_path.exists():
|
|
existing_files = list(reports_dir_path.glob('*.xlsx')) + list(reports_dir_path.glob('*.xls'))
|
|
if existing_files:
|
|
logger.warning(f"SharePoint download failed, but found {len(existing_files)} existing file(s). Will use these.")
|
|
downloaded_files = [] # Continue with existing files
|
|
else:
|
|
return jsonify({
|
|
'error': f'SharePoint download failed: {str(e)}',
|
|
'details': 'No existing files found. Please use manual file upload or fix SharePoint permissions.',
|
|
'sharepoint_error': True
|
|
}), 500
|
|
else:
|
|
return jsonify({
|
|
'error': f'SharePoint download failed: {str(e)}',
|
|
'details': 'Reports directory does not exist. Please use manual file upload or fix SharePoint permissions.',
|
|
'sharepoint_error': True
|
|
}), 500
|
|
|
|
# Generate report with timestamp
|
|
reports_dir = request_data.get('reports_dir', report_config.get('reports_dir', 'reports'))
|
|
output_dir_str = report_config.get('output_dir', 'output')
|
|
output_dir = Path(output_dir_str)
|
|
if not output_dir.is_absolute():
|
|
script_dir = Path(__file__).parent.absolute()
|
|
output_dir = script_dir / output_dir
|
|
|
|
# Create timestamped filename
|
|
timestamp = datetime.now().strftime('%Y-%m-%d-%H-%M-%S')
|
|
report_id = f"report-{timestamp}"
|
|
output_file = str(output_dir / f"{report_id}.json")
|
|
|
|
# Log which files will be used for generation
|
|
reports_dir_path = Path(reports_dir)
|
|
if not reports_dir_path.is_absolute():
|
|
script_dir = Path(__file__).parent.absolute()
|
|
reports_dir_path = script_dir / reports_dir_path
|
|
|
|
logger.info(f"Generating report from {reports_dir_path.absolute()}...")
|
|
logger.info(f"Reports directory exists: {reports_dir_path.exists()}")
|
|
|
|
# Determine which files to use for generation
|
|
# CRITICAL: Only use files that were just downloaded/uploaded, not old ones
|
|
if downloaded_files:
|
|
# Files were downloaded from SharePoint - use only those
|
|
logger.info(f"Using {len(downloaded_files)} file(s) downloaded from SharePoint")
|
|
# Verify that reports_dir only contains the downloaded files (should be empty of old files)
|
|
all_files = list(reports_dir_path.glob('*.xlsx')) + list(reports_dir_path.glob('*.xls'))
|
|
downloaded_file_paths = [Path(f).name for f in downloaded_files] # Get just filenames
|
|
if len(all_files) != len(downloaded_files):
|
|
logger.warning(f"WARNING: Found {len(all_files)} file(s) in reports_dir but only {len(downloaded_files)} were downloaded!")
|
|
logger.warning("This might indicate old files weren't cleared. Clearing now...")
|
|
for file in all_files:
|
|
if file.name not in downloaded_file_paths:
|
|
try:
|
|
file.unlink()
|
|
logger.info(f"Cleared unexpected file: {file.name}")
|
|
except Exception as e:
|
|
logger.error(f"Failed to clear unexpected file {file.name}: {e}")
|
|
elif not download_from_sp:
|
|
# Manual upload was used (download_from_sharepoint=False)
|
|
# Upload endpoint should have cleared old files, but double-check
|
|
# Only use files uploaded in the last 10 minutes to avoid combining with old files
|
|
if reports_dir_path.exists():
|
|
excel_files = list(reports_dir_path.glob('*.xlsx')) + list(reports_dir_path.glob('*.xls'))
|
|
current_time = datetime.now().timestamp()
|
|
recent_files = []
|
|
for excel_file in excel_files:
|
|
mtime = excel_file.stat().st_mtime
|
|
# Only use files modified in the last 10 minutes (should be the uploaded ones)
|
|
# Increased from 5 to 10 minutes to account for upload + generation delay
|
|
if current_time - mtime < 600: # 10 minutes
|
|
recent_files.append(excel_file)
|
|
mtime_str = datetime.fromtimestamp(mtime).strftime('%Y-%m-%d %H:%M:%S')
|
|
logger.info(f" - {excel_file.name} (modified: {mtime_str}) - will be used for manual upload generation")
|
|
else:
|
|
logger.warning(f" - {excel_file.name} (modified: {datetime.fromtimestamp(mtime).strftime('%Y-%m-%d %H:%M:%S')}) - skipping (too old, might be from previous run)")
|
|
|
|
if len(recent_files) < len(excel_files):
|
|
logger.warning(f"Found {len(excel_files)} total file(s), but only {len(recent_files)} are recent. Clearing old files to avoid combining...")
|
|
# Clear old files to ensure we only use the manually uploaded ones
|
|
for excel_file in excel_files:
|
|
if excel_file not in recent_files:
|
|
try:
|
|
excel_file.unlink()
|
|
logger.info(f"Cleared old file: {excel_file.name}")
|
|
except Exception as e:
|
|
logger.warning(f"Failed to clear old file {excel_file.name}: {e}")
|
|
|
|
if len(recent_files) == 0:
|
|
logger.error("Manual upload was used but no recent files found in reports directory!")
|
|
logger.error("This might mean:")
|
|
logger.error("1. Files were not uploaded successfully")
|
|
logger.error("2. Files were uploaded but cleared before generation")
|
|
logger.error("3. File modification times are incorrect")
|
|
return jsonify({
|
|
'error': 'No files found for manual upload generation',
|
|
'details': 'Files were uploaded but not found in reports directory. Please try uploading again.',
|
|
'manual_upload_error': True
|
|
}), 400
|
|
|
|
# Verify we only have the recently uploaded files
|
|
all_files = list(reports_dir_path.glob('*.xlsx')) + list(reports_dir_path.glob('*.xls'))
|
|
if len(all_files) != len(recent_files):
|
|
logger.warning(f"WARNING: Found {len(all_files)} file(s) but only {len(recent_files)} are recent!")
|
|
logger.warning("Clearing old files to ensure only uploaded files are used...")
|
|
for file in all_files:
|
|
if file not in recent_files:
|
|
try:
|
|
file.unlink()
|
|
logger.info(f"Cleared unexpected old file: {file.name}")
|
|
except Exception as e:
|
|
logger.error(f"Failed to clear unexpected file {file.name}: {e}")
|
|
|
|
logger.info(f"Will generate report from {len(recent_files)} recently uploaded file(s)")
|
|
else:
|
|
logger.error("Manual upload was used but reports directory does not exist!")
|
|
return jsonify({
|
|
'error': 'Reports directory does not exist',
|
|
'details': 'Cannot generate report from manual upload - reports directory is missing.',
|
|
'manual_upload_error': True
|
|
}), 500
|
|
else:
|
|
# SharePoint download was attempted but failed - this shouldn't happen if download_from_sp=True
|
|
# But if it does, we should NOT use existing files as they might be old
|
|
logger.error("SharePoint download was requested but failed, and no manual upload was used!")
|
|
logger.error("This should not happen - refusing to use potentially old files")
|
|
return jsonify({
|
|
'error': 'SharePoint download failed and no manual upload provided',
|
|
'details': 'Cannot generate report - no data source available. Please try again or use manual upload.',
|
|
'sharepoint_error': True
|
|
}), 400
|
|
|
|
report_data = generate_report(
|
|
reports_dir=str(reports_dir_path),
|
|
output_file=output_file,
|
|
verbose=False # Don't print to console in API mode
|
|
)
|
|
|
|
if report_data:
|
|
# Generate HTML with same timestamp
|
|
html_file = output_dir / f"{report_id}.html"
|
|
from html_generator import generate_html_report
|
|
generate_html_report(output_file, str(html_file))
|
|
|
|
# Cleanup old reports (keep only last 10)
|
|
# Ensure reports_dir is a Path object
|
|
reports_dir_for_cleanup = Path(reports_dir) if isinstance(reports_dir, str) else reports_dir
|
|
cleanup_old_reports(output_dir, reports_dir_for_cleanup, max_reports=10)
|
|
|
|
return jsonify({
|
|
'status': 'success',
|
|
'message': 'Report generated successfully',
|
|
'report_id': report_id,
|
|
'report_date': timestamp,
|
|
'output_file': output_file,
|
|
'summary': report_data.get('summary', {}),
|
|
'vendors_count': len(report_data.get('vendors', [])),
|
|
'downloaded_files': len(downloaded_files) if download_from_sp else 0,
|
|
'downloaded_file_names': [Path(f).name for f in downloaded_files] if download_from_sp else []
|
|
})
|
|
else:
|
|
return jsonify({
|
|
'error': 'Report generation failed'
|
|
}), 500
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error generating report: {e}", exc_info=True)
|
|
return jsonify({
|
|
'error': f'Report generation failed: {str(e)}'
|
|
}), 500
|
|
|
|
@app.route('/api/upload', methods=['POST'])
|
|
def upload_files():
|
|
"""Upload Excel files manually. Clears old files before uploading new ones."""
|
|
try:
|
|
if 'files' not in request.files:
|
|
return jsonify({'error': 'No files provided'}), 400
|
|
|
|
files = request.files.getlist('files')
|
|
if not files or all(f.filename == '' for f in files):
|
|
return jsonify({'error': 'No files selected'}), 400
|
|
|
|
report_config = app.config['REPORT_CONFIG']
|
|
reports_dir_str = report_config.get('reports_dir', 'reports')
|
|
reports_dir = Path(reports_dir_str)
|
|
if not reports_dir.is_absolute():
|
|
script_dir = Path(__file__).parent.absolute()
|
|
reports_dir = script_dir / reports_dir
|
|
|
|
# Ensure reports directory exists
|
|
reports_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# ALWAYS clear ALL old Excel files from reports directory before uploading new ones
|
|
# CRITICAL: This prevents combining multiple files in report generation
|
|
old_excel_files = list(reports_dir.glob('*.xlsx')) + list(reports_dir.glob('*.xls'))
|
|
cleared_count = 0
|
|
failed_to_clear = []
|
|
|
|
for old_file in old_excel_files:
|
|
try:
|
|
# On Windows, files might be locked - try multiple times
|
|
max_retries = 3
|
|
retry_count = 0
|
|
while retry_count < max_retries:
|
|
try:
|
|
old_file.unlink()
|
|
cleared_count += 1
|
|
logger.info(f"Cleared old file before upload: {old_file.name}")
|
|
break
|
|
except PermissionError:
|
|
retry_count += 1
|
|
if retry_count < max_retries:
|
|
import time
|
|
time.sleep(0.5) # Wait 500ms before retry
|
|
else:
|
|
raise
|
|
except Exception as e:
|
|
failed_to_clear.append(old_file.name)
|
|
logger.error(f"Failed to clear old file {old_file.name}: {e}")
|
|
|
|
# If any files failed to clear, fail the upload to prevent mixing old and new data
|
|
if failed_to_clear:
|
|
logger.error(f"CRITICAL: Failed to clear {len(failed_to_clear)} file(s) before upload: {failed_to_clear}")
|
|
return jsonify({
|
|
'error': f'Failed to clear {len(failed_to_clear)} old file(s) before upload. Please ensure files are not locked or in use.',
|
|
'failed_files': failed_to_clear,
|
|
'details': 'Old files must be cleared before upload to ensure report generation uses only the new file(s). Files may be locked by Excel or another process.'
|
|
}), 500
|
|
|
|
if cleared_count > 0:
|
|
logger.info(f"Cleared {cleared_count} old Excel file(s) before upload")
|
|
else:
|
|
logger.info("No old Excel files found to clear (reports directory was empty)")
|
|
|
|
uploaded_count = 0
|
|
uploaded_files = []
|
|
|
|
for file in files:
|
|
if file.filename == '':
|
|
continue
|
|
|
|
# Check if it's an Excel file
|
|
filename = secure_filename(file.filename)
|
|
if not (filename.endswith('.xlsx') or filename.endswith('.xls')):
|
|
logger.warning(f"Skipping non-Excel file: {filename}")
|
|
continue
|
|
|
|
# Save file to reports directory
|
|
file_path = reports_dir / filename
|
|
file.save(str(file_path))
|
|
uploaded_count += 1
|
|
uploaded_files.append(filename)
|
|
logger.info(f"Uploaded file: {filename} -> {file_path}")
|
|
|
|
if uploaded_count == 0:
|
|
return jsonify({'error': 'No valid Excel files uploaded'}), 400
|
|
|
|
# Warn if multiple files uploaded - reports should be generated from ONE file
|
|
if uploaded_count > 1:
|
|
logger.warning(f"WARNING: {uploaded_count} files uploaded. Reports should be generated from a single file. Only the newest file will be used.")
|
|
|
|
return jsonify({
|
|
'status': 'success',
|
|
'message': f'Successfully uploaded {uploaded_count} file(s)',
|
|
'uploaded_count': uploaded_count,
|
|
'uploaded_files': uploaded_files,
|
|
'cleared_old_files': cleared_count,
|
|
'warning': f'{uploaded_count} file(s) uploaded - only the newest will be used for report generation' if uploaded_count > 1 else None
|
|
})
|
|
except Exception as e:
|
|
logger.error(f"Error uploading files: {e}", exc_info=True)
|
|
return jsonify({'error': f'Failed to upload files: {str(e)}'}), 500
|
|
|
|
@app.route('/api/status', methods=['GET'])
|
|
def status():
|
|
"""Get service status and configuration."""
|
|
return jsonify({
|
|
'status': 'running',
|
|
'sharepoint_enabled': app.config['SHAREPOINT_CONFIG'].get('enabled', False),
|
|
'reports_dir': app.config['REPORT_CONFIG'].get('reports_dir', 'reports'),
|
|
'output_dir': app.config['REPORT_CONFIG'].get('output_dir', 'output')
|
|
})
|
|
|
|
@app.route('/api/report/json', methods=['GET'])
|
|
def get_report_json():
|
|
"""Get latest report JSON file."""
|
|
try:
|
|
report_config = app.config['REPORT_CONFIG']
|
|
output_dir_str = report_config.get('output_dir', 'output')
|
|
output_dir = Path(output_dir_str)
|
|
if not output_dir.is_absolute():
|
|
script_dir = Path(__file__).parent.absolute()
|
|
output_dir = script_dir / output_dir
|
|
report_file = output_dir / 'report.json'
|
|
|
|
if not report_file.exists():
|
|
return jsonify({'error': 'Report not found. Generate a report first.'}), 404
|
|
|
|
with open(report_file, 'r', encoding='utf-8') as f:
|
|
report_data = json.load(f)
|
|
|
|
return jsonify(report_data)
|
|
except Exception as e:
|
|
logger.error(f"Error reading report JSON: {e}", exc_info=True)
|
|
return jsonify({'error': f'Failed to read report: {str(e)}'}), 500
|
|
|
|
@app.route('/api/report/html', methods=['GET'])
|
|
def get_report_html():
|
|
"""Get report HTML file by report_id (or latest if not specified)."""
|
|
try:
|
|
from flask import send_from_directory
|
|
|
|
report_config = app.config['REPORT_CONFIG']
|
|
output_dir_str = report_config.get('output_dir', 'output')
|
|
output_dir = Path(output_dir_str)
|
|
if not output_dir.is_absolute():
|
|
script_dir = Path(__file__).parent.absolute()
|
|
output_dir = script_dir / output_dir
|
|
|
|
# Get report_id from query parameter, default to latest
|
|
report_id = request.args.get('report_id')
|
|
|
|
if report_id:
|
|
# Check if it's a timestamped report or legacy report
|
|
html_file = output_dir / f"{report_id}.html"
|
|
|
|
# If not found and it starts with "report-", might be a legacy report with generated ID
|
|
if not html_file.exists() and report_id.startswith('report-'):
|
|
# Try legacy report.html
|
|
legacy_file = output_dir / 'report.html'
|
|
if legacy_file.exists():
|
|
html_file = legacy_file
|
|
else:
|
|
return jsonify({'error': f'Report {report_id} not found.'}), 404
|
|
elif not html_file.exists():
|
|
return jsonify({'error': f'Report {report_id} not found.'}), 404
|
|
else:
|
|
# Get latest report (check both timestamped and legacy)
|
|
timestamped_files = list(output_dir.glob('report-*.html'))
|
|
legacy_file = output_dir / 'report.html'
|
|
|
|
html_files = []
|
|
if legacy_file.exists():
|
|
html_files.append(legacy_file)
|
|
html_files.extend(timestamped_files)
|
|
|
|
if not html_files:
|
|
return jsonify({'error': 'No reports found. Generate a report first.'}), 404
|
|
|
|
html_file = sorted(html_files, key=lambda p: p.stat().st_mtime, reverse=True)[0]
|
|
|
|
return send_from_directory(str(output_dir), html_file.name, mimetype='text/html')
|
|
except Exception as e:
|
|
logger.error(f"Error reading report HTML: {e}", exc_info=True)
|
|
return jsonify({'error': f'Failed to read report HTML: {str(e)}'}), 500
|
|
|
|
@app.route('/api/reports/list', methods=['GET'])
|
|
def list_reports():
|
|
"""List all available reports (last 10)."""
|
|
try:
|
|
report_config = app.config['REPORT_CONFIG']
|
|
output_dir_str = report_config.get('output_dir', 'output')
|
|
output_dir = Path(output_dir_str)
|
|
|
|
# Ensure absolute path
|
|
if not output_dir.is_absolute():
|
|
script_dir = Path(__file__).parent.absolute()
|
|
output_dir = script_dir / output_dir
|
|
|
|
# Log for debugging
|
|
logger.info(f"Looking for reports in: {output_dir.absolute()}")
|
|
logger.info(f"Output directory exists: {output_dir.exists()}")
|
|
if output_dir.exists():
|
|
logger.info(f"Files in output directory: {list(output_dir.glob('*'))}")
|
|
|
|
# Find all report HTML files (both timestamped and non-timestamped)
|
|
timestamped_files = list(output_dir.glob('report-*.html'))
|
|
legacy_file = output_dir / 'report.html'
|
|
|
|
logger.info(f"Found {len(timestamped_files)} timestamped report files")
|
|
logger.info(f"Legacy report.html exists: {legacy_file.exists()}")
|
|
if legacy_file.exists():
|
|
logger.info(f"Legacy report.html path: {legacy_file.absolute()}")
|
|
|
|
html_files = []
|
|
|
|
# Add legacy report.html if it exists
|
|
if legacy_file.exists():
|
|
html_files.append(legacy_file)
|
|
logger.info("Added legacy report.html to list")
|
|
|
|
# Add timestamped files
|
|
html_files.extend(timestamped_files)
|
|
logger.info(f"Total HTML files found: {len(html_files)}")
|
|
|
|
reports = []
|
|
for html_file in sorted(html_files, key=lambda p: p.stat().st_mtime, reverse=True)[:10]:
|
|
report_id = html_file.stem # e.g., "report-2025-11-08-11-25-46" or "report"
|
|
|
|
# Handle legacy report.html
|
|
if report_id == 'report':
|
|
# Use file modification time as timestamp
|
|
mtime = html_file.stat().st_mtime
|
|
dt = datetime.fromtimestamp(mtime)
|
|
timestamp_str = dt.strftime('%Y-%m-%d-%H-%M-%S')
|
|
date_str = dt.strftime('%Y-%m-%d %H:%M:%S')
|
|
report_id = f"report-{timestamp_str}"
|
|
else:
|
|
# Timestamped report
|
|
timestamp_str = report_id.replace('report-', '')
|
|
try:
|
|
# Parse timestamp to create readable date
|
|
dt = datetime.strptime(timestamp_str, '%Y-%m-%d-%H-%M-%S')
|
|
date_str = dt.strftime('%Y-%m-%d %H:%M:%S')
|
|
except:
|
|
date_str = timestamp_str
|
|
|
|
# Get file size
|
|
file_size = html_file.stat().st_size
|
|
|
|
reports.append({
|
|
'report_id': report_id,
|
|
'date': date_str,
|
|
'timestamp': timestamp_str,
|
|
'file_size': file_size
|
|
})
|
|
|
|
return jsonify({
|
|
'reports': reports,
|
|
'count': len(reports)
|
|
})
|
|
except Exception as e:
|
|
logger.error(f"Error listing reports: {e}", exc_info=True)
|
|
return jsonify({'error': f'Failed to list reports: {str(e)}'}), 500
|
|
|
|
return app
|
|
|
|
|
|
def run_server(config_path: Optional[str] = None, host: Optional[str] = None, port: Optional[int] = None):
|
|
"""Run the API server."""
|
|
app = create_app(config_path)
|
|
|
|
api_config = config.get('api', {})
|
|
server_host = host or api_config.get('host', '0.0.0.0')
|
|
server_port = port or api_config.get('port', 8080)
|
|
|
|
logger.info(f"Starting API server on {server_host}:{server_port}")
|
|
app.run(host=server_host, port=server_port, debug=False)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
)
|
|
|
|
config_path = sys.argv[1] if len(sys.argv) > 1 else None
|
|
|
|
# Check if API is enabled
|
|
config = load_config(config_path)
|
|
if not config.get('api', {}).get('enabled', False):
|
|
logger.warning("API is disabled in configuration. Set api.enabled=true to enable.")
|
|
logger.info("Starting API server anyway (for testing)...")
|
|
|
|
run_server(config_path=config_path)
|
|
|