From 4ce108defaa200830edebae8444e71948de6e044 Mon Sep 17 00:00:00 2001 From: Cizz22 Date: Thu, 9 Apr 2026 12:50:28 +0700 Subject: [PATCH] feat: add month and year fields to master data model and implement optional direct Aeros API connectivity --- src/aeros_utils.py | 90 +++++++++++++++++++----- src/config.py | 4 +- src/dashboard_model/model.py | 2 + src/dashboard_model/router.py | 2 + src/dashboard_model/schema_masterdata.py | 14 ++-- src/dashboard_model/service.py | 8 ++- 6 files changed, 96 insertions(+), 24 deletions(-) diff --git a/src/aeros_utils.py b/src/aeros_utils.py index 080dfa8..b94979a 100644 --- a/src/aeros_utils.py +++ b/src/aeros_utils.py @@ -1,15 +1,42 @@ import anyio -from licaeros import LicensedSession, device_fingerprint_hex -from src.config import AEROS_BASE_URL, WINDOWS_AEROS_BASE_URL, VAULT_URL, ROLE_ID, SECRET_ID, AEROS_SECRET_PATH, AEROS_LICENSE_ID, AEROS_LICENSE_SECRET import logging +import httpx +from src.config import ( + AEROS_BASE_URL, + WINDOWS_AEROS_BASE_URL, + VAULT_URL, + ROLE_ID, + SECRET_ID, + AEROS_SECRET_PATH, + AEROS_LICENSE_ID, + AEROS_LICENSE_SECRET, + USE_LICENSE_APP +) from src.utils import get_vault_secrets log = logging.getLogger(__name__) +# Try to import licaeros gracefully +try: + from licaeros import LicensedSession, device_fingerprint_hex + HAS_LICAEROS = True +except ImportError: + HAS_LICAEROS = False + log.warning("licaeros library not found. Falling back to direct httpx calls if enabled.") + # Initialize a global session if possible, or create on demand _aeros_session = None def get_aeros_session(base_url): + global _aeros_session + + if not USE_LICENSE_APP or not HAS_LICAEROS: + if _aeros_session is None or not isinstance(_aeros_session, httpx.Client): + log.info(f"Using direct httpx.Client for base URL: {base_url}") + _aeros_session = httpx.Client(base_url=base_url, timeout=300.0) + return _aeros_session + + # License App path - only proceed if USE_LICENSE_APP is True license_id = AEROS_LICENSE_ID license_secret = AEROS_LICENSE_SECRET @@ -30,10 +57,12 @@ def get_aeros_session(base_url): log.warning("Failed to get Aeros license from Vault, trying local env fallback") if not license_id or not license_secret: - raise Exception("Aeros license ID or Secret not provided (checked Vault and local .env)") + log.warning("Aeros license ID or Secret not provided. Falling back to direct httpx.") + if _aeros_session is None or not isinstance(_aeros_session, httpx.Client): + _aeros_session = httpx.Client(base_url=base_url, timeout=300.0) + return _aeros_session - global _aeros_session - if _aeros_session is None: + if _aeros_session is None or isinstance(_aeros_session, httpx.Client): log.info(f"Initializing LicensedSession with base URL: {base_url}") log.info(f"Encrypted Device ID: {device_fingerprint_hex()}") _aeros_session = LicensedSession( @@ -46,21 +75,48 @@ def get_aeros_session(base_url): async def aeros_post(path: str, json=None, data=None, **kwargs): """ - Asynchronous wrapper for LicensedSession.post + Asynchronous wrapper for Aeros requests (Licensed or Direct) """ - session = get_aeros_session(WINDOWS_AEROS_BASE_URL) - url = f"/api/aeros{path}" - # LicensedSession might not be async-compatible, so we run it in a thread - response = await anyio.to_thread.run_sync( - lambda: session.post(url, json_data=json, data=data, headers=kwargs.get("headers")) - ) + # If not using license app, fetch from AEROS_BASE_URL direcly + target_base_url = WINDOWS_AEROS_BASE_URL if USE_LICENSE_APP and HAS_LICAEROS else AEROS_BASE_URL + session = get_aeros_session(target_base_url) + + # Path logic: License app often uses /api/aeros prefix, direct might not. + # However, to maintain compatibility with existing code calling this with path="/api/Project/ImportAROFile" + # we should decide if we need to adjust the path. + # User said: "system still fetch to aeros direcly not to licence app then lincense app forward it to aeros" + # This implies the license app acts as a proxy for /api/aeros/ paths. + + if USE_LICENSE_APP and HAS_LICAEROS: + url = f"/api/aeros{path}" + response = await anyio.to_thread.run_sync( + lambda: session.post(url, json_data=json, data=data, headers=kwargs.get("headers")) + ) + else: + # Direct fetch from Aeros might have different path structure? + # Assuming direct Aeros path is same as what the license app proxies to. + url = path + response = await anyio.to_thread.run_sync( + lambda: session.post(url, json=json, data=data, headers=kwargs.get("headers")) + ) + return response async def aeros_file_upload(path, file, field_name, filename): - session = get_aeros_session(WINDOWS_AEROS_BASE_URL) - url = f"/api/aeros{path}" - response = await anyio.to_thread.run_sync( - lambda: session.post_multipart(url, file, field_name, filename) - ) + target_base_url = WINDOWS_AEROS_BASE_URL if USE_LICENSE_APP and HAS_LICAEROS else AEROS_BASE_URL + session = get_aeros_session(target_base_url) + + if USE_LICENSE_APP and HAS_LICAEROS: + url = f"/api/aeros{path}" + response = await anyio.to_thread.run_sync( + lambda: session.post_multipart(url, file, field_name, filename) + ) + else: + url = path + files = {field_name: (filename, file)} + response = await anyio.to_thread.run_sync( + lambda: session.post(url, files=files) + ) + return response \ No newline at end of file diff --git a/src/config.py b/src/config.py index f878af9..77cfef0 100644 --- a/src/config.py +++ b/src/config.py @@ -104,4 +104,6 @@ AEROS_LICENSE_SECRET = config("AEROS_LICENSE_SECRET", default=None) VAULT_URL=config('VAULT_URL', default=None) ROLE_ID=config('ROLE_ID', default=None) SECRET_ID=config('SECRET_ID', default=None) -AEROS_SECRET_PATH=config('AEROS_SECRET_PATH', default=None) \ No newline at end of file +AEROS_SECRET_PATH=config('AEROS_SECRET_PATH', default=None) + +USE_LICENSE_APP = config("USE_LICENSE_APP", cast=bool, default=True) \ No newline at end of file diff --git a/src/dashboard_model/model.py b/src/dashboard_model/model.py index 49f4cd6..6aa984f 100644 --- a/src/dashboard_model/model.py +++ b/src/dashboard_model/model.py @@ -8,3 +8,5 @@ class RBDMasterData(Base, DefaultMixin): name = Column(String(255), unique=True, nullable=False, index=True) value_float = Column(Float, nullable=True) value_string = Column(String(255), nullable=True) + month = Column(String(20), nullable=True) + year = Column(String(20), nullable=True) diff --git a/src/dashboard_model/router.py b/src/dashboard_model/router.py index 3ab8627..38404ba 100644 --- a/src/dashboard_model/router.py +++ b/src/dashboard_model/router.py @@ -35,6 +35,8 @@ async def get_dashboard_master_data( ): from src.dashboard_model.service import get_master_data_list result = await get_master_data_list(db_session=db_session) + + return StandardResponse( data=result, message="Master data retrieved successfully" diff --git a/src/dashboard_model/schema_masterdata.py b/src/dashboard_model/schema_masterdata.py index 0ff8fd7..b0ba4c7 100644 --- a/src/dashboard_model/schema_masterdata.py +++ b/src/dashboard_model/schema_masterdata.py @@ -1,14 +1,18 @@ from typing import Optional from pydantic import BaseModel, ConfigDict +from src.models import DefultBase +from uuid import UUID -class RBDMasterDataSchema(BaseModel): - model_config = ConfigDict(from_attributes=True) - - id: str +class RBDMasterDataSchema(DefultBase): + id: UUID name: str value_float: Optional[float] = None value_string: Optional[str] = None + month: Optional[str] = None + year: Optional[str] = None -class RBDMasterDataUpdate(BaseModel): +class RBDMasterDataUpdate(DefultBase): value_float: Optional[float] = None value_string: Optional[str] = None + month: Optional[str] = None + year: Optional[str] = None diff --git a/src/dashboard_model/service.py b/src/dashboard_model/service.py index d51d1a5..5af2793 100644 --- a/src/dashboard_model/service.py +++ b/src/dashboard_model/service.py @@ -53,6 +53,10 @@ async def update_master_data(db_session: DbSession, md_id: UUID, payload: RBDMas record.value_float = payload.value_float if payload.value_string is not None: record.value_string = payload.value_string + if payload.month is not None: + record.month = payload.month + if payload.year is not None: + record.year = payload.year await db_session.commit() await db_session.refresh(record) return record @@ -135,7 +139,9 @@ async def get_model_data(*, db_session: DbSession, simulation_id: Optional[UUID] "Prediction_EAF": EAF, "Prediction_EAF_KONKIN": EAF_KONKIN, "Real_EAF": EAF_REAL, - "Real_EAF_KONKIN": EAF_KONKIN_REAL + "Real_EAF_KONKIN": EAF_KONKIN_REAL, + "month": eaf_record.month, + "year": eaf_record.year }, "Trip": { "Prediction_Trip": Trip,