v2 init
This commit is contained in:
@@ -1,52 +0,0 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, status
|
||||
from pydantic import BaseModel, ConfigDict, PositiveFloat, field_validator
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from models.consumption import Consumption
|
||||
from routes.dependencies import get_db
|
||||
|
||||
|
||||
router = APIRouter(prefix="/api/consumption", tags=["Consumption"])
|
||||
|
||||
|
||||
class ConsumptionBase(BaseModel):
|
||||
scenario_id: int
|
||||
amount: PositiveFloat
|
||||
description: Optional[str] = None
|
||||
unit_name: Optional[str] = None
|
||||
unit_symbol: Optional[str] = None
|
||||
|
||||
@field_validator("unit_name", "unit_symbol")
|
||||
@classmethod
|
||||
def _normalize_text(cls, value: Optional[str]) -> Optional[str]:
|
||||
if value is None:
|
||||
return None
|
||||
stripped = value.strip()
|
||||
return stripped or None
|
||||
|
||||
|
||||
class ConsumptionCreate(ConsumptionBase):
|
||||
pass
|
||||
|
||||
|
||||
class ConsumptionRead(ConsumptionBase):
|
||||
id: int
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
@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.model_dump())
|
||||
db.add(db_item)
|
||||
db.commit()
|
||||
db.refresh(db_item)
|
||||
return db_item
|
||||
|
||||
|
||||
@router.get("/", response_model=List[ConsumptionRead])
|
||||
def list_consumption(db: Session = Depends(get_db)):
|
||||
return db.query(Consumption).all()
|
||||
121
routes/costs.py
121
routes/costs.py
@@ -1,121 +0,0 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import BaseModel, ConfigDict, field_validator
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from models.capex import Capex
|
||||
from models.opex import Opex
|
||||
from routes.dependencies import get_db
|
||||
|
||||
router = APIRouter(prefix="/api/costs", tags=["Costs"])
|
||||
# Pydantic schemas for CAPEX and OPEX
|
||||
|
||||
|
||||
class _CostBase(BaseModel):
|
||||
scenario_id: int
|
||||
amount: float
|
||||
description: Optional[str] = None
|
||||
currency_code: Optional[str] = "USD"
|
||||
currency_id: Optional[int] = None
|
||||
|
||||
@field_validator("currency_code")
|
||||
@classmethod
|
||||
def _normalize_currency(cls, value: Optional[str]) -> str:
|
||||
code = (value or "USD").strip().upper()
|
||||
return code[:3] if len(code) > 3 else code
|
||||
|
||||
|
||||
class CapexCreate(_CostBase):
|
||||
pass
|
||||
|
||||
|
||||
class CapexRead(_CostBase):
|
||||
id: int
|
||||
# use from_attributes so Pydantic reads attributes off SQLAlchemy model
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
# optionally include nested currency info
|
||||
currency: Optional["CurrencyRead"] = None
|
||||
|
||||
|
||||
class OpexCreate(_CostBase):
|
||||
pass
|
||||
|
||||
|
||||
class OpexRead(_CostBase):
|
||||
id: int
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
currency: Optional["CurrencyRead"] = None
|
||||
|
||||
|
||||
class CurrencyRead(BaseModel):
|
||||
id: int
|
||||
code: str
|
||||
name: Optional[str] = None
|
||||
symbol: Optional[str] = None
|
||||
is_active: Optional[bool] = True
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
# forward refs
|
||||
CapexRead.model_rebuild()
|
||||
OpexRead.model_rebuild()
|
||||
|
||||
|
||||
# Capex endpoints
|
||||
@router.post("/capex", response_model=CapexRead)
|
||||
def create_capex(item: CapexCreate, db: Session = Depends(get_db)):
|
||||
payload = item.model_dump()
|
||||
# Prefer explicit currency_id if supplied
|
||||
cid = payload.get("currency_id")
|
||||
if not cid:
|
||||
code = (payload.pop("currency_code", "USD") or "USD").strip().upper()
|
||||
currency_cls = __import__(
|
||||
"models.currency", fromlist=["Currency"]
|
||||
).Currency
|
||||
currency = db.query(currency_cls).filter_by(code=code).one_or_none()
|
||||
if currency is None:
|
||||
currency = currency_cls(code=code, name=code, symbol=None)
|
||||
db.add(currency)
|
||||
db.flush()
|
||||
payload["currency_id"] = currency.id
|
||||
db_item = Capex(**payload)
|
||||
db.add(db_item)
|
||||
db.commit()
|
||||
db.refresh(db_item)
|
||||
return db_item
|
||||
|
||||
|
||||
@router.get("/capex", response_model=List[CapexRead])
|
||||
def list_capex(db: Session = Depends(get_db)):
|
||||
return db.query(Capex).all()
|
||||
|
||||
|
||||
# Opex endpoints
|
||||
@router.post("/opex", response_model=OpexRead)
|
||||
def create_opex(item: OpexCreate, db: Session = Depends(get_db)):
|
||||
payload = item.model_dump()
|
||||
cid = payload.get("currency_id")
|
||||
if not cid:
|
||||
code = (payload.pop("currency_code", "USD") or "USD").strip().upper()
|
||||
currency_cls = __import__(
|
||||
"models.currency", fromlist=["Currency"]
|
||||
).Currency
|
||||
currency = db.query(currency_cls).filter_by(code=code).one_or_none()
|
||||
if currency is None:
|
||||
currency = currency_cls(code=code, name=code, symbol=None)
|
||||
db.add(currency)
|
||||
db.flush()
|
||||
payload["currency_id"] = currency.id
|
||||
db_item = Opex(**payload)
|
||||
db.add(db_item)
|
||||
db.commit()
|
||||
db.refresh(db_item)
|
||||
return db_item
|
||||
|
||||
|
||||
@router.get("/opex", response_model=List[OpexRead])
|
||||
def list_opex(db: Session = Depends(get_db)):
|
||||
return db.query(Opex).all()
|
||||
@@ -1,193 +0,0 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from models.currency import Currency
|
||||
from routes.dependencies import get_db
|
||||
|
||||
router = APIRouter(prefix="/api/currencies", tags=["Currencies"])
|
||||
|
||||
|
||||
DEFAULT_CURRENCY_CODE = "USD"
|
||||
DEFAULT_CURRENCY_NAME = "US Dollar"
|
||||
DEFAULT_CURRENCY_SYMBOL = "$"
|
||||
|
||||
|
||||
class CurrencyBase(BaseModel):
|
||||
name: str = Field(..., min_length=1, max_length=128)
|
||||
symbol: Optional[str] = Field(default=None, max_length=8)
|
||||
|
||||
@staticmethod
|
||||
def _normalize_symbol(value: Optional[str]) -> Optional[str]:
|
||||
if value is None:
|
||||
return None
|
||||
value = value.strip()
|
||||
return value or None
|
||||
|
||||
@field_validator("name")
|
||||
@classmethod
|
||||
def _strip_name(cls, value: str) -> str:
|
||||
return value.strip()
|
||||
|
||||
@field_validator("symbol")
|
||||
@classmethod
|
||||
def _strip_symbol(cls, value: Optional[str]) -> Optional[str]:
|
||||
return cls._normalize_symbol(value)
|
||||
|
||||
|
||||
class CurrencyCreate(CurrencyBase):
|
||||
code: str = Field(..., min_length=3, max_length=3)
|
||||
is_active: bool = True
|
||||
|
||||
@field_validator("code")
|
||||
@classmethod
|
||||
def _normalize_code(cls, value: str) -> str:
|
||||
return value.strip().upper()
|
||||
|
||||
|
||||
class CurrencyUpdate(CurrencyBase):
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
|
||||
class CurrencyActivation(BaseModel):
|
||||
is_active: bool
|
||||
|
||||
|
||||
class CurrencyRead(CurrencyBase):
|
||||
id: int
|
||||
code: str
|
||||
is_active: bool
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
def _ensure_default_currency(db: Session) -> Currency:
|
||||
existing = (
|
||||
db.query(Currency)
|
||||
.filter(Currency.code == DEFAULT_CURRENCY_CODE)
|
||||
.one_or_none()
|
||||
)
|
||||
if existing:
|
||||
return existing
|
||||
|
||||
default_currency = Currency(
|
||||
code=DEFAULT_CURRENCY_CODE,
|
||||
name=DEFAULT_CURRENCY_NAME,
|
||||
symbol=DEFAULT_CURRENCY_SYMBOL,
|
||||
is_active=True,
|
||||
)
|
||||
db.add(default_currency)
|
||||
try:
|
||||
db.commit()
|
||||
except IntegrityError:
|
||||
db.rollback()
|
||||
existing = (
|
||||
db.query(Currency)
|
||||
.filter(Currency.code == DEFAULT_CURRENCY_CODE)
|
||||
.one()
|
||||
)
|
||||
return existing
|
||||
db.refresh(default_currency)
|
||||
return default_currency
|
||||
|
||||
|
||||
def _get_currency_or_404(db: Session, code: str) -> Currency:
|
||||
normalized = code.strip().upper()
|
||||
currency = (
|
||||
db.query(Currency).filter(Currency.code == normalized).one_or_none()
|
||||
)
|
||||
if currency is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Currency not found"
|
||||
)
|
||||
return currency
|
||||
|
||||
|
||||
@router.get("/", response_model=List[CurrencyRead])
|
||||
def list_currencies(
|
||||
include_inactive: bool = Query(
|
||||
False, description="Include inactive currencies"
|
||||
),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
_ensure_default_currency(db)
|
||||
query = db.query(Currency)
|
||||
if not include_inactive:
|
||||
query = query.filter(Currency.is_active.is_(True))
|
||||
currencies = query.order_by(Currency.code).all()
|
||||
return currencies
|
||||
|
||||
|
||||
@router.post(
|
||||
"/", response_model=CurrencyRead, status_code=status.HTTP_201_CREATED
|
||||
)
|
||||
def create_currency(payload: CurrencyCreate, db: Session = Depends(get_db)):
|
||||
code = payload.code
|
||||
existing = db.query(Currency).filter(Currency.code == code).one_or_none()
|
||||
if existing is not None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=f"Currency '{code}' already exists",
|
||||
)
|
||||
|
||||
currency = Currency(
|
||||
code=code,
|
||||
name=payload.name,
|
||||
symbol=CurrencyBase._normalize_symbol(payload.symbol),
|
||||
is_active=payload.is_active,
|
||||
)
|
||||
db.add(currency)
|
||||
db.commit()
|
||||
db.refresh(currency)
|
||||
return currency
|
||||
|
||||
|
||||
@router.put("/{code}", response_model=CurrencyRead)
|
||||
def update_currency(
|
||||
code: str, payload: CurrencyUpdate, db: Session = Depends(get_db)
|
||||
):
|
||||
currency = _get_currency_or_404(db, code)
|
||||
|
||||
if payload.name is not None:
|
||||
setattr(currency, "name", payload.name)
|
||||
if payload.symbol is not None or payload.symbol == "":
|
||||
setattr(
|
||||
currency,
|
||||
"symbol",
|
||||
CurrencyBase._normalize_symbol(payload.symbol),
|
||||
)
|
||||
if payload.is_active is not None:
|
||||
code_value = getattr(currency, "code")
|
||||
if code_value == DEFAULT_CURRENCY_CODE and payload.is_active is False:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="The default currency cannot be deactivated.",
|
||||
)
|
||||
setattr(currency, "is_active", payload.is_active)
|
||||
|
||||
db.add(currency)
|
||||
db.commit()
|
||||
db.refresh(currency)
|
||||
return currency
|
||||
|
||||
|
||||
@router.patch("/{code}/activation", response_model=CurrencyRead)
|
||||
def toggle_currency_activation(
|
||||
code: str, body: CurrencyActivation, db: Session = Depends(get_db)
|
||||
):
|
||||
currency = _get_currency_or_404(db, code)
|
||||
code_value = getattr(currency, "code")
|
||||
if code_value == DEFAULT_CURRENCY_CODE and body.is_active is False:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="The default currency cannot be deactivated.",
|
||||
)
|
||||
|
||||
setattr(currency, "is_active", body.is_active)
|
||||
db.add(currency)
|
||||
db.commit()
|
||||
db.refresh(currency)
|
||||
return currency
|
||||
@@ -1,13 +0,0 @@
|
||||
from collections.abc import Generator
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from config.database import SessionLocal
|
||||
|
||||
|
||||
def get_db() -> Generator[Session, None, None]:
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
@@ -1,38 +0,0 @@
|
||||
from typing import Dict, List
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from models.distribution import Distribution
|
||||
from routes.dependencies import get_db
|
||||
|
||||
router = APIRouter(prefix="/api/distributions", tags=["Distributions"])
|
||||
|
||||
|
||||
class DistributionCreate(BaseModel):
|
||||
name: str
|
||||
distribution_type: str
|
||||
parameters: Dict[str, float | int]
|
||||
|
||||
|
||||
class DistributionRead(DistributionCreate):
|
||||
id: int
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
@router.post("/", response_model=DistributionRead)
|
||||
async def create_distribution(
|
||||
dist: DistributionCreate, db: Session = Depends(get_db)
|
||||
):
|
||||
db_dist = Distribution(**dist.model_dump())
|
||||
db.add(db_dist)
|
||||
db.commit()
|
||||
db.refresh(db_dist)
|
||||
return db_dist
|
||||
|
||||
|
||||
@router.get("/", response_model=List[DistributionRead])
|
||||
async def list_distributions(db: Session = Depends(get_db)):
|
||||
dists = db.query(Distribution).all()
|
||||
return dists
|
||||
@@ -1,38 +0,0 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from models.equipment import Equipment
|
||||
from routes.dependencies import get_db
|
||||
|
||||
router = APIRouter(prefix="/api/equipment", tags=["Equipment"])
|
||||
# Pydantic schemas
|
||||
|
||||
|
||||
class EquipmentCreate(BaseModel):
|
||||
scenario_id: int
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
class EquipmentRead(EquipmentCreate):
|
||||
id: int
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
@router.post("/", response_model=EquipmentRead)
|
||||
async def create_equipment(
|
||||
item: EquipmentCreate, db: Session = Depends(get_db)
|
||||
):
|
||||
db_item = Equipment(**item.model_dump())
|
||||
db.add(db_item)
|
||||
db.commit()
|
||||
db.refresh(db_item)
|
||||
return db_item
|
||||
|
||||
|
||||
@router.get("/", response_model=List[EquipmentRead])
|
||||
async def list_equipment(db: Session = Depends(get_db)):
|
||||
return db.query(Equipment).all()
|
||||
@@ -1,91 +0,0 @@
|
||||
from datetime import date
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel, ConfigDict, PositiveFloat
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from models.maintenance import Maintenance
|
||||
from routes.dependencies import get_db
|
||||
|
||||
|
||||
router = APIRouter(prefix="/api/maintenance", tags=["Maintenance"])
|
||||
|
||||
|
||||
class MaintenanceBase(BaseModel):
|
||||
equipment_id: int
|
||||
scenario_id: int
|
||||
maintenance_date: date
|
||||
description: Optional[str] = None
|
||||
cost: PositiveFloat
|
||||
|
||||
|
||||
class MaintenanceCreate(MaintenanceBase):
|
||||
pass
|
||||
|
||||
|
||||
class MaintenanceUpdate(MaintenanceBase):
|
||||
pass
|
||||
|
||||
|
||||
class MaintenanceRead(MaintenanceBase):
|
||||
id: int
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
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.model_dump())
|
||||
db.add(db_maintenance)
|
||||
db.commit()
|
||||
db.refresh(db_maintenance)
|
||||
return db_maintenance
|
||||
|
||||
|
||||
@router.get("/", response_model=List[MaintenanceRead])
|
||||
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.model_dump().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,90 +0,0 @@
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel, ConfigDict, field_validator
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from models.distribution import Distribution
|
||||
from models.parameters import Parameter
|
||||
from models.scenario import Scenario
|
||||
from routes.dependencies import get_db
|
||||
|
||||
router = APIRouter(prefix="/api/parameters", tags=["parameters"])
|
||||
|
||||
|
||||
class ParameterCreate(BaseModel):
|
||||
scenario_id: int
|
||||
name: str
|
||||
value: float
|
||||
distribution_id: Optional[int] = None
|
||||
distribution_type: Optional[str] = None
|
||||
distribution_parameters: Optional[Dict[str, Any]] = None
|
||||
|
||||
@field_validator("distribution_type")
|
||||
@classmethod
|
||||
def normalize_type(cls, value: Optional[str]) -> Optional[str]:
|
||||
if value is None:
|
||||
return value
|
||||
normalized = value.strip().lower()
|
||||
if not normalized:
|
||||
return None
|
||||
if normalized not in {"normal", "uniform", "triangular"}:
|
||||
raise ValueError(
|
||||
"distribution_type must be normal, uniform, or triangular"
|
||||
)
|
||||
return normalized
|
||||
|
||||
@field_validator("distribution_parameters")
|
||||
@classmethod
|
||||
def empty_dict_to_none(
|
||||
cls, value: Optional[Dict[str, Any]]
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
if value is None:
|
||||
return None
|
||||
return value or None
|
||||
|
||||
|
||||
class ParameterRead(ParameterCreate):
|
||||
id: int
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
@router.post("/", response_model=ParameterRead)
|
||||
def create_parameter(param: ParameterCreate, db: Session = Depends(get_db)):
|
||||
scen = db.query(Scenario).filter(Scenario.id == param.scenario_id).first()
|
||||
if not scen:
|
||||
raise HTTPException(status_code=404, detail="Scenario not found")
|
||||
distribution_id = param.distribution_id
|
||||
distribution_type = param.distribution_type
|
||||
distribution_parameters = param.distribution_parameters
|
||||
|
||||
if distribution_id is not None:
|
||||
distribution = (
|
||||
db.query(Distribution)
|
||||
.filter(Distribution.id == distribution_id)
|
||||
.first()
|
||||
)
|
||||
if not distribution:
|
||||
raise HTTPException(
|
||||
status_code=404, detail="Distribution not found"
|
||||
)
|
||||
distribution_type = distribution.distribution_type
|
||||
distribution_parameters = distribution.parameters or None
|
||||
|
||||
new_param = Parameter(
|
||||
scenario_id=param.scenario_id,
|
||||
name=param.name,
|
||||
value=param.value,
|
||||
distribution_id=distribution_id,
|
||||
distribution_type=distribution_type,
|
||||
distribution_parameters=distribution_parameters,
|
||||
)
|
||||
db.add(new_param)
|
||||
db.commit()
|
||||
db.refresh(new_param)
|
||||
return new_param
|
||||
|
||||
|
||||
@router.get("/", response_model=List[ParameterRead])
|
||||
def list_parameters(db: Session = Depends(get_db)):
|
||||
return db.query(Parameter).all()
|
||||
@@ -1,56 +0,0 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, status
|
||||
from pydantic import BaseModel, ConfigDict, PositiveFloat, field_validator
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from models.production_output import ProductionOutput
|
||||
from routes.dependencies import get_db
|
||||
|
||||
|
||||
router = APIRouter(prefix="/api/production", tags=["Production"])
|
||||
|
||||
|
||||
class ProductionOutputBase(BaseModel):
|
||||
scenario_id: int
|
||||
amount: PositiveFloat
|
||||
description: Optional[str] = None
|
||||
unit_name: Optional[str] = None
|
||||
unit_symbol: Optional[str] = None
|
||||
|
||||
@field_validator("unit_name", "unit_symbol")
|
||||
@classmethod
|
||||
def _normalize_text(cls, value: Optional[str]) -> Optional[str]:
|
||||
if value is None:
|
||||
return None
|
||||
stripped = value.strip()
|
||||
return stripped or None
|
||||
|
||||
|
||||
class ProductionOutputCreate(ProductionOutputBase):
|
||||
pass
|
||||
|
||||
|
||||
class ProductionOutputRead(ProductionOutputBase):
|
||||
id: int
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
@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.model_dump())
|
||||
db.add(db_item)
|
||||
db.commit()
|
||||
db.refresh(db_item)
|
||||
return db_item
|
||||
|
||||
|
||||
@router.get("/", response_model=List[ProductionOutputRead])
|
||||
def list_production(db: Session = Depends(get_db)):
|
||||
return db.query(ProductionOutput).all()
|
||||
@@ -1,73 +0,0 @@
|
||||
from typing import Any, Dict, List, cast
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Request, status
|
||||
from pydantic import BaseModel
|
||||
|
||||
from services.reporting import generate_report
|
||||
|
||||
|
||||
router = APIRouter(prefix="/api/reporting", tags=["Reporting"])
|
||||
|
||||
|
||||
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",
|
||||
)
|
||||
|
||||
typed_payload = cast(List[Any], payload)
|
||||
|
||||
validated: List[Dict[str, float]] = []
|
||||
for index, item in enumerate(typed_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 = cast(Dict[str, Any], 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
|
||||
variance: float
|
||||
percentile_10: float
|
||||
percentile_90: float
|
||||
percentile_5: float
|
||||
percentile_95: float
|
||||
value_at_risk_95: float
|
||||
expected_shortfall_95: float
|
||||
|
||||
|
||||
@router.post("/summary", response_model=ReportSummary)
|
||||
async def summary_report(request: Request):
|
||||
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"]),
|
||||
variance=float(summary["variance"]),
|
||||
percentile_10=float(summary["percentile_10"]),
|
||||
percentile_90=float(summary["percentile_90"]),
|
||||
percentile_5=float(summary["percentile_5"]),
|
||||
percentile_95=float(summary["percentile_95"]),
|
||||
value_at_risk_95=float(summary["value_at_risk_95"]),
|
||||
expected_shortfall_95=float(summary["expected_shortfall_95"]),
|
||||
)
|
||||
@@ -1,42 +0,0 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from models.scenario import Scenario
|
||||
from routes.dependencies import get_db
|
||||
|
||||
router = APIRouter(prefix="/api/scenarios", tags=["scenarios"])
|
||||
|
||||
# Pydantic schemas
|
||||
|
||||
|
||||
class ScenarioCreate(BaseModel):
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
class ScenarioRead(ScenarioCreate):
|
||||
id: int
|
||||
created_at: datetime
|
||||
updated_at: Optional[datetime] = None
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
@router.post("/", response_model=ScenarioRead)
|
||||
def create_scenario(scenario: ScenarioCreate, db: Session = Depends(get_db)):
|
||||
db_s = db.query(Scenario).filter(Scenario.name == scenario.name).first()
|
||||
if db_s:
|
||||
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)
|
||||
return new_s
|
||||
|
||||
|
||||
@router.get("/", response_model=list[ScenarioRead])
|
||||
def list_scenarios(db: Session = Depends(get_db)):
|
||||
return db.query(Scenario).all()
|
||||
@@ -1,110 +0,0 @@
|
||||
from typing import Dict, List
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from routes.dependencies import get_db
|
||||
from services.settings import (
|
||||
CSS_COLOR_DEFAULTS,
|
||||
get_css_color_settings,
|
||||
list_css_env_override_rows,
|
||||
read_css_color_env_overrides,
|
||||
update_css_color_settings,
|
||||
get_theme_settings,
|
||||
save_theme_settings,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/api/settings", tags=["Settings"])
|
||||
|
||||
|
||||
class CSSSettingsPayload(BaseModel):
|
||||
variables: Dict[str, str] = Field(default_factory=dict)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _validate_allowed_keys(self) -> "CSSSettingsPayload":
|
||||
invalid = set(self.variables.keys()) - set(CSS_COLOR_DEFAULTS.keys())
|
||||
if invalid:
|
||||
invalid_keys = ", ".join(sorted(invalid))
|
||||
raise ValueError(
|
||||
f"Unsupported CSS variables: {invalid_keys}."
|
||||
" Accepted keys align with the default theme variables."
|
||||
)
|
||||
return self
|
||||
|
||||
|
||||
class EnvOverride(BaseModel):
|
||||
css_key: str
|
||||
env_var: str
|
||||
value: str
|
||||
|
||||
|
||||
class CSSSettingsResponse(BaseModel):
|
||||
variables: Dict[str, str]
|
||||
env_overrides: Dict[str, str] = Field(default_factory=dict)
|
||||
env_sources: List[EnvOverride] = Field(default_factory=list)
|
||||
|
||||
|
||||
@router.get("/css", response_model=CSSSettingsResponse)
|
||||
def read_css_settings(db: Session = Depends(get_db)) -> CSSSettingsResponse:
|
||||
try:
|
||||
values = get_css_color_settings(db)
|
||||
env_overrides = read_css_color_env_overrides()
|
||||
env_sources = [
|
||||
EnvOverride(**row) for row in list_css_env_override_rows()
|
||||
]
|
||||
except ValueError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
return CSSSettingsResponse(
|
||||
variables=values,
|
||||
env_overrides=env_overrides,
|
||||
env_sources=env_sources,
|
||||
)
|
||||
|
||||
|
||||
@router.put(
|
||||
"/css", response_model=CSSSettingsResponse, status_code=status.HTTP_200_OK
|
||||
)
|
||||
def update_css_settings(
|
||||
payload: CSSSettingsPayload, db: Session = Depends(get_db)
|
||||
) -> CSSSettingsResponse:
|
||||
try:
|
||||
values = update_css_color_settings(db, payload.variables)
|
||||
env_overrides = read_css_color_env_overrides()
|
||||
env_sources = [
|
||||
EnvOverride(**row) for row in list_css_env_override_rows()
|
||||
]
|
||||
except ValueError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
return CSSSettingsResponse(
|
||||
variables=values,
|
||||
env_overrides=env_overrides,
|
||||
env_sources=env_sources,
|
||||
)
|
||||
|
||||
|
||||
class ThemeSettings(BaseModel):
|
||||
theme_name: str
|
||||
primary_color: str
|
||||
secondary_color: str
|
||||
accent_color: str
|
||||
background_color: str
|
||||
text_color: str
|
||||
|
||||
|
||||
@router.post("/theme")
|
||||
async def update_theme(theme_data: ThemeSettings, db: Session = Depends(get_db)):
|
||||
data_dict = theme_data.model_dump()
|
||||
save_theme_settings(db, data_dict)
|
||||
return {"message": "Theme updated", "theme": data_dict}
|
||||
|
||||
|
||||
@router.get("/theme")
|
||||
async def get_theme(db: Session = Depends(get_db)):
|
||||
return get_theme_settings(db)
|
||||
@@ -1,126 +0,0 @@
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel, PositiveInt
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from models.parameters import Parameter
|
||||
from models.scenario import Scenario
|
||||
from models.simulation_result import SimulationResult
|
||||
from routes.dependencies import get_db
|
||||
from services.reporting import generate_report
|
||||
from services.simulation import run_simulation
|
||||
|
||||
router = APIRouter(prefix="/api/simulations", tags=["Simulations"])
|
||||
|
||||
|
||||
class SimulationParameterInput(BaseModel):
|
||||
name: str
|
||||
value: float
|
||||
distribution: Optional[str] = "normal"
|
||||
std_dev: Optional[float] = None
|
||||
min: Optional[float] = None
|
||||
max: Optional[float] = None
|
||||
mode: Optional[float] = None
|
||||
|
||||
|
||||
class SimulationRunRequest(BaseModel):
|
||||
scenario_id: int
|
||||
iterations: PositiveInt = 1000
|
||||
parameters: Optional[List[SimulationParameterInput]] = None
|
||||
seed: Optional[int] = None
|
||||
|
||||
|
||||
class SimulationResultItem(BaseModel):
|
||||
iteration: int
|
||||
result: float
|
||||
|
||||
|
||||
class SimulationRunResponse(BaseModel):
|
||||
scenario_id: int
|
||||
iterations: int
|
||||
results: List[SimulationResultItem]
|
||||
summary: Dict[str, float | int]
|
||||
|
||||
|
||||
def _load_parameters(
|
||||
db: Session, scenario_id: int
|
||||
) -> List[SimulationParameterInput]:
|
||||
db_params = (
|
||||
db.query(Parameter)
|
||||
.filter(Parameter.scenario_id == scenario_id)
|
||||
.order_by(Parameter.id)
|
||||
.all()
|
||||
)
|
||||
return [
|
||||
SimulationParameterInput(
|
||||
name=item.name,
|
||||
value=item.value,
|
||||
)
|
||||
for item in db_params
|
||||
]
|
||||
|
||||
|
||||
@router.post("/run", response_model=SimulationRunResponse)
|
||||
async def simulate(
|
||||
payload: SimulationRunRequest, db: Session = Depends(get_db)
|
||||
):
|
||||
scenario = (
|
||||
db.query(Scenario).filter(Scenario.id == payload.scenario_id).first()
|
||||
)
|
||||
if scenario is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Scenario not found",
|
||||
)
|
||||
|
||||
parameters = payload.parameters or _load_parameters(db, payload.scenario_id)
|
||||
if not parameters:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="No parameters provided",
|
||||
)
|
||||
|
||||
raw_results = run_simulation(
|
||||
[param.model_dump(exclude_none=True) for param in parameters],
|
||||
iterations=payload.iterations,
|
||||
seed=payload.seed,
|
||||
)
|
||||
|
||||
if not raw_results:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Simulation produced no results",
|
||||
)
|
||||
|
||||
# Persist results (replace existing values for scenario)
|
||||
db.query(SimulationResult).filter(
|
||||
SimulationResult.scenario_id == payload.scenario_id
|
||||
).delete()
|
||||
db.bulk_save_objects(
|
||||
[
|
||||
SimulationResult(
|
||||
scenario_id=payload.scenario_id,
|
||||
iteration=item["iteration"],
|
||||
result=item["result"],
|
||||
)
|
||||
for item in raw_results
|
||||
]
|
||||
)
|
||||
db.commit()
|
||||
|
||||
summary = generate_report(raw_results)
|
||||
|
||||
response = SimulationRunResponse(
|
||||
scenario_id=payload.scenario_id,
|
||||
iterations=payload.iterations,
|
||||
results=[
|
||||
SimulationResultItem(
|
||||
iteration=int(item["iteration"]),
|
||||
result=float(item["result"]),
|
||||
)
|
||||
for item in raw_results
|
||||
],
|
||||
summary=summary,
|
||||
)
|
||||
return response
|
||||
784
routes/ui.py
784
routes/ui.py
@@ -1,784 +0,0 @@
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from models.capex import Capex
|
||||
from models.consumption import Consumption
|
||||
from models.equipment import Equipment
|
||||
from models.maintenance import Maintenance
|
||||
from models.opex import Opex
|
||||
from models.parameters import Parameter
|
||||
from models.production_output import ProductionOutput
|
||||
from models.scenario import Scenario
|
||||
from models.simulation_result import SimulationResult
|
||||
from routes.dependencies import get_db
|
||||
from services.reporting import generate_report
|
||||
from models.currency import Currency
|
||||
from routes.currencies import DEFAULT_CURRENCY_CODE, _ensure_default_currency
|
||||
from services.settings import (
|
||||
CSS_COLOR_DEFAULTS,
|
||||
get_css_color_settings,
|
||||
list_css_env_override_rows,
|
||||
read_css_color_env_overrides,
|
||||
)
|
||||
|
||||
|
||||
CURRENCY_CHOICES: list[Dict[str, Any]] = [
|
||||
{"id": "USD", "name": "US Dollar (USD)"},
|
||||
{"id": "EUR", "name": "Euro (EUR)"},
|
||||
{"id": "CLP", "name": "Chilean Peso (CLP)"},
|
||||
{"id": "RMB", "name": "Chinese Yuan (RMB)"},
|
||||
{"id": "GBP", "name": "British Pound (GBP)"},
|
||||
{"id": "CAD", "name": "Canadian Dollar (CAD)"},
|
||||
{"id": "AUD", "name": "Australian Dollar (AUD)"},
|
||||
]
|
||||
|
||||
MEASUREMENT_UNITS: list[Dict[str, Any]] = [
|
||||
{"id": "tonnes", "name": "Tonnes", "symbol": "t"},
|
||||
{"id": "kilograms", "name": "Kilograms", "symbol": "kg"},
|
||||
{"id": "pounds", "name": "Pounds", "symbol": "lb"},
|
||||
{"id": "liters", "name": "Liters", "symbol": "L"},
|
||||
{"id": "cubic_meters", "name": "Cubic Meters", "symbol": "m3"},
|
||||
{"id": "kilowatt_hours", "name": "Kilowatt Hours", "symbol": "kWh"},
|
||||
]
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# Set up Jinja2 templates directory
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
|
||||
|
||||
def _context(
|
||||
request: Request, extra: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
payload: Dict[str, Any] = {
|
||||
"request": request,
|
||||
"current_year": datetime.now(timezone.utc).year,
|
||||
}
|
||||
if extra:
|
||||
payload.update(extra)
|
||||
return payload
|
||||
|
||||
|
||||
def _render(
|
||||
request: Request,
|
||||
template_name: str,
|
||||
extra: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
context = _context(request, extra)
|
||||
return templates.TemplateResponse(request, template_name, context)
|
||||
|
||||
|
||||
def _format_currency(value: float) -> str:
|
||||
return f"${value:,.2f}"
|
||||
|
||||
|
||||
def _format_decimal(value: float) -> str:
|
||||
return f"{value:,.2f}"
|
||||
|
||||
|
||||
def _format_int(value: int) -> str:
|
||||
return f"{value:,}"
|
||||
|
||||
|
||||
def _load_scenarios(db: Session) -> Dict[str, Any]:
|
||||
scenarios: list[Dict[str, Any]] = [
|
||||
{
|
||||
"id": item.id,
|
||||
"name": item.name,
|
||||
"description": item.description,
|
||||
}
|
||||
for item in db.query(Scenario).order_by(Scenario.name).all()
|
||||
]
|
||||
return {"scenarios": scenarios}
|
||||
|
||||
|
||||
def _load_parameters(db: Session) -> Dict[str, Any]:
|
||||
grouped: defaultdict[int, list[Dict[str, Any]]] = defaultdict(list)
|
||||
for param in db.query(Parameter).order_by(
|
||||
Parameter.scenario_id, Parameter.id
|
||||
):
|
||||
grouped[param.scenario_id].append(
|
||||
{
|
||||
"id": param.id,
|
||||
"name": param.name,
|
||||
"value": param.value,
|
||||
"distribution_type": param.distribution_type,
|
||||
"distribution_parameters": param.distribution_parameters,
|
||||
}
|
||||
)
|
||||
return {"parameters_by_scenario": dict(grouped)}
|
||||
|
||||
|
||||
def _load_costs(db: Session) -> Dict[str, Any]:
|
||||
capex_grouped: defaultdict[int, list[Dict[str, Any]]] = defaultdict(list)
|
||||
for capex in db.query(Capex).order_by(Capex.scenario_id, Capex.id).all():
|
||||
capex_grouped[int(getattr(capex, "scenario_id"))].append(
|
||||
{
|
||||
"id": int(getattr(capex, "id")),
|
||||
"scenario_id": int(getattr(capex, "scenario_id")),
|
||||
"amount": float(getattr(capex, "amount", 0.0)),
|
||||
"description": getattr(capex, "description", "") or "",
|
||||
"currency_code": getattr(capex, "currency_code", "USD")
|
||||
or "USD",
|
||||
}
|
||||
)
|
||||
|
||||
opex_grouped: defaultdict[int, list[Dict[str, Any]]] = defaultdict(list)
|
||||
for opex in db.query(Opex).order_by(Opex.scenario_id, Opex.id).all():
|
||||
opex_grouped[int(getattr(opex, "scenario_id"))].append(
|
||||
{
|
||||
"id": int(getattr(opex, "id")),
|
||||
"scenario_id": int(getattr(opex, "scenario_id")),
|
||||
"amount": float(getattr(opex, "amount", 0.0)),
|
||||
"description": getattr(opex, "description", "") or "",
|
||||
"currency_code": getattr(opex, "currency_code", "USD") or "USD",
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"capex_by_scenario": dict(capex_grouped),
|
||||
"opex_by_scenario": dict(opex_grouped),
|
||||
}
|
||||
|
||||
|
||||
def _load_currencies(db: Session) -> Dict[str, Any]:
|
||||
items: list[Dict[str, Any]] = []
|
||||
for c in (
|
||||
db.query(Currency)
|
||||
.filter_by(is_active=True)
|
||||
.order_by(Currency.code)
|
||||
.all()
|
||||
):
|
||||
items.append(
|
||||
{"id": c.code, "name": f"{c.name} ({c.code})", "symbol": c.symbol}
|
||||
)
|
||||
if not items:
|
||||
items.append({"id": "USD", "name": "US Dollar (USD)", "symbol": "$"})
|
||||
return {"currency_options": items}
|
||||
|
||||
|
||||
def _load_currency_settings(db: Session) -> Dict[str, Any]:
|
||||
_ensure_default_currency(db)
|
||||
records = db.query(Currency).order_by(Currency.code).all()
|
||||
currencies: list[Dict[str, Any]] = []
|
||||
for record in records:
|
||||
code_value = getattr(record, "code")
|
||||
currencies.append(
|
||||
{
|
||||
"id": int(getattr(record, "id")),
|
||||
"code": code_value,
|
||||
"name": getattr(record, "name"),
|
||||
"symbol": getattr(record, "symbol"),
|
||||
"is_active": bool(getattr(record, "is_active", True)),
|
||||
"is_default": code_value == DEFAULT_CURRENCY_CODE,
|
||||
}
|
||||
)
|
||||
|
||||
active_count = sum(1 for item in currencies if item["is_active"])
|
||||
inactive_count = len(currencies) - active_count
|
||||
|
||||
return {
|
||||
"currencies": currencies,
|
||||
"currency_stats": {
|
||||
"total": len(currencies),
|
||||
"active": active_count,
|
||||
"inactive": inactive_count,
|
||||
},
|
||||
"default_currency_code": DEFAULT_CURRENCY_CODE,
|
||||
"currency_api_base": "/api/currencies",
|
||||
}
|
||||
|
||||
|
||||
def _load_css_settings(db: Session) -> Dict[str, Any]:
|
||||
variables = get_css_color_settings(db)
|
||||
env_overrides = read_css_color_env_overrides()
|
||||
env_rows = list_css_env_override_rows()
|
||||
env_meta = {row["css_key"]: row for row in env_rows}
|
||||
return {
|
||||
"css_variables": variables,
|
||||
"css_defaults": CSS_COLOR_DEFAULTS,
|
||||
"css_env_overrides": env_overrides,
|
||||
"css_env_override_rows": env_rows,
|
||||
"css_env_override_meta": env_meta,
|
||||
}
|
||||
|
||||
|
||||
def _load_consumption(db: Session) -> Dict[str, Any]:
|
||||
grouped: defaultdict[int, list[Dict[str, Any]]] = defaultdict(list)
|
||||
for record in (
|
||||
db.query(Consumption)
|
||||
.order_by(Consumption.scenario_id, Consumption.id)
|
||||
.all()
|
||||
):
|
||||
record_id = int(getattr(record, "id"))
|
||||
scenario_id = int(getattr(record, "scenario_id"))
|
||||
amount_value = float(getattr(record, "amount", 0.0))
|
||||
description = getattr(record, "description", "") or ""
|
||||
unit_name = getattr(record, "unit_name", None)
|
||||
unit_symbol = getattr(record, "unit_symbol", None)
|
||||
grouped[scenario_id].append(
|
||||
{
|
||||
"id": record_id,
|
||||
"scenario_id": scenario_id,
|
||||
"amount": amount_value,
|
||||
"description": description,
|
||||
"unit_name": unit_name,
|
||||
"unit_symbol": unit_symbol,
|
||||
}
|
||||
)
|
||||
return {"consumption_by_scenario": dict(grouped)}
|
||||
|
||||
|
||||
def _load_production(db: Session) -> Dict[str, Any]:
|
||||
grouped: defaultdict[int, list[Dict[str, Any]]] = defaultdict(list)
|
||||
for record in (
|
||||
db.query(ProductionOutput)
|
||||
.order_by(ProductionOutput.scenario_id, ProductionOutput.id)
|
||||
.all()
|
||||
):
|
||||
record_id = int(getattr(record, "id"))
|
||||
scenario_id = int(getattr(record, "scenario_id"))
|
||||
amount_value = float(getattr(record, "amount", 0.0))
|
||||
description = getattr(record, "description", "") or ""
|
||||
unit_name = getattr(record, "unit_name", None)
|
||||
unit_symbol = getattr(record, "unit_symbol", None)
|
||||
grouped[scenario_id].append(
|
||||
{
|
||||
"id": record_id,
|
||||
"scenario_id": scenario_id,
|
||||
"amount": amount_value,
|
||||
"description": description,
|
||||
"unit_name": unit_name,
|
||||
"unit_symbol": unit_symbol,
|
||||
}
|
||||
)
|
||||
return {"production_by_scenario": dict(grouped)}
|
||||
|
||||
|
||||
def _load_equipment(db: Session) -> Dict[str, Any]:
|
||||
grouped: defaultdict[int, list[Dict[str, Any]]] = defaultdict(list)
|
||||
for record in (
|
||||
db.query(Equipment).order_by(Equipment.scenario_id, Equipment.id).all()
|
||||
):
|
||||
record_id = int(getattr(record, "id"))
|
||||
scenario_id = int(getattr(record, "scenario_id"))
|
||||
name_value = getattr(record, "name", "") or ""
|
||||
description = getattr(record, "description", "") or ""
|
||||
grouped[scenario_id].append(
|
||||
{
|
||||
"id": record_id,
|
||||
"scenario_id": scenario_id,
|
||||
"name": name_value,
|
||||
"description": description,
|
||||
}
|
||||
)
|
||||
return {"equipment_by_scenario": dict(grouped)}
|
||||
|
||||
|
||||
def _load_maintenance(db: Session) -> Dict[str, Any]:
|
||||
grouped: defaultdict[int, list[Dict[str, Any]]] = defaultdict(list)
|
||||
for record in (
|
||||
db.query(Maintenance)
|
||||
.order_by(Maintenance.scenario_id, Maintenance.maintenance_date)
|
||||
.all()
|
||||
):
|
||||
record_id = int(getattr(record, "id"))
|
||||
scenario_id = int(getattr(record, "scenario_id"))
|
||||
equipment_id = int(getattr(record, "equipment_id"))
|
||||
equipment_obj = getattr(record, "equipment", None)
|
||||
equipment_name = (
|
||||
getattr(equipment_obj, "name", "") if equipment_obj else ""
|
||||
)
|
||||
maintenance_date = getattr(record, "maintenance_date", None)
|
||||
cost_value = float(getattr(record, "cost", 0.0))
|
||||
description = getattr(record, "description", "") or ""
|
||||
|
||||
grouped[scenario_id].append(
|
||||
{
|
||||
"id": record_id,
|
||||
"scenario_id": scenario_id,
|
||||
"equipment_id": equipment_id,
|
||||
"equipment_name": equipment_name,
|
||||
"maintenance_date": (
|
||||
maintenance_date.isoformat() if maintenance_date else ""
|
||||
),
|
||||
"cost": cost_value,
|
||||
"description": description,
|
||||
}
|
||||
)
|
||||
return {"maintenance_by_scenario": dict(grouped)}
|
||||
|
||||
|
||||
def _load_simulations(db: Session) -> Dict[str, Any]:
|
||||
scenarios: list[Dict[str, Any]] = [
|
||||
{
|
||||
"id": item.id,
|
||||
"name": item.name,
|
||||
}
|
||||
for item in db.query(Scenario).order_by(Scenario.name).all()
|
||||
]
|
||||
|
||||
results_grouped: defaultdict[int, list[Dict[str, Any]]] = defaultdict(list)
|
||||
for record in (
|
||||
db.query(SimulationResult)
|
||||
.order_by(SimulationResult.scenario_id, SimulationResult.iteration)
|
||||
.all()
|
||||
):
|
||||
scenario_id = int(getattr(record, "scenario_id"))
|
||||
results_grouped[scenario_id].append(
|
||||
{
|
||||
"iteration": int(getattr(record, "iteration")),
|
||||
"result": float(getattr(record, "result", 0.0)),
|
||||
}
|
||||
)
|
||||
|
||||
runs: list[Dict[str, Any]] = []
|
||||
sample_limit = 20
|
||||
for item in scenarios:
|
||||
scenario_id = int(item["id"])
|
||||
scenario_results = results_grouped.get(scenario_id, [])
|
||||
summary = (
|
||||
generate_report(scenario_results)
|
||||
if scenario_results
|
||||
else generate_report([])
|
||||
)
|
||||
runs.append(
|
||||
{
|
||||
"scenario_id": scenario_id,
|
||||
"scenario_name": item["name"],
|
||||
"iterations": int(summary.get("count", 0)),
|
||||
"summary": summary,
|
||||
"sample_results": scenario_results[:sample_limit],
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"simulation_scenarios": scenarios,
|
||||
"simulation_runs": runs,
|
||||
}
|
||||
|
||||
|
||||
def _load_reporting(db: Session) -> Dict[str, Any]:
|
||||
scenarios = _load_scenarios(db)["scenarios"]
|
||||
runs = _load_simulations(db)["simulation_runs"]
|
||||
|
||||
summaries: list[Dict[str, Any]] = []
|
||||
runs_by_scenario = {run["scenario_id"]: run for run in runs}
|
||||
|
||||
for scenario in scenarios:
|
||||
scenario_id = scenario["id"]
|
||||
run = runs_by_scenario.get(scenario_id)
|
||||
summary = run["summary"] if run else generate_report([])
|
||||
summaries.append(
|
||||
{
|
||||
"scenario_id": scenario_id,
|
||||
"scenario_name": scenario["name"],
|
||||
"summary": summary,
|
||||
"iterations": run["iterations"] if run else 0,
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"report_summaries": summaries,
|
||||
}
|
||||
|
||||
|
||||
def _load_dashboard(db: Session) -> Dict[str, Any]:
|
||||
scenarios = _load_scenarios(db)["scenarios"]
|
||||
parameters_by_scenario = _load_parameters(db)["parameters_by_scenario"]
|
||||
costs_context = _load_costs(db)
|
||||
capex_by_scenario = costs_context["capex_by_scenario"]
|
||||
opex_by_scenario = costs_context["opex_by_scenario"]
|
||||
consumption_by_scenario = _load_consumption(db)["consumption_by_scenario"]
|
||||
production_by_scenario = _load_production(db)["production_by_scenario"]
|
||||
equipment_by_scenario = _load_equipment(db)["equipment_by_scenario"]
|
||||
maintenance_by_scenario = _load_maintenance(db)["maintenance_by_scenario"]
|
||||
simulation_context = _load_simulations(db)
|
||||
simulation_runs = simulation_context["simulation_runs"]
|
||||
|
||||
runs_by_scenario = {run["scenario_id"]: run for run in simulation_runs}
|
||||
|
||||
def sum_amounts(
|
||||
grouped: Dict[int, list[Dict[str, Any]]], field: str = "amount"
|
||||
) -> float:
|
||||
total = 0.0
|
||||
for items in grouped.values():
|
||||
for item in items:
|
||||
value = item.get(field, 0.0)
|
||||
if isinstance(value, (int, float)):
|
||||
total += float(value)
|
||||
return total
|
||||
|
||||
total_capex = sum_amounts(capex_by_scenario)
|
||||
total_opex = sum_amounts(opex_by_scenario)
|
||||
total_consumption = sum_amounts(consumption_by_scenario)
|
||||
total_production = sum_amounts(production_by_scenario)
|
||||
total_maintenance_cost = sum_amounts(maintenance_by_scenario, field="cost")
|
||||
|
||||
total_parameters = sum(
|
||||
len(items) for items in parameters_by_scenario.values()
|
||||
)
|
||||
total_equipment = sum(
|
||||
len(items) for items in equipment_by_scenario.values()
|
||||
)
|
||||
total_maintenance_events = sum(
|
||||
len(items) for items in maintenance_by_scenario.values()
|
||||
)
|
||||
total_simulation_iterations = sum(
|
||||
run["iterations"] for run in simulation_runs
|
||||
)
|
||||
|
||||
scenario_rows: list[Dict[str, Any]] = []
|
||||
scenario_labels: list[str] = []
|
||||
scenario_capex: list[float] = []
|
||||
scenario_opex: list[float] = []
|
||||
activity_labels: list[str] = []
|
||||
activity_production: list[float] = []
|
||||
activity_consumption: list[float] = []
|
||||
|
||||
for scenario in scenarios:
|
||||
scenario_id = scenario["id"]
|
||||
scenario_name = scenario["name"]
|
||||
param_count = len(parameters_by_scenario.get(scenario_id, []))
|
||||
equipment_count = len(equipment_by_scenario.get(scenario_id, []))
|
||||
maintenance_count = len(maintenance_by_scenario.get(scenario_id, []))
|
||||
|
||||
capex_total = sum(
|
||||
float(item.get("amount", 0.0))
|
||||
for item in capex_by_scenario.get(scenario_id, [])
|
||||
)
|
||||
opex_total = sum(
|
||||
float(item.get("amount", 0.0))
|
||||
for item in opex_by_scenario.get(scenario_id, [])
|
||||
)
|
||||
consumption_total = sum(
|
||||
float(item.get("amount", 0.0))
|
||||
for item in consumption_by_scenario.get(scenario_id, [])
|
||||
)
|
||||
production_total = sum(
|
||||
float(item.get("amount", 0.0))
|
||||
for item in production_by_scenario.get(scenario_id, [])
|
||||
)
|
||||
|
||||
run = runs_by_scenario.get(scenario_id)
|
||||
summary = run["summary"] if run else generate_report([])
|
||||
iterations = run["iterations"] if run else 0
|
||||
mean_value = float(summary.get("mean", 0.0))
|
||||
|
||||
scenario_rows.append(
|
||||
{
|
||||
"scenario_name": scenario_name,
|
||||
"parameter_count": param_count,
|
||||
"parameter_display": _format_int(param_count),
|
||||
"equipment_count": equipment_count,
|
||||
"equipment_display": _format_int(equipment_count),
|
||||
"capex_total": capex_total,
|
||||
"capex_display": _format_currency(capex_total),
|
||||
"opex_total": opex_total,
|
||||
"opex_display": _format_currency(opex_total),
|
||||
"production_total": production_total,
|
||||
"production_display": _format_decimal(production_total),
|
||||
"consumption_total": consumption_total,
|
||||
"consumption_display": _format_decimal(consumption_total),
|
||||
"maintenance_count": maintenance_count,
|
||||
"maintenance_display": _format_int(maintenance_count),
|
||||
"iterations": iterations,
|
||||
"iterations_display": _format_int(iterations),
|
||||
"simulation_mean": mean_value,
|
||||
"simulation_mean_display": _format_decimal(mean_value),
|
||||
}
|
||||
)
|
||||
|
||||
scenario_labels.append(scenario_name)
|
||||
scenario_capex.append(capex_total)
|
||||
scenario_opex.append(opex_total)
|
||||
|
||||
activity_labels.append(scenario_name)
|
||||
activity_production.append(production_total)
|
||||
activity_consumption.append(consumption_total)
|
||||
|
||||
scenario_rows.sort(key=lambda row: row["scenario_name"].lower())
|
||||
|
||||
all_simulation_results = [
|
||||
{"result": float(getattr(item, "result", 0.0))}
|
||||
for item in db.query(SimulationResult).all()
|
||||
]
|
||||
overall_report = generate_report(all_simulation_results)
|
||||
|
||||
overall_report_metrics = [
|
||||
{
|
||||
"label": "Runs",
|
||||
"value": _format_int(int(overall_report.get("count", 0))),
|
||||
},
|
||||
{
|
||||
"label": "Mean",
|
||||
"value": _format_decimal(float(overall_report.get("mean", 0.0))),
|
||||
},
|
||||
{
|
||||
"label": "Median",
|
||||
"value": _format_decimal(float(overall_report.get("median", 0.0))),
|
||||
},
|
||||
{
|
||||
"label": "Std Dev",
|
||||
"value": _format_decimal(float(overall_report.get("std_dev", 0.0))),
|
||||
},
|
||||
{
|
||||
"label": "95th Percentile",
|
||||
"value": _format_decimal(
|
||||
float(overall_report.get("percentile_95", 0.0))
|
||||
),
|
||||
},
|
||||
{
|
||||
"label": "VaR (95%)",
|
||||
"value": _format_decimal(
|
||||
float(overall_report.get("value_at_risk_95", 0.0))
|
||||
),
|
||||
},
|
||||
{
|
||||
"label": "Expected Shortfall (95%)",
|
||||
"value": _format_decimal(
|
||||
float(overall_report.get("expected_shortfall_95", 0.0))
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
recent_simulations: list[Dict[str, Any]] = [
|
||||
{
|
||||
"scenario_name": run["scenario_name"],
|
||||
"iterations": run["iterations"],
|
||||
"iterations_display": _format_int(run["iterations"]),
|
||||
"mean_display": _format_decimal(
|
||||
float(run["summary"].get("mean", 0.0))
|
||||
),
|
||||
"p95_display": _format_decimal(
|
||||
float(run["summary"].get("percentile_95", 0.0))
|
||||
),
|
||||
}
|
||||
for run in simulation_runs
|
||||
if run["iterations"] > 0
|
||||
]
|
||||
recent_simulations.sort(key=lambda item: item["iterations"], reverse=True)
|
||||
recent_simulations = recent_simulations[:5]
|
||||
|
||||
upcoming_maintenance: list[Dict[str, Any]] = []
|
||||
for record in (
|
||||
db.query(Maintenance)
|
||||
.order_by(Maintenance.maintenance_date.asc())
|
||||
.limit(5)
|
||||
.all()
|
||||
):
|
||||
maintenance_date = getattr(record, "maintenance_date", None)
|
||||
upcoming_maintenance.append(
|
||||
{
|
||||
"scenario_name": getattr(
|
||||
getattr(record, "scenario", None), "name", "Unknown"
|
||||
),
|
||||
"equipment_name": getattr(
|
||||
getattr(record, "equipment", None), "name", "Unknown"
|
||||
),
|
||||
"date_display": (
|
||||
maintenance_date.strftime("%Y-%m-%d")
|
||||
if maintenance_date
|
||||
else "—"
|
||||
),
|
||||
"cost_display": _format_currency(
|
||||
float(getattr(record, "cost", 0.0))
|
||||
),
|
||||
"description": getattr(record, "description", "") or "—",
|
||||
}
|
||||
)
|
||||
|
||||
cost_chart_has_data = any(value > 0 for value in scenario_capex) or any(
|
||||
value > 0 for value in scenario_opex
|
||||
)
|
||||
activity_chart_has_data = any(
|
||||
value > 0 for value in activity_production
|
||||
) or any(value > 0 for value in activity_consumption)
|
||||
|
||||
scenario_cost_chart: Dict[str, list[Any]] = {
|
||||
"labels": scenario_labels,
|
||||
"capex": scenario_capex,
|
||||
"opex": scenario_opex,
|
||||
}
|
||||
scenario_activity_chart: Dict[str, list[Any]] = {
|
||||
"labels": activity_labels,
|
||||
"production": activity_production,
|
||||
"consumption": activity_consumption,
|
||||
}
|
||||
|
||||
summary_metrics = [
|
||||
{"label": "Active Scenarios", "value": _format_int(len(scenarios))},
|
||||
{"label": "Parameters", "value": _format_int(total_parameters)},
|
||||
{"label": "CAPEX Total", "value": _format_currency(total_capex)},
|
||||
{"label": "OPEX Total", "value": _format_currency(total_opex)},
|
||||
{"label": "Equipment Assets", "value": _format_int(total_equipment)},
|
||||
{
|
||||
"label": "Maintenance Events",
|
||||
"value": _format_int(total_maintenance_events),
|
||||
},
|
||||
{"label": "Consumption", "value": _format_decimal(total_consumption)},
|
||||
{"label": "Production", "value": _format_decimal(total_production)},
|
||||
{
|
||||
"label": "Simulation Iterations",
|
||||
"value": _format_int(total_simulation_iterations),
|
||||
},
|
||||
{
|
||||
"label": "Maintenance Cost",
|
||||
"value": _format_currency(total_maintenance_cost),
|
||||
},
|
||||
]
|
||||
|
||||
return {
|
||||
"summary_metrics": summary_metrics,
|
||||
"scenario_rows": scenario_rows,
|
||||
"overall_report_metrics": overall_report_metrics,
|
||||
"recent_simulations": recent_simulations,
|
||||
"upcoming_maintenance": upcoming_maintenance,
|
||||
"scenario_cost_chart": scenario_cost_chart,
|
||||
"scenario_activity_chart": scenario_activity_chart,
|
||||
"cost_chart_has_data": cost_chart_has_data,
|
||||
"activity_chart_has_data": activity_chart_has_data,
|
||||
"report_available": overall_report.get("count", 0) > 0,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/", response_class=HTMLResponse)
|
||||
async def dashboard_root(request: Request, db: Session = Depends(get_db)):
|
||||
"""Render the primary dashboard landing page."""
|
||||
return _render(request, "Dashboard.html", _load_dashboard(db))
|
||||
|
||||
|
||||
@router.get("/ui/dashboard", response_class=HTMLResponse)
|
||||
async def dashboard(request: Request, db: Session = Depends(get_db)):
|
||||
"""Render the legacy dashboard route for backward compatibility."""
|
||||
return _render(request, "Dashboard.html", _load_dashboard(db))
|
||||
|
||||
|
||||
@router.get("/ui/dashboard/data", response_class=JSONResponse)
|
||||
async def dashboard_data(db: Session = Depends(get_db)) -> JSONResponse:
|
||||
"""Expose dashboard aggregates as JSON for client-side refreshes."""
|
||||
return JSONResponse(_load_dashboard(db))
|
||||
|
||||
|
||||
@router.get("/ui/scenarios", response_class=HTMLResponse)
|
||||
async def scenario_form(request: Request, db: Session = Depends(get_db)):
|
||||
"""Render the scenario creation form."""
|
||||
context = _load_scenarios(db)
|
||||
return _render(request, "ScenarioForm.html", context)
|
||||
|
||||
|
||||
@router.get("/ui/parameters", response_class=HTMLResponse)
|
||||
async def parameter_form(request: Request, db: Session = Depends(get_db)):
|
||||
"""Render the parameter input form."""
|
||||
context: Dict[str, Any] = {}
|
||||
context.update(_load_scenarios(db))
|
||||
context.update(_load_parameters(db))
|
||||
return _render(request, "ParameterInput.html", context)
|
||||
|
||||
|
||||
@router.get("/ui/costs", response_class=HTMLResponse)
|
||||
async def costs_view(request: Request, db: Session = Depends(get_db)):
|
||||
"""Render the costs view with CAPEX and OPEX data."""
|
||||
context: Dict[str, Any] = {}
|
||||
context.update(_load_scenarios(db))
|
||||
context.update(_load_costs(db))
|
||||
context.update(_load_currencies(db))
|
||||
return _render(request, "costs.html", context)
|
||||
|
||||
|
||||
@router.get("/ui/consumption", response_class=HTMLResponse)
|
||||
async def consumption_view(request: Request, db: Session = Depends(get_db)):
|
||||
"""Render the consumption view with scenario consumption data."""
|
||||
context: Dict[str, Any] = {}
|
||||
context.update(_load_scenarios(db))
|
||||
context.update(_load_consumption(db))
|
||||
context["unit_options"] = MEASUREMENT_UNITS
|
||||
return _render(request, "consumption.html", context)
|
||||
|
||||
|
||||
@router.get("/ui/production", response_class=HTMLResponse)
|
||||
async def production_view(request: Request, db: Session = Depends(get_db)):
|
||||
"""Render the production view with scenario production data."""
|
||||
context: Dict[str, Any] = {}
|
||||
context.update(_load_scenarios(db))
|
||||
context.update(_load_production(db))
|
||||
context["unit_options"] = MEASUREMENT_UNITS
|
||||
return _render(request, "production.html", context)
|
||||
|
||||
|
||||
@router.get("/ui/equipment", response_class=HTMLResponse)
|
||||
async def equipment_view(request: Request, db: Session = Depends(get_db)):
|
||||
"""Render the equipment view with scenario equipment data."""
|
||||
context: Dict[str, Any] = {}
|
||||
context.update(_load_scenarios(db))
|
||||
context.update(_load_equipment(db))
|
||||
return _render(request, "equipment.html", context)
|
||||
|
||||
|
||||
@router.get("/ui/maintenance", response_class=HTMLResponse)
|
||||
async def maintenance_view(request: Request, db: Session = Depends(get_db)):
|
||||
"""Render the maintenance view with scenario maintenance data."""
|
||||
context: Dict[str, Any] = {}
|
||||
context.update(_load_scenarios(db))
|
||||
context.update(_load_equipment(db))
|
||||
context.update(_load_maintenance(db))
|
||||
return _render(request, "maintenance.html", context)
|
||||
|
||||
|
||||
@router.get("/ui/simulations", response_class=HTMLResponse)
|
||||
async def simulations_view(request: Request, db: Session = Depends(get_db)):
|
||||
"""Render the simulations view with scenario information and recent runs."""
|
||||
return _render(request, "simulations.html", _load_simulations(db))
|
||||
|
||||
|
||||
@router.get("/ui/reporting", response_class=HTMLResponse)
|
||||
async def reporting_view(request: Request, db: Session = Depends(get_db)):
|
||||
"""Render the reporting view with scenario KPI summaries."""
|
||||
return _render(request, "reporting.html", _load_reporting(db))
|
||||
|
||||
|
||||
@router.get("/ui/settings", response_class=HTMLResponse)
|
||||
async def settings_view(request: Request, db: Session = Depends(get_db)):
|
||||
"""Render the settings landing page."""
|
||||
context = _load_css_settings(db)
|
||||
return _render(request, "settings.html", context)
|
||||
|
||||
|
||||
@router.get("/ui/currencies", response_class=HTMLResponse)
|
||||
async def currencies_view(request: Request, db: Session = Depends(get_db)):
|
||||
"""Render the currency administration page with full currency context."""
|
||||
context = _load_currency_settings(db)
|
||||
return _render(request, "currencies.html", context)
|
||||
|
||||
|
||||
@router.get("/login", response_class=HTMLResponse)
|
||||
async def login_page(request: Request):
|
||||
return _render(request, "login.html")
|
||||
|
||||
|
||||
@router.get("/register", response_class=HTMLResponse)
|
||||
async def register_page(request: Request):
|
||||
return _render(request, "register.html")
|
||||
|
||||
|
||||
@router.get("/profile", response_class=HTMLResponse)
|
||||
async def profile_page(request: Request):
|
||||
return _render(request, "profile.html")
|
||||
|
||||
|
||||
@router.get("/forgot-password", response_class=HTMLResponse)
|
||||
async def forgot_password_page(request: Request):
|
||||
return _render(request, "forgot_password.html")
|
||||
|
||||
|
||||
@router.get("/theme-settings", response_class=HTMLResponse)
|
||||
async def theme_settings_page(request: Request, db: Session = Depends(get_db)):
|
||||
"""Render the theme settings page."""
|
||||
context = _load_css_settings(db)
|
||||
return _render(request, "theme_settings.html", context)
|
||||
107
routes/users.py
107
routes/users.py
@@ -1,107 +0,0 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from config.database import get_db
|
||||
from models.user import User
|
||||
from services.security import create_access_token, get_current_user
|
||||
from schemas.user import (
|
||||
PasswordReset,
|
||||
PasswordResetRequest,
|
||||
UserCreate,
|
||||
UserInDB,
|
||||
UserLogin,
|
||||
UserUpdate,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/users", tags=["users"])
|
||||
|
||||
|
||||
@router.post("/register", response_model=UserInDB, status_code=status.HTTP_201_CREATED)
|
||||
async def register_user(user: UserCreate, db: Session = Depends(get_db)):
|
||||
db_user = db.query(User).filter(User.username == user.username).first()
|
||||
if db_user:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Username already registered")
|
||||
db_user = db.query(User).filter(User.email == user.email).first()
|
||||
if db_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST, detail="Email already registered")
|
||||
|
||||
# Get or create default role
|
||||
from models.role import Role
|
||||
default_role = db.query(Role).filter(Role.name == "user").first()
|
||||
if not default_role:
|
||||
default_role = Role(name="user")
|
||||
db.add(default_role)
|
||||
db.commit()
|
||||
db.refresh(default_role)
|
||||
|
||||
new_user = User(username=user.username, email=user.email,
|
||||
role_id=default_role.id)
|
||||
new_user.set_password(user.password)
|
||||
db.add(new_user)
|
||||
db.commit()
|
||||
db.refresh(new_user)
|
||||
return new_user
|
||||
|
||||
|
||||
@router.post("/login")
|
||||
async def login_user(user: UserLogin, db: Session = Depends(get_db)):
|
||||
db_user = db.query(User).filter(User.username == user.username).first()
|
||||
if not db_user or not db_user.check_password(user.password):
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect username or password")
|
||||
access_token = create_access_token(subject=db_user.username)
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
||||
|
||||
|
||||
@router.get("/me")
|
||||
async def read_users_me(current_user: User = Depends(get_current_user)):
|
||||
return current_user
|
||||
|
||||
|
||||
@router.put("/me", response_model=UserInDB)
|
||||
async def update_user_me(user_update: UserUpdate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
|
||||
if user_update.username and user_update.username != current_user.username:
|
||||
existing_user = db.query(User).filter(
|
||||
User.username == user_update.username).first()
|
||||
if existing_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST, detail="Username already taken")
|
||||
setattr(current_user, "username", user_update.username)
|
||||
|
||||
if user_update.email and user_update.email != current_user.email:
|
||||
existing_user = db.query(User).filter(
|
||||
User.email == user_update.email).first()
|
||||
if existing_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST, detail="Email already registered")
|
||||
setattr(current_user, "email", user_update.email)
|
||||
|
||||
if user_update.password:
|
||||
current_user.set_password(user_update.password)
|
||||
|
||||
db.add(current_user)
|
||||
db.commit()
|
||||
db.refresh(current_user)
|
||||
return current_user
|
||||
|
||||
|
||||
@router.post("/forgot-password")
|
||||
async def forgot_password(request: PasswordResetRequest):
|
||||
# In a real application, this would send an email with a reset token
|
||||
return {"message": "Password reset email sent (not really)"}
|
||||
|
||||
|
||||
@router.post("/reset-password")
|
||||
async def reset_password(request: PasswordReset, db: Session = Depends(get_db)):
|
||||
# In a real application, the token would be verified
|
||||
user = db.query(User).filter(User.username ==
|
||||
request.token).first() # Use token as username for test
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid token or user")
|
||||
user.set_password(request.new_password)
|
||||
db.add(user)
|
||||
db.commit()
|
||||
return {"message": "Password has been reset successfully"}
|
||||
Reference in New Issue
Block a user