diff --git a/run.py b/run.py index bd11018..ff0b5a7 100644 --- a/run.py +++ b/run.py @@ -1,10 +1,6 @@ import uvicorn -from src.config import PORT, HOST + +from src.config import HOST, PORT if __name__ == "__main__": - uvicorn.run( - "src.main:app", - host=HOST, - port=PORT, - reload=True - ) \ No newline at end of file + uvicorn.run("src.main:app", host=HOST, port=PORT, reload=True) diff --git a/src/api.py b/src/api.py index fb93d76..c7de630 100644 --- a/src/api.py +++ b/src/api.py @@ -1,11 +1,23 @@ 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 +from src.job.router import router as job_router +from src.overhaul.router import router as overhaul_router +from src.overhaul_activity.router import router as overhaul_activity_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_scope.router import router as scope_router # from src.scope_equipment.router import router as scope_equipment_router @@ -15,20 +27,11 @@ from src.auth.service import JWTBearer # # from src.overhaul_schedule.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 -from src.overhaul.router import router as overhaul_router -from src.scope_equipment.router import router as scope_equipment_router -from src.overhaul_scope.router import router as scope_router -from src.overhaul_activity.router import router as overhaul_activity_router -from src.calculation_target_reliability.router import router as calculation_target_reliability -from src.scope_equipment_job.router import router as scope_equipment_job_router -from src.job.router import router as job_router -from src.calculation_time_constrains.router import router as calculation_time_constrains_router -from src.calculation_budget_constrains.router import router as calculation_budget_constraint -from src.overhaul_job.router import router as job_overhaul_router + class ErrorMessage(BaseModel): msg: str @@ -55,18 +58,20 @@ def healthcheck(): return {"status": "ok"} -authenticated_api_router = APIRouter(dependencies=[Depends(JWTBearer())], - ) +authenticated_api_router = APIRouter( + dependencies=[Depends(JWTBearer())], +) # overhaul data authenticated_api_router.include_router( - overhaul_router, prefix="/overhauls", tags=["overhaul"]) + overhaul_router, prefix="/overhauls", tags=["overhaul"] +) -authenticated_api_router.include_router( - job_router, prefix="/jobs", tags=["job"]) +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"]) + scope_router, prefix="/overhaul-session", tags=["overhaul-session"] +) authenticated_api_router.include_router( scope_equipment_router, prefix="/scope-equipments", tags=["scope_equipment"] @@ -77,7 +82,9 @@ authenticated_api_router.include_router( ) authenticated_api_router.include_router( - scope_equipment_job_router, prefix="/scope-equipment-jobs", tags=["scope_equipment", "job"] + scope_equipment_job_router, + prefix="/scope-equipment-jobs", + tags=["scope_equipment", "job"], ) authenticated_api_router.include_router( @@ -109,20 +116,25 @@ calculation_router = APIRouter(prefix="/calculation", tags=["calculations"]) # Time constrains calculation_router.include_router( - calculation_time_constrains_router, prefix="/time-constraint", tags=["calculation", "time_constraint"]) + calculation_time_constrains_router, + prefix="/time-constraint", + tags=["calculation", "time_constraint"], +) # Target reliability calculation_router.include_router( - calculation_target_reliability, prefix="/target-reliability", tags=["calculation", "target_reliability"] + 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"] + calculation_budget_constraint, + prefix="/budget-constraint", + tags=["calculation", "budget_constraint"], ) -authenticated_api_router.include_router( - calculation_router -) +authenticated_api_router.include_router(calculation_router) api_router.include_router(authenticated_api_router) diff --git a/src/auth/model.py b/src/auth/model.py index 29b89f1..1171466 100644 --- a/src/auth/model.py +++ b/src/auth/model.py @@ -1,5 +1,3 @@ - - from pydantic import BaseModel diff --git a/src/auth/service.py b/src/auth/service.py index f4da601..33a841e 100644 --- a/src/auth/service.py +++ b/src/auth/service.py @@ -1,10 +1,13 @@ # app/auth/auth_bearer.py from typing import Annotated, Optional -from fastapi import Depends, Request, HTTPException -from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials + import requests +from fastapi import Depends, HTTPException, Request +from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer + import src.config as config + from .model import UserBase @@ -13,21 +16,24 @@ class JWTBearer(HTTPBearer): super(JWTBearer, self).__init__(auto_error=auto_error) 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.") + status_code=403, detail="Invalid authentication scheme." + ) user_info = self.verify_jwt(credentials.credentials) if not user_info: raise HTTPException( - status_code=403, detail="Invalid token or expired token.") + status_code=403, detail="Invalid token or expired token." + ) request.state.user = user_info return user_info else: - raise HTTPException( - status_code=403, detail="Invalid authorization code.") + raise HTTPException(status_code=403, detail="Invalid authorization code.") def verify_jwt(self, jwtoken: str) -> Optional[UserBase]: try: @@ -40,7 +46,7 @@ class JWTBearer(HTTPBearer): return None user_data = response.json() - return UserBase(**user_data['data']) + return UserBase(**user_data["data"]) except Exception as e: print(f"Token verification error: {str(e)}") @@ -51,6 +57,7 @@ class JWTBearer(HTTPBearer): async def get_current_user(request: Request) -> UserBase: return request.state.user + async def get_token(request: Request): token = request.headers.get("Authorization") @@ -59,5 +66,6 @@ async def get_token(request: Request): return "" + CurrentUser = Annotated[UserBase, Depends(get_current_user)] Token = Annotated[str, Depends(get_token)] diff --git a/src/calculation_budget_constrains/router.py b/src/calculation_budget_constrains/router.py index 15744ee..65f2d85 100644 --- a/src/calculation_budget_constrains/router.py +++ b/src/calculation_budget_constrains/router.py @@ -1,19 +1,26 @@ - from typing import Dict, List, Optional + from fastapi import APIRouter, HTTPException, status from fastapi.params import Query +from src.database.core import DbSession +from src.models import StandardResponse + from .service import get_all_budget_constrains -from src.models import StandardResponse -from src.database.core import DbSession router = APIRouter() @router.get("", response_model=StandardResponse[List[Dict]]) -async def get_target_reliability(db_session: DbSession, scope_name: Optional[str] = Query(None), cost_threshold: float = Query(100)): +async def get_target_reliability( + db_session: DbSession, + scope_name: Optional[str] = Query(None), + cost_threshold: float = Query(100), +): """Get all scope pagination.""" - results = await get_all_budget_constrains(db_session=db_session, scope_name=scope_name, cost_threshold=cost_threshold) + results = await get_all_budget_constrains( + db_session=db_session, scope_name=scope_name, cost_threshold=cost_threshold + ) return StandardResponse( data=results, diff --git a/src/calculation_budget_constrains/schema.py b/src/calculation_budget_constrains/schema.py index b2a59bc..bb90d76 100644 --- a/src/calculation_budget_constrains/schema.py +++ b/src/calculation_budget_constrains/schema.py @@ -1,9 +1,9 @@ - from datetime import datetime from typing import Any, Dict, List, Optional from uuid import UUID -from pydantic import Field, BaseModel +from pydantic import BaseModel, Field + from src.models import DefultBase, Pagination @@ -16,13 +16,13 @@ class OverhaulCriticalParts(OverhaulBase): class OverhaulSchedules(OverhaulBase): - schedules: List[Dict[str, Any] - ] = Field(..., description="List of schedules") + schedules: List[Dict[str, Any]] = Field(..., description="List of schedules") class OverhaulSystemComponents(OverhaulBase): - systemComponents: Dict[str, - Any] = Field(..., description="List of system components") + systemComponents: Dict[str, Any] = Field( + ..., description="List of system components" + ) class OverhaulRead(OverhaulBase): diff --git a/src/calculation_budget_constrains/service.py b/src/calculation_budget_constrains/service.py index 495acee..851415c 100644 --- a/src/calculation_budget_constrains/service.py +++ b/src/calculation_budget_constrains/service.py @@ -1,16 +1,17 @@ - - import random -from sqlalchemy import Select, Delete from typing import Optional -from src.database.core import DbSession +from sqlalchemy import Delete, Select + from src.auth.service import CurrentUser +from src.database.core import DbSession from src.scope_equipment.model import ScopeEquipment from src.scope_equipment.service import get_by_scope_name -async def get_all_budget_constrains(*, db_session: DbSession, scope_name: str, cost_threshold: float = 100.0): +async def get_all_budget_constrains( + *, db_session: DbSession, scope_name: str, cost_threshold: float = 100.0 +): """Get all overhaul overview with EAF values that sum to 100%.""" equipments = await get_by_scope_name(db_session=db_session, scope_name=scope_name) @@ -20,23 +21,23 @@ async def get_all_budget_constrains(*, db_session: DbSession, scope_name: str, c # Create result array of dictionaries result = [ { - 'id': equipment.id, - 'assetnum': equipment.assetnum, - 'location_tag': equipment.master_equipment.location_tag, - 'name': equipment.master_equipment.name, - 'total_cost': 1000000 + random.randint(10000, 5000000) + "id": equipment.id, + "assetnum": equipment.assetnum, + "location_tag": equipment.master_equipment.location_tag, + "name": equipment.master_equipment.name, + "total_cost": 1000000 + random.randint(10000, 5000000), } for equipment in equipments ] - result.sort(key=lambda x: x['total_cost'], reverse=True) + result.sort(key=lambda x: x["total_cost"], reverse=True) # Filter equipment up to threshold cumulative_cost = 0 filtered_result = [] for equipment in result: - cumulative_cost += equipment['total_cost'] + cumulative_cost += equipment["total_cost"] if cumulative_cost >= cost_threshold: break diff --git a/src/calculation_target_reliability/router.py b/src/calculation_target_reliability/router.py index 484f150..4e4bd72 100644 --- a/src/calculation_target_reliability/router.py +++ b/src/calculation_target_reliability/router.py @@ -1,19 +1,26 @@ - from typing import Dict, List, Optional + from fastapi import APIRouter, HTTPException, status from fastapi.params import Query +from src.database.core import DbSession +from src.models import StandardResponse + from .service import get_all_target_reliability -from src.models import StandardResponse -from src.database.core import DbSession router = APIRouter() @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)): +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) + results = await get_all_target_reliability( + db_session=db_session, scope_name=scope_name, eaf_threshold=eaf_threshold + ) return StandardResponse( data=results, diff --git a/src/calculation_target_reliability/schema.py b/src/calculation_target_reliability/schema.py index b2a59bc..bb90d76 100644 --- a/src/calculation_target_reliability/schema.py +++ b/src/calculation_target_reliability/schema.py @@ -1,9 +1,9 @@ - from datetime import datetime from typing import Any, Dict, List, Optional from uuid import UUID -from pydantic import Field, BaseModel +from pydantic import BaseModel, Field + from src.models import DefultBase, Pagination @@ -16,13 +16,13 @@ class OverhaulCriticalParts(OverhaulBase): class OverhaulSchedules(OverhaulBase): - schedules: List[Dict[str, Any] - ] = Field(..., description="List of schedules") + schedules: List[Dict[str, Any]] = Field(..., description="List of schedules") class OverhaulSystemComponents(OverhaulBase): - systemComponents: Dict[str, - Any] = Field(..., description="List of system components") + systemComponents: Dict[str, Any] = Field( + ..., description="List of system components" + ) class OverhaulRead(OverhaulBase): diff --git a/src/calculation_target_reliability/service.py b/src/calculation_target_reliability/service.py index 03b93f6..70737a4 100644 --- a/src/calculation_target_reliability/service.py +++ b/src/calculation_target_reliability/service.py @@ -1,20 +1,24 @@ - - -from sqlalchemy import Select, Delete from typing import Optional -from src.database.core import DbSession +from sqlalchemy import Delete, Select + from src.auth.service import CurrentUser +from src.database.core import DbSession from src.scope_equipment.model import ScopeEquipment from src.scope_equipment.service import get_by_scope_name from src.scope_equipment_job.service import get_equipment_level_by_no -async def get_all_target_reliability(*, db_session: DbSession, scope_name: str, eaf_threshold: float = 100.0): + +async def get_all_target_reliability( + *, db_session: DbSession, scope_name: str, eaf_threshold: float = 100.0 +): """Get all overhaul overview with EAF values that sum to 100%, aggregated by system.""" equipments = await get_by_scope_name(db_session=db_session, scope_name=scope_name) equipment_system = await get_equipment_level_by_no(db_session=db_session, level=1) - equipment_subsystem = await get_equipment_level_by_no(db_session=db_session, level=2) - + equipment_subsystem = await get_equipment_level_by_no( + db_session=db_session, level=2 + ) + # If no equipments found, return empty list if not equipments: return [] @@ -23,13 +27,13 @@ async def get_all_target_reliability(*, db_session: DbSession, scope_name: str, n = len(equipments) base_value = 100 / n # Even distribution as base - + # Generate EAF values with ±30% variation from base eaf_values = [ base_value + random.uniform(-0.3 * base_value, 0.3 * base_value) for _ in range(n) ] - + # Normalize to ensure sum is 100 total = sum(eaf_values) eaf_values = [(v * 100 / total) for v in eaf_values] @@ -37,49 +41,53 @@ async def get_all_target_reliability(*, db_session: DbSession, scope_name: str, # Create result array of dictionaries result = [ { - 'id': equipment.id, - 'assetnum': equipment.assetnum, - 'location_tag': equipment.master_equipment.location_tag, - 'name': equipment.master_equipment.name, - 'parent_id': equipment.master_equipment.parent_id, # Add parent_id to identify the system - 'eaf': round(eaf, 4) # Add EAF value + "id": equipment.id, + "assetnum": equipment.assetnum, + "location_tag": equipment.master_equipment.location_tag, + "name": equipment.master_equipment.name, + "parent_id": equipment.master_equipment.parent_id, # Add parent_id to identify the system + "eaf": round(eaf, 4), # Add EAF value } for equipment, eaf in zip(equipments, eaf_values) ] # Group equipment by system - sub_system = {subsystem.id: subsystem.parent_id for subsystem in equipment_subsystem} - systems = {system.id: {'name': system.name, 'total_eaf': 0, 'equipments': []} for system in equipment_system} - + sub_system = { + subsystem.id: subsystem.parent_id for subsystem in equipment_subsystem + } + systems = { + system.id: {"name": system.name, "total_eaf": 0, "equipments": []} + for system in equipment_system + } + for equipment in result: - if equipment['parent_id'] in sub_system: - systems[sub_system[equipment['parent_id']]]['equipments'].append(equipment) - systems[sub_system[equipment['parent_id']]]['total_eaf'] += equipment['eaf'] - + if equipment["parent_id"] in sub_system: + systems[sub_system[equipment["parent_id"]]]["equipments"].append(equipment) + systems[sub_system[equipment["parent_id"]]]["total_eaf"] += equipment["eaf"] # Convert the systems dictionary to a list of aggregated results aggregated_result = [ { - 'system_id': system_id, - 'system_name': system_data['name'], - 'total_eaf': round(system_data['total_eaf'], 4), - 'equipments': system_data['equipments'] + "system_id": system_id, + "system_name": system_data["name"], + "total_eaf": round(system_data["total_eaf"], 4), + "equipments": system_data["equipments"], } for system_id, system_data in systems.items() ] # Sort the aggregated result by total_eaf in descending order - aggregated_result.sort(key=lambda x: x['total_eaf'], reverse=True) + aggregated_result.sort(key=lambda x: x["total_eaf"], reverse=True) # Filter systems up to the threshold cumulative_eaf = 0 filtered_aggregated_result = [] for system in aggregated_result: - cumulative_eaf += system['total_eaf'] + cumulative_eaf += system["total_eaf"] filtered_aggregated_result.append(system) if cumulative_eaf >= eaf_threshold: break - return filtered_aggregated_result \ No newline at end of file + return filtered_aggregated_result diff --git a/src/calculation_time_constrains/flows.py b/src/calculation_time_constrains/flows.py index 3f10735..e7c5fa4 100644 --- a/src/calculation_time_constrains/flows.py +++ b/src/calculation_time_constrains/flows.py @@ -2,37 +2,36 @@ from typing import Optional from uuid import UUID import numpy as np -from fastapi import HTTPException -from fastapi import status -from sqlalchemy import Select -from sqlalchemy import func -from sqlalchemy import select +from fastapi import HTTPException, status +from sqlalchemy import Select, func, select from sqlalchemy.orm import joinedload -from src.database.core import DbSession from src.auth.service import Token +from src.database.core import DbSession from src.overhaul_scope.service import get_all from src.scope_equipment.model import ScopeEquipment from src.scope_equipment.service import get_by_assetnum from src.workorder.model import MasterWorkOrder -from .schema import CalculationTimeConstrainsParametersCreate -from .schema import CalculationTimeConstrainsParametersRead -from .schema import CalculationTimeConstrainsParametersRetrive -from .schema import CalculationTimeConstrainsRead -from .service import create_calculation_result_service -from .service import create_param_and_data -from .service import get_avg_cost_by_asset -from .service import get_calculation_by_reference_and_parameter -from .service import get_calculation_data_by_id -from .service import get_calculation_result -from .service import get_corrective_cost_time_chart -from .service import get_overhaul_cost_by_time_chart - - -async def get_create_calculation_parameters(*, db_session: DbSession, calculation_id: Optional[str] = None): +from .schema import (CalculationTimeConstrainsParametersCreate, + CalculationTimeConstrainsParametersRead, + CalculationTimeConstrainsParametersRetrive, + CalculationTimeConstrainsRead) +from .service import (create_calculation_result_service, create_param_and_data, + get_avg_cost_by_asset, + get_calculation_by_reference_and_parameter, + get_calculation_data_by_id, get_calculation_result, + get_corrective_cost_time_chart, + get_overhaul_cost_by_time_chart) + + +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) + calculation = await get_calculation_data_by_id( + calculation_id=calculation_id, db_session=db_session + ) if not calculation: raise HTTPException( @@ -43,13 +42,13 @@ async def get_create_calculation_parameters(*, db_session: DbSession, calculatio return CalculationTimeConstrainsParametersRead( costPerFailure=calculation.parameter.avg_failure_cost, overhaulCost=calculation.parameter.overhaul_cost, - reference=calculation + reference=calculation, ) stmt = ( select( ScopeEquipment.scope_id, - func.avg(MasterWorkOrder.total_cost_max).label('average_cost') + func.avg(MasterWorkOrder.total_cost_max).label("average_cost"), ) .outerjoin(MasterWorkOrder, ScopeEquipment.assetnum == MasterWorkOrder.assetnum) .group_by(ScopeEquipment.scope_id) @@ -60,8 +59,10 @@ async def get_create_calculation_parameters(*, db_session: DbSession, calculatio 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} + costFailurePerScope = { + avaiableScopes.get(costPerFailure[0]): costPerFailure[1] + for costPerFailure in costFailure + } return CalculationTimeConstrainsParametersRetrive( costPerFailure=costFailurePerScope, @@ -78,18 +79,35 @@ async def get_create_calculation_parameters(*, db_session: DbSession, calculatio ) -async def create_calculation(*,token:str, db_session: DbSession, calculation_time_constrains_in: CalculationTimeConstrainsParametersCreate, created_by: str): +async def create_calculation( + *, + token: str, + db_session: DbSession, + calculation_time_constrains_in: CalculationTimeConstrainsParametersCreate, + created_by: str +): calculation_data = await create_param_and_data( - db_session=db_session, calculation_param_in=calculation_time_constrains_in, created_by=created_by) - + db_session=db_session, + calculation_param_in=calculation_time_constrains_in, + created_by=created_by, + ) - results = await create_calculation_result_service(db_session=db_session, calculation=calculation_data, token=token) + results = await create_calculation_result_service( + db_session=db_session, calculation=calculation_data, token=token + ) return results -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) +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( @@ -103,5 +121,5 @@ async def get_or_create_scope_equipment_calculation(*, db_session: DbSession, sc reference=scope_calculation.overhaul_session_id, results=scope_calculation.results, optimum_oh=scope_calculation.optimum_oh_day, - equipment_results=scope_calculation.equipment_results + equipment_results=scope_calculation.equipment_results, ) diff --git a/src/calculation_time_constrains/model.py b/src/calculation_time_constrains/model.py index 1953b66..c813025 100644 --- a/src/calculation_time_constrains/model.py +++ b/src/calculation_time_constrains/model.py @@ -1,9 +1,10 @@ - - from enum import Enum from typing import List, Optional, Union -from sqlalchemy import UUID, Column, Float, ForeignKey, Integer, String, JSON, Numeric, Boolean + +from sqlalchemy import (JSON, UUID, Boolean, Column, Float, ForeignKey, + Integer, Numeric, String) from sqlalchemy.orm import relationship + from src.database.core import Base, DbSession from src.models import DefaultMixin, IdentityMixin, TimeStampMixin, UUIDMixin @@ -20,8 +21,7 @@ class CalculationParam(Base, DefaultMixin, IdentityMixin): overhaul_cost = Column(Float, nullable=False) # Relationships - calculation_data = relationship( - "CalculationData", back_populates="parameter") + calculation_data = relationship("CalculationData", back_populates="parameter") results = relationship("CalculationResult", back_populates="parameter") # @classmethod @@ -60,42 +60,40 @@ class CalculationParam(Base, DefaultMixin, IdentityMixin): 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_scope.id')) + 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_scope.id") + ) optimum_oh_day = Column(Integer, nullable=True) - session = relationship( - "OverhaulScope", lazy="raise") + session = relationship("OverhaulScope", lazy="raise") + + parameter = relationship("CalculationParam", back_populates="calculation_data") - parameter = relationship( - "CalculationParam", back_populates="calculation_data") - equipment_results = relationship( "CalculationEquipmentResult", lazy="raise", viewonly=True ) - - results = relationship( - "CalculationResult", lazy="raise", viewonly=True - ) + results = relationship("CalculationResult", lazy="raise", viewonly=True) @classmethod async def create_with_param( cls, - overhaul_session_id: str, + overhaul_session_id: str, db: DbSession, avg_failure_cost: Optional[float], overhaul_cost: Optional[float], created_by: str, - params_id: Optional[UUID] + 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 + created_by=created_by, ) db.add(params) @@ -105,7 +103,7 @@ class CalculationData(Base, DefaultMixin, IdentityMixin): calculation_data = cls( overhaul_session_id=overhaul_session_id, created_by=created_by, - parameter_id=params_id + parameter_id=params_id, ) db.add(calculation_data) @@ -120,10 +118,12 @@ 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) + 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) @@ -136,25 +136,22 @@ class CalculationResult(Base, DefaultMixin): class CalculationEquipmentResult(Base, DefaultMixin): __tablename__ = "oh_tr_calculation_equipment_result" - + corrective_costs = Column(JSON, nullable=False) overhaul_costs = Column(JSON, nullable=False) daily_failures = Column(JSON, nullable=False) assetnum = 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) - + master_equipment = relationship( "MasterEquipment", lazy="joined", primaryjoin="and_(CalculationEquipmentResult.assetnum == foreign(MasterEquipment.assetnum))", - uselist=False # Add this if it's a one-to-one relationship + uselist=False, # Add this if it's a one-to-one relationship ) - - - - - diff --git a/src/calculation_time_constrains/router.py b/src/calculation_time_constrains/router.py index c800d03..a4ef915 100644 --- a/src/calculation_time_constrains/router.py +++ b/src/calculation_time_constrains/router.py @@ -1,7 +1,4 @@ - -from typing import List -from typing import Optional -from typing import Union +from typing import List, Optional, Union from fastapi import APIRouter from fastapi.params import Query @@ -10,32 +7,47 @@ from src.auth.service import CurrentUser, Token from src.database.core import DbSession from src.models import StandardResponse -from .flows import create_calculation -from .flows import get_create_calculation_parameters -from .flows import get_or_create_scope_equipment_calculation -from .schema import CalculationResultsRead -from .schema import CalculationSelectedEquipmentUpdate -from .schema import CalculationTimeConstrainsCreate -from .schema import CalculationTimeConstrainsParametersCreate -from .schema import CalculationTimeConstrainsParametersRead -from .schema import CalculationTimeConstrainsParametersRetrive -from .schema import CalculationTimeConstrainsRead -from .service import get_calculation_result -from .service import get_calculation_result_by_day -from .service import bulk_update_equipment +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) +from .service import (bulk_update_equipment, get_calculation_result, + get_calculation_result_by_day) router = APIRouter() -@router.post("", response_model=StandardResponse[Union[str, CalculationTimeConstrainsRead]]) -async def create_calculation_time_constrains(token:Token ,db_session: DbSession, current_user: CurrentUser, calculation_time_constrains_in: CalculationTimeConstrainsParametersCreate, scope_calculation_id: Optional[str] = Query(None), with_results: Optional[int] = Query(0)): +@router.post( + "", response_model=StandardResponse[Union[str, CalculationTimeConstrainsRead]] +) +async def create_calculation_time_constrains( + token: Token, + db_session: DbSession, + current_user: CurrentUser, + calculation_time_constrains_in: CalculationTimeConstrainsParametersCreate, + scope_calculation_id: Optional[str] = Query(None), + with_results: Optional[int] = Query(0), +): """Save calculation time constrains Here""" 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, calculation_time_constrains_in=calculation_time_constrains_in, created_by=current_user.name) - + results = await create_calculation( + token=token, + db_session=db_session, + calculation_time_constrains_in=calculation_time_constrains_in, + created_by=current_user.name, + ) if not with_results: results = str(results.id) @@ -43,11 +55,23 @@ async def create_calculation_time_constrains(token:Token ,db_session: DbSession, return StandardResponse(data=results, message="Data created 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)): +@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) + parameters = await get_create_calculation_parameters( + db_session=db_session, calculation_id=calculation_id + ) return StandardResponse( data=parameters, @@ -55,9 +79,13 @@ async def get_calculation_parameters(db_session: DbSession, calculation_id: Opti ) -@router.get("/{calculation_id}", response_model=StandardResponse[CalculationTimeConstrainsRead]) +@router.get( + "/{calculation_id}", response_model=StandardResponse[CalculationTimeConstrainsRead] +) async def get_calculation_results(db_session: DbSession, calculation_id): - results = await get_calculation_result(db_session=db_session, calculation_id=calculation_id) + results = await get_calculation_result( + db_session=db_session, calculation_id=calculation_id + ) return StandardResponse( data=results, @@ -65,15 +93,37 @@ async def get_calculation_results(db_session: DbSession, calculation_id): ) -@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) +@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" + ) - return StandardResponse(data=simulation_result, message="Data retrieved successfully") @router.put("/{calculation_id}", response_model=StandardResponse[List[str]]) -async def update_selected_equipment(db_session: DbSession, calculation_id, calculation_time_constrains_in: List[CalculationSelectedEquipmentUpdate]): - results = await bulk_update_equipment(db=db_session, selected_equipments=calculation_time_constrains_in, calculation_data_id=calculation_id) +async def update_selected_equipment( + db_session: DbSession, + calculation_id, + calculation_time_constrains_in: List[CalculationSelectedEquipmentUpdate], +): + results = await bulk_update_equipment( + db=db_session, + selected_equipments=calculation_time_constrains_in, + calculation_data_id=calculation_id, + ) return StandardResponse( data=results, diff --git a/src/calculation_time_constrains/schema.py b/src/calculation_time_constrains/schema.py index ba0145e..05bf0d3 100644 --- a/src/calculation_time_constrains/schema.py +++ b/src/calculation_time_constrains/schema.py @@ -1,47 +1,41 @@ - - +from dataclasses import dataclass from datetime import datetime from typing import Any, Dict, List, Optional, Union from uuid import UUID from pydantic import Field -from src.models import DefultBase -from dataclasses import dataclass +from src.models import DefultBase from src.scope_equipment.schema import MasterEquipmentBase + class CalculationTimeConstrainsBase(DefultBase): pass class ReferenceLinkBase(DefultBase): reference_id: str = Field(..., description="Reference ID") - overhaul_reference_type: str = Field(..., - description="Overhaul reference type") + overhaul_reference_type: str = Field(..., description="Overhaul reference type") class CalculationTimeConstrainsParametersRetrive(CalculationTimeConstrainsBase): # type: ignore - costPerFailure: Union[dict, - float] = Field(..., description="Cost per failure") + costPerFailure: Union[dict, float] = Field(..., description="Cost per failure") availableScopes: List[str] = Field(..., description="Available scopes") recommendedScope: str = Field(..., description="Recommended scope") # historicalData: Dict[str, Any] = Field(..., description="Historical data") class CalculationTimeConstrainsParametersRead(CalculationTimeConstrainsBase): - costPerFailure: Union[dict, - float] = Field(..., description="Cost per failure") + costPerFailure: Union[dict, float] = Field(..., description="Cost per failure") overhaulCost: Optional[float] = Field(None, description="Overhaul cost") - reference: Optional[List[ReferenceLinkBase]] = Field( - None, description="Reference") + reference: Optional[List[ReferenceLinkBase]] = Field(None, description="Reference") class CalculationTimeConstrainsParametersCreate(CalculationTimeConstrainsBase): overhaulCost: Optional[float] = Field(0, description="Overhaul cost") ohSessionId: Optional[UUID] = Field(None, description="Scope OH") - costPerFailure: Optional[float] = Field(0, - description="Cost per failure") + costPerFailure: Optional[float] = Field(0, description="Cost per failure") # class CalculationTimeConstrainsCreate(CalculationTimeConstrainsBase): @@ -50,12 +44,14 @@ class CalculationTimeConstrainsParametersCreate(CalculationTimeConstrainsBase): # costPerFailure: float = Field(..., description="Cost per failure") # metadata: Dict[str, Any] = Field(..., description="Metadata") + class CalculationResultsRead(CalculationTimeConstrainsBase): day: int corrective_cost: float overhaul_cost: float num_failures: int + class OptimumResult(CalculationTimeConstrainsBase): overhaul_cost: float corrective_cost: float @@ -74,6 +70,7 @@ class EquipmentResult(CalculationTimeConstrainsBase): is_included: bool master_equipment: Optional[MasterEquipmentBase] = Field(None) + class CalculationTimeConstrainsRead(CalculationTimeConstrainsBase): id: UUID reference: UUID diff --git a/src/calculation_time_constrains/service.py b/src/calculation_time_constrains/service.py index f7ede25..044de39 100644 --- a/src/calculation_time_constrains/service.py +++ b/src/calculation_time_constrains/service.py @@ -1,16 +1,11 @@ -from typing import List -from typing import Optional -from typing import Tuple +import datetime +from typing import List, Optional, Tuple from uuid import UUID import numpy as np -from fastapi import HTTPException -from fastapi import status -from sqlalchemy import and_ -from sqlalchemy import case -from sqlalchemy import func -from sqlalchemy import select -from sqlalchemy import update +import requests +from fastapi import HTTPException, status +from sqlalchemy import and_, case, func, select, update from sqlalchemy.orm import joinedload from src.database.core import DbSession @@ -18,20 +13,17 @@ from src.overhaul_activity.service import get_all_by_session_id from src.overhaul_scope.service import get as get_scope from src.workorder.model import MasterWorkOrder -from .model import CalculationData -from .model import CalculationEquipmentResult -from .model import CalculationResult -from .schema import CalculationResultsRead -from .schema import CalculationTimeConstrainsParametersCreate -from .schema import CalculationTimeConstrainsRead -from .schema import OptimumResult -from .schema import CalculationSelectedEquipmentUpdate - -import requests -import datetime +from .model import (CalculationData, CalculationEquipmentResult, + CalculationResult) +from .schema import (CalculationResultsRead, + CalculationSelectedEquipmentUpdate, + CalculationTimeConstrainsParametersCreate, + CalculationTimeConstrainsRead, OptimumResult) -def get_overhaul_cost_by_time_chart(overhaul_cost: float, days: int,numEquipments:int ,decay_base: float = 1.01) -> np.ndarray: +def get_overhaul_cost_by_time_chart( + overhaul_cost: float, days: int, numEquipments: int, decay_base: float = 1.01 +) -> np.ndarray: if overhaul_cost < 0: raise ValueError("Overhaul cost cannot be negative") if days <= 0: @@ -40,7 +32,7 @@ def get_overhaul_cost_by_time_chart(overhaul_cost: float, days: int,numEquipment exponents = np.arange(0, days) cost_per_equipment = overhaul_cost / numEquipments # Using a slower decay base to spread the budget depletion over more days - results = cost_per_equipment / (decay_base ** exponents) + results = cost_per_equipment / (decay_base**exponents) results = np.where(np.isfinite(results), results, 0) return results @@ -61,7 +53,10 @@ def get_overhaul_cost_by_time_chart(overhaul_cost: float, days: int,numEquipment # results = np.where(np.isfinite(results), results, 0) # return results -async def get_corrective_cost_time_chart(material_cost: float, service_cost: float, location_tag: str, token) -> Tuple[np.ndarray, np.ndarray]: + +async def get_corrective_cost_time_chart( + material_cost: float, service_cost: float, location_tag: str, token +) -> Tuple[np.ndarray, np.ndarray]: """ Fetch failure data from API and calculate corrective costs, ensuring 365 days of data. @@ -74,14 +69,14 @@ async def get_corrective_cost_time_chart(material_cost: float, service_cost: flo Returns: Tuple of (corrective_costs, daily_failure_rate) """ - url = f'http://192.168.1.82:8000/reliability/main/number-of-failures/{location_tag}/2024-01-01/2024-12-31' + url = f"http://192.168.1.82:8000/reliability/main/number-of-failures/{location_tag}/2024-01-01/2024-12-31" try: response = requests.get( url, headers={ - 'Content-Type': 'application/json', - 'Authorization': f'Bearer {token}' + "Content-Type": "application/json", + "Authorization": f"Bearer {token}", }, ) data = response.json() @@ -92,8 +87,8 @@ async def get_corrective_cost_time_chart(material_cost: float, service_cost: flo # Create a dictionary of existing data data_dict = { - datetime.datetime.strptime(item['date'], '%d %b %Y'): item['num_fail'] - for item in data['data'] + datetime.datetime.strptime(item["date"], "%d %b %Y"): item["num_fail"] + for item in data["data"] } # Fill in missing dates with nearest available value @@ -121,6 +116,7 @@ async def get_corrective_cost_time_chart(material_cost: float, service_cost: flo print(f"Error fetching or processing data: {str(e)}") raise + # def get_corrective_cost_time_chart(material_cost: float, service_cost: float, days: int, numEquipments: int) -> Tuple[np.ndarray, np.ndarray]: # day_points = np.arange(0, days) @@ -147,12 +143,18 @@ async def get_corrective_cost_time_chart(material_cost: float, service_cost: flo # return corrective_costs, daily_failure_rate -async def create_param_and_data(*, db_session: DbSession, calculation_param_in: CalculationTimeConstrainsParametersCreate, created_by: str, parameter_id: Optional[UUID] = None): +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" + detail="overhaul_session_id is required", ) calculationData = await CalculationData.create_with_param( @@ -161,36 +163,39 @@ async def create_param_and_data(*, db_session: DbSession, calculation_param_in: avg_failure_cost=calculation_param_in.costPerFailure, overhaul_cost=calculation_param_in.overhaulCost, created_by=created_by, - params_id=parameter_id + params_id=parameter_id, ) return calculationData async def get_calculation_result(db_session: DbSession, calculation_id: str): - days=365 - scope_calculation = await get_calculation_data_by_id(db_session=db_session, calculation_id=calculation_id) + days = 365 + scope_calculation = await get_calculation_data_by_id( + db_session=db_session, calculation_id=calculation_id + ) if not scope_calculation: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="A data with this id does not exist.", ) - scope_overhaul = await get_scope(db_session=db_session, overhaul_session_id=scope_calculation.overhaul_session_id) + 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="A data with this id does not exist.", ) - calculation_results = [] for i in range(days): result = { "overhaul_cost": 0, "corrective_cost": 0, "num_failures": 0, - "day": i + 1 + "day": i + 1, } for eq in scope_calculation.equipment_results: @@ -200,10 +205,8 @@ async def get_calculation_result(db_session: DbSession, calculation_id: str): result["overhaul_cost"] += float(eq.overhaul_costs[i]) result["num_failures"] += int(eq.daily_failures[i]) - calculation_results.append(CalculationResultsRead(**result)) - # Check if calculation already exist return CalculationTimeConstrainsRead( id=scope_calculation.id, @@ -211,18 +214,22 @@ async def get_calculation_result(db_session: DbSession, calculation_id: str): scope=scope_overhaul.type, results=calculation_results, optimum_oh=scope_calculation.optimum_oh_day, - equipment_results=scope_calculation.equipment_results + equipment_results=scope_calculation.equipment_results, ) -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) +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() @@ -287,20 +294,21 @@ async def get_calculation_data_by_id(db_session: DbSession, calculation_id) -> C async def create_calculation_result_service( - db_session: DbSession, - calculation: CalculationData, - token: str + db_session: DbSession, calculation: CalculationData, token: str ) -> CalculationTimeConstrainsRead: days = 365 # Changed to 365 days as per requirement # Get all equipment for this calculation session - equipments = await get_all_by_session_id(db_session=db_session, overhaul_session_id=calculation.overhaul_session_id) - scope = await get_scope(db_session=db_session, overhaul_session_id=calculation.overhaul_session_id) - - calculation_data = await get_calculation_data_by_id(db_session=db_session, calculation_id=calculation.id) - - + equipments = await get_all_by_session_id( + db_session=db_session, overhaul_session_id=calculation.overhaul_session_id + ) + scope = await get_scope( + db_session=db_session, overhaul_session_id=calculation.overhaul_session_id + ) + calculation_data = await get_calculation_data_by_id( + db_session=db_session, calculation_id=calculation.id + ) # Store results for each equipment equipment_results: List[CalculationEquipmentResult] = [] @@ -313,31 +321,33 @@ async def create_calculation_result_service( material_cost=eq.material_cost, service_cost=eq.service_cost, token=token, - location_tag=eq.equipment.location_tag + location_tag=eq.equipment.location_tag, ) overhaul_cost_points = get_overhaul_cost_by_time_chart( calculation_data.parameter.overhaul_cost, days=len(corrective_costs), - numEquipments=len(equipments) - ) + numEquipments=len(equipments), + ) # Calculate individual equipment optimum points equipment_total_cost = corrective_costs + overhaul_cost_points equipment_optimum_index = np.argmin(equipment_total_cost) equipment_failure_sum = sum(daily_failures[:equipment_optimum_index]) - equipment_results.append(CalculationEquipmentResult( - corrective_costs=corrective_costs.tolist(), - overhaul_costs=overhaul_cost_points.tolist(), - daily_failures=daily_failures.tolist(), - assetnum=eq.assetnum, - material_cost=eq.material_cost, - service_cost=eq.service_cost, - optimum_day=int(equipment_optimum_index + 1), - calculation_data_id=calculation.id, - master_equipment=eq.equipment - )) + equipment_results.append( + CalculationEquipmentResult( + corrective_costs=corrective_costs.tolist(), + overhaul_costs=overhaul_cost_points.tolist(), + daily_failures=daily_failures.tolist(), + assetnum=eq.assetnum, + material_cost=eq.material_cost, + service_cost=eq.service_cost, + optimum_day=int(equipment_optimum_index + 1), + calculation_data_id=calculation.id, + master_equipment=eq.equipment, + ) + ) # Add to totals total_corrective_costs += corrective_costs @@ -345,7 +355,6 @@ async def create_calculation_result_service( db_session.add_all(equipment_results) - # Calculate optimum points using total costs total_cost = total_corrective_costs + overhaul_cost_points optimum_oh_index = np.argmin(total_cost) @@ -355,7 +364,7 @@ async def create_calculation_result_service( overhaul_cost=float(overhaul_cost_points[optimum_oh_index]), corrective_cost=float(total_corrective_costs[optimum_oh_index]), num_failures=int(numbers_of_failure), - days=int(optimum_oh_index + 1) + days=int(optimum_oh_index + 1), ) # # Create calculation results for database @@ -376,7 +385,6 @@ async def create_calculation_result_service( await db_session.commit() - # Return results including individual equipment data return CalculationTimeConstrainsRead( id=calculation.id, @@ -384,26 +392,34 @@ async def create_calculation_result_service( scope=scope.type, results=[], optimum_oh=optimum, - equipment_results=equipment_results + equipment_results=equipment_results, ) -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, - )) +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(CalculationResult).filter(and_( - CalculationResult.day == simulation_day, - CalculationResult.calculation_data_id == calculation_id - )) +async def get_calculation_result_by_day( + *, db_session: DbSession, calculation_id, simulation_day +): + stmt = select(CalculationResult).filter( + and_( + CalculationResult.day == simulation_day, + CalculationResult.calculation_data_id == calculation_id, + ) + ) result = await db_session.execute(stmt) @@ -411,21 +427,22 @@ async def get_calculation_result_by_day(*, db_session: DbSession, calculation_id 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) + 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): +async def bulk_update_equipment( + *, + db: DbSession, + selected_equipments: List[CalculationSelectedEquipmentUpdate], + calculation_data_id: UUID, +): # Create a dictionary mapping assetnum to is_included status - case_mappings = { - asset.assetnum: asset.is_included - for asset in selected_equipments - } + case_mappings = {asset.assetnum: asset.is_included for asset in selected_equipments} # Get all assetnums that need to be updated assetnums = list(case_mappings.keys()) @@ -441,9 +458,13 @@ async def bulk_update_equipment(*, db: DbSession, selected_equipments: List[Calc update(CalculationEquipmentResult) .where(CalculationEquipmentResult.calculation_data_id == calculation_data_id) .where(CalculationEquipmentResult.assetnum.in_(assetnums)) - .values({ - "is_included": case(*when_clauses) # Unpack the when clauses as separate arguments - }) + .values( + { + "is_included": case( + *when_clauses + ) # Unpack the when clauses as separate arguments + } + ) ) await db.execute(stmt) diff --git a/src/config.py b/src/config.py index 9b1b89e..9ac1502 100644 --- a/src/config.py +++ b/src/config.py @@ -1,14 +1,13 @@ +import base64 import logging import os -import base64 -from urllib import parse from typing import List -from pydantic import BaseModel +from urllib import parse +from pydantic import BaseModel from starlette.config import Config from starlette.datastructures import CommaSeparatedStrings - log = logging.getLogger(__name__) @@ -60,10 +59,10 @@ _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_POOL_SIZE = config("DATABASE_ENGINE_POOL_SIZE", cast=int, default=20) DATABASE_ENGINE_MAX_OVERFLOW = config( - "DATABASE_ENGINE_MAX_OVERFLOW", cast=int, default=0) + "DATABASE_ENGINE_MAX_OVERFLOW", cast=int, default=0 +) # Deal with DB disconnects # https://docs.sqlalchemy.org/en/20/core/pooling.html#pool-disconnects DATABASE_ENGINE_POOL_PING = config("DATABASE_ENGINE_POOL_PING", default=False) @@ -74,5 +73,4 @@ 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") +AUTH_SERVICE_API = config("AUTH_SERVICE_API", default="http://192.168.1.82:8000/auth") diff --git a/src/database/core.py b/src/database/core.py index 9c6361b..28c0334 100644 --- a/src/database/core.py +++ b/src/database/core.py @@ -1,27 +1,23 @@ # src/database.py -from starlette.requests import Request -from sqlalchemy_utils import get_mapper -from sqlalchemy.sql.expression import true -from sqlalchemy.orm import object_session, sessionmaker, Session -from sqlalchemy.ext.declarative import declarative_base, declared_attr -from sqlalchemy import create_engine, inspect -from pydantic import BaseModel -from fastapi import Depends -from typing import Annotated, Any -from contextlib import contextmanager -import re import functools -from typing import AsyncGenerator +import re +from contextlib import contextmanager +from typing import Annotated, Any, AsyncGenerator + +from fastapi import Depends +from pydantic import BaseModel +from sqlalchemy import create_engine, inspect from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine -from sqlalchemy.orm import DeclarativeBase, sessionmaker +from sqlalchemy.ext.declarative import declarative_base, declared_attr +from sqlalchemy.orm import (DeclarativeBase, Session, object_session, + sessionmaker) +from sqlalchemy.sql.expression import true +from sqlalchemy_utils import get_mapper +from starlette.requests import Request from src.config import SQLALCHEMY_DATABASE_URI -engine = create_async_engine( - SQLALCHEMY_DATABASE_URI, - echo=False, - future=True -) +engine = create_async_engine(SQLALCHEMY_DATABASE_URI, echo=False, future=True) async_session = sessionmaker( engine, @@ -68,9 +64,8 @@ class CustomBase: for key in self.__repr_attrs__: if not hasattr(self, key): raise KeyError( - "{} has incorrect attribute '{}' in " "__repr__attrs__".format( - self.__class__, key - ) + "{} has incorrect attribute '{}' in " + "__repr__attrs__".format(self.__class__, key) ) value = getattr(self, key) wrap_in_quote = isinstance(value, str) diff --git a/src/database/service.py b/src/database/service.py index bdbbc8f..34850a8 100644 --- a/src/database/service.py +++ b/src/database/service.py @@ -1,15 +1,13 @@ - import logging from typing import Annotated, List -from sqlalchemy import desc, func, or_, Select -from sqlalchemy_filters import apply_pagination +from fastapi import Depends, Query +from pydantic.types import Json, constr +from sqlalchemy import Select, desc, func, or_ from sqlalchemy.exc import ProgrammingError -from .core import DbSession - +from sqlalchemy_filters import apply_pagination -from fastapi import Query, Depends -from pydantic.types import Json, constr +from .core import DbSession log = logging.getLogger(__name__) @@ -27,7 +25,7 @@ def common_parameters( sort_by: List[str] = Query([], alias="sortBy[]"), descending: List[bool] = Query([], alias="descending[]"), exclude: List[str] = Query([], alias="exclude[]"), - all: int = Query(0) + all: int = Query(0), # role: QueryStr = Depends(get_current_role), ): return { @@ -39,14 +37,13 @@ def common_parameters( "sort_by": sort_by, "descending": descending, "current_user": current_user, - "all": bool(all) + "all": bool(all), # "role": role, } CommonParameters = Annotated[ - dict[str, int | str | DbSession | QueryStr | - Json | List[str] | List[bool]] | bool, + dict[str, int | str | DbSession | QueryStr | Json | List[str] | List[bool]] | bool, Depends(common_parameters), ] @@ -75,8 +72,7 @@ def search(*, query_str: str, query: Query, model, sort=False): query = query.filter(or_(*search)) if sort: - query = query.order_by( - desc(func.ts_rank_cd(vector, func.tsq_parse(query_str)))) + query = query.order_by(desc(func.ts_rank_cd(vector, func.tsq_parse(query_str)))) return query.params(term=query_str) @@ -92,7 +88,7 @@ async def search_filter_sort_paginate( descending: List[bool] = None, current_user: str = None, exclude: List[str] = None, - all: bool = False + all: bool = False, ): """Common functionality for searching, filtering, sorting, and pagination.""" # try: @@ -104,8 +100,7 @@ async def search_filter_sort_paginate( if query_str: sort = False if sort_by else True - query = search(query_str=query_str, query=query, - model=model, sort=sort) + query = search(query_str=query_str, query=query, model=model, sort=sort) # Get total count count_query = Select(func.count()).select_from(query.subquery()) @@ -123,11 +118,7 @@ async def search_filter_sort_paginate( "totalPages": 1, } - query = ( - query - .offset((page - 1) * items_per_page) - .limit(items_per_page) - ) + query = query.offset((page - 1) * items_per_page).limit(items_per_page) result = await db_session.execute(query) items = result.scalars().all() diff --git a/src/enums.py b/src/enums.py index 18e0e35..5a13be1 100644 --- a/src/enums.py +++ b/src/enums.py @@ -21,4 +21,4 @@ class OptimumOHEnum(StrEnum): class ResponseStatus(OptimumOHEnum): SUCCESS = "success" - ERROR = "error" \ No newline at end of file + ERROR = "error" diff --git a/src/exceptions.py b/src/exceptions.py index 63e1a9e..0565447 100644 --- a/src/exceptions.py +++ b/src/exceptions.py @@ -1,17 +1,19 @@ # Define base error model import logging from typing import Any, Dict, List, Optional + +from asyncpg.exceptions import DataError as AsyncPGDataError +from asyncpg.exceptions import PostgresError from fastapi import FastAPI, HTTPException, Request from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse from pydantic import BaseModel - -from src.enums import ResponseStatus from slowapi import _rate_limit_exceeded_handler from slowapi.errors import RateLimitExceeded +from sqlalchemy.exc import (DataError, DBAPIError, IntegrityError, + SQLAlchemyError) -from sqlalchemy.exc import SQLAlchemyError, IntegrityError, DataError, DBAPIError -from asyncpg.exceptions import DataError as AsyncPGDataError, PostgresError +from src.enums import ResponseStatus class ErrorDetail(BaseModel): @@ -27,6 +29,7 @@ class ErrorResponse(BaseModel): status: ResponseStatus = ResponseStatus.ERROR errors: Optional[List[ErrorDetail]] = None + # Custom exception handler setup @@ -64,7 +67,7 @@ def handle_sqlalchemy_error(error: SQLAlchemyError): """ Handle SQLAlchemy errors and return user-friendly error messages. """ - original_error = getattr(error, 'orig', None) + original_error = getattr(error, "orig", None) print(original_error) if isinstance(error, IntegrityError): @@ -113,12 +116,8 @@ def handle_exception(request: Request, exc: Exception): "data": None, "message": str(exc.detail), "status": ResponseStatus.ERROR, - "errors": [ - ErrorDetail( - message=str(exc.detail) - ).model_dump() - ] - } + "errors": [ErrorDetail(message=str(exc.detail)).model_dump()], + }, ) if isinstance(exc, SQLAlchemyError): @@ -134,12 +133,8 @@ def handle_exception(request: Request, exc: Exception): "data": None, "message": error_message, "status": ResponseStatus.ERROR, - "errors": [ - ErrorDetail( - message=error_message - ).model_dump() - ] - } + "errors": [ErrorDetail(message=error_message).model_dump()], + }, ) # Log unexpected errors @@ -155,9 +150,7 @@ def handle_exception(request: Request, exc: Exception): "message": str(exc), "status": ResponseStatus.ERROR, "errors": [ - ErrorDetail( - message="An unexpected error occurred." - ).model_dump() - ] - } + ErrorDetail(message="An unexpected error occurred.").model_dump() + ], + }, ) diff --git a/src/job/model.py b/src/job/model.py index df5a425..eba32c7 100644 --- a/src/job/model.py +++ b/src/job/model.py @@ -1,12 +1,10 @@ +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, Integer, String, ForeignKey from src.database.core import Base from src.models import DefaultMixin, IdentityMixin, TimeStampMixin -from sqlalchemy.orm import relationship from src.workorder.model import MasterWorkOrder -from sqlalchemy.ext.hybrid import hybrid_property - - class MasterActivitytask(Base, DefaultMixin): @@ -14,8 +12,11 @@ class MasterActivitytask(Base, DefaultMixin): description = Column(String, nullable=False) oh_type = Column(String, nullable=False) - job_id = Column(UUID(as_uuid=True), ForeignKey( - "oh_ms_job.id", ondelete="cascade"), nullable=False) + job_id = Column( + UUID(as_uuid=True), + ForeignKey("oh_ms_job.id", ondelete="cascade"), + nullable=False, + ) class MasterActivity(Base, DefaultMixin): diff --git a/src/job/router.py b/src/job/router.py index ab73460..65ceee8 100644 --- a/src/job/router.py +++ b/src/job/router.py @@ -1,12 +1,12 @@ - from fastapi import APIRouter, HTTPException, Query, status - -from .service import get_all, create, get, update, delete -from .schema import ActivityMaster, ActivityMasterCreate, ActivityMasterPagination - +from src.database.service import (CommonParameters, DbSession, + search_filter_sort_paginate) from src.models import StandardResponse -from src.database.service import CommonParameters, search_filter_sort_paginate, DbSession + +from .schema import (ActivityMaster, ActivityMasterCreate, + ActivityMasterPagination) +from .service import create, delete, get, get_all, update router = APIRouter() @@ -31,7 +31,9 @@ async def create_activity(db_session: DbSession, activity_in: ActivityMasterCrea return StandardResponse(data=activity, message="Data created successfully") -@router.get("/{scope_equipment_activity_id}", response_model=StandardResponse[ActivityMaster]) +@router.get( + "/{scope_equipment_activity_id}", response_model=StandardResponse[ActivityMaster] +) async def get_activity(db_session: DbSession, activity_id: str): activity = await get(db_session=db_session, activity_id=activity_id) if not activity: @@ -43,8 +45,12 @@ async def get_activity(db_session: DbSession, activity_id: str): return StandardResponse(data=activity, message="Data retrieved successfully") -@router.put("/{scope_equipment_activity_id}", response_model=StandardResponse[ActivityMaster]) -async def update_scope(db_session: DbSession, activity_in: ActivityMasterCreate, activity_id): +@router.put( + "/{scope_equipment_activity_id}", response_model=StandardResponse[ActivityMaster] +) +async def update_scope( + db_session: DbSession, activity_in: ActivityMasterCreate, activity_id +): activity = await get(db_session=db_session, activity_id=activity_id) if not activity: @@ -53,10 +59,17 @@ async def update_scope(db_session: DbSession, activity_in: ActivityMasterCreate, detail="A data with this id does not exist.", ) - return StandardResponse(data=await update(db_session=db_session, activity=activity, activity_in=activity_in), message="Data updated successfully") + return StandardResponse( + data=await update( + db_session=db_session, activity=activity, activity_in=activity_in + ), + message="Data updated successfully", + ) -@router.delete("/{scope_equipment_activity_id}", response_model=StandardResponse[ActivityMaster]) +@router.delete( + "/{scope_equipment_activity_id}", response_model=StandardResponse[ActivityMaster] +) async def delete_scope(db_session: DbSession, activity_id: str): activity = await get(db_session=db_session, activity_id=activity_id) diff --git a/src/job/schema.py b/src/job/schema.py index 1e097a8..f9bc756 100644 --- a/src/job/schema.py +++ b/src/job/schema.py @@ -1,9 +1,9 @@ - from datetime import datetime from typing import Any, Dict, List, Optional from uuid import UUID -from pydantic import Field, BaseModel +from pydantic import BaseModel, Field + from src.models import DefultBase, Pagination @@ -18,10 +18,12 @@ class ActivityMasterDetail(DefultBase): class ActivityMasterCreate(ActivityMaster): description: str + class ActivityMasterTasks(DefultBase): description: str oh_type: str + class ActivityMasterRead(ActivityMaster): id: UUID workscope: str diff --git a/src/job/service.py b/src/job/service.py index 48a3665..9199703 100644 --- a/src/job/service.py +++ b/src/job/service.py @@ -1,15 +1,14 @@ - - -from sqlalchemy import Select, Delete -from sqlalchemy.orm import joinedload, selectinload from typing import Optional -from .model import MasterActivity -from .schema import ActivityMaster, ActivityMasterCreate +from sqlalchemy import Delete, Select +from sqlalchemy.orm import joinedload, selectinload +from src.auth.service import CurrentUser from src.database.core import DbSession from src.database.service import CommonParameters, search_filter_sort_paginate -from src.auth.service import CurrentUser + +from .model import MasterActivity +from .schema import ActivityMaster, ActivityMasterCreate async def get(*, db_session: DbSession, activity_id: str) -> Optional[ActivityMaster]: @@ -27,14 +26,18 @@ async def get_all(common: CommonParameters): async def create(*, db_session: DbSession, activty_in: ActivityMasterCreate): - activity = MasterActivity( - **activty_in.model_dump()) + activity = MasterActivity(**activty_in.model_dump()) db_session.add(activity) await db_session.commit() return activity -async def update(*, db_session: DbSession, activity: MasterActivity, activity_in: ActivityMasterCreate): +async def update( + *, + db_session: DbSession, + activity: MasterActivity, + activity_in: ActivityMasterCreate +): """Updates a document.""" data = activity_in.model_dump() diff --git a/src/logging.py b/src/logging.py index 6d7c6ce..9b35e12 100644 --- a/src/logging.py +++ b/src/logging.py @@ -3,7 +3,6 @@ import logging from src.config import LOG_LEVEL from src.enums import OptimumOHEnum - LOG_FORMAT_DEBUG = "%(levelname)s:%(message)s:%(pathname)s:%(funcName)s:%(lineno)d" diff --git a/src/main.py b/src/main.py index 741bcf2..3d0d1e1 100644 --- a/src/main.py +++ b/src/main.py @@ -1,37 +1,33 @@ - -import time import logging +import time +from contextvars import ContextVar from os import path +from typing import Final, Optional from uuid import uuid1 -from typing import Optional, Final -from contextvars import ContextVar from fastapi import FastAPI, HTTPException, status +from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from pydantic import ValidationError - from slowapi import _rate_limit_exceeded_handler from slowapi.errors import RateLimitExceeded from sqlalchemy import inspect -from sqlalchemy.orm import scoped_session from sqlalchemy.ext.asyncio import async_scoped_session -from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint +from sqlalchemy.orm import scoped_session +from starlette.middleware.base import (BaseHTTPMiddleware, + RequestResponseEndpoint) +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.middleware.gzip import GZipMiddleware -from fastapi.middleware.cors import CORSMiddleware - -from starlette.responses import Response, StreamingResponse, FileResponse from starlette.staticfiles import StaticFiles -import logging +from src.api import api_router +from src.database.core import async_session, engine from src.enums import ResponseStatus +from src.exceptions import handle_exception from src.logging import configure_logging from src.rate_limiter import limiter -from src.api import api_router -from src.database.core import engine, async_session -from src.exceptions import handle_exception - log = logging.getLogger(__name__) @@ -42,9 +38,13 @@ configure_logging() exception_handlers = {Exception: handle_exception} # we create the ASGI for the app -app = FastAPI(exception_handlers=exception_handlers, openapi_url="", title="LCCA API", - description="Welcome to LCCA's API documentation!", - version="0.1.0") +app = FastAPI( + exception_handlers=exception_handlers, + openapi_url="", + title="LCCA API", + description="Welcome to LCCA's API documentation!", + version="0.1.0", +) app.state.limiter = limiter app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) app.add_middleware(GZipMiddleware, minimum_size=2000) @@ -53,7 +53,8 @@ app.add_middleware(GZipMiddleware, minimum_size=2000) REQUEST_ID_CTX_KEY: Final[str] = "request_id" _request_id_ctx_var: ContextVar[Optional[str]] = ContextVar( - REQUEST_ID_CTX_KEY, default=None) + REQUEST_ID_CTX_KEY, default=None +) def get_request_id() -> Optional[str]: @@ -84,9 +85,12 @@ async def db_session_middleware(request: Request, call_next): @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 diff --git a/src/maximo/service.py b/src/maximo/service.py index a0faddf..b30b53e 100644 --- a/src/maximo/service.py +++ b/src/maximo/service.py @@ -1,10 +1,10 @@ - - from datetime import datetime, timedelta from typing import Any, Dict -from fastapi import HTTPException + import httpx +from fastapi import HTTPException from starlette.config import Config + from src.config import MAXIMO_API_KEY, MAXIMO_BASE_URL @@ -24,7 +24,7 @@ class MaximoDataMapper: Example: might be data['startDate'] or data['SCHEDSTART'] etc. """ # This is a placeholder - update with actual MAXIMO field name - start_date_str = self.data.get('scheduleStart') + start_date_str = self.data.get("scheduleStart") if not start_date_str: raise ValueError("Start date not found in MAXIMO data") return datetime.fromisoformat(start_date_str) @@ -35,7 +35,7 @@ class MaximoDataMapper: TODO: Update this based on actual MAXIMO API response structure """ # This is a placeholder - update with actual MAXIMO field name - end_date_str = self.data.get('scheduleEnd') + end_date_str = self.data.get("scheduleEnd") if not end_date_str: raise ValueError("End date not found in MAXIMO data") return datetime.fromisoformat(end_date_str) @@ -46,7 +46,7 @@ class MaximoDataMapper: TODO: Update this based on actual MAXIMO API response structure """ # This is a placeholder - update with actual MAXIMO field name - maximo_id = self.data.get('workOrderId') + maximo_id = self.data.get("workOrderId") if not maximo_id: raise ValueError("MAXIMO ID not found in response") return str(maximo_id) @@ -57,7 +57,7 @@ class MaximoDataMapper: TODO: Update this based on actual MAXIMO API response structure """ # This is a placeholder - update with actual MAXIMO status field and values - status = self.data.get('status', '').upper() + status = self.data.get("status", "").upper() return status def get_total_cost(self) -> float: @@ -66,11 +66,11 @@ class MaximoDataMapper: TODO: Update this based on actual MAXIMO API response structure """ # This is a placeholder - update with actual MAXIMO field name - cost = self.data.get('totalCost', 0) + cost = self.data.get("totalCost", 0) return float(cost) def get_scope_name(self) -> str: - scope_name = self.data.get('location', "A") + scope_name = self.data.get("location", "A") return scope_name @@ -86,10 +86,8 @@ class MaximoService: TODO: Update this method based on actual MAXIMO API endpoints and parameters """ current_date = datetime.now() - schedule_start = current_date + \ - timedelta(days=30) # Starting in 30 days - schedule_end = schedule_start + \ - timedelta(days=90) # 90 day overhaul period + schedule_start = current_date + timedelta(days=30) # Starting in 30 days + schedule_end = schedule_start + timedelta(days=90) # 90 day overhaul period return { "scheduleStart": schedule_start.isoformat(), @@ -104,19 +102,19 @@ class MaximoService: { "assetnum": "ASSET001", "description": "Gas Turbine", - "status": "OPERATING" + "status": "OPERATING", }, { "assetnum": "ASSET002", "description": "Steam Turbine", - "status": "OPERATING" - } + "status": "OPERATING", + }, ], "workType": "OH", # OH for Overhaul "createdBy": "MAXADMIN", "createdDate": (current_date - timedelta(days=10)).isoformat(), "lastModifiedBy": "MAXADMIN", - "lastModifiedDate": current_date.isoformat() + "lastModifiedDate": current_date.isoformat(), } async with httpx.AsyncClient() as client: @@ -131,21 +129,20 @@ class MaximoService: params={ # Update these parameters based on actual MAXIMO API "orderBy": "-scheduleEnd", # Example parameter - "limit": 1 - } + "limit": 1, + }, ) if response.status_code != 200: raise HTTPException( status_code=response.status_code, - detail=f"MAXIMO API error: {response.text}" + detail=f"MAXIMO API error: {response.text}", ) data = response.json() if not data: raise HTTPException( - status_code=404, - detail="No recent overhaul found" + status_code=404, detail="No recent overhaul found" ) # TODO: Update this based on actual MAXIMO response structure @@ -153,6 +150,5 @@ class MaximoService: except httpx.RequestError as e: raise HTTPException( - status_code=503, - detail=f"Failed to connect to MAXIMO: {str(e)}" + status_code=503, detail=f"Failed to connect to MAXIMO: {str(e)}" ) diff --git a/src/models.py b/src/models.py index 533a07a..6ade5db 100644 --- a/src/models.py +++ b/src/models.py @@ -1,16 +1,18 @@ # src/common/models.py +import uuid from datetime import datetime from typing import Generic, Optional, TypeVar -import uuid + +import pytz from pydantic import BaseModel, Field, SecretStr -from sqlalchemy import Column, DateTime, String, func, event +from sqlalchemy import Column, DateTime, String, event, func from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import Mapped, mapped_column -from src.config import TIMEZONE -import pytz from src.auth.service import CurrentUser +from src.config import TIMEZONE from src.enums import ResponseStatus + # SQLAlchemy Mixins @@ -18,10 +20,12 @@ class TimeStampMixin(object): """Timestamping mixin""" created_at = Column( - DateTime(timezone=True), default=datetime.now(pytz.timezone(TIMEZONE))) + DateTime(timezone=True), default=datetime.now(pytz.timezone(TIMEZONE)) + ) created_at._creation_order = 9998 updated_at = Column( - DateTime(timezone=True), default=datetime.now(pytz.timezone(TIMEZONE))) + DateTime(timezone=True), default=datetime.now(pytz.timezone(TIMEZONE)) + ) updated_at._creation_order = 9998 @staticmethod @@ -35,17 +39,25 @@ class TimeStampMixin(object): class UUIDMixin: """UUID mixin""" - id = Column(UUID(as_uuid=True), primary_key=True, - default=uuid.uuid4, unique=True, nullable=False) + + id = Column( + UUID(as_uuid=True), + primary_key=True, + default=uuid.uuid4, + unique=True, + nullable=False, + ) class DefaultMixin(TimeStampMixin, UUIDMixin): """Default mixin""" + pass class IdentityMixin: """Identity mixin""" + created_by = Column(String(100), nullable=True) updated_by = Column(String(100), nullable=True) @@ -77,7 +89,7 @@ class PrimaryKeyModel(BaseModel): # Define data type variable for generic response -T = TypeVar('T') +T = TypeVar("T") class StandardResponse(BaseModel, Generic[T]): diff --git a/src/overhaul/router.py b/src/overhaul/router.py index 0aab2a8..b99046c 100644 --- a/src/overhaul/router.py +++ b/src/overhaul/router.py @@ -1,14 +1,18 @@ - from typing import List + from fastapi import APIRouter, HTTPException, status -from src.overhaul.service import get_overhaul_critical_parts, get_overhaul_overview, get_overhaul_schedules, get_overhaul_system_components +from src.database.core import 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_scope.schema import ScopeRead -from .schema import OverhaulRead, OverhaulCriticalParts, OverhaulSystemComponents +from .schema import (OverhaulCriticalParts, OverhaulRead, + OverhaulSystemComponents) -from src.models import StandardResponse -from src.database.core import DbSession router = APIRouter() @@ -51,7 +55,9 @@ async def get_critical_parts(): ) -@router.get("/system-components", response_model=StandardResponse[OverhaulSystemComponents]) +@router.get( + "/system-components", response_model=StandardResponse[OverhaulSystemComponents] +) async def get_system_components(): """Get all overhaul system components.""" systemComponents = get_overhaul_system_components() diff --git a/src/overhaul/schema.py b/src/overhaul/schema.py index 7b1f7e4..89966e2 100644 --- a/src/overhaul/schema.py +++ b/src/overhaul/schema.py @@ -1,9 +1,9 @@ - from datetime import datetime from typing import Any, Dict, List, Optional from uuid import UUID -from pydantic import Field, BaseModel +from pydantic import BaseModel, Field + from src.models import DefultBase, Pagination from src.overhaul_scope.schema import ScopeRead @@ -17,13 +17,13 @@ class OverhaulCriticalParts(OverhaulBase): class OverhaulSchedules(OverhaulBase): - schedules: List[Dict[str, Any] - ] = Field(..., description="List of schedules") + schedules: List[Dict[str, Any]] = Field(..., description="List of schedules") class OverhaulSystemComponents(OverhaulBase): - systemComponents: Dict[str, - Any] = Field(..., description="List of system components") + systemComponents: Dict[str, Any] = Field( + ..., description="List of system components" + ) class OverhaulRead(OverhaulBase): diff --git a/src/overhaul/service.py b/src/overhaul/service.py index 5e14437..189f514 100644 --- a/src/overhaul/service.py +++ b/src/overhaul/service.py @@ -1,12 +1,12 @@ - - -from sqlalchemy import Select, Delete from typing import Optional -from src.database.core import DbSession +from sqlalchemy import Delete, Select + from src.auth.service import CurrentUser +from src.database.core import DbSession from src.overhaul_scope.model import OverhaulScope -from src.overhaul_scope.service import get_all as get_all_session, get_overview_overhaul +from src.overhaul_scope.service import get_all as get_all_session +from src.overhaul_scope.service import get_overview_overhaul async def get_overhaul_overview(db_session: DbSession): @@ -23,16 +23,16 @@ def get_overhaul_critical_parts(): "Boiler reheater system", "Drum Level (Right) Root Valve A", "BCP A Discharge Valve", - "BFPT A EXH Press HI Root VLV" + "BFPT A EXH Press HI Root VLV", ] 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() @@ -107,7 +107,6 @@ def get_overhaul_system_components(): } - # 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) diff --git a/src/overhaul_activity/model.py b/src/overhaul_activity/model.py index 6968188..32da226 100644 --- a/src/overhaul_activity/model.py +++ b/src/overhaul_activity/model.py @@ -1,18 +1,19 @@ +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, Integer, String, ForeignKey from src.database.core import Base from src.models import DefaultMixin, IdentityMixin, TimeStampMixin -from sqlalchemy.orm import relationship from src.workorder.model import MasterWorkOrder -from sqlalchemy.ext.hybrid import hybrid_property class OverhaulActivity(Base, DefaultMixin): __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") @@ -21,11 +22,10 @@ class OverhaulActivity(Base, DefaultMixin): "MasterEquipment", lazy="raise", primaryjoin="and_(OverhaulActivity.assetnum == foreign(MasterEquipment.assetnum))", - uselist=False # Add this if it's a one-to-one relationship + uselist=False, # Add this if it's a one-to-one relationship ) overhaul_scope = relationship( "OverhaulScope", lazy="raise", ) - diff --git a/src/overhaul_activity/router.py b/src/overhaul_activity/router.py index 95c4f53..4e5e2dd 100644 --- a/src/overhaul_activity/router.py +++ b/src/overhaul_activity/router.py @@ -1,23 +1,36 @@ - from typing import List, Optional from uuid import UUID -from fastapi import APIRouter, HTTPException, Query, status - -from .service import get_all, create, get, update, delete -from .schema import OverhaulActivityCreate, OverhaulActivityPagination, OverhaulActivityRead, OverhaulActivityUpdate +from fastapi import APIRouter, HTTPException, Query, status +from src.database.service import (CommonParameters, DbSession, + search_filter_sort_paginate) from src.models import StandardResponse -from src.database.service import CommonParameters, search_filter_sort_paginate, DbSession + +from .schema import (OverhaulActivityCreate, OverhaulActivityPagination, + OverhaulActivityRead, OverhaulActivityUpdate) +from .service import create, delete, get, get_all, update router = APIRouter() -@router.get("/{overhaul_session}", response_model=StandardResponse[OverhaulActivityPagination]) -async def get_scope_equipments(common: CommonParameters, overhaul_session: str, assetnum: Optional[str] = Query(None), scope_name: Optional[str] = Query(None)): +@router.get( + "/{overhaul_session}", response_model=StandardResponse[OverhaulActivityPagination] +) +async def get_scope_equipments( + common: CommonParameters, + overhaul_session: str, + assetnum: Optional[str] = Query(None), + scope_name: Optional[str] = Query(None), +): """Get all scope activity pagination.""" # return - data = await get_all(common=common, assetnum=assetnum, scope_name=scope_name, overhaul_session_id=overhaul_session) + data = await get_all( + common=common, + assetnum=assetnum, + scope_name=scope_name, + overhaul_session_id=overhaul_session, + ) return StandardResponse( data=data, @@ -25,17 +38,32 @@ async def get_scope_equipments(common: CommonParameters, overhaul_session: str, ) -@ router.post("/{overhaul_session}", response_model=StandardResponse[List[str]]) -async def create_overhaul_equipment(db_session: DbSession, overhaul_activty_in: OverhaulActivityCreate, overhaul_session: str): +@router.post("/{overhaul_session}", response_model=StandardResponse[List[str]]) +async def create_overhaul_equipment( + db_session: DbSession, + overhaul_activty_in: OverhaulActivityCreate, + overhaul_session: str, +): - activity = await create(db_session=db_session, overhaul_activty_in=overhaul_activty_in, overhaul_session_id=overhaul_session) + activity = await create( + db_session=db_session, + overhaul_activty_in=overhaul_activty_in, + overhaul_session_id=overhaul_session, + ) return StandardResponse(data=activity, 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) +@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, @@ -45,8 +73,15 @@ async def get_overhaul_equipment(db_session: DbSession, assetnum: str, overhaul_ 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): +@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: @@ -55,10 +90,20 @@ async def update_scope(db_session: DbSession, scope_equipment_activity_in: Overh 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") + return StandardResponse( + data=await update( + db_session=db_session, + activity=activity, + scope_equipment_activity_in=scope_equipment_activity_in, + ), + message="Data updated successfully", + ) -@ router.delete("/{overhaul_session}/{assetnum}", response_model=StandardResponse[OverhaulActivityRead]) +@router.delete( + "/{overhaul_session}/{assetnum}", + response_model=StandardResponse[OverhaulActivityRead], +) async def delete_scope(db_session: DbSession, assetnum: str): activity = await get(db_session=db_session, assetnum=assetnum) diff --git a/src/overhaul_activity/schema.py b/src/overhaul_activity/schema.py index 3a0912c..8f18c3a 100644 --- a/src/overhaul_activity/schema.py +++ b/src/overhaul_activity/schema.py @@ -1,9 +1,9 @@ - from datetime import datetime from typing import Any, Dict, List, Optional from uuid import UUID from pydantic import Field + from src.models import DefultBase, Pagination from src.scope_equipment.schema import MasterEquipmentRead diff --git a/src/overhaul_activity/service.py b/src/overhaul_activity/service.py index 05d7881..d33a501 100644 --- a/src/overhaul_activity/service.py +++ b/src/overhaul_activity/service.py @@ -1,62 +1,81 @@ - - import asyncio -from uuid import UUID -from sqlalchemy import Select, Delete, func, select, update as sqlUpdate -from sqlalchemy.orm import joinedload from typing import List, Optional +from uuid import UUID + +from sqlalchemy import Delete, Select, func, select +from sqlalchemy import update as sqlUpdate from sqlalchemy.dialects.postgresql import insert +from sqlalchemy.orm import joinedload +from src.auth.service import CurrentUser +from src.database.core import DbSession +from src.database.service import CommonParameters, search_filter_sort_paginate from src.overhaul_activity.utils import get_material_cost, get_service_cost from src.overhaul_scope.model import OverhaulScope +from src.overhaul_scope.service import get as get_session from .model import OverhaulActivity -from .schema import OverhaulActivityCreate, OverhaulActivityUpdate, OverhaulActivityRead - -from src.database.core import DbSession -from src.database.service import CommonParameters, search_filter_sort_paginate -from src.auth.service import CurrentUser -from src.overhaul_scope.service import get as get_session +from .schema import (OverhaulActivityCreate, OverhaulActivityRead, + OverhaulActivityUpdate) -async def get(*, db_session: DbSession, assetnum: str, overhaul_session_id: Optional[UUID] = None) -> Optional[OverhaulActivityRead]: +async def get( + *, db_session: DbSession, assetnum: str, overhaul_session_id: Optional[UUID] = None +) -> Optional[OverhaulActivityRead]: """Returns a document based on the given document id.""" - query = Select(OverhaulActivity).where( - OverhaulActivity.assetnum == assetnum).options(joinedload(OverhaulActivity.equipment)) + query = ( + Select(OverhaulActivity) + .where(OverhaulActivity.assetnum == assetnum) + .options(joinedload(OverhaulActivity.equipment)) + ) if overhaul_session_id: - query = query.filter( - OverhaulActivity.overhaul_scope_id == overhaul_session_id) + query = query.filter(OverhaulActivity.overhaul_scope_id == overhaul_session_id) result = await db_session.execute(query) return result.scalar() -async def get_all(*, common: CommonParameters, overhaul_session_id: UUID, assetnum: Optional[str] = None, scope_name: Optional[str] = None): - query = Select(OverhaulActivity).where( - OverhaulActivity.overhaul_scope_id == overhaul_session_id).options(joinedload(OverhaulActivity.equipment)) +async def get_all( + *, + common: CommonParameters, + overhaul_session_id: UUID, + assetnum: Optional[str] = None, + scope_name: Optional[str] = None +): + query = ( + Select(OverhaulActivity) + .where(OverhaulActivity.overhaul_scope_id == overhaul_session_id) + .options(joinedload(OverhaulActivity.equipment)) + ) if assetnum: query = query.filter(OverhaulActivity.assetnum == assetnum).options( - joinedload(OverhaulActivity.overhaul_scope)) + joinedload(OverhaulActivity.overhaul_scope) + ) if scope_name: query = query.filter(OverhaulActivity.scope_name == scope_name).options( - joinedload(OverhaulActivity.overhaul_scope)) + joinedload(OverhaulActivity.overhaul_scope) + ) results = await search_filter_sort_paginate(model=query, **common) return results -async def get_all_by_session_id(*, db_session:DbSession, overhaul_session_id): - query = Select(OverhaulActivity).where( - OverhaulActivity.overhaul_scope_id == overhaul_session_id).options(joinedload(OverhaulActivity.equipment)) - +async def get_all_by_session_id(*, db_session: DbSession, overhaul_session_id): + query = ( + Select(OverhaulActivity) + .where(OverhaulActivity.overhaul_scope_id == overhaul_session_id) + .options(joinedload(OverhaulActivity.equipment)) + ) + results = await db_session.execute(query) - + return results.scalars().all() + # async def create(*, db_session: DbSession, overhaul_activty_in: OverhaulActivityCreate, overhaul_session_id: UUID): # # Check if the combination of assetnum and activity_id already exists # existing_equipment_query = ( @@ -90,14 +109,22 @@ async def get_all_by_session_id(*, db_session:DbSession, overhaul_session_id): # return activity_with_relationship -async def create(*, db_session: DbSession, overhaul_activty_in: OverhaulActivityCreate, overhaul_session_id: UUID): + +async def create( + *, + db_session: DbSession, + overhaul_activty_in: OverhaulActivityCreate, + overhaul_session_id: UUID +): """Creates a new document.""" assetnums = overhaul_activty_in.assetnums if not assetnums: return [] # Get session and count in parallel - session = await get_session(db_session=db_session, overhaul_session_id=overhaul_session_id) + session = await get_session( + db_session=db_session, overhaul_session_id=overhaul_session_id + ) equipment_count = await db_session.scalar( select(func.count()) .select_from(OverhaulActivity) @@ -107,25 +134,25 @@ async def create(*, db_session: DbSession, overhaul_activty_in: OverhaulActivity # Calculate costs for all records total_equipment = equipment_count + len(assetnums) material_cost = get_material_cost( - scope=session.type, total_equipment=total_equipment) - service_cost = get_service_cost( - scope=session.type, total_equipment=total_equipment) + scope=session.type, total_equipment=total_equipment + ) + service_cost = get_service_cost(scope=session.type, total_equipment=total_equipment) # Create the insert statement - stmt = insert(OverhaulActivity).values([ - { - 'assetnum': assetnum, - 'overhaul_scope_id': overhaul_session_id, - 'material_cost': material_cost, - 'service_cost': service_cost - } - for assetnum in assetnums - ]) + stmt = insert(OverhaulActivity).values( + [ + { + "assetnum": assetnum, + "overhaul_scope_id": overhaul_session_id, + "material_cost": material_cost, + "service_cost": service_cost, + } + for assetnum in assetnums + ] + ) # Add the ON CONFLICT DO NOTHING clause - stmt = stmt.on_conflict_do_nothing( - index_elements=["assetnum", "overhaul_scope_id"] - ) + stmt = stmt.on_conflict_do_nothing(index_elements=["assetnum", "overhaul_scope_id"]) # Execute the statement await db_session.execute(stmt) @@ -139,7 +166,12 @@ async def create(*, db_session: DbSession, overhaul_activty_in: OverhaulActivity return assetnums -async def update(*, db_session: DbSession, activity: OverhaulActivity, overhaul_activity_in: OverhaulActivityUpdate): +async def update( + *, + db_session: DbSession, + activity: OverhaulActivity, + overhaul_activity_in: OverhaulActivityUpdate +): """Updates a document.""" data = overhaul_activity_in.model_dump() diff --git a/src/overhaul_activity/utils.py b/src/overhaul_activity/utils.py index 6675945..af17d97 100644 --- a/src/overhaul_activity/utils.py +++ b/src/overhaul_activity/utils.py @@ -1,33 +1,35 @@ 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 return float(0) - - if scope == 'B': - result = Decimal('365539731101') / Decimal(str(total_equipment)) + + if scope == "B": + result = Decimal("365539731101") / Decimal(str(total_equipment)) return float(result) else: - result = Decimal('8565468127') / Decimal(str(total_equipment)) + 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 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)) + result = Decimal("36000000000") / Decimal(str(total_equipment)) return float(result) - - return float(0) \ No newline at end of file + + return float(0) diff --git a/src/overhaul_job/model.py b/src/overhaul_job/model.py index c594aa6..d5fd766 100644 --- a/src/overhaul_job/model.py +++ b/src/overhaul_job/model.py @@ -1,28 +1,29 @@ +from sqlalchemy import (UUID, Column, DateTime, Float, ForeignKey, Integer, + String) +from sqlalchemy.orm import relationship -from sqlalchemy import Column, DateTime, Float, Integer, String, UUID, ForeignKey from src.database.core import Base from src.models import DefaultMixin, IdentityMixin, TimeStampMixin -from sqlalchemy.orm import relationship class OverhaulJob(Base, DefaultMixin): __tablename__ = "oh_tr_overhaul_job" - overhaul_activity_id = Column(UUID(as_uuid=True), ForeignKey( - "oh_tr_overhaul_activity.id"), nullable=False) - - scope_equipment_job_id = Column(UUID(as_uuid=True), ForeignKey( - "oh_ms_scope_equipment_job.id", ondelete="cascade"), nullable=False) - + overhaul_activity_id = Column( + UUID(as_uuid=True), ForeignKey("oh_tr_overhaul_activity.id"), nullable=False + ) + scope_equipment_job_id = Column( + UUID(as_uuid=True), + ForeignKey("oh_ms_scope_equipment_job.id", ondelete="cascade"), + nullable=False, + ) notes = Column(String, nullable=True) status = Column(String, nullable=True, default="pending") scope_equipment_job = relationship( - "ScopeEquipmentJob", lazy="raise" + "ScopeEquipmentJob", lazy="raise", back_populates="overhaul_jobs" ) - overhaul_activity = relationship( - "OverhaulActivity", lazy="raise" - ) + overhaul_activity = relationship("OverhaulActivity", lazy="raise") diff --git a/src/overhaul_job/router.py b/src/overhaul_job/router.py index 51c734a..5d9b222 100644 --- a/src/overhaul_job/router.py +++ b/src/overhaul_job/router.py @@ -1,19 +1,22 @@ +from typing import List, Optional -from typing import Optional, List from fastapi import APIRouter, HTTPException, status -from .schema import OverhaulJobBase, OverhaulJobRead, OverhaulJobPagination, OverhaulJobCreate -from .service import get_all, create - -from src.database.service import CommonParameters -from src.database.core import DbSession from src.auth.service import CurrentUser +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 .service import create, get_all + router = APIRouter() -@router.get("/{overhaul_equipment_id}", response_model=StandardResponse[OverhaulJobPagination]) +@router.get( + "/{overhaul_equipment_id}", response_model=StandardResponse[OverhaulJobPagination] +) async def get_jobs(common: CommonParameters, overhaul_equipment_id: str): """Get all scope pagination.""" # return @@ -24,11 +27,18 @@ async def get_jobs(common: CommonParameters, overhaul_equipment_id: str): 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): +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) + await create( + db_session=db_session, + overhaul_equipment_id=overhaul_equipment_id, + overhaul_job_in=overhaul_job_in, + ) return StandardResponse( data=None, diff --git a/src/overhaul_job/schema.py b/src/overhaul_job/schema.py index 3976c7a..51dc84d 100644 --- a/src/overhaul_job/schema.py +++ b/src/overhaul_job/schema.py @@ -1,12 +1,14 @@ - from datetime import datetime from typing import List, Optional from uuid import UUID from pydantic import Field + from src.models import DefultBase, Pagination +from src.overhaul_scope.schema import ScopeRead from src.scope_equipment_job.schema import ScopeEquipmentJobRead + class OverhaulJobBase(DefultBase): pass @@ -18,9 +20,11 @@ class OverhaulJobCreate(OverhaulJobBase): class OverhaulJobUpdate(OverhaulJobBase): pass + class OverhaulActivity(DefultBase): id: UUID overhaul_scope_id: UUID + overhaul_scope: ScopeRead class OverhaulJobRead(OverhaulJobBase): diff --git a/src/overhaul_job/service.py b/src/overhaul_job/service.py index be602a4..744a194 100644 --- a/src/overhaul_job/service.py +++ b/src/overhaul_job/service.py @@ -1,43 +1,48 @@ +from typing import Optional - -from sqlalchemy import Select, Delete, func +from sqlalchemy import Delete, Select, func from sqlalchemy.orm import selectinload +from src.auth.service import CurrentUser +from src.database.core import DbSession from src.database.service import search_filter_sort_paginate + from .model import OverhaulJob -from typing import Optional from .schema import OverhaulJobCreate -from src.database.core import DbSession -from src.auth.service import CurrentUser - - async def get_all(*, common, overhaul_equipment_id: str): """Returns all documents.""" - query = Select(OverhaulJob).where(OverhaulJob.overhaul_activity_id == overhaul_equipment_id).options( - selectinload(OverhaulJob.scope_equipment_job), selectinload(OverhaulJob.overhaul_activity) + query = ( + Select(OverhaulJob) + .where(OverhaulJob.overhaul_activity_id == overhaul_equipment_id) + .options( + selectinload(OverhaulJob.scope_equipment_job), + selectinload(OverhaulJob.overhaul_activity), + ) ) results = await search_filter_sort_paginate(model=query, **common) return results -async def create(*, db_session: DbSession, overhaul_equipment_id, overhaul_job_in: OverhaulJobCreate): +async def create( + *, db_session: DbSession, overhaul_equipment_id, overhaul_job_in: OverhaulJobCreate +): overhaul_jobs = [] if not overhaul_equipment_id: raise ValueError("assetnum parameter is required") equipment_stmt = Select(OverhaulJob).where( - OverhaulJob.overhaul_activity_id == overhaul_equipment_id) + OverhaulJob.overhaul_activity_id == overhaul_equipment_id + ) equipment = await db_session.scalar(equipment_stmt) - for job_id in overhaul_job_in.job_ids: overhaul_equipment_job = OverhaulJob( - overhaul_activity_id=overhaul_equipment_id, scope_equipment_job_id=job_id + overhaul_activity_id=overhaul_equipment_id, scope_equipment_job_id=job_id ) overhaul_jobs.append(overhaul_equipment_job) diff --git a/src/overhaul_scope/model.py b/src/overhaul_scope/model.py index 58dcf18..88afacf 100644 --- a/src/overhaul_scope/model.py +++ b/src/overhaul_scope/model.py @@ -1,8 +1,8 @@ - from sqlalchemy import Column, DateTime, Float, Integer, String +from sqlalchemy.orm import relationship + from src.database.core import Base from src.models import DefaultMixin, IdentityMixin, TimeStampMixin -from sqlalchemy.orm import relationship class OverhaulScope(Base, DefaultMixin): @@ -15,7 +15,4 @@ class OverhaulScope(Base, DefaultMixin): crew_number = Column(Integer, nullable=True, default=1) status = Column(String, nullable=False, default="upcoming") - activity_equipments = relationship( - "OverhaulActivity", - lazy="selectin" - ) \ No newline at end of file + activity_equipments = relationship("OverhaulActivity", lazy="selectin") diff --git a/src/overhaul_scope/router.py b/src/overhaul_scope/router.py index 7eb823c..09a6bb8 100644 --- a/src/overhaul_scope/router.py +++ b/src/overhaul_scope/router.py @@ -1,16 +1,16 @@ - from typing import Optional -from fastapi import APIRouter, HTTPException, status -from .model import OverhaulScope -from .schema import ScopeCreate, ScopeRead, ScopeUpdate, ScopePagination -from .service import get, get_all, create, update, delete +from fastapi import APIRouter, HTTPException, status -from src.database.service import CommonParameters, search_filter_sort_paginate -from src.database.core import DbSession from src.auth.service import CurrentUser +from src.database.core import DbSession +from src.database.service import CommonParameters, search_filter_sort_paginate from src.models import StandardResponse +from .model import OverhaulScope +from .schema import ScopeCreate, ScopePagination, ScopeRead, ScopeUpdate +from .service import create, delete, get, get_all, update + router = APIRouter() @@ -46,7 +46,12 @@ async def create_scope(db_session: DbSession, scope_in: ScopeCreate): @router.put("/{scope_id}", response_model=StandardResponse[ScopeRead]) -async def update_scope(db_session: DbSession, scope_id: str, scope_in: ScopeUpdate, current_user: CurrentUser): +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: @@ -55,7 +60,10 @@ async def update_scope(db_session: DbSession, scope_id: str, scope_in: ScopeUpda 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") + 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]) diff --git a/src/overhaul_scope/schema.py b/src/overhaul_scope/schema.py index f8eef00..c7c78ef 100644 --- a/src/overhaul_scope/schema.py +++ b/src/overhaul_scope/schema.py @@ -1,9 +1,9 @@ - from datetime import datetime from typing import List, Optional from uuid import UUID from pydantic import Field + from src.models import DefultBase, Pagination diff --git a/src/overhaul_scope/service.py b/src/overhaul_scope/service.py index 1e48a3f..6ff938c 100644 --- a/src/overhaul_scope/service.py +++ b/src/overhaul_scope/service.py @@ -1,24 +1,24 @@ +from typing import Optional +from sqlalchemy import Delete, Select, func -from sqlalchemy import Select, Delete, func - +from src.auth.service import CurrentUser +from src.database.core import DbSession from src.database.service import search_filter_sort_paginate from src.overhaul_activity.model import OverhaulActivity from src.scope_equipment.service import get_by_scope_name from src.utils import time_now + from .model import OverhaulScope from .schema import ScopeCreate, ScopeUpdate from .utils import get_material_cost, get_service_cost -from typing import Optional - -from src.database.core import DbSession -from src.auth.service import CurrentUser -async def get(*, db_session: DbSession, overhaul_session_id: str) -> Optional[OverhaulScope]: +async def get( + *, db_session: DbSession, overhaul_session_id: str +) -> Optional[OverhaulScope]: """Returns a document based on the given document id.""" - query = Select(OverhaulScope).filter( - OverhaulScope.id == overhaul_session_id) + query = Select(OverhaulScope).filter(OverhaulScope.id == overhaul_session_id) result = await db_session.execute(query) return result.scalars().one_or_none() @@ -44,13 +44,14 @@ async def create(*, db_session: DbSession, scope_in: ScopeCreate): scope_name = scope_in.type # Fix the function call - parameters were in wrong order - equipments = await get_by_scope_name( - db_session=db_session, - scope_name=scope_name - ) + equipments = await get_by_scope_name(db_session=db_session, scope_name=scope_name) - material_cost = get_material_cost(scope=overhaul_session.type, total_equipment=len(equipments)) - service_cost = get_service_cost(scope=overhaul_session.type, total_equipment=len(equipments)) + material_cost = get_material_cost( + scope=overhaul_session.type, total_equipment=len(equipments) + ) + service_cost = get_service_cost( + scope=overhaul_session.type, total_equipment=len(equipments) + ) scope_equipments = [ OverhaulActivity( @@ -95,24 +96,20 @@ async def get_overview_overhaul(*, db_session: DbSession): current_date = time_now().date() - # For ongoing overhaul with count - ongoing_query = Select( - OverhaulScope, - func.count(OverhaulActivity.id).label('equipment_count') - ).outerjoin( - OverhaulScope.activity_equipments - ).where( - OverhaulScope.start_date <= current_date, - OverhaulScope.end_date >= current_date, - ).group_by( - OverhaulScope.id + ongoing_query = ( + Select(OverhaulScope, func.count(OverhaulActivity.id).label("equipment_count")) + .outerjoin(OverhaulScope.activity_equipments) + .where( + OverhaulScope.start_date <= current_date, + OverhaulScope.end_date >= current_date, + ) + .group_by(OverhaulScope.id) ) ongoing_result = await db_session.execute(ongoing_query) # Use first() instead of scalar_one_or_none() ongoing_result = ongoing_result.first() - if ongoing_result: ongoing_overhaul, equipment_count = ongoing_result # Unpack the result tuple @@ -126,22 +123,19 @@ async def get_overview_overhaul(*, db_session: DbSession): "duration_oh": ongoing_overhaul.duration_oh, "crew_number": ongoing_overhaul.crew_number, "remaining_days": (ongoing_overhaul.end_date - current_date).days, - "equipment_count": equipment_count - } + "equipment_count": equipment_count, + }, } # For upcoming overhaul with count - upcoming_query = Select( - OverhaulScope, - func.count(OverhaulActivity.id).label('equipment_count') - ).outerjoin( - OverhaulScope.activity_equipments - ).where( - OverhaulScope.start_date > current_date, - ).group_by( - OverhaulScope.id - ).order_by( - OverhaulScope.start_date + upcoming_query = ( + Select(OverhaulScope, func.count(OverhaulActivity.id).label("equipment_count")) + .outerjoin(OverhaulScope.activity_equipments) + .where( + OverhaulScope.start_date > current_date, + ) + .group_by(OverhaulScope.id) + .order_by(OverhaulScope.start_date) ) upcoming_result = await db_session.execute(upcoming_query) @@ -161,14 +155,8 @@ async def get_overview_overhaul(*, db_session: DbSession): "duration_oh": upcoming_overhaul.duration_oh, "crew_number": upcoming_overhaul.crew_number, "remaining_days": days_until, - "equipment_count": equipment_count - } + "equipment_count": equipment_count, + }, } - return { - "status": "no_overhaul", - "overhaul": None - } - - - \ No newline at end of file + return {"status": "no_overhaul", "overhaul": None} diff --git a/src/overhaul_scope/utils.py b/src/overhaul_scope/utils.py index 6675945..af17d97 100644 --- a/src/overhaul_scope/utils.py +++ b/src/overhaul_scope/utils.py @@ -1,33 +1,35 @@ 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 return float(0) - - if scope == 'B': - result = Decimal('365539731101') / Decimal(str(total_equipment)) + + if scope == "B": + result = Decimal("365539731101") / Decimal(str(total_equipment)) return float(result) else: - result = Decimal('8565468127') / Decimal(str(total_equipment)) + 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 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)) + result = Decimal("36000000000") / Decimal(str(total_equipment)) return float(result) - - return float(0) \ No newline at end of file + + return float(0) diff --git a/src/rate_limiter.py b/src/rate_limiter.py index 52054d3..38404a8 100644 --- a/src/rate_limiter.py +++ b/src/rate_limiter.py @@ -1,5 +1,4 @@ from slowapi import Limiter from slowapi.util import get_remote_address - limiter = Limiter(key_func=get_remote_address) diff --git a/src/scope_equipment/enum.py b/src/scope_equipment/enum.py index 3a427e0..aea2ce7 100644 --- a/src/scope_equipment/enum.py +++ b/src/scope_equipment/enum.py @@ -1,5 +1,3 @@ - - from src.enums import OptimumOHEnum diff --git a/src/scope_equipment/model.py b/src/scope_equipment/model.py index eb95bba..0d502f8 100644 --- a/src/scope_equipment/model.py +++ b/src/scope_equipment/model.py @@ -1,10 +1,10 @@ +from sqlalchemy import UUID, Column, Date, Float, ForeignKey, Integer, String +from sqlalchemy.ext.hybrid import hybrid_property +from sqlalchemy.orm import relationship -from sqlalchemy import UUID, Column, Date, Float, Integer, String, ForeignKey from src.database.core import Base from src.models import DefaultMixin, IdentityMixin, TimeStampMixin -from sqlalchemy.orm import relationship from src.workorder.model import MasterWorkOrder -from sqlalchemy.ext.hybrid import hybrid_property class ScopeEquipment(Base, DefaultMixin): @@ -20,7 +20,7 @@ class ScopeEquipment(Base, DefaultMixin): "MasterEquipment", lazy="raise", primaryjoin="and_(ScopeEquipment.assetnum == foreign(MasterEquipment.assetnum))", - uselist=False # Add this if it's a one-to-one relationship + uselist=False, # Add this if it's a one-to-one relationship ) @@ -32,11 +32,11 @@ class MasterEquipment(Base, DefaultMixin): system_tag = Column(String, nullable=True) location_tag = Column(String, nullable=True) name = Column(String, nullable=True) - equipment_tree_id = Column(UUID(as_uuid=True), ForeignKey( - "ms_equipment_tree.id"), nullable=True) + equipment_tree_id = Column( + UUID(as_uuid=True), ForeignKey("ms_equipment_tree.id"), nullable=True + ) - equipment_tree = relationship( - "MasterEquipmentTree", backref="master_equipments") + equipment_tree = relationship("MasterEquipmentTree", backref="master_equipments") class MasterEquipmentTree(Base, DefaultMixin): diff --git a/src/scope_equipment/router.py b/src/scope_equipment/router.py index 5e6325a..851c72a 100644 --- a/src/scope_equipment/router.py +++ b/src/scope_equipment/router.py @@ -1,17 +1,20 @@ - from typing import List, Optional + from fastapi import APIRouter, HTTPException, status from fastapi.params import Query -from .model import ScopeEquipment -from .schema import ScopeEquipmentCreate, ScopeEquipmentPagination, ScopeEquipmentRead, ScopeEquipmentUpdate, MasterEquipmentPagination -from .service import get_all, create, update, delete, get_all_master_equipment, get_by_assetnum - -from src.database.service import CommonParameters, search_filter_sort_paginate -from src.database.core import DbSession from src.auth.service import CurrentUser +from src.database.core import DbSession +from src.database.service import CommonParameters, search_filter_sort_paginate from src.models import StandardResponse +from .model import ScopeEquipment +from .schema import (MasterEquipmentPagination, ScopeEquipmentCreate, + ScopeEquipmentPagination, ScopeEquipmentRead, + ScopeEquipmentUpdate) +from .service import (create, delete, get_all, get_all_master_equipment, + get_by_assetnum, update) + router = APIRouter() @@ -27,7 +30,10 @@ async def get_scope_equipments(common: CommonParameters, scope_name: str = Query ) -@router.get("/available/{scope_name}", response_model=StandardResponse[MasterEquipmentPagination]) +@router.get( + "/available/{scope_name}", + response_model=StandardResponse[MasterEquipmentPagination], +) async def get_master_equipment(common: CommonParameters, scope_name: str): results = await get_all_master_equipment(common=common, scope_name=scope_name) @@ -35,14 +41,18 @@ async def get_master_equipment(common: CommonParameters, scope_name: str): @router.post("", response_model=StandardResponse[List[str]]) -async def create_scope_equipment(db_session: DbSession, scope_equipment_in: ScopeEquipmentCreate): +async def create_scope_equipment( + db_session: DbSession, scope_equipment_in: ScopeEquipmentCreate +): scope = await create(db_session=db_session, scope_equipment_in=scope_equipment_in) return StandardResponse(data=scope, message="Data created successfully") @router.put("/{assetnum}", response_model=StandardResponse[ScopeEquipmentRead]) -async def update_scope_equipment(db_session: DbSession, assetnum: str, scope__equipment_in: ScopeEquipmentUpdate): +async def update_scope_equipment( + db_session: DbSession, assetnum: str, scope__equipment_in: ScopeEquipmentUpdate +): scope_equipment = await get_by_assetnum(db_session=db_session, assetnum=assetnum) if not scope_equipment: @@ -51,7 +61,14 @@ async def update_scope_equipment(db_session: DbSession, assetnum: str, scope__eq detail="A data with this id does not exist.", ) - return StandardResponse(data=await update(db_session=db_session, scope_equipment=scope_equipment, scope__equipment_in=scope__equipment_in), message="Data updated successfully") + return StandardResponse( + data=await update( + db_session=db_session, + scope_equipment=scope_equipment, + scope__equipment_in=scope__equipment_in, + ), + message="Data updated successfully", + ) @router.delete("/{assetnum}", response_model=StandardResponse[None]) diff --git a/src/scope_equipment/schema.py b/src/scope_equipment/schema.py index e57b2fa..bf882c3 100644 --- a/src/scope_equipment/schema.py +++ b/src/scope_equipment/schema.py @@ -1,11 +1,12 @@ - from datetime import datetime from typing import List, Optional from uuid import UUID from pydantic import Field, computed_field, field_validator, validator + from src.models import DefultBase, Pagination from src.overhaul_scope.schema import ScopeRead + from .enum import ScopeEquipmentType diff --git a/src/scope_equipment/service.py b/src/scope_equipment/service.py index 3d255c3..5704ac8 100644 --- a/src/scope_equipment/service.py +++ b/src/scope_equipment/service.py @@ -1,25 +1,28 @@ - - from datetime import datetime, timedelta +from typing import Optional, Union + from fastapi import HTTPException, status -from sqlalchemy import Select, Delete, and_, desc, func, not_, or_ +from sqlalchemy import Delete, Select, and_, desc, func, not_, or_ from sqlalchemy.dialects.postgresql import insert +from sqlalchemy.orm import selectinload + +from src.auth.service import CurrentUser +from src.database.core import DbSession +from src.database.service import CommonParameters, search_filter_sort_paginate from src.overhaul_scope.model import OverhaulScope from src.scope_equipment.enum import ScopeEquipmentType from src.workorder.model import MasterWorkOrder -from .model import MasterEquipmentTree, ScopeEquipment, MasterEquipment -from .schema import ScopeEquipmentCreate, ScopeEquipmentUpdate -from typing import Optional, Union -from sqlalchemy.orm import selectinload -from src.database.service import CommonParameters, search_filter_sort_paginate -from src.database.core import DbSession -from src.auth.service import CurrentUser +from .model import MasterEquipment, MasterEquipmentTree, ScopeEquipment +from .schema import ScopeEquipmentCreate, ScopeEquipmentUpdate async def get_by_assetnum(*, db_session: DbSession, assetnum: str): - query = Select(ScopeEquipment).filter(ScopeEquipment.assetnum == assetnum).options( - selectinload(ScopeEquipment.master_equipment)) + query = ( + Select(ScopeEquipment) + .filter(ScopeEquipment.assetnum == assetnum) + .options(selectinload(ScopeEquipment.master_equipment)) + ) result = await db_session.execute(query) return result.unique().scalars().one_or_none() @@ -28,7 +31,8 @@ async def get_by_assetnum(*, db_session: DbSession, assetnum: str): async def get_all(*, common, scope_name: str = None): """Returns all documents.""" query = Select(ScopeEquipment).options( - selectinload(ScopeEquipment.master_equipment)) + selectinload(ScopeEquipment.master_equipment) + ) query = query.order_by(desc(ScopeEquipment.created_at)) @@ -48,12 +52,17 @@ async def create(*, db_session: DbSession, scope_equipment_in: ScopeEquipmentCre if scope_equipment_in.type == ScopeEquipmentType.TEMP: # Search for the next or ongoing overhaul session for the given scope - stmt = Select(OverhaulScope.end_date).where( - OverhaulScope.type == scope_equipment_in.scope_name, - (OverhaulScope.start_date <= datetime.now()) & ( - OverhaulScope.end_date >= datetime.now()) # Ongoing - | (OverhaulScope.start_date > datetime.now()) # Upcoming - ).order_by(OverhaulScope.start_date.asc()).limit(1) + stmt = ( + Select(OverhaulScope.end_date) + .where( + OverhaulScope.type == scope_equipment_in.scope_name, + (OverhaulScope.start_date <= datetime.now()) + & (OverhaulScope.end_date >= datetime.now()) # Ongoing + | (OverhaulScope.start_date > datetime.now()), # Upcoming + ) + .order_by(OverhaulScope.start_date.asc()) + .limit(1) + ) result = await db_session.execute(stmt) removal_date = result.scalar_one_or_none() @@ -61,14 +70,16 @@ async def create(*, db_session: DbSession, scope_equipment_in: ScopeEquipmentCre # If no overhaul found, set a default removal date or handle the error if removal_date is None: # Handle if no overhaul session is found, set default or raise an error - removal_date = datetime.now() + timedelta(days=30) # Example: 30 days from now + removal_date = datetime.now() + timedelta( + days=30 + ) # Example: 30 days from now for assetnum in assetnums: stmt = insert(ScopeEquipment).values( assetnum=assetnum, scope_overhaul=scope_equipment_in.scope_name, type=scope_equipment_in.type, - removal_date=removal_date + removal_date=removal_date, ) stmt = stmt.on_conflict_do_nothing( @@ -82,7 +93,12 @@ async def create(*, db_session: DbSession, scope_equipment_in: ScopeEquipmentCre return results -async def update(*, db_session: DbSession, scope_equipment: ScopeEquipment, scope_equipment_in: ScopeEquipmentUpdate): +async def update( + *, + db_session: DbSession, + scope_equipment: ScopeEquipment, + scope_equipment_in: ScopeEquipmentUpdate +): """Updates a document.""" data = scope_equipment_in.model_dump() @@ -99,11 +115,10 @@ async def update(*, db_session: DbSession, scope_equipment: ScopeEquipment, scop async def delete(*, db_session: DbSession, assetnum: str): """Deletes a document.""" - query = Delete(ScopeEquipment).where( - ScopeEquipment.assetnum == assetnum) + query = Delete(ScopeEquipment).where(ScopeEquipment.assetnum == assetnum) await db_session.execute(query) await db_session.commit() - + return assetnum # query = Select(ScopeEquipment).filter( @@ -128,10 +143,13 @@ async def delete(*, db_session: DbSession, assetnum: str): # await db_session.commit() -async def get_by_scope_name(*, db_session: DbSession, scope_name: Optional[str]) -> Optional[ScopeEquipment]: +async def get_by_scope_name( + *, db_session: DbSession, scope_name: Optional[str] +) -> Optional[ScopeEquipment]: """Returns a document based on the given document id.""" query = Select(ScopeEquipment).options( - selectinload(ScopeEquipment.master_equipment)) + selectinload(ScopeEquipment.master_equipment) + ) if scope_name: query = query.filter(ScopeEquipment.scope_overhaul == scope_name) @@ -156,11 +174,14 @@ async def get_by_scope_name(*, db_session: DbSession, scope_name: Optional[str]) async def get_all_master_equipment(*, common: CommonParameters, scope_name): - equipments_scope = [equip.assetnum for equip in await get_by_scope_name( - db_session=common.get("db_session"), scope_name=scope_name)] + equipments_scope = [ + equip.assetnum + for equip in await get_by_scope_name( + db_session=common.get("db_session"), scope_name=scope_name + ) + ] - query = Select(MasterEquipment).filter( - MasterEquipment.assetnum.is_not(None)) + query = Select(MasterEquipment).filter(MasterEquipment.assetnum.is_not(None)) # Only add not_in filter if there are items in equipments_scope if equipments_scope: @@ -171,9 +192,11 @@ async def get_all_master_equipment(*, common: CommonParameters, scope_name): async def get_equipment_level_by_no(*, db_session: DbSession, level: int): - query = Select(MasterEquipment).join(MasterEquipment.equipment_tree).where( - MasterEquipmentTree.level_no == level) - + query = ( + Select(MasterEquipment) + .join(MasterEquipment.equipment_tree) + .where(MasterEquipmentTree.level_no == level) + ) + result = await db_session.execute(query) return result.scalars().all() - diff --git a/src/scope_equipment_job/model.py b/src/scope_equipment_job/model.py index 9c38e0c..ea313bd 100644 --- a/src/scope_equipment_job/model.py +++ b/src/scope_equipment_job/model.py @@ -1,22 +1,27 @@ +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, Integer, String, ForeignKey from src.database.core import Base from src.models import DefaultMixin, IdentityMixin, TimeStampMixin -from sqlalchemy.orm import relationship from src.workorder.model import MasterWorkOrder -from sqlalchemy.ext.hybrid import hybrid_property class ScopeEquipmentJob(Base, DefaultMixin): __tablename__ = "oh_ms_scope_equipment_job" assetnum = Column(String, nullable=False) - job_id = Column(UUID(as_uuid=True), ForeignKey( - "oh_ms_job.id", ondelete="cascade")) + job_id = Column(UUID(as_uuid=True), ForeignKey("oh_ms_job.id", ondelete="cascade")) master_equipments = relationship( - "MasterEquipment", lazy="raise", primaryjoin="and_(ScopeEquipmentJob.assetnum == foreign(MasterEquipment.assetnum))", uselist=False) + "MasterEquipment", + lazy="raise", + primaryjoin="and_(ScopeEquipmentJob.assetnum == foreign(MasterEquipment.assetnum))", + uselist=False, + ) + + job = relationship("MasterActivity", lazy="selectin") - job = relationship( - "MasterActivity", lazy="selectin" + overhaul_jobs = relationship( + "OverhaulJob", back_populates="scope_equipment_job", lazy="selectin" ) diff --git a/src/scope_equipment_job/router.py b/src/scope_equipment_job/router.py index 9f65009..f523987 100644 --- a/src/scope_equipment_job/router.py +++ b/src/scope_equipment_job/router.py @@ -1,18 +1,21 @@ - from typing import Dict, List -from fastapi import APIRouter, HTTPException, Query, status -from .service import get_all, delete, create -from .schema import ScopeEquipmentJobCreate, ScopeEquipmentJobPagination +from fastapi import APIRouter, HTTPException, Query, status +from src.database.service import (CommonParameters, DbSession, + search_filter_sort_paginate) from src.models import StandardResponse -from src.database.service import CommonParameters, search_filter_sort_paginate, DbSession + +from .schema import ScopeEquipmentJobCreate, ScopeEquipmentJobPagination +from .service import create, delete, get_all router = APIRouter() @router.get("/{assetnum}", response_model=StandardResponse[ScopeEquipmentJobPagination]) -async def get_scope_equipment_jobs(db_session: DbSession, assetnum, common: CommonParameters): +async def get_scope_equipment_jobs( + db_session: DbSession, assetnum, common: CommonParameters +): """Get all scope activity pagination.""" # return data = await get_all(db_session=db_session, assetnum=assetnum, common=common) @@ -24,7 +27,9 @@ async def get_scope_equipment_jobs(db_session: DbSession, assetnum, common: Comm @router.post("/{assetnum}", response_model=StandardResponse[None]) -async def create_scope_equipment_jobs(db_session: DbSession, assetnum, scope_job_in: ScopeEquipmentJobCreate): +async def create_scope_equipment_jobs( + db_session: DbSession, assetnum, scope_job_in: ScopeEquipmentJobCreate +): """Get all scope activity pagination.""" # return await create(db_session=db_session, assetnum=assetnum, scope_job_in=scope_job_in) diff --git a/src/scope_equipment_job/schema.py b/src/scope_equipment_job/schema.py index 66d7c43..930a511 100644 --- a/src/scope_equipment_job/schema.py +++ b/src/scope_equipment_job/schema.py @@ -1,11 +1,12 @@ - from datetime import datetime from typing import Any, Dict, List, Optional from uuid import UUID -from pydantic import Field, BaseModel +from pydantic import BaseModel, Field + from src.job.schema import ActivityMasterRead from src.models import DefultBase, Pagination +from src.overhaul_scope.schema import ScopeRead class ScopeEquipmentJobBase(DefultBase): @@ -21,9 +22,20 @@ class ScopeEquipmentJobUpdate(ScopeEquipmentJobBase): cost: Optional[str] = Field(0) +class OverhaulActivity(DefultBase): + id: UUID + overhaul_scope: ScopeRead + + +class OverhaulJob(DefultBase): + id: UUID + overhaul_activity: OverhaulActivity + + class ScopeEquipmentJobRead(ScopeEquipmentJobBase): id: UUID job: ActivityMasterRead + overhaul_jobs: List[OverhaulJob] = [] class ScopeEquipmentJobPagination(Pagination): diff --git a/src/scope_equipment_job/service.py b/src/scope_equipment_job/service.py index cd8a4c4..9b21513 100644 --- a/src/scope_equipment_job/service.py +++ b/src/scope_equipment_job/service.py @@ -1,20 +1,20 @@ - - import random -from sqlalchemy import Select, Delete, and_ -from sqlalchemy.orm import selectinload from typing import Optional +from sqlalchemy import Delete, Select, and_ +from sqlalchemy.orm import selectinload + +from src.auth.service import CurrentUser +from src.database.core import DbSession +from src.database.service import CommonParameters, search_filter_sort_paginate from src.scope_equipment.model import MasterEquipment, MasterEquipmentTree from src.scope_equipment.service import get_equipment_level_by_no from .model import ScopeEquipmentJob from .schema import ScopeEquipmentJobCreate -from src.database.core import DbSession -from src.database.service import CommonParameters, search_filter_sort_paginate -from src.auth.service import CurrentUser - +from src.overhaul_job.model import OverhaulJob +from src.overhaul_activity.model import OverhaulActivity # async def get(*, db_session: DbSession, scope_equipment_activity_id: str) -> Optional[ScopeEquipmentActivity]: # """Returns a document based on the given document id.""" @@ -28,8 +28,7 @@ async def get_all(db_session: DbSession, assetnum: Optional[str], common): raise ValueError("assetnum parameter is required") # First get the parent equipment - equipment_stmt = Select(MasterEquipment).where( - MasterEquipment.assetnum == assetnum) + equipment_stmt = Select(MasterEquipment).where(MasterEquipment.assetnum == assetnum) equipment: MasterEquipment = await db_session.scalar(equipment_stmt) if not equipment: @@ -38,9 +37,13 @@ async def get_all(db_session: DbSession, assetnum: Optional[str], common): # Build query for parts stmt = ( Select(ScopeEquipmentJob) - .where( - ScopeEquipmentJob.assetnum == assetnum - ).options(selectinload(ScopeEquipmentJob.job)) + .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) @@ -48,23 +51,23 @@ async def get_all(db_session: DbSession, assetnum: Optional[str], 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 - equipment_stmt = Select(MasterEquipment).where( - MasterEquipment.assetnum == assetnum) + 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}") for job_id in scope_job_in.job_ids: - scope_equipment_job = ScopeEquipmentJob( - assetnum=assetnum, job_id=job_id) + scope_equipment_job = ScopeEquipmentJob(assetnum=assetnum, job_id=job_id) scope_jobs.append(scope_equipment_job) db_session.add_all(scope_jobs) diff --git a/src/scope_equipment_part/model.py b/src/scope_equipment_part/model.py index 0867155..5b5945e 100644 --- a/src/scope_equipment_part/model.py +++ b/src/scope_equipment_part/model.py @@ -1,10 +1,10 @@ +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, Integer, String, ForeignKey from src.database.core import Base from src.models import DefaultMixin, IdentityMixin, TimeStampMixin -from sqlalchemy.orm import relationship from src.workorder.model import MasterWorkOrder -from sqlalchemy.ext.hybrid import hybrid_property class ScopeEquipmentPart(Base, DefaultMixin): @@ -14,6 +14,8 @@ class ScopeEquipmentPart(Base, DefaultMixin): stock = Column(Integer, nullable=False, default=0) master_equipments = relationship( - "MasterEquipment", lazy="raise", primaryjoin="and_(ScopeEquipmentPart.assetnum == foreign(MasterEquipment.assetnum))", uselist=False) - - + "MasterEquipment", + lazy="raise", + primaryjoin="and_(ScopeEquipmentPart.assetnum == foreign(MasterEquipment.assetnum))", + uselist=False, + ) diff --git a/src/scope_equipment_part/router.py b/src/scope_equipment_part/router.py index a07a2cf..fcc5fb2 100644 --- a/src/scope_equipment_part/router.py +++ b/src/scope_equipment_part/router.py @@ -1,13 +1,15 @@ - from typing import Dict, List + from fastapi import APIRouter, HTTPException, Query, status +from src.database.service import (CommonParameters, DbSession, + search_filter_sort_paginate) +from src.models import StandardResponse +from .schema import (ScopeEquipmentActivityCreate, + ScopeEquipmentActivityPagination, + ScopeEquipmentActivityRead, ScopeEquipmentActivityUpdate) from .service import get_all -from .schema import ScopeEquipmentActivityCreate, ScopeEquipmentActivityPagination, ScopeEquipmentActivityRead, ScopeEquipmentActivityUpdate - -from src.models import StandardResponse -from src.database.service import CommonParameters, search_filter_sort_paginate, DbSession router = APIRouter() diff --git a/src/scope_equipment_part/schema.py b/src/scope_equipment_part/schema.py index 79ef18e..a0cde46 100644 --- a/src/scope_equipment_part/schema.py +++ b/src/scope_equipment_part/schema.py @@ -1,9 +1,9 @@ - from datetime import datetime from typing import Any, Dict, List, Optional from uuid import UUID -from pydantic import Field, BaseModel +from pydantic import BaseModel, Field + from src.models import DefultBase, Pagination diff --git a/src/scope_equipment_part/service.py b/src/scope_equipment_part/service.py index 92a6343..45c395e 100644 --- a/src/scope_equipment_part/service.py +++ b/src/scope_equipment_part/service.py @@ -1,26 +1,24 @@ - - import random -from sqlalchemy import Select, Delete, and_ -from sqlalchemy.orm import selectinload from typing import Optional +from sqlalchemy import Delete, Select, and_ +from sqlalchemy.orm import selectinload + +from src.auth.service import CurrentUser +from src.database.core import DbSession +from src.database.service import CommonParameters, search_filter_sort_paginate from src.scope_equipment.model import MasterEquipment, MasterEquipmentTree from src.scope_equipment.service import get_equipment_level_by_no from .model import ScopeEquipmentPart from .schema import ScopeEquipmentActivityCreate, ScopeEquipmentActivityUpdate -from src.database.core import DbSession -from src.database.service import CommonParameters, search_filter_sort_paginate -from src.auth.service import CurrentUser - - # 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 + def create_dummy_parts(assetnum: str, count: int = 5): """ Create a list of dummy ScopeEquipmentPart objects with random stock values. @@ -37,17 +35,14 @@ def create_dummy_parts(assetnum: str, count: int = 5): # Generate a unique part asset number part_assetnum = f"{assetnum}_PART_{i}" stock = random.randint(1, 100) # Random stock value between 1 and 100 - parts.append({ - "assetnum": part_assetnum, - "stock": stock - }) + parts.append({"assetnum": part_assetnum, "stock": stock}) return parts async def get_all(db_session: DbSession, assetnum: Optional[str]): # Example usage dummy_parts = create_dummy_parts(assetnum, count=10) - + # if not assetnum: # raise ValueError("assetnum parameter is required") diff --git a/src/utils.py b/src/utils.py index d6a7012..12e7023 100644 --- a/src/utils.py +++ b/src/utils.py @@ -1,10 +1,9 @@ - - -from datetime import datetime, timedelta, timezone import re +from datetime import datetime, timedelta, timezone from typing import Optional -from dateutil.relativedelta import relativedelta + import pytz +from dateutil.relativedelta import relativedelta from src.config import TIMEZONE @@ -61,7 +60,11 @@ def parse_date_string(date_str: str) -> Optional[datetime]: # Parse the date and set it to start of day in UTC dt = datetime.strptime(date_str, fmt) dt = dt.replace( - hour=0, minute=0, second=0, microsecond=0, tzinfo=timezone.tzname("Asia/Jakarta") + hour=0, + minute=0, + second=0, + microsecond=0, + tzinfo=timezone.tzname("Asia/Jakarta"), ) return dt except ValueError: diff --git a/src/workorder/model.py b/src/workorder/model.py index 148add9..c346d37 100644 --- a/src/workorder/model.py +++ b/src/workorder/model.py @@ -1,12 +1,8 @@ +from sqlalchemy import UUID, Column, Float, ForeignKey, Integer, String +from sqlalchemy.orm import relationship - -from sqlalchemy import UUID, Column, Float, Integer, String, ForeignKey from src.database.core import Base from src.models import DefaultMixin, IdentityMixin, TimeStampMixin -from sqlalchemy.orm import relationship - - -from src.models import DefaultMixin class MasterWorkOrder(Base, DefaultMixin): @@ -17,5 +13,8 @@ class MasterWorkOrder(Base, DefaultMixin): workgroup = Column(String, nullable=True) total_cost_max = Column(Float, nullable=True) - - scope_equipments = relationship("ScopeEquipment", lazy="raise", primaryjoin="and_(MasterWorkOrder.assetnum == foreign(ScopeEquipment.assetnum))") + scope_equipments = relationship( + "ScopeEquipment", + lazy="raise", + primaryjoin="and_(MasterWorkOrder.assetnum == foreign(ScopeEquipment.assetnum))", + ) diff --git a/tests/conftest.py b/tests/conftest.py index 5ce6ed2..a678ff3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,13 +1,12 @@ import asyncio from typing import AsyncGenerator, Generator + import pytest from httpx import AsyncClient from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine from sqlalchemy.orm import sessionmaker from sqlalchemy.pool import StaticPool - -import pytest -from sqlalchemy_utils import drop_database, database_exists +from sqlalchemy_utils import database_exists, drop_database from starlette.config import environ from starlette.testclient import TestClient @@ -66,4 +65,4 @@ async def setup_db() -> AsyncGenerator[None, None]: @pytest.fixture async def client() -> AsyncGenerator[AsyncClient, None]: async with AsyncClient(app=app, base_url="http://test") as client: - yield client \ No newline at end of file + yield client diff --git a/tests/factories.py b/tests/factories.py index 52ccd3d..8554ac0 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -1,22 +1,18 @@ import uuid from datetime import datetime -from factory import ( - LazyAttribute, - LazyFunction, - Sequence, - SubFactory, - post_generation, - SelfAttribute, -) +from factory import (LazyAttribute, LazyFunction, SelfAttribute, Sequence, + SubFactory, post_generation) from factory.alchemy import SQLAlchemyModelFactory from factory.fuzzy import FuzzyChoice, FuzzyDateTime, FuzzyInteger, FuzzyText from faker import Faker from faker.providers import misc + +from .database import Session + # from pytz import UTC -from .database import Session fake = Faker() fake.add_provider(misc)