from flask import Flask, request, render_template, session from werkzeug.utils import secure_filename from models.time_series import process_time_series from models.plotting import create_comparison_plot from utils.file_handling import allowed_file, read_file, save_processed_file from utils.forecast_history import update_forecast_history, download_forecast_history import os app = Flask(__name__) app.config['UPLOAD_FOLDER'] = 'Uploads' app.config['ALLOWED_EXTENSIONS'] = {'csv', 'xls', 'xlsx'} app.secret_key = 'your-secret-key' # Required for session management # Ensure upload folder exists os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) @app.route('/') def index(): return render_template('index.html') @app.route('/upload', methods=['POST']) def upload_file(): if 'file' not in request.files: return render_template('index.html', error='No file part') file = request.files['file'] if file.filename == '': return render_template('index.html', error='No selected file') if file and allowed_file(file.filename): filename = secure_filename(file.filename) filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename) file.save(filepath) session['filepath'] = filepath # Store filepath in session session['forecast_history'] = [] # Initialize forecast history session['selected_indices'] = [] # Initialize selected indices # Get user selections do_decomposition = 'decomposition' in request.form do_forecasting = 'forecasting' in request.form do_acf_pacf = 'acf_pacf' in request.form train_percent = float(request.form.get('train_percent', 80)) / 100 test_percent = float(request.form.get('test_percent', 20)) / 100 forecast_periods = int(request.form.get('forecast_periods', 12)) model_type = request.form.get('model_type', 'ARIMA') # Validate train/test percentages if abs(train_percent + test_percent - 1.0) > 0.01: # Allow small float precision errors return render_template('index.html', error='Train and test percentages must sum to 100%') session['do_decomposition'] = do_decomposition session['do_forecasting'] = do_forecasting session['do_acf_pacf'] = do_acf_pacf session['train_percent'] = train_percent session['test_percent'] = test_percent session['forecast_periods'] = forecast_periods session['model_type'] = model_type result = process_time_series(filepath, do_decomposition, do_forecasting, do_acf_pacf, train_percent, forecast_periods, model_type) if 'error' in result: return render_template('index.html', error=result['error']) # Update forecast history if unique if do_forecasting and result['metrics']: update_forecast_history(session, train_percent, test_percent, forecast_periods, model_type, result['metrics']) return render_template('results.html', do_decomposition=do_decomposition, do_forecasting=do_forecasting, do_acf_pacf=do_acf_pacf, train_percent=train_percent * 100, test_percent=test_percent * 100, forecast_periods=forecast_periods, forecast_history=session['forecast_history'], selected_indices=session['selected_indices'], **result) @app.route('/reforecast', methods=['POST']) def reforecast(): filepath = session.get('filepath') if not filepath or not os.path.exists(filepath): return render_template('index.html', error='Session expired or file not found. Please upload the file again.') # Get user selections from reforecast form train_percent = float(request.form.get('train_percent', 80)) / 100 test_percent = float(request.form.get('test_percent', 20)) / 100 forecast_periods = int(request.form.get('forecast_periods', 12)) model_type = request.form.get('model_type', 'ARIMA') add_to_existing = 'add_to_existing' in request.form # Validate train/test percentages if abs(train_percent + test_percent - 1.0) > 0.01: # Allow small float precision errors return render_template('index.html', error='Train and test percentages must sum to 100%') # Get original selections from session or defaults do_decomposition = session.get('do_decomposition', False) do_forecasting = True # Since this is a reforecast do_acf_pacf = session.get('do_acf_pacf', False) result = process_time_series(filepath, do_decomposition, do_forecasting, do_acf_pacf, train_percent, forecast_periods, model_type) if 'error' in result: return render_template('index.html', error=result['error']) # Update forecast history if unique if do_forecasting and result['metrics']: update_forecast_history(session, train_percent, test_percent, forecast_periods, model_type, result['metrics'], add_to_existing) # Update session with current parameters session['train_percent'] = train_percent session['test_percent'] = test_percent session['forecast_periods'] = forecast_periods session['model_type'] = model_type # Generate comparison plot if multiple forecasts are selected if len(session.get('selected_indices', [])) > 1: result['forecast_html'] = create_comparison_plot(filepath, session['forecast_history'], session['selected_indices']) return render_template('results.html', do_decomposition=do_decomposition, do_forecasting=do_forecasting, do_acf_pacf=do_acf_pacf, train_percent=train_percent * 100, test_percent=test_percent * 100, forecast_periods=forecast_periods, forecast_history=session['forecast_history'], selected_indices=session['selected_indices'], scroll_to_forecast=True, **result) @app.route('/compare_forecasts', methods=['POST']) def compare_forecasts(): filepath = session.get('filepath') if not filepath or not os.path.exists(filepath): return render_template('index.html', error='Session expired or file not found. Please upload the file again.') # Get selected forecast indices selected_indices = [int(idx) for idx in request.form.getlist('selected_forecasts')] if not selected_indices: return render_template('index.html', error='No forecasts selected for comparison') # Update session with selected indices session['selected_indices'] = selected_indices session.modified = True # Get current parameters and settings do_decomposition = session.get('do_decomposition', False) do_forecasting = session.get('do_forecasting', True) do_acf_pacf = session.get('do_acf_pacf', False) train_percent = session.get('train_percent', 0.8) test_percent = session.get('test_percent', 0.2) forecast_periods = session.get('forecast_periods', 12) model_type = session.get('model_type', 'ARIMA') # Generate comparison plot forecast_html = create_comparison_plot(filepath, session['forecast_history'], selected_indices) # Re-run the current forecast to maintain other results result = process_time_series(filepath, do_decomposition, do_forecasting, do_acf_pacf, train_percent, forecast_periods, model_type) if 'error' in result: return render_template('index.html', error=result['error']) result['forecast_html'] = forecast_html return render_template('results.html', do_decomposition=do_decomposition, do_forecasting=do_forecasting, do_acf_pacf=do_acf_pacf, train_percent=train_percent * 100, test_percent=test_percent * 100, forecast_periods=forecast_periods, forecast_history=session['forecast_history'], selected_indices=selected_indices, scroll_to_forecast=True, **result) @app.route('/download_forecast_history') def download_forecast_history(): return download_forecast_history(session) @app.route('/download/') def download_file(filename): filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename) return send_file(filepath, as_attachment=True) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=True)