-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
feat: Add Flask application with Docker deployment
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
|
||
* text=auto |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
|
||
.env | ||
data |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
# Use an official Python runtime as a parent image | ||
FROM python:3.9 | ||
|
||
# Set the working directory in the container | ||
WORKDIR /app | ||
|
||
# Copy the current directory contents into the container at /app | ||
COPY . /app | ||
|
||
# Upgrade pip to the latest version before installing dependencies | ||
RUN pip install --upgrade pip | ||
|
||
# Install necessary dependencies | ||
RUN pip install --no-cache-dir -r requirements.txt | ||
|
||
# Expose port 5000 for Flask to run on | ||
EXPOSE 5000 | ||
|
||
# Set the default command to run the application | ||
CMD ["python", "app.py"] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
from flask import Flask, request, jsonify, send_file | ||
from flask_cors import CORS | ||
from pathlib import Path | ||
from forecasting import predict_demand, check_stock_and_alert | ||
from bokeh_forecast import create_bokeh_plots | ||
from utils import load_data | ||
import os | ||
import logging | ||
|
||
app = Flask(__name__) | ||
|
||
# Enable CORS | ||
CORS(app, resources={r"/*": {"origins": "*"}}) | ||
|
||
# Configure upload folder and file size limit | ||
UPLOAD_FOLDER = Path(os.getenv('UPLOAD_FOLDER', 'uploads')) | ||
UPLOAD_FOLDER.mkdir(parents=True, exist_ok=True) | ||
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16 MB | ||
|
||
# Logging setup | ||
app.logger.setLevel(logging.INFO) | ||
|
||
# Allowed file extensions | ||
ALLOWED_EXTENSIONS = {'csv', 'xlsx'} | ||
data_file_path = None | ||
|
||
|
||
def allowed_file(filename): | ||
"""Check if the uploaded file has an allowed extension.""" | ||
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS | ||
|
||
|
||
@app.route('/') | ||
def home(): | ||
return "Welcome to the Bookkeeping AI API! Use /upload to upload a file and /forecast for forecasting." | ||
|
||
|
||
@app.route('/upload', methods=['POST']) | ||
def upload_file(): | ||
global data_file_path | ||
|
||
if 'file' not in request.files: | ||
return jsonify({"error": "No file part in the request"}), 400 | ||
|
||
file = request.files['file'] | ||
if file.filename == '': | ||
return jsonify({"error": "No file selected"}), 400 | ||
|
||
if not allowed_file(file.filename): | ||
return jsonify({"error": "Only .csv and .xlsx files are allowed"}), 400 | ||
|
||
try: | ||
# Save the new file and overwrite the existing one | ||
data_file_path = UPLOAD_FOLDER / file.filename | ||
file.save(data_file_path) | ||
|
||
# Cleanup old plots | ||
for plot in UPLOAD_FOLDER.glob("*.html"): | ||
plot.unlink() | ||
|
||
app.logger.info(f"File uploaded successfully: {data_file_path}") | ||
return jsonify({"message": "File uploaded successfully", "file_path": str(data_file_path)}) | ||
|
||
except Exception as e: | ||
app.logger.error(f"File upload error: {e}") | ||
return jsonify({"error": "Failed to upload file"}), 500 | ||
|
||
|
||
@app.route('/forecast', methods=['POST']) | ||
def forecast(): | ||
global data_file_path | ||
if not data_file_path: | ||
return jsonify({"error": "No data file uploaded yet. Please upload a file first."}), 400 | ||
|
||
data = request.get_json() | ||
item_id = data.get('item_id') | ||
|
||
if item_id is None: | ||
return jsonify({"error": "Item ID is required."}), 400 | ||
|
||
try: | ||
# Load data | ||
df = load_data(data_file_path) | ||
|
||
# Validate item ID | ||
if item_id not in df['item_id'].values: | ||
app.logger.error(f"Item ID {item_id} not found in the dataset") | ||
return jsonify({"error": f"Item ID {item_id} not found in the dataset."}), 404 | ||
|
||
# Generate predictions | ||
future_months, predicted_demand = predict_demand(df, item_id) | ||
|
||
if predicted_demand is None: | ||
return jsonify({"error": f"Could not generate forecasts for item ID {item_id}."}), 500 | ||
|
||
# Generate alerts and plots | ||
alerts = check_stock_and_alert(df, item_id, predicted_demand, future_months) | ||
plot_path = create_bokeh_plots(df, item_id, future_months, predicted_demand) | ||
|
||
# Safely convert variables for JSON serialization | ||
response = { | ||
"future_months": future_months if isinstance(future_months, list) else future_months.tolist(), | ||
"predicted_demand": predicted_demand if isinstance(predicted_demand, list) else predicted_demand.tolist(), | ||
"alerts": alerts, | ||
"plot_url": f"/plot/{item_id}" | ||
} | ||
|
||
app.logger.info(f"Forecast successfully generated for item ID {item_id}") | ||
return jsonify(response) | ||
|
||
except Exception as e: | ||
app.logger.error(f"Error during forecasting: {e}") | ||
return jsonify({"error": "An error occurred during forecasting. Please try again."}), 500 | ||
|
||
|
||
@app.route('/plot/<item_id>') | ||
def plot(item_id): | ||
plot_path = UPLOAD_FOLDER / f"demand_forecast_{item_id}.html" | ||
if plot_path.exists(): | ||
return send_file(plot_path) | ||
else: | ||
return jsonify({"error": "Plot not found"}), 404 | ||
|
||
|
||
if __name__ == "__main__": | ||
app.run(host='0.0.0.0', port=5000, debug=True) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
from bokeh.plotting import figure, output_file, save | ||
from bokeh.layouts import column | ||
from bokeh.models import ColumnDataSource | ||
import os | ||
|
||
|
||
def create_bokeh_plots(df, item_id, future_months, predicted_demand): | ||
# Filter data for the specified item_id | ||
item_data = df[df['item_id'] == item_id].copy() | ||
|
||
# Convert transaction date to year-month format | ||
item_data['year_month'] = item_data['transaction_date'].dt.to_period('M') | ||
|
||
# Aggregate actual demand data by month | ||
actual_demand = item_data.groupby('year_month')['quantity'].sum().reset_index() | ||
|
||
# Prepare ColumnDataSource for actual demand | ||
actual_source = ColumnDataSource(data=dict( | ||
month=actual_demand['year_month'].dt.to_timestamp(), | ||
quantity=actual_demand['quantity'] | ||
)) | ||
|
||
# Prepare ColumnDataSource for predicted demand | ||
predicted_source = ColumnDataSource(data=dict( | ||
month=future_months, | ||
quantity=predicted_demand | ||
)) | ||
|
||
# Create the actual demand plot | ||
actual_plot = figure( | ||
title=f'Actual Demand for Item ID {item_id}', | ||
x_axis_label='Date', | ||
y_axis_label='Quantity', | ||
x_axis_type='datetime', | ||
width=800, | ||
height=400, | ||
toolbar_location='above', | ||
background_fill_color='#f9f9f9' | ||
) | ||
|
||
actual_plot.line( | ||
'month', 'quantity', | ||
source=actual_source, | ||
line_width=2, | ||
color='blue', | ||
legend_label='Actual Demand' | ||
) | ||
|
||
actual_plot.scatter( | ||
'month', 'quantity', | ||
source=actual_source, | ||
size=8, | ||
color='blue' | ||
) | ||
|
||
# Create the predicted demand plot | ||
predicted_plot = figure( | ||
title=f'Predicted Demand for Item ID {item_id}', | ||
x_axis_label='Date', | ||
y_axis_label='Quantity', | ||
x_axis_type='datetime', | ||
width=800, | ||
height=400, | ||
toolbar_location='above', | ||
background_fill_color='#f9f9f9' | ||
) | ||
|
||
predicted_plot.line( | ||
'month', 'quantity', | ||
source=predicted_source, | ||
line_width=2, | ||
color='orange', | ||
legend_label='Predicted Demand' | ||
) | ||
|
||
predicted_plot.scatter( | ||
'month', 'quantity', | ||
source=predicted_source, | ||
size=8, | ||
color='orange' | ||
) | ||
|
||
# Define the HTML file path | ||
plot_filename = f"uploads/demand_forecast_{item_id}.html" | ||
os.makedirs(os.path.dirname(plot_filename), exist_ok=True) | ||
|
||
# Output the Bokeh plots to an HTML file | ||
output_file(plot_filename) | ||
save(column(actual_plot, predicted_plot)) # Save the layout to the file instead of showing it | ||
|
||
return plot_filename # Return the path to the HTML file |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
version: '3' | ||
services: | ||
app: | ||
build: . | ||
ports: | ||
- "5000:5000" | ||
volumes: | ||
- .:/app | ||
environment: | ||
- FLASK_DEBUG=1 |