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:
2025-10-20 20:53:55 +02:00
parent fee857637f
commit e73a987d25
19 changed files with 794 additions and 184 deletions

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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"]),
)

View File

@@ -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

View File

@@ -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})