from fastapi import FastAPI, UploadFile, File, HTTPException, Header, Body
from [Link] import CORSMiddleware
from pydantic import BaseModel, Field
from typing import Optional, List, Dict, Any
import pandas as pd
import tempfile
import os
import logging
from datetime import datetime
from [Link] import RotatingFileHandler
# Handle imports
try:
from .model_pipeline import (
predict_future_from_dataframe,
predict_from_features,
get_monthly_features_from_dataframe
)
except ImportError:
from model_pipeline import (
predict_future_from_dataframe,
predict_from_features,
get_monthly_features_from_dataframe
)
# Ensure logs directory exists
LOGS_DIR =
[Link]([Link]([Link]([Link](__file__))), "logs")
[Link](LOGS_DIR, exist_ok=True)
# Configure logging
logger = [Link](__name__)
[Link]([Link])
file_handler = RotatingFileHandler(
[Link](LOGS_DIR, "[Link]"),
maxBytes=10*1024*1024,
backupCount=5
)
file_handler.setLevel([Link])
console_handler = [Link]()
console_handler.setLevel([Link])
formatter = [Link]('%(asctime)s - %(name)s - %(levelname)s - %
(message)s')
file_handler.setFormatter(formatter)
console_handler.setFormatter(formatter)
[Link](file_handler)
[Link](console_handler)
# ===================================
# 🔐 USERS DATABASE (Dictionary)
# ===================================
USERS_DB = {
"admin": {
"password": "admin123",
"name": "Administrator",
"role": "Admin"
},
"manager": {
"password": "manager456",
"name": "Manager User",
"role": "Manager"
},
"yousif": {
"password": "yousif",
"name": "John Doe",
"role": "Analyst"
}
}
def verify_credentials(username: str, password: str):
"""Verify username and password"""
if username not in USERS_DB:
[Link](f"❌ Login attempt with unknown username: {username}")
return None
user = USERS_DB[username]
if user["password"] != password:
[Link](f"❌ Invalid password for user: {username}")
return None
[Link](f"✅ User authenticated: {username} ({user['name']})")
return user
# ===================================
# 📋 PYDANTIC MODELS
# ===================================
class RawDataRequest(BaseModel):
"""Request model for raw transaction data"""
data: List[Dict[str, Any]] = Field(
...,
description="List of raw transaction records",
example=[
{
"TransactionDate": "2024-01-15",
"NumberOfHours": 8.5,
"EmployeeID": "E001",
"Position": "Engineer",
"Hierarchy": "Level3",
"OvertimeHourlyRate": 50.0,
"EmployeeCount": 100,
"DepartmentCount": 5
}
]
)
n_months: int = Field(
default=6,
ge=1,
le=6,
description="Number of months to forecast (1-6)"
)
class FeaturesRequest(BaseModel):
"""Request model for predict-from-features endpoint"""
features: List[Dict[str, Any]] = Field(
...,
description="List of monthly feature dictionaries",
example=[
{
"Date": "2024-01-01",
"Total_NumberOfHours": 1000,
"AVG_OvertimeRate": 50,
"AVG_EmployeeCount": 100,
"AVG_DepartmentCount": 10,
"Position_mode_monthly": 1,
"Hierarchy_mode_monthly": 2,
"Is_HolidayOrWeekend": 5,
"Total_Employees_Worked": 80
}
]
)
last_date: str = Field(
...,
description="Last historical date (YYYY-MM-DD)",
example="2024-12-01"
)
n_months: int = Field(
default=6,
ge=1,
le=6,
description="Number of months to forecast (1-6)"
)
# ===================================
# FastAPI Application
# ===================================
app = FastAPI(
title="Overtime Forecasting API",
version="2.0.0",
description="Secure API for predicting future overtime hours with flexible
forecasting duration"
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@[Link]("/")
async def root():
"""Root endpoint - service information."""
return {
"service": "Overtime Forecasting API",
"version": "2.0.0",
"message": "API is running",
"status": "running",
"authentication": "Required - Use X-Username and X-Password headers",
"total_users": len(USERS_DB),
"endpoints": {
"/predict": "Send raw JSON data and predict (1-6 months)",
"/predict-from-features": "Predict from pre-processed features (1-6
months)",
"/extract-features": "Extract monthly features from raw JSON data"
}
}
@[Link]("/health")
async def health_check():
"""Health check endpoint - No auth required."""
[Link]("Health check requested")
return {"status": "healthy", "version": "2.0.0"}
@[Link]("/hello")
async def hello():
"""Simple hello endpoint - No auth required."""
return {"message": "hello world", "version": "2.0.0"}
@[Link]("/predict")
async def predict_overtime(
request: RawDataRequest,
x_username: str = Header(None, alias="X-Username"),
x_password: str = Header(None, alias="X-Password")
):
"""
This endpoint accepts raw transaction data in JSON format and runs
the complete pipeline: preprocessing → aggregation → feature engineering →
prediction.
Args:
request: RawDataRequest with transaction data and n_months
x_username: Username header
x_password: Password header
Returns:
JSON with predictions and metadata
Example request body:
{
"data": [
{
"TransactionDate": "2024-01-15",
"NumberOfHours": 8.5,
"EmployeeID": "E001",
"Position": "Engineer",
"Hierarchy": "Level3",
"OvertimeHourlyRate": 50.0,
"EmployeeCount": 100,
"DepartmentCount": 5
},
...
],
"n_months": 3
}
"""
# ✅ Verify credentials
if not x_username or not x_password:
[Link]("❌ Missing authentication headers")
raise HTTPException(
status_code=401,
detail="Authentication required. Provide X-Username and X-Password
headers."
)
user = verify_credentials(x_username, x_password)
if not user:
raise HTTPException(
status_code=403,
detail="Invalid username or password"
)
[Link](f"✅ Authenticated user: {x_username} ({user['name']}) - Role:
{user['role']}")
[Link](f"📅 Forecast duration: {request.n_months} months")
[Link](f"📊 Received {len([Link])} transaction records")
try:
# Convert JSON data to DataFrame
raw_df = [Link]([Link])
# Validate required columns
required_cols = [
'TransactionDate', 'NumberOfHours', 'EmployeeID',
'Position', 'Hierarchy', 'OvertimeHourlyRate',
'EmployeeCount', 'DepartmentCount'
]
missing_cols = [col for col in required_cols if col not in raw_df.columns]
if missing_cols:
raise HTTPException(
status_code=400,
detail=f"Missing required columns: {missing_cols}"
)
[Link](f"Data validation passed")
# Generate predictions through full pipeline
predictions_df = predict_future_from_dataframe(raw_df,
n_months=request.n_months)
# Format response
response = {
"status": "success",
"predictions": predictions_df.to_dict("records"),
"metadata": {
"forecast_months": request.n_months,
"model_type": "XGBoost",
"start_date": predictions_df['Date'].iloc[0] if len(predictions_df)
> 0 else None,
"end_date": predictions_df['Date'].iloc[-1] if len(predictions_df)
> 0 else None,
"total_predictions": len(predictions_df),
"input_records": len([Link]),
"generated_at": [Link]().strftime("%Y-%m-%d %H:%M:%S"),
"generated_by": user['name'],
"user_role": user['role']
}
}
[Link](f"✅ Prediction successful for {x_username}:
{len(predictions_df)} months forecasted")
return response
except ValueError as e:
[Link](f"❌ Validation error for {x_username}: {str(e)}")
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
[Link](f"❌ Prediction failed for {x_username}: {str(e)}",
exc_info=True)
raise HTTPException(status_code=500, detail=f"Prediction failed: {str(e)}")
@[Link]("/predict-from-features")
async def predict_from_features_endpoint(
request: FeaturesRequest,
x_username: str = Header(None, alias="X-Username"),
x_password: str = Header(None, alias="X-Password")
):
"""
This endpoint skips data processing and uses pre-aggregated features.
Much faster than /predict endpoint.
Args:
request: FeaturesRequest with features, last_date, and n_months
x_username: Username header
x_password: Password header
Returns:
JSON with predictions and metadata
"""
# ✅ Verify credentials
if not x_username or not x_password:
[Link]("❌ Missing authentication headers")
raise HTTPException(
status_code=401,
detail="Authentication required. Provide X-Username and X-Password
headers."
)
user = verify_credentials(x_username, x_password)
if not user:
raise HTTPException(
status_code=403,
detail="Invalid username or password"
)
[Link](f"✅ Authenticated user: {x_username} ({user['name']}) - Role:
{user['role']}")
[Link](f"📅 Forecast duration: {request.n_months} months from features")
try:
# Convert features to DataFrame
features_df = [Link]([Link])
[Link](f"Received {len(features_df)} months of features")
# Generate predictions
predictions_df = predict_from_features(
features_df,
request.last_date,
n_months=request.n_months
)
# Format response
response = {
"status": "success",
"predictions": predictions_df.to_dict("records"),
"metadata": {
"forecast_months": request.n_months,
"model_type": "XGBoost",
"start_date": predictions_df['Date'].iloc[0] if len(predictions_df)
> 0 else None,
"end_date": predictions_df['Date'].iloc[-1] if len(predictions_df)
> 0 else None,
"total_predictions": len(predictions_df),
"generated_at": [Link]().strftime("%Y-%m-%d %H:%M:%S"),
"generated_by": user['name'],
"user_role": user['role'],
"input_features_count": len(features_df)
}
}
[Link](f"✅ Prediction from features successful for {x_username}:
{len(predictions_df)} months forecasted")
return response
except ValueError as e:
[Link](f"❌ Validation error for {x_username}: {str(e)}")
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
[Link](f"❌ Prediction failed for {x_username}: {str(e)}",
exc_info=True)
raise HTTPException(status_code=500, detail=f"Prediction failed: {str(e)}")
@[Link]("/extract-features")
async def extract_features(
request: RawDataRequest,
x_username: str = Header(None, alias="X-Username"),
x_password: str = Header(None, alias="X-Password")
):
"""
Extract monthly aggregated features from raw JSON data.
Args:
request: RawDataRequest with transaction data (n_months ignored here)
x_username: Username header
x_password: Password header
Returns:
JSON with monthly features and last_date
"""
# ✅ Verify credentials
if not x_username or not x_password:
[Link]("❌ Missing authentication headers")
raise HTTPException(
status_code=401,
detail="Authentication required. Provide X-Username and X-Password
headers."
)
user = verify_credentials(x_username, x_password)
if not user:
raise HTTPException(
status_code=403,
detail="Invalid username or password"
)
[Link](f"✅ Authenticated user: {x_username} ({user['name']}) - Role:
{user['role']}")
[Link](f"📊 Received {len([Link])} transaction records for feature
extraction")
try:
# Convert JSON data to DataFrame
raw_df = [Link]([Link])
# Validate required columns
required_cols = [
'TransactionDate', 'NumberOfHours', 'EmployeeID',
'Position', 'Hierarchy', 'OvertimeHourlyRate',
'EmployeeCount', 'DepartmentCount'
]
missing_cols = [col for col in required_cols if col not in raw_df.columns]
if missing_cols:
raise HTTPException(
status_code=400,
detail=f"Missing required columns: {missing_cols}"
)
[Link](f"✅ Data validation passed")
# Extract features
monthly_df, last_date = get_monthly_features_from_dataframe(raw_df)
# Format response
response = {
"status": "success",
"features": monthly_df.to_dict("records"),
"last_date": last_date.strftime("%Y-%m-%d"),
"metadata": {
"total_months": len(monthly_df),
"date_range": {
"start": monthly_df['Date'].min().strftime("%Y-%m-%d"),
"end": last_date.strftime("%Y-%m-%d")
},
"input_records": len([Link]),
"extracted_at": [Link]().strftime("%Y-%m-%d %H:%M:%S"),
"extracted_by": user['name'],
"user_role": user['role']
}
}
[Link](f"✅ Feature extraction successful for {x_username}:
{len(monthly_df)} months")
return response
except Exception as e:
[Link](f"❌ Feature extraction failed for {x_username}: {str(e)}",
exc_info=True)
raise HTTPException(status_code=500, detail=f"Feature extraction failed:
{str(e)}")
@[Link]("/users")
async def list_users(
x_username: str = Header(None, alias="X-Username"),
x_password: str = Header(None, alias="X-Password")
):
"""
🔒 PROTECTED - List all registered users (for admin purposes).
Requires admin authentication.
Returns usernames and names only, no passwords.
"""
# Verify credentials
if not x_username or not x_password:
[Link]("❌ Unauthorized access attempt to /users")
raise HTTPException(
status_code=401,
detail="Authentication required"
)
user = verify_credentials(x_username, x_password)
if not user:
raise HTTPException(
status_code=403,
detail="Invalid username or password"
)
# Only admins can view users list
if user["role"] != "Admin":
[Link](f"❌ Non-admin user {x_username} tried to access /users")
raise HTTPException(
status_code=403,
detail="Only administrators can view users list"
)
[Link](f"✅ Users list accessed by admin: {x_username}")
users_list = [
{
"username": username,
"name": data["name"],
"role": data["role"]
}
for username, data in USERS_DB.items()
]
return {
"total_users": len(users_list),
"users": users_list,
"accessed_by": user["name"]
}