clean docstring
parent
e67feed201
commit
2ba2c5750a
@ -1,7 +1,5 @@
|
|||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
from src.config import HOST, PORT, ENV
|
from src.config import HOST, PORT, ENV
|
||||||
|
if __name__ == '__main__':
|
||||||
if __name__ == "__main__":
|
is_dev = str(ENV).lower() == 'development'
|
||||||
is_dev = str(ENV).lower() == "development"
|
uvicorn.run('src.main:app', host=HOST, port=PORT, reload=is_dev)
|
||||||
uvicorn.run("src.main:app", host=HOST, port=PORT, reload=is_dev)
|
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
|
||||||
@ -1,10 +1,7 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
class UserBase(BaseModel):
|
class UserBase(BaseModel):
|
||||||
# Some users from auth service may not have a display name.
|
|
||||||
name: Optional[str] = None
|
name: Optional[str] = None
|
||||||
role: str
|
role: str
|
||||||
user_id: str
|
user_id: str
|
||||||
|
|||||||
@ -1,253 +1,137 @@
|
|||||||
# app/auth/auth_bearer.py
|
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from fastapi import Depends, HTTPException, Request
|
from fastapi import Depends, HTTPException, Request
|
||||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||||
|
|
||||||
import src.config as config
|
import src.config as config
|
||||||
|
|
||||||
from .model import UserBase
|
from .model import UserBase
|
||||||
from .util import extract_template
|
from .util import extract_template
|
||||||
|
|
||||||
class JWTBearer(HTTPBearer):
|
class JWTBearer(HTTPBearer):
|
||||||
def __init__(self, auto_error: bool = True):
|
|
||||||
# Always pass auto_error=False so that HTTPBearer returns None (instead of
|
def __init__(self, auto_error: bool=True):
|
||||||
# raising HTTP 403) when the Authorization header is absent. We raise 401
|
|
||||||
# ourselves in the else-branch below, which is the correct status for a
|
|
||||||
# missing / unauthenticated credential.
|
|
||||||
super(JWTBearer, self).__init__(auto_error=False)
|
super(JWTBearer, self).__init__(auto_error=False)
|
||||||
|
|
||||||
async def __call__(self, request: Request):
|
async def __call__(self, request: Request):
|
||||||
credentials: HTTPAuthorizationCredentials = await super(
|
credentials: HTTPAuthorizationCredentials = await super(JWTBearer, self).__call__(request)
|
||||||
JWTBearer, self
|
|
||||||
).__call__(request)
|
|
||||||
if credentials:
|
if credentials:
|
||||||
if not credentials.scheme == "Bearer":
|
if not credentials.scheme == 'Bearer':
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=401, detail='Invalid authentication scheme.')
|
||||||
status_code=401, detail="Invalid authentication scheme."
|
|
||||||
)
|
|
||||||
method = request.method
|
method = request.method
|
||||||
|
if method == 'OPTIONS':
|
||||||
if method == "OPTIONS":
|
|
||||||
return
|
return
|
||||||
|
|
||||||
path = extract_template(request.url.path, request.path_params)
|
path = extract_template(request.url.path, request.path_params)
|
||||||
|
endpoint = f'/optimumoh{path}'
|
||||||
endpoint = f"/optimumoh{path}"
|
|
||||||
|
|
||||||
user_info, message = self.verify_jwt(credentials.credentials, method, endpoint)
|
user_info, message = self.verify_jwt(credentials.credentials, method, endpoint)
|
||||||
|
|
||||||
if not user_info:
|
if not user_info:
|
||||||
if isinstance(message, dict):
|
if isinstance(message, dict):
|
||||||
message = message.get("message") or message.get("detail") or "Invalid token or expired token."
|
message = message.get('message') or message.get('detail') or 'Invalid token or expired token.'
|
||||||
else:
|
else:
|
||||||
message = str(message) if message else "Invalid token or expired token."
|
message = str(message) if message else 'Invalid token or expired token.'
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=401, detail=message)
|
||||||
status_code=401, detail=message
|
|
||||||
)
|
|
||||||
|
|
||||||
request.state.user = message
|
request.state.user = message
|
||||||
|
|
||||||
from src.context import set_user_id, set_username, set_role
|
from src.context import set_user_id, set_username, set_role
|
||||||
if hasattr(message, "user_id"):
|
if hasattr(message, 'user_id'):
|
||||||
set_user_id(str(message.user_id))
|
set_user_id(str(message.user_id))
|
||||||
username = getattr(message, "username", None) or getattr(message, "name", None)
|
username = getattr(message, 'username', None) or getattr(message, 'name', None)
|
||||||
if username:
|
if username:
|
||||||
set_username(username)
|
set_username(username)
|
||||||
if hasattr(message, "role"):
|
if hasattr(message, 'role'):
|
||||||
set_role(message.role)
|
set_role(message.role)
|
||||||
|
|
||||||
return message
|
return message
|
||||||
else:
|
else:
|
||||||
raise HTTPException(status_code=401, detail="Invalid authorization code.")
|
raise HTTPException(status_code=401, detail='Invalid authorization code.')
|
||||||
|
|
||||||
def verify_jwt(self, jwtoken: str, method: str, endpoint: str):
|
def verify_jwt(self, jwtoken: str, method: str, endpoint: str):
|
||||||
try:
|
try:
|
||||||
url_to_verify = f"{config.AUTH_SERVICE_API}/verify-token"
|
url_to_verify = f'{config.AUTH_SERVICE_API}/verify-token'
|
||||||
response = requests.get(
|
response = requests.get(url_to_verify, headers={'Authorization': f'Bearer {jwtoken}'})
|
||||||
url_to_verify,
|
|
||||||
headers={"Authorization": f"Bearer {jwtoken}"},
|
|
||||||
)
|
|
||||||
|
|
||||||
if not response.ok:
|
if not response.ok:
|
||||||
return False, response.json()
|
return (False, response.json())
|
||||||
|
|
||||||
user_data = response.json()
|
user_data = response.json()
|
||||||
return True, UserBase(**user_data["data"])
|
return (True, UserBase(**user_data['data']))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Token verification error: {str(e)}")
|
print(f'Token verification error: {str(e)}')
|
||||||
return False, str(e)
|
return (False, str(e))
|
||||||
|
|
||||||
|
|
||||||
# Create dependency to get current user from request state
|
|
||||||
async def get_current_user(request: Request) -> UserBase:
|
async def get_current_user(request: Request) -> UserBase:
|
||||||
return request.state.user
|
return request.state.user
|
||||||
|
|
||||||
async def get_token(request: Request):
|
async def get_token(request: Request):
|
||||||
token = request.headers.get("Authorization")
|
token = request.headers.get('Authorization')
|
||||||
if token:
|
if token:
|
||||||
return token.replace("Bearer ", "") # Menghapus prefix "Bearer "
|
return token.replace('Bearer ', '')
|
||||||
else:
|
else:
|
||||||
return request.cookies.get("access_token") # Fallback ke cookie
|
return request.cookies.get('access_token')
|
||||||
|
return ''
|
||||||
return "" # Mengembalikan token atau None jika tidak ada
|
|
||||||
|
|
||||||
|
|
||||||
async def internal_key(request: Request):
|
async def internal_key(request: Request):
|
||||||
token = request.headers.get("Authorization")
|
token = request.headers.get('Authorization')
|
||||||
|
if not token:
|
||||||
if not token:
|
api_key = request.headers.get('X-Internal-Key')
|
||||||
api_key = request.headers.get("X-Internal-Key")
|
|
||||||
|
|
||||||
if api_key != config.API_KEY:
|
if api_key != config.API_KEY:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=401, detail='Invalid Key.')
|
||||||
status_code=401, detail="Invalid Key."
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
headers = {
|
headers = {'Content-Type': 'application/json'}
|
||||||
'Content-Type': 'application/json'
|
response = requests.post(f'{config.AUTH_SERVICE_API}/sign-in', headers=headers, data=json.dumps({'username': 'ohuser', 'password': '123456789'}))
|
||||||
}
|
|
||||||
|
|
||||||
response = requests.post(
|
|
||||||
f"{config.AUTH_SERVICE_API}/sign-in",
|
|
||||||
headers=headers,
|
|
||||||
data=json.dumps({
|
|
||||||
"username": "ohuser",
|
|
||||||
"password": "123456789"
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
if not response.ok:
|
if not response.ok:
|
||||||
print(str(response.json()))
|
print(str(response.json()))
|
||||||
raise Exception("error auth")
|
raise Exception('error auth')
|
||||||
|
|
||||||
user_data = response.json()
|
user_data = response.json()
|
||||||
return user_data['data']['access_token']
|
return user_data['data']['access_token']
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise Exception(str(e))
|
raise Exception(str(e))
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
verify_url = f"{config.AUTH_SERVICE_API}/verify-token"
|
verify_url = f'{config.AUTH_SERVICE_API}/verify-token'
|
||||||
response = requests.get(
|
response = requests.get(verify_url, headers={'Authorization': f'{token}'})
|
||||||
verify_url,
|
|
||||||
headers={"Authorization": f"{token}"},
|
|
||||||
)
|
|
||||||
|
|
||||||
if not response.ok:
|
if not response.ok:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=401, detail='Invalid token.')
|
||||||
status_code=401, detail="Invalid token."
|
return token.split(' ')[1]
|
||||||
)
|
|
||||||
|
|
||||||
return token.split(" ")[1]
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Token verification error: {str(e)}")
|
print(f'Token verification error: {str(e)}')
|
||||||
return False, str(e)
|
return (False, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
import src.config as config
|
import src.config as config
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
AUTH_NOTIFY_ENDPOINT = f'{config.AUTH_SERVICE_API}/admin/notify-limit'
|
||||||
|
|
||||||
|
async def notify_admin_on_rate_limit(endpoint_name: str, ip_address: str, request: Request, method: str='POST', cooldown: int=900, timeout: int=5) -> Dict[str, Any]:
|
||||||
AUTH_NOTIFY_ENDPOINT = f"{config.AUTH_SERVICE_API}/admin/notify-limit"
|
payload = {'endpoint_name': f'oh/{endpoint_name}'.replace('//', ''), 'ip_address': ip_address, 'method': method, 'cooldown': cooldown}
|
||||||
|
token = request.headers.get('Authorization')
|
||||||
|
headers = {'Authorization': token} if token else {}
|
||||||
async def notify_admin_on_rate_limit(
|
|
||||||
endpoint_name: str,
|
|
||||||
ip_address: str,
|
|
||||||
request: Request,
|
|
||||||
method: str = "POST",
|
|
||||||
cooldown: int = 900,
|
|
||||||
timeout: int = 5
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Kirim notifikasi ke admin via be-auth service ketika rate limit terlampaui.
|
|
||||||
|
|
||||||
Async version - gunakan di async context.
|
|
||||||
"""
|
|
||||||
payload = {
|
|
||||||
"endpoint_name": f"oh/{endpoint_name}".replace("//", ""),
|
|
||||||
"ip_address": ip_address,
|
|
||||||
"method": method,
|
|
||||||
"cooldown": cooldown,
|
|
||||||
}
|
|
||||||
token = request.headers.get("Authorization")
|
|
||||||
headers = {"Authorization": token} if token else {}
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = await asyncio.to_thread(
|
response = await asyncio.to_thread(requests.post, AUTH_NOTIFY_ENDPOINT, json=payload, headers=headers, timeout=timeout)
|
||||||
requests.post, AUTH_NOTIFY_ENDPOINT,
|
|
||||||
json=payload, headers=headers, timeout=timeout
|
|
||||||
)
|
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
result = response.json()
|
result = response.json()
|
||||||
log.info(f"Notifikasi admin sent | Endpoint: {endpoint_name}")
|
log.info(f'Notifikasi admin sent | Endpoint: {endpoint_name}')
|
||||||
return result
|
return result
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(f"Error notifying admin: {str(e)}")
|
log.error(f'Error notifying admin: {str(e)}')
|
||||||
return {"status": False, "message": str(e), "data": payload}
|
return {'status': False, 'message': str(e), 'data': payload}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def notify_admin_on_rate_limit_sync(
|
|
||||||
endpoint_name: str,
|
|
||||||
ip_address: str,
|
|
||||||
request: Request,
|
|
||||||
method: str = "POST",
|
|
||||||
cooldown: int = 900,
|
|
||||||
timeout: int = 5
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Kirim notifikasi ke admin via be-auth service.
|
|
||||||
|
|
||||||
Sync version - gunakan di exception handler atau sync context.
|
|
||||||
RECOMMENDED untuk use case ini.
|
|
||||||
"""
|
|
||||||
payload = {
|
|
||||||
"endpoint_name": f"oh/{endpoint_name}".replace("//", "/"),
|
|
||||||
"ip_address": ip_address,
|
|
||||||
"method": method,
|
|
||||||
"cooldown": cooldown,
|
|
||||||
}
|
|
||||||
token = request.headers.get("Authorization")
|
|
||||||
headers = {"Authorization": token} if token else {}
|
|
||||||
|
|
||||||
|
def notify_admin_on_rate_limit_sync(endpoint_name: str, ip_address: str, request: Request, method: str='POST', cooldown: int=900, timeout: int=5) -> Dict[str, Any]:
|
||||||
|
payload = {'endpoint_name': f'oh/{endpoint_name}'.replace('//', '/'), 'ip_address': ip_address, 'method': method, 'cooldown': cooldown}
|
||||||
|
token = request.headers.get('Authorization')
|
||||||
|
headers = {'Authorization': token} if token else {}
|
||||||
try:
|
try:
|
||||||
response = requests.post(AUTH_NOTIFY_ENDPOINT, json=payload, headers=headers, timeout=timeout)
|
response = requests.post(AUTH_NOTIFY_ENDPOINT, json=payload, headers=headers, timeout=timeout)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
result = response.json()
|
result = response.json()
|
||||||
log.info(f"Notifikasi admin sent | Endpoint: {endpoint_name}")
|
log.info(f'Notifikasi admin sent | Endpoint: {endpoint_name}')
|
||||||
return result
|
return result
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(f"Error notifying admin: {str(e)}")
|
log.error(f'Error notifying admin: {str(e)}')
|
||||||
return {"status": False, "message": str(e), "data": payload}
|
return {'status': False, 'message': str(e), 'data': payload}
|
||||||
|
|
||||||
async def admin_required(request: Request):
|
async def admin_required(request: Request):
|
||||||
user = await get_current_user(request)
|
user = await get_current_user(request)
|
||||||
if user.role != "Admin" or user.role != "Superadmin":
|
if user.role != 'Admin' or user.role != 'Superadmin':
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=403, detail='Invalid authorization code.')
|
||||||
status_code=403, detail="Invalid authorization code."
|
|
||||||
)
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
Admin = Annotated[UserBase, Depends(admin_required)]
|
Admin = Annotated[UserBase, Depends(admin_required)]
|
||||||
CurrentUser = Annotated[UserBase, Depends(get_current_user)]
|
CurrentUser = Annotated[UserBase, Depends(get_current_user)]
|
||||||
Token = Annotated[str, Depends(get_token)]
|
Token = Annotated[str, Depends(get_token)]
|
||||||
InternalKey = Annotated[str, Depends(internal_key)]
|
InternalKey = Annotated[str, Depends(internal_key)]
|
||||||
|
|||||||
@ -1,9 +1,6 @@
|
|||||||
def extract_template(path_string, value_dict):
|
def extract_template(path_string, value_dict):
|
||||||
template = path_string
|
template = path_string
|
||||||
|
|
||||||
# Replace each value in the dict with its corresponding key placeholder
|
|
||||||
for key, value in value_dict.items():
|
for key, value in value_dict.items():
|
||||||
if str(value) in template:
|
if str(value) in template:
|
||||||
template = template.replace(str(value), f'<{key}>')
|
template = template.replace(str(value), f'<{key}>')
|
||||||
|
|
||||||
return template
|
return template
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
|
||||||
@ -1,46 +1,21 @@
|
|||||||
from typing import Annotated, Dict
|
from typing import Annotated, Dict
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from fastapi.params import Query
|
from fastapi.params import Query
|
||||||
|
|
||||||
from src.auth.service import Token
|
from src.auth.service import Token
|
||||||
from src.calculation_budget_constrains.schema import BudgetContraintQuery
|
from src.calculation_budget_constrains.schema import BudgetContraintQuery
|
||||||
from src.calculation_target_reliability.service import get_simulation_results
|
from src.calculation_target_reliability.service import get_simulation_results
|
||||||
from src.config import TC_RBD_ID
|
from src.config import TC_RBD_ID
|
||||||
from src.database.core import CollectorDbSession, DbSession
|
from src.database.core import CollectorDbSession, DbSession
|
||||||
from src.models import StandardResponse
|
from src.models import StandardResponse
|
||||||
|
|
||||||
from .service import get_all_budget_constrains
|
from .service import get_all_budget_constrains
|
||||||
from src.auth.access_control import required_permission, require_any_role, OHPermission, ALLOWED_ROLES
|
from src.auth.access_control import required_permission, require_any_role, OHPermission, ALLOWED_ROLES
|
||||||
from src.overhaul_scope.model import OverhaulScope
|
from src.overhaul_scope.model import OverhaulScope
|
||||||
from fastapi import Depends
|
from fastapi import Depends
|
||||||
|
|
||||||
router = APIRouter(dependencies=[Depends(require_any_role(*ALLOWED_ROLES))])
|
router = APIRouter(dependencies=[Depends(require_any_role(*ALLOWED_ROLES))])
|
||||||
|
|
||||||
|
@router.get('/{session_id}', response_model=StandardResponse[Dict], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
||||||
@router.get("/{session_id}", response_model=StandardResponse[Dict], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
async def get_target_reliability(db_session: DbSession, token: Token, session_id: str, collector_db: CollectorDbSession, params: Annotated[BudgetContraintQuery, Query()]):
|
||||||
async def get_target_reliability(
|
|
||||||
db_session: DbSession,
|
|
||||||
token: Token,
|
|
||||||
session_id: str,
|
|
||||||
collector_db: CollectorDbSession,
|
|
||||||
params: Annotated[BudgetContraintQuery, Query()],
|
|
||||||
):
|
|
||||||
"""Get all scope pagination."""
|
|
||||||
cost_threshold = params.cost_threshold
|
cost_threshold = params.cost_threshold
|
||||||
results = await get_simulation_results(
|
results = await get_simulation_results(simulation_id=TC_RBD_ID, token=token)
|
||||||
simulation_id = TC_RBD_ID,
|
results, consequence = await get_all_budget_constrains(db_session=db_session, session_id=session_id, cost_threshold=cost_threshold, simulation_result=results, collector_db=collector_db)
|
||||||
token=token
|
return StandardResponse(data={'results': results, 'consequence': consequence}, message='Data retrieved successfully')
|
||||||
)
|
|
||||||
|
|
||||||
results, consequence = await get_all_budget_constrains(
|
|
||||||
db_session=db_session, session_id=session_id, cost_threshold=cost_threshold, simulation_result=results, collector_db=collector_db
|
|
||||||
)
|
|
||||||
|
|
||||||
return StandardResponse(
|
|
||||||
data={
|
|
||||||
"results": results,
|
|
||||||
"consequence": consequence
|
|
||||||
},
|
|
||||||
message="Data retrieved successfully",
|
|
||||||
)
|
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
|
||||||
@ -0,0 +1 @@
|
|||||||
|
|
||||||
@ -1,191 +1,61 @@
|
|||||||
from typing import Annotated, List, Optional, Union
|
from typing import Annotated, List, Optional, Union
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from fastapi.params import Query
|
from fastapi.params import Query
|
||||||
|
|
||||||
from src.auth.service import CurrentUser, InternalKey, Token
|
from src.auth.service import CurrentUser, InternalKey, Token
|
||||||
from src.config import DEFAULT_TC_ID
|
from src.config import DEFAULT_TC_ID
|
||||||
from src.database.core import DbSession
|
from src.database.core import DbSession
|
||||||
from src.models import StandardResponse
|
from src.models import StandardResponse
|
||||||
|
from .flows import create_calculation, get_create_calculation_parameters, get_or_create_scope_equipment_calculation
|
||||||
from .flows import (create_calculation, get_create_calculation_parameters,
|
from .schema import CalculationResultsRead, CalculationSelectedEquipmentUpdate, CalculationTimeConstrainsCreate, CalculationTimeConstrainsParametersCreate, CalculationTimeConstrainsParametersRead, CalculationTimeConstrainsParametersRetrive, CalculationTimeConstrainsRead, CreateCalculationQuery, EquipmentResult, CalculationTimeConstrainsReadNoResult
|
||||||
get_or_create_scope_equipment_calculation)
|
from .service import bulk_update_equipment, get_calculation_result, get_calculation_result_by_day, get_calculation_by_assetnum, get_all_calculations
|
||||||
from .schema import (CalculationResultsRead,
|
|
||||||
CalculationSelectedEquipmentUpdate,
|
|
||||||
CalculationTimeConstrainsCreate,
|
|
||||||
CalculationTimeConstrainsParametersCreate,
|
|
||||||
CalculationTimeConstrainsParametersRead,
|
|
||||||
CalculationTimeConstrainsParametersRetrive,
|
|
||||||
CalculationTimeConstrainsRead, CreateCalculationQuery, EquipmentResult, CalculationTimeConstrainsReadNoResult)
|
|
||||||
from .service import (bulk_update_equipment, get_calculation_result,
|
|
||||||
get_calculation_result_by_day, get_calculation_by_assetnum, get_all_calculations)
|
|
||||||
from src.database.core import CollectorDbSession
|
from src.database.core import CollectorDbSession
|
||||||
from src.auth.access_control import required_permission, OHPermission
|
from src.auth.access_control import required_permission, OHPermission
|
||||||
from src.overhaul_scope.model import OverhaulScope
|
from src.overhaul_scope.model import OverhaulScope
|
||||||
from fastapi import Depends
|
from fastapi import Depends
|
||||||
from src.csrf_protect import csrf_protect
|
from src.csrf_protect import csrf_protect
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
get_calculation = APIRouter()
|
get_calculation = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post('', response_model=StandardResponse[Union[dict, CalculationTimeConstrainsRead]], dependencies=[Depends(required_permission(OHPermission.CREATE, OverhaulScope))])
|
||||||
"", response_model=StandardResponse[Union[dict, CalculationTimeConstrainsRead]],
|
async def create_calculation_time_constrains(token: Token, db_session: DbSession, collector_db_session: CollectorDbSession, current_user: CurrentUser, calculation_time_constrains_in: CalculationTimeConstrainsParametersCreate, params: Annotated[CreateCalculationQuery, Query()]):
|
||||||
dependencies=[Depends(required_permission(OHPermission.CREATE, OverhaulScope))]
|
|
||||||
)
|
|
||||||
async def create_calculation_time_constrains(
|
|
||||||
token: Token,
|
|
||||||
db_session: DbSession,
|
|
||||||
collector_db_session: CollectorDbSession,
|
|
||||||
current_user: CurrentUser,
|
|
||||||
calculation_time_constrains_in: CalculationTimeConstrainsParametersCreate,
|
|
||||||
params: Annotated[CreateCalculationQuery, Query()],
|
|
||||||
):
|
|
||||||
"""Save calculation time constrains Here"""
|
|
||||||
scope_calculation_id = params.scope_calculation_id
|
scope_calculation_id = params.scope_calculation_id
|
||||||
simulation_id = params.simulation_id
|
simulation_id = params.simulation_id
|
||||||
|
|
||||||
if scope_calculation_id:
|
if scope_calculation_id:
|
||||||
results = await get_or_create_scope_equipment_calculation(
|
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)
|
||||||
db_session=db_session,
|
|
||||||
scope_calculation_id=scope_calculation_id,
|
|
||||||
calculation_time_constrains_in=calculation_time_constrains_in,
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
results = await create_calculation(
|
results = await create_calculation(token=token, db_session=db_session, collector_db_session=collector_db_session, calculation_time_constrains_in=calculation_time_constrains_in, created_by=current_user.name, simulation_id=simulation_id)
|
||||||
token=token,
|
return StandardResponse(data=results, message='Data created successfully')
|
||||||
db_session=db_session,
|
|
||||||
collector_db_session=collector_db_session,
|
|
||||||
calculation_time_constrains_in=calculation_time_constrains_in,
|
|
||||||
created_by=current_user.name,
|
|
||||||
simulation_id=simulation_id
|
|
||||||
)
|
|
||||||
|
|
||||||
return StandardResponse(data=results, message="Data created successfully")
|
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"", response_model=StandardResponse[List[CalculationTimeConstrainsReadNoResult]],
|
|
||||||
dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))]
|
|
||||||
)
|
|
||||||
async def get_all_simulation_calculations(
|
|
||||||
db_session: DbSession,
|
|
||||||
token: Token,
|
|
||||||
current_user: CurrentUser,
|
|
||||||
):
|
|
||||||
"""Get all calculation time constrains Here"""
|
|
||||||
|
|
||||||
calculations = await get_all_calculations(
|
|
||||||
db_session=db_session,
|
|
||||||
)
|
|
||||||
|
|
||||||
return StandardResponse(
|
|
||||||
data=calculations,
|
|
||||||
message="Data retrieved successfully",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('', response_model=StandardResponse[List[CalculationTimeConstrainsReadNoResult]], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
||||||
|
async def get_all_simulation_calculations(db_session: DbSession, token: Token, current_user: CurrentUser):
|
||||||
|
calculations = await get_all_calculations(db_session=db_session)
|
||||||
|
return StandardResponse(data=calculations, message='Data retrieved successfully')
|
||||||
|
|
||||||
@router.get(
|
@router.get('/parameters', response_model=StandardResponse[Union[CalculationTimeConstrainsParametersRetrive, CalculationTimeConstrainsParametersRead]], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
||||||
"/parameters",
|
async def get_calculation_parameters(db_session: DbSession, calculation_id: Optional[str]=Query(default=None)):
|
||||||
response_model=StandardResponse[
|
parameters = await get_create_calculation_parameters(db_session=db_session, calculation_id=calculation_id)
|
||||||
Union[
|
return StandardResponse(data=parameters, message='Data retrieved successfully')
|
||||||
CalculationTimeConstrainsParametersRetrive,
|
|
||||||
CalculationTimeConstrainsParametersRead,
|
|
||||||
]
|
|
||||||
],
|
|
||||||
dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))]
|
|
||||||
)
|
|
||||||
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(
|
@get_calculation.get('/{calculation_id}', response_model=StandardResponse[CalculationTimeConstrainsRead], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
||||||
db_session=db_session, calculation_id=calculation_id
|
async def get_calculation_results(db_session: DbSession, calculation_id, token: InternalKey, include_risk_cost: int=Query(1, alias='risk_cost')):
|
||||||
)
|
|
||||||
|
|
||||||
return StandardResponse(
|
|
||||||
data=parameters,
|
|
||||||
message="Data retrieved successfully",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@get_calculation.get(
|
|
||||||
"/{calculation_id}", response_model=StandardResponse[CalculationTimeConstrainsRead],
|
|
||||||
dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))]
|
|
||||||
)
|
|
||||||
async def get_calculation_results(db_session: DbSession, calculation_id, token:InternalKey, include_risk_cost:int = Query(1, alias="risk_cost")):
|
|
||||||
if calculation_id == 'default':
|
if calculation_id == 'default':
|
||||||
calculation_id = DEFAULT_TC_ID
|
calculation_id = DEFAULT_TC_ID
|
||||||
|
results = await get_calculation_result(db_session=db_session, calculation_id=calculation_id, token=token, include_risk_cost=include_risk_cost)
|
||||||
results = await get_calculation_result(
|
return StandardResponse(data=results, message='Data retrieved successfully')
|
||||||
db_session=db_session, calculation_id=calculation_id, token=token, include_risk_cost=include_risk_cost
|
|
||||||
)
|
|
||||||
|
|
||||||
# requests.post(f"{config.AUTH_SERVICE_API}/sign-out", headers={
|
|
||||||
# "Authorization": f"Bearer {token}"
|
|
||||||
|
|
||||||
return StandardResponse(
|
@router.get('/{calculation_id}/{assetnum}', response_model=StandardResponse[EquipmentResult], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
||||||
data=results,
|
|
||||||
message="Data retrieved successfully",
|
|
||||||
)
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/{calculation_id}/{assetnum}", response_model=StandardResponse[EquipmentResult],
|
|
||||||
dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))]
|
|
||||||
)
|
|
||||||
async def get_calculation_per_equipment(db_session: DbSession, calculation_id, assetnum):
|
async def get_calculation_per_equipment(db_session: DbSession, calculation_id, assetnum):
|
||||||
|
results = await get_calculation_by_assetnum(db_session=db_session, assetnum=assetnum, calculation_id=calculation_id)
|
||||||
|
return StandardResponse(data=results, message='Data retrieved successfully')
|
||||||
|
|
||||||
results = await get_calculation_by_assetnum(
|
@router.post('/{calculation_id}/simulation', response_model=StandardResponse[CalculationResultsRead], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
||||||
db_session=db_session, assetnum=assetnum, calculation_id=calculation_id
|
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=results,
|
|
||||||
message="Data retrieved successfully",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
@router.post('/update/{calculation_id}', response_model=StandardResponse[List[str]], dependencies=[Depends(required_permission(OHPermission.EDIT, OverhaulScope)), Depends(csrf_protect)])
|
||||||
@router.post(
|
async def update_selected_equipment(db_session: DbSession, calculation_id, calculation_time_constrains_in: List[CalculationSelectedEquipmentUpdate]):
|
||||||
"/{calculation_id}/simulation",
|
|
||||||
response_model=StandardResponse[CalculationResultsRead],
|
|
||||||
dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))]
|
|
||||||
)
|
|
||||||
async def get_simulation_result(
|
|
||||||
db_session: DbSession,
|
|
||||||
calculation_id,
|
|
||||||
calculation_simuation_in: CalculationTimeConstrainsCreate,
|
|
||||||
):
|
|
||||||
simulation_result = await get_calculation_result_by_day(
|
|
||||||
db_session=db_session,
|
|
||||||
calculation_id=calculation_id,
|
|
||||||
simulation_day=calculation_simuation_in.intervalDays,
|
|
||||||
)
|
|
||||||
|
|
||||||
return StandardResponse(
|
|
||||||
data=simulation_result, message="Data retrieved successfully"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/update/{calculation_id}", response_model=StandardResponse[List[str]], dependencies=[Depends(required_permission(OHPermission.EDIT, OverhaulScope)), Depends(csrf_protect)])
|
|
||||||
async def update_selected_equipment(
|
|
||||||
db_session: DbSession,
|
|
||||||
calculation_id,
|
|
||||||
calculation_time_constrains_in: List[CalculationSelectedEquipmentUpdate],
|
|
||||||
):
|
|
||||||
if calculation_id == 'default':
|
if calculation_id == 'default':
|
||||||
calculation_id = "3b9a73a2-bde6-418c-9e2f-19046f501a05"
|
calculation_id = '3b9a73a2-bde6-418c-9e2f-19046f501a05'
|
||||||
|
results = await bulk_update_equipment(db=db_session, selected_equipments=calculation_time_constrains_in, calculation_data_id=calculation_id)
|
||||||
results = await bulk_update_equipment(
|
return StandardResponse(data=results, message='Data retrieved successfully')
|
||||||
db=db_session,
|
|
||||||
selected_equipments=calculation_time_constrains_in,
|
|
||||||
calculation_data_id=calculation_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
return StandardResponse(
|
|
||||||
data=results,
|
|
||||||
message="Data retrieved successfully",
|
|
||||||
)
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,295 +1,135 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from src.config import RBD_SERVICE_API
|
from src.config import RBD_SERVICE_API
|
||||||
|
|
||||||
def get_months_between(start_date: datetime.datetime, end_date: datetime.datetime) -> int:
|
def get_months_between(start_date: datetime.datetime, end_date: datetime.datetime) -> int:
|
||||||
"""
|
|
||||||
Calculate number of months between two dates.
|
|
||||||
"""
|
|
||||||
months = (end_date.year - start_date.year) * 12 + (end_date.month - start_date.month)
|
months = (end_date.year - start_date.year) * 12 + (end_date.month - start_date.month)
|
||||||
# Add 1 to include both start and end months
|
|
||||||
return months
|
return months
|
||||||
|
|
||||||
|
|
||||||
def create_time_series_data(chart_data, max_hours=None):
|
def create_time_series_data(chart_data, max_hours=None):
|
||||||
# Filter out ON_OH
|
filtered_data = [d for d in chart_data if d['currentEvent'] != 'ON_OH']
|
||||||
filtered_data = [d for d in chart_data if d["currentEvent"] != "ON_OH"]
|
sorted_data = sorted(filtered_data, key=lambda x: x['cumulativeTime'])
|
||||||
sorted_data = sorted(filtered_data, key=lambda x: x["cumulativeTime"])
|
|
||||||
|
|
||||||
if not sorted_data:
|
if not sorted_data:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
hourly_data = []
|
hourly_data = []
|
||||||
current_state_index = 0
|
current_state_index = 0
|
||||||
current_flow_rate = sorted_data[0]["flowRate"]
|
current_flow_rate = sorted_data[0]['flowRate']
|
||||||
current_eq_status = sorted_data[0]["currentEQStatus"]
|
current_eq_status = sorted_data[0]['currentEQStatus']
|
||||||
|
last_time = int(sorted_data[-1]['cumulativeTime'])
|
||||||
# Determine maximum bound (either given or from data)
|
|
||||||
last_time = int(sorted_data[-1]["cumulativeTime"])
|
|
||||||
if max_hours is None:
|
if max_hours is None:
|
||||||
max_hours = last_time
|
max_hours = last_time
|
||||||
|
for hour in range(0, max_hours + 1):
|
||||||
for hour in range(0, max_hours + 1): # start from 0
|
while current_state_index < len(sorted_data) - 1 and hour >= sorted_data[current_state_index + 1]['cumulativeTime']:
|
||||||
# Advance state if needed
|
|
||||||
while (current_state_index < len(sorted_data) - 1 and
|
|
||||||
hour >= sorted_data[current_state_index + 1]["cumulativeTime"]):
|
|
||||||
current_state_index += 1
|
current_state_index += 1
|
||||||
current_flow_rate = sorted_data[current_state_index]["flowRate"]
|
current_flow_rate = sorted_data[current_state_index]['flowRate']
|
||||||
current_eq_status = sorted_data[current_state_index]["currentEQStatus"]
|
current_eq_status = sorted_data[current_state_index]['currentEQStatus']
|
||||||
|
hourly_data.append({'cumulativeTime': hour, 'flowRate': current_flow_rate, 'currentEQStatus': current_eq_status})
|
||||||
hourly_data.append({
|
|
||||||
"cumulativeTime": hour,
|
|
||||||
"flowRate": current_flow_rate,
|
|
||||||
"currentEQStatus": current_eq_status
|
|
||||||
})
|
|
||||||
|
|
||||||
return hourly_data
|
return hourly_data
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def calculate_failures_per_month(hourly_data):
|
def calculate_failures_per_month(hourly_data):
|
||||||
"""
|
|
||||||
Calculate the cumulative number of failures up to each month from hourly data.
|
|
||||||
A failure is defined as when currentEQStatus = "OoS".
|
|
||||||
Only counts the start of each failure period (transition from "Svc" to "OoS").
|
|
||||||
|
|
||||||
Args:
|
|
||||||
hourly_data: List of dicts with 'hour', 'flowrate', and 'currentEQStatus' keys
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of dicts with 'month' and 'failures' keys (cumulative count)
|
|
||||||
"""
|
|
||||||
total_failures = 0
|
total_failures = 0
|
||||||
previous_eq_status = None
|
previous_eq_status = None
|
||||||
monthly_data = {}
|
monthly_data = {}
|
||||||
|
|
||||||
for data_point in hourly_data:
|
for data_point in hourly_data:
|
||||||
hour = data_point['hour']
|
hour = data_point['hour']
|
||||||
current_eq_status = data_point['currentEQStatus']
|
current_eq_status = data_point['currentEQStatus']
|
||||||
|
month = (hour - 1) // 720 + 1
|
||||||
# Calculate which month this hour belongs to (1-based)
|
if current_eq_status == 'OoS' and previous_eq_status is not None and (previous_eq_status != 'OoS'):
|
||||||
# Assuming 30 days per month = 720 hours per month
|
|
||||||
month = ((hour - 1) // 720) + 1
|
|
||||||
|
|
||||||
# Check if this is the start of a failure (transition to "OoS")
|
|
||||||
if current_eq_status == "OoS" and previous_eq_status is not None and previous_eq_status != "OoS":
|
|
||||||
total_failures += 1
|
total_failures += 1
|
||||||
# Special case: if the very first data point is a failure
|
elif current_eq_status == 'OoS' and previous_eq_status is None:
|
||||||
elif current_eq_status == "OoS" and previous_eq_status is None:
|
|
||||||
total_failures += 1
|
total_failures += 1
|
||||||
|
|
||||||
# Store the current cumulative count for this month
|
|
||||||
monthly_data[month] = total_failures
|
monthly_data[month] = total_failures
|
||||||
previous_eq_status = current_eq_status
|
previous_eq_status = current_eq_status
|
||||||
|
|
||||||
# Convert to list format
|
|
||||||
result = []
|
result = []
|
||||||
if monthly_data:
|
if monthly_data:
|
||||||
max_month = max(monthly_data.keys())
|
max_month = max(monthly_data.keys())
|
||||||
for month in range(1, max_month + 1):
|
for month in range(1, max_month + 1):
|
||||||
result.append({
|
result.append({'month': month, 'failures': monthly_data.get(month, monthly_data.get(month - 1, 0))})
|
||||||
'month': month,
|
|
||||||
'failures': monthly_data.get(month, monthly_data.get(month-1, 0))
|
|
||||||
})
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
async def plant_simulation_metrics(simulation_id: str, location_tag: str, max_interval, token, last_oh_date, use_location_tag: int = 1):
|
async def plant_simulation_metrics(simulation_id: str, location_tag: str, max_interval, token, last_oh_date, use_location_tag: int=1):
|
||||||
"""Get failure predictions for equipment from simulation service"""
|
calc_result_url = f'{RBD_SERVICE_API}/aeros/simulation/result/calc/{simulation_id}/{location_tag}'
|
||||||
calc_result_url = f"{RBD_SERVICE_API}/aeros/simulation/result/calc/{simulation_id}/{location_tag}"
|
try:
|
||||||
|
response = requests.get(calc_result_url, headers={'Content-Type': 'application/json', 'Authorization': f'Bearer {token}'}, timeout=30)
|
||||||
try:
|
response.raise_for_status()
|
||||||
response = requests.get(
|
prediction_data = response.json()['data']
|
||||||
calc_result_url,
|
except (requests.RequestException, ValueError) as e:
|
||||||
headers={
|
raise Exception(str(e))
|
||||||
"Content-Type": "application/json",
|
return prediction_data
|
||||||
"Authorization": f"Bearer {token}",
|
|
||||||
},
|
def analyze_monthly_metrics(timestamp_outs, start_date, max_flow_rate: float=550):
|
||||||
timeout=30
|
|
||||||
)
|
|
||||||
response.raise_for_status()
|
|
||||||
prediction_data = response.json()['data']
|
|
||||||
except (requests.RequestException, ValueError) as e:
|
|
||||||
raise Exception(str(e))
|
|
||||||
|
|
||||||
|
|
||||||
return prediction_data
|
|
||||||
|
|
||||||
def analyze_monthly_metrics(timestamp_outs, start_date, max_flow_rate: float = 550):
|
|
||||||
if not timestamp_outs:
|
if not timestamp_outs:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
df = pd.DataFrame(timestamp_outs)
|
df = pd.DataFrame(timestamp_outs)
|
||||||
required_columns = ['cumulativeTime', 'currentEQStatus', 'flowRate']
|
required_columns = ['cumulativeTime', 'currentEQStatus', 'flowRate']
|
||||||
if not all(col in df.columns for col in required_columns):
|
if not all((col in df.columns for col in required_columns)):
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
start_oh = datetime.datetime(start_date.year, start_date.month, start_date.day)
|
start_oh = datetime.datetime(start_date.year, start_date.month, start_date.day)
|
||||||
|
|
||||||
# Actual datetime from cumulative hours
|
|
||||||
df['datetime'] = df['cumulativeTime'].apply(lambda x: start_oh + datetime.timedelta(hours=x))
|
df['datetime'] = df['cumulativeTime'].apply(lambda x: start_oh + datetime.timedelta(hours=x))
|
||||||
df['month_year'] = df['datetime'].dt.to_period('M')
|
df['month_year'] = df['datetime'].dt.to_period('M')
|
||||||
|
|
||||||
# Duration until next timestamp
|
|
||||||
df['duration_hours'] = df['cumulativeTime'].shift(-1) - df['cumulativeTime']
|
df['duration_hours'] = df['cumulativeTime'].shift(-1) - df['cumulativeTime']
|
||||||
df['duration_hours'] = df['duration_hours'].fillna(0)
|
df['duration_hours'] = df['duration_hours'].fillna(0)
|
||||||
|
|
||||||
# Failure detection
|
|
||||||
df['status_change'] = df['currentEQStatus'].shift() != df['currentEQStatus']
|
df['status_change'] = df['currentEQStatus'].shift() != df['currentEQStatus']
|
||||||
df['failure'] = (df['currentEQStatus'] == 'OoS') & df['status_change']
|
df['failure'] = (df['currentEQStatus'] == 'OoS') & df['status_change']
|
||||||
|
|
||||||
# Cumulative tracking
|
|
||||||
df['cumulative_failures'] = df['failure'].cumsum()
|
df['cumulative_failures'] = df['failure'].cumsum()
|
||||||
df['cumulative_oos'] = (df['duration_hours'] * (df['currentEQStatus'] == 'OoS')).cumsum()
|
df['cumulative_oos'] = (df['duration_hours'] * (df['currentEQStatus'] == 'OoS')).cumsum()
|
||||||
|
|
||||||
# Derating calculation
|
|
||||||
# Derating = capacity reduction below max but not outage
|
|
||||||
df['derating'] = (max_flow_rate - df['flowRate']).clip(lower=0)
|
df['derating'] = (max_flow_rate - df['flowRate']).clip(lower=0)
|
||||||
df['is_derated'] = (df['currentEQStatus'] == 'Svc') & (df['derating'] > 0)
|
df['is_derated'] = (df['currentEQStatus'] == 'Svc') & (df['derating'] > 0)
|
||||||
|
|
||||||
# Equivalent Derated Hours (EFDH) → sum of derating * hours, then normalized by max capacity
|
|
||||||
df['derated_mwh'] = df['derating'] * df['duration_hours']
|
df['derated_mwh'] = df['derating'] * df['duration_hours']
|
||||||
df['derated_hours_equivalent'] = df['derated_mwh'] / max_flow_rate
|
df['derated_hours_equivalent'] = df['derated_mwh'] / max_flow_rate
|
||||||
|
|
||||||
monthly_results = {}
|
monthly_results = {}
|
||||||
|
|
||||||
for month_period, group in df.groupby('month_year', sort=True):
|
for month_period, group in df.groupby('month_year', sort=True):
|
||||||
month_str = str(month_period)
|
month_str = str(month_period)
|
||||||
monthly_results[month_str] = {}
|
monthly_results[month_str] = {}
|
||||||
|
|
||||||
# Failures
|
|
||||||
monthly_results[month_str]['failures_count'] = int(group['failure'].sum())
|
monthly_results[month_str]['failures_count'] = int(group['failure'].sum())
|
||||||
monthly_results[month_str]['cumulative_failures'] = int(group['cumulative_failures'].max())
|
monthly_results[month_str]['cumulative_failures'] = int(group['cumulative_failures'].max())
|
||||||
|
|
||||||
# OOS hours
|
|
||||||
oos_time = group.loc[group['currentEQStatus'] == 'OoS', 'duration_hours'].sum()
|
oos_time = group.loc[group['currentEQStatus'] == 'OoS', 'duration_hours'].sum()
|
||||||
monthly_results[month_str]['total_oos_hours'] = float(oos_time)
|
monthly_results[month_str]['total_oos_hours'] = float(oos_time)
|
||||||
monthly_results[month_str]['cummulative_oos'] = float(group['cumulative_oos'].max())
|
monthly_results[month_str]['cummulative_oos'] = float(group['cumulative_oos'].max())
|
||||||
|
|
||||||
# Flow rate (weighted average)
|
|
||||||
total_flow_time = (group['flowRate'] * group['duration_hours']).sum()
|
total_flow_time = (group['flowRate'] * group['duration_hours']).sum()
|
||||||
total_time = group['duration_hours'].sum()
|
total_time = group['duration_hours'].sum()
|
||||||
avg_flow_rate = total_flow_time / total_time if total_time > 0 else 0
|
avg_flow_rate = total_flow_time / total_time if total_time > 0 else 0
|
||||||
monthly_results[month_str]['avg_flow_rate'] = float(avg_flow_rate)
|
monthly_results[month_str]['avg_flow_rate'] = float(avg_flow_rate)
|
||||||
|
|
||||||
# Extra metrics
|
|
||||||
monthly_results[month_str]['total_hours'] = float(total_time)
|
monthly_results[month_str]['total_hours'] = float(total_time)
|
||||||
service_hours = group.loc[group['currentEQStatus'] == 'Svc', 'duration_hours'].sum()
|
service_hours = group.loc[group['currentEQStatus'] == 'Svc', 'duration_hours'].sum()
|
||||||
monthly_results[month_str]['service_hours'] = float(service_hours)
|
monthly_results[month_str]['service_hours'] = float(service_hours)
|
||||||
monthly_results[month_str]['availability_percentage'] = float(
|
monthly_results[month_str]['availability_percentage'] = float(service_hours / total_time * 100 if total_time > 0 else 0)
|
||||||
(service_hours / total_time * 100) if total_time > 0 else 0
|
|
||||||
)
|
|
||||||
|
|
||||||
# Derating metrics
|
|
||||||
derating_hours = group.loc[group['is_derated'], 'duration_hours'].sum()
|
derating_hours = group.loc[group['is_derated'], 'duration_hours'].sum()
|
||||||
derated_mwh = group['derated_mwh'].sum()
|
derated_mwh = group['derated_mwh'].sum()
|
||||||
equivalent_derated_hours = group['derated_hours_equivalent'].sum()
|
equivalent_derated_hours = group['derated_hours_equivalent'].sum()
|
||||||
|
|
||||||
monthly_results[month_str]['derating_hours'] = float(derating_hours)
|
monthly_results[month_str]['derating_hours'] = float(derating_hours)
|
||||||
monthly_results[month_str]['derated_mwh'] = float(derated_mwh)
|
monthly_results[month_str]['derated_mwh'] = float(derated_mwh)
|
||||||
monthly_results[month_str]['equivalent_derated_hours'] = float(equivalent_derated_hours)
|
monthly_results[month_str]['equivalent_derated_hours'] = float(equivalent_derated_hours)
|
||||||
|
|
||||||
return monthly_results
|
return monthly_results
|
||||||
|
|
||||||
|
|
||||||
def calculate_risk_cost_per_failure(monthly_results, birnbaum_importance, energy_price):
|
def calculate_risk_cost_per_failure(monthly_results, birnbaum_importance, energy_price):
|
||||||
"""
|
|
||||||
Calculate risk cost per failure for each month based on:
|
|
||||||
1. Equipment capacity contribution to system (flowrate * birnbaum_importance * availability)
|
|
||||||
2. Capacity lost to downtime per month
|
|
||||||
3. Energy price
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
- monthly_results: Output from analyze_monthly_metrics()
|
|
||||||
- birnbaum_importance: Birnbaum importance factor for this equipment
|
|
||||||
- energy_price: Price per unit of energy/flow
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
- Dictionary with monthly risk costs and array of risk costs per failure
|
|
||||||
"""
|
|
||||||
|
|
||||||
risk_costs = {}
|
risk_costs = {}
|
||||||
risk_cost_array = []
|
risk_cost_array = []
|
||||||
|
|
||||||
for month, data in monthly_results.items():
|
for month, data in monthly_results.items():
|
||||||
# Extract monthly data
|
|
||||||
avg_flow_rate = data['avg_flow_rate']
|
avg_flow_rate = data['avg_flow_rate']
|
||||||
availability = data['availability_percentage'] / 100 # Convert to decimal
|
availability = data['availability_percentage'] / 100
|
||||||
total_oos_hours = data['total_oos_hours']
|
total_oos_hours = data['total_oos_hours']
|
||||||
failures_count = data['failures_count']
|
failures_count = data['failures_count']
|
||||||
|
|
||||||
# 1. Calculate equipment capacity contribution to system
|
|
||||||
equipment_capacity = avg_flow_rate * birnbaum_importance * availability
|
equipment_capacity = avg_flow_rate * birnbaum_importance * availability
|
||||||
|
|
||||||
# 2. Calculate capacity lost to downtime per month
|
|
||||||
# Lost capacity = avg_flowrate * birnbaum_importance * downtime_hours
|
|
||||||
capacity_lost_to_downtime = avg_flow_rate * birnbaum_importance * total_oos_hours
|
capacity_lost_to_downtime = avg_flow_rate * birnbaum_importance * total_oos_hours
|
||||||
|
|
||||||
# 3. Calculate total risk cost for the month
|
|
||||||
# Risk cost = capacity_lost * energy_price
|
|
||||||
monthly_risk_cost = capacity_lost_to_downtime * energy_price
|
monthly_risk_cost = capacity_lost_to_downtime * energy_price
|
||||||
|
|
||||||
# 4. Calculate risk cost per failure for this month
|
|
||||||
if failures_count > 0:
|
if failures_count > 0:
|
||||||
risk_cost_per_failure = monthly_risk_cost / failures_count
|
risk_cost_per_failure = monthly_risk_cost / failures_count
|
||||||
else:
|
else:
|
||||||
# If no failures, set to 0 or use alternative approach
|
|
||||||
risk_cost_per_failure = 0
|
risk_cost_per_failure = 0
|
||||||
|
risk_costs[month] = {'equipment_capacity': equipment_capacity, 'capacity_lost_to_downtime': capacity_lost_to_downtime, 'monthly_risk_cost': monthly_risk_cost, 'failures_count': failures_count, 'risk_cost_per_failure': risk_cost_per_failure}
|
||||||
# Store results
|
|
||||||
risk_costs[month] = {
|
|
||||||
'equipment_capacity': equipment_capacity,
|
|
||||||
'capacity_lost_to_downtime': capacity_lost_to_downtime,
|
|
||||||
'monthly_risk_cost': monthly_risk_cost,
|
|
||||||
'failures_count': failures_count,
|
|
||||||
'risk_cost_per_failure': risk_cost_per_failure
|
|
||||||
}
|
|
||||||
|
|
||||||
# Add to array
|
|
||||||
risk_cost_array.append(risk_cost_per_failure)
|
risk_cost_array.append(risk_cost_per_failure)
|
||||||
|
return {'monthly_details': risk_costs, 'risk_cost_per_failure_array': risk_cost_array}
|
||||||
return {
|
|
||||||
'monthly_details': risk_costs,
|
|
||||||
'risk_cost_per_failure_array': risk_cost_array
|
|
||||||
}
|
|
||||||
|
|
||||||
# Example usage:
|
|
||||||
def get_monthly_risk_analysis(timestamp_outs, birnbaum_importance, energy_price):
|
def get_monthly_risk_analysis(timestamp_outs, birnbaum_importance, energy_price):
|
||||||
"""
|
|
||||||
Complete analysis combining monthly metrics with risk cost calculation
|
|
||||||
"""
|
|
||||||
# Get monthly metrics
|
|
||||||
monthly_metrics = analyze_monthly_metrics(timestamp_outs)
|
monthly_metrics = analyze_monthly_metrics(timestamp_outs)
|
||||||
|
risk_analysis = calculate_risk_cost_per_failure(monthly_metrics, birnbaum_importance, energy_price)
|
||||||
# Calculate risk costs
|
|
||||||
risk_analysis = calculate_risk_cost_per_failure(
|
|
||||||
monthly_metrics,
|
|
||||||
birnbaum_importance,
|
|
||||||
energy_price
|
|
||||||
)
|
|
||||||
|
|
||||||
# Combine results for comprehensive view
|
|
||||||
combined_results = {}
|
combined_results = {}
|
||||||
for month in monthly_metrics.keys():
|
for month in monthly_metrics.keys():
|
||||||
combined_results[month] = {
|
combined_results[month] = {**monthly_metrics[month], **risk_analysis['monthly_details'][month]}
|
||||||
**monthly_metrics[month],
|
return {'monthly_data': combined_results, 'risk_cost_array': risk_analysis['risk_cost_per_failure_array']}
|
||||||
**risk_analysis['monthly_details'][month]
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
'monthly_data': combined_results,
|
|
||||||
'risk_cost_array': risk_analysis['risk_cost_per_failure_array']
|
|
||||||
}
|
|
||||||
|
|
||||||
# Usage example:
|
|
||||||
#
|
|
||||||
|
|||||||
@ -1,125 +1,36 @@
|
|||||||
"""
|
|
||||||
CSRF Token Verification Dependency for be-optimumoh (FastAPI).
|
|
||||||
|
|
||||||
Menggantikan CSRFProtectMiddleware (Double Submit Cookie) dengan verifikasi
|
|
||||||
berbasis Redis. Token CSRF di-generate oleh be-auth (CSRFTokenResource) dan
|
|
||||||
disimpan di Redis dalam bentuk SHA256 hash.
|
|
||||||
|
|
||||||
Token Flow (generation di be-auth, verification di sini):
|
|
||||||
────────────────────────────────────────
|
|
||||||
GENERATION (be-auth / CSRFTokenResource):
|
|
||||||
1. token = secrets.token_urlsafe(32)
|
|
||||||
2. token_hash = SHA256(token)
|
|
||||||
3. redis[csrf:{user_id}:{path_hash}] = token_hash (dengan TTL)
|
|
||||||
4. return plain token ke client
|
|
||||||
|
|
||||||
VERIFICATION (dependency ini):
|
|
||||||
1. Baca X-CSRF-Token header dari request
|
|
||||||
2. Ambil user_id dari request.state.user (di-set oleh JWTBearer)
|
|
||||||
3. incoming_hash = SHA256(nilai X-CSRF-Token)
|
|
||||||
4. stored_hash = redis_auth.get("csrf:{user_id}:{path_hash}")
|
|
||||||
5. secrets.compare_digest(incoming_hash, stored_hash)
|
|
||||||
6. redis_auth.delete(key) ← one-time use
|
|
||||||
────────────────────────────────────────
|
|
||||||
|
|
||||||
NOTE: Dependency ini harus selalu dikombinasikan dengan JWTBearer() agar
|
|
||||||
request.state.user sudah tersedia saat dijalankan.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
from fastapi import Depends
|
|
||||||
from src.csrf_protect import csrf_protect
|
|
||||||
|
|
||||||
@router.post("/some-endpoint", dependencies=[Depends(csrf_protect)])
|
|
||||||
async def some_endpoint(...):
|
|
||||||
...
|
|
||||||
"""
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
import secrets
|
import secrets
|
||||||
|
|
||||||
import redis
|
import redis
|
||||||
from fastapi import HTTPException, Request
|
from fastapi import HTTPException, Request
|
||||||
|
|
||||||
import src.config as config
|
import src.config as config
|
||||||
from src.context import get_user_id
|
from src.context import get_user_id
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
_redis_auth = redis.Redis(host=config.REDIS_HOST, port=int(config.REDIS_PORT), db=config.REDIS_AUTH_DB)
|
||||||
# Koneksi Redis untuk CSRF — Redis DB 0 tempat be-auth menyimpan CSRF token
|
|
||||||
_redis_auth = redis.Redis(
|
|
||||||
host=config.REDIS_HOST,
|
|
||||||
port=int(config.REDIS_PORT),
|
|
||||||
db=config.REDIS_AUTH_DB,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def csrf_protect(request: Request) -> None:
|
async def csrf_protect(request: Request) -> None:
|
||||||
"""
|
user = getattr(request.state, 'user', None)
|
||||||
FastAPI dependency untuk verifikasi CSRF token berbasis Redis.
|
|
||||||
Harus dikombinasikan dengan JWTBearer() dependency.
|
|
||||||
"""
|
|
||||||
# ── Step 1: ambil user_id dari request.state (di-set oleh JWTBearer) ────
|
|
||||||
user = getattr(request.state, "user", None)
|
|
||||||
user_id = str(user.user_id) if user else get_user_id()
|
user_id = str(user.user_id) if user else get_user_id()
|
||||||
|
|
||||||
if not user_id:
|
if not user_id:
|
||||||
raise HTTPException(status_code=401, detail="Authentication required")
|
raise HTTPException(status_code=401, detail='Authentication required')
|
||||||
|
csrf_token_header = request.headers.get('x-csrf-token') or request.headers.get('x-xsrf-token') or request.headers.get('x-csrf-token')
|
||||||
# ── Step 2: baca CSRF token dari request header ───────────────────────────
|
|
||||||
# Starlette lowercases all header names
|
|
||||||
csrf_token_header = (
|
|
||||||
request.headers.get("x-csrf-token")
|
|
||||||
or request.headers.get("x-xsrf-token")
|
|
||||||
or request.headers.get("x-csrf-token")
|
|
||||||
)
|
|
||||||
|
|
||||||
if not csrf_token_header:
|
if not csrf_token_header:
|
||||||
log.warning(
|
log.warning('[CSRF] Missing X-CSRF-Token header | user_id=%s | IP: %s', user_id, request.client.host if request.client else 'unknown')
|
||||||
"[CSRF] Missing X-CSRF-Token header | user_id=%s | IP: %s",
|
raise HTTPException(status_code=403, detail='CSRF token missing in header')
|
||||||
user_id,
|
|
||||||
request.client.host if request.client else "unknown",
|
|
||||||
)
|
|
||||||
raise HTTPException(status_code=403, detail="CSRF token missing in header")
|
|
||||||
|
|
||||||
csrf_token_header = csrf_token_header.strip()
|
csrf_token_header = csrf_token_header.strip()
|
||||||
|
|
||||||
# ── Step 3: bangun Redis key (harus sama dengan format di be-auth) ─────────
|
|
||||||
current_path = request.url.path
|
current_path = request.url.path
|
||||||
path_hash = hashlib.sha256(current_path.encode()).hexdigest()[:16]
|
path_hash = hashlib.sha256(current_path.encode()).hexdigest()[:16]
|
||||||
redis_key = f"csrf:{user_id}:{path_hash}"
|
redis_key = f'csrf:{user_id}:{path_hash}'
|
||||||
|
|
||||||
# ── Step 4: ambil stored token hash dari Redis (DB 0) ────────────────────
|
|
||||||
stored_token_hash = _redis_auth.get(redis_key)
|
stored_token_hash = _redis_auth.get(redis_key)
|
||||||
if not stored_token_hash:
|
if not stored_token_hash:
|
||||||
log.warning(
|
log.warning('[CSRF] Token not found in Redis | user_id=%s | path=%s | IP: %s', user_id, current_path, request.client.host if request.client else 'unknown')
|
||||||
"[CSRF] Token not found in Redis | user_id=%s | path=%s | IP: %s",
|
raise HTTPException(status_code=403, detail='CSRF token expired or invalid')
|
||||||
user_id,
|
|
||||||
current_path,
|
|
||||||
request.client.host if request.client else "unknown",
|
|
||||||
)
|
|
||||||
raise HTTPException(status_code=403, detail="CSRF token expired or invalid")
|
|
||||||
|
|
||||||
if isinstance(stored_token_hash, bytes):
|
if isinstance(stored_token_hash, bytes):
|
||||||
stored_token_hash = stored_token_hash.decode("utf-8")
|
stored_token_hash = stored_token_hash.decode('utf-8')
|
||||||
|
|
||||||
# ── Step 5: bandingkan hash dengan constant-time digest ───────────────────
|
|
||||||
incoming_hash = hashlib.sha256(csrf_token_header.encode()).hexdigest()
|
incoming_hash = hashlib.sha256(csrf_token_header.encode()).hexdigest()
|
||||||
if not secrets.compare_digest(incoming_hash, stored_token_hash):
|
if not secrets.compare_digest(incoming_hash, stored_token_hash):
|
||||||
log.warning(
|
log.warning('[CSRF] Token mismatch | user_id=%s | path=%s | IP: %s', user_id, current_path, request.client.host if request.client else 'unknown')
|
||||||
"[CSRF] Token mismatch | user_id=%s | path=%s | IP: %s",
|
|
||||||
user_id,
|
|
||||||
current_path,
|
|
||||||
request.client.host if request.client else "unknown",
|
|
||||||
)
|
|
||||||
_redis_auth.delete(redis_key)
|
_redis_auth.delete(redis_key)
|
||||||
raise HTTPException(status_code=403, detail="CSRF token verification failed")
|
raise HTTPException(status_code=403, detail='CSRF token verification failed')
|
||||||
|
|
||||||
# ── Step 6: hapus token (one-time use) dan lanjutkan ─────────────────────
|
|
||||||
_redis_auth.delete(redis_key)
|
_redis_auth.delete(redis_key)
|
||||||
log.info(
|
log.info('[CSRF] Verified & deleted | user_id=%s | path=%s', user_id, current_path)
|
||||||
"[CSRF] Verified & deleted | user_id=%s | path=%s",
|
|
||||||
user_id,
|
|
||||||
current_path,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
|
||||||
@ -1,22 +1,18 @@
|
|||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
from src.models import DefultBase
|
from src.models import DefultBase
|
||||||
|
|
||||||
|
|
||||||
class CommonParams(DefultBase):
|
class CommonParams(DefultBase):
|
||||||
# This ensures no extra query params are allowed
|
current_user: Optional[str] = Field(None, alias='currentUser')
|
||||||
current_user: Optional[str] = Field(None, alias="currentUser")
|
|
||||||
page: int = Field(1, gt=0, le=1000000)
|
page: int = Field(1, gt=0, le=1000000)
|
||||||
items_per_page: int = Field(5, gt=0, le=50, multiple_of=5, alias="itemsPerPage")
|
items_per_page: int = Field(5, gt=0, le=50, multiple_of=5, alias='itemsPerPage')
|
||||||
query_str: Optional[str] = Field(None, alias="q")
|
query_str: Optional[str] = Field(None, alias='q')
|
||||||
filter_spec: Optional[str] = Field(None, alias="filter")
|
filter_spec: Optional[str] = Field(None, alias='filter')
|
||||||
sort_by: List[str] = Field(default_factory=list, alias="sortBy[]")
|
sort_by: List[str] = Field(default_factory=list, alias='sortBy[]')
|
||||||
descending: List[bool] = Field(default_factory=list, alias="descending[]")
|
descending: List[bool] = Field(default_factory=list, alias='descending[]')
|
||||||
exclude: List[str] = Field(default_factory=list, alias="exclude[]")
|
exclude: List[str] = Field(default_factory=list, alias='exclude[]')
|
||||||
all_params: int = Field(0, alias="all", ge=0, le=1000000)
|
all_params: int = Field(0, alias='all', ge=0, le=1000000)
|
||||||
|
|
||||||
# Property to mirror your original return dict's bool conversion
|
|
||||||
@property
|
@property
|
||||||
def is_all(self) -> bool:
|
def is_all(self) -> bool:
|
||||||
return bool(self.all_params)
|
return bool(self.all_params)
|
||||||
|
|||||||
@ -1,144 +1,58 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Annotated, List, Type, TypeVar
|
from typing import Annotated, List, Type, TypeVar
|
||||||
|
|
||||||
from fastapi import Depends, Query
|
from fastapi import Depends, Query
|
||||||
from pydantic.types import Json, constr
|
from pydantic.types import Json, constr
|
||||||
from sqlalchemy import Select, desc, func, or_
|
from sqlalchemy import Select, desc, func, or_
|
||||||
|
|
||||||
from src.database.schema import CommonParams
|
from src.database.schema import CommonParams
|
||||||
|
|
||||||
from .core import DbSession
|
from .core import DbSession
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
QueryStr = constr(pattern='^[ -~]+$', min_length=1)
|
||||||
|
|
||||||
# allows only printable characters
|
def common_parameters(db_session: DbSession, current_user: QueryStr=Query(None, alias='currentUser'), page: int=Query(1, gt=0, le=1000000), items_per_page: int=Query(5, alias='itemsPerPage', gt=-2, le=1000000), query_str: QueryStr=Query(None, alias='q'), filter_spec: QueryStr=Query(None, alias='filter'), sort_by: List[str]=Query([], alias='sortBy[]'), descending: List[bool]=Query([], alias='descending[]'), exclude: List[str]=Query([], alias='exclude[]'), all: int=Query(0, ge=0, le=1000000)):
|
||||||
QueryStr = constr(pattern=r"^[ -~]+$", min_length=1)
|
return {'db_session': db_session, 'page': page, 'items_per_page': items_per_page, 'query_str': query_str, 'filter_spec': filter_spec, 'sort_by': sort_by, 'descending': descending, 'current_user': current_user, 'all': bool(all)}
|
||||||
|
CommonParameters = Annotated[dict[str, int | str | DbSession | QueryStr | Json | List[str] | List[bool]] | bool, Depends(common_parameters)]
|
||||||
|
T = TypeVar('T', bound=CommonParams)
|
||||||
def common_parameters(
|
|
||||||
db_session: DbSession, # type: ignore
|
|
||||||
current_user: QueryStr = Query(None, alias="currentUser"), # type: ignore
|
|
||||||
page: int = Query(1, gt=0, le=1000000),
|
|
||||||
items_per_page: int = Query(5, alias="itemsPerPage", gt=-2, le=1000000),
|
|
||||||
query_str: QueryStr = Query(None, alias="q"), # type: ignore
|
|
||||||
filter_spec: QueryStr = Query(None, alias="filter"), # type: ignore
|
|
||||||
sort_by: List[str] = Query([], alias="sortBy[]"),
|
|
||||||
descending: List[bool] = Query([], alias="descending[]"),
|
|
||||||
exclude: List[str] = Query([], alias="exclude[]"),
|
|
||||||
all: int = Query(0, ge=0, le=1000000),
|
|
||||||
):
|
|
||||||
return {
|
|
||||||
"db_session": db_session,
|
|
||||||
"page": page,
|
|
||||||
"items_per_page": items_per_page,
|
|
||||||
"query_str": query_str,
|
|
||||||
"filter_spec": filter_spec,
|
|
||||||
"sort_by": sort_by,
|
|
||||||
"descending": descending,
|
|
||||||
"current_user": current_user,
|
|
||||||
"all": bool(all),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
CommonParameters = Annotated[
|
|
||||||
dict[str, int | str | DbSession | QueryStr | Json | List[str] | List[bool]] | bool,
|
|
||||||
Depends(common_parameters),
|
|
||||||
]
|
|
||||||
|
|
||||||
T = TypeVar("T", bound=CommonParams)
|
|
||||||
|
|
||||||
def get_params_factory(model_type: Type[T]):
|
def get_params_factory(model_type: Type[T]):
|
||||||
async def wrapper(
|
|
||||||
db_session: DbSession,
|
async def wrapper(db_session: DbSession, params: Annotated[model_type, Query()]):
|
||||||
params: Annotated[model_type, Query()] # type: ignore
|
|
||||||
):
|
|
||||||
res = params.model_dump()
|
res = params.model_dump()
|
||||||
return {
|
return {'db_session': db_session, 'all': params.is_all, **res}
|
||||||
"db_session": db_session,
|
|
||||||
"all": params.is_all,
|
|
||||||
**res
|
|
||||||
}
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
def search(*, query_str: str, query: Query, model, sort=False):
|
def search(*, query_str: str, query: Query, model, sort=False):
|
||||||
"""Perform a search based on the query."""
|
|
||||||
search_model = model
|
search_model = model
|
||||||
|
|
||||||
if not query_str.strip():
|
if not query_str.strip():
|
||||||
return query
|
return query
|
||||||
|
|
||||||
search = []
|
search = []
|
||||||
if hasattr(search_model, "search_vector"):
|
if hasattr(search_model, 'search_vector'):
|
||||||
vector = search_model.search_vector
|
vector = search_model.search_vector
|
||||||
search.append(vector.op("@@")(func.tsq_parse(query_str)))
|
search.append(vector.op('@@')(func.tsq_parse(query_str)))
|
||||||
|
if hasattr(search_model, 'name'):
|
||||||
if hasattr(search_model, "name"):
|
search.append(search_model.name.ilike(f'%{query_str}%'))
|
||||||
search.append(
|
|
||||||
search_model.name.ilike(f"%{query_str}%"),
|
|
||||||
)
|
|
||||||
search.append(search_model.name == query_str)
|
search.append(search_model.name == query_str)
|
||||||
|
|
||||||
if not search:
|
if not search:
|
||||||
raise Exception(f"Search not supported for model: {model}")
|
raise Exception(f'Search not supported for model: {model}')
|
||||||
|
|
||||||
query = query.filter(or_(*search))
|
query = query.filter(or_(*search))
|
||||||
|
|
||||||
if sort:
|
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)
|
return query.params(term=query_str)
|
||||||
|
|
||||||
|
async def search_filter_sort_paginate(db_session: DbSession, model, query_str: str=None, filter_spec: str | dict | None=None, page: int=1, items_per_page: int=5, sort_by: List[str]=None, descending: List[bool]=None, current_user: str=None, exclude: List[str]=None, all: bool=False):
|
||||||
async def search_filter_sort_paginate(
|
|
||||||
db_session: DbSession,
|
|
||||||
model,
|
|
||||||
query_str: str = None,
|
|
||||||
filter_spec: str | dict | None = None,
|
|
||||||
page: int = 1,
|
|
||||||
items_per_page: int = 5,
|
|
||||||
sort_by: List[str] = None,
|
|
||||||
descending: List[bool] = None,
|
|
||||||
current_user: str = None,
|
|
||||||
exclude: List[str] = None,
|
|
||||||
all: bool = False,
|
|
||||||
):
|
|
||||||
"""Common functionality for searching, filtering, sorting, and pagination."""
|
|
||||||
# Check if model is Select
|
|
||||||
if not isinstance(model, Select):
|
if not isinstance(model, Select):
|
||||||
query = Select(model)
|
query = Select(model)
|
||||||
else:
|
else:
|
||||||
query = model
|
query = model
|
||||||
|
|
||||||
if query_str:
|
if query_str:
|
||||||
sort = False if sort_by else True
|
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())
|
count_query = Select(func.count()).select_from(query.subquery())
|
||||||
total = await db_session.scalar(count_query)
|
total = await db_session.scalar(count_query)
|
||||||
|
|
||||||
if all:
|
if all:
|
||||||
result = await db_session.execute(query)
|
result = await db_session.execute(query)
|
||||||
items = result.scalars().all()
|
items = result.scalars().all()
|
||||||
|
return {'items': items, 'itemsPerPage': total, 'page': 1, 'total': total, 'totalPages': 1}
|
||||||
return {
|
|
||||||
"items": items,
|
|
||||||
"itemsPerPage": total,
|
|
||||||
"page": 1,
|
|
||||||
"total": total,
|
|
||||||
"totalPages": 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
query = query.offset((page - 1) * items_per_page).limit(items_per_page)
|
query = query.offset((page - 1) * items_per_page).limit(items_per_page)
|
||||||
|
|
||||||
result = await db_session.execute(query)
|
result = await db_session.execute(query)
|
||||||
items = result.scalars().all()
|
items = result.scalars().all()
|
||||||
|
return {'items': items, 'itemsPerPage': items_per_page, 'page': page, 'total': total, 'totalPages': (total + items_per_page - 1) // items_per_page}
|
||||||
return {
|
|
||||||
"items": items,
|
|
||||||
"itemsPerPage": items_per_page,
|
|
||||||
"page": page,
|
|
||||||
"total": total,
|
|
||||||
"totalPages": (total + items_per_page - 1) // items_per_page,
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,29 +1,12 @@
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
if sys.version_info >= (3, 11):
|
if sys.version_info >= (3, 11):
|
||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
else:
|
else:
|
||||||
from strenum import StrEnum
|
from strenum import StrEnum
|
||||||
|
|
||||||
|
|
||||||
class OptimumOHEnum(StrEnum):
|
class OptimumOHEnum(StrEnum):
|
||||||
"""
|
pass
|
||||||
A custom Enum class that extends StrEnum.
|
|
||||||
|
|
||||||
This class inherits all functionality from StrEnum, including
|
|
||||||
string representation and automatic value conversion to strings.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
class Visibility(DispatchEnum):
|
|
||||||
OPEN = "Open"
|
|
||||||
RESTRICTED = "Restricted"
|
|
||||||
|
|
||||||
assert str(Visibility.OPEN) == "Open"
|
|
||||||
"""
|
|
||||||
|
|
||||||
pass # No additional implementation needed
|
|
||||||
|
|
||||||
|
|
||||||
class ResponseStatus(OptimumOHEnum):
|
class ResponseStatus(OptimumOHEnum):
|
||||||
SUCCESS = "success"
|
SUCCESS = 'success'
|
||||||
ERROR = "error"
|
ERROR = 'error'
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
|
||||||
@ -1,19 +1,12 @@
|
|||||||
from sqlalchemy import UUID, Column, Float, ForeignKey, String
|
from sqlalchemy import UUID, Column, Float, ForeignKey, String
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
from src.database.core import Base
|
from src.database.core import Base
|
||||||
from src.models import DefaultMixin
|
from src.models import DefaultMixin
|
||||||
|
|
||||||
|
|
||||||
class ScopeEquipmentPart(Base, DefaultMixin):
|
class ScopeEquipmentPart(Base, DefaultMixin):
|
||||||
__tablename__ = "oh_ms_scope_equipment_part"
|
__tablename__ = 'oh_ms_scope_equipment_part'
|
||||||
|
|
||||||
required_stock = Column(Float, nullable=False, default=0)
|
required_stock = Column(Float, nullable=False, default=0)
|
||||||
sparepart_id = Column(UUID(as_uuid=True), ForeignKey("oh_ms_sparepart.id"), nullable=False)
|
sparepart_id = Column(UUID(as_uuid=True), ForeignKey('oh_ms_sparepart.id'), nullable=False)
|
||||||
location_tag = Column(String, nullable=False)
|
location_tag = Column(String, nullable=False)
|
||||||
location_tag = Column(String, nullable=False)
|
location_tag = Column(String, nullable=False)
|
||||||
|
part = relationship('MasterSparePart', lazy='selectin')
|
||||||
part = relationship(
|
|
||||||
"MasterSparePart",
|
|
||||||
lazy="selectin",
|
|
||||||
)
|
|
||||||
|
|||||||
@ -1,22 +1,12 @@
|
|||||||
from typing import Dict, List
|
from typing import Dict, List
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
from src.database.core import CollectorDbSession
|
from src.database.core import CollectorDbSession
|
||||||
from src.models import StandardResponse
|
from src.models import StandardResponse
|
||||||
from src.auth.access_control import require_any_role, ALLOWED_ROLES
|
from src.auth.access_control import require_any_role, ALLOWED_ROLES
|
||||||
|
|
||||||
from .service import get_all
|
from .service import get_all
|
||||||
|
|
||||||
router = APIRouter(dependencies=[Depends(require_any_role(*ALLOWED_ROLES))])
|
router = APIRouter(dependencies=[Depends(require_any_role(*ALLOWED_ROLES))])
|
||||||
|
|
||||||
|
@router.get('/{location_tag}', response_model=StandardResponse[List[Dict]])
|
||||||
@router.get("/{location_tag}", response_model=StandardResponse[List[Dict]])
|
|
||||||
async def get_scope_equipment_parts(collector_db_session: CollectorDbSession, location_tag):
|
async def get_scope_equipment_parts(collector_db_session: CollectorDbSession, location_tag):
|
||||||
"""Get all scope activity pagination."""
|
|
||||||
data = await get_all(db_session=collector_db_session, location_tag=location_tag)
|
data = await get_all(db_session=collector_db_session, location_tag=location_tag)
|
||||||
|
return StandardResponse(data=data, message='Data retrieved successfully')
|
||||||
return StandardResponse(
|
|
||||||
data=data,
|
|
||||||
message="Data retrieved successfully",
|
|
||||||
)
|
|
||||||
|
|||||||
@ -1,174 +1,29 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# async def get(*, db_session: DbSession, scope_equipment_activity_id: str) -> Optional[ScopeEquipmentActivity]:
|
|
||||||
# """Returns a document based on the given document id."""
|
|
||||||
|
|
||||||
from typing import Optional, List, Dict, Any
|
from typing import Optional, List, Dict, Any
|
||||||
from sqlalchemy.sql import text
|
from sqlalchemy.sql import text
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# async def get_all(
|
|
||||||
# db_session: CollectorDbSession,
|
|
||||||
# ) -> List[Dict[str, Any]]:
|
|
||||||
# """
|
|
||||||
# Retrieve overhaul spare parts consumption data.
|
|
||||||
# Handles missing data, null parent WO, and query safety.
|
|
||||||
|
|
||||||
# Args:
|
|
||||||
# db_session: Async SQLAlchemy session
|
|
||||||
# location_tag: Optional location filter
|
|
||||||
# start_year: Year to start analysis (default 2023)
|
|
||||||
# end_year: Optional year to end analysis (default start_year + 1)
|
|
||||||
# parent_wonum: Parent work order number (required for context)
|
|
||||||
|
|
||||||
# Returns:
|
|
||||||
# List of dictionaries with spare part usage per overhaul WO.
|
|
||||||
# """
|
|
||||||
|
|
||||||
# # --- 1. Basic validation ---
|
|
||||||
# if not parent_wonum:
|
|
||||||
|
|
||||||
# if start_year < 1900 or (end_year and end_year < start_year):
|
|
||||||
|
|
||||||
# if end_year is None:
|
|
||||||
|
|
||||||
# # --- 2. Build SQL safely ---
|
|
||||||
# base_query = """
|
|
||||||
# WITH filtered_wo AS (
|
|
||||||
# SELECT wonum, location_tag
|
|
||||||
# FROM public.wo_max
|
|
||||||
# WHERE worktype = 'OH'
|
|
||||||
# AND xx_parent = :parent_wonum
|
|
||||||
# """
|
|
||||||
|
|
||||||
|
|
||||||
# if location_tag:
|
|
||||||
|
|
||||||
# base_query += """
|
|
||||||
# ),
|
|
||||||
# filtered_materials AS (
|
|
||||||
# SELECT wonum, itemnum, itemqty, inv_curbaltotal, inv_avgcost
|
|
||||||
# FROM public.wo_max_material
|
|
||||||
# WHERE wonum IN (SELECT wonum FROM filtered_wo)
|
|
||||||
# SELECT
|
|
||||||
# fwo.location_tag AS location_tag,
|
|
||||||
# fm.itemnum,
|
|
||||||
# spl.description AS sparepart_name,
|
|
||||||
# COALESCE(SUM(fm.itemqty), 0) AS parts_consumed_in_oh,
|
|
||||||
# COALESCE(AVG(fm.inv_avgcost), 0) AS avgcost,
|
|
||||||
# COALESCE(AVG(fm.inv_curbaltotal), 0) AS inv_curbaltotal
|
|
||||||
# FROM filtered_wo fwo
|
|
||||||
# INNER JOIN filtered_materials fm ON fwo.wonum = fm.wonum
|
|
||||||
# LEFT JOIN public.maximo_sparepart_pr_po_line spl ON fm.itemnum = spl.item_num
|
|
||||||
# GROUP BY fwo.location_tag, fm.itemnum, spl.description
|
|
||||||
# ORDER BY fwo.location_tag, fm.itemnum;
|
|
||||||
# """
|
|
||||||
|
|
||||||
# # --- 3. Execute query ---
|
|
||||||
|
|
||||||
# # Handle "no data found"
|
|
||||||
# if not rows:
|
|
||||||
|
|
||||||
# # --- 4. Map results cleanly ---
|
|
||||||
# for row in rows:
|
|
||||||
# equipment_parts.append({
|
|
||||||
# "inv_curbaltotal": float(row.inv_curbaltotal or 0)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
from typing import List, Dict, Any, Optional
|
from typing import List, Dict, Any, Optional
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy.sql import text
|
from sqlalchemy.sql import text
|
||||||
|
|
||||||
|
async def get_all(db_session: AsyncSession, location_tag: Optional[str]=None, start_year: int=2023, end_year: Optional[int]=None) -> List[Dict[str, Any]]:
|
||||||
async def get_all(
|
|
||||||
db_session: AsyncSession,
|
|
||||||
location_tag: Optional[str] = None,
|
|
||||||
start_year: int = 2023,
|
|
||||||
end_year: Optional[int] = None
|
|
||||||
) -> List[Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
Get overhaul spare parts consumption data with optimized query.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db_session: SQLAlchemy async database session
|
|
||||||
location_tag: Optional filter for location (asset_location)
|
|
||||||
start_year: Starting year (default: 2023)
|
|
||||||
end_year: Ending year (default: start_year + 1)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of dictionaries with spare parts consumption data
|
|
||||||
"""
|
|
||||||
# Set default end year
|
|
||||||
if end_year is None:
|
if end_year is None:
|
||||||
end_year = start_year + 1
|
end_year = start_year + 1
|
||||||
|
query_str = "\n WITH filtered_wo AS (\n SELECT DISTINCT wonum, asset_location, asset_unit\n FROM public.wo_maximo ma\n WHERE ma.xx_parent IN ('155026', '155027', '155029', '155030')\n "
|
||||||
# Build query dynamically
|
|
||||||
query_str = """
|
|
||||||
WITH filtered_wo AS (
|
|
||||||
SELECT DISTINCT wonum, asset_location, asset_unit
|
|
||||||
FROM public.wo_maximo ma
|
|
||||||
WHERE ma.xx_parent IN ('155026', '155027', '155029', '155030')
|
|
||||||
"""
|
|
||||||
|
|
||||||
params = {}
|
params = {}
|
||||||
|
|
||||||
# Optional filter for location
|
|
||||||
if location_tag:
|
if location_tag:
|
||||||
query_str += " AND asset_location = :location_tag"
|
query_str += ' AND asset_location = :location_tag'
|
||||||
params["location_tag"] = location_tag
|
params['location_tag'] = location_tag
|
||||||
|
query_str += "\n ),\n filtered_materials AS (\n SELECT \n mat.wonum,\n mat.itemnum,\n mat.itemqty,\n mat.inv_curbaltotal AS inv_curbaltotal,\n mat.inv_avgcost AS inv_avgcost\n FROM public.wo_maximo_material AS mat\n WHERE mat.wonum IN (SELECT wonum FROM filtered_wo)\n )\n SELECT \n fwo.asset_location AS location_tag,\n ft.itemnum,\n COALESCE(spl.description, 'Unknown') AS sparepart_name,\n AVG(ft.itemqty) AS total_parts_used,\n COALESCE(AVG(ft.inv_avgcost), 0) AS avg_cost,\n COALESCE(AVG(ft.inv_curbaltotal), 0) AS avg_inventory_balance\n FROM filtered_wo AS fwo\n INNER JOIN filtered_materials AS ft \n ON fwo.wonum = ft.wonum\n LEFT JOIN public.maximo_sparepart_pr_po_line AS spl \n ON ft.itemnum = spl.item_num\n GROUP BY fwo.asset_location, ft.itemnum, spl.description\n ORDER BY fwo.asset_location, ft.itemnum;\n "
|
||||||
query_str += """
|
|
||||||
),
|
|
||||||
filtered_materials AS (
|
|
||||||
SELECT
|
|
||||||
mat.wonum,
|
|
||||||
mat.itemnum,
|
|
||||||
mat.itemqty,
|
|
||||||
mat.inv_curbaltotal AS inv_curbaltotal,
|
|
||||||
mat.inv_avgcost AS inv_avgcost
|
|
||||||
FROM public.wo_maximo_material AS mat
|
|
||||||
WHERE mat.wonum IN (SELECT wonum FROM filtered_wo)
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
fwo.asset_location AS location_tag,
|
|
||||||
ft.itemnum,
|
|
||||||
COALESCE(spl.description, 'Unknown') AS sparepart_name,
|
|
||||||
AVG(ft.itemqty) AS total_parts_used,
|
|
||||||
COALESCE(AVG(ft.inv_avgcost), 0) AS avg_cost,
|
|
||||||
COALESCE(AVG(ft.inv_curbaltotal), 0) AS avg_inventory_balance
|
|
||||||
FROM filtered_wo AS fwo
|
|
||||||
INNER JOIN filtered_materials AS ft
|
|
||||||
ON fwo.wonum = ft.wonum
|
|
||||||
LEFT JOIN public.maximo_sparepart_pr_po_line AS spl
|
|
||||||
ON ft.itemnum = spl.item_num
|
|
||||||
GROUP BY fwo.asset_location, ft.itemnum, spl.description
|
|
||||||
ORDER BY fwo.asset_location, ft.itemnum;
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = await db_session.execute(text(query_str), params)
|
result = await db_session.execute(text(query_str), params)
|
||||||
rows = result.fetchall()
|
rows = result.fetchall()
|
||||||
|
|
||||||
equipment_parts = []
|
equipment_parts = []
|
||||||
for row in rows:
|
for row in rows:
|
||||||
equipment_parts.append({
|
equipment_parts.append({'location_tag': row.location_tag, 'itemnum': row.itemnum, 'sparepart_name': row.sparepart_name, 'parts_consumed_in_oh': float(row.total_parts_used or 0), 'avg_cost': float(row.avg_cost or 0), 'inv_curbaltotal': float(row.avg_inventory_balance or 0)})
|
||||||
"location_tag": row.location_tag,
|
|
||||||
"itemnum": row.itemnum,
|
|
||||||
"sparepart_name": row.sparepart_name,
|
|
||||||
"parts_consumed_in_oh": float(row.total_parts_used or 0),
|
|
||||||
"avg_cost": float(row.avg_cost or 0),
|
|
||||||
"inv_curbaltotal": float(row.avg_inventory_balance or 0),
|
|
||||||
})
|
|
||||||
|
|
||||||
return equipment_parts
|
return equipment_parts
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[get_all] Database query error: {e}")
|
print(f'[get_all] Database query error: {e}')
|
||||||
raise
|
raise
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
|
||||||
@ -1,20 +1,11 @@
|
|||||||
from sqlalchemy import UUID, Column, ForeignKey, String
|
from sqlalchemy import UUID, Column, ForeignKey, String
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
from src.database.core import Base
|
from src.database.core import Base
|
||||||
from src.models import DefaultMixin
|
from src.models import DefaultMixin
|
||||||
|
|
||||||
|
|
||||||
class EquipmentWorkscopeGroup(Base, DefaultMixin):
|
class EquipmentWorkscopeGroup(Base, DefaultMixin):
|
||||||
__tablename__ = "oh_tr_equipment_workscope_group"
|
__tablename__ = 'oh_tr_equipment_workscope_group'
|
||||||
|
|
||||||
workscope_group_id = Column(UUID(as_uuid=True), ForeignKey('oh_ms_workscope_group.id'))
|
workscope_group_id = Column(UUID(as_uuid=True), ForeignKey('oh_ms_workscope_group.id'))
|
||||||
location_tag = Column(String, nullable=False)
|
location_tag = Column(String, nullable=False)
|
||||||
|
workscope_group = relationship('MasterActivity', lazy='selectin', back_populates='equipment_workscope_groups')
|
||||||
workscope_group = relationship("MasterActivity", lazy="selectin", back_populates="equipment_workscope_groups")
|
equipment = relationship('StandardScope', lazy='raise', primaryjoin='and_(EquipmentWorkscopeGroup.location_tag == foreign(StandardScope.location_tag))', uselist=False)
|
||||||
equipment = relationship(
|
|
||||||
"StandardScope",
|
|
||||||
lazy="raise",
|
|
||||||
primaryjoin="and_(EquipmentWorkscopeGroup.location_tag == foreign(StandardScope.location_tag))",
|
|
||||||
uselist=False, # Add this if it's a one-to-one relationship
|
|
||||||
)
|
|
||||||
|
|||||||
@ -1,48 +1,22 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from fastapi import APIRouter, Query
|
from fastapi import APIRouter, Query
|
||||||
|
from src.database.service import CommonParameters, DbSession
|
||||||
from src.database.service import (CommonParameters, DbSession)
|
|
||||||
from src.models import StandardResponse
|
from src.models import StandardResponse
|
||||||
|
|
||||||
from .schema import ScopeEquipmentJobCreate, ScopeEquipmentJobPagination
|
from .schema import ScopeEquipmentJobCreate, ScopeEquipmentJobPagination
|
||||||
from .service import create, delete, get_all
|
from .service import create, delete, get_all
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.get('/{location_tag}', response_model=StandardResponse[ScopeEquipmentJobPagination])
|
||||||
@router.get(
|
async def get_jobs(common: CommonParameters, location_tag: str, scope: Optional[str]=Query(None)):
|
||||||
"/{location_tag}", response_model=StandardResponse[ScopeEquipmentJobPagination]
|
|
||||||
)
|
|
||||||
async def get_jobs(common: CommonParameters, location_tag: str, scope: Optional[str] = Query(None)):
|
|
||||||
"""Get all scope pagination."""
|
|
||||||
results = await get_all(common=common, location_tag=location_tag, scope=scope)
|
results = await get_all(common=common, location_tag=location_tag, scope=scope)
|
||||||
|
return StandardResponse(data=results, message='Data retrieved successfully')
|
||||||
|
|
||||||
return StandardResponse(
|
@router.post('/{assetnum}', response_model=StandardResponse[None])
|
||||||
data=results,
|
async def create_scope_equipment_jobs(db_session: DbSession, assetnum, scope_job_in: ScopeEquipmentJobCreate):
|
||||||
message="Data retrieved successfully",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{assetnum}", response_model=StandardResponse[None])
|
|
||||||
async def create_scope_equipment_jobs(
|
|
||||||
db_session: DbSession, assetnum, scope_job_in: ScopeEquipmentJobCreate
|
|
||||||
):
|
|
||||||
"""Get all scope activity pagination."""
|
|
||||||
await create(db_session=db_session, assetnum=assetnum, scope_job_in=scope_job_in)
|
await create(db_session=db_session, assetnum=assetnum, scope_job_in=scope_job_in)
|
||||||
|
return StandardResponse(data=None, message='Data created successfully')
|
||||||
|
|
||||||
return StandardResponse(
|
@router.post('/delete/{scope_job_id}', response_model=StandardResponse[None])
|
||||||
data=None,
|
|
||||||
message="Data created successfully",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/delete/{scope_job_id}", response_model=StandardResponse[None])
|
|
||||||
async def delete_scope_equipment_job(db_session: DbSession, scope_job_id):
|
async def delete_scope_equipment_job(db_session: DbSession, scope_job_id):
|
||||||
|
|
||||||
await delete(db_session=db_session, scope_job_id=scope_job_id)
|
await delete(db_session=db_session, scope_job_id=scope_job_id)
|
||||||
|
return StandardResponse(data=None, message='Data deleted successfully')
|
||||||
return StandardResponse(
|
|
||||||
data=None,
|
|
||||||
message="Data deleted successfully",
|
|
||||||
)
|
|
||||||
|
|||||||
@ -1,129 +1,47 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from fastapi import HTTPException, status
|
from fastapi import HTTPException, status
|
||||||
from sqlalchemy import Select
|
from sqlalchemy import Select
|
||||||
|
|
||||||
from src.database.core import DbSession
|
from src.database.core import DbSession
|
||||||
from src.database.service import search_filter_sort_paginate
|
from src.database.service import search_filter_sort_paginate
|
||||||
from src.standard_scope.model import MasterEquipment
|
from src.standard_scope.model import MasterEquipment
|
||||||
from src.workscope_group.model import MasterActivity
|
from src.workscope_group.model import MasterActivity
|
||||||
from src.workscope_group_maintenance_type.model import WorkscopeOHType
|
from src.workscope_group_maintenance_type.model import WorkscopeOHType
|
||||||
from src.overhaul_scope.model import MaintenanceType
|
from src.overhaul_scope.model import MaintenanceType
|
||||||
|
|
||||||
from .model import EquipmentWorkscopeGroup
|
from .model import EquipmentWorkscopeGroup
|
||||||
from .schema import ScopeEquipmentJobCreate
|
from .schema import ScopeEquipmentJobCreate
|
||||||
from src.soft_delete import apply_not_deleted_filter
|
from src.soft_delete import apply_not_deleted_filter
|
||||||
|
|
||||||
# async def get(*, db_session: DbSession, scope_equipment_activity_id: str) -> Optional[ScopeEquipmentActivity]:
|
async def get_all(*, common, location_tag: str, scope: Optional[str]=None):
|
||||||
# """Returns a document based on the given document id."""
|
query = Select(EquipmentWorkscopeGroup).where(EquipmentWorkscopeGroup.location_tag == location_tag).filter(EquipmentWorkscopeGroup.workscope_group_id.is_not(None))
|
||||||
|
|
||||||
|
|
||||||
# async def get_all(db_session: DbSession, assetnum: Optional[str], common):
|
|
||||||
# # Example usage
|
|
||||||
# if not assetnum:
|
|
||||||
|
|
||||||
# # First get the parent equipment
|
|
||||||
|
|
||||||
# if not equipment:
|
|
||||||
|
|
||||||
# # Build query for parts
|
|
||||||
# .where(ScopeEquipmentJob.assetnum == assetnum)
|
|
||||||
# .options(
|
|
||||||
# .selectinload(OverhaulJob.overhaul_activity)
|
|
||||||
# .selectinload(OverhaulActivity.overhaul_scope),
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def get_all(*, common, location_tag: str, scope: Optional[str] = None):
|
|
||||||
"""Returns all documents."""
|
|
||||||
query = (
|
|
||||||
Select(EquipmentWorkscopeGroup)
|
|
||||||
.where(EquipmentWorkscopeGroup.location_tag == location_tag).filter(EquipmentWorkscopeGroup.workscope_group_id.is_not(None))
|
|
||||||
)
|
|
||||||
query = apply_not_deleted_filter(query, EquipmentWorkscopeGroup)
|
query = apply_not_deleted_filter(query, EquipmentWorkscopeGroup)
|
||||||
|
|
||||||
if scope:
|
if scope:
|
||||||
query = (
|
query = query.join(EquipmentWorkscopeGroup.workscope_group).join(MasterActivity.oh_types).join(WorkscopeOHType.oh_type).filter(MaintenanceType.name == scope)
|
||||||
query
|
|
||||||
.join(EquipmentWorkscopeGroup.workscope_group)
|
|
||||||
.join(MasterActivity.oh_types)
|
|
||||||
.join(WorkscopeOHType.oh_type)
|
|
||||||
.filter(MaintenanceType.name == scope)
|
|
||||||
)
|
|
||||||
|
|
||||||
results = await search_filter_sort_paginate(model=query, **common)
|
results = await search_filter_sort_paginate(model=query, **common)
|
||||||
return results
|
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 = []
|
scope_jobs = []
|
||||||
|
|
||||||
if not assetnum:
|
if not assetnum:
|
||||||
raise ValueError("assetnum parameter is required")
|
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)
|
equipment: MasterEquipment = await db_session.scalar(equipment_stmt)
|
||||||
|
|
||||||
if not equipment:
|
if not equipment:
|
||||||
raise ValueError(f"No equipment found with assetnum: {assetnum}")
|
raise ValueError(f'No equipment found with assetnum: {assetnum}')
|
||||||
|
|
||||||
for job_id in scope_job_in.job_ids:
|
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)
|
scope_jobs.append(scope_equipment_job)
|
||||||
|
|
||||||
db_session.add_all(scope_jobs)
|
db_session.add_all(scope_jobs)
|
||||||
await db_session.commit()
|
await db_session.commit()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
async def delete(*, db_session: DbSession, scope_job_id: int) -> bool:
|
||||||
# async def update(*, db_session: DbSession, activity: ScopeEquipmentActivity, scope_equipment_activty_in: ScopeEquipmentActivityUpdate):
|
|
||||||
# """Updates a document."""
|
|
||||||
|
|
||||||
|
|
||||||
# for field in data:
|
|
||||||
# if field in update_data:
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def delete(
|
|
||||||
*,
|
|
||||||
db_session: DbSession,
|
|
||||||
scope_job_id: int,
|
|
||||||
) -> bool:
|
|
||||||
"""
|
|
||||||
Deletes a scope job and returns success status.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db_session: Database session
|
|
||||||
scope_job_id: ID of the scope job to delete
|
|
||||||
user_id: ID of user performing the deletion
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if deletion was successful, False otherwise
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
NotFoundException: If scope job doesn't exist
|
|
||||||
AuthorizationError: If user lacks delete permission
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
# Check if job exists
|
|
||||||
scope_job = await db_session.get(ScopeEquipmentJob, scope_job_id)
|
scope_job = await db_session.get(ScopeEquipmentJob, scope_job_id)
|
||||||
if not scope_job:
|
if not scope_job:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='A data with this id does not exist.')
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
detail="A data with this id does not exist.",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Perform deletion
|
|
||||||
await db_session.delete(scope_job)
|
await db_session.delete(scope_job)
|
||||||
await db_session.commit()
|
await db_session.commit()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
await db_session.rollback()
|
await db_session.rollback()
|
||||||
raise
|
raise
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
|
||||||
@ -0,0 +1 @@
|
|||||||
|
|
||||||
@ -1,72 +1,35 @@
|
|||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from src.auth.service import Token
|
from src.auth.service import Token
|
||||||
from src.database.core import CollectorDbSession, DbSession
|
from src.database.core import CollectorDbSession, DbSession
|
||||||
from src.models import StandardResponse
|
from src.models import StandardResponse
|
||||||
from src.overhaul.service import (get_overhaul_critical_parts,
|
from src.overhaul.service import get_overhaul_critical_parts, get_overhaul_overview, get_overhaul_schedules, get_overhaul_system_components
|
||||||
get_overhaul_overview,
|
|
||||||
get_overhaul_schedules,
|
|
||||||
get_overhaul_system_components)
|
|
||||||
from src.overhaul_scope.schema import ScopeRead
|
from src.overhaul_scope.schema import ScopeRead
|
||||||
|
from .schema import OverhaulCriticalParts, OverhaulRead, OverhaulSystemComponents
|
||||||
from .schema import (OverhaulCriticalParts, OverhaulRead,
|
|
||||||
OverhaulSystemComponents)
|
|
||||||
from src.auth.access_control import required_permission, OHPermission
|
from src.auth.access_control import required_permission, OHPermission
|
||||||
from src.overhaul_scope.model import OverhaulScope
|
from src.overhaul_scope.model import OverhaulScope
|
||||||
from fastapi import Depends
|
from fastapi import Depends
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.get('', response_model=StandardResponse[OverhaulRead], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
||||||
@router.get("", response_model=StandardResponse[OverhaulRead], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
async def get_overhaul(db_session: DbSession, token: Token, collector_db_session: CollectorDbSession):
|
||||||
async def get_overhaul(db_session: DbSession, token:Token, collector_db_session:CollectorDbSession):
|
|
||||||
"""Get all scope pagination."""
|
|
||||||
overview = await get_overhaul_overview(db_session=db_session)
|
overview = await get_overhaul_overview(db_session=db_session)
|
||||||
schedules = await get_overhaul_schedules(db_session=db_session)
|
schedules = await get_overhaul_schedules(db_session=db_session)
|
||||||
criticalParts = await get_overhaul_critical_parts(db_session=db_session, session_id=overview["overhaul"]["id"], token=token, collector_db_session=collector_db_session)
|
criticalParts = await get_overhaul_critical_parts(db_session=db_session, session_id=overview['overhaul']['id'], token=token, collector_db_session=collector_db_session)
|
||||||
systemComponents = get_overhaul_system_components()
|
systemComponents = get_overhaul_system_components()
|
||||||
|
return StandardResponse(data=OverhaulRead(overview=overview, schedules=schedules, criticalParts=criticalParts, systemComponents=systemComponents), message='Data retrieved successfully')
|
||||||
|
|
||||||
return StandardResponse(
|
@router.get('/schedules', response_model=StandardResponse[List[ScopeRead]], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
||||||
data=OverhaulRead(
|
|
||||||
overview=overview,
|
|
||||||
schedules=schedules,
|
|
||||||
criticalParts=criticalParts,
|
|
||||||
systemComponents=systemComponents,
|
|
||||||
),
|
|
||||||
message="Data retrieved successfully",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/schedules", response_model=StandardResponse[List[ScopeRead]], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
|
||||||
async def get_schedules():
|
async def get_schedules():
|
||||||
"""Get all overhaul schedules."""
|
|
||||||
schedules = get_overhaul_schedules()
|
schedules = get_overhaul_schedules()
|
||||||
return StandardResponse(
|
return StandardResponse(data=schedules, message='Data retrieved successfully')
|
||||||
data=schedules,
|
|
||||||
message="Data retrieved successfully",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
@router.get('/critical-parts', response_model=StandardResponse[OverhaulCriticalParts], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
||||||
@router.get("/critical-parts", response_model=StandardResponse[OverhaulCriticalParts], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
|
||||||
async def get_critical_parts():
|
async def get_critical_parts():
|
||||||
"""Get all overhaul critical parts."""
|
|
||||||
criticalParts = get_overhaul_critical_parts()
|
criticalParts = get_overhaul_critical_parts()
|
||||||
return StandardResponse(
|
return StandardResponse(data=OverhaulCriticalParts(criticalParts=criticalParts), message='Data retrieved successfully')
|
||||||
data=OverhaulCriticalParts(criticalParts=criticalParts),
|
|
||||||
message="Data retrieved successfully",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get('/system-components', response_model=StandardResponse[OverhaulSystemComponents], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
||||||
"/system-components", response_model=StandardResponse[OverhaulSystemComponents],
|
|
||||||
dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))]
|
|
||||||
)
|
|
||||||
async def get_system_components():
|
async def get_system_components():
|
||||||
"""Get all overhaul system components."""
|
|
||||||
systemComponents = get_overhaul_system_components()
|
systemComponents = get_overhaul_system_components()
|
||||||
return StandardResponse(
|
return StandardResponse(data=OverhaulSystemComponents(systemComponents=systemComponents), message='Data retrieved successfully')
|
||||||
data=OverhaulSystemComponents(systemComponents=systemComponents),
|
|
||||||
message="Data retrieved successfully",
|
|
||||||
)
|
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
|
||||||
@ -1,25 +1,11 @@
|
|||||||
from sqlalchemy import UUID, Column, Float, ForeignKey, String
|
from sqlalchemy import UUID, Column, Float, ForeignKey, String
|
||||||
|
|
||||||
from src.database.core import Base
|
from src.database.core import Base
|
||||||
from src.models import DefaultMixin
|
from src.models import DefaultMixin
|
||||||
|
|
||||||
|
|
||||||
class OverhaulActivity(Base, DefaultMixin):
|
class OverhaulActivity(Base, DefaultMixin):
|
||||||
__tablename__ = "oh_tr_overhaul_activity"
|
__tablename__ = 'oh_tr_overhaul_activity'
|
||||||
|
|
||||||
assetnum = Column(String, nullable=True)
|
assetnum = Column(String, nullable=True)
|
||||||
overhaul_scope_id = Column(
|
overhaul_scope_id = Column(UUID(as_uuid=True), ForeignKey('oh_ms_overhaul_scope.id'), nullable=False)
|
||||||
UUID(as_uuid=True), ForeignKey("oh_ms_overhaul_scope.id"), nullable=False
|
|
||||||
)
|
|
||||||
material_cost = Column(Float, nullable=False, default=0)
|
material_cost = Column(Float, nullable=False, default=0)
|
||||||
service_cost = Column(Float, nullable=False, default=0)
|
service_cost = Column(Float, nullable=False, default=0)
|
||||||
status = Column(String, nullable=False, default="pending")
|
status = Column(String, nullable=False, default='pending')
|
||||||
|
|
||||||
# "MasterEquipment",
|
|
||||||
|
|
||||||
# # "ScopeEquipmentPart",
|
|
||||||
|
|
||||||
|
|
||||||
# "OverhaulScope",
|
|
||||||
|
|
||||||
# "OverhaulJob", back_populates="overhaul_activity", lazy="raise"
|
|
||||||
|
|||||||
@ -1,103 +1,33 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||||
|
|
||||||
from src.csrf_protect import csrf_protect
|
from src.csrf_protect import csrf_protect
|
||||||
from src.database.core import CollectorDbSession
|
from src.database.core import CollectorDbSession
|
||||||
from src.database.service import (CommonParameters, DbSession)
|
from src.database.service import CommonParameters, DbSession
|
||||||
from src.models import StandardResponse
|
from src.models import StandardResponse
|
||||||
from src.auth.access_control import require_any_role, ALLOWED_ROLES
|
from src.auth.access_control import require_any_role, ALLOWED_ROLES
|
||||||
|
from .schema import OverhaulActivityCreate, OverhaulActivityRead
|
||||||
from .schema import (OverhaulActivityCreate, OverhaulActivityRead)
|
|
||||||
from .service import add_multiple_equipment_to_session, get, get_all, remove_equipment_from_session
|
from .service import add_multiple_equipment_to_session, get, get_all, remove_equipment_from_session
|
||||||
|
|
||||||
router = APIRouter(dependencies=[Depends(require_any_role(*ALLOWED_ROLES))])
|
router = APIRouter(dependencies=[Depends(require_any_role(*ALLOWED_ROLES))])
|
||||||
|
|
||||||
|
@router.get('/{overhaul_session}', response_model=StandardResponse[dict])
|
||||||
|
async def get_scope_equipments(common: CommonParameters, overhaul_session: str, collector_db: CollectorDbSession, location_tag: Optional[str]=Query(None), scope_name: Optional[str]=Query(None)):
|
||||||
|
data = await get_all(common=common, location_tag=location_tag, scope_name=scope_name, overhaul_session_id=overhaul_session, collector_db=collector_db)
|
||||||
|
return StandardResponse(data=data, message='Data retrieved successfully')
|
||||||
|
|
||||||
@router.get(
|
@router.post('/{overhaul_session_id}', response_model=StandardResponse[None])
|
||||||
"/{overhaul_session}", response_model=StandardResponse[dict]
|
async def create_overhaul_equipment(db_session: DbSession, collector_db_session: CollectorDbSession, overhaul_activty_in: OverhaulActivityCreate, overhaul_session_id: UUID):
|
||||||
)
|
await add_multiple_equipment_to_session(db_session=db_session, collector_db=collector_db_session, overhaul_session_id=overhaul_session_id, location_tags=overhaul_activty_in.location_tags)
|
||||||
async def get_scope_equipments(
|
return StandardResponse(data=None, message='Data created successfully')
|
||||||
common: CommonParameters,
|
|
||||||
overhaul_session: str,
|
|
||||||
collector_db: CollectorDbSession,
|
|
||||||
location_tag: Optional[str] = Query(None),
|
|
||||||
scope_name: Optional[str] = Query(None),
|
|
||||||
):
|
|
||||||
"""Get all scope activity pagination."""
|
|
||||||
data = await get_all(
|
|
||||||
common=common,
|
|
||||||
location_tag=location_tag,
|
|
||||||
scope_name=scope_name,
|
|
||||||
overhaul_session_id=overhaul_session,
|
|
||||||
collector_db=collector_db,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
return StandardResponse(
|
|
||||||
data=data,
|
|
||||||
message="Data retrieved successfully",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{overhaul_session_id}", response_model=StandardResponse[None])
|
|
||||||
async def create_overhaul_equipment(
|
|
||||||
db_session: DbSession,
|
|
||||||
collector_db_session: CollectorDbSession,
|
|
||||||
overhaul_activty_in: OverhaulActivityCreate,
|
|
||||||
overhaul_session_id: UUID,
|
|
||||||
):
|
|
||||||
|
|
||||||
await add_multiple_equipment_to_session(
|
|
||||||
db_session=db_session,
|
|
||||||
collector_db=collector_db_session,
|
|
||||||
overhaul_session_id=overhaul_session_id,
|
|
||||||
location_tags=overhaul_activty_in.location_tags
|
|
||||||
)
|
|
||||||
|
|
||||||
return StandardResponse(data=None, message="Data created successfully")
|
@router.get('/{overhaul_session}/{assetnum}', response_model=StandardResponse[OverhaulActivityRead])
|
||||||
|
async def get_overhaul_equipment(db_session: DbSession, assetnum: str, overhaul_session):
|
||||||
|
equipment = await get(db_session=db_session, assetnum=assetnum, overhaul_session_id=overhaul_session)
|
||||||
@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:
|
if not equipment:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='A data with this id does not exist.')
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
return StandardResponse(data=equipment, message='Data retrieved successfully')
|
||||||
detail="A data with this id does not exist.",
|
|
||||||
)
|
|
||||||
|
|
||||||
return StandardResponse(data=equipment, message="Data retrieved successfully")
|
|
||||||
|
|
||||||
|
|
||||||
# @router.put(
|
|
||||||
# async def update_scope(
|
|
||||||
# db_session: DbSession,
|
|
||||||
# scope_equipment_activity_in: OverhaulActivityUpdate,
|
|
||||||
# assetnum: str,
|
|
||||||
# ):
|
|
||||||
|
|
||||||
# if not activity:
|
@router.post('/delete/{overhaul_session}/{location_tag}', response_model=StandardResponse[None], dependencies=[Depends(csrf_protect)])
|
||||||
# raise HTTPException(
|
async def delete_scope(db_session: DbSession, location_tag: str, overhaul_session: UUID):
|
||||||
|
|
||||||
# return StandardResponse(
|
|
||||||
# ),
|
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
|
||||||
"/delete/{overhaul_session}/{location_tag}",
|
|
||||||
response_model=StandardResponse[None],
|
|
||||||
dependencies=[Depends(csrf_protect)],
|
|
||||||
)
|
|
||||||
async def delete_scope(db_session: DbSession, location_tag: str, overhaul_session:UUID):
|
|
||||||
await remove_equipment_from_session(db_session=db_session, overhaul_session_id=overhaul_session, location_tag=location_tag)
|
await remove_equipment_from_session(db_session=db_session, overhaul_session_id=overhaul_session, location_tag=location_tag)
|
||||||
|
return StandardResponse(message='Data deleted successfully', data=None)
|
||||||
return StandardResponse(message="Data deleted successfully", data=None)
|
|
||||||
|
|||||||
@ -1,37 +1,26 @@
|
|||||||
from decimal import Decimal, getcontext
|
from decimal import Decimal, getcontext
|
||||||
|
|
||||||
|
|
||||||
def get_material_cost(scope, total_equipment):
|
def get_material_cost(scope, total_equipment):
|
||||||
# Set precision to 28 digits (maximum precision for Decimal)
|
|
||||||
getcontext().prec = 28
|
getcontext().prec = 28
|
||||||
|
if not total_equipment:
|
||||||
if not total_equipment: # Guard against division by zero
|
|
||||||
return float(0)
|
return float(0)
|
||||||
|
|
||||||
cost = 365539731101 / 10
|
cost = 365539731101 / 10
|
||||||
|
if scope == 'B':
|
||||||
if scope == "B":
|
result = Decimal(f'{cost}') / Decimal(str(total_equipment))
|
||||||
result = Decimal(f"{cost}") / Decimal(str(total_equipment))
|
|
||||||
return float(result)
|
return float(result)
|
||||||
else:
|
else:
|
||||||
result = Decimal("8565468127") / Decimal(str(total_equipment))
|
result = Decimal('8565468127') / Decimal(str(total_equipment))
|
||||||
return float(result)
|
return float(result)
|
||||||
|
|
||||||
return float(0)
|
return float(0)
|
||||||
|
|
||||||
|
|
||||||
def get_service_cost(scope, total_equipment):
|
def get_service_cost(scope, total_equipment):
|
||||||
# Set precision to 28 digits (maximum precision for Decimal)
|
|
||||||
getcontext().prec = 28
|
getcontext().prec = 28
|
||||||
|
if not total_equipment:
|
||||||
if not total_equipment: # Guard against division by zero
|
|
||||||
return float(0)
|
return float(0)
|
||||||
|
if scope == 'B':
|
||||||
if scope == "B":
|
result = Decimal('36405830225') / Decimal(str(total_equipment))
|
||||||
result = Decimal("36405830225") / Decimal(str(total_equipment))
|
|
||||||
return float(result)
|
return float(result)
|
||||||
else:
|
else:
|
||||||
result = Decimal("36000000000") / Decimal(str(total_equipment))
|
result = Decimal('36000000000') / Decimal(str(total_equipment))
|
||||||
return float(result)
|
return float(result)
|
||||||
|
|
||||||
return float(0)
|
return float(0)
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
|
||||||
@ -1,17 +1,8 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
from sqlalchemy import Column, String
|
from sqlalchemy import Column, String
|
||||||
from src.database.core import Base
|
from src.database.core import Base
|
||||||
from src.models import DefaultMixin
|
from src.models import DefaultMixin
|
||||||
|
|
||||||
|
|
||||||
class OverhaulGantt(Base, DefaultMixin):
|
class OverhaulGantt(Base, DefaultMixin):
|
||||||
__tablename__ = "oh_ms_monitoring_spreadsheet"
|
__tablename__ = 'oh_ms_monitoring_spreadsheet'
|
||||||
|
|
||||||
spreadsheet_id = Column(String, nullable=True)
|
spreadsheet_id = Column(String, nullable=True)
|
||||||
spreadsheet_link = Column(String, nullable=True)
|
spreadsheet_link = Column(String, nullable=True)
|
||||||
|
|
||||||
@ -1,123 +1,46 @@
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
|
|
||||||
from src.auth.service import Admin
|
from src.auth.service import Admin
|
||||||
from src.csrf_protect import csrf_protect
|
from src.csrf_protect import csrf_protect
|
||||||
from src.database.core import DbSession
|
from src.database.core import DbSession
|
||||||
from src.models import StandardResponse
|
from src.models import StandardResponse
|
||||||
from src.overhaul_gantt.model import OverhaulGantt
|
from src.overhaul_gantt.model import OverhaulGantt
|
||||||
from src.overhaul_gantt.schema import OverhaulGanttIn
|
from src.overhaul_gantt.schema import OverhaulGanttIn
|
||||||
|
|
||||||
from .service import get_gantt_performance_chart
|
from .service import get_gantt_performance_chart
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.get('', response_model=StandardResponse[dict])
|
||||||
@router.get(
|
|
||||||
"", response_model=StandardResponse[dict]
|
|
||||||
)
|
|
||||||
async def get_gantt_performance(db_session: DbSession):
|
async def get_gantt_performance(db_session: DbSession):
|
||||||
"""Get all scope pagination."""
|
|
||||||
query = select(OverhaulGantt).limit(1)
|
query = select(OverhaulGantt).limit(1)
|
||||||
|
|
||||||
data = (await db_session.execute(query)).scalar_one_or_none()
|
data = (await db_session.execute(query)).scalar_one_or_none()
|
||||||
|
|
||||||
results, gantt_data = await get_gantt_performance_chart(spreadsheet_id=data.spreadsheet_id)
|
results, gantt_data = await get_gantt_performance_chart(spreadsheet_id=data.spreadsheet_id)
|
||||||
|
return StandardResponse(data={'chart_data': results, 'gantt_data': gantt_data}, message='Data retrieved successfully')
|
||||||
|
|
||||||
return StandardResponse(
|
@router.get('/spreadsheet', response_model=StandardResponse[dict])
|
||||||
data={
|
|
||||||
"chart_data": results,
|
|
||||||
"gantt_data": gantt_data
|
|
||||||
},
|
|
||||||
message="Data retrieved successfully",
|
|
||||||
)
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/spreadsheet", response_model=StandardResponse[dict]
|
|
||||||
)
|
|
||||||
async def get_gantt_spreadsheet(db_session: DbSession):
|
async def get_gantt_spreadsheet(db_session: DbSession):
|
||||||
"""Get all scope pagination."""
|
|
||||||
query = select(OverhaulGantt).limit(1)
|
query = select(OverhaulGantt).limit(1)
|
||||||
|
|
||||||
data = (await db_session.execute(query)).scalar_one_or_none()
|
data = (await db_session.execute(query)).scalar_one_or_none()
|
||||||
result = {
|
result = {'spreadsheet_id': None, 'spreadsheet_link': None}
|
||||||
"spreadsheet_id": None,
|
|
||||||
"spreadsheet_link": None
|
|
||||||
}
|
|
||||||
|
|
||||||
if data:
|
if data:
|
||||||
result = {
|
result = {'spreadsheet_id': data.spreadsheet_id, 'spreadsheet_link': data.spreadsheet_link}
|
||||||
"spreadsheet_id": data.spreadsheet_id,
|
return StandardResponse(data=result, message='Data retrieved successfully')
|
||||||
"spreadsheet_link": data.spreadsheet_link
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return StandardResponse(
|
@router.post('/spreadsheet', response_model=StandardResponse[dict], dependencies=[Depends(csrf_protect)])
|
||||||
data=result,
|
|
||||||
message="Data retrieved successfully",
|
|
||||||
)
|
|
||||||
|
|
||||||
@router.post(
|
|
||||||
"/spreadsheet", response_model=StandardResponse[dict], dependencies=[Depends(csrf_protect)]
|
|
||||||
)
|
|
||||||
async def update_gantt_spreadsheet(db_session: DbSession, spreadsheet_in: OverhaulGanttIn, admin: Admin):
|
async def update_gantt_spreadsheet(db_session: DbSession, spreadsheet_in: OverhaulGanttIn, admin: Admin):
|
||||||
"""Get all scope pagination."""
|
match = re.search('/d/([a-zA-Z0-9-_]+)', spreadsheet_in.spreadsheet_link)
|
||||||
|
|
||||||
match = re.search(r"/d/([a-zA-Z0-9-_]+)", spreadsheet_in.spreadsheet_link)
|
|
||||||
if not match:
|
if not match:
|
||||||
raise ValueError("Invalid Google Sheets URL")
|
raise ValueError('Invalid Google Sheets URL')
|
||||||
|
|
||||||
spreadsheet_id = match.group(1)
|
spreadsheet_id = match.group(1)
|
||||||
|
|
||||||
query = select(OverhaulGantt).limit(1)
|
query = select(OverhaulGantt).limit(1)
|
||||||
|
|
||||||
data = (await db_session.execute(query)).scalar_one_or_none()
|
data = (await db_session.execute(query)).scalar_one_or_none()
|
||||||
if data:
|
if data:
|
||||||
data.spreadsheet_link = spreadsheet_in.spreadsheet_link
|
data.spreadsheet_link = spreadsheet_in.spreadsheet_link
|
||||||
data.spreadsheet_id = spreadsheet_id
|
data.spreadsheet_id = spreadsheet_id
|
||||||
else:
|
else:
|
||||||
spreadsheet = OverhaulGantt(
|
spreadsheet = OverhaulGantt(spreadsheet_id=spreadsheet_id, spreadsheet_link=spreadsheet_in.spreadsheet_link)
|
||||||
spreadsheet_id=spreadsheet_id,
|
|
||||||
spreadsheet_link=spreadsheet_in.spreadsheet_link
|
|
||||||
)
|
|
||||||
db_session.add(spreadsheet)
|
db_session.add(spreadsheet)
|
||||||
|
|
||||||
|
|
||||||
await db_session.commit()
|
await db_session.commit()
|
||||||
|
|
||||||
if data:
|
if data:
|
||||||
result = {
|
result = {'spreadsheet_id': spreadsheet_id}
|
||||||
"spreadsheet_id": spreadsheet_id
|
return StandardResponse(data=result, message='Data retrieved successfully')
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return StandardResponse(
|
|
||||||
data=result,
|
|
||||||
message="Data retrieved successfully",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# @router.post("", response_model=StandardResponse[None])
|
|
||||||
# async def create_overhaul_equipment_jobs(
|
|
||||||
# db_session: DbSession, overhaul_job_in: OverhaulScheduleCreate
|
|
||||||
# ):
|
|
||||||
# await create(
|
|
||||||
|
|
||||||
# return StandardResponse(
|
|
||||||
|
|
||||||
# @router.put("/{overhaul_job_id}", response_model=StandardResponse[None])
|
|
||||||
# async def update_overhaul_schedule(
|
|
||||||
# db_session: DbSession, overhaul_job_id: str, overhaul_job_in: OverhaulScheduleUpdate
|
|
||||||
# ):
|
|
||||||
|
|
||||||
# return StandardResponse(
|
|
||||||
|
|
||||||
# @router.delete("/{overhaul_job_id}", response_model=StandardResponse[None])
|
|
||||||
# async def delete_overhaul_equipment_job(db_session: DbSession, overhaul_job_id):
|
|
||||||
|
|
||||||
# return StandardResponse(
|
|
||||||
|
|||||||
@ -1,91 +1,34 @@
|
|||||||
|
|
||||||
from fastapi import HTTPException, status
|
from fastapi import HTTPException, status
|
||||||
|
|
||||||
from .utils import fetch_all_sections, get_google_creds, get_spreatsheed_service, process_spreadsheet_data
|
from .utils import fetch_all_sections, get_google_creds, get_spreatsheed_service, process_spreadsheet_data
|
||||||
|
|
||||||
# async def get_all(*, common):
|
async def get_gantt_performance_chart(*, spreadsheet_id='1gZXuwA97zU1v4QBv56wKeiqadc6skHUucGKYG8qVFRk'):
|
||||||
# """Returns all documents."""
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# async def create(
|
|
||||||
# *, db_session: DbSession, overhaul_job_in: OverhaulScheduleCreate
|
|
||||||
# ):
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# async def update(*, db_session: DbSession, overhaul_schedule_id: str, overhaul_job_in: OverhaulScheduleUpdate):
|
|
||||||
# """Updates a document."""
|
|
||||||
|
|
||||||
|
|
||||||
# for field in data:
|
|
||||||
# if field in update_data:
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# async def delete(*, db_session: DbSession, overhaul_schedule_id: str):
|
|
||||||
# """Deletes a document."""
|
|
||||||
|
|
||||||
|
|
||||||
async def get_gantt_performance_chart(*, spreadsheet_id = "1gZXuwA97zU1v4QBv56wKeiqadc6skHUucGKYG8qVFRk"):
|
|
||||||
creds = get_google_creds()
|
creds = get_google_creds()
|
||||||
RANGE_NAME = "'SUMMARY'!K34:AZ38" # Or just "2024 schedule"
|
RANGE_NAME = "'SUMMARY'!K34:AZ38"
|
||||||
GANTT_DATA_NAME = "ACTUAL PROGRESS"
|
GANTT_DATA_NAME = 'ACTUAL PROGRESS'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
service = get_spreatsheed_service(creds)
|
service = get_spreatsheed_service(creds)
|
||||||
sheet = service.spreadsheets()
|
sheet = service.spreadsheets()
|
||||||
|
response = sheet.values().get(spreadsheetId=spreadsheet_id, range=RANGE_NAME).execute()
|
||||||
response = sheet.values().get(
|
values = response.get('values', [])
|
||||||
spreadsheetId=spreadsheet_id,
|
|
||||||
range=RANGE_NAME
|
|
||||||
).execute()
|
|
||||||
|
|
||||||
values = response.get("values", [])
|
|
||||||
|
|
||||||
if len(values) < 4:
|
if len(values) < 4:
|
||||||
raise Exception("Spreadsheet format invalid: need 4 rows (DAY, DATE, PLAN, ACTUAL).")
|
raise Exception('Spreadsheet format invalid: need 4 rows (DAY, DATE, PLAN, ACTUAL).')
|
||||||
|
day_row = values[0][1:]
|
||||||
# Extract rows
|
date_row = values[1][1:]
|
||||||
day_row = values[0][1:]
|
plan_row = values[3][1:]
|
||||||
date_row = values[1][1:]
|
|
||||||
plan_row = values[3][1:]
|
|
||||||
actual_row = values[4][1:]
|
actual_row = values[4][1:]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
total_days = len(day_row)
|
total_days = len(day_row)
|
||||||
|
date_row += [''] * (total_days - len(date_row))
|
||||||
# PAD rows so lengths match day count
|
plan_row += [''] * (total_days - len(plan_row))
|
||||||
date_row += [""] * (total_days - len(date_row))
|
actual_row += [''] * (total_days - len(actual_row))
|
||||||
plan_row += [""] * (total_days - len(plan_row))
|
|
||||||
actual_row += [""] * (total_days - len(actual_row))
|
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
for i in range(total_days):
|
for i in range(total_days):
|
||||||
|
|
||||||
day = day_row[i]
|
day = day_row[i]
|
||||||
date = date_row[i]
|
date = date_row[i]
|
||||||
plan = plan_row[i]
|
plan = plan_row[i]
|
||||||
actual = actual_row[i] if actual_row[i] else "0%" # <-- FIX HERE
|
actual = actual_row[i] if actual_row[i] else '0%'
|
||||||
|
results.append({'day': day, 'date': date, 'plan': plan, 'actual': actual})
|
||||||
results.append({
|
|
||||||
"day": day,
|
|
||||||
"date": date,
|
|
||||||
"plan": plan,
|
|
||||||
"actual": actual
|
|
||||||
})
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
processed_data = process_spreadsheet_data(results)
|
processed_data = process_spreadsheet_data(results)
|
||||||
gantt_data = fetch_all_sections(service=service, spreadsheet_id=spreadsheet_id, sheet_name=GANTT_DATA_NAME)
|
gantt_data = fetch_all_sections(service=service, spreadsheet_id=spreadsheet_id, sheet_name=GANTT_DATA_NAME)
|
||||||
|
return (processed_data, gantt_data)
|
||||||
|
|
||||||
return processed_data, gantt_data
|
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
|
||||||
@ -1,74 +1,23 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from fastapi import APIRouter, Query
|
from fastapi import APIRouter, Query
|
||||||
|
|
||||||
from src.database.core import DbSession
|
from src.database.core import DbSession
|
||||||
from src.database.service import CommonParameters
|
from src.database.service import CommonParameters
|
||||||
from src.models import StandardResponse
|
from src.models import StandardResponse
|
||||||
|
from .schema import OverhaulJobCreate, OverhaulJobPagination
|
||||||
from .schema import (OverhaulJobCreate, OverhaulJobPagination)
|
|
||||||
from .service import create, delete, get_all
|
from .service import create, delete, get_all
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.get('/{location_tag}', response_model=StandardResponse[OverhaulJobPagination])
|
||||||
@router.get(
|
async def get_jobs(common: CommonParameters, location_tag: str, scope: Optional[str]=Query(None)):
|
||||||
"/{location_tag}", response_model=StandardResponse[OverhaulJobPagination]
|
|
||||||
)
|
|
||||||
async def get_jobs(common: CommonParameters, location_tag: str, scope: Optional[str] = Query(None)):
|
|
||||||
"""Get all scope pagination."""
|
|
||||||
results = await get_all(common=common, location_tag=location_tag, scope=scope)
|
results = await get_all(common=common, location_tag=location_tag, scope=scope)
|
||||||
|
return StandardResponse(data=results, message='Data retrieved successfully')
|
||||||
|
|
||||||
return StandardResponse(
|
@router.post('/{overhaul_equipment_id}', response_model=StandardResponse[None])
|
||||||
data=results,
|
async def create_overhaul_equipment_jobs(db_session: DbSession, overhaul_equipment_id, overhaul_job_in: OverhaulJobCreate):
|
||||||
message="Data retrieved successfully",
|
await create(db_session=db_session, overhaul_equipment_id=overhaul_equipment_id, overhaul_job_in=overhaul_job_in)
|
||||||
)
|
return StandardResponse(data=None, message='Data created successfully')
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{overhaul_equipment_id}", response_model=StandardResponse[None])
|
@router.post('/delete/{overhaul_job_id}', response_model=StandardResponse[None])
|
||||||
async def create_overhaul_equipment_jobs(
|
|
||||||
db_session: DbSession, overhaul_equipment_id, overhaul_job_in: OverhaulJobCreate
|
|
||||||
):
|
|
||||||
"""Get all scope activity pagination."""
|
|
||||||
await create(
|
|
||||||
db_session=db_session,
|
|
||||||
overhaul_equipment_id=overhaul_equipment_id,
|
|
||||||
overhaul_job_in=overhaul_job_in,
|
|
||||||
)
|
|
||||||
|
|
||||||
return StandardResponse(
|
|
||||||
data=None,
|
|
||||||
message="Data created successfully",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/delete/{overhaul_job_id}", response_model=StandardResponse[None])
|
|
||||||
async def delete_overhaul_equipment_job(db_session: DbSession, overhaul_job_id):
|
async def delete_overhaul_equipment_job(db_session: DbSession, overhaul_job_id):
|
||||||
await delete(db_session=db_session, overhaul_job_id=overhaul_job_id)
|
await delete(db_session=db_session, overhaul_job_id=overhaul_job_id)
|
||||||
|
return StandardResponse(data=None, message='Data deleted successfully')
|
||||||
return StandardResponse(
|
|
||||||
data=None,
|
|
||||||
message="Data deleted successfully",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# @router.post("", response_model=StandardResponse[List[str]])
|
|
||||||
# async def create_scope(db_session: DbSession, scope_in: OverhaulJobCreate):
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# @router.put("/{scope_id}", response_model=StandardResponse[ScopeRead])
|
|
||||||
# async def update_scope(db_session: DbSession, scope_id: str, scope_in: ScopeUpdate, current_user: CurrentUser):
|
|
||||||
|
|
||||||
# if not scope:
|
|
||||||
# raise HTTPException(
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# @router.delete("/{scope_id}", response_model=StandardResponse[ScopeRead])
|
|
||||||
# async def delete_scope(db_session: DbSession, scope_id: str):
|
|
||||||
|
|
||||||
# if not scope:
|
|
||||||
# raise HTTPException(
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
|
||||||
@ -1,17 +1,13 @@
|
|||||||
from sqlalchemy import (Column, DateTime, Integer,
|
from sqlalchemy import Column, DateTime, Integer, String
|
||||||
String)
|
|
||||||
|
|
||||||
from src.database.core import Base
|
from src.database.core import Base
|
||||||
from src.models import DefaultMixin
|
from src.models import DefaultMixin
|
||||||
|
|
||||||
|
|
||||||
class OverhaulSchedule(Base, DefaultMixin):
|
class OverhaulSchedule(Base, DefaultMixin):
|
||||||
__tablename__ = "rp_oh_schedule"
|
__tablename__ = 'rp_oh_schedule'
|
||||||
|
|
||||||
year = Column(Integer, nullable=False)
|
year = Column(Integer, nullable=False)
|
||||||
plan_duration = Column(Integer, nullable=True)
|
plan_duration = Column(Integer, nullable=True)
|
||||||
planned_outage = Column(Integer, nullable=True)
|
planned_outage = Column(Integer, nullable=True)
|
||||||
actual_shutdown = Column(Integer, nullable=True)
|
actual_shutdown = Column(Integer, nullable=True)
|
||||||
start = Column(DateTime(timezone=True)) # This will be TIMESTAMP WITH TIME ZONE
|
start = Column(DateTime(timezone=True))
|
||||||
finish = Column(DateTime(timezone=True))
|
finish = Column(DateTime(timezone=True))
|
||||||
remark = Column(String, nullable=True)
|
remark = Column(String, nullable=True)
|
||||||
|
|||||||
@ -1,60 +1,27 @@
|
|||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from src.database.core import DbSession
|
from src.database.core import DbSession
|
||||||
from src.database.service import CommonParameters
|
from src.database.service import CommonParameters
|
||||||
from src.models import StandardResponse
|
from src.models import StandardResponse
|
||||||
|
from .schema import OverhaulScheduleCreate, OverhaulSchedulePagination, OverhaulScheduleUpdate
|
||||||
from .schema import (OverhaulScheduleCreate, OverhaulSchedulePagination, OverhaulScheduleUpdate)
|
|
||||||
from .service import create, get_all, delete, update
|
from .service import create, get_all, delete, update
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.get('', response_model=StandardResponse[OverhaulSchedulePagination])
|
||||||
@router.get(
|
|
||||||
"", response_model=StandardResponse[OverhaulSchedulePagination]
|
|
||||||
)
|
|
||||||
async def get_schedules(common: CommonParameters):
|
async def get_schedules(common: CommonParameters):
|
||||||
"""Get all scope pagination."""
|
|
||||||
results = await get_all(common=common)
|
results = await get_all(common=common)
|
||||||
|
return StandardResponse(data=results, message='Data retrieved successfully')
|
||||||
|
|
||||||
|
@router.post('', response_model=StandardResponse[None])
|
||||||
|
async def create_overhaul_equipment_jobs(db_session: DbSession, overhaul_job_in: OverhaulScheduleCreate):
|
||||||
|
await create(db_session=db_session, overhaul_job_in=overhaul_job_in)
|
||||||
|
return StandardResponse(data=None, message='Data created successfully')
|
||||||
|
|
||||||
return StandardResponse(
|
@router.post('/update/{overhaul_job_id}', response_model=StandardResponse[None])
|
||||||
data=results,
|
async def update_overhaul_schedule(db_session: DbSession, overhaul_job_id: str, overhaul_job_in: OverhaulScheduleUpdate):
|
||||||
message="Data retrieved successfully",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("", response_model=StandardResponse[None])
|
|
||||||
async def create_overhaul_equipment_jobs(
|
|
||||||
db_session: DbSession, overhaul_job_in: OverhaulScheduleCreate
|
|
||||||
):
|
|
||||||
await create(
|
|
||||||
db_session=db_session,
|
|
||||||
overhaul_job_in=overhaul_job_in,
|
|
||||||
)
|
|
||||||
|
|
||||||
return StandardResponse(
|
|
||||||
data=None,
|
|
||||||
message="Data created successfully",
|
|
||||||
)
|
|
||||||
|
|
||||||
@router.post("/update/{overhaul_job_id}", response_model=StandardResponse[None])
|
|
||||||
async def update_overhaul_schedule(
|
|
||||||
db_session: DbSession, overhaul_job_id: str, overhaul_job_in: OverhaulScheduleUpdate
|
|
||||||
):
|
|
||||||
await update(db_session=db_session, overhaul_schedule_id=overhaul_job_id, overhaul_job_in=overhaul_job_in)
|
await update(db_session=db_session, overhaul_schedule_id=overhaul_job_id, overhaul_job_in=overhaul_job_in)
|
||||||
|
return StandardResponse(data=None, message='Data updated successfully')
|
||||||
|
|
||||||
return StandardResponse(
|
@router.post('/delete/{overhaul_job_id}', response_model=StandardResponse[None])
|
||||||
data=None,
|
|
||||||
message="Data updated successfully",
|
|
||||||
)
|
|
||||||
|
|
||||||
@router.post("/delete/{overhaul_job_id}", response_model=StandardResponse[None])
|
|
||||||
async def delete_overhaul_equipment_job(db_session: DbSession, overhaul_job_id):
|
async def delete_overhaul_equipment_job(db_session: DbSession, overhaul_job_id):
|
||||||
await delete(db_session=db_session, overhaul_schedule_id=overhaul_job_id)
|
await delete(db_session=db_session, overhaul_schedule_id=overhaul_job_id)
|
||||||
|
return StandardResponse(data=None, message='Data deleted successfully')
|
||||||
return StandardResponse(
|
|
||||||
data=None,
|
|
||||||
message="Data deleted successfully",
|
|
||||||
)
|
|
||||||
|
|||||||
@ -1,54 +1,34 @@
|
|||||||
|
|
||||||
from sqlalchemy import Select
|
from sqlalchemy import Select
|
||||||
|
|
||||||
from src.database.core import DbSession
|
from src.database.core import DbSession
|
||||||
from src.database.service import search_filter_sort_paginate
|
from src.database.service import search_filter_sort_paginate
|
||||||
from src.utils import update_model
|
from src.utils import update_model
|
||||||
from src.soft_delete import apply_not_deleted_filter, soft_delete_record
|
from src.soft_delete import apply_not_deleted_filter, soft_delete_record
|
||||||
|
|
||||||
from .model import OverhaulSchedule
|
from .model import OverhaulSchedule
|
||||||
from .schema import OverhaulScheduleCreate, OverhaulScheduleUpdate
|
from .schema import OverhaulScheduleCreate, OverhaulScheduleUpdate
|
||||||
|
|
||||||
|
|
||||||
async def get_all(*, common):
|
async def get_all(*, common):
|
||||||
"""Returns all documents."""
|
|
||||||
query = Select(OverhaulSchedule).order_by(OverhaulSchedule.start.desc())
|
query = Select(OverhaulSchedule).order_by(OverhaulSchedule.start.desc())
|
||||||
query = apply_not_deleted_filter(query, OverhaulSchedule)
|
query = apply_not_deleted_filter(query, OverhaulSchedule)
|
||||||
|
|
||||||
results = await search_filter_sort_paginate(model=query, **common)
|
results = await search_filter_sort_paginate(model=query, **common)
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
async def create(*, db_session: DbSession, overhaul_job_in: OverhaulScheduleCreate):
|
||||||
async def create(
|
|
||||||
*, db_session: DbSession, overhaul_job_in: OverhaulScheduleCreate
|
|
||||||
):
|
|
||||||
|
|
||||||
|
|
||||||
schedule = OverhaulSchedule(**overhaul_job_in.model_dump())
|
schedule = OverhaulSchedule(**overhaul_job_in.model_dump())
|
||||||
db_session.add(schedule)
|
db_session.add(schedule)
|
||||||
await db_session.commit()
|
await db_session.commit()
|
||||||
return schedule
|
return schedule
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def update(*, db_session: DbSession, overhaul_schedule_id: str, overhaul_job_in: OverhaulScheduleUpdate):
|
async def update(*, db_session: DbSession, overhaul_schedule_id: str, overhaul_job_in: OverhaulScheduleUpdate):
|
||||||
"""Updates a document."""
|
|
||||||
overhaul_job_in.model_dump()
|
overhaul_job_in.model_dump()
|
||||||
query = Select(OverhaulSchedule).where(OverhaulSchedule.id == overhaul_schedule_id)
|
query = Select(OverhaulSchedule).where(OverhaulSchedule.id == overhaul_schedule_id)
|
||||||
query = apply_not_deleted_filter(query, OverhaulSchedule)
|
query = apply_not_deleted_filter(query, OverhaulSchedule)
|
||||||
query = query.with_for_update()
|
query = query.with_for_update()
|
||||||
result = await db_session.execute(query)
|
result = await db_session.execute(query)
|
||||||
overhaul_schedule = result.scalars().one_or_none()
|
overhaul_schedule = result.scalars().one_or_none()
|
||||||
|
|
||||||
update_data = overhaul_job_in.model_dump(exclude_defaults=True)
|
update_data = overhaul_job_in.model_dump(exclude_defaults=True)
|
||||||
|
|
||||||
update_model(overhaul_schedule, update_data)
|
update_model(overhaul_schedule, update_data)
|
||||||
|
|
||||||
await db_session.commit()
|
await db_session.commit()
|
||||||
|
|
||||||
return overhaul_schedule
|
return overhaul_schedule
|
||||||
|
|
||||||
|
|
||||||
async def delete(*, db_session: DbSession, overhaul_schedule_id: str):
|
async def delete(*, db_session: DbSession, overhaul_schedule_id: str):
|
||||||
"""Soft-deletes a document."""
|
|
||||||
await soft_delete_record(db_session=db_session, model=OverhaulSchedule, record_id=overhaul_schedule_id)
|
await soft_delete_record(db_session=db_session, model=OverhaulSchedule, record_id=overhaul_schedule_id)
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
|
||||||
@ -1,47 +1,29 @@
|
|||||||
from sqlalchemy import JSON
|
from sqlalchemy import JSON
|
||||||
from sqlalchemy import Column, DateTime , Integer, String, ForeignKey, UUID
|
from sqlalchemy import Column, DateTime, Integer, String, ForeignKey, UUID
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
from src.database.core import Base
|
from src.database.core import Base
|
||||||
from src.models import DefaultMixin
|
from src.models import DefaultMixin
|
||||||
|
|
||||||
|
|
||||||
from src.auth.access_control import Allow, RolePrincipal, OHPermission
|
from src.auth.access_control import Allow, RolePrincipal, OHPermission
|
||||||
|
|
||||||
class OverhaulScope(Base, DefaultMixin):
|
class OverhaulScope(Base, DefaultMixin):
|
||||||
__tablename__ = "oh_ms_overhaul"
|
__tablename__ = 'oh_ms_overhaul'
|
||||||
start_date = Column(DateTime(timezone=True), nullable=False) # Made non-nullable to match model
|
start_date = Column(DateTime(timezone=True), nullable=False)
|
||||||
end_date = Column(DateTime(timezone=True), nullable=True) # Already nullable
|
end_date = Column(DateTime(timezone=True), nullable=True)
|
||||||
duration_oh = Column(Integer, nullable=True)
|
duration_oh = Column(Integer, nullable=True)
|
||||||
crew_number = Column(Integer, nullable=True, default=1)
|
crew_number = Column(Integer, nullable=True, default=1)
|
||||||
status = Column(String, nullable=False, default="Upcoming")
|
status = Column(String, nullable=False, default='Upcoming')
|
||||||
maintenance_type_id = Column(
|
maintenance_type_id = Column(UUID(as_uuid=True), ForeignKey('oh_ms_maintenance_type.id'), nullable=False)
|
||||||
UUID(as_uuid=True), ForeignKey("oh_ms_maintenance_type.id"), nullable=False)
|
|
||||||
wo_parent = Column(JSON, nullable=True)
|
wo_parent = Column(JSON, nullable=True)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def __acl__(self):
|
def __acl__(self):
|
||||||
basic_permissions = [OHPermission.READ]
|
basic_permissions = [OHPermission.READ]
|
||||||
engineer_permissions = [
|
engineer_permissions = [OHPermission.READ, OHPermission.CREATE, OHPermission.EDIT, OHPermission.DELETE]
|
||||||
OHPermission.READ,
|
|
||||||
OHPermission.CREATE,
|
|
||||||
OHPermission.EDIT,
|
|
||||||
OHPermission.DELETE,
|
|
||||||
]
|
|
||||||
all_permissions = list(OHPermission)
|
all_permissions = list(OHPermission)
|
||||||
|
return [(Allow, RolePrincipal('Management'), basic_permissions), (Allow, RolePrincipal('Engineer'), engineer_permissions), (Allow, RolePrincipal('Admin'), all_permissions), (Allow, RolePrincipal('Application Administrator'), all_permissions)]
|
||||||
return [
|
maintenance_type = relationship('MaintenanceType', lazy='selectin', backref='overhaul_scopes')
|
||||||
(Allow, RolePrincipal("Management"), basic_permissions),
|
|
||||||
(Allow, RolePrincipal("Engineer"), engineer_permissions),
|
|
||||||
(Allow, RolePrincipal("Admin"), all_permissions),
|
|
||||||
(Allow, RolePrincipal("Application Administrator"), all_permissions),
|
|
||||||
]
|
|
||||||
|
|
||||||
maintenance_type = relationship("MaintenanceType", lazy="selectin", backref="overhaul_scopes")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class MaintenanceType(Base, DefaultMixin):
|
class MaintenanceType(Base, DefaultMixin):
|
||||||
__tablename__ = "oh_ms_maintenance_type"
|
__tablename__ = 'oh_ms_maintenance_type'
|
||||||
code = Column(String, nullable=False, default="OH")
|
code = Column(String, nullable=False, default='OH')
|
||||||
name = Column(String, nullable=False)
|
name = Column(String, nullable=False)
|
||||||
|
|||||||
@ -1,81 +1,47 @@
|
|||||||
from typing import List
|
from typing import List
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, status
|
from fastapi import APIRouter, HTTPException, status
|
||||||
|
|
||||||
from src.auth.service import CurrentUser
|
from src.auth.service import CurrentUser
|
||||||
from src.database.core import DbSession
|
from src.database.core import DbSession
|
||||||
from src.database.service import CommonParameters
|
from src.database.service import CommonParameters
|
||||||
from src.models import StandardResponse
|
from src.models import StandardResponse
|
||||||
|
|
||||||
from .model import OverhaulScope
|
from .model import OverhaulScope
|
||||||
from .schema import ScopeCreate, ScopePagination, ScopeRead, ScopeUpdate
|
from .schema import ScopeCreate, ScopePagination, ScopeRead, ScopeUpdate
|
||||||
from .service import create, delete, get, get_all, update,get_all_oh_with_history_service
|
from .service import create, delete, get, get_all, update, get_all_oh_with_history_service
|
||||||
from src.auth.access_control import required_permission, require_any_role, OHPermission, ALLOWED_ROLES
|
from src.auth.access_control import required_permission, require_any_role, OHPermission, ALLOWED_ROLES
|
||||||
from src.csrf_protect import csrf_protect
|
from src.csrf_protect import csrf_protect
|
||||||
from fastapi import Depends
|
from fastapi import Depends
|
||||||
|
|
||||||
router = APIRouter(dependencies=[Depends(require_any_role(*ALLOWED_ROLES))])
|
router = APIRouter(dependencies=[Depends(require_any_role(*ALLOWED_ROLES))])
|
||||||
|
|
||||||
|
@router.get('', response_model=StandardResponse[ScopePagination], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
||||||
@router.get("", response_model=StandardResponse[ScopePagination], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
async def get_scopes(common: CommonParameters, scope_name: Optional[str]=None):
|
||||||
async def get_scopes(common: CommonParameters, scope_name: Optional[str] = None):
|
|
||||||
"""Get all scope pagination."""
|
|
||||||
results = await get_all(common=common, scope_name=scope_name)
|
results = await get_all(common=common, scope_name=scope_name)
|
||||||
|
return StandardResponse(data=results, message='Data retrieved successfully')
|
||||||
|
|
||||||
return StandardResponse(
|
@router.get('/history', response_model=StandardResponse[List[ScopeRead]], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
||||||
data=results,
|
|
||||||
message="Data retrieved successfully",
|
|
||||||
)
|
|
||||||
|
|
||||||
@router.get("/history", response_model=StandardResponse[List[ScopeRead]], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
|
||||||
async def get_history(db_session: DbSession):
|
async def get_history(db_session: DbSession):
|
||||||
return StandardResponse(data=await get_all_oh_with_history_service(db_session=db_session), message="Data retrieved successfully")
|
return StandardResponse(data=await get_all_oh_with_history_service(db_session=db_session), message='Data retrieved successfully')
|
||||||
|
|
||||||
@router.get("/{overhaul_session_id}", response_model=StandardResponse[ScopeRead], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
@router.get('/{overhaul_session_id}', response_model=StandardResponse[ScopeRead], dependencies=[Depends(required_permission(OHPermission.READ, OverhaulScope))])
|
||||||
async def get_scope(db_session: DbSession, overhaul_session_id: str):
|
async def get_scope(db_session: DbSession, overhaul_session_id: str):
|
||||||
scope = await get(db_session=db_session, overhaul_session_id=overhaul_session_id)
|
scope = await get(db_session=db_session, overhaul_session_id=overhaul_session_id)
|
||||||
if not scope:
|
if not scope:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='A data with this id does not exist.')
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
return StandardResponse(data=scope, message='Data retrieved successfully')
|
||||||
detail="A data with this id does not exist.",
|
|
||||||
)
|
|
||||||
|
|
||||||
return StandardResponse(data=scope, message="Data retrieved successfully")
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("", response_model=StandardResponse[ScopeRead], dependencies=[Depends(required_permission(OHPermission.CREATE, OverhaulScope))])
|
@router.post('', response_model=StandardResponse[ScopeRead], dependencies=[Depends(required_permission(OHPermission.CREATE, OverhaulScope))])
|
||||||
async def create_scope(db_session: DbSession, scope_in: ScopeCreate):
|
async def create_scope(db_session: DbSession, scope_in: ScopeCreate):
|
||||||
scope = await create(db_session=db_session, scope_in=scope_in)
|
scope = await create(db_session=db_session, scope_in=scope_in)
|
||||||
|
return StandardResponse(data=scope, message='Data created successfully')
|
||||||
|
|
||||||
return StandardResponse(data=scope, message="Data created successfully")
|
@router.post('/update/{scope_id}', response_model=StandardResponse[ScopeRead], dependencies=[Depends(required_permission(OHPermission.EDIT, OverhaulScope))])
|
||||||
|
async def update_scope(db_session: DbSession, scope_id: str, scope_in: ScopeUpdate, current_user: CurrentUser):
|
||||||
|
return StandardResponse(data=await update(db_session=db_session, scope_id=scope_id, scope_in=scope_in), message='Data updated successfully')
|
||||||
|
|
||||||
|
@router.post('/delete/{scope_id}', response_model=StandardResponse[ScopeRead], dependencies=[Depends(required_permission(OHPermission.DELETE, OverhaulScope)), Depends(csrf_protect)])
|
||||||
@router.post("/update/{scope_id}", response_model=StandardResponse[ScopeRead], dependencies=[Depends(required_permission(OHPermission.EDIT, OverhaulScope))])
|
|
||||||
async def update_scope(
|
|
||||||
db_session: DbSession,
|
|
||||||
scope_id: str,
|
|
||||||
scope_in: ScopeUpdate,
|
|
||||||
current_user: CurrentUser,
|
|
||||||
):
|
|
||||||
return StandardResponse(
|
|
||||||
data=await update(db_session=db_session, scope_id=scope_id, scope_in=scope_in),
|
|
||||||
message="Data updated successfully",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/delete/{scope_id}", response_model=StandardResponse[ScopeRead], dependencies=[Depends(required_permission(OHPermission.DELETE, OverhaulScope)), Depends(csrf_protect)])
|
|
||||||
async def delete_scope(db_session: DbSession, scope_id: str):
|
async def delete_scope(db_session: DbSession, scope_id: str):
|
||||||
scope = await get(db_session=db_session, overhaul_session_id=scope_id)
|
scope = await get(db_session=db_session, overhaul_session_id=scope_id)
|
||||||
|
|
||||||
if not scope:
|
if not scope:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=[{'msg': 'A data with this id does not exist.'}])
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
detail=[{"msg": "A data with this id does not exist."}],
|
|
||||||
)
|
|
||||||
|
|
||||||
await delete(db_session=db_session, scope_id=scope_id)
|
await delete(db_session=db_session, scope_id=scope_id)
|
||||||
|
return StandardResponse(message='Data deleted successfully', data=scope)
|
||||||
return StandardResponse(message="Data deleted successfully", data=scope)
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,35 +1,25 @@
|
|||||||
from decimal import Decimal, getcontext
|
from decimal import Decimal, getcontext
|
||||||
|
|
||||||
|
|
||||||
def get_material_cost(scope, total_equipment):
|
def get_material_cost(scope, total_equipment):
|
||||||
# Set precision to 28 digits (maximum precision for Decimal)
|
|
||||||
getcontext().prec = 28
|
getcontext().prec = 28
|
||||||
|
if not total_equipment:
|
||||||
if not total_equipment: # Guard against division by zero
|
|
||||||
return float(0)
|
return float(0)
|
||||||
|
if scope == 'B':
|
||||||
if scope == "B":
|
result = Decimal('365539731101') / Decimal(str(total_equipment))
|
||||||
result = Decimal("365539731101") / Decimal(str(total_equipment))
|
|
||||||
return float(result)
|
return float(result)
|
||||||
else:
|
else:
|
||||||
result = Decimal("8565468127") / Decimal(str(total_equipment))
|
result = Decimal('8565468127') / Decimal(str(total_equipment))
|
||||||
return float(result)
|
return float(result)
|
||||||
|
|
||||||
return float(0)
|
return float(0)
|
||||||
|
|
||||||
|
|
||||||
def get_service_cost(scope, total_equipment):
|
def get_service_cost(scope, total_equipment):
|
||||||
# Set precision to 28 digits (maximum precision for Decimal)
|
|
||||||
getcontext().prec = 28
|
getcontext().prec = 28
|
||||||
|
if not total_equipment:
|
||||||
if not total_equipment: # Guard against division by zero
|
|
||||||
return float(0)
|
return float(0)
|
||||||
|
if scope == 'B':
|
||||||
if scope == "B":
|
result = Decimal('36405830225') / Decimal(str(total_equipment))
|
||||||
result = Decimal("36405830225") / Decimal(str(total_equipment))
|
|
||||||
return float(result)
|
return float(result)
|
||||||
else:
|
else:
|
||||||
result = Decimal("36000000000") / Decimal(str(total_equipment))
|
result = Decimal('36000000000') / Decimal(str(total_equipment))
|
||||||
return float(result)
|
return float(result)
|
||||||
|
|
||||||
return float(0)
|
return float(0)
|
||||||
|
|||||||
@ -1,77 +1,22 @@
|
|||||||
"""
|
|
||||||
Soft delete utilities.
|
|
||||||
|
|
||||||
Provides reusable functions to convert hard-delete operations into soft-deletes.
|
|
||||||
The `soft_delete_record` function works with any model that inherits `SoftDeleteMixin`
|
|
||||||
(which is included in `DefaultMixin`).
|
|
||||||
|
|
||||||
Usage in service files:
|
|
||||||
from src.soft_delete import soft_delete_record, apply_not_deleted_filter
|
|
||||||
|
|
||||||
async def delete(*, db_session, record_id: str):
|
|
||||||
await soft_delete_record(db_session=db_session, model=MyModel, record_id=record_id)
|
|
||||||
|
|
||||||
async def get_all(*, db_session, ...):
|
|
||||||
query = Select(MyModel)
|
|
||||||
query = apply_not_deleted_filter(query, MyModel)
|
|
||||||
...
|
|
||||||
"""
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import TypeVar, Type
|
from typing import TypeVar, Type
|
||||||
|
|
||||||
from sqlalchemy import Select, Update
|
from sqlalchemy import Select, Update
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
from src.config import TIMEZONE
|
from src.config import TIMEZONE
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
T = TypeVar('T')
|
||||||
T = TypeVar("T")
|
|
||||||
|
|
||||||
|
|
||||||
def apply_not_deleted_filter(query: Select, model) -> Select:
|
def apply_not_deleted_filter(query: Select, model) -> Select:
|
||||||
"""
|
if hasattr(model, 'deleted_at'):
|
||||||
Appends a WHERE clause to exclude soft-deleted records.
|
|
||||||
Use this on all "get" queries to hide deleted data.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
query = Select(OverhaulScope)
|
|
||||||
query = apply_not_deleted_filter(query, OverhaulScope)
|
|
||||||
"""
|
|
||||||
if hasattr(model, "deleted_at"):
|
|
||||||
return query.filter(model.deleted_at.is_(None))
|
return query.filter(model.deleted_at.is_(None))
|
||||||
return query
|
return query
|
||||||
|
|
||||||
|
async def soft_delete_record(*, db_session: AsyncSession, model: Type[T], record_id: str, id_column: str='id') -> None:
|
||||||
async def soft_delete_record(
|
|
||||||
*,
|
|
||||||
db_session: AsyncSession,
|
|
||||||
model: Type[T],
|
|
||||||
record_id: str,
|
|
||||||
id_column: str = "id",
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Soft-delete a record by setting its `deleted_at` timestamp.
|
|
||||||
Instead of DELETE, this performs an UPDATE.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db_session: The async database session.
|
|
||||||
model: The SQLAlchemy model class.
|
|
||||||
record_id: The value of the ID to match.
|
|
||||||
id_column: The name of the ID column (default: "id").
|
|
||||||
"""
|
|
||||||
now = datetime.now(pytz.timezone(TIMEZONE))
|
now = datetime.now(pytz.timezone(TIMEZONE))
|
||||||
id_col = getattr(model, id_column)
|
id_col = getattr(model, id_column)
|
||||||
|
stmt = Update(model).where(id_col == record_id).values(deleted_at=now)
|
||||||
stmt = (
|
|
||||||
Update(model)
|
|
||||||
.where(id_col == record_id)
|
|
||||||
.values(deleted_at=now)
|
|
||||||
)
|
|
||||||
await db_session.execute(stmt)
|
await db_session.execute(stmt)
|
||||||
await db_session.commit()
|
await db_session.commit()
|
||||||
|
log.info(f'Soft-deleted {model.__tablename__} record: {record_id}')
|
||||||
log.info(f"Soft-deleted {model.__tablename__} record: {record_id}")
|
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
|
||||||
@ -1,41 +1,28 @@
|
|||||||
from sqlalchemy import UUID, Column, Float, ForeignKey, Integer, String, Date
|
from sqlalchemy import UUID, Column, Float, ForeignKey, Integer, String, Date
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
from src.database.core import Base
|
from src.database.core import Base
|
||||||
from src.models import DefaultMixin
|
from src.models import DefaultMixin
|
||||||
|
|
||||||
|
|
||||||
class MasterSparePart(Base, DefaultMixin):
|
class MasterSparePart(Base, DefaultMixin):
|
||||||
__tablename__ = "oh_ms_sparepart"
|
__tablename__ = 'oh_ms_sparepart'
|
||||||
|
|
||||||
assetnum = Column(String, nullable=False)
|
assetnum = Column(String, nullable=False)
|
||||||
location_tag = Column(String, nullable=False)
|
location_tag = Column(String, nullable=False)
|
||||||
stock = Column(Integer, nullable=False, default=0)
|
stock = Column(Integer, nullable=False, default=0)
|
||||||
name = Column(String, nullable=False)
|
name = Column(String, nullable=False)
|
||||||
cost_per_stock = Column(Float, nullable=False)
|
cost_per_stock = Column(Float, nullable=False)
|
||||||
unit = Column(String, nullable=False)
|
unit = Column(String, nullable=False)
|
||||||
|
sparepart_procurements = relationship('MasterSparepartProcurement', lazy='selectin')
|
||||||
sparepart_procurements = relationship("MasterSparepartProcurement", lazy="selectin")
|
|
||||||
|
|
||||||
|
|
||||||
class MasterSparepartProcurement(Base, DefaultMixin):
|
class MasterSparepartProcurement(Base, DefaultMixin):
|
||||||
__tablename__ = "oh_ms_sparepart_procurement"
|
__tablename__ = 'oh_ms_sparepart_procurement'
|
||||||
|
sparepart_id = Column(UUID(as_uuid=True), ForeignKey('oh_ms_sparepart.id', ondelete='cascade'), nullable=False)
|
||||||
sparepart_id = Column(
|
|
||||||
UUID(as_uuid=True),
|
|
||||||
ForeignKey("oh_ms_sparepart.id", ondelete="cascade"),
|
|
||||||
nullable=False,
|
|
||||||
)
|
|
||||||
quantity = Column(Integer, nullable=False)
|
quantity = Column(Integer, nullable=False)
|
||||||
status = Column(String, nullable=False)
|
status = Column(String, nullable=False)
|
||||||
eta_requisition = Column(Date, nullable=False)
|
eta_requisition = Column(Date, nullable=False)
|
||||||
eta_ordered = Column(Date, nullable=True)
|
eta_ordered = Column(Date, nullable=True)
|
||||||
eta_received = Column(Date, nullable=True)
|
eta_received = Column(Date, nullable=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class SparepartRemark(Base, DefaultMixin):
|
class SparepartRemark(Base, DefaultMixin):
|
||||||
__tablename__ = "oh_ms_sparepart_remark"
|
__tablename__ = 'oh_ms_sparepart_remark'
|
||||||
|
|
||||||
itemnum = Column(String, nullable=False)
|
itemnum = Column(String, nullable=False)
|
||||||
remark = Column(String, nullable=False)
|
remark = Column(String, nullable=False)
|
||||||
|
|||||||
@ -1,69 +1,19 @@
|
|||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
from src.csrf_protect import csrf_protect
|
from src.csrf_protect import csrf_protect
|
||||||
from src.database.core import CollectorDbSession
|
from src.database.core import CollectorDbSession
|
||||||
from src.database.service import (DbSession)
|
from src.database.service import DbSession
|
||||||
from src.models import StandardResponse
|
from src.models import StandardResponse
|
||||||
from src.sparepart.schema import SparepartRemark
|
from src.sparepart.schema import SparepartRemark
|
||||||
from src.auth.access_control import require_any_role, ALLOWED_ROLES
|
from src.auth.access_control import require_any_role, ALLOWED_ROLES
|
||||||
from .service import create_remark, get_spareparts_paginated
|
from .service import create_remark, get_spareparts_paginated
|
||||||
|
|
||||||
router = APIRouter(dependencies=[Depends(require_any_role(*ALLOWED_ROLES))])
|
router = APIRouter(dependencies=[Depends(require_any_role(*ALLOWED_ROLES))])
|
||||||
|
|
||||||
|
@router.get('', response_model=StandardResponse[list])
|
||||||
@router.get("", response_model=StandardResponse[list])
|
async def get_sparepart(collector_db_session: CollectorDbSession, db_session: DbSession):
|
||||||
async def get_sparepart(collector_db_session:CollectorDbSession, db_session: DbSession):
|
|
||||||
"""Get all scope activity pagination."""
|
|
||||||
data = await get_spareparts_paginated(db_session=db_session, collector_db_session=collector_db_session)
|
data = await get_spareparts_paginated(db_session=db_session, collector_db_session=collector_db_session)
|
||||||
|
return StandardResponse(data=data, message='Data retrieved successfully')
|
||||||
|
|
||||||
return StandardResponse(
|
@router.post('', response_model=StandardResponse[SparepartRemark], dependencies=[Depends(csrf_protect)])
|
||||||
data=data,
|
async def create_remark_route(collector_db_session: CollectorDbSession, db_session: DbSession, remark_in: SparepartRemark):
|
||||||
message="Data retrieved successfully",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("", response_model=StandardResponse[SparepartRemark], dependencies=[Depends(csrf_protect)])
|
|
||||||
async def create_remark_route(collector_db_session:CollectorDbSession, db_session: DbSession, remark_in:SparepartRemark):
|
|
||||||
sparepart_remark = await create_remark(db_session=db_session, collector_db_session=collector_db_session, remark_in=remark_in)
|
sparepart_remark = await create_remark(db_session=db_session, collector_db_session=collector_db_session, remark_in=remark_in)
|
||||||
|
return StandardResponse(data=sparepart_remark, message='Remark Created successfully')
|
||||||
return StandardResponse(
|
|
||||||
data=sparepart_remark,
|
|
||||||
message="Remark Created successfully"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# @router.post("", response_model=StandardResponse[ActivityMasterCreate])
|
|
||||||
# async def create_activity(db_session: DbSession, activity_in: ActivityMasterCreate):
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# @router.get(
|
|
||||||
# "/{scope_equipment_activity_id}", response_model=StandardResponse[ActivityMaster]
|
|
||||||
# async def get_activity(db_session: DbSession, activity_id: str):
|
|
||||||
# if not activity:
|
|
||||||
# raise HTTPException(
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# @router.put(
|
|
||||||
# "/{scope_equipment_activity_id}", response_model=StandardResponse[ActivityMaster]
|
|
||||||
# async def update_scope(
|
|
||||||
# db_session: DbSession, activity_in: ActivityMasterCreate, activity_id
|
|
||||||
# ):
|
|
||||||
|
|
||||||
# if not activity:
|
|
||||||
# raise HTTPException(
|
|
||||||
|
|
||||||
# return StandardResponse(
|
|
||||||
# ),
|
|
||||||
|
|
||||||
|
|
||||||
# @router.delete(
|
|
||||||
# "/{scope_equipment_activity_id}", response_model=StandardResponse[ActivityMaster]
|
|
||||||
# async def delete_scope(db_session: DbSession, activity_id: str):
|
|
||||||
|
|
||||||
# if not activity:
|
|
||||||
# raise HTTPException(
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -0,0 +1 @@
|
|||||||
|
|
||||||
@ -1,6 +1,5 @@
|
|||||||
from src.enums import OptimumOHEnum
|
from src.enums import OptimumOHEnum
|
||||||
|
|
||||||
|
|
||||||
class ScopeEquipmentType(OptimumOHEnum):
|
class ScopeEquipmentType(OptimumOHEnum):
|
||||||
TEMP = "Temporary"
|
TEMP = 'Temporary'
|
||||||
PERM = "Permanent"
|
PERM = 'Permanent'
|
||||||
|
|||||||
@ -1,69 +1,36 @@
|
|||||||
from sqlalchemy import UUID, Column, Date, Float, ForeignKey, Integer, String, Boolean
|
from sqlalchemy import UUID, Column, Date, Float, ForeignKey, Integer, String, Boolean
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
from src.database.core import Base
|
from src.database.core import Base
|
||||||
from src.models import DefaultMixin
|
from src.models import DefaultMixin
|
||||||
|
|
||||||
|
|
||||||
class StandardScope(Base, DefaultMixin):
|
class StandardScope(Base, DefaultMixin):
|
||||||
__tablename__ = "oh_ms_standard_scope"
|
__tablename__ = 'oh_ms_standard_scope'
|
||||||
|
|
||||||
location_tag = Column(String, nullable=False)
|
location_tag = Column(String, nullable=False)
|
||||||
is_alternating_oh = Column(Boolean, nullable=False, default=False)
|
is_alternating_oh = Column(Boolean, nullable=False, default=False)
|
||||||
assigned_date = Column(Date, nullable=True)
|
assigned_date = Column(Date, nullable=True)
|
||||||
service_cost = Column(Float, nullable=True)
|
service_cost = Column(Float, nullable=True)
|
||||||
|
master_equipment = relationship('MasterEquipment', lazy='selectin', primaryjoin='and_(StandardScope.location_tag == foreign(MasterEquipment.location_tag))', uselist=False)
|
||||||
master_equipment = relationship(
|
oh_history = relationship('EquipmentOHHistory', lazy='selectin', primaryjoin='and_(StandardScope.location_tag == foreign(EquipmentOHHistory.location_tag))', uselist=False)
|
||||||
"MasterEquipment",
|
workscope_groups = relationship('EquipmentWorkscopeGroup', lazy='selectin', primaryjoin='and_(EquipmentWorkscopeGroup.location_tag == foreign(StandardScope.location_tag))', uselist=True)
|
||||||
lazy="selectin",
|
|
||||||
primaryjoin="and_(StandardScope.location_tag == foreign(MasterEquipment.location_tag))",
|
|
||||||
uselist=False, # Add this if it's a one-to-one relationship
|
|
||||||
)
|
|
||||||
|
|
||||||
oh_history = relationship(
|
|
||||||
"EquipmentOHHistory",
|
|
||||||
lazy="selectin",
|
|
||||||
primaryjoin="and_(StandardScope.location_tag == foreign(EquipmentOHHistory.location_tag))",
|
|
||||||
uselist=False, # Add this if it's a one-to-one relationship
|
|
||||||
)
|
|
||||||
|
|
||||||
workscope_groups = relationship(
|
|
||||||
"EquipmentWorkscopeGroup",
|
|
||||||
lazy="selectin",
|
|
||||||
primaryjoin="and_(EquipmentWorkscopeGroup.location_tag == foreign(StandardScope.location_tag))",
|
|
||||||
uselist=True, # Add this if it's a one-to-one relationship
|
|
||||||
)
|
|
||||||
|
|
||||||
class EquipmentOHHistory(Base, DefaultMixin):
|
class EquipmentOHHistory(Base, DefaultMixin):
|
||||||
__tablename__ = "oh_ms_equipment_oh_history"
|
__tablename__ = 'oh_ms_equipment_oh_history'
|
||||||
location_tag = Column(String, nullable=False)
|
location_tag = Column(String, nullable=False)
|
||||||
last_oh_date = Column(Date, nullable=True)
|
last_oh_date = Column(Date, nullable=True)
|
||||||
last_oh_type = Column(String, nullable=True)
|
last_oh_type = Column(String, nullable=True)
|
||||||
|
|
||||||
|
|
||||||
class MasterEquipment(Base, DefaultMixin):
|
class MasterEquipment(Base, DefaultMixin):
|
||||||
__tablename__ = "ms_equipment_master"
|
__tablename__ = 'ms_equipment_master'
|
||||||
|
|
||||||
id = Column(UUID(as_uuid=True), primary_key=True, index=True)
|
id = Column(UUID(as_uuid=True), primary_key=True, index=True)
|
||||||
parent_id = Column(
|
parent_id = Column(UUID(as_uuid=True), ForeignKey('ms_equipment_master.id', ondelete='CASCADE'), nullable=True)
|
||||||
UUID(as_uuid=True),
|
|
||||||
ForeignKey("ms_equipment_master.id", ondelete="CASCADE"),
|
|
||||||
nullable=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
assetnum = Column(String, nullable=True)
|
assetnum = Column(String, nullable=True)
|
||||||
system_tag = Column(String, nullable=True)
|
system_tag = Column(String, nullable=True)
|
||||||
location_tag = Column(String, nullable=True)
|
location_tag = Column(String, nullable=True)
|
||||||
name = Column(String, nullable=True)
|
name = Column(String, nullable=True)
|
||||||
equipment_tree_id = Column(
|
equipment_tree_id = Column(UUID(as_uuid=True), ForeignKey('ms_equipment_tree.id'), nullable=True)
|
||||||
UUID(as_uuid=True), ForeignKey("ms_equipment_tree.id"), nullable=True
|
equipment_tree = relationship('MasterEquipmentTree', backref='master_equipments')
|
||||||
)
|
parent = relationship('MasterEquipment', remote_side=[id], lazy='selectin')
|
||||||
|
|
||||||
equipment_tree = relationship("MasterEquipmentTree", backref="master_equipments")
|
|
||||||
parent = relationship("MasterEquipment", remote_side=[id], lazy="selectin")
|
|
||||||
|
|
||||||
|
|
||||||
class MasterEquipmentTree(Base, DefaultMixin):
|
class MasterEquipmentTree(Base, DefaultMixin):
|
||||||
__tablename__ = "ms_equipment_tree"
|
__tablename__ = 'ms_equipment_tree'
|
||||||
|
|
||||||
level_no = Column(Integer)
|
level_no = Column(Integer)
|
||||||
|
|||||||
@ -1,73 +1,31 @@
|
|||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
from fastapi.params import Query
|
from fastapi.params import Query
|
||||||
|
|
||||||
from src.database.core import DbSession, CollectorDbSession
|
from src.database.core import DbSession, CollectorDbSession
|
||||||
from src.database.service import CommonParameters
|
from src.database.service import CommonParameters
|
||||||
from src.models import StandardResponse
|
from src.models import StandardResponse
|
||||||
from src.auth.access_control import require_any_role, ALLOWED_ROLES
|
from src.auth.access_control import require_any_role, ALLOWED_ROLES
|
||||||
|
from .schema import MasterEquipmentPagination, ScopeEquipmentCreate, ScopeEquipmentPagination
|
||||||
from .schema import (MasterEquipmentPagination, ScopeEquipmentCreate,
|
from .service import create, get_all, get_all_master_equipment, get_history_standard_scope_wo_service
|
||||||
ScopeEquipmentPagination)
|
|
||||||
from .service import (create, get_all, get_all_master_equipment, get_history_standard_scope_wo_service)
|
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
router = APIRouter(dependencies=[Depends(require_any_role(*ALLOWED_ROLES))])
|
router = APIRouter(dependencies=[Depends(require_any_role(*ALLOWED_ROLES))])
|
||||||
|
|
||||||
|
@router.get('', response_model=StandardResponse[ScopeEquipmentPagination])
|
||||||
@router.get("", response_model=StandardResponse[ScopeEquipmentPagination])
|
async def get_scope_equipments(common: CommonParameters, scope_name: str=Query(None)):
|
||||||
async def get_scope_equipments(common: CommonParameters, scope_name: str = Query(None)):
|
|
||||||
"""Get all scope pagination."""
|
|
||||||
data = await get_all(common=common, oh_scope=scope_name)
|
data = await get_all(common=common, oh_scope=scope_name)
|
||||||
|
return StandardResponse(data=data, message='Data retrieved successfully')
|
||||||
|
|
||||||
return StandardResponse(
|
@router.get('/available/{scope_name}', response_model=StandardResponse[MasterEquipmentPagination])
|
||||||
data=data,
|
|
||||||
message="Data retrieved successfully",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/available/{scope_name}",
|
|
||||||
response_model=StandardResponse[MasterEquipmentPagination],
|
|
||||||
)
|
|
||||||
async def get_master_equipment(common: CommonParameters, scope_name: str):
|
async def get_master_equipment(common: CommonParameters, scope_name: str):
|
||||||
results = await get_all_master_equipment(common=common, scope_name=scope_name)
|
results = await get_all_master_equipment(common=common, scope_name=scope_name)
|
||||||
|
return StandardResponse(data=results, message='Data retrieved successfully')
|
||||||
|
|
||||||
return StandardResponse(data=results, message="Data retrieved successfully")
|
@router.post('', response_model=StandardResponse[List[str]])
|
||||||
|
async def create_scope_equipment(db_session: DbSession, scope_equipment_in: ScopeEquipmentCreate):
|
||||||
|
|
||||||
@router.post("", response_model=StandardResponse[List[str]])
|
|
||||||
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)
|
scope = await create(db_session=db_session, scope_equipment_in=scope_equipment_in)
|
||||||
|
return StandardResponse(data=scope, message='Data created successfully')
|
||||||
|
|
||||||
return StandardResponse(data=scope, message="Data created successfully")
|
@router.get('/history/{oh_session_id}', response_model=StandardResponse[List[dict]])
|
||||||
|
async def get_history_standard_scope_wo(db_session: DbSession, collector_db_session: CollectorDbSession, oh_session_id: UUID):
|
||||||
|
|
||||||
@router.get("/history/{oh_session_id}", response_model=StandardResponse[List[dict]])
|
|
||||||
async def get_history_standard_scope_wo(
|
|
||||||
db_session: DbSession, collector_db_session:CollectorDbSession, oh_session_id:UUID):
|
|
||||||
results = await get_history_standard_scope_wo_service(db_session=db_session, collector_db_session=collector_db_session, oh_session_id=oh_session_id)
|
results = await get_history_standard_scope_wo_service(db_session=db_session, collector_db_session=collector_db_session, oh_session_id=oh_session_id)
|
||||||
|
return StandardResponse(data=results, message='Data retrieved successfully')
|
||||||
return StandardResponse(data=results, message="Data retrieved successfully")
|
|
||||||
|
|
||||||
# @router.put("/{assetnum}", response_model=StandardResponse[ScopeEquipmentRead])
|
|
||||||
# async def update_scope_equipment(
|
|
||||||
# db_session: DbSession, assetnum: str, scope__equipment_in: ScopeEquipmentUpdate
|
|
||||||
# ):
|
|
||||||
|
|
||||||
# if not scope_equipment:
|
|
||||||
# raise HTTPException(
|
|
||||||
|
|
||||||
# return StandardResponse(
|
|
||||||
# ),
|
|
||||||
|
|
||||||
|
|
||||||
# @router.delete("/{assetnum}", response_model=StandardResponse[None])
|
|
||||||
# async def delete_scope_equipment(db_session: DbSession, assetnum: str):
|
|
||||||
|
|
||||||
# if not scope_equipment:
|
|
||||||
# raise HTTPException(
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,296 +1,31 @@
|
|||||||
"""
|
|
||||||
Integration tests for Optimum OH backend.
|
|
||||||
All endpoints are sourced from frontend API calls using OPTIMUM_OH_API_URL.
|
|
||||||
|
|
||||||
Run with:
|
|
||||||
pytest src/testing/integration_test.py -v
|
|
||||||
"""
|
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
BASE_URL = 'http://100.125.115.116:8000/optimumoh'
|
||||||
BASE_URL = "http://100.125.115.116:8000/optimumoh"
|
AUTH_BASE_URL = 'http://100.125.115.116:8000/auth'
|
||||||
AUTH_BASE_URL = "http://100.125.115.116:8000/auth"
|
AUTH_TOKEN = 'Bearer <your_token_here>'
|
||||||
|
HEADERS = {'Authorization': AUTH_TOKEN, 'Content-Type': 'application/json'}
|
||||||
# Replace with a valid token before running
|
OVERHAUL_SESSION_ID = '3704c82c-cfc5-47f7-813a-1661f89b0738'
|
||||||
AUTH_TOKEN = "Bearer <your_token_here>"
|
SCOPE_JOB_ID = 'db590764-72cb-4eb1-a30f-014116df0472'
|
||||||
|
CALCULATION_ID = '44f483f3-bfe4-4094-a59f-b97a10f2fea6'
|
||||||
HEADERS = {
|
|
||||||
"Authorization": AUTH_TOKEN,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
}
|
|
||||||
|
|
||||||
# Placeholder IDs — replace with real values from your database before running
|
|
||||||
OVERHAUL_SESSION_ID = "3704c82c-cfc5-47f7-813a-1661f89b0738"
|
|
||||||
SCOPE_JOB_ID = "db590764-72cb-4eb1-a30f-014116df0472"
|
|
||||||
CALCULATION_ID = "44f483f3-bfe4-4094-a59f-b97a10f2fea6"
|
|
||||||
BUDGET_THRESHOLD = 100000
|
BUDGET_THRESHOLD = 100000
|
||||||
RISK_COST = 1
|
RISK_COST = 1
|
||||||
LOCATION_TAG = "3AL-F501A"
|
LOCATION_TAG = '3AL-F501A'
|
||||||
SCOPE_NAME = "A"
|
SCOPE_NAME = 'A'
|
||||||
ASSET_NUM = "A19825"
|
ASSET_NUM = 'A19825'
|
||||||
EAF_INPUT = 0.85
|
EAF_INPUT = 0.85
|
||||||
DURATION = 17520
|
DURATION = 17520
|
||||||
SCOPE_EQUIPMENT_ACTIVITY_ID = "4527eb2b-0bc4-46b3-80a1-e1d2f67882f8"
|
SCOPE_EQUIPMENT_ACTIVITY_ID = '4527eb2b-0bc4-46b3-80a1-e1d2f67882f8'
|
||||||
ITEMNUM = "206510"
|
ITEMNUM = '206510'
|
||||||
SPREADSHEET_LINK = "https://docs.google.com/spreadsheets/d/example_spreadsheet_id/edit"
|
SPREADSHEET_LINK = 'https://docs.google.com/spreadsheets/d/example_spreadsheet_id/edit'
|
||||||
# UUID-format placeholder — replace with a real UUID from your database
|
USER_LIST = [{'id': 1, 'name': 'devmustbeadmin', 'pass': '', 'email': '', 'authorization_token': ''}, {'id': 2, 'name': 'devmustbeengineer', 'pass': '', 'email': '', 'authorization_token': ''}, {'id': 3, 'name': 'devmustbeapplicationadmin', 'pass': '', 'email': '', 'authorization_token': ''}, {'id': 4, 'name': 'devmustbemanagement', 'pass': '', 'email': '', 'authorization_token': ''}]
|
||||||
|
NON_MANAGEMENT = {'devmustbeadmin', 'devmustbeengineer', 'devmustbeapplicationadmin'}
|
||||||
# ---------------------------------------------------------------------------
|
ALL_ALLOWED = {'devmustbeadmin', 'devmustbeengineer', 'devmustbeapplicationadmin', 'devmustbemanagement'}
|
||||||
# Users
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
USER_LIST = [
|
|
||||||
{"id": 1, "name": "devmustbeadmin", "pass": "", "email": "", "authorization_token": ""},
|
|
||||||
{"id": 2, "name": "devmustbeengineer", "pass": "", "email": "", "authorization_token": ""},
|
|
||||||
{"id": 3, "name": "devmustbeapplicationadmin", "pass": "", "email": "", "authorization_token": ""},
|
|
||||||
{"id": 4, "name": "devmustbemanagement", "pass": "", "email": "", "authorization_token": ""},
|
|
||||||
]
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Role-based access constants
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Endpoints guarded by require_any_role(*ALLOWED_ROLES) block Management entirely.
|
|
||||||
# Endpoints with only required_permission(READ) allow Management (has READ in ACL).
|
|
||||||
# Endpoints with no guard at all allow every role.
|
|
||||||
NON_MANAGEMENT = {"devmustbeadmin", "devmustbeengineer", "devmustbeapplicationadmin"}
|
|
||||||
ALL_ALLOWED = {"devmustbeadmin", "devmustbeengineer", "devmustbeapplicationadmin", "devmustbemanagement"}
|
|
||||||
|
|
||||||
|
|
||||||
def user_auth_headers(token: str) -> dict:
|
def user_auth_headers(token: str) -> dict:
|
||||||
return {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
return {'Authorization': f'Bearer {token}', 'Content-Type': 'application/json'}
|
||||||
|
|
||||||
|
|
||||||
def get_csrf_token(path: str, auth_token: str) -> str:
|
def get_csrf_token(path: str, auth_token: str) -> str:
|
||||||
"""Fetch a one-time CSRF token from be-auth for the given endpoint path.
|
res = requests.post(f'{AUTH_BASE_URL}/csrf-token', headers={'Authorization': f'Bearer {auth_token}', 'Content-Type': 'application/json', 'X-Target-Path': path}, json={'_csrf_request': True})
|
||||||
|
print(f'Requested CSRF token for {path}, got status {res.status_code}, response: {res.text}')
|
||||||
Args:
|
assert res.status_code == 200, f'Failed to get CSRF token for {path}: {res.status_code} {res.text}'
|
||||||
path: The endpoint path as seen by be-optimumoh (e.g. "/spareparts").
|
|
||||||
Do NOT include the "/optimumoh" Kong prefix.
|
|
||||||
auth_token: The raw JWT token string (without "Bearer " prefix).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The plain-text CSRF token to send in the X-CSRF-Token header.
|
|
||||||
"""
|
|
||||||
res = requests.post(
|
|
||||||
f"{AUTH_BASE_URL}/csrf-token",
|
|
||||||
headers={
|
|
||||||
"Authorization": f"Bearer {auth_token}",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"X-Target-Path": path,
|
|
||||||
},
|
|
||||||
json={
|
|
||||||
"_csrf_request": True,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
print(f"Requested CSRF token for {path}, got status {res.status_code}, response: {res.text}")
|
|
||||||
assert res.status_code == 200, f"Failed to get CSRF token for {path}: {res.status_code} {res.text}"
|
|
||||||
data = res.json()
|
data = res.json()
|
||||||
# Response shape: {"data": {"csrf_token": "..."}, ...}
|
return data['data']['csrf_token']
|
||||||
return data["data"]["csrf_token"]
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# OH GANTT
|
|
||||||
|
|
||||||
# # [[BUG IN THIS ROUTE]]
|
|
||||||
# def test_get_overhaul_gantt():
|
|
||||||
# """GET /overhaul-gantt — no role guard; all roles allowed"""
|
|
||||||
# for user in USER_LIST:
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
# Overhaul Session
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
|
|
||||||
# # [[Source code got commented]]
|
|
||||||
# def test_get_overhaul_session():
|
|
||||||
# for user in USER_LIST:
|
|
||||||
|
|
||||||
# # [[Source code got commented]]
|
|
||||||
# def test_post_overhaul_session():
|
|
||||||
# """POST /overhaul-session — require_any_role + CREATE blocks Management"""
|
|
||||||
# for user in USER_LIST:
|
|
||||||
# if user["name"] in NON_MANAGEMENT:
|
|
||||||
|
|
||||||
# # ─────────────────────────────────────────────
|
|
||||||
# # Overhaul Schedules
|
|
||||||
# # ─────────────────────────────────────────────
|
|
||||||
|
|
||||||
# # [[Attribute error]]
|
|
||||||
# def test_post_overhaul_schedules():
|
|
||||||
# """POST /overhaul-schedules — require_any_role + CREATE blocks Management"""
|
|
||||||
# "status": "Upcoming"
|
|
||||||
# for user in USER_LIST:
|
|
||||||
# if user["name"] in NON_MANAGEMENT:
|
|
||||||
|
|
||||||
|
|
||||||
# # [[Source code got commented]]
|
|
||||||
# def test_delete_overhaul_schedule():
|
|
||||||
# for user in USER_LIST:
|
|
||||||
# if user["name"] in NON_MANAGEMENT:
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# # ─────────────────────────────────────────────
|
|
||||||
# # Overhaul Jobs
|
|
||||||
# # ─────────────────────────────────────────────
|
|
||||||
|
|
||||||
# # [[Source code got commented]]
|
|
||||||
# def test_post_overhaul_jobs():
|
|
||||||
# for user in USER_LIST:
|
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
# Scope Equipments
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
|
|
||||||
# # [[Database error: Unconsumed column names: scope_overhaul, assetnum, type, removal_date]]
|
|
||||||
# def test_post_scope_equipments():
|
|
||||||
# """POST /scope-equipments — require_any_role blocks Management"""
|
|
||||||
# "scope_name": "A"
|
|
||||||
# for user in USER_LIST:
|
|
||||||
# if user["name"] in NON_MANAGEMENT:
|
|
||||||
|
|
||||||
# # [[No endpoint exists]]
|
|
||||||
# def test_delete_scope_equipment():
|
|
||||||
# for user in USER_LIST:
|
|
||||||
# if user["name"] in NON_MANAGEMENT:
|
|
||||||
|
|
||||||
|
|
||||||
# # ─────────────────────────────────────────────
|
|
||||||
# # Scope Equipment Jobs
|
|
||||||
# # ─────────────────────────────────────────────
|
|
||||||
|
|
||||||
# # [[Source code got commented]]
|
|
||||||
# def test_post_scope_equipment_jobs():
|
|
||||||
# for user in USER_LIST:
|
|
||||||
|
|
||||||
# # [[Source code got commented]]
|
|
||||||
# def test_delete_scope_equipment_job():
|
|
||||||
# for user in USER_LIST:
|
|
||||||
|
|
||||||
# # ─────────────────────────────────────────────
|
|
||||||
# # Overhaul Jobs
|
|
||||||
# # ─────────────────────────────────────────────
|
|
||||||
|
|
||||||
# # [[Source code got commented]]
|
|
||||||
# def test_get_overhaul_jobs():
|
|
||||||
# for user in USER_LIST:
|
|
||||||
|
|
||||||
|
|
||||||
# # ─────────────────────────────────────────────
|
|
||||||
# # Calculation — Budget Constraint
|
|
||||||
# # ─────────────────────────────────────────────
|
|
||||||
|
|
||||||
# def test_post_calculation_time_constraint():
|
|
||||||
# for user in USER_LIST:
|
|
||||||
# if user["name"] in NON_MANAGEMENT:
|
|
||||||
|
|
||||||
|
|
||||||
# # ─────────────────────────────────────────────
|
|
||||||
# # Equipment Activities / Workscopes
|
|
||||||
# # ─────────────────────────────────────────────
|
|
||||||
|
|
||||||
# # [[Source code got commented]]
|
|
||||||
# def test_post_equipment_activities():
|
|
||||||
# """POST /equipment-activities — no role guard; all roles allowed
|
|
||||||
# Note: route commented out in api.py; active equivalent is POST /overhaul-activity/{session_id}
|
|
||||||
# """
|
|
||||||
# for user in USER_LIST:
|
|
||||||
|
|
||||||
|
|
||||||
# # [[Source code got commented]]
|
|
||||||
# def test_delete_equipment_activity():
|
|
||||||
# """POST /equipment-activities/delete/{id} — no role guard; all roles allowed
|
|
||||||
# Note: route commented out in api.py; active equivalent is POST /overhaul-activity/delete/{session}/{location_tag}
|
|
||||||
# """
|
|
||||||
# for user in USER_LIST:
|
|
||||||
|
|
||||||
|
|
||||||
# # ─────────────────────────────────────────────
|
|
||||||
# # Jobs
|
|
||||||
# # ─────────────────────────────────────────────
|
|
||||||
|
|
||||||
# # [[Source code got commented]]
|
|
||||||
# def test_get_jobs():
|
|
||||||
# """GET /jobs — no role guard; all roles allowed"""
|
|
||||||
# for user in USER_LIST:
|
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
# Overhauls sub-routes
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
|
|
||||||
# def test_get_overhauls_schedules():
|
|
||||||
# for user in USER_LIST:
|
|
||||||
|
|
||||||
# def test_get_overhauls_critical_parts():
|
|
||||||
# for user in USER_LIST:
|
|
||||||
|
|
||||||
|
|
||||||
# # ─────────────────────────────────────────────
|
|
||||||
# # Overhaul Activity
|
|
||||||
# # ─────────────────────────────────────────────
|
|
||||||
|
|
||||||
# # [[Attribute Error]]
|
|
||||||
# def test_get_overhaul_activity_by_assetnum():
|
|
||||||
# for user in USER_LIST:
|
|
||||||
|
|
||||||
|
|
||||||
# # ─────────────────────────────────────────────
|
|
||||||
# # Workscopes
|
|
||||||
# # ─────────────────────────────────────────────
|
|
||||||
|
|
||||||
# def test_post_workscopes():
|
|
||||||
# """POST /workscopes — no role guard; all roles allowed
|
|
||||||
# """
|
|
||||||
# for user in USER_LIST:
|
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
# Equipment Workscopes
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
|
|
||||||
# def test_post_equipment_workscopes_delete():
|
|
||||||
# """POST /equipment-workscopes/delete/{scope_job_id} — no role guard; all roles allowed
|
|
||||||
# No body required.
|
|
||||||
# """
|
|
||||||
# for user in USER_LIST:
|
|
||||||
|
|
||||||
|
|
||||||
# # ─────────────────────────────────────────────
|
|
||||||
# # Calculation — Time Constraint
|
|
||||||
# # ─────────────────────────────────────────────
|
|
||||||
|
|
||||||
# def test_post_calculation_time_constraint():
|
|
||||||
# """POST /calculation/time-constraint — required_permission(CREATE); Management has no CREATE
|
|
||||||
# Payload: overhaulCost (float), ohSessionId (UUID), costPerFailure (float)
|
|
||||||
# Query params: scope_calculation_id (optional), with_results (optional int), simulation_id (optional)
|
|
||||||
# """
|
|
||||||
# for user in USER_LIST:
|
|
||||||
# if user["name"] in NON_MANAGEMENT:
|
|
||||||
|
|
||||||
# def test_get_calculation_time_constraint_parameters():
|
|
||||||
# """GET /calculation/time-constraint/parameters — required_permission(READ); Management has READ
|
|
||||||
# Query params: calculation_id (optional)
|
|
||||||
# """
|
|
||||||
# for user in USER_LIST:
|
|
||||||
|
|
||||||
# def test_get_calculation_time_constraint_by_id_and_assetnum():
|
|
||||||
# for user in USER_LIST:
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# # ─────────────────────────────────────────────
|
|
||||||
# # Calculation — Budget Constraint
|
|
||||||
# # ─────────────────────────────────────────────
|
|
||||||
|
|
||||||
# def test_get_calculation_budget_constraint():
|
|
||||||
# """GET /calculation/budget-constraint/{session_id} — require_any_role blocks Management
|
|
||||||
# Query params: cost_threshold (float, default 100)
|
|
||||||
# """
|
|
||||||
# for user in USER_LIST:
|
|
||||||
|
|||||||
@ -1,529 +1,255 @@
|
|||||||
"""
|
|
||||||
Integration tests for Optimum OH backend.
|
|
||||||
All endpoints are sourced from frontend API calls using OPTIMUM_OH_API_URL.
|
|
||||||
|
|
||||||
Run with:
|
|
||||||
pytest src/testing/integration_test.py -v
|
|
||||||
"""
|
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
BASE_URL = 'http://100.125.115.116:8000/optimumoh'
|
||||||
BASE_URL = "http://100.125.115.116:8000/optimumoh"
|
AUTH_BASE_URL = 'http://100.125.115.116:8000/auth'
|
||||||
AUTH_BASE_URL = "http://100.125.115.116:8000/auth"
|
OVERHAUL_SESSION_ID = '3704c82c-cfc5-47f7-813a-1661f89b0738'
|
||||||
|
SCOPE_JOB_ID = 'db590764-72cb-4eb1-a30f-014116df0472'
|
||||||
OVERHAUL_SESSION_ID = "3704c82c-cfc5-47f7-813a-1661f89b0738"
|
CALCULATION_ID = '44f483f3-bfe4-4094-a59f-b97a10f2fea6'
|
||||||
SCOPE_JOB_ID = "db590764-72cb-4eb1-a30f-014116df0472"
|
|
||||||
CALCULATION_ID = "44f483f3-bfe4-4094-a59f-b97a10f2fea6"
|
|
||||||
BUDGET_THRESHOLD = 100000
|
BUDGET_THRESHOLD = 100000
|
||||||
RISK_COST = 1
|
RISK_COST = 1
|
||||||
LOCATION_TAG = "3AL-F501A"
|
LOCATION_TAG = '3AL-F501A'
|
||||||
SCOPE_NAME = "A"
|
SCOPE_NAME = 'A'
|
||||||
ASSET_NUM = "A19825"
|
ASSET_NUM = 'A19825'
|
||||||
EAF_INPUT = 0.85
|
EAF_INPUT = 0.85
|
||||||
DURATION = 17520
|
DURATION = 17520
|
||||||
SCOPE_EQUIPMENT_ACTIVITY_ID = "4527eb2b-0bc4-46b3-80a1-e1d2f67882f8"
|
SCOPE_EQUIPMENT_ACTIVITY_ID = '4527eb2b-0bc4-46b3-80a1-e1d2f67882f8'
|
||||||
ITEMNUM = "206510"
|
ITEMNUM = '206510'
|
||||||
SPREADSHEET_LINK = "https://docs.google.com/spreadsheets/d/example_spreadsheet_id/edit"
|
SPREADSHEET_LINK = 'https://docs.google.com/spreadsheets/d/example_spreadsheet_id/edit'
|
||||||
|
USER_LIST = [{'id': 1, 'name': 'devmustbeadmin', 'pass': '', 'email': '', 'authorization_token': ''}, {'id': 2, 'name': 'devmustbeengineer', 'pass': '', 'email': '', 'authorization_token': ''}, {'id': 3, 'name': 'devmustbeapplicationadmin', 'pass': '', 'email': '', 'authorization_token': ''}, {'id': 4, 'name': 'devmustbemanagement', 'pass': '', 'email': '', 'authorization_token': ''}]
|
||||||
# ---------------------------------------------------------------------------
|
NON_MANAGEMENT = {'devmustbeadmin', 'devmustbeengineer', 'devmustbeapplicationadmin'}
|
||||||
# Users
|
ALL_ALLOWED = {'devmustbeadmin', 'devmustbeengineer', 'devmustbeapplicationadmin', 'devmustbemanagement'}
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
USER_LIST = [
|
|
||||||
{"id": 1, "name": "devmustbeadmin", "pass": "", "email": "", "authorization_token": ""},
|
|
||||||
{"id": 2, "name": "devmustbeengineer", "pass": "", "email": "", "authorization_token": ""},
|
|
||||||
{"id": 3, "name": "devmustbeapplicationadmin", "pass": "", "email": "", "authorization_token": ""},
|
|
||||||
{"id": 4, "name": "devmustbemanagement", "pass": "", "email": "", "authorization_token": ""},
|
|
||||||
]
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Role-based access constants
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Endpoints guarded by require_any_role(*ALLOWED_ROLES) block Management entirely.
|
|
||||||
# Endpoints with only required_permission(READ) allow Management (has READ in ACL).
|
|
||||||
# Endpoints with no guard at all allow every role.
|
|
||||||
NON_MANAGEMENT = {"devmustbeadmin", "devmustbeengineer", "devmustbeapplicationadmin"}
|
|
||||||
ALL_ALLOWED = {"devmustbeadmin", "devmustbeengineer", "devmustbeapplicationadmin", "devmustbemanagement"}
|
|
||||||
|
|
||||||
|
|
||||||
def user_auth_headers(token: str) -> dict:
|
def user_auth_headers(token: str) -> dict:
|
||||||
return {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
return {'Authorization': f'Bearer {token}', 'Content-Type': 'application/json'}
|
||||||
|
|
||||||
|
|
||||||
def get_csrf_token(path: str, auth_token: str) -> str:
|
def get_csrf_token(path: str, auth_token: str) -> str:
|
||||||
"""Fetch a one-time CSRF token from be-auth for the given endpoint path.
|
res = requests.post(f'{AUTH_BASE_URL}/csrf-token', headers={'Authorization': f'Bearer {auth_token}', 'Content-Type': 'application/json', 'X-Target-Path': path}, json={'_csrf_request': True})
|
||||||
|
print(f'Requested CSRF token for {path}, got status {res.status_code}, response: {res.text}')
|
||||||
Args:
|
assert res.status_code == 200, f'Failed to get CSRF token for {path}: {res.status_code} {res.text}'
|
||||||
path: The endpoint path as seen by be-optimumoh (e.g. "/spareparts").
|
|
||||||
Do NOT include the "/optimumoh" Kong prefix.
|
|
||||||
auth_token: The raw JWT token string (without "Bearer " prefix).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The plain-text CSRF token to send in the X-CSRF-Token header.
|
|
||||||
"""
|
|
||||||
res = requests.post(
|
|
||||||
f"{AUTH_BASE_URL}/csrf-token",
|
|
||||||
headers={
|
|
||||||
"Authorization": f"Bearer {auth_token}",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"X-Target-Path": path,
|
|
||||||
},
|
|
||||||
json={
|
|
||||||
"_csrf_request": True,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
print(f"Requested CSRF token for {path}, got status {res.status_code}, response: {res.text}")
|
|
||||||
assert res.status_code == 200, f"Failed to get CSRF token for {path}: {res.status_code} {res.text}"
|
|
||||||
data = res.json()
|
data = res.json()
|
||||||
# Response shape: {"data": {"csrf_token": "..."}, ...}
|
return data['data']['csrf_token']
|
||||||
return data["data"]["csrf_token"]
|
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
# Overhaul Schedules
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
|
|
||||||
def test_get_overhaul_schedules():
|
def test_get_overhaul_schedules():
|
||||||
"""GET /overhaul-schedules — require_any_role blocks Management"""
|
|
||||||
for user in USER_LIST:
|
for user in USER_LIST:
|
||||||
res = requests.get(f"{BASE_URL}/overhaul-schedules", headers=user_auth_headers(user["authorization_token"]))
|
res = requests.get(f'{BASE_URL}/overhaul-schedules', headers=user_auth_headers(user['authorization_token']))
|
||||||
expected = 200 if user["name"] in NON_MANAGEMENT else 403
|
expected = 200 if user['name'] in NON_MANAGEMENT else 403
|
||||||
assert res.status_code == expected, f"[{user['name']}] expected {expected}, got {res.status_code}"
|
assert res.status_code == expected, f"[{user['name']}] expected {expected}, got {res.status_code}"
|
||||||
|
|
||||||
def test_get_overhaul_schedules_by_id():
|
def test_get_overhaul_schedules_by_id():
|
||||||
"""GET /overhaul-schedules/{overhaul_session_id} — require_any_role blocks Management"""
|
|
||||||
for user in USER_LIST:
|
for user in USER_LIST:
|
||||||
res = requests.get(
|
res = requests.get(f'{BASE_URL}/overhaul-schedules/{OVERHAUL_SESSION_ID}', headers=user_auth_headers(user['authorization_token']))
|
||||||
f"{BASE_URL}/overhaul-schedules/{OVERHAUL_SESSION_ID}",
|
expected = 200 if user['name'] in NON_MANAGEMENT else 403
|
||||||
headers=user_auth_headers(user["authorization_token"]),
|
|
||||||
)
|
|
||||||
expected = 200 if user["name"] in NON_MANAGEMENT else 403
|
|
||||||
assert res.status_code == expected, f"[{user['name']}] expected {expected}, got {res.status_code}: {res.text}"
|
assert res.status_code == expected, f"[{user['name']}] expected {expected}, got {res.status_code}: {res.text}"
|
||||||
|
|
||||||
def test_post_overhaul_schedules_update():
|
def test_post_overhaul_schedules_update():
|
||||||
"""POST /overhaul-schedules/update/{scope_id} — require_any_role + EDIT blocks Management
|
payload = {'duration_oh': 720, 'crew_number': 10, 'status': 'Upcoming'}
|
||||||
Payload: duration_oh (optional int), crew_number (optional int), status (optional str)
|
for user in USER_LIST:
|
||||||
"""
|
csrf_token = get_csrf_token('/overhaul-schedules/update', user['authorization_token'])
|
||||||
payload = {
|
headers = {**user_auth_headers(user['authorization_token']), 'X-CSRF-Token': csrf_token}
|
||||||
"duration_oh": 720,
|
res = requests.post(f'{BASE_URL}/overhaul-schedules/update/{OVERHAUL_SESSION_ID}', json=payload, headers=headers)
|
||||||
"crew_number": 10,
|
|
||||||
"status": "Upcoming",
|
|
||||||
}
|
|
||||||
for user in USER_LIST:
|
|
||||||
csrf_token = get_csrf_token("/overhaul-schedules/update", user["authorization_token"])
|
|
||||||
headers = {**user_auth_headers(user["authorization_token"]), "X-CSRF-Token": csrf_token}
|
|
||||||
res = requests.post(
|
|
||||||
f"{BASE_URL}/overhaul-schedules/update/{OVERHAUL_SESSION_ID}",
|
|
||||||
json=payload,
|
|
||||||
headers=headers,
|
|
||||||
)
|
|
||||||
print(f"[{user['name']}], got {res.status_code}, response: {res.text}")
|
print(f"[{user['name']}], got {res.status_code}, response: {res.text}")
|
||||||
if user["name"] in NON_MANAGEMENT:
|
if user['name'] in NON_MANAGEMENT:
|
||||||
assert res.status_code in (200, 204, 404), f"[{user['name']}] expected 200/204/404, got {res.status_code}"
|
assert res.status_code in (200, 204, 404), f"[{user['name']}] expected 200/204/404, got {res.status_code}"
|
||||||
else:
|
else:
|
||||||
assert res.status_code == 403, f"[{user['name']}] expected 403, got {res.status_code}"
|
assert res.status_code == 403, f"[{user['name']}] expected 403, got {res.status_code}"
|
||||||
|
|
||||||
def test_get_overhaul_schedules_history():
|
def test_get_overhaul_schedules_history():
|
||||||
"""GET /overhaul-schedules/history — require_any_role blocks Management"""
|
|
||||||
for user in USER_LIST:
|
for user in USER_LIST:
|
||||||
res = requests.get(f"{BASE_URL}/overhaul-schedules/history", headers=user_auth_headers(user["authorization_token"]))
|
res = requests.get(f'{BASE_URL}/overhaul-schedules/history', headers=user_auth_headers(user['authorization_token']))
|
||||||
expected = 200 if user["name"] in NON_MANAGEMENT else 403
|
expected = 200 if user['name'] in NON_MANAGEMENT else 403
|
||||||
assert res.status_code == expected, f"[{user['name']}] expected {expected}, got {res.status_code}"
|
assert res.status_code == expected, f"[{user['name']}] expected {expected}, got {res.status_code}"
|
||||||
|
|
||||||
def test_post_overhaul_schedules_delete():
|
def test_post_overhaul_schedules_delete():
|
||||||
"""POST /overhaul-schedules/delete/{scope_id} — require_any_role + DELETE blocks Management
|
for user in USER_LIST:
|
||||||
No body required. CSRF token required in production.
|
csrf_token = get_csrf_token(f'/overhaul-schedules/delete/{OVERHAUL_SESSION_ID}', user['authorization_token'])
|
||||||
"""
|
headers = {**user_auth_headers(user['authorization_token']), 'X-CSRF-Token': csrf_token}
|
||||||
for user in USER_LIST:
|
res = requests.post(f'{BASE_URL}/overhaul-schedules/delete/{OVERHAUL_SESSION_ID}', headers=headers)
|
||||||
csrf_token = get_csrf_token(f"/overhaul-schedules/delete/{OVERHAUL_SESSION_ID}", user["authorization_token"])
|
|
||||||
headers = {**user_auth_headers(user["authorization_token"]), "X-CSRF-Token": csrf_token}
|
|
||||||
res = requests.post(
|
|
||||||
f"{BASE_URL}/overhaul-schedules/delete/{OVERHAUL_SESSION_ID}",
|
|
||||||
headers=headers,
|
|
||||||
)
|
|
||||||
print(f"[{user['name']}], got {res.status_code}, response: {res.text}")
|
print(f"[{user['name']}], got {res.status_code}, response: {res.text}")
|
||||||
if user["name"] in NON_MANAGEMENT:
|
if user['name'] in NON_MANAGEMENT:
|
||||||
assert res.status_code in (200, 204, 404), f"[{user['name']}] expected 200/204/404, got {res.status_code}"
|
assert res.status_code in (200, 204, 404), f"[{user['name']}] expected 200/204/404, got {res.status_code}"
|
||||||
else:
|
else:
|
||||||
assert res.status_code == 403, f"[{user['name']}] expected 403, got {res.status_code}"
|
assert res.status_code == 403, f"[{user['name']}] expected 403, got {res.status_code}"
|
||||||
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
# Overhauls
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
|
|
||||||
def test_get_overhauls():
|
def test_get_overhauls():
|
||||||
"""GET /overhauls — required_permission(READ) only; Management has READ in ACL"""
|
|
||||||
for user in USER_LIST:
|
for user in USER_LIST:
|
||||||
res = requests.get(f"{BASE_URL}/overhauls", headers=user_auth_headers(user["authorization_token"]))
|
res = requests.get(f'{BASE_URL}/overhauls', headers=user_auth_headers(user['authorization_token']))
|
||||||
assert res.status_code == 200, f"[{user['name']}] expected 200, got {res.status_code}"
|
assert res.status_code == 200, f"[{user['name']}] expected 200, got {res.status_code}"
|
||||||
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
# Overhaul Gantt
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
|
|
||||||
def test_get_overhaul_gantt_spreadsheet():
|
def test_get_overhaul_gantt_spreadsheet():
|
||||||
"""GET /overhaul-gantt/spreadsheet — no role guard; all roles allowed"""
|
|
||||||
for user in USER_LIST:
|
for user in USER_LIST:
|
||||||
res = requests.get(
|
res = requests.get(f'{BASE_URL}/overhaul-gantt/spreadsheet', headers=user_auth_headers(user['authorization_token']))
|
||||||
f"{BASE_URL}/overhaul-gantt/spreadsheet",
|
|
||||||
headers=user_auth_headers(user["authorization_token"]),
|
|
||||||
)
|
|
||||||
assert res.status_code == 200, f"[{user['name']}] expected 200, got {res.status_code}: {res.text}"
|
assert res.status_code == 200, f"[{user['name']}] expected 200, got {res.status_code}: {res.text}"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
# Spareparts
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
|
|
||||||
def test_get_spareparts():
|
def test_get_spareparts():
|
||||||
"""GET /spareparts — require_any_role blocks Management"""
|
|
||||||
for user in USER_LIST:
|
for user in USER_LIST:
|
||||||
res = requests.get(f"{BASE_URL}/spareparts", headers=user_auth_headers(user["authorization_token"]))
|
res = requests.get(f'{BASE_URL}/spareparts', headers=user_auth_headers(user['authorization_token']))
|
||||||
expected = 200 if user["name"] in NON_MANAGEMENT else 403
|
expected = 200 if user['name'] in NON_MANAGEMENT else 403
|
||||||
assert res.status_code == expected, f"[{user['name']}] expected {expected}, got {res.status_code}"
|
assert res.status_code == expected, f"[{user['name']}] expected {expected}, got {res.status_code}"
|
||||||
|
|
||||||
def test_get_equipment_spareparts():
|
def test_get_equipment_spareparts():
|
||||||
"""GET /equipment-spareparts/{location_tag} — require_any_role blocks Management"""
|
|
||||||
for user in USER_LIST:
|
for user in USER_LIST:
|
||||||
res = requests.get(
|
res = requests.get(f'{BASE_URL}/equipment-spareparts/{LOCATION_TAG}', headers=user_auth_headers(user['authorization_token']))
|
||||||
f"{BASE_URL}/equipment-spareparts/{LOCATION_TAG}",
|
expected = 200 if user['name'] in NON_MANAGEMENT else 403
|
||||||
headers=user_auth_headers(user["authorization_token"]),
|
|
||||||
)
|
|
||||||
expected = 200 if user["name"] in NON_MANAGEMENT else 403
|
|
||||||
assert res.status_code == expected, f"[{user['name']}] expected {expected}, got {res.status_code}"
|
assert res.status_code == expected, f"[{user['name']}] expected {expected}, got {res.status_code}"
|
||||||
|
|
||||||
def test_post_spareparts():
|
def test_post_spareparts():
|
||||||
"""POST /spareparts — require_any_role blocks Management
|
payload = {'itemnum': ITEMNUM, 'remark': 'Test remark for integration test'}
|
||||||
Payload: itemnum (str), remark (str). CSRF token required.
|
for user in USER_LIST:
|
||||||
"""
|
csrf_token = get_csrf_token('/spareparts', user['authorization_token'])
|
||||||
payload = {
|
headers = {**user_auth_headers(user['authorization_token']), 'X-CSRF-Token': csrf_token}
|
||||||
"itemnum": ITEMNUM,
|
res = requests.post(f'{BASE_URL}/spareparts', json=payload, headers=headers)
|
||||||
"remark": "Test remark for integration test",
|
if user['name'] in NON_MANAGEMENT:
|
||||||
}
|
|
||||||
for user in USER_LIST:
|
|
||||||
csrf_token = get_csrf_token("/spareparts", user["authorization_token"])
|
|
||||||
headers = {**user_auth_headers(user["authorization_token"]), "X-CSRF-Token": csrf_token}
|
|
||||||
res = requests.post(f"{BASE_URL}/spareparts", json=payload, headers=headers)
|
|
||||||
if user["name"] in NON_MANAGEMENT:
|
|
||||||
assert res.status_code in (200, 201), f"[{user['name']}] expected 200/201, got {res.status_code}"
|
assert res.status_code in (200, 201), f"[{user['name']}] expected 200/201, got {res.status_code}"
|
||||||
else:
|
else:
|
||||||
assert res.status_code == 403, f"[{user['name']}] expected 403, got {res.status_code}"
|
assert res.status_code == 403, f"[{user['name']}] expected 403, got {res.status_code}"
|
||||||
|
|
||||||
def test_post_overhaul_gantt_spreadsheet():
|
def test_post_overhaul_gantt_spreadsheet():
|
||||||
"""POST /overhaul-gantt/spreadsheet — no role guard; all roles allowed
|
payload = {'spreadsheet_link': SPREADSHEET_LINK}
|
||||||
Payload: spreadsheet_link (str). CSRF token required in production.
|
for user in USER_LIST:
|
||||||
"""
|
csrf_token = get_csrf_token('/overhaul-gantt/spreadsheet', user['authorization_token'])
|
||||||
payload = {
|
headers = {**user_auth_headers(user['authorization_token']), 'X-CSRF-Token': csrf_token}
|
||||||
"spreadsheet_link": SPREADSHEET_LINK,
|
res = requests.post(f'{BASE_URL}/overhaul-gantt/spreadsheet', json=payload, headers=headers)
|
||||||
}
|
|
||||||
for user in USER_LIST:
|
|
||||||
csrf_token = get_csrf_token("/overhaul-gantt/spreadsheet", user["authorization_token"])
|
|
||||||
headers = {**user_auth_headers(user["authorization_token"]), "X-CSRF-Token": csrf_token}
|
|
||||||
res = requests.post(
|
|
||||||
f"{BASE_URL}/overhaul-gantt/spreadsheet",
|
|
||||||
json=payload,
|
|
||||||
headers=headers,
|
|
||||||
)
|
|
||||||
assert res.status_code in (200, 201), f"[{user['name']}] expected 200/201, got {res.status_code}"
|
assert res.status_code in (200, 201), f"[{user['name']}] expected 200/201, got {res.status_code}"
|
||||||
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
# Scope Equipments
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
|
|
||||||
def test_get_scope_equipments():
|
def test_get_scope_equipments():
|
||||||
"""GET /scope-equipments?page=1 — require_any_role blocks Management"""
|
|
||||||
for user in USER_LIST:
|
for user in USER_LIST:
|
||||||
res = requests.get(f"{BASE_URL}/scope-equipments?page=1", headers=user_auth_headers(user["authorization_token"]))
|
res = requests.get(f'{BASE_URL}/scope-equipments?page=1', headers=user_auth_headers(user['authorization_token']))
|
||||||
expected = 200 if user["name"] in NON_MANAGEMENT else 403
|
expected = 200 if user['name'] in NON_MANAGEMENT else 403
|
||||||
assert res.status_code == expected, f"[{user['name']}] expected {expected}, got {res.status_code}"
|
assert res.status_code == expected, f"[{user['name']}] expected {expected}, got {res.status_code}"
|
||||||
|
|
||||||
|
|
||||||
def test_get_scope_equipments_available():
|
def test_get_scope_equipments_available():
|
||||||
"""GET /scope-equipments/available/{scope_name} — require_any_role blocks Management"""
|
|
||||||
for user in USER_LIST:
|
for user in USER_LIST:
|
||||||
res = requests.get(
|
res = requests.get(f'{BASE_URL}/scope-equipments/available/{SCOPE_NAME}', headers=user_auth_headers(user['authorization_token']))
|
||||||
f"{BASE_URL}/scope-equipments/available/{SCOPE_NAME}",
|
expected = 200 if user['name'] in NON_MANAGEMENT else 403
|
||||||
headers=user_auth_headers(user["authorization_token"]),
|
|
||||||
)
|
|
||||||
expected = 200 if user["name"] in NON_MANAGEMENT else 403
|
|
||||||
assert res.status_code == expected, f"[{user['name']}] expected {expected}, got {res.status_code}"
|
assert res.status_code == expected, f"[{user['name']}] expected {expected}, got {res.status_code}"
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
# Overhaul Schedules (additional)
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
|
|
||||||
def test_get_overhaul_schedules():
|
def test_get_overhaul_schedules():
|
||||||
"""GET /overhaul-schedules — require_any_role blocks Management"""
|
|
||||||
for user in USER_LIST:
|
for user in USER_LIST:
|
||||||
res = requests.get(f"{BASE_URL}/overhaul-schedules", headers=user_auth_headers(user["authorization_token"]))
|
res = requests.get(f'{BASE_URL}/overhaul-schedules', headers=user_auth_headers(user['authorization_token']))
|
||||||
expected = 200 if user["name"] in NON_MANAGEMENT else 403
|
expected = 200 if user['name'] in NON_MANAGEMENT else 403
|
||||||
assert res.status_code == expected, f"[{user['name']}] expected {expected}, got {res.status_code}: {res.text}"
|
assert res.status_code == expected, f"[{user['name']}] expected {expected}, got {res.status_code}: {res.text}"
|
||||||
|
|
||||||
|
|
||||||
def test_get_overhaul_schedules_history():
|
def test_get_overhaul_schedules_history():
|
||||||
"""GET /overhaul-schedules/history — require_any_role blocks Management"""
|
|
||||||
for user in USER_LIST:
|
for user in USER_LIST:
|
||||||
res = requests.get(f"{BASE_URL}/overhaul-schedules/history", headers=user_auth_headers(user["authorization_token"]))
|
res = requests.get(f'{BASE_URL}/overhaul-schedules/history', headers=user_auth_headers(user['authorization_token']))
|
||||||
expected = 200 if user["name"] in NON_MANAGEMENT else 403
|
expected = 200 if user['name'] in NON_MANAGEMENT else 403
|
||||||
assert res.status_code == expected, f"[{user['name']}] expected {expected}, got {res.status_code}: {res.text}"
|
assert res.status_code == expected, f"[{user['name']}] expected {expected}, got {res.status_code}: {res.text}"
|
||||||
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
# Overhauls sub-routes
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
|
|
||||||
def test_get_overhauls_system_components():
|
def test_get_overhauls_system_components():
|
||||||
"""GET /overhauls/system-components — required_permission(READ); Management has READ"""
|
|
||||||
for user in USER_LIST:
|
for user in USER_LIST:
|
||||||
res = requests.get(f"{BASE_URL}/overhauls/system-components", headers=user_auth_headers(user["authorization_token"]))
|
res = requests.get(f'{BASE_URL}/overhauls/system-components', headers=user_auth_headers(user['authorization_token']))
|
||||||
assert res.status_code == 200, f"[{user['name']}] expected 200, got {res.status_code}: {res.text}"
|
assert res.status_code == 200, f"[{user['name']}] expected 200, got {res.status_code}: {res.text}"
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
# Workscopes
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
|
|
||||||
def test_get_workscopes():
|
def test_get_workscopes():
|
||||||
"""GET /workscopes — no role guard; all roles allowed"""
|
|
||||||
for user in USER_LIST:
|
for user in USER_LIST:
|
||||||
res = requests.get(f"{BASE_URL}/workscopes", headers=user_auth_headers(user["authorization_token"]))
|
res = requests.get(f'{BASE_URL}/workscopes', headers=user_auth_headers(user['authorization_token']))
|
||||||
assert res.status_code == 200, f"[{user['name']}] expected 200, got {res.status_code}: {res.text}"
|
assert res.status_code == 200, f"[{user['name']}] expected 200, got {res.status_code}: {res.text}"
|
||||||
|
|
||||||
def test_get_workscopes_by_id():
|
def test_get_workscopes_by_id():
|
||||||
"""GET /workscopes/{scope_equipment_activity_id}?activity_id=... — no role guard; all roles allowed
|
for user in USER_LIST:
|
||||||
"""
|
res = requests.get(f'{BASE_URL}/workscopes/{SCOPE_EQUIPMENT_ACTIVITY_ID}', params={'activity_id': SCOPE_EQUIPMENT_ACTIVITY_ID}, headers=user_auth_headers(user['authorization_token']))
|
||||||
for user in USER_LIST:
|
|
||||||
res = requests.get(
|
|
||||||
f"{BASE_URL}/workscopes/{SCOPE_EQUIPMENT_ACTIVITY_ID}",
|
|
||||||
params={"activity_id": SCOPE_EQUIPMENT_ACTIVITY_ID},
|
|
||||||
headers=user_auth_headers(user["authorization_token"]),
|
|
||||||
)
|
|
||||||
assert res.status_code in (200, 404), f"[{user['name']}] expected 200/404, got {res.status_code}: {res.text}"
|
assert res.status_code in (200, 404), f"[{user['name']}] expected 200/404, got {res.status_code}: {res.text}"
|
||||||
|
|
||||||
def test_post_workscopes_update():
|
def test_post_workscopes_update():
|
||||||
"""POST /workscopes/update/{scope_equipment_activity_id} — no role guard; all roles allowed
|
payload = {'description': 'Updated workscope description'}
|
||||||
Payload: description (str)
|
for user in USER_LIST:
|
||||||
"""
|
csrf_token = get_csrf_token('/workscopes/update', user['authorization_token'])
|
||||||
payload = {
|
headers = {**user_auth_headers(user['authorization_token']), 'X-CSRF-Token': csrf_token}
|
||||||
"description": "Updated workscope description",
|
res = requests.post(f'{BASE_URL}/workscopes/update/{SCOPE_EQUIPMENT_ACTIVITY_ID}', json=payload, params={'activity_id': SCOPE_EQUIPMENT_ACTIVITY_ID}, headers=headers)
|
||||||
}
|
|
||||||
for user in USER_LIST:
|
|
||||||
csrf_token = get_csrf_token("/workscopes/update", user["authorization_token"])
|
|
||||||
headers = {**user_auth_headers(user["authorization_token"]), "X-CSRF-Token": csrf_token}
|
|
||||||
res = requests.post(
|
|
||||||
f"{BASE_URL}/workscopes/update/{SCOPE_EQUIPMENT_ACTIVITY_ID}",
|
|
||||||
json=payload,
|
|
||||||
params={"activity_id": SCOPE_EQUIPMENT_ACTIVITY_ID},
|
|
||||||
headers=headers,
|
|
||||||
)
|
|
||||||
print(f"[{user['name']}], got {res.status_code}, response: {res.text}")
|
print(f"[{user['name']}], got {res.status_code}, response: {res.text}")
|
||||||
assert res.status_code in (200, 404), f"[{user['name']}] expected 200/404, got {res.status_code}"
|
assert res.status_code in (200, 404), f"[{user['name']}] expected 200/404, got {res.status_code}"
|
||||||
|
|
||||||
|
|
||||||
def test_post_workscopes_delete():
|
def test_post_workscopes_delete():
|
||||||
"""POST /workscopes/delete/{scope_equipment_activity_id} — no role guard; all roles allowed
|
for user in USER_LIST:
|
||||||
No body required.
|
csrf_token = get_csrf_token('/workscopes/delete', user['authorization_token'])
|
||||||
"""
|
headers = {**user_auth_headers(user['authorization_token']), 'X-CSRF-Token': csrf_token}
|
||||||
for user in USER_LIST:
|
res = requests.post(f'{BASE_URL}/workscopes/delete/{SCOPE_EQUIPMENT_ACTIVITY_ID}', params={'activity_id': SCOPE_EQUIPMENT_ACTIVITY_ID}, headers=headers)
|
||||||
csrf_token = get_csrf_token("/workscopes/delete", user["authorization_token"])
|
|
||||||
headers = {**user_auth_headers(user["authorization_token"]), "X-CSRF-Token": csrf_token}
|
|
||||||
res = requests.post(
|
|
||||||
f"{BASE_URL}/workscopes/delete/{SCOPE_EQUIPMENT_ACTIVITY_ID}",
|
|
||||||
params={"activity_id": SCOPE_EQUIPMENT_ACTIVITY_ID},
|
|
||||||
headers=headers,
|
|
||||||
)
|
|
||||||
print(f"[{user['name']}], got {res.status_code}, response: {res.text}")
|
print(f"[{user['name']}], got {res.status_code}, response: {res.text}")
|
||||||
assert res.status_code in (200, 204, 404), f"[{user['name']}] expected 200/204/404, got {res.status_code}"
|
assert res.status_code in (200, 204, 404), f"[{user['name']}] expected 200/204/404, got {res.status_code}"
|
||||||
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
# Overhaul Activity
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
|
|
||||||
def test_get_overhaul_activity():
|
def test_get_overhaul_activity():
|
||||||
"""GET /overhaul-activity/{overhaul_session} — require_any_role blocks Management"""
|
|
||||||
for user in USER_LIST:
|
for user in USER_LIST:
|
||||||
res = requests.get(
|
res = requests.get(f'{BASE_URL}/overhaul-activity/{OVERHAUL_SESSION_ID}', headers=user_auth_headers(user['authorization_token']))
|
||||||
f"{BASE_URL}/overhaul-activity/{OVERHAUL_SESSION_ID}",
|
expected = 200 if user['name'] in NON_MANAGEMENT else 403
|
||||||
headers=user_auth_headers(user["authorization_token"]),
|
|
||||||
)
|
|
||||||
expected = 200 if user["name"] in NON_MANAGEMENT else 403
|
|
||||||
assert res.status_code == expected, f"[{user['name']}] expected {expected}, got {res.status_code}"
|
assert res.status_code == expected, f"[{user['name']}] expected {expected}, got {res.status_code}"
|
||||||
|
|
||||||
def test_post_overhaul_activity_current():
|
def test_post_overhaul_activity_current():
|
||||||
"""POST /overhaul-activity/{session_id} — require_any_role blocks Management
|
payload = {'location_tags': [LOCATION_TAG]}
|
||||||
Payload: location_tags (List[str]). CSRF token required.
|
for user in USER_LIST:
|
||||||
"""
|
csrf_token = get_csrf_token('/overhaul-activity', user['authorization_token'])
|
||||||
payload = {
|
headers = {**user_auth_headers(user['authorization_token']), 'X-CSRF-Token': csrf_token}
|
||||||
"location_tags": [LOCATION_TAG],
|
res = requests.post(f'{BASE_URL}/overhaul-activity/{OVERHAUL_SESSION_ID}', json=payload, headers=headers)
|
||||||
}
|
if user['name'] in NON_MANAGEMENT:
|
||||||
for user in USER_LIST:
|
|
||||||
csrf_token = get_csrf_token("/overhaul-activity", user["authorization_token"])
|
|
||||||
headers = {**user_auth_headers(user["authorization_token"]), "X-CSRF-Token": csrf_token}
|
|
||||||
res = requests.post(
|
|
||||||
f"{BASE_URL}/overhaul-activity/{OVERHAUL_SESSION_ID}",
|
|
||||||
json=payload,
|
|
||||||
headers=headers,
|
|
||||||
)
|
|
||||||
if user["name"] in NON_MANAGEMENT:
|
|
||||||
assert res.status_code in (200, 201), f"[{user['name']}] expected 200/201, got {res.status_code}"
|
assert res.status_code in (200, 201), f"[{user['name']}] expected 200/201, got {res.status_code}"
|
||||||
else:
|
else:
|
||||||
assert res.status_code == 403, f"[{user['name']}] expected 403, got {res.status_code}"
|
assert res.status_code == 403, f"[{user['name']}] expected 403, got {res.status_code}"
|
||||||
|
|
||||||
def test_delete_overhaul_activity():
|
def test_delete_overhaul_activity():
|
||||||
"""POST /overhaul-activity/delete/{overhaul_session}/{location_tag} — require_any_role blocks Management. CSRF required."""
|
for user in USER_LIST:
|
||||||
for user in USER_LIST:
|
csrf_token = get_csrf_token(f'/overhaul-activity/delete/{OVERHAUL_SESSION_ID}/{LOCATION_TAG}', user['authorization_token'])
|
||||||
csrf_token = get_csrf_token(f"/overhaul-activity/delete/{OVERHAUL_SESSION_ID}/{LOCATION_TAG}", user["authorization_token"])
|
headers = {**user_auth_headers(user['authorization_token']), 'X-CSRF-Token': csrf_token}
|
||||||
headers = {**user_auth_headers(user["authorization_token"]), "X-CSRF-Token": csrf_token}
|
res = requests.post(f'{BASE_URL}/overhaul-activity/delete/{OVERHAUL_SESSION_ID}/{LOCATION_TAG}', headers=headers)
|
||||||
res = requests.post(
|
if user['name'] in NON_MANAGEMENT:
|
||||||
f"{BASE_URL}/overhaul-activity/delete/{OVERHAUL_SESSION_ID}/{LOCATION_TAG}",
|
|
||||||
headers=headers,
|
|
||||||
)
|
|
||||||
if user["name"] in NON_MANAGEMENT:
|
|
||||||
assert res.status_code in (200, 204, 404), f"[{user['name']}] expected 200/204/404, got {res.status_code}"
|
assert res.status_code in (200, 204, 404), f"[{user['name']}] expected 200/204/404, got {res.status_code}"
|
||||||
else:
|
else:
|
||||||
assert res.status_code == 403, f"[{user['name']}] expected 403, got {res.status_code}"
|
assert res.status_code == 403, f"[{user['name']}] expected 403, got {res.status_code}"
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
# Scope Equipments History
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
|
|
||||||
def test_get_scope_equipments_history():
|
def test_get_scope_equipments_history():
|
||||||
"""GET /scope-equipments/history/{oh_session_id} — require_any_role blocks Management"""
|
|
||||||
for user in USER_LIST:
|
for user in USER_LIST:
|
||||||
res = requests.get(
|
res = requests.get(f'{BASE_URL}/scope-equipments/history/{OVERHAUL_SESSION_ID}', headers=user_auth_headers(user['authorization_token']))
|
||||||
f"{BASE_URL}/scope-equipments/history/{OVERHAUL_SESSION_ID}",
|
expected = 200 if user['name'] in NON_MANAGEMENT else 403
|
||||||
headers=user_auth_headers(user["authorization_token"]),
|
|
||||||
)
|
|
||||||
expected = 200 if user["name"] in NON_MANAGEMENT else 403
|
|
||||||
assert res.status_code == expected, f"[{user['name']}] expected {expected}, got {res.status_code}: {res.text}"
|
assert res.status_code == expected, f"[{user['name']}] expected {expected}, got {res.status_code}: {res.text}"
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
# Equipment Workscopes
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
|
|
||||||
def test_get_equipment_workscopes():
|
def test_get_equipment_workscopes():
|
||||||
"""GET /equipment-workscopes/{location_tag} — no role guard; all roles allowed
|
for user in USER_LIST:
|
||||||
Query params: scope (optional str)
|
res = requests.get(f'{BASE_URL}/equipment-workscopes/{LOCATION_TAG}', headers=user_auth_headers(user['authorization_token']))
|
||||||
"""
|
|
||||||
for user in USER_LIST:
|
|
||||||
res = requests.get(
|
|
||||||
f"{BASE_URL}/equipment-workscopes/{LOCATION_TAG}",
|
|
||||||
headers=user_auth_headers(user["authorization_token"]),
|
|
||||||
)
|
|
||||||
assert res.status_code == 200, f"[{user['name']}] expected 200, got {res.status_code}: {res.text}"
|
assert res.status_code == 200, f"[{user['name']}] expected 200, got {res.status_code}: {res.text}"
|
||||||
|
|
||||||
|
|
||||||
def test_post_equipment_workscopes():
|
def test_post_equipment_workscopes():
|
||||||
"""POST /equipment-workscopes/{assetnum} — no role guard; all roles allowed
|
payload = {'job_ids': []}
|
||||||
Payload: job_ids (List[UUID])
|
for user in USER_LIST:
|
||||||
"""
|
csrf_token = get_csrf_token('/equipment-workscopes', user['authorization_token'])
|
||||||
payload = {
|
headers = {**user_auth_headers(user['authorization_token']), 'X-CSRF-Token': csrf_token}
|
||||||
"job_ids": [],
|
res = requests.post(f'{BASE_URL}/equipment-workscopes/{ASSET_NUM}', json=payload, headers=headers)
|
||||||
}
|
|
||||||
for user in USER_LIST:
|
|
||||||
csrf_token = get_csrf_token("/equipment-workscopes", user["authorization_token"])
|
|
||||||
headers = {**user_auth_headers(user["authorization_token"]), "X-CSRF-Token": csrf_token}
|
|
||||||
res = requests.post(
|
|
||||||
f"{BASE_URL}/equipment-workscopes/{ASSET_NUM}",
|
|
||||||
json=payload,
|
|
||||||
headers=headers,
|
|
||||||
)
|
|
||||||
print(f"[{user['name']}], got {res.status_code}, response: {res.text}")
|
print(f"[{user['name']}], got {res.status_code}, response: {res.text}")
|
||||||
assert res.status_code in (200, 201), f"[{user['name']}] expected 200/201, got {res.status_code}"
|
assert res.status_code in (200, 201), f"[{user['name']}] expected 200/201, got {res.status_code}"
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
# Calculation — Target Reliability
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
|
|
||||||
def test_get_calculation_target_reliability():
|
def test_get_calculation_target_reliability():
|
||||||
"""GET /calculation/target-reliability — required_permission(READ); Management has READ
|
for user in USER_LIST:
|
||||||
Query params: oh_session_id (required), eaf_input (float), duration (int), simulation_id (optional), cut_hours (optional)
|
res = requests.get(f'{BASE_URL}/calculation/target-reliability?oh_session_id={OVERHAUL_SESSION_ID}&eaf_input={EAF_INPUT}&duration={DURATION}', headers=user_auth_headers(user['authorization_token']))
|
||||||
"""
|
|
||||||
for user in USER_LIST:
|
|
||||||
res = requests.get(
|
|
||||||
f"{BASE_URL}/calculation/target-reliability"
|
|
||||||
f"?oh_session_id={OVERHAUL_SESSION_ID}&eaf_input={EAF_INPUT}&duration={DURATION}",
|
|
||||||
headers=user_auth_headers(user["authorization_token"]),
|
|
||||||
)
|
|
||||||
assert res.status_code in (200, 400, 404, 425), f"[{user['name']}] expected 200/400/404/425, got {res.status_code}: {res.text}"
|
assert res.status_code in (200, 400, 404, 425), f"[{user['name']}] expected 200/400/404/425, got {res.status_code}: {res.text}"
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
# Calculation — Time Constraint
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
|
|
||||||
def test_get_calculation_time_constraint():
|
def test_get_calculation_time_constraint():
|
||||||
"""GET /calculation/time-constraint — required_permission(READ); Management has READ"""
|
|
||||||
for user in USER_LIST:
|
for user in USER_LIST:
|
||||||
res = requests.get(
|
res = requests.get(f'{BASE_URL}/calculation/time-constraint', headers=user_auth_headers(user['authorization_token']))
|
||||||
f"{BASE_URL}/calculation/time-constraint",
|
|
||||||
headers=user_auth_headers(user["authorization_token"]),
|
|
||||||
)
|
|
||||||
assert res.status_code == 200, f"[{user['name']}] expected 200, got {res.status_code}: {res.text}"
|
assert res.status_code == 200, f"[{user['name']}] expected 200, got {res.status_code}: {res.text}"
|
||||||
|
|
||||||
def test_get_calculation_time_constraint_by_id():
|
def test_get_calculation_time_constraint_by_id():
|
||||||
"""GET /calculation/time-constraint/{calculation_id} — required_permission(READ); Management has READ
|
for user in USER_LIST:
|
||||||
Query params: risk_cost (optional int, default 1)
|
res = requests.get(f'{BASE_URL}/calculation/time-constraint/{CALCULATION_ID}?risk_cost={RISK_COST}', headers=user_auth_headers(user['authorization_token']))
|
||||||
"""
|
|
||||||
for user in USER_LIST:
|
|
||||||
res = requests.get(
|
|
||||||
f"{BASE_URL}/calculation/time-constraint/{CALCULATION_ID}?risk_cost={RISK_COST}",
|
|
||||||
headers=user_auth_headers(user["authorization_token"]),
|
|
||||||
)
|
|
||||||
assert res.status_code in (200, 404), f"[{user['name']}] expected 200/404, got {res.status_code}: {res.text}"
|
assert res.status_code in (200, 404), f"[{user['name']}] expected 200/404, got {res.status_code}: {res.text}"
|
||||||
|
|
||||||
def test_post_calculation_time_constraint_simulation():
|
def test_post_calculation_time_constraint_simulation():
|
||||||
"""POST /calculation/time-constraint/{calculation_id}/simulation — required_permission(READ); Management has READ
|
payload = {'intervalDays': 365}
|
||||||
Payload: intervalDays (int)
|
for user in USER_LIST:
|
||||||
"""
|
csrf_token = get_csrf_token('/calculation/time-constraint/simulation', user['authorization_token'])
|
||||||
payload = {
|
headers = {**user_auth_headers(user['authorization_token']), 'X-CSRF-Token': csrf_token}
|
||||||
"intervalDays": 365,
|
res = requests.post(f'{BASE_URL}/calculation/time-constraint/{CALCULATION_ID}/simulation', json=payload, headers=headers)
|
||||||
}
|
|
||||||
for user in USER_LIST:
|
|
||||||
csrf_token = get_csrf_token("/calculation/time-constraint/simulation", user["authorization_token"])
|
|
||||||
headers = {**user_auth_headers(user["authorization_token"]), "X-CSRF-Token": csrf_token}
|
|
||||||
res = requests.post(
|
|
||||||
f"{BASE_URL}/calculation/time-constraint/{CALCULATION_ID}/simulation",
|
|
||||||
json=payload,
|
|
||||||
headers=headers,
|
|
||||||
)
|
|
||||||
print(f"[{user['name']}], got {res.status_code}, response: {res.text}")
|
print(f"[{user['name']}], got {res.status_code}, response: {res.text}")
|
||||||
assert res.status_code in (200, 201, 404), f"[{user['name']}] expected 200/201/404, got {res.status_code}"
|
assert res.status_code in (200, 201, 404), f"[{user['name']}] expected 200/201/404, got {res.status_code}"
|
||||||
|
|
||||||
def test_post_calculation_time_constraint_update():
|
def test_post_calculation_time_constraint_update():
|
||||||
"""POST /calculation/time-constraint/update/{calculation_id} — required_permission(EDIT); Management has no EDIT
|
payload = [{'location_tag': LOCATION_TAG, 'name': 'Test Equipment', 'is_included': True}]
|
||||||
Payload: List of { location_tag (str), name (str), is_included (bool) }. CSRF token required in production.
|
for user in USER_LIST:
|
||||||
"""
|
csrf_token = get_csrf_token(f'/calculation/time-constraint/update/{CALCULATION_ID}', user['authorization_token'])
|
||||||
payload = [
|
headers = {**user_auth_headers(user['authorization_token']), 'X-CSRF-Token': csrf_token}
|
||||||
{"location_tag": LOCATION_TAG, "name": "Test Equipment", "is_included": True},
|
res = requests.post(f'{BASE_URL}/calculation/time-constraint/update/{CALCULATION_ID}', json=payload, headers=headers)
|
||||||
]
|
|
||||||
for user in USER_LIST:
|
|
||||||
csrf_token = get_csrf_token(f"/calculation/time-constraint/update/{CALCULATION_ID}", user["authorization_token"])
|
|
||||||
headers = {**user_auth_headers(user["authorization_token"]), "X-CSRF-Token": csrf_token}
|
|
||||||
res = requests.post(
|
|
||||||
f"{BASE_URL}/calculation/time-constraint/update/{CALCULATION_ID}",
|
|
||||||
json=payload,
|
|
||||||
headers=headers,
|
|
||||||
)
|
|
||||||
print(f"[{user['name']}], got {res.status_code}, response: {res.text}")
|
print(f"[{user['name']}], got {res.status_code}, response: {res.text}")
|
||||||
if user["name"] in NON_MANAGEMENT:
|
if user['name'] in NON_MANAGEMENT:
|
||||||
assert res.status_code in (200, 204, 404), f"[{user['name']}] expected 200/204/404, got {res.status_code}"
|
assert res.status_code in (200, 204, 404), f"[{user['name']}] expected 200/204/404, got {res.status_code}"
|
||||||
else:
|
else:
|
||||||
assert res.status_code == 403, f"[{user['name']}] expected 403, got {res.status_code}"
|
assert res.status_code == 403, f"[{user['name']}] expected 403, got {res.status_code}"
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
# Equipment Activities / Workscopes
|
|
||||||
# ─────────────────────────────────────────────
|
|
||||||
|
|
||||||
def test_get_equipment_workscopes():
|
def test_get_equipment_workscopes():
|
||||||
"""GET /equipment-workscopes/{location_tag} — require_any_role blocks Management"""
|
|
||||||
for user in USER_LIST:
|
for user in USER_LIST:
|
||||||
res = requests.get(
|
res = requests.get(f'{BASE_URL}/equipment-workscopes/{LOCATION_TAG}', headers=user_auth_headers(user['authorization_token']))
|
||||||
f"{BASE_URL}/equipment-workscopes/{LOCATION_TAG}",
|
|
||||||
headers=user_auth_headers(user["authorization_token"]),
|
|
||||||
)
|
|
||||||
assert res.status_code == 200, f"[{user['name']}] expected 200, got {res.status_code}"
|
assert res.status_code == 200, f"[{user['name']}] expected 200, got {res.status_code}"
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue