Compare commits
54 Commits
main
...
oh_securit
| Author | SHA1 | Date |
|---|---|---|
|
|
9322a89822 | 2 weeks ago |
|
|
2ba2c5750a | 4 weeks ago |
|
|
e67feed201 | 4 weeks ago |
|
|
044da9735a | 4 weeks ago |
|
|
0ecae96664 | 4 weeks ago |
|
|
95ae35c0fe | 1 month ago |
|
|
98b50b6532 | 1 month ago |
|
|
1029cd5ca9 | 1 month ago |
|
|
20ea1b580a | 1 month ago |
|
|
19066475c9 | 1 month ago |
|
|
d2ecf0849e | 1 month ago |
|
|
130f8acf8f | 2 months ago |
|
|
67eae7a881 | 2 months ago |
|
|
5d3562e05b | 2 months ago |
|
|
091bab1714 | 2 months ago |
|
|
54fe0e1c12 | 2 months ago |
|
|
2dfa783a58 | 2 months ago |
|
|
9ccca3b5a5 | 2 months ago |
|
|
1e78a1d974 | 2 months ago |
|
|
5049310547 | 2 months ago |
|
|
6fc5cd908f | 2 months ago |
|
|
64752e6c42 | 2 months ago |
|
|
e291959508 | 2 months ago |
|
|
2919660504 | 2 months ago |
|
|
600030199d | 3 months ago |
|
|
d4175f75d0 | 3 months ago |
|
|
2615ff9a6e | 3 months ago |
|
|
2a57ac8bec | 4 months ago |
|
|
f74f6b6b4c | 4 months ago |
|
|
89662b535b | 4 months ago |
|
|
707cd851d6 | 4 months ago |
|
|
543bc84f70 | 4 months ago |
|
|
c5f7384b2d | 4 months ago |
|
|
a728d39fce | 4 months ago |
|
|
6bab027427 | 4 months ago |
|
|
47074423e1 | 4 months ago |
|
|
ca8006efd2 | 4 months ago |
|
|
38d898a028 | 4 months ago |
|
|
7377123f6c | 4 months ago |
|
|
083fcf9911 | 4 months ago |
|
|
82dd96652b | 4 months ago |
|
|
76aee62fa7 | 4 months ago |
|
|
914eacb16a | 4 months ago |
|
|
71c62b6b3c | 4 months ago |
|
|
af13a22104 | 4 months ago |
|
|
21baabab65 | 4 months ago |
|
|
308bdf1839 | 4 months ago |
|
|
895c50eebb | 5 months ago |
|
|
35ff567473 | 5 months ago |
|
|
72658ead62 | 5 months ago |
|
|
806aa79b16 | 5 months ago |
|
|
7ea00a0f77 | 5 months ago |
|
|
7f9c484540 | 5 months ago |
|
|
a49718af02 | 5 months ago |
@ -0,0 +1,2 @@
|
||||
tests/
|
||||
.venv/
|
||||
@ -1,3 +1,4 @@
|
||||
env/
|
||||
__pycache__/
|
||||
venv/
|
||||
venv/
|
||||
tests/
|
||||
@ -1,208 +0,0 @@
|
||||
# DigitalTwin - Optimum Overhaul App (be-optimumoh) API Documentation
|
||||
|
||||
This document maps all the API endpoints, route handlers, database models, and external service layers inside the `be-optimumoh` microservice. It is a FastAPI-based backend engine managing Overhaul schedules, standard scopes, spare parts readiness, Temporal workflow executions, and optimization/constraint calculations.
|
||||
|
||||
---
|
||||
|
||||
## 📊 Summary of Services
|
||||
|
||||
| Service / Module | Purpose | Endpoints Count | Primary Database Tables |
|
||||
| :--- | :--- | :---: | :--- |
|
||||
| **Overhauls** (`overhauls`) | Core overhaul session status, critical parts, schedules, and systems overview. | 4 | `oh_ms_overhaul` |
|
||||
| **Standard Scope** (`scope-equipments`) | Fleet scopes configuration, master equipment availability, and historical overhaul logs. | 4 | `oh_ms_standard_scope`, `oh_ms_equipment_oh_history` |
|
||||
| **Overhaul Activities** (`overhaul-activity`) | Operations/activities scheduling and batch additions to active overhaul sessions. | 4 | `oh_tr_overhaul_activity` |
|
||||
| **Workscope Groups** (`workscopes`) | Maintenance activity groups, tasks, and overhaul scheduling. | 5 | `oh_ms_workscope_group`, `oh_ms_workscope_task` |
|
||||
| **Spare Parts** (`spareparts`) | Global inventory levels, procurement planning status, and planning remarks. | 2 | `oh_ms_sparepart`, `oh_ms_sparepart_remark` |
|
||||
| **Equipment Workscopes** (`equipment-workscopes`) | Equipment-specific maintenance work scope groups mapping and jobs tracking. | 3 | `oh_tr_equipment_workscope_group` |
|
||||
| **Equipment Spare Parts** (`equipment-spareparts`) | Material part catalog tracking per asset location tag. | 1 | `oh_ms_scope_equipment_part` |
|
||||
| **Monitoring Gantt** (`overhaul-gantt`) | Google Sheets Gantt performance charts extraction and sync triggers. | 3 | `oh_ms_monitoring_spreadsheet` |
|
||||
| **Overhaul Schedules** (`overhaul-schedules`) | Detailed calendar session creation, history tracking, updates, and deletes. | 6 | `oh_ms_overhaul` |
|
||||
| **Time Constraints** (`time-constraint`) | Scheduling simulations under a rigid time window restriction. | 9 | `oh_ms_calculation_param`, `oh_tr_calculation_result` |
|
||||
| **Optimized Time Constraints** | Simulations for ideal schedules under a variable optimal calendar logic. | 9 | `oh_tr_calculation_data`, `oh_tr_calculation_equipment_result` |
|
||||
| **Target Reliability** | Background RBD Monte Carlo simulations executed via Temporal workflows. | 2 | None (Leverages Temporal Client SDK) |
|
||||
| **Budget Constraints** | Financial threshold filtering and consequence calculation matrix. | 1 | `oh_tr_calculation_result` |
|
||||
|
||||
---
|
||||
|
||||
## 🛡️ App Middlewares & Framework Architecture
|
||||
|
||||
### 1. Security Headers Middleware
|
||||
FastAPI adds rigid HTTP security headers to all incoming connections inside `src/main.py`. In production, it enforces `Strict-Transport-Security`, `X-Frame-Options` (DENY), and full CSP blockades. In development mode, it supports relaxed parameters.
|
||||
|
||||
### 2. Request Sanitization & Slowapi Limiting
|
||||
All JSON and path structures pass through `RequestValidationMiddleware` (`slowapi` integration) to enforce rate limiting thresholds globally, returning standard payload error formatting on breaches.
|
||||
|
||||
### 3. Multi-Session Context Scoping
|
||||
Scoped database sessions are generated per request context dynamically utilizing ContextVars in `src/context.py` and SQLAlchemy's async connection pool engine:
|
||||
* `db` session: Optimum overhaul local PostgreSQL dataset (`default`).
|
||||
* `collector_db` session: Asset metadata and equipment configurations.
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Complete API Endpoints Catalog
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
UI[Frontend Client Application] -->|JWTBearer token| G[FastAPI Gateway Router]
|
||||
G -->|Overhaul Master| OH[Overhaul Service]
|
||||
G -->|Standard Scopes| SS[Standard Scope Service]
|
||||
G -->|Spare Parts| SP[Spare Parts Service]
|
||||
G -->|Gantt Tracking| GT[Gantt Chart Service]
|
||||
G -->|Simulations API| TC[Constraint Calculations Service]
|
||||
TC -->|Simulation Worker| TMP[Temporalio Cluster Engine]
|
||||
```
|
||||
|
||||
### 1. 📂 Overhauls Service (`overhauls`)
|
||||
Prefix: `/overhauls` — Exposes master overhaul dashboard summaries, schedules, critical parts status, and system maps.
|
||||
|
||||
| Service | Endpoint | Method | Type | DB Tables Touched | Called By | LOC | Protect? (Y/N) | Dependencies |
|
||||
| :--- | :--- | :---: | :--- | :--- | :--- | :---: | :---: | :--- |
|
||||
| **Overhauls** | `/overhauls` | `GET` | Algorithm | `oh_ms_overhaul` | Frontend | ~28 | **Y** | JWTBearer |
|
||||
| **Overhauls** | `/overhauls/schedules` | `GET` | CRUD | `oh_ms_overhaul` | Frontend | ~8 | **Y** | JWTBearer |
|
||||
| **Overhauls** | `/overhauls/critical-parts` | `GET` | CRUD | `oh_ms_sparepart`, `oh_ms_sparepart_procurement` | Frontend | ~8 | **Y** | JWTBearer |
|
||||
| **Overhauls** | `/overhauls/system-components` | `GET` | CRUD | None (Static mapping) | Frontend | ~10 | **Y** | JWTBearer |
|
||||
|
||||
---
|
||||
|
||||
### 2. 📋 Standard Scope Service (`scope-equipments`)
|
||||
Prefix: `/scope-equipments` — Manages fleet-wide equipment checklists and standard scopes configurations.
|
||||
|
||||
| Service | Endpoint | Method | Type | DB Tables Touched | Called By | LOC | Protect? (Y/N) | Dependencies |
|
||||
| :--- | :--- | :---: | :--- | :--- | :--- | :---: | :---: | :--- |
|
||||
| **Standard Scope** | `/scope-equipments` | `GET` | CRUD | `oh_ms_standard_scope` | Frontend | ~10 | **Y** | JWTBearer |
|
||||
| **Standard Scope** | `/scope-equipments/available/{scope_name}` | `GET` | CRUD | `ms_equipment_master`, `oh_ms_standard_scope` | Frontend | ~9 | **Y** | JWTBearer |
|
||||
| **Standard Scope** | `/scope-equipments` | `POST` | CRUD | `oh_ms_standard_scope` | Frontend | ~8 | **Y** | JWTBearer |
|
||||
| **Standard Scope** | `/scope-equipments/history/{oh_session_id}`| `GET` | CRUD | `oh_ms_equipment_oh_history`, `ms_equipment_master` | Frontend | ~10 | **Y** | JWTBearer |
|
||||
|
||||
---
|
||||
|
||||
### 3. 🏃♂️ Overhaul Activity Service (`overhaul-activity`)
|
||||
Prefix: `/overhaul-activity` — Controls tasks assigned to individual overhaul sessions.
|
||||
|
||||
| Service | Endpoint | Method | Type | DB Tables Touched | Called By | LOC | Protect? (Y/N) | Dependencies |
|
||||
| :--- | :--- | :---: | :--- | :--- | :--- | :---: | :---: | :--- |
|
||||
| **Overhaul Activity** | `/overhaul-activity/{overhaul_session}` | `GET` | CRUD | `oh_tr_overhaul_activity` | Frontend | ~25 | **Y** | JWTBearer |
|
||||
| **Overhaul Activity** | `/overhaul-activity/{overhaul_session_id}`| `POST` | CRUD | `oh_tr_overhaul_activity` | Frontend | ~15 | **Y** | JWTBearer |
|
||||
| **Overhaul Activity** | `/overhaul-activity/{overhaul_session}/{assetnum}`| `GET` | CRUD | `oh_tr_overhaul_activity` | Frontend | ~17 | **Y** | JWTBearer |
|
||||
| **Overhaul Activity** | `/overhaul-activity/delete/{overhaul_session}/{location_tag}`| `POST` | CRUD | `oh_tr_overhaul_activity` | Frontend | ~8 | **Y** | JWTBearer |
|
||||
|
||||
---
|
||||
|
||||
### 4. 🔨 Workscope Group Service (`workscopes`)
|
||||
Prefix: `/workscopes` — Defines tasks and maintenance groupings.
|
||||
|
||||
| Service | Endpoint | Method | Type | DB Tables Touched | Called By | LOC | Protect? (Y/N) | Dependencies |
|
||||
| :--- | :--- | :---: | :--- | :--- | :--- | :---: | :---: | :--- |
|
||||
| **Workscope Group** | `/workscopes` | `GET` | CRUD | `oh_ms_workscope_group` | Frontend | ~12 | **Y** | JWTBearer |
|
||||
| **Workscope Group** | `/workscopes` | `POST` | CRUD | `oh_ms_workscope_group` | Frontend | ~8 | **Y** | JWTBearer |
|
||||
| **Workscope Group** | `/workscopes/{scope_equipment_activity_id}`| `GET` | CRUD | `oh_ms_workscope_task` | Frontend | ~12 | **Y** | JWTBearer |
|
||||
| **Workscope Group** | `/workscopes/update/{scope_equipment_activity_id}`| `POST` | CRUD | `oh_ms_workscope_task` | Frontend | ~17 | **Y** | JWTBearer |
|
||||
| **Workscope Group** | `/workscopes/delete/{scope_equipment_activity_id}`| `POST` | CRUD | `oh_ms_workscope_task` | Frontend | ~14 | **Y** | JWTBearer |
|
||||
|
||||
---
|
||||
|
||||
### 5. 📦 Spare Parts Service (`spareparts`)
|
||||
Prefix: `/spareparts` — Manages part catalogs and remark notations.
|
||||
|
||||
| Service | Endpoint | Method | Type | DB Tables Touched | Called By | LOC | Protect? (Y/N) | Dependencies |
|
||||
| :--- | :--- | :---: | :--- | :--- | :--- | :---: | :---: | :--- |
|
||||
| **Spare Parts** | `/spareparts` | `GET` | CRUD | `oh_ms_sparepart`, `oh_ms_sparepart_procurement` | Frontend | ~12 | **Y** | JWTBearer |
|
||||
| **Spare Parts** | `/spareparts` | `POST` | CRUD | `oh_ms_sparepart_remark` | Frontend | ~8 | **Y** | JWTBearer |
|
||||
|
||||
---
|
||||
|
||||
### 6. 🔌 Equipment Workscopes Service (`equipment-workscopes`)
|
||||
Prefix: `/equipment-workscopes` — Maps equipment directly to defined work scopes.
|
||||
|
||||
| Service | Endpoint | Method | Type | DB Tables Touched | Called By | LOC | Protect? (Y/N) | Dependencies |
|
||||
| :--- | :--- | :---: | :--- | :--- | :--- | :---: | :---: | :--- |
|
||||
| **Equipment Workscopes**| `/equipment-workscopes/{location_tag}`| `GET` | CRUD | `oh_tr_equipment_workscope_group` | Frontend | ~12 | **Y** | JWTBearer |
|
||||
| **Equipment Workscopes**| `/equipment-workscopes/{assetnum}` | `POST` | CRUD | `oh_tr_equipment_workscope_group` | Frontend | ~11 | **Y** | JWTBearer |
|
||||
| **Equipment Workscopes**| `/equipment-workscopes/delete/{scope_job_id}`| `POST` | CRUD | `oh_tr_equipment_workscope_group` | Frontend | ~9 | **Y** | JWTBearer |
|
||||
|
||||
---
|
||||
|
||||
### 7. 🔩 Equipment Spare Parts Service (`equipment-spareparts`)
|
||||
Prefix: `/equipment-spareparts` — Lists active spare parts mapping per equipment tag.
|
||||
|
||||
| Service | Endpoint | Method | Type | DB Tables Touched | Called By | LOC | Protect? (Y/N) | Dependencies |
|
||||
| :--- | :--- | :---: | :--- | :--- | :--- | :---: | :---: | :--- |
|
||||
| **Equipment Spareparts**| `/equipment-spareparts/{location_tag}`| `GET` | CRUD | `oh_ms_scope_equipment_part` | Frontend | ~10 | **Y** | JWTBearer |
|
||||
|
||||
---
|
||||
|
||||
### 8. 📊 Overhaul Gantt Chart Service (`overhaul-gantt`)
|
||||
Prefix: `/overhaul-gantt` — Coordinates Google Sheets sync actions to build overhaul performance monitoring dashboards.
|
||||
|
||||
| Service | Endpoint | Method | Type | DB Tables Touched | Called By | LOC | Protect? (Y/N) | Dependencies |
|
||||
| :--- | :--- | :---: | :--- | :--- | :--- | :---: | :---: | :--- |
|
||||
| **Overhaul Gantt** | `/overhaul-gantt` | `GET` | Integration | `oh_ms_monitoring_spreadsheet` | Frontend | ~19 | **Y** | Google Sheets APIs, cell parsers |
|
||||
| **Overhaul Gantt** | `/overhaul-gantt/spreadsheet`| `GET` | CRUD | `oh_ms_monitoring_spreadsheet` | Frontend | ~22 | **Y** | JWTBearer |
|
||||
| **Overhaul Gantt** | `/overhaul-gantt/spreadsheet`| `POST` | CRUD | `oh_ms_monitoring_spreadsheet` | Frontend | ~36 | **Y** | Google Sheets URL validator |
|
||||
|
||||
---
|
||||
|
||||
### 9. 📅 Overhaul Session Schedules Service (`overhaul-schedules`)
|
||||
Prefix: `/overhaul-schedules` — Controls scheduler calendar timelines.
|
||||
|
||||
| Service | Endpoint | Method | Type | DB Tables Touched | Called By | LOC | Protect? (Y/N) | Dependencies |
|
||||
| :--- | :--- | :---: | :--- | :--- | :--- | :---: | :---: | :--- |
|
||||
| **Schedules** | `/overhaul-schedules` | `GET` | CRUD | `oh_ms_overhaul` | Frontend | ~10 | **Y** | JWTBearer |
|
||||
| **Schedules** | `/overhaul-schedules/history`| `GET` | CRUD | `oh_ms_overhaul` | Frontend | ~4 | **Y** | JWTBearer |
|
||||
| **Schedules** | `/overhaul-schedules/{overhaul_session_id}`| `GET` | CRUD | `oh_ms_overhaul` | Frontend | ~10 | **Y** | JWTBearer |
|
||||
| **Schedules** | `/overhaul-schedules` | `POST` | CRUD | `oh_ms_overhaul` | Frontend | ~5 | **Y** | JWTBearer |
|
||||
| **Schedules** | `/overhaul-schedules/update/{scope_id}`| `POST` | CRUD | `oh_ms_overhaul` | Frontend | ~12 | **Y** | JWTBearer |
|
||||
| **Schedules** | `/overhaul-schedules/delete/{scope_id}`| `POST` | CRUD | `oh_ms_overhaul` | Frontend | ~13 | **Y** | JWTBearer |
|
||||
|
||||
---
|
||||
|
||||
### 10. ⏱️ Time Constraints Calculation Service (`time-constraint`)
|
||||
Prefix: `/calculation/time-constraint` — Simulates failure counts and risks under strict overhaul downtime constraints.
|
||||
|
||||
| Service | Endpoint | Method | Type | DB Tables Touched | Called By | LOC | Protect? (Y/N) | Dependencies |
|
||||
| :--- | :--- | :---: | :--- | :--- | :--- | :---: | :---: | :--- |
|
||||
| **Time Constraints** | `/calculation/time-constraint` | `POST` | Algorithm | `oh_ms_calculation_param` | Frontend | ~36 | **Y** | RBD risk simulation logic |
|
||||
| **Time Constraints** | `/calculation/time-constraint` | `GET` | CRUD | `oh_tr_calculation_data` | Frontend | ~18 | **Y** | JWTBearer |
|
||||
| **Time Constraints** | `/calculation/time-constraint/parameters`| `GET` | CRUD | `oh_ms_calculation_param` | Frontend | ~13 | **Y** | JWTBearer |
|
||||
| **Time Constraints** | `/calculation/time-constraint/{calculation_id}`| `GET` | Algorithm | `oh_tr_calculation_result` | Frontend | ~18 | **N** | Internal Auth Key (Unprotected endpoint) |
|
||||
| **Time Constraints** | `/calculation/time-constraint/{calculation_id}/{assetnum}`| `GET` | CRUD | `oh_tr_calculation_equipment_result` | Frontend | ~9 | **Y** | JWTBearer |
|
||||
| **Time Constraints** | `/calculation/time-constraint/{calculation_id}/simulation`| `POST` | Algorithm | `oh_tr_calculation_result` | Frontend | ~13 | **Y** | Temporal timelines calculator |
|
||||
| **Time Constraints** | `/calculation/time-constraint/update/{calculation_id}`| `POST` | CRUD | `oh_tr_calculation_equipment_result` | Frontend | ~18 | **Y** | Bulk parameters updates |
|
||||
| **Time Constraints** | `/calculation/time-constraint/{calculation_id}/refresh-spareparts`| `POST` | Integration | `oh_ms_sparepart`, `oh_ms_calculation_param` | Frontend | ~15 | **Y** | Inventory sync parameters |
|
||||
| **Time Constraints** | `/calculation/time-constraint/{calculation_id}/recalculate`| `POST` | Algorithm / Int | `oh_tr_calculation_result` | Frontend | ~17 | **Y** | Temporal workflows trigger |
|
||||
|
||||
---
|
||||
|
||||
### 11. ⚙️ Optimized Time Constraints Service (`optimum-time-constraint`)
|
||||
Prefix: `/calculation/optimum-time-constraint` — Solves for optimized downtime intervals.
|
||||
|
||||
| Service | Endpoint | Method | Type | DB Tables Touched | Called By | LOC | Protect? (Y/N) | Dependencies |
|
||||
| :--- | :--- | :---: | :--- | :--- | :--- | :---: | :---: | :--- |
|
||||
| **Optimum Constraints** | `/calculation/optimum-time-constraint` | `POST` | Algorithm | `oh_ms_calculation_param` | Frontend | ~29 | **Y** | Optimum solver iterations |
|
||||
| **Optimum Constraints** | `/calculation/optimum-time-constraint` | `GET` | CRUD | `oh_tr_calculation_data` | Frontend | ~15 | **Y** | JWTBearer |
|
||||
| **Optimum Constraints** | `/calculation/optimum-time-constraint/parameters`| `GET` | CRUD | `oh_ms_calculation_param` | Frontend | ~13 | **Y** | JWTBearer |
|
||||
| **Optimum Constraints** | `/calculation/optimum-time-constraint/{calculation_id}`| `GET` | Algorithm | `oh_tr_calculation_result` | Frontend | ~19 | **N** | Internal Auth Key (Unprotected endpoint) |
|
||||
| **Optimum Constraints** | `/calculation/optimum-time-constraint/{calculation_id}/{assetnum}`| `GET` | CRUD | `oh_tr_calculation_equipment_result` | Frontend | ~8 | **Y** | JWTBearer |
|
||||
| **Optimum Constraints** | `/calculation/optimum-time-constraint/{calculation_id}/simulation`| `POST` | Algorithm | `oh_tr_calculation_result` | Frontend | ~13 | **Y** | Optimization simulation matrix |
|
||||
| **Optimum Constraints** | `/calculation/optimum-time-constraint/update/{calculation_id}`| `POST` | CRUD | `oh_tr_calculation_equipment_result` | Frontend | ~18 | **Y** | JWTBearer |
|
||||
| **Optimum Constraints** | `/calculation/optimum-time-constraint/{calculation_id}/refresh-spareparts`| `POST` | Integration | `oh_ms_sparepart` | Frontend | ~15 | **Y** | Inventory sync parameters |
|
||||
| **Optimum Constraints** | `/calculation/optimum-time-constraint/{calculation_id}/recalculate`| `POST` | Algorithm / Int | `oh_tr_calculation_result` | Frontend | ~17 | **Y** | Temporal workflows trigger |
|
||||
|
||||
---
|
||||
|
||||
### 12. ⚡ Target Reliability Simulation Service (`target-reliability`)
|
||||
Prefix: `/calculation/target-reliability` — Leverages Temporal client triggers to dispatch heavy RBD Monte Carlo workflows.
|
||||
|
||||
| Service | Endpoint | Method | Type | DB Tables Touched | Called By | LOC | Protect? (Y/N) | Dependencies |
|
||||
| :--- | :--- | :---: | :--- | :--- | :--- | :---: | :---: | :--- |
|
||||
| **Target Reliability** | `/calculation/target-reliability/simulate`| `POST` | Integration | None (Triggers Temporal workflow) | Frontend | ~18 | **Y** | Temporal IO client runner |
|
||||
| **Target Reliability** | `/calculation/target-reliability` | `GET` | Algorithm | `oh_ms_overhaul` | Frontend | ~88 | **Y** | Temporal client status tracking, EAF math |
|
||||
|
||||
---
|
||||
|
||||
### 13. 💰 Budget Constraints Service (`budget-constraint`)
|
||||
Prefix: `/calculation/budget-constraint` — Computes maintenance strategies according to cost limit thresholds.
|
||||
|
||||
| Service | Endpoint | Method | Type | DB Tables Touched | Called By | LOC | Protect? (Y/N) | Dependencies |
|
||||
| :--- | :--- | :---: | :--- | :--- | :--- | :---: | :---: | :--- |
|
||||
| **Budget Constraints** | `/calculation/budget-constraint/{session_id}`| `GET` | Algorithm | `oh_ms_overhaul`, `oh_tr_calculation_result` | Frontend | ~25 | **Y** | Risk-cost matrix calculator |
|
||||
@ -1,41 +0,0 @@
|
||||
# Equipment Level Overhaul Optimization
|
||||
|
||||
This document explains the mathematical theory and implementation used to determine the optimal overhaul interval for individual assets.
|
||||
|
||||
## 1. Theoretical Foundation
|
||||
The optimization model is based on the **Expected Total Cost per Unit Time (CPUT)** theory, a standard approach in reliability engineering for age-replacement and block-replacement policies.
|
||||
|
||||
### Objective Function
|
||||
The goal is to find an overhaul interval ($T$) that minimizes the total expected cost amortized over time:
|
||||
|
||||
$$C(T) = \frac{C_{Preventive} + C_{Corrective} \cdot E[N(T)]}{T}$$
|
||||
|
||||
Where:
|
||||
* **$T$**: The overhaul interval (e.g., month 12, 24, etc.).
|
||||
* **$C_{Preventive}$**: The cost of a planned overhaul (materials, labor, and procurement).
|
||||
* **$C_{Corrective}$**: The cost of an unplanned failure (repairs + risk cost of downtime).
|
||||
* **$E[N(T)]$**: The expected number of failures occurring in the interval $(0, T]$. This is derived from the **NHPP (Non-Homogeneous Poisson Process)** reliability model.
|
||||
|
||||
## 2. The Cost Balance (The "U-Curve")
|
||||
Optimization works by balancing two competing costs:
|
||||
1. **Overhaul Cost ($C_{PM}/T$)**: As the interval $T$ increases, the amortized cost of the overhaul decreases (economy of waiting).
|
||||
2. **Failure Risk ($C_{CM} \cdot E[N(T)]/T$)**: As the interval $T$ increases, the probability and expected frequency of failure increase (cost of wear-out).
|
||||
|
||||
The point where these two lines intersect typically represents the **Global Minimum** of the total cost curve, known as the **Optimum Overhaul Month**.
|
||||
|
||||
## 3. Implementation Logic
|
||||
In the `service.py` engine, the search is performed as follows:
|
||||
|
||||
1. **Failure Projection**: The system fetches reliability prediction data (cumulative failures) for the analysis window.
|
||||
2. **Sparepart Simulation**: For every potential month $T$, the system simulates the procurement process to calculate the real $C_{Preventive}$ (including any shortage penalties).
|
||||
3. **Cost Amortization**:
|
||||
```python
|
||||
total_cycle_cost = total_expected_failure_cost + total_preventive_cost
|
||||
cput = total_cycle_cost / month_index
|
||||
```
|
||||
4. **Grid Search**: The system iterates through all months in the analysis window and identifies the index with the lowest `cput` value.
|
||||
|
||||
## 4. Interpretation in UI
|
||||
* **Analysis Window Card**: Shows the CPUT value at your currently selected month.
|
||||
* **Optimum Target Card**: Shows the month $T$ where the curve is at its lowest point.
|
||||
* **Potential Benefit**: Calculated as $CPUT(current) - CPUT(optimum)$. This represents the real monthly cash-flow saving if the maintenance interval is adjusted.
|
||||
@ -1,47 +0,0 @@
|
||||
# Plant Level Overhaul Optimization
|
||||
|
||||
This document explains how individual asset optimizations are aggregated to find the best economic strategy for the entire fleet or plant.
|
||||
|
||||
## 1. Fleet Aggregation Theory
|
||||
At the plant level, the objective is to minimize the **Total Fleet Cost per Unit Time**. This assumes a **Uniform Interval Policy** or a **Synchronized Overhaul Strategy** where the plant looks for a common maintenance rhythm.
|
||||
|
||||
### Plant Objective Function
|
||||
The plant-level cost function is the summation of individual equipment cost functions ($C_i$):
|
||||
|
||||
$$C_{Plant}(T) = \sum_{i=1}^{N} C_i(T) = \frac{\sum_{i=1}^{N} (C_{p,i} + C_{f,i} \cdot E[N_i(T)])}{T}$$
|
||||
|
||||
Where:
|
||||
* **$N$**: Total number of equipments in the scope.
|
||||
* **$C_{p,i}$**: Preventive cost for equipment $i$.
|
||||
* **$C_{f,i}$**: Failure cost for equipment $i$.
|
||||
* **$E[N_i(T)]$**: Expected failures for equipment $i$ until time $T$.
|
||||
|
||||
## 2. Searching for the Fleet Optimum
|
||||
The "actual theory" used by the engine involves a two-phase search:
|
||||
|
||||
### Phase 1: Unconstrained Summation
|
||||
The system calculates the CPUT curve for every piece of equipment independently. It then sums these curves to create a "Fleet U-Curve." The minimum of this sum represents the **Theoretical Fleet Optimum**.
|
||||
|
||||
### Phase 2: Sparepart Interaction & Constraints
|
||||
Unlike a simple sum, the real plant optimum must account for **shared resources** (e.g., a limited budget or limited spare parts).
|
||||
1. **Sparepart Conflicts**: If multiple equipments reach their optimal interval at the same time, the `SparepartManager` checks if there are enough parts in the warehouse.
|
||||
2. **Constraint Penalty**: If parts are missing, a "Procurement Penalty" is added to the $C_{p,i}$ for that specific month, effectively shifting the "U-curve" to the right (delaying) or left (earlier) depending on availability.
|
||||
3. **Final Selection**: The system chooses the Month $T$ that minimizes the *constrained* total fleet cost.
|
||||
|
||||
## 3. Implementation in `service.py`
|
||||
The code uses `numpy` to perform vector addition of the cost curves:
|
||||
|
||||
```python
|
||||
# Aggregate amortized costs for fleet analysis
|
||||
total_corrective_costs += np.array(corrective_costs)
|
||||
total_preventive_costs += np.array(preventive_costs)
|
||||
total_costs += np.array(total_costs_equipment)
|
||||
|
||||
# Find the month T that minimizes the sum
|
||||
fleet_optimal_index = np.argmin(total_costs)
|
||||
```
|
||||
|
||||
## 4. Key Metrics for Decision Makers
|
||||
* **Fleet CPUT**: The average monthly budget required to maintain the plant at the chosen interval.
|
||||
* **Accumulation Control**: By using amortized costs (Rp/Month) instead of total cumulative costs, the plant chart remains stable and allows for direct comparison between intervals regardless of the number of assets.
|
||||
* **Risk vs. Cost**: The plant chart shows the trade-off between the *Fleet Failure Risk* (increasing line) and *Fixed Overhaul Costs* (decreasing line).
|
||||
@ -1,26 +0,0 @@
|
||||
import asyncio
|
||||
from src.database.core import engine
|
||||
from sqlalchemy import text
|
||||
|
||||
async def alter_table():
|
||||
async with engine.begin() as conn:
|
||||
try:
|
||||
await conn.execute(text("ALTER TABLE oh_tr_calculation_data ADD COLUMN plant_results JSONB"))
|
||||
print("Added plant_results")
|
||||
except Exception as e:
|
||||
print(f"plant_results exists: {e}")
|
||||
|
||||
try:
|
||||
await conn.execute(text("ALTER TABLE oh_tr_calculation_data ADD COLUMN fleet_statistics JSONB"))
|
||||
print("Added fleet_statistics")
|
||||
except Exception as e:
|
||||
print(f"fleet_statistics exists: {e}")
|
||||
|
||||
try:
|
||||
await conn.execute(text("ALTER TABLE oh_tr_calculation_data ADD COLUMN analysis_metadata JSONB"))
|
||||
print("Added analysis_metadata")
|
||||
except Exception as e:
|
||||
print(f"analysis_metadata exists: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(alter_table())
|
||||
@ -1,155 +0,0 @@
|
||||
|
||||
import sys
|
||||
|
||||
with open('/home/atra/Development/be-optimumoh/src/calculation_time_constrains/service.py', 'r') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
# Find the point where it broke
|
||||
# Line 159 is ' \'month_index\': i + 1,\n'
|
||||
# We want to keep up to line 159 (index 158)
|
||||
|
||||
new_lines = lines[:159]
|
||||
new_lines.append(" 'source': source\n")
|
||||
new_lines.append(" }\n")
|
||||
new_lines.append(" \n")
|
||||
new_lines.append(" return monthly_data\n")
|
||||
new_lines.append("\n")
|
||||
new_lines.append(" async def get_simulation_results(self, simulation_id: str = \"default\"):\n")
|
||||
new_lines.append(" \"\"\"Get simulation results for Birnbaum importance calculations\"\"\"\n")
|
||||
new_lines.append(" headers = {\n")
|
||||
new_lines.append(" \"Authorization\": f\"Bearer {self.token}\",\n")
|
||||
new_lines.append(" \"Content-Type\": \"application/json\"\n")
|
||||
new_lines.append(" }\n")
|
||||
new_lines.append("\n")
|
||||
new_lines.append(" calc_result_url = f\"{self.api_base_url}/aeros/simulation/result/calc/{simulation_id}?nodetype=RegularNode\"\n")
|
||||
new_lines.append(" plant_result_url = f\"{self.api_base_url}/aeros/simulation/result/calc/{simulation_id}/plant\"\n")
|
||||
new_lines.append("\n")
|
||||
new_lines.append(" async with httpx.AsyncClient(timeout=300.0) as client:\n")
|
||||
new_lines.append(" calc_task = client.get(calc_result_url, headers=headers)\n")
|
||||
new_lines.append(" plant_task = client.get(plant_result_url, headers=headers)\n")
|
||||
new_lines.append("\n")
|
||||
new_lines.append(" calc_response, plant_response = await asyncio.gather(calc_task, plant_task)\n")
|
||||
new_lines.append("\n")
|
||||
new_lines.append(" calc_response.raise_for_status()\n")
|
||||
new_lines.append(" plant_response.raise_for_status()\n")
|
||||
new_lines.append("\n")
|
||||
new_lines.append(" calc_data = calc_response.json()[\"data\"]\n")
|
||||
new_lines.append(" plant_data = plant_response.json()[\"data\"]\n")
|
||||
new_lines.append("\n")
|
||||
new_lines.append(" return {\n")
|
||||
new_lines.append(" \"calc_result\": calc_data,\n")
|
||||
new_lines.append(" \"plant_result\": plant_data\n")
|
||||
new_lines.append(" }\n")
|
||||
new_lines.append("\n")
|
||||
new_lines.append(" def _calculate_equipment_costs_with_spareparts(self, failures_prediction: Dict, birnbaum_importance: float,\n")
|
||||
new_lines.append(" preventive_cost: float, failure_replacement_cost: float, ecs,\n")
|
||||
new_lines.append(" location_tag: str, planned_overhauls: List = None, loss_production_permonth=0) -> List[Dict]:\n")
|
||||
new_lines.append(" \"\"\"Calculate costs for each month including sparepart costs and availability\"\"\"\n")
|
||||
new_lines.append(" \n")
|
||||
new_lines.append(" if not failures_prediction:\n")
|
||||
new_lines.append(" self.logger.warning(f\"No failure prediction data for {location_tag}\")\n")
|
||||
new_lines.append(" return []\n")
|
||||
new_lines.append("\n")
|
||||
new_lines.append(" results = []\n")
|
||||
new_lines.append(" months = list(failures_prediction.keys())\n")
|
||||
new_lines.append(" num_months = len(months)\n")
|
||||
new_lines.append(" failure_counts = []\n")
|
||||
new_lines.append(" \n")
|
||||
new_lines.append(" monthly_risk_cost_per_failure = 0\n")
|
||||
new_lines.append(" \n")
|
||||
new_lines.append(" if ecs:\n")
|
||||
new_lines.append(" is_trip = 1 if ecs.get(\"Diskripsi Operasional Akibat Equip. Failure\") == \"Trip\" else 0\n")
|
||||
new_lines.append(" is_series = 0 if not birnbaum_importance else math.floor(birnbaum_importance)\n")
|
||||
new_lines.append(" if is_trip:\n")
|
||||
new_lines.append(" downtime = ecs.get(\"Estimasi Waktu Maint. / Downtime / Gangguan (Jam)\")\n")
|
||||
new_lines.append(" monthly_risk_cost_per_failure = 660 * 1000000 * is_trip * downtime * is_series\n")
|
||||
new_lines.append(" \n")
|
||||
new_lines.append(" for month_key in months:\n")
|
||||
new_lines.append(" data = failures_prediction[month_key]\n")
|
||||
new_lines.append(" failure_counts.append(data['cumulative_failures'])\n")
|
||||
new_lines.append(" \n")
|
||||
new_lines.append(" for i in range(num_months):\n")
|
||||
new_lines.append(" month_index = i + 1\n")
|
||||
new_lines.append(" \n")
|
||||
new_lines.append(" # Use only months within the analysis window\n")
|
||||
new_lines.append(" if month_index > self.time_window_months:\n")
|
||||
new_lines.append(" continue\n")
|
||||
new_lines.append("\n")
|
||||
new_lines.append(" # Check sparepart availability for this month\n")
|
||||
new_lines.append(" sparepart_analysis = self._analyze_sparepart_availability(\n")
|
||||
new_lines.append(" location_tag, month_index - 1, planned_overhauls or []\n")
|
||||
new_lines.append(" )\n")
|
||||
new_lines.append(" \n")
|
||||
new_lines.append(" # THEORY: Total Expected Cost per Unit Time (CPUT)\n")
|
||||
new_lines.append(" # Reference: Maintenance Optimization Models (Age/Block Replacement)\n")
|
||||
new_lines.append(" # C(T) = [Total Preventive Cost + Total Expected Corrective Cost(T)] / T\n")
|
||||
new_lines.append(" \n")
|
||||
new_lines.append(" # 1. Total Expected Corrective Cost until month_index (Expected number of failures * cost per failure)\n")
|
||||
new_lines.append(" # In NHPP model, Expected Failures E[N(T)] = Cumulative Failures\n")
|
||||
new_lines.append(" total_expected_failure_cost = failure_counts[i] * (failure_replacement_cost + monthly_risk_cost_per_failure)\n")
|
||||
new_lines.append(" \n")
|
||||
new_lines.append(" # 2. Total Preventive Cost (One-time cost at month_index)\n")
|
||||
new_lines.append(" # Includes labor, materials, and procurement delays\n")
|
||||
new_lines.append(" procurement_cost = sparepart_analysis['total_procurement_cost']\n")
|
||||
new_lines.append(" total_preventive_cost = preventive_cost + procurement_cost\n")
|
||||
new_lines.append(" \n")
|
||||
new_lines.append(" # 3. Expected Total Cycle Cost\n")
|
||||
new_lines.append(" total_cycle_cost = total_expected_failure_cost + total_preventive_cost\n")
|
||||
new_lines.append(" \n")
|
||||
new_lines.append(" # 4. Expected Cost Per Unit Time (Optimization Target)\n")
|
||||
new_lines.append(" # This value forms the U-shaped curve\n")
|
||||
new_lines.append(" cput = total_cycle_cost / month_index\n")
|
||||
new_lines.append(" \n")
|
||||
new_lines.append(" # Store both absolute and amortized components for visualization\n")
|
||||
new_lines.append(" results.append({\n")
|
||||
new_lines.append(" 'month': month_index,\n")
|
||||
new_lines.append(" 'number_of_failures': failure_counts[i],\n")
|
||||
new_lines.append(" 'is_actual': failures_prediction[months[i]].get('source') == 'actual',\n")
|
||||
new_lines.append(" \n")
|
||||
new_lines.append(" # Amortized components (for the \"U\" chart)\n")
|
||||
new_lines.append(" 'failure_cost': total_expected_failure_cost / month_index,\n")
|
||||
new_lines.append(" 'preventive_cost': preventive_cost / month_index,\n")
|
||||
new_lines.append(" 'procurement_cost': procurement_cost / month_index,\n")
|
||||
new_lines.append(" 'total_cost': cput,\n")
|
||||
new_lines.append(" \n")
|
||||
new_lines.append(" # Absolute values (for breakdown analysis)\n")
|
||||
new_lines.append(" 'absolute_failure_cost': total_expected_failure_cost,\n")
|
||||
new_lines.append(" 'absolute_overhaul_cost': preventive_cost,\n")
|
||||
new_lines.append(" 'absolute_procurement_cost': procurement_cost,\n")
|
||||
new_lines.append(" 'total_cycle_cost': total_cycle_cost,\n")
|
||||
new_lines.append("\n")
|
||||
new_lines.append(" 'is_after_planned_oh': month_index > self.planned_oh_months,\n")
|
||||
new_lines.append(" 'delay_months': max(0, month_index - self.planned_oh_months),\n")
|
||||
new_lines.append(" 'procurement_details': sparepart_analysis,\n")
|
||||
new_lines.append(" 'sparepart_available': sparepart_analysis['available'],\n")
|
||||
new_lines.append(" 'sparepart_status': sparepart_analysis['message'],\n")
|
||||
new_lines.append(" 'can_proceed': sparepart_analysis['can_proceed_with_delays']\n")
|
||||
new_lines.append(" })\n")
|
||||
new_lines.append(" \n")
|
||||
new_lines.append(" return results\n")
|
||||
new_lines.append("\n")
|
||||
new_lines.append(" def _analyze_sparepart_availability(self, equipment_tag: str, target_month: int, \n")
|
||||
new_lines.append(" planned_overhauls: List) -> Dict:\n")
|
||||
new_lines.append(" \"\"\"Analyze sparepart availability for equipment at target month\"\"\"\n")
|
||||
new_lines.append(" if not self.sparepart_manager:\n")
|
||||
new_lines.append(" return {\n")
|
||||
new_lines.append(" 'available': True,\n")
|
||||
new_lines.append(" 'message': 'Sparepart manager not initialized',\n")
|
||||
new_lines.append(" 'total_procurement_cost': 0,\n")
|
||||
new_lines.append(" 'procurement_needed': [],\n")
|
||||
new_lines.append(" 'can_proceed_with_delays': True\n")
|
||||
new_lines.append(" }\n")
|
||||
new_lines.append(" \n")
|
||||
new_lines.append(" # Convert planned overhauls to format expected by sparepart manager\n")
|
||||
new_lines.append(" other_overhauls = [(eq_tag, month) for eq_tag, month in planned_overhauls\n")
|
||||
new_lines.append(" if eq_tag != equipment_tag and month <= target_month]\n")
|
||||
new_lines.append("\n")
|
||||
new_lines.append(" return self.sparepart_manager.check_sparepart_availability(\n")
|
||||
new_lines.append(" equipment_tag, target_month, other_overhauls\n")
|
||||
new_lines.append(" )\n")
|
||||
new_lines.append("\n")
|
||||
|
||||
new_lines.extend(lines[159:])
|
||||
|
||||
with open('/home/atra/Development/be-optimumoh/src/calculation_time_constrains/service.py', 'w') as f:
|
||||
f.writelines(new_lines)
|
||||
@ -1,263 +0,0 @@
|
||||
import re
|
||||
|
||||
with open('src/calculation_time_constrains/service.py', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Replacement 1: run_simulation_with_spareparts
|
||||
replacement_1 = """ finally:
|
||||
await optimum_oh_model._close_session()
|
||||
|
||||
# Re-fetch calculation_data with equipment_results to ensure they are loaded
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import selectinload
|
||||
calculation_query = await db_session.execute(
|
||||
select(CalculationData)
|
||||
.options(selectinload(CalculationData.equipment_results), selectinload(CalculationData.parameter))
|
||||
.where(CalculationData.id == calculation.id)
|
||||
)
|
||||
scope_calculation = calculation_query.scalar_one_or_none()
|
||||
|
||||
data_num = scope_calculation.max_interval
|
||||
all_equipment = scope_calculation.equipment_results
|
||||
included_equipment = [eq for eq in all_equipment if eq.is_included]
|
||||
|
||||
calculation_results = []
|
||||
fleet_statistics = {
|
||||
'total_equipment': len(all_equipment),
|
||||
'included_equipment': len(included_equipment),
|
||||
'excluded_equipment': len(all_equipment) - len(included_equipment),
|
||||
'equipment_with_sparepart_constraints': 0,
|
||||
'total_procurement_items': 0,
|
||||
'critical_procurement_items': 0,
|
||||
'total_spareparts': 745
|
||||
}
|
||||
|
||||
avg_failure_cost = sum((eq.material_cost or 0) + (3 * 111000 * 3) for eq in all_equipment) / len(all_equipment) if all_equipment else 0
|
||||
|
||||
rbd_marginal_fails = [0] * data_num
|
||||
try:
|
||||
if scope_calculation.rbd_simulation_id:
|
||||
from src.config import RBD_SERVICE_API
|
||||
import httpx
|
||||
plant_result_url = f"{RBD_SERVICE_API}/aeros/simulation/result/calc/{scope_calculation.rbd_simulation_id}/plant"
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
response = await client.get(
|
||||
plant_result_url,
|
||||
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||
)
|
||||
if response.status_code == 200:
|
||||
plant_data = response.json().get("data", {})
|
||||
timestamp_outs = plant_data.get("timestamp_outs", [])
|
||||
if timestamp_outs:
|
||||
from src.calculation_time_constrains.utils import create_time_series_data, calculate_failures_per_month
|
||||
hourly_data = create_time_series_data(timestamp_outs, max_hours=data_num * 720)
|
||||
cumulative_rbd_fails = calculate_failures_per_month(hourly_data)
|
||||
rbd_fails_map = {m['month']: m['failures'] for m in cumulative_rbd_fails}
|
||||
prev_fail = 0
|
||||
for m in range(1, data_num + 1):
|
||||
curr_fail = rbd_fails_map.get(m, prev_fail)
|
||||
rbd_marginal_fails[m-1] = curr_fail - prev_fail
|
||||
prev_fail = curr_fail
|
||||
except Exception as e:
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.warning(f"Failed to fetch plant simulation: {e}")
|
||||
|
||||
cumulative_plant_failures = 0
|
||||
import numpy as np
|
||||
from .schema import CalculationResultsRead
|
||||
for month_index in range(data_num):
|
||||
historical_marginal_fail = 0
|
||||
for eq in all_equipment:
|
||||
if eq.is_actual and month_index < len(eq.is_actual) and eq.is_actual[month_index]:
|
||||
curr_fail = eq.daily_failures[month_index] if month_index < len(eq.daily_failures) else 0
|
||||
prev_fail = eq.daily_failures[month_index-1] if month_index > 0 and (month_index - 1) < len(eq.daily_failures) else 0
|
||||
historical_marginal_fail += max(0, curr_fail - prev_fail)
|
||||
|
||||
marginal_fail = rbd_marginal_fails[month_index] + historical_marginal_fail
|
||||
cumulative_plant_failures += marginal_fail
|
||||
|
||||
month_result = {
|
||||
"overhaul_cost": 0.0,
|
||||
"corrective_cost": 0.0,
|
||||
"procurement_cost": 0.0,
|
||||
"num_failures": cumulative_plant_failures,
|
||||
"day": month_index + 1,
|
||||
"month": month_index + 1,
|
||||
"procurement_details": {},
|
||||
"sparepart_summary": {
|
||||
"total_procurement_cost": 0.0,
|
||||
"equipment_requiring_procurement": 0,
|
||||
"critical_shortages": 0,
|
||||
"existing_orders_value": 0.0,
|
||||
"new_orders_required": 0,
|
||||
"urgent_procurements": 0
|
||||
}
|
||||
}
|
||||
|
||||
equipment_requiring_procurement = 0
|
||||
total_existing_orders_value = 0.0
|
||||
total_new_orders_value = 0.0
|
||||
critical_shortages = 0
|
||||
urgent_procurements = 0
|
||||
|
||||
for eq in all_equipment:
|
||||
if month_index >= len(eq.procurement_details):
|
||||
continue
|
||||
procurement_detail = eq.procurement_details[month_index]
|
||||
if (procurement_detail and isinstance(procurement_detail, dict) and procurement_detail.get("procurement_needed")):
|
||||
equipment_requiring_procurement += 1
|
||||
pr_po_summary = procurement_detail.get("pr_po_summary", {})
|
||||
existing_orders_value = pr_po_summary.get("total_existing_value", 0)
|
||||
total_existing_orders_value += existing_orders_value
|
||||
new_orders_value = pr_po_summary.get("total_new_orders_value", 0)
|
||||
total_new_orders_value += new_orders_value
|
||||
critical_missing = procurement_detail.get("critical_missing_parts", 0)
|
||||
if critical_missing > 0:
|
||||
critical_shortages += 1
|
||||
recommendations = procurement_detail.get("recommendations", [])
|
||||
urgent_items = [r for r in recommendations if r.get("priority") == "CRITICAL"]
|
||||
if urgent_items:
|
||||
urgent_procurements += 1
|
||||
is_included_eq = False if eq.is_initial else eq.is_included
|
||||
month_result["procurement_details"][eq.location_tag] = {
|
||||
"is_included": is_included_eq,
|
||||
"location_tag": eq.location_tag,
|
||||
"details": procurement_detail.get("procurement_needed", []),
|
||||
"detailed_message": procurement_detail.get("detailed_message", ""),
|
||||
"pr_po_summary": pr_po_summary,
|
||||
"recommendations": recommendations,
|
||||
"sparepart_available": procurement_detail.get("sparepart_available", True),
|
||||
"can_proceed": procurement_detail.get("can_proceed_with_delays", True),
|
||||
"critical_missing_parts": critical_missing,
|
||||
"existing_orders_value": existing_orders_value,
|
||||
"new_orders_value": new_orders_value
|
||||
}
|
||||
if eq.is_included:
|
||||
if (month_index < len(eq.overhaul_costs) and month_index < len(eq.procurement_costs)):
|
||||
month_result["overhaul_cost"] += float(eq.overhaul_costs[month_index])
|
||||
month_result["procurement_cost"] += float(eq.procurement_costs[month_index])
|
||||
|
||||
month_result["corrective_cost"] = (cumulative_plant_failures * avg_failure_cost) / (month_index + 1)
|
||||
month_result["sparepart_summary"].update({
|
||||
"total_procurement_cost": month_result["procurement_cost"],
|
||||
"equipment_requiring_procurement": equipment_requiring_procurement,
|
||||
"critical_shortages": critical_shortages,
|
||||
"existing_orders_value": total_existing_orders_value,
|
||||
"new_orders_required": len([eq for eq in all_equipment if month_index < len(eq.procurement_details) and eq.procurement_details[month_index] and eq.procurement_details[month_index].get("procurement_needed")]),
|
||||
"urgent_procurements": urgent_procurements
|
||||
})
|
||||
month_result["total_cost"] = month_result["corrective_cost"] + month_result["overhaul_cost"] + month_result["procurement_cost"]
|
||||
calculation_results.append(month_result)
|
||||
|
||||
optimum_day = np.argmin([month["total_cost"] for month in calculation_results])
|
||||
scope_calculation.optimum_oh_day = int(optimum_day)
|
||||
|
||||
fleet_statistics['equipment_with_sparepart_constraints'] = len([
|
||||
eq for eq in all_equipment if any(detail and detail.get("procurement_needed") for detail in eq.procurement_details if detail)
|
||||
])
|
||||
fleet_statistics['total_procurement_items'] = sum([
|
||||
len(detail.get("procurement_needed", [])) for eq in all_equipment for detail in eq.procurement_details if detail and isinstance(detail, dict)
|
||||
])
|
||||
|
||||
analysis_metadata = {
|
||||
"planned_month": (scope.start_date.year - prev_oh_scope.end_date.year) * 12 + (scope.start_date.month - prev_oh_scope.end_date.month) if prev_oh_scope and scope else 0,
|
||||
"total_fleet_procurement_cost": sum([eq.procurement_costs[int(scope_calculation.optimum_oh_day)] for eq in all_equipment if eq.procurement_costs]),
|
||||
"last_oh_date": prev_oh_scope.end_date.isoformat() if prev_oh_scope else None,
|
||||
"next_oh_date": scope.start_date.isoformat() if scope else None,
|
||||
"optimal_stat": None
|
||||
}
|
||||
|
||||
calc_results_read = [CalculationResultsRead(**r) for r in calculation_results]
|
||||
optimal_analysis = _analyze_optimal_timing(
|
||||
calc_results_read, scope_calculation.optimum_oh_day, prev_oh_scope, scope
|
||||
)
|
||||
|
||||
scope_calculation.plant_results = calculation_results
|
||||
scope_calculation.fleet_statistics = fleet_statistics
|
||||
scope_calculation.analysis_metadata = analysis_metadata
|
||||
scope_calculation.optimum_analysis = optimal_analysis
|
||||
|
||||
await db_session.commit()
|
||||
|
||||
return {
|
||||
"id": calculation.id,
|
||||
"optimum": optimal_analysis
|
||||
}"""
|
||||
|
||||
pattern_1 = re.compile(r" finally:\n await optimum_oh_model\._close_session\(\).*?return \{\n \"id\": calculation_data\.id,\n \"optimum\": stats\n \}", re.DOTALL)
|
||||
if pattern_1.search(content):
|
||||
content = pattern_1.sub(replacement_1, content)
|
||||
else:
|
||||
print("Could not find Replacement 1 target")
|
||||
|
||||
# Replacement 2: get_calculation_result
|
||||
replacement_2 = """async def get_calculation_result(db_session: DbSession, calculation_id: str, token, include_risk_cost):
|
||||
\"\"\"
|
||||
Get calculation results from DB, returning pre-calculated plant and equipment results.
|
||||
\"\"\"
|
||||
try:
|
||||
# Get calculation data with equipment results
|
||||
calculation_query = await db_session.execute(
|
||||
select(CalculationData)
|
||||
.options(selectinload(CalculationData.equipment_results))
|
||||
.where(CalculationData.id == calculation_id)
|
||||
)
|
||||
scope_calculation = calculation_query.scalar_one_or_none()
|
||||
|
||||
if not scope_calculation:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Calculation with id {calculation_id} does not exist.",
|
||||
)
|
||||
|
||||
scope_overhaul = await get_scope(
|
||||
db_session=db_session,
|
||||
overhaul_session_id=scope_calculation.overhaul_session_id
|
||||
)
|
||||
if not scope_overhaul:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Overhaul scope for session {scope_calculation.overhaul_session_id} does not exist.",
|
||||
)
|
||||
|
||||
# Parse pre-calculated plant results
|
||||
plant_results_raw = scope_calculation.plant_results or []
|
||||
calculation_results = [CalculationResultsRead(**r) for r in plant_results_raw]
|
||||
|
||||
# Return comprehensive result
|
||||
return CalculationTimeConstrainsRead(
|
||||
id=scope_calculation.id,
|
||||
reference=scope_calculation.overhaul_session_id,
|
||||
scope=scope_overhaul.maintenance_type.name,
|
||||
results=calculation_results,
|
||||
optimum_oh=scope_calculation.optimum_oh_day,
|
||||
optimum_oh_month=scope_calculation.optimum_oh_day + 1 if scope_calculation.optimum_oh_day is not None else None,
|
||||
equipment_results=scope_calculation.equipment_results,
|
||||
fleet_statistics=scope_calculation.fleet_statistics or {},
|
||||
optimal_analysis=scope_calculation.optimum_analysis or {},
|
||||
analysis_metadata=scope_calculation.analysis_metadata or {}
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.error(f"Error in get_calculation_result for calculation_id {calculation_id}: {str(e)}")
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Internal error processing calculation results: {str(e)}",
|
||||
)"""
|
||||
|
||||
pattern_2 = re.compile(r"async def get_calculation_result\(db_session: DbSession, calculation_id: str, token, include_risk_cost\):.*?raise HTTPException\(\n status_code=status\.HTTP_500_INTERNAL_SERVER_ERROR,\n detail=f\"Internal error processing calculation results: \{str\(e\)\}\",\n \)", re.DOTALL)
|
||||
if pattern_2.search(content):
|
||||
content = pattern_2.sub(replacement_2, content)
|
||||
else:
|
||||
print("Could not find Replacement 2 target")
|
||||
|
||||
with open('src/calculation_time_constrains/service.py', 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
print("Patch applied.")
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 63 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 72 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 63 KiB |
@ -1,6 +1,5 @@
|
||||
import uvicorn
|
||||
|
||||
from src.config import HOST, PORT
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run("src.main:app", host=HOST, port=PORT, reload=True)
|
||||
from src.config import HOST, PORT, ENV
|
||||
if __name__ == '__main__':
|
||||
is_dev = str(ENV).lower() == 'development'
|
||||
uvicorn.run('src.main:app', host=HOST, port=PORT, reload=is_dev)
|
||||
|
||||
@ -1,35 +0,0 @@
|
||||
import asyncio
|
||||
import os
|
||||
from temporalio.client import Client
|
||||
from temporalio.worker import Worker
|
||||
|
||||
from temporal.temporal_workflows import OptimumOHCalculationWorkflow
|
||||
from temporal.temporal_workflows import create_optimum_oh_calculation, request_rbd_simulation, run_optimum_oh_calculation
|
||||
|
||||
TEMPORAL_URL = os.environ.get("TEMPORAL_URL", "http://192.168.1.86:7233")
|
||||
|
||||
async def main():
|
||||
client = await Client.connect(TEMPORAL_URL)
|
||||
|
||||
try:
|
||||
worker = Worker(
|
||||
client,
|
||||
task_queue="oh-sim-queue",
|
||||
workflows=[OptimumOHCalculationWorkflow],
|
||||
activities=[
|
||||
create_optimum_oh_calculation,
|
||||
request_rbd_simulation,
|
||||
run_optimum_oh_calculation
|
||||
],
|
||||
max_concurrent_workflow_tasks=50,
|
||||
max_concurrent_activities=12
|
||||
)
|
||||
await worker.run()
|
||||
except Exception as e:
|
||||
print(f"Worker failed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@ -0,0 +1 @@
|
||||
|
||||
@ -1,197 +1,45 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
from src.auth.service import JWTBearer
|
||||
from src.calculation_budget_constrains.router import \
|
||||
router as calculation_budget_constraint
|
||||
from src.calculation_target_reliability.router import \
|
||||
router as calculation_target_reliability
|
||||
from src.calculation_time_constrains.router import \
|
||||
router as calculation_time_constrains_router, get_calculation
|
||||
from src.optimum_time_constraint.router import router as optimum_time_constraint_router
|
||||
from src.optimum_time_constraint.router import get_calculation as optimum_get_calculation
|
||||
# from src.job.router import router as job_router
|
||||
|
||||
from src.calculation_budget_constrains.router import router as calculation_budget_constraint
|
||||
from src.calculation_target_reliability.router import router as calculation_target_reliability
|
||||
from src.calculation_time_constrains.router import router as calculation_time_constrains_router, get_calculation
|
||||
from src.overhaul.router import router as overhaul_router
|
||||
from src.standard_scope.router import router as standard_scope_router
|
||||
from src.overhaul_activity.router import router as overhaul_activity_router
|
||||
from src.workscope_group.router import router as workscope_group_router
|
||||
from src.equipment_workscope_group.router import router as equipment_workscope_group_router
|
||||
# from src.overhaul_job.router import router as job_overhaul_router
|
||||
# from src.overhaul_scope.router import router as scope_router
|
||||
# from src.scope_equipment.router import router as scope_equipment_router
|
||||
# from src.scope_equipment_job.router import router as scope_equipment_job_router
|
||||
# from src.overhaul_schedule.router import router as overhaul_schedule_router
|
||||
from src.overhaul_gantt.router import router as gantt_router
|
||||
from src.sparepart.router import router as sparepart_router
|
||||
from src.equipment_sparepart.router import router as equipment_sparepart_router
|
||||
|
||||
# from src.overhaul_scope.router import router as scope_router
|
||||
# from src.scope_equipment.router import router as scope_equipment_router
|
||||
# from src.overhaul.router import router as overhaul_router
|
||||
# from src.overhaul_history.router import router as overhaul_history_router
|
||||
# from src.overhaul_activity.router import router as scope_equipment_activity_router
|
||||
from src.overhaul_scope.router import router as ovehaul_schedule_router
|
||||
# from src.scope_equipment_part.router import router as scope_equipment_part_router
|
||||
# from src.calculation_target_reliability.router import router as calculation_target_reliability
|
||||
#
|
||||
# from src.master_activity.router import router as activity_router
|
||||
|
||||
|
||||
class ErrorMessage(BaseModel):
|
||||
msg: str
|
||||
|
||||
|
||||
class ErrorResponse(BaseModel):
|
||||
detail: Optional[List[ErrorMessage]]
|
||||
api_router = APIRouter(default_response_class=JSONResponse, responses={400: {'model': ErrorResponse}, 401: {'model': ErrorResponse}, 403: {'model': ErrorResponse}, 404: {'model': ErrorResponse}, 500: {'model': ErrorResponse}})
|
||||
|
||||
|
||||
api_router = APIRouter(
|
||||
default_response_class=JSONResponse,
|
||||
responses={
|
||||
400: {"model": ErrorResponse},
|
||||
401: {"model": ErrorResponse},
|
||||
403: {"model": ErrorResponse},
|
||||
404: {"model": ErrorResponse},
|
||||
500: {"model": ErrorResponse},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@api_router.get("/healthcheck", include_in_schema=False)
|
||||
@api_router.get('/healthcheck', include_in_schema=False)
|
||||
def healthcheck():
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
authenticated_api_router = APIRouter(
|
||||
dependencies=[Depends(JWTBearer())],
|
||||
)
|
||||
# overhaul data
|
||||
authenticated_api_router.include_router(
|
||||
overhaul_router, prefix="/overhauls", tags=["overhaul"]
|
||||
)
|
||||
|
||||
|
||||
# authenticated_api_router.include_router(job_router, prefix="/jobs", tags=["job"])
|
||||
|
||||
# # # Overhaul session data
|
||||
# authenticated_api_router.include_router(
|
||||
# scope_router, prefix="/overhaul-session", tags=["overhaul-session"]
|
||||
# )
|
||||
|
||||
authenticated_api_router.include_router(
|
||||
standard_scope_router, prefix="/scope-equipments", tags=["scope_equipment"]
|
||||
)
|
||||
|
||||
authenticated_api_router.include_router(
|
||||
overhaul_activity_router, prefix="/overhaul-activity", tags=["activity"]
|
||||
)
|
||||
|
||||
authenticated_api_router.include_router(
|
||||
workscope_group_router, prefix="/workscopes", tags=["workscope_groups"]
|
||||
)
|
||||
|
||||
authenticated_api_router.include_router(
|
||||
sparepart_router, prefix="/spareparts", tags=["sparepart"]
|
||||
)
|
||||
|
||||
authenticated_api_router.include_router(
|
||||
equipment_workscope_group_router,
|
||||
prefix="/equipment-workscopes",
|
||||
tags=["equipment_workscope_groups"],
|
||||
)
|
||||
|
||||
authenticated_api_router.include_router(
|
||||
equipment_sparepart_router,
|
||||
prefix="/equipment-spareparts",
|
||||
tags=["equipment_workscope_sparepart_router"]
|
||||
)
|
||||
# authenticated_api_router.include_router(
|
||||
# scope_equipment_job_router,
|
||||
# prefix="/scope-equipment-jobs",
|
||||
# tags=["scope_equipment", "job"],
|
||||
# )
|
||||
|
||||
# authenticated_api_router.include_router(
|
||||
# overhaul_schedule_router,
|
||||
# prefix="/overhaul-schedules",
|
||||
# tags=["overhaul_schedule"],
|
||||
# )
|
||||
|
||||
# authenticated_api_router.include_router(
|
||||
# job_overhaul_router, prefix="/overhaul-jobs", tags=["job", "overhaul"]
|
||||
# )
|
||||
|
||||
authenticated_api_router.include_router(
|
||||
gantt_router, prefix="/overhaul-gantt", tags=["gantt"]
|
||||
)
|
||||
|
||||
# authenticated_api_router.include_router(
|
||||
# overhaul_history_router, prefix="/overhaul-history", tags=["overhaul_history"]
|
||||
# )
|
||||
|
||||
# authenticated_api_router.include_router(
|
||||
# scope_equipment_activity_router, prefix="/equipment-activities", tags=["scope_equipment_activities"]
|
||||
# )
|
||||
|
||||
# authenticated_api_router.include_router(
|
||||
# activity_router, prefix="/activities", tags=["activities"]
|
||||
# )
|
||||
|
||||
# authenticated_api_router.include_router(
|
||||
# scope_equipment_part_router, prefix="/equipment-parts", tags=["scope_equipment_parts"]
|
||||
# )
|
||||
|
||||
authenticated_api_router.include_router(
|
||||
ovehaul_schedule_router, prefix="/overhaul-schedules", tags=["overhaul_schedules"]
|
||||
)
|
||||
|
||||
# calculation
|
||||
calculation_router = APIRouter(prefix="/calculation", tags=["calculations"])
|
||||
|
||||
# Time constrains
|
||||
calculation_router.include_router(
|
||||
calculation_time_constrains_router,
|
||||
prefix="/time-constraint",
|
||||
tags=["calculation", "time_constraint"],
|
||||
)
|
||||
|
||||
# Optimized Time constrains
|
||||
calculation_router.include_router(
|
||||
optimum_time_constraint_router,
|
||||
prefix="/optimum-time-constraint",
|
||||
tags=["calculation", "optimum_time_constraint"],
|
||||
)
|
||||
|
||||
# Target reliability
|
||||
calculation_router.include_router(
|
||||
calculation_target_reliability,
|
||||
prefix="/target-reliability",
|
||||
tags=["calculation", "target_reliability"],
|
||||
)
|
||||
|
||||
# # Budget Constrain
|
||||
calculation_router.include_router(
|
||||
calculation_budget_constraint,
|
||||
prefix="/budget-constraint",
|
||||
tags=["calculation", "budget_constraint"],
|
||||
)
|
||||
|
||||
return {'status': 'ok'}
|
||||
authenticated_api_router = APIRouter(dependencies=[Depends(JWTBearer())])
|
||||
authenticated_api_router.include_router(overhaul_router, prefix='/overhauls', tags=['overhaul'])
|
||||
authenticated_api_router.include_router(standard_scope_router, prefix='/scope-equipments', tags=['scope_equipment'])
|
||||
authenticated_api_router.include_router(overhaul_activity_router, prefix='/overhaul-activity', tags=['activity'])
|
||||
authenticated_api_router.include_router(workscope_group_router, prefix='/workscopes', tags=['workscope_groups'])
|
||||
authenticated_api_router.include_router(sparepart_router, prefix='/spareparts', tags=['sparepart'])
|
||||
authenticated_api_router.include_router(equipment_workscope_group_router, prefix='/equipment-workscopes', tags=['equipment_workscope_groups'])
|
||||
authenticated_api_router.include_router(equipment_sparepart_router, prefix='/equipment-spareparts', tags=['equipment_workscope_sparepart_router'])
|
||||
authenticated_api_router.include_router(gantt_router, prefix='/overhaul-gantt', tags=['gantt'])
|
||||
authenticated_api_router.include_router(ovehaul_schedule_router, prefix='/overhaul-schedules', tags=['overhaul_schedules'])
|
||||
calculation_router = APIRouter(prefix='/calculation', tags=['calculations'])
|
||||
calculation_router.include_router(calculation_time_constrains_router, prefix='/time-constraint', tags=['calculation', 'time_constraint'])
|
||||
calculation_router.include_router(calculation_target_reliability, prefix='/target-reliability', tags=['calculation', 'target_reliability'])
|
||||
calculation_router.include_router(calculation_budget_constraint, prefix='/budget-constraint', tags=['calculation', 'budget_constraint'])
|
||||
authenticated_api_router.include_router(calculation_router)
|
||||
|
||||
api_router.include_router(
|
||||
get_calculation,
|
||||
prefix="/calculation/time-constraint",
|
||||
tags=["calculation", "time_constraint"],
|
||||
)
|
||||
|
||||
api_router.include_router(
|
||||
optimum_get_calculation,
|
||||
prefix="/calculation/optimum-time-constraint",
|
||||
tags=["calculation", "optimum_time_constraint"],
|
||||
)
|
||||
|
||||
authenticated_api_router.include_router(get_calculation, prefix='/calculation/time-constraint', tags=['calculation', 'time_constraint'])
|
||||
api_router.include_router(authenticated_api_router)
|
||||
|
||||
@ -0,0 +1,64 @@
|
||||
import logging
|
||||
import logging.handlers
|
||||
import json
|
||||
import datetime
|
||||
import os
|
||||
import sys
|
||||
try:
|
||||
from zoneinfo import ZoneInfo
|
||||
except ImportError:
|
||||
from backports.zoneinfo import ZoneInfo
|
||||
from src.config import LOG_LEVEL, SERVICE_NAME
|
||||
from src.enums import OptimumOHEnum
|
||||
_TZ_LOCAL = ZoneInfo('Asia/Jakarta')
|
||||
RESET = '\x1b[0m'
|
||||
COLORS = {'DEBUG': '\x1b[36m', 'INFO': '\x1b[32m', 'WARNING': '\x1b[33m', 'WARN': '\x1b[33m', 'ERROR': '\x1b[31m', 'CRITICAL': '\x1b[1;31m'}
|
||||
_REQUEST_FIELDS = frozenset({'method', 'path', 'status_code', 'duration_ms', 'request_id', 'user_id', 'user_ip', 'error_id', 'result', 'error_type', 'error_message'})
|
||||
|
||||
class LogLevels(OptimumOHEnum):
|
||||
info = 'INFO'
|
||||
warn = 'WARN'
|
||||
error = 'ERROR'
|
||||
debug = 'DEBUG'
|
||||
|
||||
class JSONFormatter(logging.Formatter):
|
||||
|
||||
def format(self, record: logging.LogRecord) -> str:
|
||||
dt = datetime.datetime.fromtimestamp(record.created, tz=_TZ_LOCAL)
|
||||
timestamp = dt.isoformat(timespec='microseconds')
|
||||
status_code = getattr(record, 'status_code', None)
|
||||
result = getattr(record, 'result', None)
|
||||
if result is None and status_code is not None:
|
||||
result = 'Request completed' if int(status_code) < 400 else 'Request failed'
|
||||
log_record = {'timestamp': timestamp, 'service_name': SERVICE_NAME, 'level': record.levelname, 'method': getattr(record, 'method', None), 'path': getattr(record, 'path', None), 'status_code': status_code, 'duration_ms': getattr(record, 'duration_ms', None), 'request_id': getattr(record, 'request_id', None), 'user_id': getattr(record, 'user_id', None), 'user_ip': getattr(record, 'user_ip', None), 'error_id': getattr(record, 'error_id', None), 'result': result, 'error_type': getattr(record, 'error_type', None), 'error_message': getattr(record, 'error_message', None)}
|
||||
log_json = json.dumps(log_record, default=str)
|
||||
if sys.stdout.isatty():
|
||||
color = COLORS.get(record.levelname, '')
|
||||
return f'{color}{log_json}{RESET}'
|
||||
return log_json
|
||||
|
||||
def configure_logging() -> None:
|
||||
log_level = str(LOG_LEVEL).upper()
|
||||
if log_level not in list(LogLevels):
|
||||
log_level = LogLevels.error
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.setLevel(log_level)
|
||||
if root_logger.hasHandlers():
|
||||
root_logger.handlers.clear()
|
||||
formatter = JSONFormatter()
|
||||
stream_handler = logging.StreamHandler(sys.stdout)
|
||||
stream_handler.setFormatter(formatter)
|
||||
root_logger.addHandler(stream_handler)
|
||||
os.makedirs('logs', exist_ok=True)
|
||||
file_handler = logging.handlers.RotatingFileHandler('logs/app.log', maxBytes=10 * 1024 * 1024, backupCount=5, encoding='utf-8')
|
||||
file_handler.setFormatter(formatter)
|
||||
root_logger.addHandler(file_handler)
|
||||
for logger_name in ['uvicorn', 'uvicorn.access', 'uvicorn.error', 'fastapi']:
|
||||
uvicorn_logger = logging.getLogger(logger_name)
|
||||
uvicorn_logger.handlers = []
|
||||
uvicorn_logger.propagate = True
|
||||
logging.getLogger('uvicorn.access').setLevel(logging.WARNING)
|
||||
slowapi_logger = logging.getLogger('slowapi')
|
||||
slowapi_logger.setLevel(logging.ERROR)
|
||||
slowapi_logger.propagate = False
|
||||
logging.getLogger('slack_sdk.web.base_client').setLevel(logging.CRITICAL)
|
||||
@ -0,0 +1 @@
|
||||
|
||||
@ -0,0 +1,105 @@
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Any, List, Union
|
||||
from fastapi import Request, HTTPException, status
|
||||
Allow: str = 'allow'
|
||||
Deny: str = 'deny'
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Principal:
|
||||
key: str
|
||||
value: str
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'{self.key}:{self.value}'
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.__repr__()
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SystemPrincipal(Principal):
|
||||
|
||||
def __init__(self, value: str):
|
||||
super().__init__(key='system', value=value)
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RolePrincipal(Principal):
|
||||
|
||||
def __init__(self, value: str):
|
||||
super().__init__(key='role', value=value)
|
||||
Everyone = SystemPrincipal(value='everyone')
|
||||
Authenticated = SystemPrincipal(value='authenticated')
|
||||
|
||||
class OHPermission(Enum):
|
||||
CREATE = 'create'
|
||||
READ = 'read'
|
||||
EDIT = 'edit'
|
||||
DELETE = 'delete'
|
||||
|
||||
class AccessControl:
|
||||
|
||||
def __init__(self, permission_exception: Any=None):
|
||||
self.permission_exception = permission_exception or HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='Permission denied')
|
||||
|
||||
def _acl(self, resource):
|
||||
acl = getattr(resource, '__acl__', [])
|
||||
if callable(acl):
|
||||
return acl()
|
||||
return acl
|
||||
|
||||
def has_permission(self, principals: List[Principal], required_permissions: List[OHPermission], resource: Any) -> bool:
|
||||
if not isinstance(resource, list):
|
||||
resource = [resource]
|
||||
permits = []
|
||||
for res in resource:
|
||||
granted = False
|
||||
acl = self._acl(res)
|
||||
for action, principal, permissions in acl:
|
||||
is_permitted = any((p in permissions for p in required_permissions))
|
||||
if (action == Allow and is_permitted) and (principal in principals or principal == Everyone):
|
||||
granted = True
|
||||
break
|
||||
if (action == Deny and is_permitted) and (principal in principals or principal == Everyone):
|
||||
granted = False
|
||||
break
|
||||
permits.append(granted)
|
||||
return all(permits)
|
||||
|
||||
def assert_access(self, principals: List[Principal], required_permissions: List[OHPermission], resource: Any):
|
||||
if not self.has_permission(principals, required_permissions, resource):
|
||||
raise self.permission_exception
|
||||
ALLOWED_ROLES = {'Admin', 'Super Admin', 'Engineer', 'Application Administrator'}
|
||||
|
||||
def require_any_role(*allowed_roles: str):
|
||||
normalized = {r.lower() for r in allowed_roles}
|
||||
|
||||
async def dependency(request: Request):
|
||||
user = getattr(request.state, 'user', None)
|
||||
if not user:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Not authenticated')
|
||||
user_role = user.get('role') if isinstance(user, dict) else getattr(user, 'role', None)
|
||||
if not user_role or user_role.lower() not in normalized:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='Your role does not have permission to access this resource.')
|
||||
return True
|
||||
return dependency
|
||||
|
||||
def required_permission(permissions: Union[OHPermission, List[OHPermission]], resources: Any):
|
||||
if isinstance(permissions, OHPermission):
|
||||
permissions = [permissions]
|
||||
|
||||
async def dependency(request: Request):
|
||||
user = getattr(request.state, 'user', None)
|
||||
if not user:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Not authenticated')
|
||||
user_role = None
|
||||
if isinstance(user, dict):
|
||||
user_role = user.get('role')
|
||||
else:
|
||||
user_role = getattr(user, 'role', None)
|
||||
if not user_role:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='No user role found')
|
||||
user_principals = [Authenticated, RolePrincipal(user_role)]
|
||||
ac = AccessControl()
|
||||
ac.assert_access(user_principals, permissions, resources)
|
||||
return True
|
||||
return dependency
|
||||
@ -1,7 +1,7 @@
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class UserBase(BaseModel):
|
||||
name: str
|
||||
name: Optional[str] = None
|
||||
role: str
|
||||
user_id: str
|
||||
|
||||
@ -1,235 +1,135 @@
|
||||
# app/auth/auth_bearer.py
|
||||
|
||||
import json
|
||||
from typing import Annotated, Optional
|
||||
|
||||
from typing import Annotated
|
||||
import requests
|
||||
from fastapi import Depends, HTTPException, Request
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
from sqlalchemy.sql.expression import false
|
||||
|
||||
import src.config as config
|
||||
|
||||
from .model import UserBase
|
||||
from .util import extract_template
|
||||
|
||||
class JWTBearer(HTTPBearer):
|
||||
def __init__(self, auto_error: bool = True):
|
||||
super(JWTBearer, self).__init__(auto_error=auto_error)
|
||||
|
||||
def __init__(self, auto_error: bool=True):
|
||||
super(JWTBearer, self).__init__(auto_error=False)
|
||||
|
||||
async def __call__(self, request: Request):
|
||||
credentials: HTTPAuthorizationCredentials = await super(
|
||||
JWTBearer, self
|
||||
).__call__(request)
|
||||
credentials: HTTPAuthorizationCredentials = await super(JWTBearer, self).__call__(request)
|
||||
if credentials:
|
||||
if not credentials.scheme == "Bearer":
|
||||
raise HTTPException(
|
||||
status_code=403, detail="Invalid authentication scheme."
|
||||
)
|
||||
if not credentials.scheme == 'Bearer':
|
||||
raise HTTPException(status_code=401, detail='Invalid authentication scheme.')
|
||||
method = request.method
|
||||
|
||||
if method == "OPTIONS":
|
||||
return
|
||||
|
||||
if method == 'OPTIONS':
|
||||
return None
|
||||
path = extract_template(request.url.path, request.path_params)
|
||||
|
||||
endpoint = f"/optimumoh{path}"
|
||||
|
||||
endpoint = f'/optimumoh{path}'
|
||||
user_info, message = self.verify_jwt(credentials.credentials, method, endpoint)
|
||||
|
||||
if not user_info:
|
||||
message = message.get("message", "Invalid token or expired token.")
|
||||
raise HTTPException(
|
||||
status_code=403, detail=message
|
||||
)
|
||||
|
||||
if isinstance(message, dict):
|
||||
message = message.get('message') or message.get('detail') or 'Invalid token or expired token.'
|
||||
else:
|
||||
message = str(message) if message else 'Invalid token or expired token.'
|
||||
raise HTTPException(status_code=401, detail=message)
|
||||
request.state.user = message
|
||||
|
||||
from src.context import set_user_id, set_username, set_role
|
||||
if hasattr(message, "user_id"):
|
||||
if hasattr(message, 'user_id'):
|
||||
set_user_id(str(message.user_id))
|
||||
if hasattr(message, "username"):
|
||||
set_username(message.username)
|
||||
elif hasattr(message, "name"):
|
||||
set_username(message.name)
|
||||
if hasattr(message, "role"):
|
||||
username = getattr(message, 'username', None) or getattr(message, 'name', None)
|
||||
if username:
|
||||
set_username(username)
|
||||
if hasattr(message, 'role'):
|
||||
set_role(message.role)
|
||||
|
||||
return message
|
||||
else:
|
||||
raise HTTPException(status_code=403, detail="Invalid authorization code.")
|
||||
raise HTTPException(status_code=401, detail='Invalid authorization code.')
|
||||
|
||||
def verify_jwt(self, jwtoken: str, method: str, endpoint: str):
|
||||
try:
|
||||
response = requests.get(
|
||||
f"{config.AUTH_SERVICE_API}/verify-token",
|
||||
headers={"Authorization": f"Bearer {jwtoken}"},
|
||||
)
|
||||
|
||||
url_to_verify = f'{config.AUTH_SERVICE_API}/verify-token'
|
||||
response = requests.get(url_to_verify, headers={'Authorization': f'Bearer {jwtoken}'})
|
||||
if not response.ok:
|
||||
return False, response.json()
|
||||
|
||||
return (False, response.json())
|
||||
user_data = response.json()
|
||||
return True, UserBase(**user_data["data"])
|
||||
|
||||
return (True, UserBase(**user_data['data']))
|
||||
except Exception as e:
|
||||
print(f"Token verification error: {str(e)}")
|
||||
return False, str(e)
|
||||
print(f'Token verification error: {str(e)}')
|
||||
return (False, str(e))
|
||||
|
||||
|
||||
# Create dependency to get current user from request state
|
||||
async def get_current_user(request: Request) -> UserBase:
|
||||
return request.state.user
|
||||
|
||||
async def get_token(request: Request):
|
||||
token = request.headers.get("Authorization")
|
||||
token = request.headers.get('Authorization')
|
||||
if token:
|
||||
return token.replace("Bearer ", "") # Menghapus prefix "Bearer "
|
||||
else:
|
||||
return request.cookies.get("access_token") # Fallback ke cookie
|
||||
|
||||
return "" # Mengembalikan token atau None jika tidak ada
|
||||
|
||||
return token.replace('Bearer ', '')
|
||||
return request.cookies.get('access_token')
|
||||
return ''
|
||||
|
||||
async def internal_key(request: Request):
|
||||
token = request.headers.get("Authorization")
|
||||
|
||||
if not token:
|
||||
api_key = request.headers.get("X-Internal-Key")
|
||||
|
||||
token = request.headers.get('Authorization')
|
||||
if not token:
|
||||
api_key = request.headers.get('X-Internal-Key')
|
||||
if api_key != config.API_KEY:
|
||||
raise HTTPException(
|
||||
status_code=403, detail="Invalid Key."
|
||||
)
|
||||
|
||||
raise HTTPException(status_code=401, detail='Invalid Key.')
|
||||
try:
|
||||
headers = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
f"{config.AUTH_SERVICE_API}/sign-in",
|
||||
headers=headers,
|
||||
data=json.dumps({
|
||||
"username": "ohuser",
|
||||
"password": "123456789"
|
||||
})
|
||||
)
|
||||
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
response = requests.post(f'{config.AUTH_SERVICE_API}/sign-in', headers=headers, data=json.dumps({'username': 'ohuser', 'password': '123456789'}))
|
||||
if not response.ok:
|
||||
print(str(response.json()))
|
||||
raise Exception("error auth")
|
||||
|
||||
raise Exception('error auth')
|
||||
user_data = response.json()
|
||||
return user_data['data']['access_token']
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(str(e))
|
||||
else:
|
||||
try:
|
||||
response = requests.get(
|
||||
f"{config.AUTH_SERVICE_API}/verify-token",
|
||||
headers={"Authorization": f"{token}"},
|
||||
)
|
||||
|
||||
verify_url = f'{config.AUTH_SERVICE_API}/verify-token'
|
||||
response = requests.get(verify_url, headers={'Authorization': f'{token}'})
|
||||
if not response.ok:
|
||||
raise HTTPException(
|
||||
status_code=403, detail="Invalid token."
|
||||
)
|
||||
|
||||
return token.split(" ")[1]
|
||||
|
||||
raise HTTPException(status_code=401, detail='Invalid token.')
|
||||
return token.split(' ')[1]
|
||||
except Exception as e:
|
||||
print(f"Token verification error: {str(e)}")
|
||||
return False, str(e)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
import httpx
|
||||
print(f'Token verification error: {str(e)}')
|
||||
return (False, str(e))
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Dict, Any
|
||||
import src.config as config
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
AUTH_NOTIFY_ENDPOINT = f'{config.AUTH_SERVICE_API}/admin/notify-limit'
|
||||
|
||||
|
||||
AUTH_NOTIFY_ENDPOINT = f"{config.AUTH_SERVICE_API}/admin/notify-limit"
|
||||
|
||||
|
||||
async def notify_admin_on_rate_limit(
|
||||
endpoint_name: str,
|
||||
ip_address: str,
|
||||
method: str = "POST",
|
||||
cooldown: int = 900,
|
||||
timeout: int = 5
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Kirim notifikasi ke admin via be-auth service ketika rate limit terlampaui.
|
||||
|
||||
Async version - gunakan di async context.
|
||||
"""
|
||||
payload = {
|
||||
"endpoint_name": endpoint_name,
|
||||
"ip_address": ip_address,
|
||||
"method": method,
|
||||
"cooldown": cooldown,
|
||||
}
|
||||
|
||||
|
||||
async def notify_admin_on_rate_limit(endpoint_name: str, ip_address: str, request: Request, method: str='POST', cooldown: int=900, timeout: int=5) -> Dict[str, Any]:
|
||||
payload = {'endpoint_name': f'oh/{endpoint_name}'.replace('//', ''), 'ip_address': ip_address, 'method': method, 'cooldown': cooldown}
|
||||
token = request.headers.get('Authorization')
|
||||
headers = {'Authorization': token} if token else {}
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||
response = await client.post(AUTH_NOTIFY_ENDPOINT, json=payload)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
log.info(f"Notifikasi admin sent | Endpoint: {endpoint_name}")
|
||||
return result
|
||||
|
||||
|
||||
response = await asyncio.to_thread(requests.post, AUTH_NOTIFY_ENDPOINT, json=payload, headers=headers, timeout=timeout)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
log.info(f'Notifikasi admin sent | Endpoint: {endpoint_name}')
|
||||
return result
|
||||
except Exception as e:
|
||||
log.error(f"Error notifying admin: {str(e)}")
|
||||
return {"status": False, "message": str(e), "data": payload}
|
||||
|
||||
|
||||
|
||||
|
||||
def notify_admin_on_rate_limit_sync(
|
||||
endpoint_name: str,
|
||||
ip_address: str,
|
||||
method: str = "POST",
|
||||
cooldown: int = 900,
|
||||
timeout: int = 5
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Kirim notifikasi ke admin via be-auth service.
|
||||
|
||||
Sync version - gunakan di exception handler atau sync context.
|
||||
RECOMMENDED untuk use case ini.
|
||||
"""
|
||||
payload = {
|
||||
"endpoint_name": endpoint_name,
|
||||
"ip_address": ip_address,
|
||||
"method": method,
|
||||
"cooldown": cooldown,
|
||||
}
|
||||
|
||||
log.error(f'Error notifying admin: {str(e)}')
|
||||
return {'status': False, 'message': str(e), 'data': payload}
|
||||
|
||||
def notify_admin_on_rate_limit_sync(endpoint_name: str, ip_address: str, request: Request, method: str='POST', cooldown: int=900, timeout: int=5) -> Dict[str, Any]:
|
||||
payload = {'endpoint_name': f'oh/{endpoint_name}'.replace('//', '/'), 'ip_address': ip_address, 'method': method, 'cooldown': cooldown}
|
||||
token = request.headers.get('Authorization')
|
||||
headers = {'Authorization': token} if token else {}
|
||||
try:
|
||||
response = httpx.post(AUTH_NOTIFY_ENDPOINT, json=payload, timeout=timeout)
|
||||
response = requests.post(AUTH_NOTIFY_ENDPOINT, json=payload, headers=headers, timeout=timeout)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
log.info(f"Notifikasi admin sent | Endpoint: {endpoint_name}")
|
||||
log.info(f'Notifikasi admin sent | Endpoint: {endpoint_name}')
|
||||
return result
|
||||
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"Error notifying admin: {str(e)}")
|
||||
return {"status": False, "message": str(e), "data": payload}
|
||||
|
||||
|
||||
|
||||
log.error(f'Error notifying admin: {str(e)}')
|
||||
return {'status': False, 'message': str(e), 'data': payload}
|
||||
|
||||
async def admin_required(request: Request):
|
||||
user = await get_current_user(request)
|
||||
if user.role != 'Admin' or user.role != 'Superadmin':
|
||||
raise HTTPException(status_code=403, detail='Invalid authorization code.')
|
||||
return user
|
||||
Admin = Annotated[UserBase, Depends(admin_required)]
|
||||
CurrentUser = Annotated[UserBase, Depends(get_current_user)]
|
||||
Token = Annotated[str, Depends(get_token)]
|
||||
InternalKey = Annotated[str, Depends(internal_key)]
|
||||
InternalKey = Annotated[str, Depends(internal_key)]
|
||||
|
||||
@ -1,9 +1,6 @@
|
||||
def extract_template(path_string, value_dict):
|
||||
template = path_string
|
||||
|
||||
# Replace each value in the dict with its corresponding key placeholder
|
||||
for key, value in value_dict.items():
|
||||
if str(value) in template:
|
||||
template = template.replace(str(value), f'<{key}>')
|
||||
|
||||
return template
|
||||
|
||||
@ -0,0 +1 @@
|
||||
|
||||
@ -1,43 +1,21 @@
|
||||
from typing import Annotated, Dict, List, Optional
|
||||
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from typing import Annotated, Dict
|
||||
from fastapi import APIRouter
|
||||
from fastapi.params import Query
|
||||
|
||||
from src.auth.service import Token
|
||||
from src.calculation_budget_constrains.schema import BudgetContraintQuery
|
||||
from src.calculation_target_reliability.service import get_simulation_results
|
||||
from src.config import TC_RBD_ID
|
||||
from src.database.core import CollectorDbSession, DbSession
|
||||
from src.models import StandardResponse
|
||||
|
||||
from .service import get_all_budget_constrains
|
||||
from src.auth.access_control import required_permission, require_any_role, OHPermission, ALLOWED_ROLES
|
||||
from src.overhaul_scope.model import OverhaulScope
|
||||
from fastapi import Depends
|
||||
router = APIRouter(dependencies=[Depends(require_any_role(*ALLOWED_ROLES))])
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/{session_id}", response_model=StandardResponse[Dict])
|
||||
async def get_target_reliability(
|
||||
db_session: DbSession,
|
||||
token: Token,
|
||||
session_id: str,
|
||||
collector_db: CollectorDbSession,
|
||||
params: Annotated[BudgetContraintQuery, Query()],
|
||||
):
|
||||
"""Get all scope pagination."""
|
||||
@router.get('/{session_id}', response_model=StandardResponse[Dict], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
||||
async def get_target_reliability(db_session: DbSession, token: Token, session_id: str, collector_db: CollectorDbSession, params: Annotated[BudgetContraintQuery, Query()]):
|
||||
cost_threshold = params.cost_threshold
|
||||
results = await get_simulation_results(
|
||||
simulation_id = TC_RBD_ID,
|
||||
token=token
|
||||
)
|
||||
|
||||
results, consequence = await get_all_budget_constrains(
|
||||
db_session=db_session, session_id=session_id, cost_threshold=cost_threshold, simulation_result=results, collector_db=collector_db
|
||||
)
|
||||
|
||||
return StandardResponse(
|
||||
data={
|
||||
"results": results,
|
||||
"consequence": consequence
|
||||
},
|
||||
message="Data retrieved successfully",
|
||||
)
|
||||
results = await get_simulation_results(simulation_id=TC_RBD_ID, token=token)
|
||||
results, consequence = await get_all_budget_constrains(db_session=db_session, session_id=session_id, cost_threshold=cost_threshold, simulation_result=results, collector_db=collector_db)
|
||||
return StandardResponse(data={'results': results, 'consequence': consequence}, message='Data retrieved successfully')
|
||||
|
||||
@ -0,0 +1 @@
|
||||
|
||||
@ -1,150 +1,45 @@
|
||||
import asyncio
|
||||
from typing import Dict, List, Optional
|
||||
from typing_extensions import Annotated
|
||||
from temporalio.client import Client
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from fastapi.params import Query
|
||||
|
||||
from src.calculation_target_reliability.utils import wait_for_workflow
|
||||
from src.config import TEMPORAL_URL, TR_RBD_ID
|
||||
from src.database.core import DbSession, CollectorDbSession
|
||||
from src.auth.service import Token
|
||||
from src.models import StandardResponse
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from .service import run_rbd_simulation, get_simulation_results, identify_worst_eaf_contributors
|
||||
from .service import get_simulation_results, identify_worst_eaf_contributors
|
||||
from .schema import OptimizationResult, TargetReliabiltiyQuery
|
||||
from src.auth.access_control import required_permission, OHPermission
|
||||
from src.overhaul_scope.model import OverhaulScope
|
||||
from fastapi import Depends
|
||||
router = APIRouter()
|
||||
|
||||
class SimulateRequest(BaseModel):
|
||||
duration: int = Field(17520, description="Simulation duration in hours")
|
||||
oh_duration: int = Field(1200, description="Overhaul duration in hours")
|
||||
oh_session_id: str = Field(..., description="Overhaul session ID")
|
||||
|
||||
@router.post("/simulate", response_model=StandardResponse)
|
||||
async def start_simulation(
|
||||
token: Token,
|
||||
db_session: DbSession,
|
||||
payload: SimulateRequest
|
||||
):
|
||||
"""Start RBD simulation for target reliability."""
|
||||
token_str = token.token if hasattr(token, 'token') else str(token)
|
||||
result = await run_rbd_simulation(
|
||||
sim_hours=payload.duration,
|
||||
oh_duration=payload.oh_duration,
|
||||
oh_session_id=payload.oh_session_id,
|
||||
db_session=db_session,
|
||||
token=token_str
|
||||
)
|
||||
return StandardResponse(
|
||||
data={"simulation_id": result.get("data")},
|
||||
message=result.get("message", "Simulation started successfully")
|
||||
)
|
||||
|
||||
|
||||
# @router.get("", response_model=StandardResponse[List[Dict]])
|
||||
# async def get_target_reliability(
|
||||
# db_session: DbSession,
|
||||
# scope_name: Optional[str] = Query(None),
|
||||
# eaf_threshold: float = Query(100),
|
||||
# ):
|
||||
# """Get all scope pagination."""
|
||||
# results = await get_all_target_reliability(
|
||||
# db_session=db_session, scope_name=scope_name, eaf_threshold=eaf_threshold
|
||||
# )
|
||||
|
||||
# return StandardResponse(
|
||||
# data=results,
|
||||
# message="Data retrieved successfully",
|
||||
# )
|
||||
|
||||
|
||||
@router.get("", response_model=StandardResponse[OptimizationResult])
|
||||
async def get_target_reliability(
|
||||
db_session: DbSession,
|
||||
token: Token,
|
||||
collector_db: CollectorDbSession,
|
||||
params: Annotated[TargetReliabiltiyQuery, Query()],
|
||||
# oh_session_id: Optional[str] = Query(None),
|
||||
# eaf_input: float = Query(99.8),
|
||||
# duration: int = Query(17520),
|
||||
# simulation_id: Optional[str] = Query(None),
|
||||
# cut_hours = Query(0)
|
||||
):
|
||||
"""Get all scope pagination."""
|
||||
@router.get('', response_model=StandardResponse[OptimizationResult], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
||||
async def get_target_reliability(db_session: DbSession, token: Token, collector_db: CollectorDbSession, params: Annotated[TargetReliabiltiyQuery, Query()]):
|
||||
oh_session_id = params.oh_session_id
|
||||
eaf_input = params.eaf_input
|
||||
duration = params.duration
|
||||
simulation_id = params.simulation_id
|
||||
cut_hours = params.cut_hours
|
||||
oh_duration = params.oh_duration
|
||||
|
||||
if not oh_session_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="oh_session_id is required",
|
||||
)
|
||||
|
||||
# results = await get_eaf_timeline(
|
||||
# db_session=db_session,
|
||||
# oh_session_id=oh_session_id,
|
||||
# eaf_input=eaf_input,
|
||||
# oh_duration=duration
|
||||
# )
|
||||
|
||||
if simulation_id:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='oh_session_id is required')
|
||||
if duration != 17520:
|
||||
if not simulation_id:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='Simulation ID is required for non-default duration. Please run simulation first.')
|
||||
try:
|
||||
temporal_client = await Client.connect(TEMPORAL_URL)
|
||||
handle = temporal_client.get_workflow_handle(f"simulation-{simulation_id}")
|
||||
handle = temporal_client.get_workflow_handle(f'simulation-{simulation_id}')
|
||||
desc = await handle.describe()
|
||||
status_name = desc.status.name
|
||||
|
||||
if status_name in ["RUNNING", "CONTINUED_AS_NEW"]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_425_TOO_EARLY,
|
||||
detail="Simulation is still running.",
|
||||
)
|
||||
elif status_name != "COMPLETED":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Simulation failed with status: {status_name}",
|
||||
)
|
||||
if status_name in ['RUNNING', 'CONTINUED_AS_NEW']:
|
||||
raise HTTPException(status_code=status.HTTP_425_TOO_EARLY, detail='Simulation is still running.')
|
||||
if status_name != 'COMPLETED':
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f'Simulation failed with status: {status_name}')
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
# Handle connection errors or invalid workflow IDs
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Simulation not found or error checking status: {str(e)}",
|
||||
)
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f'Simulation not found or error checking status: {str(e)}')
|
||||
else:
|
||||
if duration != 17520 or oh_duration != 1200:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Simulation ID is required for non-default duration or OH duration. Please run simulation first.",
|
||||
)
|
||||
simulation_id = TR_RBD_ID
|
||||
|
||||
|
||||
results = await get_simulation_results(
|
||||
simulation_id=simulation_id,
|
||||
token=token
|
||||
)
|
||||
|
||||
optimize_result = await identify_worst_eaf_contributors(
|
||||
simulation_result=results,
|
||||
target_eaf=eaf_input,
|
||||
db_session=db_session,
|
||||
oh_session_id=oh_session_id,
|
||||
collector_db=collector_db,
|
||||
simulation_id=simulation_id,
|
||||
duration=duration,
|
||||
po_duration=oh_duration,
|
||||
cut_hours=float(cut_hours)
|
||||
)
|
||||
|
||||
|
||||
return StandardResponse(
|
||||
data=optimize_result,
|
||||
message="Data retrieved successfully",
|
||||
)
|
||||
results = await get_simulation_results(simulation_id=simulation_id, token=token)
|
||||
optimize_result = await identify_worst_eaf_contributors(simulation_result=results, target_eaf=eaf_input, db_session=db_session, oh_session_id=oh_session_id, collector_db=collector_db, simulation_id=simulation_id, duration=duration, po_duration=1200, cut_hours=float(cut_hours))
|
||||
return StandardResponse(data=optimize_result, message='Data retrieved successfully')
|
||||
|
||||
@ -1,91 +1,49 @@
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
import random
|
||||
from typing import List, Optional
|
||||
from typing import Optional
|
||||
from temporalio.client import Client
|
||||
from src.config import TEMPORAL_URL
|
||||
|
||||
from src.config import TEMPORAL_URL, TR_RBD_ID
|
||||
|
||||
def generate_down_periods(start_date: datetime, end_date: datetime,
|
||||
num_periods: Optional[int] = None, min_duration: int = 3,
|
||||
max_duration: int = 7) -> list[tuple[datetime, datetime]]:
|
||||
"""
|
||||
Generate random system down periods within a date range.
|
||||
|
||||
Args:
|
||||
start_date (datetime): Start date of the overall period
|
||||
end_date (datetime): End date of the overall period
|
||||
num_periods (int, optional): Number of down periods to generate.
|
||||
If None, generates 1-3 periods randomly
|
||||
min_duration (int): Minimum duration of each down period in days
|
||||
max_duration (int): Maximum duration of each down period in days
|
||||
|
||||
Returns:
|
||||
list[tuple[datetime, datetime]]: List of (start_date, end_date) tuples
|
||||
for each down period
|
||||
"""
|
||||
def generate_down_periods(start_date: datetime, end_date: datetime, num_periods: Optional[int]=None, min_duration: int=3, max_duration: int=7) -> list[tuple[datetime, datetime]]:
|
||||
if num_periods is None:
|
||||
num_periods = random.randint(1, 3)
|
||||
|
||||
total_days = (end_date - start_date).days
|
||||
down_periods = []
|
||||
|
||||
# Generate random down periods
|
||||
for _ in range(num_periods):
|
||||
# Random duration for this period
|
||||
duration = random.randint(min_duration, max_duration)
|
||||
|
||||
# Ensure we don't exceed the total date range
|
||||
latest_possible_start = total_days - duration
|
||||
|
||||
if latest_possible_start < 0:
|
||||
continue
|
||||
|
||||
# Random start day within available range
|
||||
start_day = random.randint(0, latest_possible_start)
|
||||
period_start = start_date + timedelta(days=start_day)
|
||||
period_end = period_start + timedelta(days=duration)
|
||||
|
||||
# Check for overlaps with existing periods
|
||||
overlaps = any(
|
||||
(p_start <= period_end and period_start <= p_end)
|
||||
for p_start, p_end in down_periods
|
||||
)
|
||||
|
||||
overlaps = any((p_start <= period_end and period_start <= p_end for p_start, p_end in down_periods))
|
||||
if not overlaps:
|
||||
down_periods.append((period_start, period_end))
|
||||
|
||||
return sorted(down_periods)
|
||||
|
||||
|
||||
async def wait_for_workflow(simulation_id, max_retries=3):
|
||||
workflow_id = f"simulation-{simulation_id}" # use returned ID
|
||||
workflow_id = f'simulation-{simulation_id}'
|
||||
retries = 0
|
||||
temporal_client = await Client.connect(TEMPORAL_URL)
|
||||
|
||||
while True:
|
||||
try:
|
||||
handle = temporal_client.get_workflow_handle(workflow_id=workflow_id)
|
||||
desc = await handle.describe()
|
||||
status = desc.status.name
|
||||
|
||||
if status not in ["RUNNING", "CONTINUED_AS_NEW"]:
|
||||
print(f"✅ Workflow {workflow_id} finished with status: {status}")
|
||||
if status not in ['RUNNING', 'CONTINUED_AS_NEW']:
|
||||
print(f'✅ Workflow {workflow_id} finished with status: {status}')
|
||||
break
|
||||
|
||||
print(f"⏳ Workflow {workflow_id} still {status}, checking again in 10s...")
|
||||
|
||||
print(f'⏳ Workflow {workflow_id} still {status}, checking again in 10s...')
|
||||
except Exception as e:
|
||||
retries += 1
|
||||
if retries > max_retries:
|
||||
print(f"⚠️ Workflow {workflow_id} not found after {max_retries} retries, treating as done. Error: {e}")
|
||||
print(f'⚠️ Workflow {workflow_id} not found after {max_retries} retries, treating as done. Error: {e}')
|
||||
break
|
||||
else:
|
||||
print(f"⚠️ Workflow {workflow_id} not found (retry {retries}/{max_retries}), waiting 10s before retry...")
|
||||
await asyncio.sleep(10)
|
||||
continue
|
||||
|
||||
retries = 0 # reset retries if describe() worked
|
||||
print(f'⚠️ Workflow {workflow_id} not found (retry {retries}/{max_retries}), waiting 10s before retry...')
|
||||
await asyncio.sleep(10)
|
||||
continue
|
||||
retries = 0
|
||||
await asyncio.sleep(30)
|
||||
|
||||
return simulation_id
|
||||
return simulation_id
|
||||
|
||||
@ -0,0 +1 @@
|
||||
|
||||
@ -1,172 +1,70 @@
|
||||
from enum import Enum
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from sqlalchemy import (JSON, UUID, Boolean, Column, Float, ForeignKey,
|
||||
Integer, Numeric, String)
|
||||
from typing import Optional
|
||||
from sqlalchemy import JSON, UUID, Boolean, Column, Float, ForeignKey, Integer, String
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from src.database.core import Base, DbSession
|
||||
from src.models import DefaultMixin, IdentityMixin, TimeStampMixin, UUIDMixin
|
||||
|
||||
from src.models import DefaultMixin, IdentityMixin
|
||||
|
||||
class OverhaulReferenceType(str, Enum):
|
||||
SCOPE = "SCOPE"
|
||||
ASSET = "ASSET"
|
||||
|
||||
SCOPE = 'SCOPE'
|
||||
ASSET = 'ASSET'
|
||||
|
||||
class CalculationParam(Base, DefaultMixin, IdentityMixin):
|
||||
__tablename__ = "oh_ms_calculation_param"
|
||||
|
||||
__tablename__ = 'oh_ms_calculation_param'
|
||||
avg_failure_cost = Column(Float, nullable=False)
|
||||
overhaul_cost = Column(Float, nullable=False)
|
||||
|
||||
# Relationships
|
||||
calculation_data = relationship("CalculationData", back_populates="parameter")
|
||||
results = relationship("CalculationResult", back_populates="parameter")
|
||||
|
||||
# @classmethod
|
||||
# async def create_with_references(
|
||||
# cls,
|
||||
# db: DbSession,
|
||||
# avg_failure_cost: float,
|
||||
# overhaul_cost: float,
|
||||
# created_by: str,
|
||||
# # list of {"reference_type": OverhaulReferenceType, "reference_id": str}
|
||||
# references: List[dict]
|
||||
# ):
|
||||
# # Create parameter
|
||||
# param = cls(
|
||||
# avg_failure_cost=avg_failure_cost,
|
||||
# overhaul_cost=overhaul_cost,
|
||||
# created_by=created_by
|
||||
# )
|
||||
# db.add(param)
|
||||
# await db.flush() # Flush to get the param.id
|
||||
|
||||
# # Create reference links
|
||||
# for ref in references:
|
||||
# reference_link = ReferenceLink(
|
||||
# parameter_id=param.id,
|
||||
# overhaul_reference_type=ref["reference_type"],
|
||||
# reference_id=ref["reference_id"]
|
||||
# )
|
||||
# db.add(reference_link)
|
||||
|
||||
# await db.commit()
|
||||
# await db.refresh(param)
|
||||
# return param
|
||||
|
||||
calculation_data = relationship('CalculationData', back_populates='parameter')
|
||||
results = relationship('CalculationResult', back_populates='parameter')
|
||||
|
||||
class CalculationData(Base, DefaultMixin, IdentityMixin):
|
||||
__tablename__ = "oh_tr_calculation_data"
|
||||
|
||||
parameter_id = Column(
|
||||
UUID(as_uuid=True), ForeignKey("oh_ms_calculation_param.id"), nullable=True
|
||||
)
|
||||
overhaul_session_id = Column(
|
||||
UUID(as_uuid=True), ForeignKey("oh_ms_overhaul.id")
|
||||
)
|
||||
__tablename__ = 'oh_tr_calculation_data'
|
||||
parameter_id = Column(UUID(as_uuid=True), ForeignKey('oh_ms_calculation_param.id'), nullable=True)
|
||||
overhaul_session_id = Column(UUID(as_uuid=True), ForeignKey('oh_ms_overhaul.id'))
|
||||
optimum_oh_day = Column(Integer, nullable=True)
|
||||
|
||||
max_interval = Column(Integer, nullable=True)
|
||||
|
||||
rbd_simulation_id = Column(UUID(as_uuid=True), nullable=True)
|
||||
|
||||
optimum_analysis = Column(JSON, nullable=True)
|
||||
plant_results = Column(JSON, nullable=True)
|
||||
fleet_statistics = Column(JSON, nullable=True)
|
||||
analysis_metadata = Column(JSON, nullable=True)
|
||||
|
||||
session = relationship("OverhaulScope", lazy="raise")
|
||||
|
||||
parameter = relationship("CalculationParam", back_populates="calculation_data")
|
||||
|
||||
equipment_results = relationship(
|
||||
"CalculationEquipmentResult", lazy="raise", viewonly=True
|
||||
)
|
||||
|
||||
|
||||
results = relationship("CalculationResult", lazy="raise", viewonly=True)
|
||||
|
||||
session = relationship('OverhaulScope', lazy='raise')
|
||||
parameter = relationship('CalculationParam', back_populates='calculation_data')
|
||||
equipment_results = relationship('CalculationEquipmentResult', lazy='raise', viewonly=True)
|
||||
results = relationship('CalculationResult', lazy='raise', viewonly=True)
|
||||
|
||||
@classmethod
|
||||
async def create_with_param(
|
||||
cls,
|
||||
overhaul_session_id: str,
|
||||
db: DbSession,
|
||||
avg_failure_cost: Optional[float],
|
||||
overhaul_cost: Optional[float],
|
||||
created_by: str,
|
||||
params_id: Optional[UUID],
|
||||
):
|
||||
async def create_with_param(cls, overhaul_session_id: str, db: DbSession, avg_failure_cost: Optional[float], overhaul_cost: Optional[float], created_by: str, params_id: Optional[UUID]):
|
||||
if not params_id:
|
||||
# Create Params
|
||||
params = CalculationParam(
|
||||
avg_failure_cost=avg_failure_cost,
|
||||
overhaul_cost=overhaul_cost,
|
||||
created_by=created_by,
|
||||
)
|
||||
|
||||
params = CalculationParam(avg_failure_cost=avg_failure_cost, overhaul_cost=overhaul_cost, created_by=created_by)
|
||||
db.add(params)
|
||||
await db.flush()
|
||||
params_id = params.id
|
||||
|
||||
calculation_data = cls(
|
||||
overhaul_session_id=overhaul_session_id,
|
||||
created_by=created_by,
|
||||
parameter_id=params_id,
|
||||
)
|
||||
|
||||
calculation_data = cls(overhaul_session_id=overhaul_session_id, created_by=created_by, parameter_id=params_id)
|
||||
db.add(calculation_data)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(calculation_data)
|
||||
|
||||
return calculation_data
|
||||
|
||||
|
||||
class CalculationResult(Base, DefaultMixin):
|
||||
|
||||
__tablename__ = "oh_tr_calculation_result"
|
||||
|
||||
parameter_id = Column(
|
||||
UUID(as_uuid=True), ForeignKey("oh_ms_calculation_param.id"), nullable=False
|
||||
)
|
||||
calculation_data_id = Column(
|
||||
UUID(as_uuid=True), ForeignKey("oh_tr_calculation_data.id"), nullable=False
|
||||
)
|
||||
__tablename__ = 'oh_tr_calculation_result'
|
||||
parameter_id = Column(UUID(as_uuid=True), ForeignKey('oh_ms_calculation_param.id'), nullable=False)
|
||||
calculation_data_id = Column(UUID(as_uuid=True), ForeignKey('oh_tr_calculation_data.id'), nullable=False)
|
||||
day = Column(Integer, nullable=False)
|
||||
corrective_cost = Column(Float, nullable=False)
|
||||
overhaul_cost = Column(Float, nullable=False)
|
||||
num_failures = Column(Integer, nullable=False)
|
||||
|
||||
parameter = relationship("CalculationParam", back_populates="results")
|
||||
reference_link = relationship("CalculationData")
|
||||
|
||||
parameter = relationship('CalculationParam', back_populates='results')
|
||||
reference_link = relationship('CalculationData')
|
||||
|
||||
class CalculationEquipmentResult(Base, DefaultMixin):
|
||||
|
||||
__tablename__ = "oh_tr_calculation_equipment_result"
|
||||
|
||||
__tablename__ = 'oh_tr_calculation_equipment_result'
|
||||
corrective_costs = Column(JSON, nullable=False)
|
||||
overhaul_costs = Column(JSON, nullable=False)
|
||||
daily_failures = Column(JSON, nullable=False)
|
||||
is_actual = Column(JSON, nullable=True) # List of booleans
|
||||
procurement_costs = Column(JSON, nullable=False)
|
||||
location_tag = Column(String(255), nullable=False)
|
||||
material_cost = Column(Float, nullable=False)
|
||||
service_cost = Column(Float, nullable=False)
|
||||
calculation_data_id = Column(
|
||||
UUID(as_uuid=True), ForeignKey("oh_tr_calculation_data.id"), nullable=True
|
||||
)
|
||||
calculation_data_id = Column(UUID(as_uuid=True), ForeignKey('oh_tr_calculation_data.id'), nullable=True)
|
||||
optimum_day = Column(Integer, default=1)
|
||||
is_included = Column(Boolean, default=True)
|
||||
procurement_details = Column(JSON, nullable=True)
|
||||
is_initial = Column(Boolean, default=True)
|
||||
|
||||
master_equipment = relationship(
|
||||
"MasterEquipment",
|
||||
lazy="joined",
|
||||
primaryjoin="and_(CalculationEquipmentResult.location_tag == foreign(MasterEquipment.location_tag))",
|
||||
uselist=False, # Add this if it's a one-to-one relationship
|
||||
)
|
||||
master_equipment = relationship('MasterEquipment', lazy='joined', primaryjoin='and_(CalculationEquipmentResult.location_tag == foreign(MasterEquipment.location_tag))', uselist=False)
|
||||
|
||||
@ -1,230 +1,61 @@
|
||||
from typing import Annotated, List, Optional, Union
|
||||
|
||||
from fastapi import APIRouter
|
||||
from fastapi.params import Query
|
||||
import requests
|
||||
|
||||
from src import config
|
||||
from src.auth.service import CurrentUser, InternalKey, Token
|
||||
from src.config import DEFAULT_TC_ID
|
||||
from src.database.core import DbSession
|
||||
from src.models import StandardResponse
|
||||
|
||||
from .flows import (create_calculation, get_create_calculation_parameters,
|
||||
get_or_create_scope_equipment_calculation)
|
||||
from .schema import (CalculationResultsRead,
|
||||
CalculationSelectedEquipmentUpdate,
|
||||
CalculationTimeConstrainsCreate,
|
||||
CalculationTimeConstrainsParametersCreate,
|
||||
CalculationTimeConstrainsParametersRead,
|
||||
CalculationTimeConstrainsParametersRetrive,
|
||||
CalculationTimeConstrainsRead, CreateCalculationQuery, EquipmentResult, CalculationTimeConstrainsReadNoResult)
|
||||
from .service import (bulk_update_equipment, get_calculation_result,
|
||||
get_calculation_result_by_day, get_calculation_by_assetnum, get_all_calculations)
|
||||
from .flows import create_calculation, get_create_calculation_parameters, get_or_create_scope_equipment_calculation
|
||||
from .schema import CalculationResultsRead, CalculationSelectedEquipmentUpdate, CalculationTimeConstrainsCreate, CalculationTimeConstrainsParametersCreate, CalculationTimeConstrainsParametersRead, CalculationTimeConstrainsParametersRetrive, CalculationTimeConstrainsRead, CreateCalculationQuery, EquipmentResult, CalculationTimeConstrainsReadNoResult
|
||||
from .service import bulk_update_equipment, get_calculation_result, get_calculation_result_by_day, get_calculation_by_assetnum, get_all_calculations
|
||||
from src.database.core import CollectorDbSession
|
||||
|
||||
from src.auth.access_control import required_permission, OHPermission
|
||||
from src.overhaul_scope.model import OverhaulScope
|
||||
from fastapi import Depends
|
||||
from src.csrf_protect import csrf_protect
|
||||
router = APIRouter()
|
||||
get_calculation = APIRouter()
|
||||
get_calculation = APIRouter()
|
||||
|
||||
|
||||
@router.post(
|
||||
"", response_model=StandardResponse[Union[dict, CalculationTimeConstrainsRead]]
|
||||
)
|
||||
async def create_calculation_time_constrains(
|
||||
token: Token,
|
||||
db_session: DbSession,
|
||||
collector_db_session: CollectorDbSession,
|
||||
current_user: CurrentUser,
|
||||
calculation_time_constrains_in: CalculationTimeConstrainsParametersCreate,
|
||||
params: Annotated[CreateCalculationQuery, Query()],
|
||||
# scope_calculation_id: Optional[str] = Query(None),
|
||||
# with_results: Optional[int] = Query(0),
|
||||
# simulation_id = Query(None)
|
||||
):
|
||||
"""Save calculation time constrains Here"""
|
||||
@router.post('', response_model=StandardResponse[Union[dict, CalculationTimeConstrainsRead]], dependencies=[Depends(required_permission(OHPermission.CREATE, OverhaulScope))])
|
||||
async def create_calculation_time_constrains(token: Token, db_session: DbSession, collector_db_session: CollectorDbSession, current_user: CurrentUser, calculation_time_constrains_in: CalculationTimeConstrainsParametersCreate, params: Annotated[CreateCalculationQuery, Query()]):
|
||||
scope_calculation_id = params.scope_calculation_id
|
||||
with_results = params.with_results
|
||||
simulation_id = calculation_time_constrains_in.simulationRBDId
|
||||
|
||||
|
||||
simulation_id = params.simulation_id
|
||||
if scope_calculation_id:
|
||||
results = await get_or_create_scope_equipment_calculation(
|
||||
db_session=db_session,
|
||||
scope_calculation_id=scope_calculation_id,
|
||||
calculation_time_constrains_in=calculation_time_constrains_in,
|
||||
)
|
||||
results = await get_or_create_scope_equipment_calculation(db_session=db_session, scope_calculation_id=scope_calculation_id, calculation_time_constrains_in=calculation_time_constrains_in)
|
||||
else:
|
||||
results = await create_calculation(
|
||||
token=token,
|
||||
db_session=db_session,
|
||||
collector_db_session=collector_db_session,
|
||||
calculation_time_constrains_in=calculation_time_constrains_in,
|
||||
created_by=current_user.name,
|
||||
simulation_id=simulation_id
|
||||
)
|
||||
|
||||
return StandardResponse(data=results, message="Data created successfully")
|
||||
|
||||
|
||||
|
||||
|
||||
@router.get(
|
||||
"", response_model=StandardResponse[List[CalculationTimeConstrainsReadNoResult]]
|
||||
)
|
||||
async def get_all_simulation_calculations(
|
||||
db_session: DbSession,
|
||||
token: Token,
|
||||
current_user: CurrentUser,
|
||||
):
|
||||
"""Get all calculation time constrains Here"""
|
||||
|
||||
calculations = await get_all_calculations(
|
||||
db_session=db_session,
|
||||
)
|
||||
|
||||
return StandardResponse(
|
||||
data=calculations,
|
||||
message="Data retrieved successfully",
|
||||
)
|
||||
|
||||
|
||||
results = await create_calculation(token=token, db_session=db_session, collector_db_session=collector_db_session, calculation_time_constrains_in=calculation_time_constrains_in, created_by=current_user.name, simulation_id=simulation_id)
|
||||
return StandardResponse(data=results, message='Data created successfully')
|
||||
|
||||
@router.get('', response_model=StandardResponse[List[CalculationTimeConstrainsReadNoResult]], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
||||
async def get_all_simulation_calculations(db_session: DbSession, token: Token, current_user: CurrentUser):
|
||||
calculations = await get_all_calculations(db_session=db_session)
|
||||
return StandardResponse(data=calculations, message='Data retrieved successfully')
|
||||
|
||||
@router.get(
|
||||
"/parameters",
|
||||
response_model=StandardResponse[
|
||||
Union[
|
||||
CalculationTimeConstrainsParametersRetrive,
|
||||
CalculationTimeConstrainsParametersRead,
|
||||
]
|
||||
],
|
||||
)
|
||||
async def get_calculation_parameters(
|
||||
db_session: DbSession, calculation_id: Optional[str] = Query(default=None)
|
||||
):
|
||||
"""Get all calculation parameter."""
|
||||
@router.get('/parameters', response_model=StandardResponse[Union[CalculationTimeConstrainsParametersRetrive, CalculationTimeConstrainsParametersRead]], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
||||
async def get_calculation_parameters(db_session: DbSession, calculation_id: Optional[str]=Query(default=None)):
|
||||
parameters = await get_create_calculation_parameters(db_session=db_session, calculation_id=calculation_id)
|
||||
return StandardResponse(data=parameters, message='Data retrieved successfully')
|
||||
|
||||
parameters = await get_create_calculation_parameters(
|
||||
db_session=db_session, calculation_id=calculation_id
|
||||
)
|
||||
|
||||
return StandardResponse(
|
||||
data=parameters,
|
||||
message="Data retrieved successfully",
|
||||
)
|
||||
|
||||
|
||||
@get_calculation.get(
|
||||
"/{calculation_id}", response_model=StandardResponse[CalculationTimeConstrainsRead]
|
||||
)
|
||||
async def get_calculation_results(db_session: DbSession, calculation_id, token:InternalKey, include_risk_cost:int = Query(1, alias="risk_cost")):
|
||||
@get_calculation.get('/{calculation_id}', response_model=StandardResponse[CalculationTimeConstrainsRead], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
||||
async def get_calculation_results(db_session: DbSession, calculation_id, token: InternalKey, include_risk_cost: int=Query(1, alias='risk_cost')):
|
||||
if calculation_id == 'default':
|
||||
calculation_id = DEFAULT_TC_ID
|
||||
|
||||
results = await get_calculation_result(
|
||||
db_session=db_session, calculation_id=calculation_id, token=token, include_risk_cost=include_risk_cost
|
||||
)
|
||||
|
||||
# requests.post(f"{config.AUTH_SERVICE_API}/sign-out", headers={
|
||||
# "Authorization": f"Bearer {token}"
|
||||
# })
|
||||
|
||||
return StandardResponse(
|
||||
data=results,
|
||||
message="Data retrieved successfully",
|
||||
)
|
||||
results = await get_calculation_result(db_session=db_session, calculation_id=calculation_id, token=token, include_risk_cost=include_risk_cost)
|
||||
return StandardResponse(data=results, message='Data retrieved successfully')
|
||||
|
||||
@router.get(
|
||||
"/{calculation_id}/{assetnum}", response_model=StandardResponse[EquipmentResult]
|
||||
)
|
||||
@router.get('/{calculation_id}/{assetnum}', response_model=StandardResponse[EquipmentResult], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
||||
async def get_calculation_per_equipment(db_session: DbSession, calculation_id, assetnum):
|
||||
results = await get_calculation_by_assetnum(db_session=db_session, assetnum=assetnum, calculation_id=calculation_id)
|
||||
return StandardResponse(data=results, message='Data retrieved successfully')
|
||||
|
||||
results = await get_calculation_by_assetnum(
|
||||
db_session=db_session, assetnum=assetnum, calculation_id=calculation_id
|
||||
)
|
||||
|
||||
return StandardResponse(
|
||||
data=results,
|
||||
message="Data retrieved successfully",
|
||||
)
|
||||
|
||||
|
||||
@router.post('/{calculation_id}/simulation', response_model=StandardResponse[CalculationResultsRead], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
||||
async def get_simulation_result(db_session: DbSession, calculation_id, calculation_simuation_in: CalculationTimeConstrainsCreate):
|
||||
simulation_result = await get_calculation_result_by_day(db_session=db_session, calculation_id=calculation_id, simulation_day=calculation_simuation_in.intervalDays)
|
||||
return StandardResponse(data=simulation_result, message='Data retrieved successfully')
|
||||
|
||||
@router.post(
|
||||
"/{calculation_id}/simulation",
|
||||
response_model=StandardResponse[CalculationResultsRead],
|
||||
)
|
||||
async def get_simulation_result(
|
||||
db_session: DbSession,
|
||||
calculation_id,
|
||||
calculation_simuation_in: CalculationTimeConstrainsCreate,
|
||||
):
|
||||
simulation_result = await get_calculation_result_by_day(
|
||||
db_session=db_session,
|
||||
calculation_id=calculation_id,
|
||||
simulation_day=calculation_simuation_in.intervalDays,
|
||||
)
|
||||
|
||||
return StandardResponse(
|
||||
data=simulation_result, message="Data retrieved successfully"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/update/{calculation_id}", response_model=StandardResponse[List[str]])
|
||||
async def update_selected_equipment(
|
||||
db_session: DbSession,
|
||||
calculation_id,
|
||||
calculation_time_constrains_in: List[CalculationSelectedEquipmentUpdate],
|
||||
):
|
||||
@router.post('/update/{calculation_id}', response_model=StandardResponse[List[str]], dependencies=[Depends(required_permission(OHPermission.EDIT, OverhaulScope)), Depends(csrf_protect)])
|
||||
async def update_selected_equipment(db_session: DbSession, calculation_id, calculation_time_constrains_in: List[CalculationSelectedEquipmentUpdate]):
|
||||
if calculation_id == 'default':
|
||||
calculation_id = "3b9a73a2-bde6-418c-9e2f-19046f501a05"
|
||||
|
||||
results = await bulk_update_equipment(
|
||||
db=db_session,
|
||||
selected_equipments=calculation_time_constrains_in,
|
||||
calculation_data_id=calculation_id,
|
||||
)
|
||||
|
||||
return StandardResponse(
|
||||
data=results,
|
||||
message="Data retrieved successfully",
|
||||
)
|
||||
|
||||
@router.post("/{calculation_id}/refresh-spareparts", response_model=StandardResponse[dict])
|
||||
async def refresh_spareparts(
|
||||
db_session: DbSession,
|
||||
collector_db_session: CollectorDbSession,
|
||||
calculation_id: str,
|
||||
current_user: CurrentUser,
|
||||
):
|
||||
"""Refresh sparepart availability for an existing calculation"""
|
||||
from .service import refresh_spareparts_service
|
||||
|
||||
await refresh_spareparts_service(
|
||||
db_session=db_session,
|
||||
collector_db_session=collector_db_session,
|
||||
calculation_id=calculation_id
|
||||
)
|
||||
|
||||
return StandardResponse(data={}, message="Spareparts refreshed successfully")
|
||||
|
||||
|
||||
@router.post("/{calculation_id}/recalculate", response_model=StandardResponse[CalculationTimeConstrainsRead])
|
||||
async def recalculate_calculation_api(
|
||||
db_session: DbSession,
|
||||
collector_db_session: CollectorDbSession,
|
||||
calculation_id: str,
|
||||
token: Token,
|
||||
current_user: CurrentUser,
|
||||
):
|
||||
"""Recalculate an existing simulation with fresh data"""
|
||||
from .flows import recalculate_calculation
|
||||
|
||||
results = await recalculate_calculation(
|
||||
token=token,
|
||||
db_session=db_session,
|
||||
collector_db_session=collector_db_session,
|
||||
calculation_id=calculation_id
|
||||
)
|
||||
|
||||
return StandardResponse(data=results, message="Calculation updated with fresh data")
|
||||
calculation_id = '3b9a73a2-bde6-418c-9e2f-19046f501a05'
|
||||
results = await bulk_update_equipment(db=db_session, selected_equipments=calculation_time_constrains_in, calculation_data_id=calculation_id)
|
||||
return StandardResponse(data=results, message='Data retrieved successfully')
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,345 +1,134 @@
|
||||
import datetime
|
||||
import json
|
||||
|
||||
import pandas as pd
|
||||
import requests
|
||||
|
||||
from src.config import RBD_SERVICE_API
|
||||
|
||||
def get_months_between(start_date: datetime.datetime, end_date: datetime.datetime) -> int:
|
||||
"""
|
||||
Calculate number of months between two dates.
|
||||
"""
|
||||
months = (end_date.year - start_date.year) * 12 + (end_date.month - start_date.month)
|
||||
# Add 1 to include both start and end months
|
||||
return months
|
||||
|
||||
return (end_date.year - start_date.year) * 12 + (end_date.month - start_date.month)
|
||||
|
||||
def create_time_series_data(chart_data, max_hours=None):
|
||||
# Filter out ON_OH
|
||||
filtered_data = [d for d in chart_data if d["currentEvent"] != "ON_OH"]
|
||||
sorted_data = sorted(filtered_data, key=lambda x: x["cumulativeTime"])
|
||||
|
||||
filtered_data = [d for d in chart_data if d['currentEvent'] != 'ON_OH']
|
||||
sorted_data = sorted(filtered_data, key=lambda x: x['cumulativeTime'])
|
||||
if not sorted_data:
|
||||
return []
|
||||
|
||||
hourly_data = []
|
||||
current_state_index = 0
|
||||
current_flow_rate = sorted_data[0]["flowRate"]
|
||||
current_eq_status = sorted_data[0]["currentEQStatus"]
|
||||
|
||||
# Determine maximum bound (either given or from data)
|
||||
last_time = int(sorted_data[-1]["cumulativeTime"])
|
||||
current_flow_rate = sorted_data[0]['flowRate']
|
||||
current_eq_status = sorted_data[0]['currentEQStatus']
|
||||
last_time = int(sorted_data[-1]['cumulativeTime'])
|
||||
if max_hours is None:
|
||||
max_hours = last_time
|
||||
|
||||
for hour in range(0, max_hours + 1): # start from 0
|
||||
# Advance state if needed
|
||||
while (current_state_index < len(sorted_data) - 1 and
|
||||
hour >= sorted_data[current_state_index + 1]["cumulativeTime"]):
|
||||
for hour in range(0, max_hours + 1):
|
||||
while current_state_index < len(sorted_data) - 1 and hour >= sorted_data[current_state_index + 1]['cumulativeTime']:
|
||||
current_state_index += 1
|
||||
current_flow_rate = sorted_data[current_state_index]["flowRate"]
|
||||
current_eq_status = sorted_data[current_state_index]["currentEQStatus"]
|
||||
|
||||
hourly_data.append({
|
||||
"cumulativeTime": hour,
|
||||
"flowRate": current_flow_rate,
|
||||
"currentEQStatus": current_eq_status
|
||||
})
|
||||
|
||||
current_flow_rate = sorted_data[current_state_index]['flowRate']
|
||||
current_eq_status = sorted_data[current_state_index]['currentEQStatus']
|
||||
hourly_data.append({'cumulativeTime': hour, 'flowRate': current_flow_rate, 'currentEQStatus': current_eq_status})
|
||||
return hourly_data
|
||||
|
||||
|
||||
|
||||
def calculate_failures_per_month(hourly_data):
|
||||
"""
|
||||
Calculate the cumulative number of failures up to each month from hourly data.
|
||||
A failure is defined as when currentEQStatus = "OoS".
|
||||
Only counts the start of each failure period (transition from "Svc" to "OoS").
|
||||
|
||||
Args:
|
||||
hourly_data: List of dicts with 'hour', 'flowrate', and 'currentEQStatus' keys
|
||||
|
||||
Returns:
|
||||
List of dicts with 'month' and 'failures' keys (cumulative count)
|
||||
"""
|
||||
total_failures = 0
|
||||
previous_eq_status = None
|
||||
monthly_data = {}
|
||||
|
||||
for data_point in hourly_data:
|
||||
hour = data_point['cumulativeTime']
|
||||
hour = data_point['hour']
|
||||
current_eq_status = data_point['currentEQStatus']
|
||||
|
||||
# Calculate which month this hour belongs to (1-based)
|
||||
# Assuming 30 days per month = 720 hours per month
|
||||
month = ((hour - 1) // 720) + 1
|
||||
|
||||
# Check if this is the start of a failure (transition to "OoS")
|
||||
if current_eq_status == "OoS" and previous_eq_status is not None and previous_eq_status != "OoS":
|
||||
month = (hour - 1) // 720 + 1
|
||||
if current_eq_status == 'OoS' and previous_eq_status is not None and (previous_eq_status != 'OoS'):
|
||||
total_failures += 1
|
||||
|
||||
# Special case: if the very first data point is a failure
|
||||
elif current_eq_status == "OoS" and previous_eq_status is None:
|
||||
elif current_eq_status == 'OoS' and previous_eq_status is None:
|
||||
total_failures += 1
|
||||
|
||||
# Store the current cumulative count for this month
|
||||
monthly_data[month] = total_failures
|
||||
previous_eq_status = current_eq_status
|
||||
|
||||
# Convert to list format
|
||||
result = []
|
||||
if monthly_data:
|
||||
max_month = max(monthly_data.keys())
|
||||
for month in range(1, max_month + 1):
|
||||
result.append({
|
||||
'month': month,
|
||||
'failures': monthly_data.get(month, monthly_data.get(month-1, 0))
|
||||
})
|
||||
|
||||
result.append({'month': month, 'failures': monthly_data.get(month, monthly_data.get(month - 1, 0))})
|
||||
return result
|
||||
|
||||
|
||||
def calculate_oos_per_month(hourly_data):
|
||||
"""
|
||||
Calculate the discrete OOS hours for each month from hourly data.
|
||||
A failure is defined as when currentEQStatus = "OoS".
|
||||
|
||||
Args:
|
||||
hourly_data: List of dicts with 'cumulativeTime', 'flowRate', and 'currentEQStatus' keys
|
||||
|
||||
Returns:
|
||||
List of dicts with 'month' and 'oos_hours' keys (discrete monthly count)
|
||||
"""
|
||||
monthly_data = {}
|
||||
|
||||
for data_point in hourly_data:
|
||||
hour = data_point['cumulativeTime']
|
||||
current_eq_status = data_point['currentEQStatus']
|
||||
|
||||
# Calculate which month this hour belongs to (1-based)
|
||||
# Assuming 30 days per month = 720 hours per month
|
||||
month = ((hour - 1) // 720) + 1
|
||||
|
||||
if month not in monthly_data:
|
||||
monthly_data[month] = 0
|
||||
|
||||
if current_eq_status == "OoS":
|
||||
monthly_data[month] += 1
|
||||
|
||||
# Convert to list format
|
||||
result = []
|
||||
if monthly_data:
|
||||
max_month = max(monthly_data.keys())
|
||||
for month in range(1, max_month + 1):
|
||||
result.append({
|
||||
'month': month,
|
||||
'oos_hours': monthly_data.get(month, 0)
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
|
||||
|
||||
|
||||
import pandas as pd
|
||||
import datetime
|
||||
|
||||
import datetime
|
||||
import pandas as pd
|
||||
|
||||
async def plant_simulation_metrics(simulation_id: str, location_tag: str, max_interval, token, last_oh_date, use_location_tag: int = 1):
|
||||
"""Get failure predictions for equipment from simulation service"""
|
||||
calc_result_url = f"{RBD_SERVICE_API}/aeros/simulation/result/calc/{simulation_id}/{location_tag}"
|
||||
|
||||
try:
|
||||
response = requests.get(
|
||||
calc_result_url,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {token}",
|
||||
},
|
||||
timeout=30
|
||||
)
|
||||
response.raise_for_status()
|
||||
prediction_data = response.json()['data']
|
||||
except (requests.RequestException, ValueError) as e:
|
||||
raise Exception(str(e))
|
||||
|
||||
|
||||
return prediction_data
|
||||
|
||||
def analyze_monthly_metrics(timestamp_outs, start_date, max_flow_rate: float = 550):
|
||||
async def plant_simulation_metrics(simulation_id: str, location_tag: str, max_interval, token, last_oh_date, use_location_tag: int=1):
|
||||
calc_result_url = f'{RBD_SERVICE_API}/aeros/simulation/result/calc/{simulation_id}/{location_tag}'
|
||||
try:
|
||||
response = requests.get(calc_result_url, headers={'Content-Type': 'application/json', 'Authorization': f'Bearer {token}'}, timeout=30)
|
||||
response.raise_for_status()
|
||||
prediction_data = response.json()['data']
|
||||
except (requests.RequestException, ValueError) as e:
|
||||
raise Exception(str(e))
|
||||
return prediction_data
|
||||
|
||||
def analyze_monthly_metrics(timestamp_outs, start_date, max_flow_rate: float=550):
|
||||
if not timestamp_outs:
|
||||
return {}
|
||||
|
||||
df = pd.DataFrame(timestamp_outs)
|
||||
required_columns = ['cumulativeTime', 'currentEQStatus', 'flowRate']
|
||||
if not all(col in df.columns for col in required_columns):
|
||||
if not all((col in df.columns for col in required_columns)):
|
||||
return {}
|
||||
|
||||
start_oh = datetime.datetime(start_date.year, start_date.month, start_date.day)
|
||||
|
||||
# Actual datetime from cumulative hours
|
||||
df['datetime'] = df['cumulativeTime'].apply(lambda x: start_oh + datetime.timedelta(hours=x))
|
||||
df['month_year'] = df['datetime'].dt.to_period('M')
|
||||
|
||||
# Duration until next timestamp
|
||||
df['duration_hours'] = df['cumulativeTime'].shift(-1) - df['cumulativeTime']
|
||||
df['duration_hours'] = df['duration_hours'].fillna(0)
|
||||
|
||||
# Failure detection
|
||||
df['status_change'] = df['currentEQStatus'].shift() != df['currentEQStatus']
|
||||
df['failure'] = (df['currentEQStatus'] == 'OoS') & df['status_change']
|
||||
|
||||
# Cumulative tracking
|
||||
df['cumulative_failures'] = df['failure'].cumsum()
|
||||
df['cumulative_oos'] = (df['duration_hours'] * (df['currentEQStatus'] == 'OoS')).cumsum()
|
||||
|
||||
# Derating calculation
|
||||
# Derating = capacity reduction below max but not outage
|
||||
df['derating'] = (max_flow_rate - df['flowRate']).clip(lower=0)
|
||||
df['is_derated'] = (df['currentEQStatus'] == 'Svc') & (df['derating'] > 0)
|
||||
|
||||
# Equivalent Derated Hours (EFDH) → sum of derating * hours, then normalized by max capacity
|
||||
df['derated_mwh'] = df['derating'] * df['duration_hours']
|
||||
df['derated_hours_equivalent'] = df['derated_mwh'] / max_flow_rate
|
||||
|
||||
monthly_results = {}
|
||||
|
||||
for month_period, group in df.groupby('month_year', sort=True):
|
||||
month_str = str(month_period)
|
||||
monthly_results[month_str] = {}
|
||||
|
||||
# Failures
|
||||
monthly_results[month_str]['failures_count'] = int(group['failure'].sum())
|
||||
monthly_results[month_str]['cumulative_failures'] = int(group['cumulative_failures'].max())
|
||||
|
||||
# OOS hours
|
||||
oos_time = group.loc[group['currentEQStatus'] == 'OoS', 'duration_hours'].sum()
|
||||
monthly_results[month_str]['total_oos_hours'] = float(oos_time)
|
||||
monthly_results[month_str]['cummulative_oos'] = float(group['cumulative_oos'].max())
|
||||
|
||||
# Flow rate (weighted average)
|
||||
total_flow_time = (group['flowRate'] * group['duration_hours']).sum()
|
||||
total_time = group['duration_hours'].sum()
|
||||
avg_flow_rate = total_flow_time / total_time if total_time > 0 else 0
|
||||
monthly_results[month_str]['avg_flow_rate'] = float(avg_flow_rate)
|
||||
|
||||
# Extra metrics
|
||||
monthly_results[month_str]['total_hours'] = float(total_time)
|
||||
service_hours = group.loc[group['currentEQStatus'] == 'Svc', 'duration_hours'].sum()
|
||||
monthly_results[month_str]['service_hours'] = float(service_hours)
|
||||
monthly_results[month_str]['availability_percentage'] = float(
|
||||
(service_hours / total_time * 100) if total_time > 0 else 0
|
||||
)
|
||||
|
||||
# Derating metrics
|
||||
monthly_results[month_str]['availability_percentage'] = float(service_hours / total_time * 100 if total_time > 0 else 0)
|
||||
derating_hours = group.loc[group['is_derated'], 'duration_hours'].sum()
|
||||
derated_mwh = group['derated_mwh'].sum()
|
||||
equivalent_derated_hours = group['derated_hours_equivalent'].sum()
|
||||
|
||||
monthly_results[month_str]['derating_hours'] = float(derating_hours)
|
||||
monthly_results[month_str]['derated_mwh'] = float(derated_mwh)
|
||||
monthly_results[month_str]['equivalent_derated_hours'] = float(equivalent_derated_hours)
|
||||
|
||||
return monthly_results
|
||||
|
||||
|
||||
def calculate_risk_cost_per_failure(monthly_results, birnbaum_importance, energy_price):
|
||||
"""
|
||||
Calculate risk cost per failure for each month based on:
|
||||
1. Equipment capacity contribution to system (flowrate * birnbaum_importance * availability)
|
||||
2. Capacity lost to downtime per month
|
||||
3. Energy price
|
||||
|
||||
Parameters:
|
||||
- monthly_results: Output from analyze_monthly_metrics()
|
||||
- birnbaum_importance: Birnbaum importance factor for this equipment
|
||||
- energy_price: Price per unit of energy/flow
|
||||
|
||||
Returns:
|
||||
- Dictionary with monthly risk costs and array of risk costs per failure
|
||||
"""
|
||||
|
||||
risk_costs = {}
|
||||
risk_cost_array = []
|
||||
|
||||
for month, data in monthly_results.items():
|
||||
# Extract monthly data
|
||||
avg_flow_rate = data['avg_flow_rate']
|
||||
availability = data['availability_percentage'] / 100 # Convert to decimal
|
||||
availability = data['availability_percentage'] / 100
|
||||
total_oos_hours = data['total_oos_hours']
|
||||
failures_count = data['failures_count']
|
||||
|
||||
# 1. Calculate equipment capacity contribution to system
|
||||
# Capacity = avg_flowrate * birnbaum_importance * availability
|
||||
equipment_capacity = avg_flow_rate * birnbaum_importance * availability
|
||||
|
||||
# 2. Calculate capacity lost to downtime per month
|
||||
# Lost capacity = avg_flowrate * birnbaum_importance * downtime_hours
|
||||
capacity_lost_to_downtime = avg_flow_rate * birnbaum_importance * total_oos_hours
|
||||
|
||||
# 3. Calculate total risk cost for the month
|
||||
# Risk cost = capacity_lost * energy_price
|
||||
monthly_risk_cost = capacity_lost_to_downtime * energy_price
|
||||
|
||||
# 4. Calculate risk cost per failure for this month
|
||||
if failures_count > 0:
|
||||
risk_cost_per_failure = monthly_risk_cost / failures_count
|
||||
else:
|
||||
# If no failures, set to 0 or use alternative approach
|
||||
risk_cost_per_failure = 0
|
||||
|
||||
# Store results
|
||||
risk_costs[month] = {
|
||||
'equipment_capacity': equipment_capacity,
|
||||
'capacity_lost_to_downtime': capacity_lost_to_downtime,
|
||||
'monthly_risk_cost': monthly_risk_cost,
|
||||
'failures_count': failures_count,
|
||||
'risk_cost_per_failure': risk_cost_per_failure
|
||||
}
|
||||
|
||||
# Add to array
|
||||
risk_costs[month] = {'equipment_capacity': equipment_capacity, 'capacity_lost_to_downtime': capacity_lost_to_downtime, 'monthly_risk_cost': monthly_risk_cost, 'failures_count': failures_count, 'risk_cost_per_failure': risk_cost_per_failure}
|
||||
risk_cost_array.append(risk_cost_per_failure)
|
||||
|
||||
return {
|
||||
'monthly_details': risk_costs,
|
||||
'risk_cost_per_failure_array': risk_cost_array
|
||||
}
|
||||
return {'monthly_details': risk_costs, 'risk_cost_per_failure_array': risk_cost_array}
|
||||
|
||||
# Example usage:
|
||||
def get_monthly_risk_analysis(timestamp_outs, birnbaum_importance, energy_price):
|
||||
"""
|
||||
Complete analysis combining monthly metrics with risk cost calculation
|
||||
"""
|
||||
# Get monthly metrics
|
||||
monthly_metrics = analyze_monthly_metrics(timestamp_outs)
|
||||
|
||||
# Calculate risk costs
|
||||
risk_analysis = calculate_risk_cost_per_failure(
|
||||
monthly_metrics,
|
||||
birnbaum_importance,
|
||||
energy_price
|
||||
)
|
||||
|
||||
# Combine results for comprehensive view
|
||||
risk_analysis = calculate_risk_cost_per_failure(monthly_metrics, birnbaum_importance, energy_price)
|
||||
combined_results = {}
|
||||
for month in monthly_metrics.keys():
|
||||
combined_results[month] = {
|
||||
**monthly_metrics[month],
|
||||
**risk_analysis['monthly_details'][month]
|
||||
}
|
||||
|
||||
return {
|
||||
'monthly_data': combined_results,
|
||||
'risk_cost_array': risk_analysis['risk_cost_per_failure_array']
|
||||
}
|
||||
|
||||
# Usage example:
|
||||
# birnbaum_importance = 0.85 # Example value
|
||||
# energy_price = 100 # Example: $100 per unit
|
||||
#
|
||||
# results = get_monthly_risk_analysis(timestamp_outs, birnbaum_importance, energy_price)
|
||||
# risk_cost_array = results['risk_cost_array']
|
||||
# print("Risk cost per failure each month:", risk_cost_array)
|
||||
combined_results[month] = {**monthly_metrics[month], **risk_analysis['monthly_details'][month]}
|
||||
return {'monthly_data': combined_results, 'risk_cost_array': risk_analysis['risk_cost_per_failure_array']}
|
||||
|
||||
@ -1,97 +1,64 @@
|
||||
import base64
|
||||
import logging
|
||||
import os
|
||||
from typing import List
|
||||
from urllib import parse
|
||||
|
||||
from pydantic import BaseModel
|
||||
from starlette.config import Config
|
||||
from starlette.datastructures import CommaSeparatedStrings
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseConfigurationModel(BaseModel):
|
||||
"""Base configuration model used by all config options."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def get_env_tags(tag_list: List[str]) -> dict:
|
||||
"""Create dictionary of available env tags."""
|
||||
tags = {}
|
||||
for t in tag_list:
|
||||
tag_key, env_key = t.split(":")
|
||||
|
||||
tag_key, env_key = t.split(':')
|
||||
env_value = os.environ.get(env_key)
|
||||
|
||||
if env_value:
|
||||
tags.update({tag_key: env_value})
|
||||
|
||||
return tags
|
||||
|
||||
|
||||
def get_config():
|
||||
try:
|
||||
# Try to load from .env file first
|
||||
config = Config(".env")
|
||||
config = Config('.env')
|
||||
except FileNotFoundError:
|
||||
# If .env doesn't exist, use environment variables
|
||||
config = Config(environ=os.environ)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
config = get_config()
|
||||
|
||||
|
||||
LOG_LEVEL = config("LOG_LEVEL", default="INFO")
|
||||
ENV = (os.getenv("ENV") or os.getenv("ENVIRONMENT") or os.getenv("ENVIRONEMENT") or "production").strip()
|
||||
DEBUG = 1 if (ENV.upper() in ("DEV", "DEVELOPMENT", "LOCAL") and "PROD" not in ENV.upper()) else 0
|
||||
PORT = config("PORT", cast=int, default=8000)
|
||||
HOST = config("HOST", default="localhost")
|
||||
|
||||
|
||||
# database
|
||||
DATABASE_HOSTNAME = config("DATABASE_HOSTNAME")
|
||||
_DATABASE_CREDENTIAL_USER = config("DATABASE_CREDENTIAL_USER")
|
||||
_DATABASE_CREDENTIAL_PASSWORD = config("DATABASE_CREDENTIAL_PASSWORD")
|
||||
LOG_LEVEL = config('LOG_LEVEL', default='INFO')
|
||||
SERVICE_NAME = config('SERVICE_NAME', default='Be-OptimumOH')
|
||||
ENV = config('ENV')
|
||||
PORT = config('PORT', cast=int)
|
||||
HOST = config('HOST')
|
||||
DATABASE_HOSTNAME = config('DATABASE_HOSTNAME')
|
||||
_DATABASE_CREDENTIAL_USER = config('DATABASE_CREDENTIAL_USER')
|
||||
_DATABASE_CREDENTIAL_PASSWORD = config('DATABASE_CREDENTIAL_PASSWORD')
|
||||
_QUOTED_DATABASE_PASSWORD = parse.quote(str(_DATABASE_CREDENTIAL_PASSWORD))
|
||||
DATABASE_NAME = config("DATABASE_NAME", default="digital_twin")
|
||||
DATABASE_PORT = config("DATABASE_PORT", default="5432")
|
||||
|
||||
DATABASE_ENGINE_POOL_SIZE = config("DATABASE_ENGINE_POOL_SIZE", cast=int, default=20)
|
||||
DATABASE_ENGINE_MAX_OVERFLOW = config(
|
||||
"DATABASE_ENGINE_MAX_OVERFLOW", cast=int, default=0
|
||||
)
|
||||
|
||||
COLLECTOR_HOSTNAME = config("COLLECTOR_HOSTNAME")
|
||||
COLLECTOR_PORT = config("COLLECTOR_PORT", default="5432")
|
||||
COLLECTOR_CREDENTIAL_USER = config("COLLECTOR_CREDENTIAL_USER")
|
||||
COLLECTOR_CREDENTIAL_PASSWORD = config("COLLECTOR_CREDENTIAL_PASSWORD")
|
||||
DATABASE_NAME = config('DATABASE_NAME')
|
||||
DATABASE_PORT = config('DATABASE_PORT')
|
||||
DATABASE_ENGINE_POOL_SIZE = config('DATABASE_ENGINE_POOL_SIZE', cast=int, default=20)
|
||||
DATABASE_ENGINE_MAX_OVERFLOW = config('DATABASE_ENGINE_MAX_OVERFLOW', cast=int, default=0)
|
||||
COLLECTOR_HOSTNAME = config('COLLECTOR_HOSTNAME')
|
||||
COLLECTOR_PORT = config('COLLECTOR_PORT', cast=int)
|
||||
COLLECTOR_CREDENTIAL_USER = config('COLLECTOR_CREDENTIAL_USER')
|
||||
COLLECTOR_CREDENTIAL_PASSWORD = config('COLLECTOR_CREDENTIAL_PASSWORD')
|
||||
QUOTED_COLLECTOR_CREDENTIAL_PASSWORD = parse.quote(str(COLLECTOR_CREDENTIAL_PASSWORD))
|
||||
COLLECTOR_NAME = config("COLLECTOR_NAME")
|
||||
|
||||
# Deal w
|
||||
SQLALCHEMY_DATABASE_URI = f"postgresql+asyncpg://{_DATABASE_CREDENTIAL_USER}:{_QUOTED_DATABASE_PASSWORD}@{DATABASE_HOSTNAME}:{DATABASE_PORT}/{DATABASE_NAME}"
|
||||
SQLALCHEMY_COLLECTOR_URI = f"postgresql+asyncpg://{COLLECTOR_CREDENTIAL_USER}:{QUOTED_COLLECTOR_CREDENTIAL_PASSWORD}@{COLLECTOR_HOSTNAME}:{COLLECTOR_PORT}/{COLLECTOR_NAME}"
|
||||
|
||||
|
||||
TIMEZONE = "Asia/Jakarta"
|
||||
|
||||
MAXIMO_BASE_URL = config("MAXIMO_BASE_URL", default="http://example.com")
|
||||
MAXIMO_API_KEY = config("MAXIMO_API_KEY", default="keys")
|
||||
|
||||
AUTH_SERVICE_API = config("AUTH_SERVICE_API", default="http://192.168.1.82:8000/auth")
|
||||
REALIBILITY_SERVICE_API = config("REALIBILITY_SERVICE_API", default="http://192.168.1.82:8000/reliability")
|
||||
RBD_SERVICE_API = config("RBD_SERVICE_API", default="http://192.168.1.82:8000/rbd")
|
||||
TEMPORAL_URL = config("TEMPORAL_URL", default="http://192.168.1.86:7233")
|
||||
|
||||
|
||||
API_KEY = config("API_KEY", default="0KFvcB7zWENyKVjoma9FKZNofVSViEshYr59zEQNGaYjyUP34gCJKDuqHuk9VfvE")
|
||||
|
||||
TR_RBD_ID = config("TR_RBD_ID", default="f04f365e-25d8-4036-87c2-ba1bfe1f9229")
|
||||
TC_RBD_ID = config("TC_RBD_ID", default="f8523cb0-dc3c-4edb-bcf1-eea7b62582f1")
|
||||
DEFAULT_TC_ID = config("DEFAULT_TC_ID", default="44f483f3-bfe4-4094-a59f-b97a10f2fea6")
|
||||
|
||||
TEMPORAL_URL
|
||||
COLLECTOR_NAME = config('COLLECTOR_NAME')
|
||||
SQLALCHEMY_DATABASE_URI = f'postgresql+asyncpg://{_DATABASE_CREDENTIAL_USER}:{_QUOTED_DATABASE_PASSWORD}@{DATABASE_HOSTNAME}:{DATABASE_PORT}/{DATABASE_NAME}'
|
||||
SQLALCHEMY_COLLECTOR_URI = f'postgresql+asyncpg://{COLLECTOR_CREDENTIAL_USER}:{QUOTED_COLLECTOR_CREDENTIAL_PASSWORD}@{COLLECTOR_HOSTNAME}:{COLLECTOR_PORT}/{COLLECTOR_NAME}'
|
||||
TIMEZONE = config('TIMEZONE')
|
||||
MAXIMO_BASE_URL = config('MAXIMO_BASE_URL')
|
||||
MAXIMO_API_KEY = config('MAXIMO_API_KEY')
|
||||
AUTH_SERVICE_API = config('AUTH_SERVICE_API')
|
||||
REALIBILITY_SERVICE_API = config('REALIBILITY_SERVICE_API')
|
||||
RBD_SERVICE_API = config('RBD_SERVICE_API')
|
||||
TEMPORAL_URL = config('TEMPORAL_URL')
|
||||
BASE_URL = config('BASE_URL')
|
||||
API_KEY = config('API_KEY')
|
||||
TR_RBD_ID = config('TR_RBD_ID')
|
||||
TC_RBD_ID = config('TC_RBD_ID')
|
||||
DEFAULT_TC_ID = config('DEFAULT_TC_ID')
|
||||
REDIS_HOST = config('REDIS_HOST')
|
||||
REDIS_PORT = config('REDIS_PORT', cast=int)
|
||||
RATELIMIT_STORAGE_URI = f'redis://{REDIS_HOST}:{REDIS_PORT}'
|
||||
REDIS_AUTH_DB = config('REDIS_AUTH_DB', cast=int)
|
||||
|
||||
@ -0,0 +1,36 @@
|
||||
import hashlib
|
||||
import logging
|
||||
import secrets
|
||||
import redis
|
||||
from fastapi import HTTPException, Request
|
||||
import src.config as config
|
||||
from src.context import get_user_id
|
||||
log = logging.getLogger(__name__)
|
||||
_redis_auth = redis.Redis(host=config.REDIS_HOST, port=int(config.REDIS_PORT), db=config.REDIS_AUTH_DB)
|
||||
|
||||
async def csrf_protect(request: Request) -> None:
|
||||
user = getattr(request.state, 'user', None)
|
||||
user_id = str(user.user_id) if user else get_user_id()
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail='Authentication required')
|
||||
csrf_token_header = request.headers.get('x-csrf-token') or request.headers.get('x-xsrf-token') or request.headers.get('x-csrf-token')
|
||||
if not csrf_token_header:
|
||||
log.warning('[CSRF] Missing X-CSRF-Token header | user_id=%s | IP: %s', user_id, request.client.host if request.client else 'unknown')
|
||||
raise HTTPException(status_code=403, detail='CSRF token missing in header')
|
||||
csrf_token_header = csrf_token_header.strip()
|
||||
current_path = request.url.path
|
||||
path_hash = hashlib.sha256(current_path.encode()).hexdigest()[:16]
|
||||
redis_key = f'csrf:{user_id}:{path_hash}'
|
||||
stored_token_hash = _redis_auth.get(redis_key)
|
||||
if not stored_token_hash:
|
||||
log.warning('[CSRF] Token not found in Redis | user_id=%s | path=%s | IP: %s', user_id, current_path, request.client.host if request.client else 'unknown')
|
||||
raise HTTPException(status_code=403, detail='CSRF token expired or invalid')
|
||||
if isinstance(stored_token_hash, bytes):
|
||||
stored_token_hash = stored_token_hash.decode('utf-8')
|
||||
incoming_hash = hashlib.sha256(csrf_token_header.encode()).hexdigest()
|
||||
if not secrets.compare_digest(incoming_hash, stored_token_hash):
|
||||
log.warning('[CSRF] Token mismatch | user_id=%s | path=%s | IP: %s', user_id, current_path, request.client.host if request.client else 'unknown')
|
||||
_redis_auth.delete(redis_key)
|
||||
raise HTTPException(status_code=403, detail='CSRF token verification failed')
|
||||
_redis_auth.delete(redis_key)
|
||||
log.info('[CSRF] Verified & deleted | user_id=%s | path=%s', user_id, current_path)
|
||||
@ -0,0 +1 @@
|
||||
|
||||
@ -1,22 +1,18 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from pydantic import Field
|
||||
from src.models import DefultBase
|
||||
|
||||
|
||||
class CommonParams(DefultBase):
|
||||
# This ensures no extra query params are allowed
|
||||
current_user: Optional[str] = Field(None, alias="currentUser")
|
||||
page: int = Field(1, gt=0, lt=2147483647)
|
||||
items_per_page: int = Field(5, gt=0, le=50, multiple_of=5, alias="itemsPerPage")
|
||||
query_str: Optional[str] = Field(None, alias="q")
|
||||
filter_spec: Optional[str] = Field(None, alias="filter")
|
||||
sort_by: List[str] = Field(default_factory=list, alias="sortBy[]")
|
||||
descending: List[bool] = Field(default_factory=list, alias="descending[]")
|
||||
exclude: List[str] = Field(default_factory=list, alias="exclude[]")
|
||||
all_params: int = Field(0, alias="all")
|
||||
current_user: Optional[str] = Field(None, alias='currentUser')
|
||||
page: int = Field(1, gt=0, le=1000000)
|
||||
items_per_page: int = Field(5, gt=0, le=50, multiple_of=5, alias='itemsPerPage')
|
||||
query_str: Optional[str] = Field(None, alias='q')
|
||||
filter_spec: Optional[str] = Field(None, alias='filter')
|
||||
sort_by: List[str] = Field(default_factory=list, alias='sortBy[]')
|
||||
descending: List[bool] = Field(default_factory=list, alias='descending[]')
|
||||
exclude: List[str] = Field(default_factory=list, alias='exclude[]')
|
||||
all_params: int = Field(0, alias='all', ge=0, le=1000000)
|
||||
|
||||
# Property to mirror your original return dict's bool conversion
|
||||
@property
|
||||
def is_all(self) -> bool:
|
||||
return bool(self.all_params)
|
||||
return bool(self.all_params)
|
||||
|
||||
@ -1,149 +1,58 @@
|
||||
import logging
|
||||
from typing import Annotated, List, Type, TypeVar
|
||||
|
||||
from fastapi import Depends, Query
|
||||
from pydantic.types import Json, constr
|
||||
from sqlalchemy import Select, desc, func, or_
|
||||
from sqlalchemy.exc import ProgrammingError
|
||||
from sqlalchemy_filters import apply_pagination
|
||||
|
||||
from src.database.schema import CommonParams
|
||||
|
||||
from .core import DbSession
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
QueryStr = constr(pattern='^[ -~]+$', min_length=1)
|
||||
|
||||
# allows only printable characters
|
||||
QueryStr = constr(pattern=r"^[ -~]+$", min_length=1)
|
||||
|
||||
|
||||
def common_parameters(
|
||||
db_session: DbSession, # type: ignore
|
||||
current_user: QueryStr = Query(None, alias="currentUser"), # type: ignore
|
||||
page: int = Query(1, gt=0, lt=2147483647),
|
||||
items_per_page: int = Query(5, alias="itemsPerPage", gt=-2, lt=2147483647),
|
||||
query_str: QueryStr = Query(None, alias="q"), # type: ignore
|
||||
filter_spec: QueryStr = Query(None, alias="filter"), # type: ignore
|
||||
sort_by: List[str] = Query([], alias="sortBy[]"),
|
||||
descending: List[bool] = Query([], alias="descending[]"),
|
||||
exclude: List[str] = Query([], alias="exclude[]"),
|
||||
all: int = Query(0),
|
||||
# role: QueryStr = Depends(get_current_role),
|
||||
):
|
||||
return {
|
||||
"db_session": db_session,
|
||||
"page": page,
|
||||
"items_per_page": items_per_page,
|
||||
"query_str": query_str,
|
||||
"filter_spec": filter_spec,
|
||||
"sort_by": sort_by,
|
||||
"descending": descending,
|
||||
"current_user": current_user,
|
||||
"all": bool(all),
|
||||
# "role": role,
|
||||
}
|
||||
|
||||
|
||||
CommonParameters = Annotated[
|
||||
dict[str, int | str | DbSession | QueryStr | Json | List[str] | List[bool]] | bool,
|
||||
Depends(common_parameters),
|
||||
]
|
||||
|
||||
T = TypeVar("T", bound=CommonParams)
|
||||
def common_parameters(db_session: DbSession, current_user: QueryStr=Query(None, alias='currentUser'), page: int=Query(1, gt=0, le=1000000), items_per_page: int=Query(5, alias='itemsPerPage', gt=-2, le=1000000), query_str: QueryStr=Query(None, alias='q'), filter_spec: QueryStr=Query(None, alias='filter'), sort_by: List[str]=Query([], alias='sortBy[]'), descending: List[bool]=Query([], alias='descending[]'), exclude: List[str]=Query([], alias='exclude[]'), all: int=Query(0, ge=0, le=1000000)):
|
||||
return {'db_session': db_session, 'page': page, 'items_per_page': items_per_page, 'query_str': query_str, 'filter_spec': filter_spec, 'sort_by': sort_by, 'descending': descending, 'current_user': current_user, 'all': bool(all)}
|
||||
CommonParameters = Annotated[dict[str, int | str | DbSession | QueryStr | Json | List[str] | List[bool]] | bool, Depends(common_parameters)]
|
||||
T = TypeVar('T', bound=CommonParams)
|
||||
|
||||
def get_params_factory(model_type: Type[T]):
|
||||
async def wrapper(
|
||||
db_session: DbSession,
|
||||
params: Annotated[model_type, Query()] # type: ignore
|
||||
):
|
||||
|
||||
async def wrapper(db_session: DbSession, params: Annotated[model_type, Query()]):
|
||||
res = params.model_dump()
|
||||
return {
|
||||
"db_session": db_session,
|
||||
"all": params.is_all,
|
||||
**res
|
||||
}
|
||||
return {'db_session': db_session, 'all': params.is_all, **res}
|
||||
return wrapper
|
||||
|
||||
|
||||
def search(*, query_str: str, query: Query, model, sort=False):
|
||||
"""Perform a search based on the query."""
|
||||
search_model = model
|
||||
|
||||
if not query_str.strip():
|
||||
return query
|
||||
|
||||
search = []
|
||||
if hasattr(search_model, "search_vector"):
|
||||
if hasattr(search_model, 'search_vector'):
|
||||
vector = search_model.search_vector
|
||||
search.append(vector.op("@@")(func.tsq_parse(query_str)))
|
||||
|
||||
if hasattr(search_model, "name"):
|
||||
search.append(
|
||||
search_model.name.ilike(f"%{query_str}%"),
|
||||
)
|
||||
search.append(vector.op('@@')(func.tsq_parse(query_str)))
|
||||
if hasattr(search_model, 'name'):
|
||||
search.append(search_model.name.ilike(f'%{query_str}%'))
|
||||
search.append(search_model.name == query_str)
|
||||
|
||||
if not search:
|
||||
raise Exception(f"Search not supported for model: {model}")
|
||||
|
||||
raise Exception(f'Search not supported for model: {model}')
|
||||
query = query.filter(or_(*search))
|
||||
|
||||
if sort:
|
||||
query = query.order_by(desc(func.ts_rank_cd(vector, func.tsq_parse(query_str))))
|
||||
|
||||
return query.params(term=query_str)
|
||||
|
||||
|
||||
async def search_filter_sort_paginate(
|
||||
db_session: DbSession,
|
||||
model,
|
||||
query_str: str = None,
|
||||
filter_spec: str | dict | None = None,
|
||||
page: int = 1,
|
||||
items_per_page: int = 5,
|
||||
sort_by: List[str] = None,
|
||||
descending: List[bool] = None,
|
||||
current_user: str = None,
|
||||
exclude: List[str] = None,
|
||||
all: bool = False,
|
||||
):
|
||||
"""Common functionality for searching, filtering, sorting, and pagination."""
|
||||
# try:
|
||||
# Check if model is Select
|
||||
async def search_filter_sort_paginate(db_session: DbSession, model, query_str: str=None, filter_spec: str | dict | None=None, page: int=1, items_per_page: int=5, sort_by: List[str]=None, descending: List[bool]=None, current_user: str=None, exclude: List[str]=None, all: bool=False):
|
||||
if not isinstance(model, Select):
|
||||
query = Select(model)
|
||||
else:
|
||||
query = model
|
||||
|
||||
if query_str:
|
||||
sort = False if sort_by else True
|
||||
query = search(query_str=query_str, query=query, model=model, sort=sort)
|
||||
|
||||
# Get total count
|
||||
count_query = Select(func.count()).select_from(query.subquery())
|
||||
total = await db_session.scalar(count_query)
|
||||
|
||||
if all:
|
||||
result = await db_session.execute(query)
|
||||
items = result.scalars().all()
|
||||
|
||||
return {
|
||||
"items": items,
|
||||
"itemsPerPage": total,
|
||||
"page": 1,
|
||||
"total": total,
|
||||
"totalPages": 1,
|
||||
}
|
||||
|
||||
return {'items': items, 'itemsPerPage': total, 'page': 1, 'total': total, 'totalPages': 1}
|
||||
query = query.offset((page - 1) * items_per_page).limit(items_per_page)
|
||||
|
||||
result = await db_session.execute(query)
|
||||
items = result.scalars().all()
|
||||
|
||||
return {
|
||||
"items": items,
|
||||
"itemsPerPage": items_per_page,
|
||||
"page": page,
|
||||
"total": total,
|
||||
"totalPages": (total + items_per_page - 1) // items_per_page,
|
||||
}
|
||||
return {'items': items, 'itemsPerPage': items_per_page, 'page': page, 'total': total, 'totalPages': (total + items_per_page - 1) // items_per_page}
|
||||
|
||||
@ -1,24 +1,12 @@
|
||||
from enum import StrEnum
|
||||
|
||||
import sys
|
||||
if sys.version_info >= (3, 11):
|
||||
from enum import StrEnum
|
||||
else:
|
||||
from strenum import StrEnum
|
||||
|
||||
class OptimumOHEnum(StrEnum):
|
||||
"""
|
||||
A custom Enum class that extends StrEnum.
|
||||
|
||||
This class inherits all functionality from StrEnum, including
|
||||
string representation and automatic value conversion to strings.
|
||||
|
||||
Example:
|
||||
class Visibility(DispatchEnum):
|
||||
OPEN = "Open"
|
||||
RESTRICTED = "Restricted"
|
||||
|
||||
assert str(Visibility.OPEN) == "Open"
|
||||
"""
|
||||
|
||||
pass # No additional implementation needed
|
||||
|
||||
pass
|
||||
|
||||
class ResponseStatus(OptimumOHEnum):
|
||||
SUCCESS = "success"
|
||||
ERROR = "error"
|
||||
SUCCESS = 'success'
|
||||
ERROR = 'error'
|
||||
|
||||
@ -0,0 +1 @@
|
||||
|
||||
@ -1,21 +1,12 @@
|
||||
from sqlalchemy import UUID, Column, Float, ForeignKey, Integer, String
|
||||
from sqlalchemy.ext.hybrid import hybrid_property
|
||||
from sqlalchemy import UUID, Column, Float, ForeignKey, String
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from src.database.core import Base
|
||||
from src.models import DefaultMixin, IdentityMixin, TimeStampMixin
|
||||
from src.workorder.model import MasterWorkOrder
|
||||
|
||||
from src.models import DefaultMixin
|
||||
|
||||
class ScopeEquipmentPart(Base, DefaultMixin):
|
||||
__tablename__ = "oh_ms_scope_equipment_part"
|
||||
|
||||
__tablename__ = 'oh_ms_scope_equipment_part'
|
||||
required_stock = Column(Float, nullable=False, default=0)
|
||||
sparepart_id = Column(UUID(as_uuid=True), ForeignKey("oh_ms_sparepart.id"), nullable=False)
|
||||
sparepart_id = Column(UUID(as_uuid=True), ForeignKey('oh_ms_sparepart.id'), nullable=False)
|
||||
location_tag = Column(String, nullable=False)
|
||||
location_tag = Column(String, nullable=False)
|
||||
|
||||
part = relationship(
|
||||
"MasterSparePart",
|
||||
lazy="selectin",
|
||||
)
|
||||
part = relationship('MasterSparePart', lazy='selectin')
|
||||
|
||||
@ -1,27 +1,12 @@
|
||||
from typing import Dict, List
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query, status
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from src.database.core import CollectorDbSession
|
||||
from src.database.service import (CommonParameters, DbSession,
|
||||
search_filter_sort_paginate)
|
||||
from src.models import StandardResponse
|
||||
|
||||
from .schema import (ScopeEquipmentActivityCreate,
|
||||
ScopeEquipmentActivityPagination,
|
||||
ScopeEquipmentActivityRead, ScopeEquipmentActivityUpdate)
|
||||
from src.auth.access_control import require_any_role, ALLOWED_ROLES
|
||||
from .service import get_all
|
||||
router = APIRouter(dependencies=[Depends(require_any_role(*ALLOWED_ROLES))])
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/{location_tag}", response_model=StandardResponse[List[Dict]])
|
||||
@router.get('/{location_tag}', response_model=StandardResponse[List[Dict]])
|
||||
async def get_scope_equipment_parts(collector_db_session: CollectorDbSession, location_tag):
|
||||
"""Get all scope activity pagination."""
|
||||
# return
|
||||
data = await get_all(db_session=collector_db_session, location_tag=location_tag)
|
||||
|
||||
return StandardResponse(
|
||||
data=data,
|
||||
message="Data retrieved successfully",
|
||||
)
|
||||
return StandardResponse(data=data, message='Data retrieved successfully')
|
||||
|
||||
@ -1,218 +1,29 @@
|
||||
import random
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import Delete, Select, and_, text
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from src.auth.service import CurrentUser
|
||||
from src.database.core import CollectorDbSession, DbSession
|
||||
from src.database.service import CommonParameters, search_filter_sort_paginate
|
||||
|
||||
from .model import ScopeEquipmentPart
|
||||
from .schema import ScopeEquipmentActivityCreate, ScopeEquipmentActivityUpdate
|
||||
|
||||
# async def get(*, db_session: DbSession, scope_equipment_activity_id: str) -> Optional[ScopeEquipmentActivity]:
|
||||
# """Returns a document based on the given document id."""
|
||||
# result = await db_session.get(ScopeEquipmentActivity, scope_equipment_activity_id)
|
||||
# return result
|
||||
|
||||
from sqlalchemy import text
|
||||
from typing import Optional, List, Dict, Any
|
||||
from sqlalchemy.ext.asyncio import AsyncSession as DbSession
|
||||
from sqlalchemy.sql import text
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# async def get_all(
|
||||
# db_session: CollectorDbSession,
|
||||
# location_tag: Optional[str] = None,
|
||||
# start_year: int = 2023,
|
||||
# end_year: Optional[int] = None,
|
||||
# parent_wonum: Optional[str] = None
|
||||
# ) -> List[Dict[str, Any]]:
|
||||
# """
|
||||
# Retrieve overhaul spare parts consumption data.
|
||||
# Handles missing data, null parent WO, and query safety.
|
||||
|
||||
# Args:
|
||||
# db_session: Async SQLAlchemy session
|
||||
# location_tag: Optional location filter
|
||||
# start_year: Year to start analysis (default 2023)
|
||||
# end_year: Optional year to end analysis (default start_year + 1)
|
||||
# parent_wonum: Parent work order number (required for context)
|
||||
|
||||
# Returns:
|
||||
# List of dictionaries with spare part usage per overhaul WO.
|
||||
# """
|
||||
|
||||
# # --- 1. Basic validation ---
|
||||
# if not parent_wonum:
|
||||
# logger.warning("Parent WO number not provided. Returning empty result.")
|
||||
# return []
|
||||
|
||||
# if start_year < 1900 or (end_year and end_year < start_year):
|
||||
# raise ValueError("Invalid year range provided.")
|
||||
|
||||
# if end_year is None:
|
||||
# end_year = start_year + 1
|
||||
|
||||
# # --- 2. Build SQL safely ---
|
||||
# base_query = """
|
||||
# WITH filtered_wo AS (
|
||||
# SELECT wonum, location_tag
|
||||
# FROM public.wo_max
|
||||
# WHERE worktype = 'OH'
|
||||
# AND xx_parent = :parent_wonum
|
||||
# """
|
||||
|
||||
# params = {
|
||||
# "parent_wonum": parent_wonum,
|
||||
# }
|
||||
|
||||
# if location_tag:
|
||||
# base_query += " AND location_tag = :location_tag"
|
||||
# params["location_tag"] = location_tag
|
||||
|
||||
# base_query += """
|
||||
# ),
|
||||
# filtered_materials AS (
|
||||
# SELECT wonum, itemnum, itemqty, inv_curbaltotal, inv_avgcost
|
||||
# FROM public.wo_max_material
|
||||
# WHERE wonum IN (SELECT wonum FROM filtered_wo)
|
||||
# )
|
||||
# SELECT
|
||||
# fwo.location_tag AS location_tag,
|
||||
# fm.itemnum,
|
||||
# spl.description AS sparepart_name,
|
||||
# COALESCE(SUM(fm.itemqty), 0) AS parts_consumed_in_oh,
|
||||
# COALESCE(AVG(fm.inv_avgcost), 0) AS avgcost,
|
||||
# COALESCE(AVG(fm.inv_curbaltotal), 0) AS inv_curbaltotal
|
||||
# FROM filtered_wo fwo
|
||||
# INNER JOIN filtered_materials fm ON fwo.wonum = fm.wonum
|
||||
# LEFT JOIN public.maximo_sparepart_pr_po_line spl ON fm.itemnum = spl.item_num
|
||||
# GROUP BY fwo.location_tag, fm.itemnum, spl.description
|
||||
# ORDER BY fwo.location_tag, fm.itemnum;
|
||||
# """
|
||||
|
||||
# # --- 3. Execute query ---
|
||||
# try:
|
||||
# result = await db_session.execute(text(base_query), params)
|
||||
# rows = result.fetchall()
|
||||
|
||||
# # Handle "no data found"
|
||||
# if not rows:
|
||||
# logger.info(f"No spare part data found for parent WO {parent_wonum}.")
|
||||
# return []
|
||||
|
||||
# # --- 4. Map results cleanly ---
|
||||
# equipment_parts = []
|
||||
# for row in rows:
|
||||
# try:
|
||||
# equipment_parts.append({
|
||||
# "location_tag": row.location_tag,
|
||||
# "itemnum": row.itemnum,
|
||||
# "sparepart_name": row.sparepart_name or "-",
|
||||
# "parts_consumed_in_oh": float(row.parts_consumed_in_oh or 0),
|
||||
# "avgcost": float(row.avgcost or 0),
|
||||
# "inv_curbaltotal": float(row.inv_curbaltotal or 0)
|
||||
# })
|
||||
# except Exception as parse_err:
|
||||
# logger.error(f"Failed to parse row {row}: {parse_err}")
|
||||
# continue # Skip malformed rows
|
||||
|
||||
# return equipment_parts
|
||||
|
||||
# except Exception as e:
|
||||
# logger.exception(f"Database query failed: {e}")
|
||||
# raise RuntimeError("Failed to fetch overhaul spare parts data.") from e
|
||||
|
||||
from typing import List, Dict, Any, Optional
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.sql import text
|
||||
|
||||
|
||||
async def get_all(
|
||||
db_session: AsyncSession,
|
||||
location_tag: Optional[str] = None,
|
||||
start_year: int = 2023,
|
||||
end_year: Optional[int] = None
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get overhaul spare parts consumption data with optimized query.
|
||||
|
||||
Args:
|
||||
db_session: SQLAlchemy async database session
|
||||
location_tag: Optional filter for location (asset_location)
|
||||
start_year: Starting year (default: 2023)
|
||||
end_year: Ending year (default: start_year + 1)
|
||||
|
||||
Returns:
|
||||
List of dictionaries with spare parts consumption data
|
||||
"""
|
||||
# Set default end year
|
||||
async def get_all(db_session: AsyncSession, location_tag: Optional[str]=None, start_year: int=2023, end_year: Optional[int]=None) -> List[Dict[str, Any]]:
|
||||
if end_year is None:
|
||||
end_year = start_year + 1
|
||||
|
||||
# Build query dynamically
|
||||
query_str = """
|
||||
WITH filtered_wo AS (
|
||||
SELECT DISTINCT wonum, asset_location, asset_unit
|
||||
FROM public.wo_maximo ma
|
||||
WHERE ma.xx_parent IN ('155026', '155027', '155029', '155030')
|
||||
"""
|
||||
|
||||
query_str = "\n WITH filtered_wo AS (\n SELECT DISTINCT wonum, asset_location, asset_unit\n FROM public.wo_maximo ma\n WHERE ma.xx_parent IN ('155026', '155027', '155029', '155030')\n "
|
||||
params = {}
|
||||
|
||||
# Optional filter for location
|
||||
if location_tag:
|
||||
query_str += " AND asset_location = :location_tag"
|
||||
params["location_tag"] = location_tag
|
||||
|
||||
query_str += """
|
||||
),
|
||||
filtered_materials AS (
|
||||
SELECT
|
||||
mat.wonum,
|
||||
mat.itemnum,
|
||||
mat.itemqty,
|
||||
mat.inv_curbaltotal AS inv_curbaltotal,
|
||||
mat.inv_avgcost AS inv_avgcost
|
||||
FROM public.wo_maximo_material AS mat
|
||||
WHERE mat.wonum IN (SELECT wonum FROM filtered_wo)
|
||||
)
|
||||
SELECT
|
||||
fwo.asset_location AS location_tag,
|
||||
ft.itemnum,
|
||||
COALESCE(spl.description, 'Unknown') AS sparepart_name,
|
||||
AVG(ft.itemqty) AS total_parts_used,
|
||||
COALESCE(AVG(ft.inv_avgcost), 0) AS avg_cost,
|
||||
COALESCE(AVG(ft.inv_curbaltotal), 0) AS avg_inventory_balance
|
||||
FROM filtered_wo AS fwo
|
||||
INNER JOIN filtered_materials AS ft
|
||||
ON fwo.wonum = ft.wonum
|
||||
LEFT JOIN public.maximo_sparepart_pr_po_line AS spl
|
||||
ON ft.itemnum = spl.item_num
|
||||
GROUP BY fwo.asset_location, ft.itemnum, spl.description
|
||||
ORDER BY fwo.asset_location, ft.itemnum;
|
||||
"""
|
||||
|
||||
query_str += ' AND asset_location = :location_tag'
|
||||
params['location_tag'] = location_tag
|
||||
query_str += "\n ),\n filtered_materials AS (\n SELECT \n mat.wonum,\n mat.itemnum,\n mat.itemqty,\n mat.inv_curbaltotal AS inv_curbaltotal,\n mat.inv_avgcost AS inv_avgcost\n FROM public.wo_maximo_material AS mat\n WHERE mat.wonum IN (SELECT wonum FROM filtered_wo)\n )\n SELECT \n fwo.asset_location AS location_tag,\n ft.itemnum,\n COALESCE(spl.description, 'Unknown') AS sparepart_name,\n AVG(ft.itemqty) AS total_parts_used,\n COALESCE(AVG(ft.inv_avgcost), 0) AS avg_cost,\n COALESCE(AVG(ft.inv_curbaltotal), 0) AS avg_inventory_balance\n FROM filtered_wo AS fwo\n INNER JOIN filtered_materials AS ft \n ON fwo.wonum = ft.wonum\n LEFT JOIN public.maximo_sparepart_pr_po_line AS spl \n ON ft.itemnum = spl.item_num\n GROUP BY fwo.asset_location, ft.itemnum, spl.description\n ORDER BY fwo.asset_location, ft.itemnum;\n "
|
||||
try:
|
||||
result = await db_session.execute(text(query_str), params)
|
||||
rows = result.fetchall()
|
||||
|
||||
equipment_parts = []
|
||||
for row in rows:
|
||||
equipment_parts.append({
|
||||
"location_tag": row.location_tag,
|
||||
"itemnum": row.itemnum,
|
||||
"sparepart_name": row.sparepart_name,
|
||||
"parts_consumed_in_oh": float(row.total_parts_used or 0),
|
||||
"avg_cost": float(row.avg_cost or 0),
|
||||
"inv_curbaltotal": float(row.avg_inventory_balance or 0),
|
||||
})
|
||||
|
||||
equipment_parts.append({'location_tag': row.location_tag, 'itemnum': row.itemnum, 'sparepart_name': row.sparepart_name, 'parts_consumed_in_oh': float(row.total_parts_used or 0), 'avg_cost': float(row.avg_cost or 0), 'inv_curbaltotal': float(row.avg_inventory_balance or 0)})
|
||||
return equipment_parts
|
||||
|
||||
except Exception as e:
|
||||
print(f"[get_all] Database query error: {e}")
|
||||
print(f'[get_all] Database query error: {e}')
|
||||
raise
|
||||
|
||||
@ -0,0 +1 @@
|
||||
|
||||
@ -1,22 +1,11 @@
|
||||
from sqlalchemy import UUID, Column, Float, ForeignKey, Integer, String
|
||||
from sqlalchemy.ext.hybrid import hybrid_property
|
||||
from sqlalchemy import UUID, Column, ForeignKey, String
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from src.database.core import Base
|
||||
from src.models import DefaultMixin, IdentityMixin, TimeStampMixin
|
||||
from src.workorder.model import MasterWorkOrder
|
||||
|
||||
from src.models import DefaultMixin
|
||||
|
||||
class EquipmentWorkscopeGroup(Base, DefaultMixin):
|
||||
__tablename__ = "oh_tr_equipment_workscope_group"
|
||||
|
||||
__tablename__ = 'oh_tr_equipment_workscope_group'
|
||||
workscope_group_id = Column(UUID(as_uuid=True), ForeignKey('oh_ms_workscope_group.id'))
|
||||
location_tag = Column(String, nullable=False)
|
||||
|
||||
workscope_group = relationship("MasterActivity", lazy="selectin", back_populates="equipment_workscope_groups")
|
||||
equipment = relationship(
|
||||
"StandardScope",
|
||||
lazy="raise",
|
||||
primaryjoin="and_(EquipmentWorkscopeGroup.location_tag == foreign(StandardScope.location_tag))",
|
||||
uselist=False, # Add this if it's a one-to-one relationship
|
||||
)
|
||||
workscope_group = relationship('MasterActivity', lazy='selectin', back_populates='equipment_workscope_groups')
|
||||
equipment = relationship('StandardScope', lazy='raise', primaryjoin='and_(EquipmentWorkscopeGroup.location_tag == foreign(StandardScope.location_tag))', uselist=False)
|
||||
|
||||
@ -1,51 +1,22 @@
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query, status
|
||||
|
||||
from src.database.service import (CommonParameters, DbSession,
|
||||
search_filter_sort_paginate)
|
||||
from typing import Optional
|
||||
from fastapi import APIRouter, Query
|
||||
from src.database.service import CommonParameters, DbSession
|
||||
from src.models import StandardResponse
|
||||
|
||||
from .schema import ScopeEquipmentJobCreate, ScopeEquipmentJobPagination
|
||||
from .service import create, delete, get_all
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{location_tag}", response_model=StandardResponse[ScopeEquipmentJobPagination]
|
||||
)
|
||||
async def get_jobs(common: CommonParameters, location_tag: str, scope: Optional[str] = Query(None)):
|
||||
"""Get all scope pagination."""
|
||||
# return
|
||||
@router.get('/{location_tag}', response_model=StandardResponse[ScopeEquipmentJobPagination])
|
||||
async def get_jobs(common: CommonParameters, location_tag: str, scope: Optional[str]=Query(None)):
|
||||
results = await get_all(common=common, location_tag=location_tag, scope=scope)
|
||||
return StandardResponse(data=results, message='Data retrieved successfully')
|
||||
|
||||
return StandardResponse(
|
||||
data=results,
|
||||
message="Data retrieved successfully",
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{assetnum}", response_model=StandardResponse[None])
|
||||
async def create_scope_equipment_jobs(
|
||||
db_session: DbSession, assetnum, scope_job_in: ScopeEquipmentJobCreate
|
||||
):
|
||||
"""Get all scope activity pagination."""
|
||||
# return
|
||||
@router.post('/{assetnum}', response_model=StandardResponse[None])
|
||||
async def create_scope_equipment_jobs(db_session: DbSession, assetnum, scope_job_in: ScopeEquipmentJobCreate):
|
||||
await create(db_session=db_session, assetnum=assetnum, scope_job_in=scope_job_in)
|
||||
return StandardResponse(data=None, message='Data created successfully')
|
||||
|
||||
return StandardResponse(
|
||||
data=None,
|
||||
message="Data created successfully",
|
||||
)
|
||||
|
||||
|
||||
@router.post("/delete/{scope_job_id}", response_model=StandardResponse[None])
|
||||
@router.post('/delete/{scope_job_id}', response_model=StandardResponse[None])
|
||||
async def delete_scope_equipment_job(db_session: DbSession, scope_job_id):
|
||||
|
||||
await delete(db_session=db_session, scope_job_id=scope_job_id)
|
||||
|
||||
return StandardResponse(
|
||||
data=None,
|
||||
message="Data deleted successfully",
|
||||
)
|
||||
return StandardResponse(data=None, message='Data deleted successfully')
|
||||
|
||||
@ -1,151 +1,46 @@
|
||||
import random
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
from sqlalchemy import Delete, Select, and_
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from src.auth.service import CurrentUser
|
||||
from sqlalchemy import Select
|
||||
from src.database.core import DbSession
|
||||
from src.database.service import CommonParameters, search_filter_sort_paginate
|
||||
from src.overhaul_activity.model import OverhaulActivity
|
||||
from src.database.service import search_filter_sort_paginate
|
||||
from src.standard_scope.model import MasterEquipment
|
||||
from src.standard_scope.service import get_equipment_level_by_no
|
||||
from src.workscope_group.model import MasterActivity
|
||||
from src.workscope_group_maintenance_type.model import WorkscopeOHType
|
||||
from src.overhaul_scope.model import MaintenanceType
|
||||
|
||||
from .model import EquipmentWorkscopeGroup
|
||||
from .schema import ScopeEquipmentJobCreate
|
||||
from src.soft_delete import apply_not_deleted_filter
|
||||
|
||||
# async def get(*, db_session: DbSession, scope_equipment_activity_id: str) -> Optional[ScopeEquipmentActivity]:
|
||||
# """Returns a document based on the given document id."""
|
||||
# result = await db_session.get(ScopeEquipmentActivity, scope_equipment_activity_id)
|
||||
# return result
|
||||
|
||||
|
||||
# async def get_all(db_session: DbSession, assetnum: Optional[str], common):
|
||||
# # Example usage
|
||||
# if not assetnum:
|
||||
# raise ValueError("assetnum parameter is required")
|
||||
|
||||
# # First get the parent equipment
|
||||
# equipment_stmt = Select(MasterEquipment).where(MasterEquipment.assetnum == assetnum)
|
||||
# equipment: MasterEquipment = await db_session.scalar(equipment_stmt)
|
||||
|
||||
# if not equipment:
|
||||
# raise ValueError(f"No equipment found with assetnum: {assetnum}")
|
||||
|
||||
# # Build query for parts
|
||||
# stmt = (
|
||||
# Select(ScopeEquipmentJob)
|
||||
# .where(ScopeEquipmentJob.assetnum == assetnum)
|
||||
# .options(
|
||||
# selectinload(ScopeEquipmentJob.job),
|
||||
# selectinload(ScopeEquipmentJob.overhaul_jobs)
|
||||
# .selectinload(OverhaulJob.overhaul_activity)
|
||||
# .selectinload(OverhaulActivity.overhaul_scope),
|
||||
# )
|
||||
# )
|
||||
|
||||
# results = await search_filter_sort_paginate(model=stmt, **common)
|
||||
|
||||
# return results
|
||||
|
||||
async def get_all(*, common, location_tag: str, scope: Optional[str] = None):
|
||||
"""Returns all documents."""
|
||||
query = (
|
||||
Select(EquipmentWorkscopeGroup)
|
||||
.where(EquipmentWorkscopeGroup.location_tag == location_tag).filter(EquipmentWorkscopeGroup.workscope_group_id.is_not(None))
|
||||
)
|
||||
|
||||
async def get_all(*, common, location_tag: str, scope: Optional[str]=None):
|
||||
query = Select(EquipmentWorkscopeGroup).where(EquipmentWorkscopeGroup.location_tag == location_tag).filter(EquipmentWorkscopeGroup.workscope_group_id.is_not(None))
|
||||
query = apply_not_deleted_filter(query, EquipmentWorkscopeGroup)
|
||||
if scope:
|
||||
query = (
|
||||
query
|
||||
.join(EquipmentWorkscopeGroup.workscope_group)
|
||||
.join(MasterActivity.oh_types)
|
||||
.join(WorkscopeOHType.oh_type)
|
||||
.filter(MaintenanceType.name == scope)
|
||||
)
|
||||
query = query.join(EquipmentWorkscopeGroup.workscope_group).join(MasterActivity.oh_types).join(WorkscopeOHType.oh_type).filter(MaintenanceType.name == scope)
|
||||
return await search_filter_sort_paginate(model=query, **common)
|
||||
|
||||
results = await search_filter_sort_paginate(model=query, **common)
|
||||
return results
|
||||
|
||||
|
||||
async def create(
|
||||
*, db_session: DbSession, assetnum, scope_job_in: ScopeEquipmentJobCreate
|
||||
):
|
||||
async def create(*, db_session: DbSession, assetnum, scope_job_in: ScopeEquipmentJobCreate):
|
||||
scope_jobs = []
|
||||
|
||||
if not assetnum:
|
||||
raise ValueError("assetnum parameter is required")
|
||||
|
||||
# First get the parent equipment
|
||||
raise ValueError('assetnum parameter is required')
|
||||
equipment_stmt = Select(MasterEquipment).where(MasterEquipment.assetnum == assetnum)
|
||||
equipment: MasterEquipment = await db_session.scalar(equipment_stmt)
|
||||
|
||||
if not equipment:
|
||||
raise ValueError(f"No equipment found with assetnum: {assetnum}")
|
||||
|
||||
raise ValueError(f'No equipment found with assetnum: {assetnum}')
|
||||
for job_id in scope_job_in.job_ids:
|
||||
scope_equipment_job = ScopeEquipmentJob(assetnum=assetnum, job_id=job_id)
|
||||
scope_jobs.append(scope_equipment_job)
|
||||
|
||||
db_session.add_all(scope_jobs)
|
||||
await db_session.commit()
|
||||
return
|
||||
|
||||
|
||||
# async def update(*, db_session: DbSession, activity: ScopeEquipmentActivity, scope_equipment_activty_in: ScopeEquipmentActivityUpdate):
|
||||
# """Updates a document."""
|
||||
# data = scope_equipment_activty_in.model_dump()
|
||||
|
||||
# update_data = scope_equipment_activty_in.model_dump(exclude_defaults=True)
|
||||
|
||||
# for field in data:
|
||||
# if field in update_data:
|
||||
# setattr(activity, field, update_data[field])
|
||||
|
||||
# await db_session.commit()
|
||||
|
||||
# return activity
|
||||
|
||||
|
||||
async def delete(
|
||||
*,
|
||||
db_session: DbSession,
|
||||
scope_job_id: int,
|
||||
) -> bool:
|
||||
"""
|
||||
Deletes a scope job and returns success status.
|
||||
|
||||
Args:
|
||||
db_session: Database session
|
||||
scope_job_id: ID of the scope job to delete
|
||||
user_id: ID of user performing the deletion
|
||||
|
||||
Returns:
|
||||
bool: True if deletion was successful, False otherwise
|
||||
|
||||
Raises:
|
||||
NotFoundException: If scope job doesn't exist
|
||||
AuthorizationError: If user lacks delete permission
|
||||
"""
|
||||
async def delete(*, db_session: DbSession, scope_job_id: int) -> bool:
|
||||
try:
|
||||
# Check if job exists
|
||||
scope_job = await db_session.get(ScopeEquipmentJob, scope_job_id)
|
||||
if not scope_job:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="A data with this id does not exist.",
|
||||
)
|
||||
|
||||
# Perform deletion
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='A data with this id does not exist.')
|
||||
await db_session.delete(scope_job)
|
||||
await db_session.commit()
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
await db_session.rollback()
|
||||
raise
|
||||
|
||||
@ -1,133 +0,0 @@
|
||||
import logging
|
||||
import json
|
||||
import datetime
|
||||
import os
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
from src.config import LOG_LEVEL
|
||||
from src.enums import OptimumOHEnum
|
||||
|
||||
|
||||
LOG_FORMAT_DEBUG = "%(levelname)s:%(message)s:%(pathname)s:%(funcName)s:%(lineno)d"
|
||||
|
||||
# ANSI Color Codes
|
||||
RESET = "\033[0m"
|
||||
COLORS = {
|
||||
"DEBUG": "\033[36m", # Cyan
|
||||
"INFO": "\033[32m", # Green
|
||||
"WARNING": "\033[33m", # Yellow
|
||||
"WARN": "\033[33m", # Yellow
|
||||
"ERROR": "\033[31m", # Red
|
||||
"CRITICAL": "\033[1;31m", # Bold Red
|
||||
}
|
||||
|
||||
|
||||
class LogLevels(OptimumOHEnum):
|
||||
info = "INFO"
|
||||
warn = "WARN"
|
||||
error = "ERROR"
|
||||
debug = "DEBUG"
|
||||
|
||||
|
||||
class JSONFormatter(logging.Formatter):
|
||||
"""
|
||||
Custom formatter to output logs in JSON format.
|
||||
"""
|
||||
def format(self, record):
|
||||
from src.context import get_request_id, get_user_id, get_username, get_role
|
||||
|
||||
request_id = None
|
||||
user_id = None
|
||||
username = None
|
||||
role = None
|
||||
|
||||
try:
|
||||
request_id = get_request_id()
|
||||
user_id = get_user_id()
|
||||
username = get_username()
|
||||
role = get_role()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Standard fields from requirements
|
||||
log_record = {
|
||||
"timestamp": datetime.datetime.fromtimestamp(record.created).strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"level": record.levelname,
|
||||
"message": record.getMessage(),
|
||||
}
|
||||
|
||||
# Add Context information if available
|
||||
if request_id:
|
||||
log_record["request_id"] = request_id
|
||||
|
||||
# Add Error context if available
|
||||
if hasattr(record, "error_id"):
|
||||
log_record["error_id"] = record.error_id
|
||||
elif "error_id" in record.__dict__:
|
||||
log_record["error_id"] = record.error_id
|
||||
|
||||
if user_id:
|
||||
log_record["user_id"] = user_id
|
||||
|
||||
# Add any extra attributes passed to the log call
|
||||
standard_attrs = {
|
||||
"args", "asctime", "created", "exc_info", "exc_text", "filename",
|
||||
"funcName", "levelname", "levelno", "lineno", "module", "msecs",
|
||||
"message", "msg", "name", "pathname", "process", "processName",
|
||||
"relativeCreated", "stack_info", "thread", "threadName", "error_id"
|
||||
}
|
||||
for key, value in record.__dict__.items():
|
||||
if key not in standard_attrs and not key.startswith("_"):
|
||||
log_record[key] = value
|
||||
|
||||
log_json = json.dumps(log_record)
|
||||
|
||||
# Apply color if the output is a terminal
|
||||
if sys.stdout.isatty():
|
||||
level_color = COLORS.get(record.levelname, "")
|
||||
return f"{level_color}{log_json}{RESET}"
|
||||
|
||||
return log_json
|
||||
|
||||
|
||||
def configure_logging():
|
||||
log_level = str(LOG_LEVEL).upper() # cast to string
|
||||
log_levels = list(LogLevels)
|
||||
|
||||
if log_level not in log_levels:
|
||||
log_level = LogLevels.error
|
||||
|
||||
# Get the root logger
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.setLevel(log_level)
|
||||
|
||||
# Clear existing handlers to avoid duplicate logs
|
||||
if root_logger.hasHandlers():
|
||||
root_logger.handlers.clear()
|
||||
|
||||
# Create a stream handler that outputs to stdout
|
||||
handler = logging.StreamHandler(sys.stdout)
|
||||
|
||||
# Use JSONFormatter for all environments, or could be conditional
|
||||
# For now, let's assume the user wants JSON everywhere as requested
|
||||
formatter = JSONFormatter()
|
||||
|
||||
# If debug mode is specifically requested and we want the old format for debug:
|
||||
# if log_level == LogLevels.debug:
|
||||
# formatter = logging.Formatter(LOG_FORMAT_DEBUG)
|
||||
|
||||
handler.setFormatter(formatter)
|
||||
root_logger.addHandler(handler)
|
||||
|
||||
# Reconfigure uvicorn loggers to use our JSON formatter
|
||||
for logger_name in ["uvicorn", "uvicorn.access", "uvicorn.error", "fastapi"]:
|
||||
logger = logging.getLogger(logger_name)
|
||||
logger.handlers = []
|
||||
logger.propagate = True
|
||||
|
||||
# Silence the chatty uvicorn access logs as we have custom middleware logging
|
||||
logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
|
||||
|
||||
# sometimes the slack client can be too verbose
|
||||
logging.getLogger("slack_sdk.web.base_client").setLevel(logging.CRITICAL)
|
||||
@ -1,247 +1,153 @@
|
||||
import logging
|
||||
import asyncio
|
||||
import time
|
||||
from src.context import set_request_id, reset_request_id, get_request_id
|
||||
from contextvars import ContextVar
|
||||
from os import path
|
||||
from typing import Final, Optional
|
||||
from uuid import uuid1
|
||||
|
||||
from fastapi import FastAPI, HTTPException, status
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi import Depends, FastAPI, HTTPException
|
||||
from src.auth.service import JWTBearer
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import ValidationError
|
||||
from slowapi import _rate_limit_exceeded_handler
|
||||
from slowapi.errors import RateLimitExceeded
|
||||
from slowapi.middleware import SlowAPIMiddleware
|
||||
from sqlalchemy import inspect
|
||||
from sqlalchemy.ext.asyncio import async_scoped_session
|
||||
from sqlalchemy.orm import scoped_session
|
||||
from starlette.middleware.base import (BaseHTTPMiddleware,
|
||||
RequestResponseEndpoint)
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from starlette.middleware.gzip import GZipMiddleware
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import FileResponse, Response, StreamingResponse
|
||||
from starlette.routing import compile_path
|
||||
from starlette.staticfiles import StaticFiles
|
||||
|
||||
from starlette.responses import Response
|
||||
import uuid as _uuid_module
|
||||
from src.api import api_router
|
||||
from src.database.core import async_session, engine, async_collector_session
|
||||
from src.database.core import async_session, async_collector_session
|
||||
from src.enums import ResponseStatus
|
||||
from src.exceptions import handle_exception
|
||||
from src.logging import configure_logging
|
||||
from src.middleware import RequestValidationMiddleware
|
||||
from src.rate_limiter import limiter
|
||||
from src.app_logging import configure_logging
|
||||
from src.middleware import RequestValidationMiddleware, RequestTimeoutMiddleware
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||
|
||||
from src.csrf_protect import csrf_protect
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# we configure the logging level and format
|
||||
configure_logging()
|
||||
|
||||
from src.config import DEBUG
|
||||
|
||||
# we create the ASGI for the app
|
||||
app = FastAPI(
|
||||
openapi_url="",
|
||||
title="LCCA API",
|
||||
description="Welcome to LCCA's API documentation!",
|
||||
version="0.1.0",
|
||||
debug=bool(DEBUG),
|
||||
)
|
||||
|
||||
# we define the exception handlers
|
||||
REQUEST_TIMEOUT_SECONDS = 20
|
||||
app = FastAPI(openapi_url='', title='Optimum OH API', description="Welcome to Optimum OH's API documentation!", version='0.1.0')
|
||||
|
||||
def _sync_rate_limit_handler(request: Request, exc: RateLimitExceeded) -> JSONResponse:
|
||||
error_id = str(_uuid_module.uuid4())
|
||||
request.state.log_status_code = 429
|
||||
request.state.log_error_id = error_id
|
||||
request.state.log_error_type = 'Too Many Requests'
|
||||
request.state.log_error_message = 'Please try again later'
|
||||
return JSONResponse(status_code=429, content={'data': None, 'message': f'Please try again later. Error ID: {error_id}', 'status': ResponseStatus.ERROR})
|
||||
app.add_exception_handler(Exception, handle_exception)
|
||||
app.add_exception_handler(HTTPException, handle_exception)
|
||||
app.add_exception_handler(StarletteHTTPException, handle_exception)
|
||||
app.add_exception_handler(RequestValidationError, handle_exception)
|
||||
app.add_exception_handler(RateLimitExceeded, handle_exception)
|
||||
|
||||
app.state.limiter = limiter
|
||||
app.add_exception_handler(RateLimitExceeded, _sync_rate_limit_handler)
|
||||
app.add_middleware(GZipMiddleware, minimum_size=1000)
|
||||
app.add_middleware(SlowAPIMiddleware)
|
||||
|
||||
# credentials: "include",
|
||||
|
||||
app.add_middleware(RequestTimeoutMiddleware, timeout=REQUEST_TIMEOUT_SECONDS)
|
||||
|
||||
@app.route('/slow', methods=['GET'])
|
||||
async def slow_endpoint(request: Request):
|
||||
await asyncio.sleep(REQUEST_TIMEOUT_SECONDS + 10)
|
||||
return JSONResponse(content={'message': 'This should timeout'}, status_code=200)
|
||||
|
||||
@app.post('/testcsrf', dependencies=[Depends(JWTBearer()), Depends(csrf_protect)])
|
||||
async def test_csrf(request: Request):
|
||||
return JSONResponse(content={'data': None, 'message': 'CSRF token is valid', 'status': ResponseStatus.SUCCESS}, status_code=200)
|
||||
|
||||
def security_headers_middleware(app: FastAPI):
|
||||
is_production = False
|
||||
csp_policy = {'default-src': "'self'", 'script-src': "'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net", 'style-src': "'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.jsdelivr.net", 'img-src': "'self' data: https: blob:", 'font-src': "'self' https://fonts.gstatic.com data:", 'connect-src': "'self' https://api.your-domain.com wss://ws.your-domain.com", 'frame-src': "'none'", 'object-src': "'none'", 'base-uri': "'self'", 'form-action': "'self'"}
|
||||
feature_policy = {'geolocation': "'none'", 'midi': "'none'", 'notifications': "'none'", 'push': "'none'", 'sync-xhr': "'none'", 'microphone': "'none'", 'camera': "'none'", 'magnetometer': "'none'", 'gyroscope': "'none'", 'speaker': "'none'", 'vibrate': "'none'", 'fullscreen': "'self'", 'payment': "'none'"}
|
||||
csp_header_value = '; '.join((f'{k} {v}' for k, v in csp_policy.items()))
|
||||
feature_header_value = '; '.join((f'{k}={v}' for k, v in feature_policy.items()))
|
||||
|
||||
# CSP rules
|
||||
csp_policy = {
|
||||
"default-src": "'self'",
|
||||
"script-src": "'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net",
|
||||
"style-src": "'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.jsdelivr.net",
|
||||
"img-src": "'self' data: https: blob:",
|
||||
"font-src": "'self' https://fonts.gstatic.com data:",
|
||||
"connect-src": "'self' https://api.your-domain.com wss://ws.your-domain.com",
|
||||
"frame-src": "'none'",
|
||||
"object-src": "'none'",
|
||||
"base-uri": "'self'",
|
||||
"form-action": "'self'",
|
||||
}
|
||||
|
||||
# Feature / Permissions Policy
|
||||
feature_policy = {
|
||||
"geolocation": "'none'",
|
||||
"midi": "'none'",
|
||||
"notifications": "'none'",
|
||||
"push": "'none'",
|
||||
"sync-xhr": "'none'",
|
||||
"microphone": "'none'",
|
||||
"camera": "'none'",
|
||||
"magnetometer": "'none'",
|
||||
"gyroscope": "'none'",
|
||||
"speaker": "'none'",
|
||||
"vibrate": "'none'",
|
||||
"fullscreen": "'self'",
|
||||
"payment": "'none'",
|
||||
}
|
||||
|
||||
csp_header_value = "; ".join(f"{k} {v}" for k, v in csp_policy.items())
|
||||
feature_header_value = "; ".join(f"{k}={v}" for k, v in feature_policy.items())
|
||||
|
||||
# Middleware definition
|
||||
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
|
||||
|
||||
async def dispatch(self, request: Request, call_next):
|
||||
response: Response = await call_next(request)
|
||||
|
||||
if is_production:
|
||||
response.headers["Strict-Transport-Security"] = "max-age=15724800; includeSubDomains; preload"
|
||||
response.headers["X-Frame-Options"] = "DENY"
|
||||
response.headers["X-Content-Type-Options"] = "nosniff"
|
||||
response.headers["X-XSS-Protection"] = "1; mode=block"
|
||||
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
|
||||
response.headers["Content-Security-Policy"] = csp_header_value
|
||||
response.headers["Permissions-Policy"] = feature_header_value
|
||||
response.headers['Strict-Transport-Security'] = 'max-age=15724800; includeSubDomains; preload'
|
||||
response.headers['X-Frame-Options'] = 'DENY'
|
||||
response.headers['X-Content-Type-Options'] = 'nosniff'
|
||||
response.headers['X-XSS-Protection'] = '1; mode=block'
|
||||
response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'
|
||||
response.headers['Content-Security-Policy'] = csp_header_value
|
||||
response.headers['Permissions-Policy'] = feature_header_value
|
||||
else:
|
||||
# Relaxed settings for development
|
||||
response.headers["Content-Security-Policy"] = "default-src 'self' 'unsafe-inline' 'unsafe-eval' *"
|
||||
# You can skip some headers here for local testing
|
||||
|
||||
response.headers['Content-Security-Policy'] = "default-src 'self' 'unsafe-inline' 'unsafe-eval' *"
|
||||
return response
|
||||
|
||||
app.add_middleware(SecurityHeadersMiddleware)
|
||||
|
||||
security_headers_middleware(app)
|
||||
|
||||
|
||||
app.add_middleware(RequestValidationMiddleware)
|
||||
|
||||
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
@app.middleware('http')
|
||||
async def db_session_middleware(request: Request, call_next):
|
||||
request_id = str(uuid1())
|
||||
|
||||
# we create a per-request id such that we can ensure that our session is scoped for a particular request.
|
||||
# see: https://github.com/tiangolo/fastapi/issues/726
|
||||
ctx_token = set_request_id(request_id)
|
||||
|
||||
request.state.request_id = request_id
|
||||
try:
|
||||
session = async_scoped_session(async_session, scopefunc=get_request_id)
|
||||
request.state.db = session()
|
||||
|
||||
collector_session = async_scoped_session(async_collector_session, scopefunc=get_request_id)
|
||||
request.state.collector_db = collector_session()
|
||||
|
||||
start_time = time.time()
|
||||
response = await call_next(request)
|
||||
process_time = (time.time() - start_time) * 1000
|
||||
|
||||
# Skip logging in middleware if it's an error (already logged in handle_exception)
|
||||
if response.status_code >= 400:
|
||||
return response
|
||||
|
||||
from src.context import get_username, get_role, get_user_id, set_user_id, set_username, set_role
|
||||
|
||||
# Pull from context or fallback to request.state.user
|
||||
username = get_username()
|
||||
role = get_role()
|
||||
try:
|
||||
response = await call_next(request)
|
||||
except Exception as exc:
|
||||
response = await handle_exception(request, exc)
|
||||
duration_ms = round((time.time() - start_time) * 1000, 2)
|
||||
timed_out = request.scope.get('x_timed_out', False)
|
||||
if timed_out:
|
||||
status_code = 408
|
||||
error_id = request.scope.get('x_timeout_error_id', None)
|
||||
error_type = 'Request Timeout'
|
||||
error_message = 'Request timeout over 20 seconds'
|
||||
else:
|
||||
status_code = getattr(request.state, 'log_status_code', None) or response.status_code
|
||||
error_id = getattr(request.state, 'log_error_id', None)
|
||||
error_type = getattr(request.state, 'log_error_type', None)
|
||||
error_message = getattr(request.state, 'log_error_message', None)
|
||||
_FALLBACK_ERROR_TYPES = {400: 'Bad Request', 401: 'Unauthorized', 403: 'Forbidden', 404: 'Not Found', 408: 'Request Timeout', 409: 'Conflict', 422: 'Unprocessable Entity', 429: 'Too Many Requests', 500: 'Internal Server Error'}
|
||||
_FALLBACK_ERROR_MESSAGES = {400: 'Bad request', 401: 'Invalid authorization token', 403: 'Forbidden Access', 404: 'Resource not found', 408: 'Request timeout', 409: 'This record already exists', 422: 'Input validation failed', 429: 'Please try again later', 500: 'An unexpected error occurred'}
|
||||
if status_code >= 400 and (not error_type):
|
||||
error_type = _FALLBACK_ERROR_TYPES.get(status_code, f'HTTP {status_code}')
|
||||
if status_code >= 400 and (not error_message):
|
||||
error_message = _FALLBACK_ERROR_MESSAGES.get(status_code, 'Request failed')
|
||||
from src.context import get_user_id, set_user_id
|
||||
user_id = get_user_id()
|
||||
|
||||
user_obj = getattr(request.state, "user", None)
|
||||
if user_obj:
|
||||
# message is UserBase dict/obj in this project
|
||||
user_obj = getattr(request.state, 'user', None)
|
||||
if user_obj and (not user_id):
|
||||
if isinstance(user_obj, dict):
|
||||
u_id = user_obj.get("user_id")
|
||||
u_name = user_obj.get("name") or user_obj.get("username")
|
||||
u_role = user_obj.get("role")
|
||||
u_id = user_obj.get('user_id')
|
||||
else:
|
||||
u_id = getattr(user_obj, "user_id", None)
|
||||
u_name = getattr(user_obj, "name", None) or getattr(user_obj, "username", None)
|
||||
u_role = getattr(user_obj, "role", None)
|
||||
|
||||
if not user_id and u_id:
|
||||
u_id = getattr(user_obj, 'user_id', None)
|
||||
if u_id:
|
||||
user_id = str(u_id)
|
||||
set_user_id(user_id)
|
||||
if not username and u_name:
|
||||
username = u_name
|
||||
set_username(username)
|
||||
if not role and u_role:
|
||||
role = u_role
|
||||
set_role(role)
|
||||
|
||||
user_info_str = ""
|
||||
if user_id:
|
||||
user_info_str = f" | User ID: {user_id}"
|
||||
|
||||
error_id = getattr(request.state, "error_id", None)
|
||||
log_msg = f"HTTP {request.method} {request.url.path} completed in {round(process_time, 2)}ms{user_info_str}"
|
||||
if error_id:
|
||||
log_msg += f" | Error ID: {error_id}"
|
||||
|
||||
log.info(
|
||||
log_msg,
|
||||
extra={
|
||||
"method": request.method,
|
||||
"path": request.url.path,
|
||||
"status_code": response.status_code,
|
||||
"duration_ms": round(process_time, 2),
|
||||
"user_id": user_id,
|
||||
"error_id": error_id,
|
||||
},
|
||||
)
|
||||
headers = request.headers
|
||||
if 'X-Real-IP' in headers:
|
||||
user_ip = headers['X-Real-IP']
|
||||
elif 'X-Forwarded-For' in headers:
|
||||
user_ip = headers['X-Forwarded-For'].split(',')[0].strip()
|
||||
else:
|
||||
user_ip = request.client.host if request.client else None
|
||||
path = request.url.path
|
||||
if request.url.query:
|
||||
path = f'{path}?{request.url.query}'
|
||||
result = 'Request completed' if status_code < 400 else 'Request failed'
|
||||
if status_code >= 500:
|
||||
log_level = logging.ERROR
|
||||
elif status_code >= 400:
|
||||
log_level = logging.WARNING
|
||||
else:
|
||||
log_level = logging.INFO
|
||||
log.log(log_level, f'{request.method} {path} {status_code}', extra={'method': request.method, 'path': path, 'status_code': status_code, 'duration_ms': duration_ms, 'request_id': request_id, 'user_id': user_id, 'user_ip': user_ip, 'error_id': error_id, 'result': result, 'error_type': error_type if status_code >= 400 else None, 'error_message': error_message if status_code >= 400 else None})
|
||||
finally:
|
||||
await request.state.db.close()
|
||||
await request.state.collector_db.close()
|
||||
|
||||
reset_request_id(ctx_token)
|
||||
return response
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
@app.middleware('http')
|
||||
async def add_security_headers(request: Request, call_next):
|
||||
response = await call_next(request)
|
||||
response.headers["Strict-Transport-Security"] = (
|
||||
"max-age=31536000 ; includeSubDomains"
|
||||
)
|
||||
response.headers['Strict-Transport-Security'] = 'max-age=31536000 ; includeSubDomains'
|
||||
return response
|
||||
|
||||
|
||||
# class MetricsMiddleware(BaseHTTPMiddleware):
|
||||
# async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
|
||||
# method = request.method
|
||||
# endpoint = request.url.path
|
||||
# tags = {"method": method, "endpoint": endpoint}
|
||||
|
||||
# try:
|
||||
# start = time.perf_counter()
|
||||
# response = await call_next(request)
|
||||
# elapsed_time = time.perf_counter() - start
|
||||
# tags.update({"status_code": response.status_code})
|
||||
# metric_provider.counter("server.call.counter", tags=tags)
|
||||
# metric_provider.timer("server.call.elapsed", value=elapsed_time, tags=tags)
|
||||
# log.debug(f"server.call.elapsed.{endpoint}: {elapsed_time}")
|
||||
# except Exception as e:
|
||||
# metric_provider.counter("server.call.exception.counter", tags=tags)
|
||||
# raise e from None
|
||||
# return response
|
||||
|
||||
|
||||
# app.add_middleware(ExceptionMiddleware)
|
||||
|
||||
app.include_router(api_router)
|
||||
|
||||
@ -0,0 +1 @@
|
||||
|
||||
@ -1,332 +1,37 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional, Union
|
||||
from sqlalchemy import select, func, cast, Numeric, text
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_
|
||||
from sqlalchemy.sql import not_
|
||||
from src.maximo.model import WorkOrderData # Assuming this is where your model is
|
||||
from sqlalchemy import select, text
|
||||
from src.database.core import CollectorDbSession, DbSession
|
||||
from src.overhaul_scope.model import OverhaulScope
|
||||
|
||||
async def get_cm_cost_summary(collector_db: CollectorDbSession, last_oh_date:datetime, upcoming_oh_date:datetime):
|
||||
query = text("""WITH part_costs AS (
|
||||
SELECT
|
||||
mu.wonum,
|
||||
SUM(mu.itemqty * COALESCE(inv.avgcost, po.unit_cost, 0)) AS parts_total_cost
|
||||
FROM maximo_workorder_materials mu
|
||||
LEFT JOIN maximo_inventory inv
|
||||
ON mu.itemnum = inv.itemnum
|
||||
LEFT JOIN (
|
||||
SELECT item_num, AVG(unit_cost) AS unit_cost
|
||||
FROM maximo_sparepart_pr_po_line
|
||||
GROUP BY item_num
|
||||
) po
|
||||
ON mu.itemnum = po.item_num
|
||||
GROUP BY mu.wonum
|
||||
),
|
||||
wo_costs AS (
|
||||
SELECT
|
||||
w.wonum,
|
||||
w.asset_location,
|
||||
(COALESCE(w.mat_cost_max, 0) + COALESCE(pc.parts_total_cost, 0)) AS total_wo_cost
|
||||
FROM wo_staging_maximo_2 w
|
||||
LEFT JOIN part_costs pc
|
||||
ON w.wonum = pc.wonum
|
||||
WHERE
|
||||
w.worktype IN ('CM', 'EM', 'PROACTIVE')
|
||||
AND w.asset_system IN (
|
||||
'HPB','AH','APC','SCR','CL','DM','CRH','ASH','BAD','DS','WTP',
|
||||
'MT','SUP','DCS','FF','EG','AI','SPS','EVM','SCW','KLH','CH',
|
||||
'TUR','LOT','HRH','ESP','CAE','GMC','BFT','LSH','CHB','BSS',
|
||||
'LOS','LPB','SAC','CP','EHS','RO','GG','MS','CW','SO','ATT',
|
||||
'AFG','EHB','RP','FO','PC','APE','AF','DMW','BRS','GEN','ABS',
|
||||
'CHA','TR','H2','BDW','LOM','ACR','AL','FW','COND','CCCW','IA',
|
||||
'GSS','BOL','SSB','CO','OA','CTH-UPD','AS','DP'
|
||||
)
|
||||
AND w.reportdate IS NOT NULL
|
||||
AND w.actstart IS NOT NULL
|
||||
AND w.actfinish IS NOT NULL
|
||||
AND w.asset_unit IN ('3','00')
|
||||
AND w.reportdate >= '2015-01-01'
|
||||
AND w.wonum NOT LIKE 'T%'
|
||||
),
|
||||
-- find max cost per location
|
||||
location_max AS (
|
||||
SELECT asset_location, MAX(total_wo_cost) AS max_cost
|
||||
FROM wo_costs
|
||||
WHERE total_wo_cost > 0
|
||||
GROUP BY asset_location
|
||||
),
|
||||
-- filter WO costs to only reasonable range (e.g. >0 and >=10% of max)
|
||||
filtered_wo AS (
|
||||
SELECT w.*
|
||||
FROM wo_costs w
|
||||
JOIN location_max lm ON w.asset_location = lm.asset_location
|
||||
WHERE w.total_wo_cost > 0
|
||||
)
|
||||
SELECT
|
||||
asset_location,
|
||||
SUM(total_wo_cost)::numeric / COUNT(wonum) AS avg_cost
|
||||
FROM filtered_wo
|
||||
GROUP BY asset_location
|
||||
ORDER BY avg_cost DESC;
|
||||
""")
|
||||
async def get_cm_cost_summary(collector_db: CollectorDbSession, last_oh_date: datetime, upcoming_oh_date: datetime):
|
||||
query = text("WITH part_costs AS (\n SELECT \n mu.wonum,\n SUM(mu.itemqty * COALESCE(inv.avgcost, po.unit_cost, 0)) AS parts_total_cost\n FROM maximo_workorder_materials mu\n LEFT JOIN maximo_inventory inv\n ON mu.itemnum = inv.itemnum\n LEFT JOIN (\n SELECT item_num, AVG(unit_cost) AS unit_cost\n FROM maximo_sparepart_pr_po_line\n GROUP BY item_num\n ) po\n ON mu.itemnum = po.item_num\n GROUP BY mu.wonum\n),\nwo_costs AS (\n SELECT \n w.wonum,\n w.asset_location,\n (COALESCE(w.mat_cost_max, 0) + COALESCE(pc.parts_total_cost, 0)) AS total_wo_cost\n FROM wo_staging_maximo_2 w\n LEFT JOIN part_costs pc\n ON w.wonum = pc.wonum\n WHERE \n w.worktype IN ('CM', 'EM', 'PROACTIVE')\n AND w.asset_system IN (\n 'HPB','AH','APC','SCR','CL','DM','CRH','ASH','BAD','DS','WTP',\n 'MT','SUP','DCS','FF','EG','AI','SPS','EVM','SCW','KLH','CH',\n 'TUR','LOT','HRH','ESP','CAE','GMC','BFT','LSH','CHB','BSS',\n 'LOS','LPB','SAC','CP','EHS','RO','GG','MS','CW','SO','ATT',\n 'AFG','EHB','RP','FO','PC','APE','AF','DMW','BRS','GEN','ABS',\n 'CHA','TR','H2','BDW','LOM','ACR','AL','FW','COND','CCCW','IA',\n 'GSS','BOL','SSB','CO','OA','CTH-UPD','AS','DP'\n )\n AND w.reportdate IS NOT NULL\n AND w.actstart IS NOT NULL\n AND w.actfinish IS NOT NULL\n AND w.asset_unit IN ('3','00')\n AND w.reportdate >= '2015-01-01'\n AND w.wonum NOT LIKE 'T%'\n),\n-- find max cost per location\nlocation_max AS (\n SELECT asset_location, MAX(total_wo_cost) AS max_cost\n FROM wo_costs\n WHERE total_wo_cost > 0\n GROUP BY asset_location\n),\n-- filter WO costs to only reasonable range (e.g. >0 and >=10% of max)\nfiltered_wo AS (\n SELECT w.*\n FROM wo_costs w\n JOIN location_max lm ON w.asset_location = lm.asset_location\n WHERE w.total_wo_cost > 0\n)\nSELECT \n asset_location,\n SUM(total_wo_cost)::numeric / COUNT(wonum) AS avg_cost\nFROM filtered_wo\nGROUP BY asset_location\nORDER BY avg_cost DESC;\n ")
|
||||
results = await collector_db.execute(query)
|
||||
data = []
|
||||
|
||||
for row in results:
|
||||
data.append({
|
||||
"location_tag": row.asset_location,
|
||||
"avg_cost": row.avg_cost
|
||||
})
|
||||
|
||||
data.append({'location_tag': row.asset_location, 'avg_cost': row.avg_cost})
|
||||
return {item['location_tag']: item['avg_cost'] for item in data}
|
||||
|
||||
return {
|
||||
item["location_tag"]: item["avg_cost"] for item in data
|
||||
}
|
||||
|
||||
|
||||
# async def get_oh_cost_summary(collector_db: CollectorDbSession, last_oh_date:datetime, upcoming_oh_date:datetime):
|
||||
# query = text("""
|
||||
# WITH target_wo AS (
|
||||
# -- Get work orders under a specific parent(s)
|
||||
# SELECT
|
||||
# wonum,
|
||||
# xx_parent,
|
||||
# assetnum,
|
||||
# location_tag AS asset_location,
|
||||
# actmatcost,
|
||||
# actservcost,
|
||||
# reportdate
|
||||
# FROM public.wo_maxim
|
||||
# WHERE xx_parent = ANY(:parent_nums)
|
||||
# ),
|
||||
# part_costs AS (
|
||||
# -- Calculate parts cost per WO if actmatcost = 0
|
||||
# SELECT
|
||||
# wm.wonum,
|
||||
# SUM(
|
||||
# wm.itemqty *
|
||||
# COALESCE(wm.inv_avgcost, po.unit_cost, 0)
|
||||
# ) AS parts_total_cost
|
||||
# FROM public.wo_maxim_material wm
|
||||
# LEFT JOIN (
|
||||
# SELECT item_num, AVG(unit_cost) AS unit_cost
|
||||
# FROM public.maximo_sparepart_pr_po_line
|
||||
# GROUP BY item_num
|
||||
# ) po ON wm.itemnum = po.item_num
|
||||
# WHERE wm.itemnum IS NOT NULL
|
||||
# GROUP BY wm.wonum
|
||||
# ),
|
||||
# wo_costs AS (
|
||||
# SELECT
|
||||
# w.wonum,
|
||||
# w.asset_location,
|
||||
# CASE
|
||||
# WHEN COALESCE(w.actmatcost, 0) > 0 THEN COALESCE(w.actmatcost, 0)
|
||||
# ELSE COALESCE(pc.parts_total_cost, 0)
|
||||
# END AS material_cost,
|
||||
# COALESCE(w.actservcost, 0) AS service_cost
|
||||
# FROM target_wo w
|
||||
# LEFT JOIN part_costs pc ON w.wonum = pc.wonum
|
||||
# )
|
||||
# SELECT
|
||||
# asset_location,
|
||||
# ROUND(SUM(material_cost + service_cost)::numeric / COUNT(wonum), 2) AS avg_cost,
|
||||
# COUNT(wonum) AS total_wo_count
|
||||
# FROM wo_costs
|
||||
# GROUP BY asset_location
|
||||
# ORDER BY total_wo_count DESC;
|
||||
# """)
|
||||
|
||||
# parent_nums = []
|
||||
|
||||
# result = await collector_db.execute(query, {"parent_nums": parent_nums})
|
||||
# data = []
|
||||
|
||||
# for row in result:
|
||||
# data.append({
|
||||
# "location_tag": row.asset_location,
|
||||
# "avg_cost": float(row.avg_cost or 0.0),
|
||||
# "total_wo_count": row.total_wo_count,
|
||||
# })
|
||||
|
||||
# return {item["location_tag"]: item["avg_cost"] for item in data}
|
||||
|
||||
|
||||
async def get_oh_cost_summary(collector_db: CollectorDbSession, last_oh_date:datetime, upcoming_oh_date:datetime):
|
||||
# query = text("""
|
||||
# WITH part_costs AS (
|
||||
# SELECT
|
||||
# wm.wonum,
|
||||
# SUM(wm.itemqty * COALESCE(wm.inv_avgcost, po.unit_cost, 0)) AS parts_total_cost
|
||||
# FROM public.wo_maxim_material wm
|
||||
# LEFT JOIN (
|
||||
# SELECT item_num, AVG(unit_cost) AS unit_cost
|
||||
# FROM public.maximo_sparepart_pr_po_line
|
||||
# GROUP BY item_num
|
||||
# ) po ON wm.itemnum = po.item_num
|
||||
# WHERE wm.itemnum IS NOT NULL
|
||||
# GROUP BY wm.wonum
|
||||
# ),
|
||||
# wo_costs AS (
|
||||
# SELECT
|
||||
# w.wonum,
|
||||
# w.asset_location,
|
||||
# -- Use mat_cost_max if parts_total_cost = 0
|
||||
# CASE
|
||||
# WHEN COALESCE(pc.parts_total_cost, 0) = 0 THEN COALESCE(w.mat_cost_max , 0)
|
||||
# ELSE COALESCE(pc.parts_total_cost, 0)
|
||||
# END AS total_wo_cost
|
||||
# FROM wo_staging_maximo_2 w
|
||||
# LEFT JOIN part_costs pc
|
||||
# ON w.wonum = pc.wonum
|
||||
# WHERE
|
||||
# w.worktype = 'OH'
|
||||
# AND w.reportdate IS NOT NULL
|
||||
# AND w.actstart IS NOT NULL
|
||||
# AND w.actfinish IS NOT NULL
|
||||
# AND w.asset_unit IN ('3', '00')
|
||||
# AND w.wonum NOT LIKE 'T%'
|
||||
# )
|
||||
# SELECT
|
||||
# asset_location,
|
||||
# AVG(total_wo_cost) AS avg_cost
|
||||
# FROM wo_costs
|
||||
# GROUP BY asset_location
|
||||
# ORDER BY COUNT(wonum) DESC;
|
||||
# """)
|
||||
|
||||
query = text("""
|
||||
WITH part_costs AS (
|
||||
SELECT
|
||||
wm.wonum,
|
||||
SUM(wm.itemqty * COALESCE(inv.avgcost, po.unit_cost, 0)) AS parts_total_cost
|
||||
FROM public.maximo_workorder_materials wm
|
||||
JOIN public.maximo_inventory AS inv on inv.itemnum = wm.itemnum
|
||||
LEFT JOIN (
|
||||
SELECT item_num, AVG(unit_cost) AS unit_cost
|
||||
FROM public.maximo_sparepart_pr_po_line
|
||||
GROUP BY item_num
|
||||
) po ON wm.itemnum = po.item_num
|
||||
WHERE wm.itemnum IS NOT NULL
|
||||
GROUP BY wm.wonum
|
||||
),
|
||||
wo_costs AS (
|
||||
SELECT
|
||||
w.wonum,
|
||||
w.asset_location,
|
||||
-- Use mat_cost_max if parts_total_cost = 0
|
||||
CASE
|
||||
WHEN COALESCE(pc.parts_total_cost, 0) = 0 THEN COALESCE(w.mat_cost_max , 0)
|
||||
ELSE COALESCE(pc.parts_total_cost, 0)
|
||||
END AS total_wo_cost
|
||||
FROM wo_staging_maximo_2 w
|
||||
LEFT JOIN part_costs pc
|
||||
ON w.wonum = pc.wonum
|
||||
WHERE
|
||||
w.worktype = 'OH'
|
||||
AND w.reportdate IS NOT NULL
|
||||
AND w.actstart IS NOT NULL
|
||||
AND w.actfinish IS NOT NULL
|
||||
AND w.asset_unit IN ('3', '00')
|
||||
AND w.wonum NOT LIKE 'T%'
|
||||
)
|
||||
SELECT
|
||||
asset_location,
|
||||
AVG(total_wo_cost) AS avg_cost
|
||||
FROM wo_costs
|
||||
GROUP BY asset_location
|
||||
ORDER BY COUNT(wonum) DESC;
|
||||
""")
|
||||
|
||||
async def get_oh_cost_summary(collector_db: CollectorDbSession, last_oh_date: datetime, upcoming_oh_date: datetime):
|
||||
query = text("\n WITH part_costs AS (\n SELECT \n wm.wonum,\n SUM(wm.itemqty * COALESCE(inv.avgcost, po.unit_cost, 0)) AS parts_total_cost\n FROM public.maximo_workorder_materials wm\n JOIN public.maximo_inventory AS inv on inv.itemnum = wm.itemnum\n LEFT JOIN (\n SELECT item_num, AVG(unit_cost) AS unit_cost\n FROM public.maximo_sparepart_pr_po_line\n GROUP BY item_num\n ) po ON wm.itemnum = po.item_num\n WHERE wm.itemnum IS NOT NULL\n GROUP BY wm.wonum\n ),\n wo_costs AS (\n SELECT \n w.wonum,\n w.asset_location,\n -- Use mat_cost_max if parts_total_cost = 0\n CASE \n WHEN COALESCE(pc.parts_total_cost, 0) = 0 THEN COALESCE(w.mat_cost_max , 0)\n ELSE COALESCE(pc.parts_total_cost, 0)\n END AS total_wo_cost\n FROM wo_staging_maximo_2 w\n LEFT JOIN part_costs pc\n ON w.wonum = pc.wonum\n WHERE \n w.worktype = 'OH'\n AND w.reportdate IS NOT NULL\n AND w.actstart IS NOT NULL\n AND w.actfinish IS NOT NULL\n AND w.asset_unit IN ('3', '00')\n AND w.wonum NOT LIKE 'T%'\n )\n SELECT \n asset_location,\n AVG(total_wo_cost) AS avg_cost\n FROM wo_costs\n GROUP BY asset_location\n ORDER BY COUNT(wonum) DESC;\n ")
|
||||
result = await collector_db.execute(query)
|
||||
data = []
|
||||
|
||||
for row in result:
|
||||
data.append({
|
||||
"location_tag": row.asset_location,
|
||||
"avg_cost": row.avg_cost
|
||||
})
|
||||
|
||||
return {
|
||||
item["location_tag"]: item["avg_cost"] for item in data
|
||||
}
|
||||
|
||||
|
||||
data.append({'location_tag': row.asset_location, 'avg_cost': row.avg_cost})
|
||||
return {item['location_tag']: item['avg_cost'] for item in data}
|
||||
from uuid import UUID
|
||||
|
||||
async def get_history_oh_wo(*, db_session: DbSession, collector_db_session: CollectorDbSession, oh_session_id: UUID, parent_wo_num: Optional[Union[str, list]] = None):
|
||||
## Get Parent wo num from oh session table
|
||||
async def get_history_oh_wo(*, db_session: DbSession, collector_db_session: CollectorDbSession, oh_session_id: UUID, parent_wo_num: Optional[Union[str, list]]=None):
|
||||
if not parent_wo_num:
|
||||
query = select(OverhaulScope.wo_parent).where(OverhaulScope.id == oh_session_id)
|
||||
result = await db_session.execute(query)
|
||||
parent_wo_num = result.scalar()
|
||||
|
||||
if not parent_wo_num:
|
||||
return []
|
||||
|
||||
# Ensure parent_wo_num is a list and removed duplicates if any
|
||||
if isinstance(parent_wo_num, str):
|
||||
parent_wo_num = [parent_wo_num]
|
||||
else:
|
||||
parent_wo_num = list(set(parent_wo_num))
|
||||
|
||||
sql_query = text("""
|
||||
WITH target_wos AS (
|
||||
SELECT
|
||||
w.wonum,
|
||||
w.assetnum,
|
||||
COALESCE(w.actmatcost, 0) as actmatcost,
|
||||
COALESCE(w.actservcost, 0) as actservcost
|
||||
FROM public.wo_maximo w
|
||||
WHERE w.xx_parent = ANY(:parent_wo_num)
|
||||
),
|
||||
wo_tasks AS (
|
||||
SELECT
|
||||
t.xx_parent AS parent_wonum,
|
||||
JSON_AGG(t.description) AS task_list
|
||||
FROM public.wo_maximo t
|
||||
JOIN target_wos tw ON t.xx_parent = tw.wonum
|
||||
GROUP BY t.xx_parent
|
||||
)
|
||||
SELECT
|
||||
w.assetnum,
|
||||
e.name AS equipment_name,
|
||||
e.location_tag,
|
||||
JSON_OBJECT_AGG(w.wonum, COALESCE(wt.task_list, '[]'::json)) AS wonum_list,
|
||||
COUNT(w.wonum) AS total_wo_count,
|
||||
COALESCE(SUM(w.actmatcost), 0) AS total_material_cost,
|
||||
COALESCE(SUM(w.actservcost), 0) AS total_service_cost,
|
||||
COALESCE(SUM(w.actmatcost + w.actservcost), 0) AS total_actual_cost
|
||||
FROM target_wos w
|
||||
INNER JOIN public.ms_equipment_master e
|
||||
ON w.assetnum = e.assetnum
|
||||
LEFT JOIN wo_tasks wt
|
||||
ON w.wonum = wt.parent_wonum
|
||||
GROUP BY
|
||||
w.assetnum,
|
||||
e.name,
|
||||
e.location_tag
|
||||
ORDER BY total_actual_cost DESC;
|
||||
""")
|
||||
|
||||
results = await collector_db_session.execute(sql_query, {"parent_wo_num": parent_wo_num})
|
||||
|
||||
return [
|
||||
{
|
||||
"assetnum": row.assetnum,
|
||||
"equipment_name": row.equipment_name,
|
||||
"location_tag": row.location_tag,
|
||||
"wonum_list": row.wonum_list,
|
||||
"total_wo_count": row.total_wo_count,
|
||||
"total_material_cost": float(row.total_material_cost),
|
||||
"total_service_cost": float(row.total_service_cost),
|
||||
"total_actual_cost": float(row.total_actual_cost)
|
||||
}
|
||||
for row in results
|
||||
]
|
||||
|
||||
|
||||
|
||||
|
||||
sql_query = text("\n WITH target_wos AS (\n SELECT \n w.wonum,\n w.assetnum,\n COALESCE(w.actmatcost, 0) as actmatcost,\n COALESCE(w.actservcost, 0) as actservcost\n FROM public.wo_maximo w\n WHERE w.xx_parent = ANY(:parent_wo_num)\n ),\n wo_tasks AS (\n SELECT \n t.xx_parent AS parent_wonum,\n JSON_AGG(t.description) AS task_list\n FROM public.wo_maximo t\n JOIN target_wos tw ON t.xx_parent = tw.wonum\n GROUP BY t.xx_parent\n )\n SELECT \n w.assetnum,\n e.name AS equipment_name,\n e.location_tag,\n JSON_OBJECT_AGG(w.wonum, COALESCE(wt.task_list, '[]'::json)) AS wonum_list,\n COUNT(w.wonum) AS total_wo_count,\n COALESCE(SUM(w.actmatcost), 0) AS total_material_cost,\n COALESCE(SUM(w.actservcost), 0) AS total_service_cost,\n COALESCE(SUM(w.actmatcost + w.actservcost), 0) AS total_actual_cost\n FROM target_wos w\n INNER JOIN public.ms_equipment_master e \n ON w.assetnum = e.assetnum\n LEFT JOIN wo_tasks wt \n ON w.wonum = wt.parent_wonum\n GROUP BY \n w.assetnum, \n e.name, \n e.location_tag\n ORDER BY total_actual_cost DESC;\n ")
|
||||
results = await collector_db_session.execute(sql_query, {'parent_wo_num': parent_wo_num})
|
||||
return [{'assetnum': row.assetnum, 'equipment_name': row.equipment_name, 'location_tag': row.location_tag, 'wonum_list': row.wonum_list, 'total_wo_count': row.total_wo_count, 'total_material_cost': float(row.total_material_cost), 'total_service_cost': float(row.total_service_cost), 'total_actual_cost': float(row.total_actual_cost)} for row in results]
|
||||
|
||||
@ -1 +0,0 @@
|
||||
# Empty init file
|
||||
@ -1,172 +0,0 @@
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
from sqlalchemy import func, select
|
||||
|
||||
from src.auth.service import Token
|
||||
from src.config import TC_RBD_ID
|
||||
from src.database.core import DbSession, CollectorDbSession
|
||||
from src.overhaul_scope.service import get_all
|
||||
from src.standard_scope.model import StandardScope
|
||||
from src.workorder.model import MasterWorkOrder
|
||||
|
||||
from src.calculation_time_constrains.schema import (
|
||||
CalculationTimeConstrainsParametersCreate,
|
||||
CalculationTimeConstrainsParametersRead,
|
||||
CalculationTimeConstrainsParametersRetrive,
|
||||
)
|
||||
from src.optimum_time_constraint.service import (
|
||||
get_calculation_data_by_id,
|
||||
get_calculation_result,
|
||||
)
|
||||
|
||||
|
||||
async def get_create_calculation_parameters(
|
||||
*, db_session: DbSession, calculation_id: Optional[str] = None
|
||||
):
|
||||
if calculation_id is not None:
|
||||
calculation = await get_calculation_data_by_id(
|
||||
calculation_id=calculation_id, db_session=db_session
|
||||
)
|
||||
|
||||
if not calculation:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="A data with this id does not exist.",
|
||||
)
|
||||
|
||||
return CalculationTimeConstrainsParametersRead(
|
||||
costPerFailure=calculation.parameter.avg_failure_cost,
|
||||
overhaulCost=calculation.parameter.overhaul_cost,
|
||||
reference=calculation,
|
||||
)
|
||||
|
||||
stmt = (
|
||||
select(
|
||||
StandardScope,
|
||||
func.avg(MasterWorkOrder.total_cost_max).label("average_cost"),
|
||||
)
|
||||
.outerjoin(MasterWorkOrder, StandardScope.location_tag == MasterWorkOrder.location_tag)
|
||||
.group_by(StandardScope.id)
|
||||
)
|
||||
|
||||
results = await db_session.execute(stmt)
|
||||
costFailure = results.all()
|
||||
scopes = await get_all(db_session=db_session)
|
||||
avaiableScopes = {scope.id: scope.scope_name for scope in scopes}
|
||||
costFailurePerScope = {
|
||||
avaiableScopes.get(costPerFailure[0]): costPerFailure[1]
|
||||
for costPerFailure in costFailure
|
||||
}
|
||||
|
||||
return CalculationTimeConstrainsParametersRetrive(
|
||||
costPerFailure=costFailurePerScope,
|
||||
availableScopes=avaiableScopes.values(),
|
||||
recommendedScope="A",
|
||||
)
|
||||
|
||||
|
||||
async def create_calculation(
|
||||
*,
|
||||
token: str,
|
||||
db_session: DbSession,
|
||||
collector_db_session: CollectorDbSession,
|
||||
calculation_time_constrains_in: CalculationTimeConstrainsParametersCreate,
|
||||
created_by: str,
|
||||
simulation_id
|
||||
):
|
||||
from temporalio.client import Client
|
||||
from src.config import TEMPORAL_URL
|
||||
from temporal.temporal_workflows import OptimumOHCalculationWorkflow
|
||||
import uuid
|
||||
|
||||
temporal_client = await Client.connect(TEMPORAL_URL)
|
||||
|
||||
workflow_id = f"optimum-oh-calc-{uuid.uuid4()}"
|
||||
|
||||
args = {
|
||||
"token": token.token if hasattr(token, 'token') else str(token),
|
||||
"calculation_in": calculation_time_constrains_in.model_dump(mode="json"),
|
||||
"created_by": created_by
|
||||
}
|
||||
|
||||
handle = await temporal_client.start_workflow(
|
||||
OptimumOHCalculationWorkflow.run,
|
||||
args,
|
||||
id=workflow_id,
|
||||
task_queue="oh-task-queue"
|
||||
)
|
||||
|
||||
return {
|
||||
"data": workflow_id,
|
||||
"status": "success",
|
||||
"message": "Calculation workflow started successfully"
|
||||
}
|
||||
|
||||
|
||||
async def recalculate_calculation(
|
||||
*,
|
||||
token: str,
|
||||
db_session: DbSession,
|
||||
collector_db_session: CollectorDbSession,
|
||||
calculation_id: str,
|
||||
simulation_id: Optional[str] = None
|
||||
):
|
||||
calculation_data = await get_calculation_data_by_id(
|
||||
db_session=db_session, calculation_id=calculation_id
|
||||
)
|
||||
|
||||
if not calculation_data:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Calculation not found",
|
||||
)
|
||||
|
||||
rbd_simulation_id = simulation_id or calculation_data.rbd_simulation_id or TC_RBD_ID
|
||||
|
||||
from sqlalchemy import delete
|
||||
from src.calculation_time_constrains.model import CalculationEquipmentResult, CalculationResult
|
||||
|
||||
await db_session.execute(
|
||||
delete(CalculationResult).where(CalculationResult.calculation_data_id == calculation_data.id)
|
||||
)
|
||||
await db_session.execute(
|
||||
delete(CalculationEquipmentResult).where(CalculationEquipmentResult.calculation_data_id == calculation_data.id)
|
||||
)
|
||||
await db_session.commit()
|
||||
|
||||
from src.optimum_time_constraint.optimizer import run_simulation_with_spareparts
|
||||
await run_simulation_with_spareparts(
|
||||
db_session=db_session,
|
||||
calculation=calculation_data,
|
||||
token=token,
|
||||
collector_db_session=collector_db_session,
|
||||
simulation_id=rbd_simulation_id
|
||||
)
|
||||
|
||||
return await get_calculation_result(
|
||||
db_session=db_session,
|
||||
calculation_id=calculation_id,
|
||||
token=token,
|
||||
include_risk_cost=1
|
||||
)
|
||||
|
||||
|
||||
async def get_or_create_scope_equipment_calculation(
|
||||
*,
|
||||
db_session: DbSession,
|
||||
scope_calculation_id,
|
||||
calculation_time_constrains_in: Optional[CalculationTimeConstrainsParametersCreate]
|
||||
):
|
||||
scope_calculation = await get_calculation_data_by_id(
|
||||
db_session=db_session, calculation_id=scope_calculation_id
|
||||
)
|
||||
|
||||
if not scope_calculation:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="A data with this id does not exist.",
|
||||
)
|
||||
|
||||
return scope_calculation.id
|
||||
@ -1,917 +0,0 @@
|
||||
import asyncio
|
||||
import calendar
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
from datetime import date, timedelta
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
import aiohttp
|
||||
import httpx
|
||||
import numpy as np
|
||||
import requests
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from src.config import REALIBILITY_SERVICE_API, RBD_SERVICE_API
|
||||
from src.database.core import CollectorDbSession, DbSession
|
||||
from src.calculation_time_constrains.model import CalculationData, CalculationEquipmentResult
|
||||
from src.calculation_time_constrains.schema import CalculationResultsRead
|
||||
from src.calculation_time_constrains.utils import (
|
||||
calculate_failures_per_month,
|
||||
create_time_series_data,
|
||||
get_months_between,
|
||||
)
|
||||
from src.overhaul_scope.service import get as get_scope, get_prev_oh
|
||||
from src.sparepart.service import load_sparepart_data_from_db
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OptimumCostModelWithSpareparts:
|
||||
def __init__(
|
||||
self,
|
||||
token: str,
|
||||
last_oh_date: date,
|
||||
next_oh_date: date,
|
||||
sparepart_manager,
|
||||
time_window_months: Optional[int] = None,
|
||||
base_url: str = "http://192.168.1.82:8000",
|
||||
):
|
||||
"""
|
||||
Initialize the Optimum Cost Model with sparepart management
|
||||
"""
|
||||
self.api_base_url = base_url
|
||||
self.token = token
|
||||
self.last_oh_date = last_oh_date
|
||||
self.next_oh_date = next_oh_date
|
||||
self.session = None
|
||||
self.sparepart_manager = sparepart_manager
|
||||
|
||||
# Calculate planned overhaul interval in months
|
||||
self.planned_oh_months = get_months_between(last_oh_date, next_oh_date)
|
||||
|
||||
# Set analysis time window: next OH + 6 months
|
||||
self.time_window_months = time_window_months or (self.planned_oh_months + 6)
|
||||
|
||||
# Pre-calculate date range for API calls
|
||||
self.date_range = self._generate_date_range()
|
||||
|
||||
self.logger = log
|
||||
|
||||
def _generate_date_range(self) -> List[datetime.datetime]:
|
||||
"""Generate date range for analysis based on time window"""
|
||||
dates = []
|
||||
current_date = datetime.datetime.combine(self.last_oh_date, datetime.datetime.min.time())
|
||||
end_date = current_date + timedelta(days=self.time_window_months * 30)
|
||||
|
||||
while current_date <= end_date:
|
||||
dates.append(current_date)
|
||||
current_date += timedelta(days=31)
|
||||
|
||||
return dates
|
||||
|
||||
async def _create_session(self):
|
||||
"""Create aiohttp session with connection pooling"""
|
||||
if self.session is None:
|
||||
timeout = aiohttp.ClientTimeout(total=300)
|
||||
connector = aiohttp.TCPConnector(
|
||||
limit=500,
|
||||
limit_per_host=200,
|
||||
ttl_dns_cache=300,
|
||||
use_dns_cache=True,
|
||||
force_close=False,
|
||||
enable_cleanup_closed=True,
|
||||
)
|
||||
self.session = aiohttp.ClientSession(
|
||||
timeout=timeout,
|
||||
connector=connector,
|
||||
headers={"Authorization": f"Bearer {self.token}"},
|
||||
)
|
||||
|
||||
async def _close_session(self):
|
||||
"""Close aiohttp session"""
|
||||
if self.session:
|
||||
await self.session.close()
|
||||
self.session = None
|
||||
|
||||
async def get_failures_prediction(self, location_tag: str):
|
||||
"""Get failure predictions for equipment from Reliability Predict service"""
|
||||
|
||||
start_date = self.last_oh_date.strftime("%Y-%m-%d")
|
||||
end_date_val = self.next_oh_date + timedelta(days=6 * 30)
|
||||
end_date = end_date_val.strftime("%Y-%m-%d")
|
||||
|
||||
predict_url = f"{REALIBILITY_SERVICE_API}/main/predict/{location_tag}/{start_date}/{end_date}"
|
||||
|
||||
try:
|
||||
response = requests.get(
|
||||
predict_url,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {self.token}",
|
||||
},
|
||||
timeout=30,
|
||||
)
|
||||
response.raise_for_status()
|
||||
prediction_data = response.json()
|
||||
except (requests.RequestException, ValueError) as e:
|
||||
self.logger.error(f"Failed to fetch prediction data for {location_tag}: {e}")
|
||||
return None
|
||||
|
||||
predictions = (
|
||||
prediction_data.get("data", {}).get("predictions")
|
||||
if prediction_data.get("data")
|
||||
else None
|
||||
)
|
||||
|
||||
if not predictions:
|
||||
self.logger.warning(f"No prediction data available for {location_tag}")
|
||||
return None
|
||||
|
||||
monthly_data = {}
|
||||
cumulative = 0.0
|
||||
for i, pred in enumerate(predictions):
|
||||
month_key = pred["month"]
|
||||
monthly_fail = pred["predicted_failures"]
|
||||
source = pred.get("source", "predicted")
|
||||
cumulative += monthly_fail
|
||||
monthly_data[month_key] = {
|
||||
"cumulative_failures": cumulative,
|
||||
"monthly_failures": monthly_fail,
|
||||
"month_index": i + 1,
|
||||
"source": source,
|
||||
}
|
||||
|
||||
return monthly_data
|
||||
|
||||
async def get_simulation_results(self, simulation_id: str = "default"):
|
||||
"""Get simulation results for Birnbaum importance calculations"""
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.token}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
calc_result_url = f"{self.api_base_url}/aeros/simulation/result/calc/{simulation_id}?nodetype=RegularNode"
|
||||
plant_result_url = f"{self.api_base_url}/aeros/simulation/result/calc/{simulation_id}/plant"
|
||||
|
||||
async with httpx.AsyncClient(timeout=300.0) as client:
|
||||
calc_task = client.get(calc_result_url, headers=headers)
|
||||
plant_task = client.get(plant_result_url, headers=headers)
|
||||
|
||||
calc_response, plant_response = await asyncio.gather(calc_task, plant_task)
|
||||
|
||||
calc_response.raise_for_status()
|
||||
plant_response.raise_for_status()
|
||||
|
||||
calc_data = calc_response.json()["data"]
|
||||
plant_data = plant_response.json()["data"]
|
||||
|
||||
return {"calc_result": calc_data, "plant_result": plant_data}
|
||||
|
||||
def _calculate_equipment_costs_with_spareparts(
|
||||
self,
|
||||
failures_prediction: Dict,
|
||||
birnbaum_importance: float,
|
||||
preventive_cost: float,
|
||||
failure_replacement_cost: float,
|
||||
ecs,
|
||||
location_tag: str,
|
||||
planned_overhauls: List = None,
|
||||
loss_production_permonth=0,
|
||||
) -> List[Dict]:
|
||||
"""Calculate costs for each month including sparepart costs and availability"""
|
||||
|
||||
if not failures_prediction:
|
||||
self.logger.warning(f"No failure prediction data for {location_tag}")
|
||||
return []
|
||||
|
||||
results = []
|
||||
months = list(failures_prediction.keys())
|
||||
num_months = len(months)
|
||||
failure_counts = []
|
||||
|
||||
monthly_risk_cost_per_failure = 0
|
||||
|
||||
if ecs:
|
||||
is_trip = 1 if ecs.get("Diskripsi Operasional Akibat Equip. Failure") == "Trip" else 0
|
||||
is_series = 0 if not birnbaum_importance else math.floor(birnbaum_importance)
|
||||
if is_trip:
|
||||
downtime = ecs.get("Estimasi Waktu Maint. / Downtime / Gangguan (Jam)")
|
||||
monthly_risk_cost_per_failure = 660 * 1000000 * is_trip * downtime * is_series
|
||||
|
||||
for month_key in months:
|
||||
data = failures_prediction[month_key]
|
||||
failure_counts.append(data["cumulative_failures"])
|
||||
|
||||
for i in range(num_months):
|
||||
month_index = i + 1
|
||||
|
||||
if month_index > self.time_window_months:
|
||||
continue
|
||||
|
||||
sparepart_analysis = self._analyze_sparepart_availability(
|
||||
location_tag, month_index - 1, planned_overhauls or []
|
||||
)
|
||||
|
||||
total_expected_failure_cost = failure_counts[i] * (
|
||||
failure_replacement_cost + monthly_risk_cost_per_failure
|
||||
)
|
||||
|
||||
procurement_cost = sparepart_analysis["total_procurement_cost"]
|
||||
total_preventive_cost = preventive_cost + procurement_cost
|
||||
|
||||
total_cycle_cost = total_expected_failure_cost + total_preventive_cost
|
||||
|
||||
cput = total_cycle_cost / month_index
|
||||
|
||||
results.append(
|
||||
{
|
||||
"month": month_index,
|
||||
"number_of_failures": failure_counts[i],
|
||||
"is_actual": failures_prediction[months[i]].get("source") == "actual",
|
||||
"failure_cost": total_expected_failure_cost / month_index,
|
||||
"preventive_cost": preventive_cost / month_index,
|
||||
"procurement_cost": procurement_cost / month_index,
|
||||
"total_cost": cput,
|
||||
"absolute_failure_cost": total_expected_failure_cost,
|
||||
"absolute_overhaul_cost": preventive_cost,
|
||||
"absolute_procurement_cost": procurement_cost,
|
||||
"total_cycle_cost": total_cycle_cost,
|
||||
"is_after_planned_oh": month_index > self.planned_oh_months,
|
||||
"delay_months": max(0, month_index - self.planned_oh_months),
|
||||
"procurement_details": sparepart_analysis,
|
||||
"sparepart_available": sparepart_analysis["available"],
|
||||
"sparepart_status": sparepart_analysis["message"],
|
||||
"can_proceed": sparepart_analysis["can_proceed_with_delays"],
|
||||
}
|
||||
)
|
||||
|
||||
return results
|
||||
|
||||
def _analyze_sparepart_availability(
|
||||
self, equipment_tag: str, target_month: int, planned_overhauls: List
|
||||
) -> Dict:
|
||||
"""Analyze sparepart availability for equipment at target month"""
|
||||
if not self.sparepart_manager:
|
||||
return {
|
||||
"available": True,
|
||||
"message": "Sparepart manager not initialized",
|
||||
"total_procurement_cost": 0,
|
||||
"procurement_needed": [],
|
||||
"can_proceed_with_delays": True,
|
||||
}
|
||||
|
||||
other_overhauls = [
|
||||
(eq_tag, month)
|
||||
for eq_tag, month in planned_overhauls
|
||||
if eq_tag != equipment_tag and month <= target_month
|
||||
]
|
||||
|
||||
return self.sparepart_manager.check_sparepart_availability(
|
||||
equipment_tag, target_month, other_overhauls
|
||||
)
|
||||
|
||||
def _find_optimal_timing_with_spareparts(
|
||||
self, cost_results: List[Dict], location_tag: str
|
||||
) -> Optional[Dict]:
|
||||
"""Find optimal timing considering sparepart constraints"""
|
||||
if not cost_results:
|
||||
return None
|
||||
|
||||
feasible_results = [r for r in cost_results if r["can_proceed"]]
|
||||
|
||||
min_cost = float("inf")
|
||||
optimal_result = None
|
||||
|
||||
for i, result in enumerate(cost_results):
|
||||
if result in feasible_results and result["total_cost"] < min_cost:
|
||||
min_cost = result["total_cost"]
|
||||
optimal_result = result
|
||||
|
||||
if optimal_result is None:
|
||||
return None
|
||||
|
||||
return self._create_optimal_result(optimal_result, location_tag, "OPTIMAL")
|
||||
|
||||
def _create_optimal_result(
|
||||
self, optimal_result: Dict, location_tag: str, status: str
|
||||
) -> Dict:
|
||||
"""Create standardized optimal result dictionary"""
|
||||
return {
|
||||
"location_tag": location_tag,
|
||||
"optimal_month": optimal_result["month"],
|
||||
"optimal_index": optimal_result["month"] - 1,
|
||||
"optimal_cost": optimal_result["total_cost"],
|
||||
"failure_cost": optimal_result["failure_cost"],
|
||||
"preventive_cost": optimal_result["preventive_cost"],
|
||||
"procurement_cost": optimal_result["procurement_cost"],
|
||||
"number_of_failures": optimal_result["number_of_failures"],
|
||||
"is_delayed": optimal_result["is_after_planned_oh"],
|
||||
"delay_months": optimal_result["delay_months"],
|
||||
"planned_oh_month": self.planned_oh_months,
|
||||
"planned_cost": None,
|
||||
"cost_vs_planned": None,
|
||||
"savings_from_delay": 0,
|
||||
"cost_of_delay": 0,
|
||||
"sparepart_available": optimal_result["sparepart_available"],
|
||||
"sparepart_status": optimal_result["sparepart_status"],
|
||||
"procurement_details": optimal_result["procurement_details"],
|
||||
"optimization_status": status,
|
||||
"all_monthly_costs": [],
|
||||
}
|
||||
|
||||
async def calculate_cost_all_equipment_with_spareparts(
|
||||
self,
|
||||
db_session,
|
||||
collector_db_session,
|
||||
equipments: List,
|
||||
calculation,
|
||||
preventive_cost: float,
|
||||
simulation_id: str = "default",
|
||||
):
|
||||
"""
|
||||
Calculate optimal overhaul timing for entire fleet considering sparepart constraints
|
||||
"""
|
||||
|
||||
self.logger.info(
|
||||
f"Starting fleet optimization with reliability prediction for {len(equipments)} equipment"
|
||||
)
|
||||
max_interval = self.time_window_months
|
||||
|
||||
# Phase 1: Calculate individual optimal timings without considering interactions
|
||||
individual_results = {}
|
||||
|
||||
try:
|
||||
with open("src/calculation_time_constrains/full_equipment_with_downtime_opdesc.json", "r") as f:
|
||||
data = json.load(f)
|
||||
ecs_tags = {eq["Location"]: eq for eq in data}
|
||||
except FileNotFoundError:
|
||||
ecs_tags = {}
|
||||
|
||||
for equipment in equipments:
|
||||
location_tag = equipment.location_tag
|
||||
contribution_factor = 1.0
|
||||
ecs = ecs_tags.get(location_tag, None)
|
||||
|
||||
monthly_data = await self.get_failures_prediction(location_tag)
|
||||
|
||||
if not monthly_data:
|
||||
continue
|
||||
|
||||
equipment_preventive_cost = equipment.overhaul_cost + equipment.service_cost
|
||||
failure_replacement_cost = equipment.material_cost + (3 * 111000 * 3)
|
||||
|
||||
cost_results = self._calculate_equipment_costs_with_spareparts(
|
||||
failures_prediction=monthly_data,
|
||||
birnbaum_importance=contribution_factor,
|
||||
preventive_cost=equipment_preventive_cost,
|
||||
failure_replacement_cost=failure_replacement_cost,
|
||||
location_tag=location_tag,
|
||||
planned_overhauls=[],
|
||||
ecs=ecs,
|
||||
loss_production_permonth=0,
|
||||
)
|
||||
|
||||
if not cost_results:
|
||||
continue
|
||||
|
||||
optimal_timing = self._find_optimal_timing_with_spareparts(cost_results, location_tag)
|
||||
|
||||
if optimal_timing:
|
||||
optimal_timing["all_monthly_costs"] = cost_results
|
||||
individual_results[location_tag] = optimal_timing
|
||||
self.logger.info(
|
||||
f"Individual optimal for {location_tag}: Month {optimal_timing['optimal_month']}"
|
||||
)
|
||||
|
||||
# Phase 2: Optimize considering sparepart interactions
|
||||
self.logger.info("Phase 2: Optimizing with sparepart interactions...")
|
||||
|
||||
improved_plan = self._optimize_fleet_with_sparepart_constraints(
|
||||
individual_results, equipments, simulation_id
|
||||
)
|
||||
|
||||
# Phase 3: Generate final results and database objects
|
||||
fleet_results = []
|
||||
total_corrective_costs = np.zeros(max_interval)
|
||||
total_preventive_costs = np.zeros(max_interval)
|
||||
total_procurement_costs = np.zeros(max_interval)
|
||||
total_costs = np.zeros(max_interval)
|
||||
|
||||
total_fleet_procurement_cost = 0
|
||||
|
||||
for equipment in equipments:
|
||||
location_tag = equipment.location_tag
|
||||
|
||||
if location_tag not in individual_results:
|
||||
continue
|
||||
|
||||
equipment_timing = next(
|
||||
(month for tag, month in improved_plan if tag == location_tag),
|
||||
individual_results[location_tag]["optimal_month"],
|
||||
)
|
||||
|
||||
cost_data = individual_results[location_tag]["all_monthly_costs"][equipment_timing - 1]
|
||||
|
||||
all_costs = individual_results[location_tag]["all_monthly_costs"]
|
||||
|
||||
corrective_costs = [r["failure_cost"] for r in all_costs]
|
||||
preventive_costs = [r["preventive_cost"] for r in all_costs]
|
||||
procurement_costs = [r["procurement_cost"] for r in all_costs]
|
||||
failures = [r["number_of_failures"] for r in all_costs]
|
||||
total_costs_equipment = [r["total_cost"] for r in all_costs]
|
||||
procurement_details = [r["procurement_details"] for r in all_costs]
|
||||
|
||||
def pad_array(arr, target_length):
|
||||
if len(arr) < target_length:
|
||||
return arr + [arr[-1]] * (target_length - len(arr))
|
||||
return arr[:target_length]
|
||||
|
||||
corrective_costs = pad_array(corrective_costs, max_interval)
|
||||
preventive_costs = pad_array(preventive_costs, max_interval)
|
||||
procurement_costs = pad_array(procurement_costs, max_interval)
|
||||
failures = pad_array(failures, max_interval)
|
||||
total_costs_equipment = pad_array(total_costs_equipment, max_interval)
|
||||
procurement_details = pad_array(procurement_details, max_interval)
|
||||
|
||||
is_actual_list = [r.get("is_actual", False) for r in all_costs]
|
||||
is_actual_list = pad_array(is_actual_list, max_interval)
|
||||
|
||||
equipment_result = CalculationEquipmentResult(
|
||||
corrective_costs=corrective_costs,
|
||||
overhaul_costs=preventive_costs,
|
||||
procurement_costs=procurement_costs,
|
||||
daily_failures=failures,
|
||||
is_actual=is_actual_list,
|
||||
location_tag=equipment.location_tag,
|
||||
material_cost=equipment.material_cost,
|
||||
service_cost=equipment.service_cost,
|
||||
optimum_day=equipment_timing - 1,
|
||||
calculation_data_id=calculation.id,
|
||||
procurement_details=procurement_details,
|
||||
)
|
||||
|
||||
fleet_results.append(equipment_result)
|
||||
|
||||
total_corrective_costs += np.array(corrective_costs)
|
||||
total_preventive_costs += np.array(preventive_costs)
|
||||
total_procurement_costs += np.array(procurement_costs)
|
||||
total_costs += np.array(total_costs_equipment)
|
||||
|
||||
total_fleet_procurement_cost += cost_data["procurement_cost"]
|
||||
|
||||
fleet_optimal_index = np.argmin(total_costs)
|
||||
|
||||
calculation.optimum_oh_day = int(fleet_optimal_index)
|
||||
calculation.max_interval = max_interval
|
||||
calculation.rbd_simulation_id = simulation_id
|
||||
|
||||
db_session.add_all(fleet_results)
|
||||
await db_session.commit()
|
||||
|
||||
return int(fleet_optimal_index)
|
||||
|
||||
def _optimize_fleet_with_sparepart_constraints(
|
||||
self, individual_results: Dict, equipments: List, simulation_id: str
|
||||
) -> List[Tuple[str, int]]:
|
||||
"""
|
||||
Optimize fleet overhaul timing considering sparepart sharing constraints
|
||||
"""
|
||||
current_plan = [(tag, result["optimal_month"]) for tag, result in individual_results.items()]
|
||||
current_plan.sort(key=lambda x: x[1])
|
||||
|
||||
improved_plan = []
|
||||
processed_equipment = []
|
||||
|
||||
for equipment_tag, optimal_month in current_plan:
|
||||
sparepart_analysis = self.sparepart_manager.check_sparepart_availability(
|
||||
equipment_tag, optimal_month - 1, processed_equipment
|
||||
)
|
||||
|
||||
if sparepart_analysis["available"] or sparepart_analysis["can_proceed_with_delays"]:
|
||||
improved_plan.append((equipment_tag, optimal_month))
|
||||
processed_equipment.append((equipment_tag, optimal_month))
|
||||
else:
|
||||
alternative_month = self._find_alternative_timing(
|
||||
equipment_tag,
|
||||
optimal_month,
|
||||
individual_results[equipment_tag]["all_monthly_costs"],
|
||||
processed_equipment,
|
||||
)
|
||||
|
||||
if alternative_month:
|
||||
improved_plan.append((equipment_tag, alternative_month))
|
||||
processed_equipment.append((equipment_tag, alternative_month))
|
||||
else:
|
||||
improved_plan.append((equipment_tag, optimal_month))
|
||||
processed_equipment.append((equipment_tag, optimal_month))
|
||||
|
||||
return improved_plan
|
||||
|
||||
def _find_alternative_timing(
|
||||
self,
|
||||
equipment_tag: str,
|
||||
preferred_month: int,
|
||||
cost_results: List[Dict],
|
||||
processed_equipment: List[Tuple[str, int]],
|
||||
) -> Optional[int]:
|
||||
"""
|
||||
Find alternative timing when preferred month has sparepart constraints
|
||||
"""
|
||||
search_range = 6
|
||||
|
||||
candidates = []
|
||||
|
||||
for offset in range(-search_range // 2, search_range // 2 + 1):
|
||||
candidate_month = preferred_month + offset
|
||||
|
||||
if candidate_month <= 0 or candidate_month > len(cost_results):
|
||||
continue
|
||||
|
||||
if candidate_month == preferred_month:
|
||||
continue
|
||||
|
||||
sparepart_analysis = self.sparepart_manager.check_sparepart_availability(
|
||||
equipment_tag, candidate_month - 1, processed_equipment
|
||||
)
|
||||
|
||||
if sparepart_analysis["available"] or sparepart_analysis["can_proceed_with_delays"]:
|
||||
cost_data = cost_results[candidate_month - 1]
|
||||
candidates.append((candidate_month, cost_data["total_cost"]))
|
||||
|
||||
if not candidates:
|
||||
return None
|
||||
|
||||
candidates.sort(key=lambda x: x[1])
|
||||
return candidates[0][0]
|
||||
|
||||
async def __aenter__(self):
|
||||
await self._create_session()
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
await self._close_session()
|
||||
|
||||
|
||||
def _analyze_optimal_timing(
|
||||
calculation_results: List, optimum_oh_day: int, prev_oh_scope, scope_overhaul
|
||||
) -> Dict:
|
||||
"""Analyze optimal timing and provide recommendations"""
|
||||
|
||||
if not calculation_results:
|
||||
return {}
|
||||
|
||||
optimal_result = None
|
||||
if 0 <= optimum_oh_day < len(calculation_results):
|
||||
optimal_result = calculation_results[optimum_oh_day]
|
||||
|
||||
planned_oh_months = None
|
||||
if prev_oh_scope and scope_overhaul:
|
||||
planned_oh_months = get_months_between(prev_oh_scope.end_date, scope_overhaul.start_date)
|
||||
|
||||
timing_recommendation = "OPTIMAL"
|
||||
if planned_oh_months:
|
||||
if optimum_oh_day + 1 < planned_oh_months:
|
||||
timing_recommendation = "EARLY"
|
||||
elif optimum_oh_day + 1 > planned_oh_months:
|
||||
timing_recommendation = "DELAYED"
|
||||
else:
|
||||
timing_recommendation = "ON_SCHEDULE"
|
||||
|
||||
cost_trend = "STABLE"
|
||||
if len(calculation_results) > 1:
|
||||
early_costs = [r.total_cost for r in calculation_results[: len(calculation_results) // 3]]
|
||||
late_costs = [r.total_cost for r in calculation_results[-len(calculation_results) // 3 :]]
|
||||
|
||||
avg_early = sum(early_costs) / len(early_costs) if early_costs else 0
|
||||
avg_late = sum(late_costs) / len(late_costs) if late_costs else 0
|
||||
|
||||
if avg_late > avg_early * 1.2:
|
||||
cost_trend = "INCREASING"
|
||||
elif avg_late < avg_early * 0.8:
|
||||
cost_trend = "DECREASING"
|
||||
|
||||
return {
|
||||
"optimal_month": optimum_oh_day + 1,
|
||||
"planned_month": planned_oh_months,
|
||||
"timing_recommendation": timing_recommendation,
|
||||
"optimal_total_cost": optimal_result.total_cost if optimal_result else 0,
|
||||
"optimal_breakdown": {
|
||||
"corrective_cost": optimal_result.corrective_cost if optimal_result else 0,
|
||||
"overhaul_cost": optimal_result.overhaul_cost if optimal_result else 0,
|
||||
"procurement_cost": optimal_result.procurement_cost if optimal_result else 0,
|
||||
"num_failures": optimal_result.num_failures if optimal_result else 0,
|
||||
},
|
||||
"cost_trend": cost_trend,
|
||||
"months_from_planned": (optimum_oh_day + 1 - planned_oh_months)
|
||||
if planned_oh_months
|
||||
else None,
|
||||
"cost_savings_vs_planned": None,
|
||||
"sparepart_impact": {
|
||||
"equipment_with_constraints": optimal_result.sparepart_summary["equipment_requiring_procurement"]
|
||||
if optimal_result
|
||||
else 0,
|
||||
"critical_shortages": optimal_result.sparepart_summary["critical_shortages"]
|
||||
if optimal_result
|
||||
else 0,
|
||||
"procurement_investment": optimal_result.sparepart_summary["total_procurement_cost"]
|
||||
if optimal_result
|
||||
else 0,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
async def run_simulation_with_spareparts(
|
||||
*,
|
||||
db_session: DbSession,
|
||||
calculation,
|
||||
token: str,
|
||||
collector_db_session: CollectorDbSession,
|
||||
time_window_months: Optional[int] = None,
|
||||
simulation_id: str = "default",
|
||||
) -> Dict:
|
||||
"""
|
||||
Run complete overhaul optimization simulation with sparepart management
|
||||
"""
|
||||
from src.optimum_time_constraint.service import get_calculation_data_by_id
|
||||
from src.overhaul_activity.service import get_standard_scope_by_session_id
|
||||
|
||||
equipments = await get_standard_scope_by_session_id(
|
||||
db_session=db_session,
|
||||
overhaul_session_id=calculation.overhaul_session_id,
|
||||
collector_db=collector_db_session,
|
||||
)
|
||||
|
||||
scope = await get_scope(
|
||||
db_session=db_session, overhaul_session_id=calculation.overhaul_session_id
|
||||
)
|
||||
prev_oh_scope = await get_prev_oh(db_session=db_session, overhaul_session=scope)
|
||||
|
||||
calculation_data = await get_calculation_data_by_id(
|
||||
db_session=db_session, calculation_id=calculation.id
|
||||
)
|
||||
|
||||
time_window_months = get_months_between(prev_oh_scope.end_date, scope.start_date) + 6
|
||||
|
||||
sparepart_manager = await load_sparepart_data_from_db(
|
||||
scope=scope,
|
||||
prev_oh_scope=prev_oh_scope,
|
||||
db_session=collector_db_session,
|
||||
app_db_session=db_session,
|
||||
analysis_window_months=time_window_months,
|
||||
)
|
||||
|
||||
optimum_oh_model = OptimumCostModelWithSpareparts(
|
||||
token=token,
|
||||
last_oh_date=prev_oh_scope.end_date,
|
||||
next_oh_date=scope.start_date,
|
||||
base_url=RBD_SERVICE_API,
|
||||
sparepart_manager=sparepart_manager,
|
||||
)
|
||||
|
||||
try:
|
||||
fleet_optimal_index = await optimum_oh_model.calculate_cost_all_equipment_with_spareparts(
|
||||
db_session=db_session,
|
||||
collector_db_session=collector_db_session,
|
||||
equipments=equipments,
|
||||
calculation=calculation_data,
|
||||
preventive_cost=calculation_data.parameter.overhaul_cost,
|
||||
simulation_id=simulation_id,
|
||||
)
|
||||
finally:
|
||||
await optimum_oh_model._close_session()
|
||||
|
||||
calculation_query = await db_session.execute(
|
||||
select(CalculationData)
|
||||
.options(
|
||||
selectinload(CalculationData.equipment_results),
|
||||
selectinload(CalculationData.parameter),
|
||||
)
|
||||
.where(CalculationData.id == calculation.id)
|
||||
)
|
||||
scope_calculation = calculation_query.scalar_one_or_none()
|
||||
|
||||
data_num = scope_calculation.max_interval
|
||||
all_equipment = scope_calculation.equipment_results
|
||||
included_equipment = [eq for eq in all_equipment if eq.is_included]
|
||||
|
||||
calculation_results = []
|
||||
fleet_statistics = {
|
||||
"total_equipment": len(all_equipment),
|
||||
"included_equipment": len(included_equipment),
|
||||
"excluded_equipment": len(all_equipment) - len(included_equipment),
|
||||
"equipment_with_sparepart_constraints": 0,
|
||||
"total_procurement_items": 0,
|
||||
"critical_procurement_items": 0,
|
||||
"total_spareparts": 745,
|
||||
}
|
||||
|
||||
avg_failure_cost = (
|
||||
sum((eq.material_cost or 0) + (3 * 111000 * 3) for eq in all_equipment) / len(all_equipment)
|
||||
if all_equipment
|
||||
else 0
|
||||
)
|
||||
|
||||
rbd_marginal_fails = [0] * data_num
|
||||
try:
|
||||
if scope_calculation.rbd_simulation_id:
|
||||
plant_result_url = f"{RBD_SERVICE_API}/aeros/simulation/result/calc/{scope_calculation.rbd_simulation_id}/plant"
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
response = await client.get(
|
||||
plant_result_url,
|
||||
headers={
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
)
|
||||
if response.status_code == 200:
|
||||
plant_data = response.json().get("data", {})
|
||||
timestamp_outs = plant_data.get("timestamp_outs", [])
|
||||
if timestamp_outs:
|
||||
hourly_data = create_time_series_data(
|
||||
timestamp_outs, max_hours=data_num * 720
|
||||
)
|
||||
cumulative_rbd_fails = calculate_failures_per_month(hourly_data)
|
||||
rbd_fails_map = {m["month"]: m["failures"] for m in cumulative_rbd_fails}
|
||||
prev_fail = 0
|
||||
for m in range(1, data_num + 1):
|
||||
curr_fail = rbd_fails_map.get(m, prev_fail)
|
||||
rbd_marginal_fails[m - 1] = curr_fail - prev_fail
|
||||
prev_fail = curr_fail
|
||||
except Exception as e:
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.warning(f"Failed to fetch plant simulation: {e}")
|
||||
|
||||
cumulative_plant_failures = 0
|
||||
|
||||
for month_index in range(data_num):
|
||||
historical_marginal_fail = 0
|
||||
for eq in all_equipment:
|
||||
if eq.is_actual and month_index < len(eq.is_actual) and eq.is_actual[month_index]:
|
||||
curr_fail = (
|
||||
eq.daily_failures[month_index]
|
||||
if month_index < len(eq.daily_failures)
|
||||
else 0
|
||||
)
|
||||
prev_fail = (
|
||||
eq.daily_failures[month_index - 1]
|
||||
if month_index > 0 and (month_index - 1) < len(eq.daily_failures)
|
||||
else 0
|
||||
)
|
||||
historical_marginal_fail += max(0, curr_fail - prev_fail)
|
||||
|
||||
marginal_fail = rbd_marginal_fails[month_index] + historical_marginal_fail
|
||||
cumulative_plant_failures += marginal_fail
|
||||
|
||||
month_result = {
|
||||
"overhaul_cost": 0.0,
|
||||
"corrective_cost": 0.0,
|
||||
"procurement_cost": 0.0,
|
||||
"num_failures": cumulative_plant_failures,
|
||||
"day": month_index + 1,
|
||||
"month": month_index + 1,
|
||||
"procurement_details": {},
|
||||
"sparepart_summary": {
|
||||
"total_procurement_cost": 0.0,
|
||||
"equipment_requiring_procurement": 0,
|
||||
"critical_shortages": 0,
|
||||
"existing_orders_value": 0.0,
|
||||
"new_orders_required": 0,
|
||||
"urgent_procurements": 0,
|
||||
},
|
||||
}
|
||||
|
||||
equipment_requiring_procurement = 0
|
||||
total_existing_orders_value = 0.0
|
||||
total_new_orders_value = 0.0
|
||||
critical_shortages = 0
|
||||
urgent_procurements = 0
|
||||
|
||||
for eq in all_equipment:
|
||||
if month_index >= len(eq.procurement_details):
|
||||
continue
|
||||
procurement_detail = eq.procurement_details[month_index]
|
||||
if (
|
||||
procurement_detail
|
||||
and isinstance(procurement_detail, dict)
|
||||
and procurement_detail.get("procurement_needed")
|
||||
):
|
||||
equipment_requiring_procurement += 1
|
||||
pr_po_summary = procurement_detail.get("pr_po_summary", {})
|
||||
existing_orders_value = pr_po_summary.get("total_existing_value", 0)
|
||||
total_existing_orders_value += existing_orders_value
|
||||
new_orders_value = pr_po_summary.get("total_new_orders_value", 0)
|
||||
total_new_orders_value += new_orders_value
|
||||
critical_missing = procurement_detail.get("critical_missing_parts", 0)
|
||||
if critical_missing > 0:
|
||||
critical_shortages += 1
|
||||
recommendations = procurement_detail.get("recommendations", [])
|
||||
urgent_items = [
|
||||
r for r in recommendations if r.get("priority") == "CRITICAL"
|
||||
]
|
||||
if urgent_items:
|
||||
urgent_procurements += 1
|
||||
is_included_eq = False if eq.is_initial else eq.is_included
|
||||
month_result["procurement_details"][eq.location_tag] = {
|
||||
"is_included": is_included_eq,
|
||||
"location_tag": eq.location_tag,
|
||||
"details": procurement_detail.get("procurement_needed", []),
|
||||
"detailed_message": procurement_detail.get("detailed_message", ""),
|
||||
"pr_po_summary": pr_po_summary,
|
||||
"recommendations": recommendations,
|
||||
"sparepart_available": procurement_detail.get("sparepart_available", True),
|
||||
"can_proceed": procurement_detail.get("can_proceed_with_delays", True),
|
||||
"critical_missing_parts": critical_missing,
|
||||
"existing_orders_value": existing_orders_value,
|
||||
"new_orders_value": new_orders_value,
|
||||
}
|
||||
if eq.is_included:
|
||||
if month_index < len(eq.overhaul_costs) and month_index < len(
|
||||
eq.procurement_costs
|
||||
):
|
||||
month_result["overhaul_cost"] += float(eq.overhaul_costs[month_index])
|
||||
month_result["procurement_cost"] += float(eq.procurement_costs[month_index])
|
||||
|
||||
month_result["corrective_cost"] = (
|
||||
cumulative_plant_failures * avg_failure_cost
|
||||
) / (month_index + 1)
|
||||
month_result["sparepart_summary"].update(
|
||||
{
|
||||
"total_procurement_cost": month_result["procurement_cost"],
|
||||
"equipment_requiring_procurement": equipment_requiring_procurement,
|
||||
"critical_shortages": critical_shortages,
|
||||
"existing_orders_value": total_existing_orders_value,
|
||||
"new_orders_required": len(
|
||||
[
|
||||
eq
|
||||
for eq in all_equipment
|
||||
if month_index < len(eq.procurement_details)
|
||||
and eq.procurement_details[month_index]
|
||||
and eq.procurement_details[month_index].get("procurement_needed")
|
||||
]
|
||||
),
|
||||
"urgent_procurements": urgent_procurements,
|
||||
}
|
||||
)
|
||||
month_result["total_cost"] = (
|
||||
month_result["corrective_cost"]
|
||||
+ month_result["overhaul_cost"]
|
||||
+ month_result["procurement_cost"]
|
||||
)
|
||||
calculation_results.append(month_result)
|
||||
|
||||
optimum_day = np.argmin([month["total_cost"] for month in calculation_results])
|
||||
scope_calculation.optimum_oh_day = int(optimum_day)
|
||||
|
||||
fleet_statistics["equipment_with_sparepart_constraints"] = len(
|
||||
[
|
||||
eq
|
||||
for eq in all_equipment
|
||||
if any(
|
||||
detail and detail.get("procurement_needed")
|
||||
for detail in eq.procurement_details
|
||||
if detail
|
||||
)
|
||||
]
|
||||
)
|
||||
fleet_statistics["total_procurement_items"] = sum(
|
||||
[
|
||||
len(detail.get("procurement_needed", []))
|
||||
for eq in all_equipment
|
||||
for detail in eq.procurement_details
|
||||
if detail and isinstance(detail, dict)
|
||||
]
|
||||
)
|
||||
|
||||
analysis_metadata = {
|
||||
"planned_month": (scope.start_date.year - prev_oh_scope.end_date.year) * 12
|
||||
+ (scope.start_date.month - prev_oh_scope.end_date.month)
|
||||
if prev_oh_scope and scope
|
||||
else 0,
|
||||
"total_fleet_procurement_cost": sum(
|
||||
[
|
||||
eq.procurement_costs[int(scope_calculation.optimum_oh_day)]
|
||||
for eq in all_equipment
|
||||
if eq.procurement_costs
|
||||
]
|
||||
),
|
||||
"last_oh_date": prev_oh_scope.end_date.isoformat() if prev_oh_scope else None,
|
||||
"next_oh_date": scope.start_date.isoformat() if scope else None,
|
||||
"optimal_stat": None,
|
||||
}
|
||||
|
||||
calc_results_read = [CalculationResultsRead(**r) for r in calculation_results]
|
||||
optimal_analysis = _analyze_optimal_timing(
|
||||
calc_results_read, scope_calculation.optimum_oh_day, prev_oh_scope, scope
|
||||
)
|
||||
|
||||
scope_calculation.plant_results = calculation_results
|
||||
scope_calculation.fleet_statistics = fleet_statistics
|
||||
scope_calculation.analysis_metadata = analysis_metadata
|
||||
scope_calculation.optimum_analysis = optimal_analysis
|
||||
|
||||
await db_session.commit()
|
||||
|
||||
return {"id": calculation.id, "optimum": optimal_analysis}
|
||||
@ -1,233 +0,0 @@
|
||||
from typing import Annotated, List, Optional, Union
|
||||
|
||||
from fastapi import APIRouter
|
||||
from fastapi.params import Query
|
||||
|
||||
from src.auth.service import CurrentUser, InternalKey, Token
|
||||
from src.config import DEFAULT_TC_ID
|
||||
from src.database.core import DbSession, CollectorDbSession
|
||||
from src.models import StandardResponse
|
||||
|
||||
from src.optimum_time_constraint.flows import (
|
||||
create_calculation,
|
||||
get_create_calculation_parameters,
|
||||
get_or_create_scope_equipment_calculation,
|
||||
recalculate_calculation,
|
||||
)
|
||||
from src.calculation_time_constrains.schema import (
|
||||
CalculationResultsRead,
|
||||
CalculationSelectedEquipmentUpdate,
|
||||
CalculationTimeConstrainsCreate,
|
||||
CalculationTimeConstrainsParametersCreate,
|
||||
CalculationTimeConstrainsParametersRead,
|
||||
CalculationTimeConstrainsParametersRetrive,
|
||||
CalculationTimeConstrainsRead,
|
||||
CreateCalculationQuery,
|
||||
EquipmentResult,
|
||||
CalculationTimeConstrainsReadNoResult,
|
||||
)
|
||||
from src.optimum_time_constraint.service import (
|
||||
bulk_update_equipment,
|
||||
get_calculation_result,
|
||||
get_calculation_result_by_day,
|
||||
get_calculation_by_assetnum,
|
||||
get_all_calculations,
|
||||
refresh_spareparts_service,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
get_calculation = APIRouter()
|
||||
|
||||
|
||||
@router.post(
|
||||
"", response_model=StandardResponse[Union[dict, CalculationTimeConstrainsRead]]
|
||||
)
|
||||
async def create_calculation_time_constrains(
|
||||
token: Token,
|
||||
db_session: DbSession,
|
||||
collector_db_session: CollectorDbSession,
|
||||
current_user: CurrentUser,
|
||||
calculation_time_constrains_in: CalculationTimeConstrainsParametersCreate,
|
||||
params: Annotated[CreateCalculationQuery, Query()],
|
||||
):
|
||||
"""Save calculation time constrains Here"""
|
||||
scope_calculation_id = params.scope_calculation_id
|
||||
simulation_id = params.simulation_id
|
||||
|
||||
if scope_calculation_id:
|
||||
results = await get_or_create_scope_equipment_calculation(
|
||||
db_session=db_session,
|
||||
scope_calculation_id=scope_calculation_id,
|
||||
calculation_time_constrains_in=calculation_time_constrains_in,
|
||||
)
|
||||
else:
|
||||
results = await create_calculation(
|
||||
token=token,
|
||||
db_session=db_session,
|
||||
collector_db_session=collector_db_session,
|
||||
calculation_time_constrains_in=calculation_time_constrains_in,
|
||||
created_by=current_user.name,
|
||||
simulation_id=simulation_id,
|
||||
)
|
||||
|
||||
return StandardResponse(data=results, message="Data created successfully")
|
||||
|
||||
|
||||
@router.get(
|
||||
"", response_model=StandardResponse[List[CalculationTimeConstrainsReadNoResult]]
|
||||
)
|
||||
async def get_all_simulation_calculations(
|
||||
db_session: DbSession,
|
||||
token: Token,
|
||||
current_user: CurrentUser,
|
||||
):
|
||||
"""Get all calculation time constrains Here"""
|
||||
|
||||
calculations = await get_all_calculations(db_session=db_session)
|
||||
|
||||
return StandardResponse(
|
||||
data=calculations,
|
||||
message="Data retrieved successfully",
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/parameters",
|
||||
response_model=StandardResponse[
|
||||
Union[
|
||||
CalculationTimeConstrainsParametersRetrive,
|
||||
CalculationTimeConstrainsParametersRead,
|
||||
]
|
||||
],
|
||||
)
|
||||
async def get_calculation_parameters(
|
||||
db_session: DbSession, calculation_id: Optional[str] = Query(default=None)
|
||||
):
|
||||
"""Get all calculation parameter."""
|
||||
|
||||
parameters = await get_create_calculation_parameters(
|
||||
db_session=db_session, calculation_id=calculation_id
|
||||
)
|
||||
|
||||
return StandardResponse(
|
||||
data=parameters,
|
||||
message="Data retrieved successfully",
|
||||
)
|
||||
|
||||
|
||||
@get_calculation.get(
|
||||
"/{calculation_id}", response_model=StandardResponse[CalculationTimeConstrainsRead]
|
||||
)
|
||||
async def get_calculation_results(
|
||||
db_session: DbSession,
|
||||
calculation_id,
|
||||
token: InternalKey,
|
||||
include_risk_cost: int = Query(1, alias="risk_cost"),
|
||||
):
|
||||
if calculation_id == "default":
|
||||
calculation_id = DEFAULT_TC_ID
|
||||
|
||||
results = await get_calculation_result(
|
||||
db_session=db_session,
|
||||
calculation_id=calculation_id,
|
||||
token=token,
|
||||
include_risk_cost=include_risk_cost,
|
||||
)
|
||||
|
||||
return StandardResponse(
|
||||
data=results,
|
||||
message="Data retrieved successfully",
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{calculation_id}/{assetnum}", response_model=StandardResponse[EquipmentResult]
|
||||
)
|
||||
async def get_calculation_per_equipment(db_session: DbSession, calculation_id, assetnum):
|
||||
results = await get_calculation_by_assetnum(
|
||||
db_session=db_session, assetnum=assetnum, calculation_id=calculation_id
|
||||
)
|
||||
|
||||
return StandardResponse(
|
||||
data=results,
|
||||
message="Data retrieved successfully",
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{calculation_id}/simulation",
|
||||
response_model=StandardResponse[CalculationResultsRead],
|
||||
)
|
||||
async def get_simulation_result(
|
||||
db_session: DbSession,
|
||||
calculation_id,
|
||||
calculation_simuation_in: CalculationTimeConstrainsCreate,
|
||||
):
|
||||
simulation_result = await get_calculation_result_by_day(
|
||||
db_session=db_session,
|
||||
calculation_id=calculation_id,
|
||||
simulation_day=calculation_simuation_in.intervalDays,
|
||||
)
|
||||
|
||||
return StandardResponse(
|
||||
data=simulation_result, message="Data retrieved successfully"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/update/{calculation_id}", response_model=StandardResponse[List[str]])
|
||||
async def update_selected_equipment(
|
||||
db_session: DbSession,
|
||||
calculation_id,
|
||||
calculation_time_constrains_in: List[CalculationSelectedEquipmentUpdate],
|
||||
):
|
||||
if calculation_id == "default":
|
||||
calculation_id = "3b9a73a2-bde6-418c-9e2f-19046f501a05"
|
||||
|
||||
results = await bulk_update_equipment(
|
||||
db=db_session,
|
||||
selected_equipments=calculation_time_constrains_in,
|
||||
calculation_data_id=calculation_id,
|
||||
)
|
||||
|
||||
return StandardResponse(
|
||||
data=results,
|
||||
message="Data retrieved successfully",
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{calculation_id}/refresh-spareparts", response_model=StandardResponse[dict])
|
||||
async def refresh_spareparts(
|
||||
db_session: DbSession,
|
||||
collector_db_session: CollectorDbSession,
|
||||
calculation_id: str,
|
||||
current_user: CurrentUser,
|
||||
):
|
||||
"""Refresh sparepart availability for an existing calculation"""
|
||||
|
||||
await refresh_spareparts_service(
|
||||
db_session=db_session,
|
||||
collector_db_session=collector_db_session,
|
||||
calculation_id=calculation_id,
|
||||
)
|
||||
|
||||
return StandardResponse(data={}, message="Spareparts refreshed successfully")
|
||||
|
||||
|
||||
@router.post("/{calculation_id}/recalculate", response_model=StandardResponse[CalculationTimeConstrainsRead])
|
||||
async def recalculate_calculation_api(
|
||||
db_session: DbSession,
|
||||
collector_db_session: CollectorDbSession,
|
||||
calculation_id: str,
|
||||
token: Token,
|
||||
current_user: CurrentUser,
|
||||
):
|
||||
"""Recalculate an existing simulation with fresh data"""
|
||||
|
||||
results = await recalculate_calculation(
|
||||
token=token,
|
||||
db_session=db_session,
|
||||
collector_db_session=collector_db_session,
|
||||
calculation_id=calculation_id,
|
||||
)
|
||||
|
||||
return StandardResponse(data=results, message="Calculation updated with fresh data")
|
||||
@ -1,252 +0,0 @@
|
||||
import datetime
|
||||
from typing import Coroutine, List, Optional, Tuple, Dict
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
from sqlalchemy import and_, case, func, select, update
|
||||
from sqlalchemy.orm import joinedload, selectinload
|
||||
|
||||
from src.database.core import DbSession, CollectorDbSession
|
||||
from src.workorder.model import MasterWorkOrder
|
||||
from src.calculation_time_constrains.model import (CalculationData, CalculationEquipmentResult, CalculationResult)
|
||||
from src.calculation_time_constrains.schema import (
|
||||
CalculationResultsRead,
|
||||
CalculationSelectedEquipmentUpdate,
|
||||
CalculationTimeConstrainsParametersCreate,
|
||||
CalculationTimeConstrainsRead
|
||||
)
|
||||
from src.overhaul_scope.service import get as get_scope, get_prev_oh
|
||||
|
||||
|
||||
async def create_param_and_data(
|
||||
*,
|
||||
db_session: DbSession,
|
||||
calculation_param_in: CalculationTimeConstrainsParametersCreate,
|
||||
created_by: str,
|
||||
parameter_id: Optional[UUID] = None,
|
||||
):
|
||||
"""Creates a new document."""
|
||||
if calculation_param_in.ohSessionId is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="overhaul_session_id is required",
|
||||
)
|
||||
|
||||
calculationData = await CalculationData.create_with_param(
|
||||
db=db_session,
|
||||
overhaul_session_id=calculation_param_in.ohSessionId,
|
||||
avg_failure_cost=calculation_param_in.costPerFailure,
|
||||
overhaul_cost=calculation_param_in.overhaulCost,
|
||||
created_by=created_by,
|
||||
params_id=parameter_id,
|
||||
)
|
||||
return calculationData
|
||||
|
||||
|
||||
async def get_calculation_result(db_session: DbSession, calculation_id: str, token, include_risk_cost):
|
||||
"""
|
||||
Get calculation results from DB, returning pre-calculated plant and equipment results.
|
||||
"""
|
||||
try:
|
||||
# Get calculation data with equipment results
|
||||
calculation_query = await db_session.execute(
|
||||
select(CalculationData)
|
||||
.options(selectinload(CalculationData.equipment_results))
|
||||
.where(CalculationData.id == calculation_id)
|
||||
)
|
||||
scope_calculation = calculation_query.scalar_one_or_none()
|
||||
|
||||
if not scope_calculation:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Calculation with id {calculation_id} does not exist.",
|
||||
)
|
||||
|
||||
scope_overhaul = await get_scope(
|
||||
db_session=db_session,
|
||||
overhaul_session_id=scope_calculation.overhaul_session_id
|
||||
)
|
||||
if not scope_overhaul:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Overhaul scope for session {scope_calculation.overhaul_session_id} does not exist.",
|
||||
)
|
||||
|
||||
# Parse pre-calculated plant results
|
||||
plant_results_raw = scope_calculation.plant_results or []
|
||||
calculation_results = [CalculationResultsRead(**r) for r in plant_results_raw]
|
||||
|
||||
# Return comprehensive result
|
||||
return CalculationTimeConstrainsRead(
|
||||
id=scope_calculation.id,
|
||||
reference=scope_calculation.overhaul_session_id,
|
||||
scope=scope_overhaul.maintenance_type.name,
|
||||
results=calculation_results,
|
||||
optimum_oh=scope_calculation.optimum_oh_day,
|
||||
optimum_oh_month=scope_calculation.optimum_oh_day + 1 if scope_calculation.optimum_oh_day is not None else None,
|
||||
equipment_results=scope_calculation.equipment_results,
|
||||
fleet_statistics=scope_calculation.fleet_statistics or {},
|
||||
optimal_analysis=scope_calculation.optimum_analysis or {},
|
||||
analysis_metadata=scope_calculation.analysis_metadata or {}
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.error(f"Error in get_calculation_result for calculation_id {calculation_id}: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Internal error processing calculation results: {str(e)}",
|
||||
)
|
||||
|
||||
|
||||
async def get_calculation_data_by_id(db_session: DbSession, calculation_id) -> CalculationData:
|
||||
stmt = (
|
||||
select(CalculationData)
|
||||
.filter(CalculationData.id == calculation_id)
|
||||
.options(
|
||||
joinedload(CalculationData.equipment_results),
|
||||
joinedload(CalculationData.parameter),
|
||||
)
|
||||
)
|
||||
result = await db_session.execute(stmt)
|
||||
return result.unique().scalar()
|
||||
|
||||
|
||||
async def get_all_calculations(db_session: DbSession) -> List[CalculationData]:
|
||||
stmt = (
|
||||
select(CalculationData)
|
||||
.options(selectinload(CalculationData.session))
|
||||
.where(
|
||||
CalculationData.optimum_oh_day.isnot(None),
|
||||
CalculationData.max_interval.isnot(None),
|
||||
CalculationData.optimum_analysis.isnot(None),
|
||||
)
|
||||
.order_by(CalculationData.created_at.desc())
|
||||
)
|
||||
result = await db_session.execute(stmt)
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
async def get_calculation_by_assetnum(*, db_session: DbSession, assetnum: str, calculation_id: str):
|
||||
stmt = (
|
||||
select(CalculationEquipmentResult)
|
||||
.where(CalculationEquipmentResult.assetnum == assetnum)
|
||||
.where(CalculationEquipmentResult.calculation_data_id == calculation_id)
|
||||
)
|
||||
result = await db_session.execute(stmt)
|
||||
return result.scalar()
|
||||
|
||||
|
||||
async def get_calculation_by_reference_and_parameter(
|
||||
*, db_session: DbSession, calculation_reference_id, parameter_id
|
||||
):
|
||||
stmt = select(CalculationData).filter(
|
||||
and_(
|
||||
CalculationData.reference_id == calculation_reference_id,
|
||||
CalculationData.parameter_id == parameter_id,
|
||||
)
|
||||
)
|
||||
result = await db_session.execute(stmt)
|
||||
return result.scalar()
|
||||
|
||||
|
||||
async def get_calculation_result_by_day(*, db_session: DbSession, calculation_id, simulation_day):
|
||||
stmt = select(CalculationData).filter(CalculationData.id == calculation_id)
|
||||
result = await db_session.execute(stmt)
|
||||
calculation_data = result.scalar_one_or_none()
|
||||
|
||||
if not calculation_data or not calculation_data.plant_results:
|
||||
return None
|
||||
|
||||
for res in calculation_data.plant_results:
|
||||
if res.get("day") == simulation_day:
|
||||
return res
|
||||
|
||||
return None
|
||||
|
||||
|
||||
async def get_avg_cost_by_asset(*, db_session: DbSession, assetnum: str):
|
||||
stmt = select(func.avg(MasterWorkOrder.total_cost_max).label("average_cost")).where(
|
||||
MasterWorkOrder.assetnum == assetnum
|
||||
)
|
||||
result = await db_session.execute(stmt)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def bulk_update_equipment(
|
||||
*,
|
||||
db: DbSession,
|
||||
selected_equipments: List[CalculationSelectedEquipmentUpdate],
|
||||
calculation_data_id: UUID,
|
||||
):
|
||||
case_mappings = {asset.location_tag: asset.is_included for asset in selected_equipments}
|
||||
location_tags = list(case_mappings.keys())
|
||||
|
||||
when_clauses = [
|
||||
(CalculationEquipmentResult.location_tag == location_tag, is_included)
|
||||
for location_tag, is_included in case_mappings.items()
|
||||
]
|
||||
|
||||
stmt = (
|
||||
update(CalculationEquipmentResult)
|
||||
.where(CalculationEquipmentResult.calculation_data_id == calculation_data_id)
|
||||
.where(CalculationEquipmentResult.location_tag.in_(location_tags))
|
||||
.values(
|
||||
{
|
||||
"is_included": case(*when_clauses),
|
||||
"is_initial": False
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
await db.execute(stmt)
|
||||
await db.commit()
|
||||
return location_tags
|
||||
|
||||
|
||||
async def refresh_spareparts_service(db_session: DbSession, collector_db_session: CollectorDbSession, calculation_id: str):
|
||||
stmt = select(CalculationData).where(CalculationData.id == calculation_id).options(
|
||||
joinedload(CalculationData.equipment_results)
|
||||
)
|
||||
result = await db_session.execute(stmt)
|
||||
calculation = result.scalar_one_or_none()
|
||||
|
||||
if not calculation:
|
||||
raise HTTPException(status_code=404, detail="Calculation not found")
|
||||
|
||||
scope = await get_scope(db_session=db_session, overhaul_session_id=calculation.overhaul_session_id)
|
||||
prev_oh_scope = await get_prev_oh(db_session=db_session, overhaul_session=scope)
|
||||
|
||||
last_oh_date = prev_oh_scope.end_date
|
||||
next_oh_date = scope.start_date
|
||||
time_window_months = ((next_oh_date.year - last_oh_date.year) * 12 +
|
||||
(next_oh_date.month - last_oh_date.month) + 6)
|
||||
|
||||
from src.sparepart.service import load_sparepart_data_from_db
|
||||
sparepart_manager = await load_sparepart_data_from_db(
|
||||
scope=scope,
|
||||
prev_oh_scope=prev_oh_scope,
|
||||
db_session=collector_db_session,
|
||||
app_db_session=db_session,
|
||||
analysis_window_months=time_window_months
|
||||
)
|
||||
|
||||
for eq in calculation.equipment_results:
|
||||
procurement_details = []
|
||||
procurement_costs = []
|
||||
for month_index in range(time_window_months):
|
||||
status = sparepart_manager.check_sparepart_availability(eq.location_tag, month_index)
|
||||
procurement_details.append(status)
|
||||
|
||||
if not status['available']:
|
||||
procurement_costs.append(status['total_procurement_cost'])
|
||||
else:
|
||||
procurement_costs.append(0.0)
|
||||
|
||||
eq.procurement_details = procurement_details
|
||||
eq.procurement_costs = procurement_costs
|
||||
|
||||
await db_session.commit()
|
||||
@ -1,140 +0,0 @@
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import httpx
|
||||
from temporalio import activity, workflow
|
||||
|
||||
from src.config import RBD_SERVICE_API
|
||||
from src.database.core import get_main_session, get_collector_session
|
||||
from src.optimum_time_constraint.service import (
|
||||
create_param_and_data,
|
||||
get_calculation_data_by_id
|
||||
)
|
||||
from src.optimum_time_constraint.optimizer import run_simulation_with_spareparts
|
||||
from src.calculation_time_constrains.schema import CalculationTimeConstrainsParametersCreate
|
||||
from src.overhaul_scope.service import get as get_scope, get_prev_oh
|
||||
from src.calculation_time_constrains.utils import get_months_between
|
||||
from src.calculation_target_reliability.utils import wait_for_workflow
|
||||
|
||||
@activity.defn
|
||||
async def create_optimum_oh_calculation(args: dict) -> str:
|
||||
token = args["token"]
|
||||
calc_in_dict = args["calculation_in"]
|
||||
created_by = args["created_by"]
|
||||
|
||||
calc_in = CalculationTimeConstrainsParametersCreate(**calc_in_dict)
|
||||
|
||||
async with get_main_session() as db_session:
|
||||
calc_data = await create_param_and_data(
|
||||
db_session=db_session,
|
||||
calculation_param_in=calc_in,
|
||||
created_by=created_by
|
||||
)
|
||||
return str(calc_data.id)
|
||||
|
||||
@activity.defn
|
||||
async def request_rbd_simulation(args: dict) -> str:
|
||||
calc_id = args["calc_id"]
|
||||
token = args["token"]
|
||||
|
||||
async with get_main_session() as db_session:
|
||||
calc_data = await get_calculation_data_by_id(db_session=db_session, calculation_id=calc_id)
|
||||
|
||||
scope = await get_scope(db_session=db_session, overhaul_session_id=calc_data.overhaul_session_id)
|
||||
prev_oh_scope = await get_prev_oh(db_session=db_session, overhaul_session=scope)
|
||||
|
||||
time_window_months = get_months_between(prev_oh_scope.end_date, scope.start_date) + 6
|
||||
sim_duration_hours = time_window_months * 720
|
||||
|
||||
sim_input = {
|
||||
"SchematicName": "- TJB - Unit 3 -",
|
||||
"SimulationName": f"OptimumOH_Calc_{calc_id}",
|
||||
"SimDuration": sim_duration_hours,
|
||||
"DurationUnit": "UHour",
|
||||
"OffSet": 0,
|
||||
"SimSeed": 99,
|
||||
"SimNumRun": 1,
|
||||
"OverhaulInterval": 0,
|
||||
"MaintenanceOutages": 0,
|
||||
"IsDefault": False,
|
||||
"OverhaulDuration": 0,
|
||||
"AhmJobId": None
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
resp = await client.post(
|
||||
f"{RBD_SERVICE_API}/aeros/simulation/run",
|
||||
json=sim_input,
|
||||
headers={"Authorization": f"Bearer {token}"}
|
||||
)
|
||||
resp.raise_for_status()
|
||||
sim_id = resp.json()["data"]
|
||||
|
||||
calc_data.rbd_simulation_id = sim_id
|
||||
return str(sim_id)
|
||||
|
||||
@activity.defn
|
||||
async def wait_for_rbd_simulation(sim_id: str) -> str:
|
||||
await wait_for_workflow(sim_id)
|
||||
return sim_id
|
||||
|
||||
@activity.defn
|
||||
async def run_optimum_oh_calculation(args: dict) -> dict:
|
||||
calc_id = args["calc_id"]
|
||||
token = args["token"]
|
||||
sim_id = args["sim_id"]
|
||||
|
||||
async with get_main_session() as db_session:
|
||||
async with get_collector_session() as collector_db:
|
||||
calc_data = await get_calculation_data_by_id(db_session=db_session, calculation_id=calc_id)
|
||||
results = await run_simulation_with_spareparts(
|
||||
db_session=db_session,
|
||||
calculation=calc_data,
|
||||
token=token,
|
||||
collector_db_session=collector_db,
|
||||
simulation_id=sim_id
|
||||
)
|
||||
return {"id": str(results["id"]), "optimum": results["optimum"]}
|
||||
|
||||
@workflow.defn
|
||||
class OptimumOHCalculationWorkflow:
|
||||
def __init__(self):
|
||||
self.done = False
|
||||
self.result = None
|
||||
|
||||
@workflow.signal
|
||||
def notify_done(self, result: dict):
|
||||
self.done = True
|
||||
self.result = result
|
||||
|
||||
@workflow.run
|
||||
async def run(self, args: dict) -> dict:
|
||||
calc_id = await workflow.execute_activity(
|
||||
create_optimum_oh_calculation,
|
||||
args,
|
||||
start_to_close_timeout=timedelta(minutes=1)
|
||||
)
|
||||
|
||||
args["calc_id"] = calc_id
|
||||
|
||||
sim_id = await workflow.execute_activity(
|
||||
request_rbd_simulation,
|
||||
args,
|
||||
start_to_close_timeout=timedelta(minutes=2)
|
||||
)
|
||||
|
||||
args["sim_id"] = sim_id
|
||||
|
||||
# await workflow.execute_activity(
|
||||
# wait_for_rbd_simulation,
|
||||
# sim_id,
|
||||
# start_to_close_timeout=timedelta(hours=2)
|
||||
# )
|
||||
await workflow.wait_condition(lambda: self.done)
|
||||
|
||||
result = await workflow.execute_activity(
|
||||
run_optimum_oh_calculation,
|
||||
args,
|
||||
start_to_close_timeout=timedelta(minutes=30)
|
||||
)
|
||||
|
||||
return result
|
||||
@ -0,0 +1 @@
|
||||
|
||||
@ -1,68 +1,35 @@
|
||||
from typing import List
|
||||
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
|
||||
from fastapi import APIRouter
|
||||
from src.auth.service import Token
|
||||
from src.database.core import CollectorDbSession, DbSession
|
||||
from src.models import StandardResponse
|
||||
from src.overhaul.service import (get_overhaul_critical_parts,
|
||||
get_overhaul_overview,
|
||||
get_overhaul_schedules,
|
||||
get_overhaul_system_components)
|
||||
from src.overhaul.service import get_overhaul_critical_parts, get_overhaul_overview, get_overhaul_schedules, get_overhaul_system_components
|
||||
from src.overhaul_scope.schema import ScopeRead
|
||||
|
||||
from .schema import (OverhaulCriticalParts, OverhaulRead,
|
||||
OverhaulSystemComponents)
|
||||
|
||||
from .schema import OverhaulCriticalParts, OverhaulRead, OverhaulSystemComponents
|
||||
from src.auth.access_control import required_permission, OHPermission
|
||||
from src.overhaul_scope.model import OverhaulScope
|
||||
from fastapi import Depends
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("", response_model=StandardResponse[OverhaulRead])
|
||||
async def get_overhaul(db_session: DbSession, token:Token, collector_db_session:CollectorDbSession):
|
||||
"""Get all scope pagination."""
|
||||
@router.get('', response_model=StandardResponse[OverhaulRead], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
||||
async def get_overhaul(db_session: DbSession, token: Token, collector_db_session: CollectorDbSession):
|
||||
overview = await get_overhaul_overview(db_session=db_session)
|
||||
schedules = await get_overhaul_schedules(db_session=db_session)
|
||||
criticalParts = await get_overhaul_critical_parts(db_session=db_session, session_id=overview["overhaul"]["id"], token=token, collector_db_session=collector_db_session)
|
||||
criticalParts = await get_overhaul_critical_parts(db_session=db_session, session_id=overview['overhaul']['id'], token=token, collector_db_session=collector_db_session)
|
||||
systemComponents = get_overhaul_system_components()
|
||||
return StandardResponse(data=OverhaulRead(overview=overview, schedules=schedules, criticalParts=criticalParts, systemComponents=systemComponents), message='Data retrieved successfully')
|
||||
|
||||
return StandardResponse(
|
||||
data=OverhaulRead(
|
||||
overview=overview,
|
||||
schedules=schedules,
|
||||
criticalParts=criticalParts,
|
||||
systemComponents=systemComponents,
|
||||
),
|
||||
message="Data retrieved successfully",
|
||||
)
|
||||
|
||||
|
||||
@router.get("/schedules", response_model=StandardResponse[List[ScopeRead]])
|
||||
@router.get('/schedules', response_model=StandardResponse[List[ScopeRead]], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
||||
async def get_schedules():
|
||||
"""Get all overhaul schedules."""
|
||||
schedules = get_overhaul_schedules()
|
||||
return StandardResponse(
|
||||
data=schedules,
|
||||
message="Data retrieved successfully",
|
||||
)
|
||||
return StandardResponse(data=schedules, message='Data retrieved successfully')
|
||||
|
||||
|
||||
@router.get("/critical-parts", response_model=StandardResponse[OverhaulCriticalParts])
|
||||
@router.get('/critical-parts', response_model=StandardResponse[OverhaulCriticalParts], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
||||
async def get_critical_parts():
|
||||
"""Get all overhaul critical parts."""
|
||||
criticalParts = get_overhaul_critical_parts()
|
||||
return StandardResponse(
|
||||
data=OverhaulCriticalParts(criticalParts=criticalParts),
|
||||
message="Data retrieved successfully",
|
||||
)
|
||||
|
||||
return StandardResponse(data=OverhaulCriticalParts(criticalParts=criticalParts), message='Data retrieved successfully')
|
||||
|
||||
@router.get(
|
||||
"/system-components", response_model=StandardResponse[OverhaulSystemComponents]
|
||||
)
|
||||
@router.get('/system-components', response_model=StandardResponse[OverhaulSystemComponents], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
||||
async def get_system_components():
|
||||
"""Get all overhaul system components."""
|
||||
systemComponents = get_overhaul_system_components()
|
||||
return StandardResponse(
|
||||
data=OverhaulSystemComponents(systemComponents=systemComponents),
|
||||
message="Data retrieved successfully",
|
||||
)
|
||||
return StandardResponse(data=OverhaulSystemComponents(systemComponents=systemComponents), message='Data retrieved successfully')
|
||||
|
||||
@ -1,355 +1,51 @@
|
||||
import asyncio
|
||||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
from sqlalchemy import Delete, Select
|
||||
|
||||
from src.auth.service import CurrentUser
|
||||
from sqlalchemy import Select
|
||||
from src.calculation_target_reliability.service import RBD_SERVICE_API
|
||||
from src.config import TC_RBD_ID
|
||||
from src.database.core import DbSession
|
||||
from src.contribution_util import calculate_contribution
|
||||
from src.overhaul_activity.service import get_standard_scope_by_session_id
|
||||
from src.overhaul_scope.model import OverhaulScope
|
||||
from src.overhaul_scope.service import get_all as get_all_session
|
||||
from src.overhaul_scope.service import get_overview_overhaul
|
||||
from src.standard_scope.service import get_by_oh_session_id
|
||||
|
||||
|
||||
async def get_overhaul_overview(db_session: DbSession):
|
||||
"""Get all overhaul overview."""
|
||||
results = await get_overview_overhaul(db_session=db_session)
|
||||
|
||||
return results
|
||||
|
||||
return await get_overview_overhaul(db_session=db_session)
|
||||
|
||||
async def get_simulation_results(*, simulation_id: str, token: str):
|
||||
headers = {
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
calc_result_url = f"{RBD_SERVICE_API}/aeros/simulation/result/calc/{simulation_id}?nodetype=RegularNode"
|
||||
# plot_result_url = f"{RBD_SERVICE_API}/aeros/simulation/result/plot/{simulation_id}?nodetype=RegularNode"
|
||||
calc_plant_result = f"{RBD_SERVICE_API}/aeros/simulation/result/calc/{simulation_id}/plant"
|
||||
|
||||
headers = {'Authorization': f'Bearer {token}', 'Content-Type': 'application/json'}
|
||||
calc_result_url = f'{RBD_SERVICE_API}/aeros/simulation/result/calc/{simulation_id}?nodetype=RegularNode'
|
||||
calc_plant_result = f'{RBD_SERVICE_API}/aeros/simulation/result/calc/{simulation_id}/plant'
|
||||
async with httpx.AsyncClient(timeout=300.0) as client:
|
||||
calc_task = client.get(calc_result_url, headers=headers)
|
||||
# plot_task = client.get(plot_result_url, headers=headers)
|
||||
plant_task = client.get(calc_plant_result, headers=headers)
|
||||
|
||||
# Run all three requests concurrently
|
||||
calc_response, plant_response = await asyncio.gather(calc_task, plant_task)
|
||||
|
||||
calc_response.raise_for_status()
|
||||
# plot_response.raise_for_status()
|
||||
plant_response.raise_for_status()
|
||||
|
||||
calc_data = calc_response.json()["data"]
|
||||
# plot_data = plot_response.json()["data"]
|
||||
plant_data = plant_response.json()["data"]
|
||||
|
||||
return {
|
||||
"calc_result": calc_data,
|
||||
# "plot_result": plot_data,
|
||||
"plant_result": plant_data
|
||||
}
|
||||
calc_data = calc_response.json()['data']
|
||||
plant_data = plant_response.json()['data']
|
||||
return {'calc_result': calc_data, 'plant_result': plant_data}
|
||||
|
||||
async def get_overhaul_critical_parts(db_session, session_id, token, collector_db_session):
|
||||
"""Get all overhaul critical parts."""
|
||||
equipments = await get_standard_scope_by_session_id(
|
||||
db_session=db_session,
|
||||
overhaul_session_id=session_id,
|
||||
collector_db=collector_db_session
|
||||
)
|
||||
|
||||
|
||||
criticality_simulation = await get_simulation_results(
|
||||
simulation_id = TC_RBD_ID,
|
||||
token=token
|
||||
)
|
||||
|
||||
|
||||
|
||||
rbd_simulation = {asset['aeros_node']["node_name"]: {
|
||||
"availability": asset["availability"],
|
||||
"criticality": asset["criticality"]
|
||||
} for asset in criticality_simulation["calc_result"]}
|
||||
|
||||
# Create the base result list
|
||||
base_result = [
|
||||
{
|
||||
"id": equipment.id,
|
||||
"location_tag": equipment.location_tag,
|
||||
"name": equipment.equipment_name,
|
||||
"matrix": rbd_simulation.get(equipment.location_tag)
|
||||
|
||||
} for equipment in equipments
|
||||
]
|
||||
|
||||
|
||||
# Filter out items without matrix data (where rbd_simulation.get() returned None)
|
||||
filtered_result = [item for item in base_result if item["matrix"] is not None]
|
||||
|
||||
# Sort by availability (lowest to highest) and limit to 10
|
||||
availability_result = sorted(
|
||||
filtered_result,
|
||||
key=lambda x: x["matrix"]["availability"]
|
||||
)[:10]
|
||||
|
||||
# Sort by criticality (highest to lowest) and limit to 10
|
||||
criticality_result = sorted(
|
||||
filtered_result,
|
||||
key=lambda x: x["matrix"]["criticality"],
|
||||
reverse=True
|
||||
)[:10]
|
||||
|
||||
return {
|
||||
"availability" :availability_result,
|
||||
"criticality": criticality_result
|
||||
}
|
||||
|
||||
equipments = await get_standard_scope_by_session_id(db_session=db_session, overhaul_session_id=session_id, collector_db=collector_db_session)
|
||||
criticality_simulation = await get_simulation_results(simulation_id=TC_RBD_ID, token=token)
|
||||
rbd_simulation = {asset['aeros_node']['node_name']: {'availability': asset['availability'], 'criticality': asset['criticality']} for asset in criticality_simulation['calc_result']}
|
||||
base_result = [{'id': equipment.id, 'location_tag': equipment.location_tag, 'name': equipment.equipment_name, 'matrix': rbd_simulation.get(equipment.location_tag)} for equipment in equipments]
|
||||
filtered_result = [item for item in base_result if item['matrix'] is not None]
|
||||
availability_result = sorted(filtered_result, key=lambda x: x['matrix']['availability'])[:10]
|
||||
criticality_result = sorted(filtered_result, key=lambda x: x['matrix']['criticality'], reverse=True)[:10]
|
||||
return {'availability': availability_result, 'criticality': criticality_result}
|
||||
|
||||
async def get_overhaul_schedules(*, db_session: DbSession):
|
||||
"""Get all overhaul schedules."""
|
||||
query = Select(OverhaulScope)
|
||||
|
||||
results = await db_session.execute(query)
|
||||
|
||||
return results.scalars().all()
|
||||
|
||||
|
||||
def get_overhaul_system_components():
|
||||
"""Get all overhaul system components with dummy data."""
|
||||
|
||||
powerplant_reliability = {
|
||||
"Plant Control": {
|
||||
"availability": 0.9994866529774127,
|
||||
"efficiency": 0.9994956204510826,
|
||||
"total_uptime": 17523.0,
|
||||
},
|
||||
"SPS": {
|
||||
"availability": 0.9932694501483038,
|
||||
"efficiency": 0.9810193821516867,
|
||||
"total_uptime": 17414.000000000062,
|
||||
},
|
||||
"Turbine": {
|
||||
"availability": 0.9931553730321733,
|
||||
"efficiency": 0.9666721976783572,
|
||||
"total_uptime": 17412.000000000062,
|
||||
},
|
||||
"Generator": {
|
||||
"availability": 0.9934405658224995,
|
||||
"efficiency": 0.9625424646119242,
|
||||
"total_uptime": 17417.000000000062,
|
||||
},
|
||||
"Condensate Water": {
|
||||
"availability": 0.9934405658224995,
|
||||
"efficiency": 0.9531653517377116,
|
||||
"total_uptime": 17417.000000000062,
|
||||
},
|
||||
"Feedwater System": {
|
||||
"availability": 0.9936116814966953,
|
||||
"efficiency": 0.9687512254370128,
|
||||
"total_uptime": 17420.000000000062,
|
||||
},
|
||||
"Cooling Water": {
|
||||
"availability": 0.99355464293863,
|
||||
"efficiency": 0.9749999189617731,
|
||||
"total_uptime": 17419.000000000062,
|
||||
},
|
||||
"SCR": {
|
||||
"availability": 0.9196577686516085,
|
||||
"efficiency": 0.9996690612127086,
|
||||
"total_uptime": 17526.0,
|
||||
},
|
||||
|
||||
"Ash Handling": {
|
||||
"availability": 0.9931838923112059,
|
||||
"efficiency": 0.9933669638506657,
|
||||
"total_uptime": 17412.500000000062,
|
||||
},
|
||||
"Air Flue Gas": {
|
||||
"availability": 0.9906456764773022,
|
||||
"efficiency": 0.8565868045084128,
|
||||
"total_uptime": 17368.000000000062,
|
||||
},
|
||||
"Boiler": {
|
||||
"availability": 0.9934976043805648,
|
||||
"efficiency": 0.9693239775686654,
|
||||
"total_uptime": 17418.000000000062,
|
||||
},
|
||||
"SAC-IAC": {
|
||||
"availability": 0.9936116814966953,
|
||||
"efficiency": 0.9878742473840645,
|
||||
"total_uptime": 17420.000000000062,
|
||||
},
|
||||
"KLH": {
|
||||
"availability": 0.992185717545064,
|
||||
"efficiency": 0.9426658166507496,
|
||||
"total_uptime": 17395.000000000062,
|
||||
},
|
||||
"CL": {
|
||||
"availability": 0.9984029203741729,
|
||||
"efficiency": 0.9984546987034779,
|
||||
"total_uptime": 17504.0,
|
||||
},
|
||||
"Desalination": {
|
||||
"total_uptime": 17275.500000000062,
|
||||
"availability": 0.9853696098562663,
|
||||
"efficiency": 0.9118366404915063,
|
||||
|
||||
},
|
||||
"FGD": {
|
||||
"availability": 0.9933835272644342,
|
||||
"efficiency": 0.9623105228919693,
|
||||
"total_uptime": 17416.000000000062,
|
||||
},
|
||||
"CHS": {
|
||||
"availability": 1.0,
|
||||
"efficiency": 0.9665857756206829,
|
||||
"total_uptime": 17532.0,
|
||||
},
|
||||
"SSB": {
|
||||
"availability": 0.9933264887063691,
|
||||
"efficiency": 0.993508327346586,
|
||||
"total_uptime": 17415.000000000062,
|
||||
|
||||
},
|
||||
"WTP": {
|
||||
"availability": 0.9925849874515208,
|
||||
"efficiency": 0.9925849874515206,
|
||||
"total_uptime": 17402.000000000062,
|
||||
},
|
||||
}
|
||||
availabilities = {schematic: item['availability'] for schematic, item in powerplant_reliability.items() }
|
||||
|
||||
|
||||
powerplant_reliability = {'Plant Control': {'availability': 0.9994866529774127, 'efficiency': 0.9994956204510826, 'total_uptime': 17523.0}, 'SPS': {'availability': 0.9932694501483038, 'efficiency': 0.9810193821516867, 'total_uptime': 17414.000000000062}, 'Turbine': {'availability': 0.9931553730321733, 'efficiency': 0.9666721976783572, 'total_uptime': 17412.000000000062}, 'Generator': {'availability': 0.9934405658224995, 'efficiency': 0.9625424646119242, 'total_uptime': 17417.000000000062}, 'Condensate Water': {'availability': 0.9934405658224995, 'efficiency': 0.9531653517377116, 'total_uptime': 17417.000000000062}, 'Feedwater System': {'availability': 0.9936116814966953, 'efficiency': 0.9687512254370128, 'total_uptime': 17420.000000000062}, 'Cooling Water': {'availability': 0.99355464293863, 'efficiency': 0.9749999189617731, 'total_uptime': 17419.000000000062}, 'SCR': {'availability': 0.9196577686516085, 'efficiency': 0.9996690612127086, 'total_uptime': 17526.0}, 'Ash Handling': {'availability': 0.9931838923112059, 'efficiency': 0.9933669638506657, 'total_uptime': 17412.500000000062}, 'Air Flue Gas': {'availability': 0.9906456764773022, 'efficiency': 0.8565868045084128, 'total_uptime': 17368.000000000062}, 'Boiler': {'availability': 0.9934976043805648, 'efficiency': 0.9693239775686654, 'total_uptime': 17418.000000000062}, 'SAC-IAC': {'availability': 0.9936116814966953, 'efficiency': 0.9878742473840645, 'total_uptime': 17420.000000000062}, 'KLH': {'availability': 0.992185717545064, 'efficiency': 0.9426658166507496, 'total_uptime': 17395.000000000062}, 'CL': {'availability': 0.9984029203741729, 'efficiency': 0.9984546987034779, 'total_uptime': 17504.0}, 'Desalination': {'total_uptime': 17275.500000000062, 'availability': 0.9853696098562663, 'efficiency': 0.9118366404915063}, 'FGD': {'availability': 0.9933835272644342, 'efficiency': 0.9623105228919693, 'total_uptime': 17416.000000000062}, 'CHS': {'availability': 1.0, 'efficiency': 0.9665857756206829, 'total_uptime': 17532.0}, 'SSB': {'availability': 0.9933264887063691, 'efficiency': 0.993508327346586, 'total_uptime': 17415.000000000062}, 'WTP': {'availability': 0.9925849874515208, 'efficiency': 0.9925849874515206, 'total_uptime': 17402.000000000062}}
|
||||
availabilities = {schematic: item['availability'] for schematic, item in powerplant_reliability.items()}
|
||||
percentages = calculate_contribution(availabilities)
|
||||
|
||||
for schema, contribution in percentages.items():
|
||||
powerplant_reliability[schema]["critical_contribution"] = contribution['criticality_importance']
|
||||
|
||||
# Sort the powerplant_reliability dictionary by critical_contribution in descending order
|
||||
sorted_powerplant_reliability = dict(sorted(
|
||||
powerplant_reliability.items(),
|
||||
key=lambda x: x[1]["critical_contribution"],
|
||||
reverse=True # Set to True for high to low sorting
|
||||
))
|
||||
|
||||
return sorted_powerplant_reliability
|
||||
|
||||
return {
|
||||
"HPT": {
|
||||
"efficiency": "92%",
|
||||
"work_hours": "1200",
|
||||
"reliability": "96%",
|
||||
},
|
||||
"IPT": {
|
||||
"efficiency": "91%",
|
||||
"work_hours": "1100",
|
||||
"reliability": "95%",
|
||||
},
|
||||
"LPT": {
|
||||
"efficiency": "90%",
|
||||
"work_hours": "1000",
|
||||
"reliability": "94%",
|
||||
},
|
||||
"EG": {
|
||||
"efficiency": "88%",
|
||||
"work_hours": "950",
|
||||
"reliability": "93%",
|
||||
},
|
||||
"boiler": {
|
||||
"efficiency": "90%",
|
||||
"work_hours": "1000",
|
||||
"reliability": "95%",
|
||||
},
|
||||
"HPH1": {
|
||||
"efficiency": "89%",
|
||||
"work_hours": "1050",
|
||||
"reliability": "94%",
|
||||
},
|
||||
"HPH2": {
|
||||
"efficiency": "88%",
|
||||
"work_hours": "1020",
|
||||
"reliability": "93%",
|
||||
},
|
||||
"HPH3": {
|
||||
"efficiency": "87%",
|
||||
"work_hours": "1010",
|
||||
"reliability": "92%",
|
||||
},
|
||||
"HPH5": {
|
||||
"efficiency": "86%",
|
||||
"work_hours": "980",
|
||||
"reliability": "91%",
|
||||
},
|
||||
"HPH6": {
|
||||
"efficiency": "85%",
|
||||
"work_hours": "970",
|
||||
"reliability": "90%",
|
||||
},
|
||||
"HPH7": {
|
||||
"efficiency": "84%",
|
||||
"work_hours": "960",
|
||||
"reliability": "89%",
|
||||
},
|
||||
"Condensor": {
|
||||
"efficiency": "83%",
|
||||
"work_hours": "940",
|
||||
"reliability": "88%",
|
||||
},
|
||||
"Deaerator": {
|
||||
"efficiency": "82%",
|
||||
"work_hours": "930",
|
||||
"reliability": "87%",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# async def get(*, db_session: DbSession, scope_id: str) -> Optional[Scope]:
|
||||
# """Returns a document based on the given document id."""
|
||||
# query = Select(Scope).filter(Scope.id == scope_id)
|
||||
# result = await db_session.execute(query)
|
||||
# return result.scalars().one_or_none()
|
||||
|
||||
|
||||
# async def get_all(*, db_session: DbSession):
|
||||
# """Returns all documents."""
|
||||
# query = Select(Scope)
|
||||
# result = await db_session.execute(query)
|
||||
# return result.scalars().all()
|
||||
|
||||
|
||||
# async def create(*, db_session: DbSession, scope_id: ScopeCreate):
|
||||
# """Creates a new document."""
|
||||
# scope = Scope(**scope_id.model_dump())
|
||||
# db_session.add(scope)
|
||||
# await db_session.commit()
|
||||
# return scope
|
||||
|
||||
|
||||
# async def update(*, db_session: DbSession, scope: Scope, scope_id: ScopeUpdate):
|
||||
# """Updates a document."""
|
||||
# data = scope_id.model_dump()
|
||||
|
||||
# update_data = scope_id.model_dump(exclude_defaults=True)
|
||||
|
||||
# for field in data:
|
||||
# if field in update_data:
|
||||
# setattr(scope, field, update_data[field])
|
||||
|
||||
# await db_session.commit()
|
||||
|
||||
# return scope
|
||||
|
||||
|
||||
# async def delete(*, db_session: DbSession, scope_id: str):
|
||||
# """Deletes a document."""
|
||||
# query = Delete(Scope).where(Scope.id == scope_id)
|
||||
# await db_session.execute(query)
|
||||
# await db_session.commit()
|
||||
powerplant_reliability[schema]['critical_contribution'] = contribution['criticality_importance']
|
||||
return dict(sorted(powerplant_reliability.items(), key=lambda x: x[1]['critical_contribution'], reverse=True))
|
||||
return {'HPT': {'efficiency': '92%', 'work_hours': '1200', 'reliability': '96%'}, 'IPT': {'efficiency': '91%', 'work_hours': '1100', 'reliability': '95%'}, 'LPT': {'efficiency': '90%', 'work_hours': '1000', 'reliability': '94%'}, 'EG': {'efficiency': '88%', 'work_hours': '950', 'reliability': '93%'}, 'boiler': {'efficiency': '90%', 'work_hours': '1000', 'reliability': '95%'}, 'HPH1': {'efficiency': '89%', 'work_hours': '1050', 'reliability': '94%'}, 'HPH2': {'efficiency': '88%', 'work_hours': '1020', 'reliability': '93%'}, 'HPH3': {'efficiency': '87%', 'work_hours': '1010', 'reliability': '92%'}, 'HPH5': {'efficiency': '86%', 'work_hours': '980', 'reliability': '91%'}, 'HPH6': {'efficiency': '85%', 'work_hours': '970', 'reliability': '90%'}, 'HPH7': {'efficiency': '84%', 'work_hours': '960', 'reliability': '89%'}, 'Condensor': {'efficiency': '83%', 'work_hours': '940', 'reliability': '88%'}, 'Deaerator': {'efficiency': '82%', 'work_hours': '930', 'reliability': '87%'}}
|
||||
|
||||
@ -0,0 +1 @@
|
||||
|
||||
@ -1,43 +1,11 @@
|
||||
from sqlalchemy import UUID, Column, Float, ForeignKey, Integer, String
|
||||
from sqlalchemy.ext.hybrid import hybrid_property
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from sqlalchemy import UUID, Column, Float, ForeignKey, String
|
||||
from src.database.core import Base
|
||||
from src.models import DefaultMixin, IdentityMixin, TimeStampMixin
|
||||
from src.workorder.model import MasterWorkOrder
|
||||
|
||||
from src.models import DefaultMixin
|
||||
|
||||
class OverhaulActivity(Base, DefaultMixin):
|
||||
__tablename__ = "oh_tr_overhaul_activity"
|
||||
|
||||
__tablename__ = 'oh_tr_overhaul_activity'
|
||||
assetnum = Column(String, nullable=True)
|
||||
overhaul_scope_id = Column(
|
||||
UUID(as_uuid=True), ForeignKey("oh_ms_overhaul_scope.id"), nullable=False
|
||||
)
|
||||
overhaul_scope_id = Column(UUID(as_uuid=True), ForeignKey('oh_ms_overhaul_scope.id'), nullable=False)
|
||||
material_cost = Column(Float, nullable=False, default=0)
|
||||
service_cost = Column(Float, nullable=False, default=0)
|
||||
status = Column(String, nullable=False, default="pending")
|
||||
|
||||
# equipment = relationship(
|
||||
# "MasterEquipment",
|
||||
# lazy="raise",
|
||||
# primaryjoin="and_(OverhaulActivity.assetnum == foreign(MasterEquipment.assetnum))",
|
||||
# uselist=False, # Add this if it's a one-to-one relationship
|
||||
# )
|
||||
|
||||
# # sparepart_equipments = relationship(
|
||||
# # "ScopeEquipmentPart",
|
||||
# # lazy="select", # or "joined", "subquery", "dynamic" depending on your needs
|
||||
# # primaryjoin="OverhaulActivity.assetnum == foreign(ScopeEquipmentPart.assetnum)",
|
||||
# # uselist=True
|
||||
# # )
|
||||
|
||||
|
||||
# overhaul_scope = relationship(
|
||||
# "OverhaulScope",
|
||||
# lazy="raise",
|
||||
# )
|
||||
|
||||
# overhaul_jobs = relationship(
|
||||
# "OverhaulJob", back_populates="overhaul_activity", lazy="raise"
|
||||
# )
|
||||
status = Column(String, nullable=False, default='pending')
|
||||
|
||||
@ -1,116 +1,33 @@
|
||||
from typing import List, Optional
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query, status
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from src.csrf_protect import csrf_protect
|
||||
from src.database.core import CollectorDbSession
|
||||
from src.database.service import (CommonParameters, DbSession,
|
||||
search_filter_sort_paginate)
|
||||
from src.database.service import CommonParameters, DbSession
|
||||
from src.models import StandardResponse
|
||||
|
||||
from .schema import (OverhaulActivityCreate, OverhaulActivityPagination,
|
||||
OverhaulActivityRead, OverhaulActivityUpdate)
|
||||
from .service import add_multiple_equipment_to_session, get, get_all, remove_equipment_from_session, update
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{overhaul_session}", response_model=StandardResponse[dict]
|
||||
)
|
||||
async def get_scope_equipments(
|
||||
common: CommonParameters,
|
||||
overhaul_session: str,
|
||||
collector_db: CollectorDbSession,
|
||||
location_tag: Optional[str] = Query(None),
|
||||
scope_name: Optional[str] = Query(None),
|
||||
):
|
||||
"""Get all scope activity pagination."""
|
||||
# return
|
||||
data = await get_all(
|
||||
common=common,
|
||||
location_tag=location_tag,
|
||||
scope_name=scope_name,
|
||||
overhaul_session_id=overhaul_session,
|
||||
collector_db=collector_db,
|
||||
)
|
||||
|
||||
|
||||
return StandardResponse(
|
||||
data=data,
|
||||
message="Data retrieved successfully",
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{overhaul_session_id}", response_model=StandardResponse[None])
|
||||
async def create_overhaul_equipment(
|
||||
db_session: DbSession,
|
||||
collector_db_session: CollectorDbSession,
|
||||
overhaul_activty_in: OverhaulActivityCreate,
|
||||
overhaul_session_id: UUID,
|
||||
):
|
||||
|
||||
activity = await add_multiple_equipment_to_session(
|
||||
db_session=db_session,
|
||||
collector_db=collector_db_session,
|
||||
overhaul_session_id=overhaul_session_id,
|
||||
location_tags=overhaul_activty_in.location_tags
|
||||
)
|
||||
|
||||
return StandardResponse(data=None, message="Data created successfully")
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{overhaul_session}/{assetnum}",
|
||||
response_model=StandardResponse[OverhaulActivityRead],
|
||||
)
|
||||
async def get_overhaul_equipment(
|
||||
db_session: DbSession, assetnum: str, overhaul_session
|
||||
):
|
||||
equipment = await get(
|
||||
db_session=db_session, assetnum=assetnum, overhaul_session_id=overhaul_session
|
||||
)
|
||||
from src.auth.access_control import require_any_role, ALLOWED_ROLES
|
||||
from .schema import OverhaulActivityCreate, OverhaulActivityRead
|
||||
from .service import add_multiple_equipment_to_session, get, get_all, remove_equipment_from_session
|
||||
router = APIRouter(dependencies=[Depends(require_any_role(*ALLOWED_ROLES))])
|
||||
|
||||
@router.get('/{overhaul_session}', response_model=StandardResponse[dict])
|
||||
async def get_scope_equipments(common: CommonParameters, overhaul_session: str, collector_db: CollectorDbSession, location_tag: Optional[str]=Query(None), scope_name: Optional[str]=Query(None)):
|
||||
data = await get_all(common=common, location_tag=location_tag, scope_name=scope_name, overhaul_session_id=overhaul_session, collector_db=collector_db)
|
||||
return StandardResponse(data=data, message='Data retrieved successfully')
|
||||
|
||||
@router.post('/{overhaul_session_id}', response_model=StandardResponse[None])
|
||||
async def create_overhaul_equipment(db_session: DbSession, collector_db_session: CollectorDbSession, overhaul_activty_in: OverhaulActivityCreate, overhaul_session_id: UUID):
|
||||
await add_multiple_equipment_to_session(db_session=db_session, collector_db=collector_db_session, overhaul_session_id=overhaul_session_id, location_tags=overhaul_activty_in.location_tags)
|
||||
return StandardResponse(data=None, message='Data created successfully')
|
||||
|
||||
@router.get('/{overhaul_session}/{assetnum}', response_model=StandardResponse[OverhaulActivityRead])
|
||||
async def get_overhaul_equipment(db_session: DbSession, assetnum: str, overhaul_session):
|
||||
equipment = await get(db_session=db_session, assetnum=assetnum, overhaul_session_id=overhaul_session)
|
||||
if not equipment:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="A data with this id does not exist.",
|
||||
)
|
||||
|
||||
return StandardResponse(data=equipment, message="Data retrieved successfully")
|
||||
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='A data with this id does not exist.')
|
||||
return StandardResponse(data=equipment, message='Data retrieved successfully')
|
||||
|
||||
# @router.put(
|
||||
# "/{overhaul_session}/{assetnum}",
|
||||
# response_model=StandardResponse[OverhaulActivityRead],
|
||||
# )
|
||||
# async def update_scope(
|
||||
# db_session: DbSession,
|
||||
# scope_equipment_activity_in: OverhaulActivityUpdate,
|
||||
# assetnum: str,
|
||||
# ):
|
||||
# activity = await get(db_session=db_session, assetnum=assetnum)
|
||||
|
||||
# if not activity:
|
||||
# raise HTTPException(
|
||||
# status_code=status.HTTP_404_NOT_FOUND,
|
||||
# detail="A data with this id does not exist.",
|
||||
# )
|
||||
|
||||
# return StandardResponse(
|
||||
# data=await update(
|
||||
# db_session=db_session,
|
||||
# activity=activity,
|
||||
# scope_equipment_activity_in=scope_equipment_activity_in,
|
||||
# ),
|
||||
# message="Data updated successfully",
|
||||
# )
|
||||
|
||||
|
||||
@router.post(
|
||||
"/delete/{overhaul_session}/{location_tag}",
|
||||
response_model=StandardResponse[None],
|
||||
)
|
||||
async def delete_scope(db_session: DbSession, location_tag: str, overhaul_session:UUID):
|
||||
@router.post('/delete/{overhaul_session}/{location_tag}', response_model=StandardResponse[None], dependencies=[Depends(csrf_protect)])
|
||||
async def delete_scope(db_session: DbSession, location_tag: str, overhaul_session: UUID):
|
||||
await remove_equipment_from_session(db_session=db_session, overhaul_session_id=overhaul_session, location_tag=location_tag)
|
||||
|
||||
return StandardResponse(message="Data deleted successfully", data=None)
|
||||
return StandardResponse(message='Data deleted successfully', data=None)
|
||||
|
||||
@ -1,37 +1,24 @@
|
||||
from decimal import Decimal, getcontext
|
||||
|
||||
|
||||
def get_material_cost(scope, total_equipment):
|
||||
# Set precision to 28 digits (maximum precision for Decimal)
|
||||
getcontext().prec = 28
|
||||
|
||||
if not total_equipment: # Guard against division by zero
|
||||
if not total_equipment:
|
||||
return float(0)
|
||||
|
||||
cost = 365539731101 / 10
|
||||
|
||||
if scope == "B":
|
||||
result = Decimal(f"{cost}") / Decimal(str(total_equipment))
|
||||
return float(result)
|
||||
else:
|
||||
result = Decimal("8565468127") / Decimal(str(total_equipment))
|
||||
if scope == 'B':
|
||||
result = Decimal(f'{cost}') / Decimal(str(total_equipment))
|
||||
return float(result)
|
||||
|
||||
result = Decimal('8565468127') / Decimal(str(total_equipment))
|
||||
return float(result)
|
||||
return float(0)
|
||||
|
||||
|
||||
def get_service_cost(scope, total_equipment):
|
||||
# Set precision to 28 digits (maximum precision for Decimal)
|
||||
getcontext().prec = 28
|
||||
|
||||
if not total_equipment: # Guard against division by zero
|
||||
if not total_equipment:
|
||||
return float(0)
|
||||
|
||||
if scope == "B":
|
||||
result = Decimal("36405830225") / Decimal(str(total_equipment))
|
||||
if scope == 'B':
|
||||
result = Decimal('36405830225') / Decimal(str(total_equipment))
|
||||
return float(result)
|
||||
else:
|
||||
result = Decimal("36000000000") / Decimal(str(total_equipment))
|
||||
return float(result)
|
||||
|
||||
result = Decimal('36000000000') / Decimal(str(total_equipment))
|
||||
return float(result)
|
||||
return float(0)
|
||||
|
||||
@ -0,0 +1 @@
|
||||
|
||||
@ -1,17 +1,8 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
from sqlalchemy import Column, String
|
||||
from src.database.core import Base
|
||||
from src.models import DefaultMixin
|
||||
|
||||
|
||||
class OverhaulGantt(Base, DefaultMixin):
|
||||
__tablename__ = "oh_ms_monitoring_spreadsheet"
|
||||
|
||||
__tablename__ = 'oh_ms_monitoring_spreadsheet'
|
||||
spreadsheet_id = Column(String, nullable=True)
|
||||
spreadsheet_link = Column(String, nullable=True)
|
||||
|
||||
@ -1,142 +1,46 @@
|
||||
import re
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy import select
|
||||
|
||||
from src.auth.service import CurrentUser
|
||||
from src.auth.service import Admin
|
||||
from src.csrf_protect import csrf_protect
|
||||
from src.database.core import DbSession
|
||||
from src.database.service import CommonParameters
|
||||
from src.models import StandardResponse
|
||||
from src.overhaul_gantt.model import OverhaulGantt
|
||||
from src.overhaul_gantt.schema import OverhaulGanttIn
|
||||
|
||||
# from .schema import (OverhaulScheduleCreate, OverhaulSchedulePagination, OverhaulScheduleUpdate)
|
||||
from .service import get_gantt_performance_chart
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get(
|
||||
"", response_model=StandardResponse[dict]
|
||||
)
|
||||
@router.get('', response_model=StandardResponse[dict])
|
||||
async def get_gantt_performance(db_session: DbSession):
|
||||
"""Get all scope pagination."""
|
||||
# return
|
||||
query = select(OverhaulGantt).limit(1)
|
||||
|
||||
data = (await db_session.execute(query)).scalar_one_or_none()
|
||||
|
||||
results, gantt_data = await get_gantt_performance_chart(spreadsheet_id=data.spreadsheet_id)
|
||||
return StandardResponse(data={'chart_data': results, 'gantt_data': gantt_data}, message='Data retrieved successfully')
|
||||
|
||||
return StandardResponse(
|
||||
data={
|
||||
"chart_data": results,
|
||||
"gantt_data": gantt_data
|
||||
},
|
||||
message="Data retrieved successfully",
|
||||
)
|
||||
|
||||
@router.get(
|
||||
"/spreadsheet", response_model=StandardResponse[dict]
|
||||
)
|
||||
@router.get('/spreadsheet', response_model=StandardResponse[dict])
|
||||
async def get_gantt_spreadsheet(db_session: DbSession):
|
||||
"""Get all scope pagination."""
|
||||
# return
|
||||
query = select(OverhaulGantt).limit(1)
|
||||
|
||||
data = (await db_session.execute(query)).scalar_one_or_none()
|
||||
result = {
|
||||
"spreadsheet_id": None,
|
||||
"spreadsheet_link": None
|
||||
}
|
||||
|
||||
result = {'spreadsheet_id': None, 'spreadsheet_link': None}
|
||||
if data:
|
||||
result = {
|
||||
"spreadsheet_id": data.spreadsheet_id,
|
||||
"spreadsheet_link": data.spreadsheet_link
|
||||
}
|
||||
|
||||
result = {'spreadsheet_id': data.spreadsheet_id, 'spreadsheet_link': data.spreadsheet_link}
|
||||
return StandardResponse(data=result, message='Data retrieved successfully')
|
||||
|
||||
return StandardResponse(
|
||||
data=result,
|
||||
message="Data retrieved successfully",
|
||||
)
|
||||
|
||||
@router.post(
|
||||
"/spreadsheet", response_model=StandardResponse[dict]
|
||||
)
|
||||
async def update_gantt_spreadsheet(db_session: DbSession, spreadsheet_in: OverhaulGanttIn):
|
||||
"""Get all scope pagination."""
|
||||
# return
|
||||
|
||||
match = re.search(r"/d/([a-zA-Z0-9-_]+)", spreadsheet_in.spreadsheet_link)
|
||||
@router.post('/spreadsheet', response_model=StandardResponse[dict], dependencies=[Depends(csrf_protect)])
|
||||
async def update_gantt_spreadsheet(db_session: DbSession, spreadsheet_in: OverhaulGanttIn, admin: Admin):
|
||||
match = re.search('/d/([a-zA-Z0-9-_]+)', spreadsheet_in.spreadsheet_link)
|
||||
if not match:
|
||||
raise ValueError("Invalid Google Sheets URL")
|
||||
|
||||
raise ValueError('Invalid Google Sheets URL')
|
||||
spreadsheet_id = match.group(1)
|
||||
|
||||
query = select(OverhaulGantt).limit(1)
|
||||
|
||||
data = (await db_session.execute(query)).scalar_one_or_none()
|
||||
if data:
|
||||
data.spreadsheet_link = spreadsheet_in.spreadsheet_link
|
||||
data.spreadsheet_id = spreadsheet_id
|
||||
else:
|
||||
spreadsheet = OverhaulGantt(
|
||||
spreadsheet_id=spreadsheet_id,
|
||||
spreadsheet_link=spreadsheet_in.spreadsheet_link
|
||||
)
|
||||
spreadsheet = OverhaulGantt(spreadsheet_id=spreadsheet_id, spreadsheet_link=spreadsheet_in.spreadsheet_link)
|
||||
db_session.add(spreadsheet)
|
||||
|
||||
|
||||
await db_session.commit()
|
||||
|
||||
if data:
|
||||
result = {
|
||||
"spreadsheet_id": spreadsheet_id
|
||||
}
|
||||
|
||||
|
||||
return StandardResponse(
|
||||
data=result,
|
||||
message="Data retrieved successfully",
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# @router.post("", response_model=StandardResponse[None])
|
||||
# async def create_overhaul_equipment_jobs(
|
||||
# db_session: DbSession, overhaul_job_in: OverhaulScheduleCreate
|
||||
# ):
|
||||
# await create(
|
||||
# db_session=db_session,
|
||||
# overhaul_job_in=overhaul_job_in,
|
||||
# )
|
||||
|
||||
# return StandardResponse(
|
||||
# data=None,
|
||||
# message="Data created successfully",
|
||||
# )
|
||||
|
||||
# @router.put("/{overhaul_job_id}", response_model=StandardResponse[None])
|
||||
# async def update_overhaul_schedule(
|
||||
# db_session: DbSession, overhaul_job_id: str, overhaul_job_in: OverhaulScheduleUpdate
|
||||
# ):
|
||||
# await update(db_session=db_session, overhaul_schedule_id=overhaul_job_id, overhaul_job_in=overhaul_job_in)
|
||||
|
||||
# return StandardResponse(
|
||||
# data=None,
|
||||
# message="Data updated successfully",
|
||||
# )
|
||||
|
||||
# @router.delete("/{overhaul_job_id}", response_model=StandardResponse[None])
|
||||
# async def delete_overhaul_equipment_job(db_session: DbSession, overhaul_job_id):
|
||||
# await delete(db_session=db_session, overhaul_schedule_id=overhaul_job_id)
|
||||
|
||||
# return StandardResponse(
|
||||
# data=None,
|
||||
# message="Data deleted successfully",
|
||||
# )
|
||||
result = {'spreadsheet_id': spreadsheet_id}
|
||||
return StandardResponse(data=result, message='Data retrieved successfully')
|
||||
|
||||
@ -1,112 +1,34 @@
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
from sqlalchemy import Delete, Select, func
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
# from .model import OverhaulSchedule
|
||||
# from .schema import OverhaulScheduleCreate, OverhaulScheduleUpdate
|
||||
from .utils import fetch_all_sections, get_google_creds, get_spreatsheed_service, process_spreadsheet_data
|
||||
|
||||
# async def get_all(*, common):
|
||||
# """Returns all documents."""
|
||||
# query = Select(OverhaulSchedule).order_by(OverhaulSchedule.start.desc())
|
||||
|
||||
# results = await search_filter_sort_paginate(model=query, **common)
|
||||
# return results
|
||||
|
||||
|
||||
# async def create(
|
||||
# *, db_session: DbSession, overhaul_job_in: OverhaulScheduleCreate
|
||||
# ):
|
||||
|
||||
|
||||
# schedule = OverhaulSchedule(**overhaul_job_in.model_dump())
|
||||
# db_session.add(schedule)
|
||||
# await db_session.commit()
|
||||
# return schedule
|
||||
|
||||
|
||||
|
||||
# async def update(*, db_session: DbSession, overhaul_schedule_id: str, overhaul_job_in: OverhaulScheduleUpdate):
|
||||
# """Updates a document."""
|
||||
# data = overhaul_job_in.model_dump()
|
||||
# overhaul_schedule = await db_session.get(OverhaulSchedule, overhaul_schedule_id)
|
||||
|
||||
# update_data = overhaul_job_in.model_dump(exclude_defaults=True)
|
||||
|
||||
# for field in data:
|
||||
# if field in update_data:
|
||||
# setattr(overhaul_schedule, field, update_data[field])
|
||||
|
||||
# await db_session.commit()
|
||||
|
||||
# return overhaul_schedule
|
||||
|
||||
|
||||
# async def delete(*, db_session: DbSession, overhaul_schedule_id: str):
|
||||
# """Deletes a document."""
|
||||
# query = Delete(OverhaulSchedule).where(OverhaulSchedule.id == overhaul_schedule_id)
|
||||
# await db_session.execute(query)
|
||||
# await db_session.commit()
|
||||
|
||||
|
||||
async def get_gantt_performance_chart(*, spreadsheet_id = "1gZXuwA97zU1v4QBv56wKeiqadc6skHUucGKYG8qVFRk"):
|
||||
async def get_gantt_performance_chart(*, spreadsheet_id='1gZXuwA97zU1v4QBv56wKeiqadc6skHUucGKYG8qVFRk'):
|
||||
creds = get_google_creds()
|
||||
RANGE_NAME = "'SUMMARY'!K34:AZ38" # Or just "2024 schedule"
|
||||
GANTT_DATA_NAME = "ACTUAL PROGRESS"
|
||||
|
||||
RANGE_NAME = "'SUMMARY'!K34:AZ38"
|
||||
GANTT_DATA_NAME = 'ACTUAL PROGRESS'
|
||||
try:
|
||||
service = get_spreatsheed_service(creds)
|
||||
sheet = service.spreadsheets()
|
||||
|
||||
response = sheet.values().get(
|
||||
spreadsheetId=spreadsheet_id,
|
||||
range=RANGE_NAME
|
||||
).execute()
|
||||
|
||||
values = response.get("values", [])
|
||||
|
||||
response = sheet.values().get(spreadsheetId=spreadsheet_id, range=RANGE_NAME).execute()
|
||||
values = response.get('values', [])
|
||||
if len(values) < 4:
|
||||
raise Exception("Spreadsheet format invalid: need 4 rows (DAY, DATE, PLAN, ACTUAL).")
|
||||
|
||||
# Extract rows
|
||||
day_row = values[0][1:]
|
||||
date_row = values[1][1:]
|
||||
plan_row = values[3][1:]
|
||||
raise Exception('Spreadsheet format invalid: need 4 rows (DAY, DATE, PLAN, ACTUAL).')
|
||||
day_row = values[0][1:]
|
||||
date_row = values[1][1:]
|
||||
plan_row = values[3][1:]
|
||||
actual_row = values[4][1:]
|
||||
|
||||
|
||||
|
||||
total_days = len(day_row)
|
||||
|
||||
# PAD rows so lengths match day count
|
||||
date_row += [""] * (total_days - len(date_row))
|
||||
plan_row += [""] * (total_days - len(plan_row))
|
||||
actual_row += [""] * (total_days - len(actual_row))
|
||||
|
||||
date_row += [''] * (total_days - len(date_row))
|
||||
plan_row += [''] * (total_days - len(plan_row))
|
||||
actual_row += [''] * (total_days - len(actual_row))
|
||||
results = []
|
||||
|
||||
for i in range(total_days):
|
||||
|
||||
day = day_row[i]
|
||||
date = date_row[i]
|
||||
plan = plan_row[i]
|
||||
actual = actual_row[i] if actual_row[i] else "0%" # <-- FIX HERE
|
||||
|
||||
results.append({
|
||||
"day": day,
|
||||
"date": date,
|
||||
"plan": plan,
|
||||
"actual": actual
|
||||
})
|
||||
|
||||
actual = actual_row[i] if actual_row[i] else '0%'
|
||||
results.append({'day': day, 'date': date, 'plan': plan, 'actual': actual})
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||
|
||||
|
||||
processed_data = process_spreadsheet_data(results)
|
||||
gantt_data = fetch_all_sections(service=service, spreadsheet_id=spreadsheet_id, sheet_name=GANTT_DATA_NAME)
|
||||
|
||||
|
||||
return processed_data, gantt_data
|
||||
return (processed_data, gantt_data)
|
||||
|
||||
@ -0,0 +1 @@
|
||||
|
||||
@ -1,91 +1,23 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, HTTPException, status, Query
|
||||
|
||||
from src.auth.service import CurrentUser
|
||||
from typing import Optional
|
||||
from fastapi import APIRouter, Query
|
||||
from src.database.core import DbSession
|
||||
from src.database.service import CommonParameters
|
||||
from src.models import StandardResponse
|
||||
|
||||
from .schema import (OverhaulJobBase, OverhaulJobCreate, OverhaulJobPagination,
|
||||
OverhaulJobRead)
|
||||
from .schema import OverhaulJobCreate, OverhaulJobPagination
|
||||
from .service import create, delete, get_all
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{location_tag}", response_model=StandardResponse[OverhaulJobPagination]
|
||||
)
|
||||
async def get_jobs(common: CommonParameters, location_tag: str, scope: Optional[str] = Query(None)):
|
||||
"""Get all scope pagination."""
|
||||
# return
|
||||
@router.get('/{location_tag}', response_model=StandardResponse[OverhaulJobPagination])
|
||||
async def get_jobs(common: CommonParameters, location_tag: str, scope: Optional[str]=Query(None)):
|
||||
results = await get_all(common=common, location_tag=location_tag, scope=scope)
|
||||
return StandardResponse(data=results, message='Data retrieved successfully')
|
||||
|
||||
return StandardResponse(
|
||||
data=results,
|
||||
message="Data retrieved successfully",
|
||||
)
|
||||
|
||||
@router.post('/{overhaul_equipment_id}', response_model=StandardResponse[None])
|
||||
async def create_overhaul_equipment_jobs(db_session: DbSession, overhaul_equipment_id, overhaul_job_in: OverhaulJobCreate):
|
||||
await create(db_session=db_session, overhaul_equipment_id=overhaul_equipment_id, overhaul_job_in=overhaul_job_in)
|
||||
return StandardResponse(data=None, message='Data created successfully')
|
||||
|
||||
@router.post("/{overhaul_equipment_id}", response_model=StandardResponse[None])
|
||||
async def create_overhaul_equipment_jobs(
|
||||
db_session: DbSession, overhaul_equipment_id, overhaul_job_in: OverhaulJobCreate
|
||||
):
|
||||
"""Get all scope activity pagination."""
|
||||
# return
|
||||
await create(
|
||||
db_session=db_session,
|
||||
overhaul_equipment_id=overhaul_equipment_id,
|
||||
overhaul_job_in=overhaul_job_in,
|
||||
)
|
||||
|
||||
return StandardResponse(
|
||||
data=None,
|
||||
message="Data created successfully",
|
||||
)
|
||||
|
||||
|
||||
@router.post("/delete/{overhaul_job_id}", response_model=StandardResponse[None])
|
||||
@router.post('/delete/{overhaul_job_id}', response_model=StandardResponse[None])
|
||||
async def delete_overhaul_equipment_job(db_session: DbSession, overhaul_job_id):
|
||||
await delete(db_session=db_session, overhaul_job_id=overhaul_job_id)
|
||||
|
||||
return StandardResponse(
|
||||
data=None,
|
||||
message="Data deleted successfully",
|
||||
)
|
||||
|
||||
|
||||
# @router.post("", response_model=StandardResponse[List[str]])
|
||||
# async def create_scope(db_session: DbSession, scope_in: OverhaulJobCreate):
|
||||
# overhaul_job = await create(db_session=db_session, scope_in=scope_in)
|
||||
|
||||
# return StandardResponse(data=overhaul_job, message="Data created successfully")
|
||||
|
||||
|
||||
# @router.put("/{scope_id}", response_model=StandardResponse[ScopeRead])
|
||||
# async def update_scope(db_session: DbSession, scope_id: str, scope_in: ScopeUpdate, current_user: CurrentUser):
|
||||
# scope = await get(db_session=db_session, scope_id=scope_id)
|
||||
|
||||
# if not scope:
|
||||
# raise HTTPException(
|
||||
# status_code=status.HTTP_404_NOT_FOUND,
|
||||
# detail="A data with this id does not exist.",
|
||||
# )
|
||||
|
||||
# return StandardResponse(data=await update(db_session=db_session, scope=scope, scope_in=scope_in), message="Data updated successfully")
|
||||
|
||||
|
||||
# @router.delete("/{scope_id}", response_model=StandardResponse[ScopeRead])
|
||||
# async def delete_scope(db_session: DbSession, scope_id: str):
|
||||
# scope = await get(db_session=db_session, scope_id=scope_id)
|
||||
|
||||
# if not scope:
|
||||
# raise HTTPException(
|
||||
# status_code=status.HTTP_404_NOT_FOUND,
|
||||
# detail=[{"msg": "A data with this id does not exist."}],
|
||||
# )
|
||||
|
||||
# await delete(db_session=db_session, scope_id=scope_id)
|
||||
|
||||
# return StandardResponse(message="Data deleted successfully", data=scope)
|
||||
return StandardResponse(data=None, message='Data deleted successfully')
|
||||
|
||||
@ -0,0 +1 @@
|
||||
|
||||
@ -1,18 +1,13 @@
|
||||
from sqlalchemy import (UUID, Column, DateTime, Float, ForeignKey, Integer,
|
||||
String)
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from sqlalchemy import Column, DateTime, Integer, String
|
||||
from src.database.core import Base
|
||||
from src.models import DefaultMixin, IdentityMixin, TimeStampMixin
|
||||
|
||||
from src.models import DefaultMixin
|
||||
|
||||
class OverhaulSchedule(Base, DefaultMixin):
|
||||
__tablename__ = "rp_oh_schedule"
|
||||
|
||||
__tablename__ = 'rp_oh_schedule'
|
||||
year = Column(Integer, nullable=False)
|
||||
plan_duration = Column(Integer, nullable=True)
|
||||
planned_outage = Column(Integer, nullable=True)
|
||||
actual_shutdown = Column(Integer, nullable=True)
|
||||
start = Column(DateTime(timezone=True)) # This will be TIMESTAMP WITH TIME ZONE
|
||||
start = Column(DateTime(timezone=True))
|
||||
finish = Column(DateTime(timezone=True))
|
||||
remark = Column(String, nullable=True)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue