Refactor and enhance CalMiner application
- Updated README.md to reflect new features and usage instructions. - Removed deprecated Dashboard.html component and integrated dashboard functionality directly into the main application. - Revised architecture documentation for clarity and added module map and request flow diagrams. - Enhanced maintenance model to include equipment association and cost tracking. - Updated requirements.txt to include new dependencies (httpx, pandas, numpy). - Improved consumption, maintenance, production, and reporting routes with better validation and response handling. - Added unit tests for maintenance and production routes, ensuring proper CRUD operations and validation. - Enhanced reporting service to calculate and return detailed summary statistics. - Redesigned Dashboard.html for improved user experience and integrated Chart.js for visualizing simulation results.
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional
|
||||
from pydantic import BaseModel
|
||||
|
||||
from fastapi import APIRouter, Depends, status
|
||||
from pydantic import BaseModel, PositiveFloat
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from config.database import SessionLocal
|
||||
from models.consumption import Consumption
|
||||
|
||||
|
||||
router = APIRouter(prefix="/api/consumption", tags=["Consumption"])
|
||||
|
||||
|
||||
@@ -16,22 +19,25 @@ def get_db():
|
||||
db.close()
|
||||
|
||||
|
||||
# Pydantic schemas
|
||||
class ConsumptionCreate(BaseModel):
|
||||
class ConsumptionBase(BaseModel):
|
||||
scenario_id: int
|
||||
amount: float
|
||||
amount: PositiveFloat
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
class ConsumptionRead(ConsumptionCreate):
|
||||
class ConsumptionCreate(ConsumptionBase):
|
||||
pass
|
||||
|
||||
|
||||
class ConsumptionRead(ConsumptionBase):
|
||||
id: int
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
@router.post("/", response_model=ConsumptionRead)
|
||||
async def create_consumption(item: ConsumptionCreate, db: Session = Depends(get_db)):
|
||||
@router.post("/", response_model=ConsumptionRead, status_code=status.HTTP_201_CREATED)
|
||||
def create_consumption(item: ConsumptionCreate, db: Session = Depends(get_db)):
|
||||
db_item = Consumption(**item.dict())
|
||||
db.add(db_item)
|
||||
db.commit()
|
||||
@@ -40,5 +46,5 @@ async def create_consumption(item: ConsumptionCreate, db: Session = Depends(get_
|
||||
|
||||
|
||||
@router.get("/", response_model=List[ConsumptionRead])
|
||||
async def list_consumption(db: Session = Depends(get_db)):
|
||||
def list_consumption(db: Session = Depends(get_db)):
|
||||
return db.query(Consumption).all()
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from datetime import date
|
||||
from typing import List, Optional
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel, PositiveFloat
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from config.database import SessionLocal
|
||||
from models.maintenance import Maintenance
|
||||
|
||||
|
||||
router = APIRouter(prefix="/api/maintenance", tags=["Maintenance"])
|
||||
|
||||
|
||||
@@ -17,29 +20,75 @@ def get_db():
|
||||
db.close()
|
||||
|
||||
|
||||
# Pydantic schemas
|
||||
class MaintenanceCreate(BaseModel):
|
||||
class MaintenanceBase(BaseModel):
|
||||
equipment_id: int
|
||||
scenario_id: int
|
||||
details: Optional[str] = None
|
||||
maintenance_date: date
|
||||
description: Optional[str] = None
|
||||
cost: PositiveFloat
|
||||
|
||||
|
||||
class MaintenanceRead(MaintenanceCreate):
|
||||
class MaintenanceCreate(MaintenanceBase):
|
||||
pass
|
||||
|
||||
|
||||
class MaintenanceUpdate(MaintenanceBase):
|
||||
pass
|
||||
|
||||
|
||||
class MaintenanceRead(MaintenanceBase):
|
||||
id: int
|
||||
performed_at: datetime
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
@router.post("/", response_model=MaintenanceRead)
|
||||
async def create_maintenance(item: MaintenanceCreate, db: Session = Depends(get_db)):
|
||||
db_item = Maintenance(**item.dict())
|
||||
db.add(db_item)
|
||||
def _get_maintenance_or_404(db: Session, maintenance_id: int) -> Maintenance:
|
||||
maintenance = db.query(Maintenance).filter(
|
||||
Maintenance.id == maintenance_id).first()
|
||||
if maintenance is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Maintenance record {maintenance_id} not found",
|
||||
)
|
||||
return maintenance
|
||||
|
||||
|
||||
@router.post("/", response_model=MaintenanceRead, status_code=status.HTTP_201_CREATED)
|
||||
def create_maintenance(maintenance: MaintenanceCreate, db: Session = Depends(get_db)):
|
||||
db_maintenance = Maintenance(**maintenance.dict())
|
||||
db.add(db_maintenance)
|
||||
db.commit()
|
||||
db.refresh(db_item)
|
||||
return db_item
|
||||
db.refresh(db_maintenance)
|
||||
return db_maintenance
|
||||
|
||||
|
||||
@router.get("/", response_model=List[MaintenanceRead])
|
||||
async def list_maintenance(db: Session = Depends(get_db)):
|
||||
return db.query(Maintenance).all()
|
||||
def list_maintenance(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
|
||||
return db.query(Maintenance).offset(skip).limit(limit).all()
|
||||
|
||||
|
||||
@router.get("/{maintenance_id}", response_model=MaintenanceRead)
|
||||
def get_maintenance(maintenance_id: int, db: Session = Depends(get_db)):
|
||||
return _get_maintenance_or_404(db, maintenance_id)
|
||||
|
||||
|
||||
@router.put("/{maintenance_id}", response_model=MaintenanceRead)
|
||||
def update_maintenance(
|
||||
maintenance_id: int,
|
||||
payload: MaintenanceUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
db_maintenance = _get_maintenance_or_404(db, maintenance_id)
|
||||
for field, value in payload.dict().items():
|
||||
setattr(db_maintenance, field, value)
|
||||
db.commit()
|
||||
db.refresh(db_maintenance)
|
||||
return db_maintenance
|
||||
|
||||
|
||||
@router.delete("/{maintenance_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_maintenance(maintenance_id: int, db: Session = Depends(get_db)):
|
||||
db_maintenance = _get_maintenance_or_404(db, maintenance_id)
|
||||
db.delete(db_maintenance)
|
||||
db.commit()
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional
|
||||
from pydantic import BaseModel
|
||||
|
||||
from fastapi import APIRouter, Depends, status
|
||||
from pydantic import BaseModel, PositiveFloat
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from config.database import SessionLocal
|
||||
from models.production_output import ProductionOutput
|
||||
|
||||
|
||||
router = APIRouter(prefix="/api/production", tags=["Production"])
|
||||
|
||||
|
||||
@@ -16,22 +19,25 @@ def get_db():
|
||||
db.close()
|
||||
|
||||
|
||||
# Pydantic schemas
|
||||
class ProductionOutputCreate(BaseModel):
|
||||
class ProductionOutputBase(BaseModel):
|
||||
scenario_id: int
|
||||
amount: float
|
||||
amount: PositiveFloat
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
class ProductionOutputRead(ProductionOutputCreate):
|
||||
class ProductionOutputCreate(ProductionOutputBase):
|
||||
pass
|
||||
|
||||
|
||||
class ProductionOutputRead(ProductionOutputBase):
|
||||
id: int
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
@router.post("/", response_model=ProductionOutputRead)
|
||||
async def create_production(item: ProductionOutputCreate, db: Session = Depends(get_db)):
|
||||
@router.post("/", response_model=ProductionOutputRead, status_code=status.HTTP_201_CREATED)
|
||||
def create_production(item: ProductionOutputCreate, db: Session = Depends(get_db)):
|
||||
db_item = ProductionOutput(**item.dict())
|
||||
db.add(db_item)
|
||||
db.commit()
|
||||
@@ -40,5 +46,5 @@ async def create_production(item: ProductionOutputCreate, db: Session = Depends(
|
||||
|
||||
|
||||
@router.get("/", response_model=List[ProductionOutputRead])
|
||||
async def list_production(db: Session = Depends(get_db)):
|
||||
def list_production(db: Session = Depends(get_db)):
|
||||
return db.query(ProductionOutput).all()
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from fastapi import APIRouter, HTTPException, Request
|
||||
from typing import Dict, Any
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Request, status
|
||||
from pydantic import BaseModel
|
||||
|
||||
from services.reporting import generate_report
|
||||
from sqlalchemy.orm import Session
|
||||
from config.database import SessionLocal
|
||||
from services.reporting import generate_report
|
||||
|
||||
|
||||
router = APIRouter(prefix="/api/reporting", tags=["Reporting"])
|
||||
|
||||
@@ -16,11 +18,53 @@ def get_db():
|
||||
db.close()
|
||||
|
||||
|
||||
@router.post("/summary", response_model=Dict[str, float])
|
||||
def _validate_payload(payload: Any) -> List[Dict[str, float]]:
|
||||
if not isinstance(payload, list):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Invalid input format",
|
||||
)
|
||||
|
||||
validated: List[Dict[str, float]] = []
|
||||
for index, item in enumerate(payload):
|
||||
if not isinstance(item, dict):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Entry at index {index} must be an object",
|
||||
)
|
||||
value = item.get("result")
|
||||
if not isinstance(value, (int, float)):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Entry at index {index} must include numeric 'result'",
|
||||
)
|
||||
validated.append({"result": float(value)})
|
||||
return validated
|
||||
|
||||
|
||||
class ReportSummary(BaseModel):
|
||||
count: int
|
||||
mean: float
|
||||
median: float
|
||||
min: float
|
||||
max: float
|
||||
std_dev: float
|
||||
percentile_10: float
|
||||
percentile_90: float
|
||||
|
||||
|
||||
@router.post("/summary", response_model=ReportSummary)
|
||||
async def summary_report(request: Request):
|
||||
# Read raw JSON to handle invalid input formats
|
||||
data = await request.json()
|
||||
if not isinstance(data, list):
|
||||
raise HTTPException(status_code=400, detail="Invalid input format")
|
||||
report = generate_report(data)
|
||||
return report
|
||||
payload = await request.json()
|
||||
validated_payload = _validate_payload(payload)
|
||||
summary = generate_report(validated_payload)
|
||||
return ReportSummary(
|
||||
count=int(summary["count"]),
|
||||
mean=float(summary["mean"]),
|
||||
median=float(summary["median"]),
|
||||
min=float(summary["min"]),
|
||||
max=float(summary["max"]),
|
||||
std_dev=float(summary["std_dev"]),
|
||||
percentile_10=float(summary["percentile_10"]),
|
||||
percentile_90=float(summary["percentile_90"]),
|
||||
)
|
||||
|
||||
@@ -37,13 +37,16 @@ def get_db():
|
||||
|
||||
@router.post("/", response_model=ScenarioRead)
|
||||
def create_scenario(scenario: ScenarioCreate, db: Session = Depends(get_db)):
|
||||
print(f"Creating scenario with name: {scenario.name}")
|
||||
db_s = db.query(Scenario).filter(Scenario.name == scenario.name).first()
|
||||
if db_s:
|
||||
print(f"Scenario with name {scenario.name} already exists.")
|
||||
raise HTTPException(status_code=400, detail="Scenario already exists")
|
||||
new_s = Scenario(name=scenario.name, description=scenario.description)
|
||||
db.add(new_s)
|
||||
db.commit()
|
||||
db.refresh(new_s)
|
||||
print(f"Scenario with name {scenario.name} created successfully.")
|
||||
return new_s
|
||||
|
||||
|
||||
|
||||
@@ -18,9 +18,3 @@ async def scenario_form(request: Request):
|
||||
async def parameter_form(request: Request):
|
||||
"""Render the parameter input form."""
|
||||
return templates.TemplateResponse("ParameterInput.html", {"request": request})
|
||||
|
||||
|
||||
@router.get("/", response_class=HTMLResponse)
|
||||
async def dashboard(request: Request):
|
||||
"""Render the central dashboard page."""
|
||||
return templates.TemplateResponse("Dashboard.html", {"request": request})
|
||||
|
||||
Reference in New Issue
Block a user