"""Раздел «Анализ надёжности» (инженеры и руководители; включается в настройках сервера).""" from datetime import datetime, timedelta from typing import Literal, Optional from fastapi import APIRouter, Depends, HTTPException, Query from fastapi.responses import Response from sqlalchemy import select from sqlalchemy.orm import Session from .. import config, reliability as R, schemas as S, scope, settings_store as ss from ..db import get_db from ..models import Equipment, User from ..security import require_engineer router = APIRouter(prefix="/reliability", tags=["reliability"]) def enabled(db: Session) -> bool: return ss.get(db, "reliability") != "0" def guard(db: Session = Depends(get_db), u: User = Depends(require_engineer)) -> User: if not enabled(db): raise HTTPException(404, "Анализ надёжности выключен в настройках сервера") return u def period(date_from: Optional[datetime], date_to: Optional[datetime]) -> tuple[datetime, datetime]: end = S._dt_in(date_to) if date_to else config.now() start = S._dt_in(date_from) if date_from else end - timedelta(days=365) if start >= end: raise HTTPException(422, "Начало периода должно быть раньше конца") return start, end def machines(db: Session, u: User, department_id: Optional[int], cranes: Optional[bool], equipment_id: Optional[int]) -> set[int]: """Машины, которые видит пользователь (отделы), с фильтрами отчёта.""" stmt = select(Equipment.id) cond = scope.equipment_filter(db, u) if cond is not None: stmt = stmt.where(cond) if department_id: stmt = stmt.where(Equipment.department_id.in_(scope.dept_tree(db, department_id))) if cranes is not None: stmt = stmt.where(Equipment.is_crane.is_(cranes)) if equipment_id: stmt = stmt.where(Equipment.id == equipment_id) return set(db.scalars(stmt)) @router.get("/dictionaries") def dictionaries(_: User = Depends(guard)): """Справочники формы закрытия отказа: причины, характер, что сделано, признаки заранее.""" return {"cause": R.CAUSE_RU, "nature": R.NATURE_RU, "action": R.ACTION_RU, "precursor": R.PRECURSOR_RU} @router.get("") def summary(date_from: Optional[datetime] = Query(None, alias="from"), date_to: Optional[datetime] = Query(None, alias="to"), department_id: Optional[int] = None, cranes: Optional[bool] = None, equipment_id: Optional[int] = None, confidence: float = Query(0.9, ge=0.5, le=0.99), horizon_days: int = Query(30, ge=1, le=365), db: Session = Depends(get_db), u: User = Depends(guard)): """Показатели надёжности по машинам и парку за период: λ, θ, оценки по малоотказному опыту, Tв, Kг, прогноз, причины.""" start, end = period(date_from, date_to) return R.analyze(db, start, end, machines(db, u, department_id, cranes, equipment_id), confidence, horizon_days) @router.get("/drift") def drift(days: int = Query(180, ge=14, le=1095), horizon: int = Query(90, ge=7, le=730), department_id: Optional[int] = None, cranes: Optional[bool] = None, equipment_id: Optional[int] = None, db: Session = Depends(get_db), u: User = Depends(guard)): """«Параметр — граница работоспособности»: дрейф числовых пунктов чек-листов к границе нормы и выходы за неё.""" return R.drift(db, machines(db, u, department_id, cranes, equipment_id), days, horizon) @router.get("/export") def export(kind: Literal["failures", "service"] = "failures", date_from: Optional[datetime] = Query(None, alias="from"), date_to: Optional[datetime] = Query(None, alias="to"), department_id: Optional[int] = None, cranes: Optional[bool] = None, equipment_id: Optional[int] = None, db: Session = Depends(get_db), u: User = Depends(guard)): """CSV для расчётов: failures — отказы и неисправности (строка на запись); service — история обслуживания узлов.""" start, end = period(date_from, date_to) ids = machines(db, u, department_id, cranes, equipment_id) text = (R.export_failures if kind == "failures" else R.export_service_events)(db, start, end, ids) name = f"{'failures' if kind == 'failures' else 'service_events'}_{start:%Y%m%d}_{end:%Y%m%d}.csv" return Response(text, media_type="text/csv; charset=utf-8", headers={"Content-Disposition": f'attachment; filename="{name}"'})