From a2152f33b46513e1936a8d384829a2ae70ef958a Mon Sep 17 00:00:00 2001 From: MrWaradana Date: Fri, 19 Dec 2025 17:07:10 +0700 Subject: [PATCH] feat: Implement RBD integration for EAF simulation and result collection, and enhance equipment data with joined Maximo records. --- src/__pycache__/config.cpython-311.pyc | Bin 4148 -> 4245 bytes src/config.py | 3 + .../__pycache__/router.cpython-311.pyc | Bin 11643 -> 11691 bytes .../__pycache__/schema.cpython-311.pyc | Bin 13937 -> 14057 bytes .../__pycache__/service.cpython-311.pyc | Bin 27739 -> 30775 bytes src/equipment/router.py | 2 + src/equipment/schema.py | 1 + src/equipment/service.py | 70 +++++++- src/modules/plant/collect_results.py | 24 +++ src/modules/plant/fetch_eaf_from_rbd.py | 154 ++++++++++++++++++ src/modules/plant/trigger_simulations.py | 24 +++ .../__pycache__/model.cpython-311.pyc | Bin 1592 -> 1764 bytes .../__pycache__/schema.cpython-311.pyc | Bin 3639 -> 3950 bytes .../__pycache__/service.cpython-311.pyc | Bin 3884 -> 3884 bytes src/yeardata/model.py | 3 +- src/yeardata/schema.py | 1 + 16 files changed, 280 insertions(+), 2 deletions(-) create mode 100644 src/modules/plant/collect_results.py create mode 100644 src/modules/plant/fetch_eaf_from_rbd.py create mode 100644 src/modules/plant/trigger_simulations.py diff --git a/src/__pycache__/config.cpython-311.pyc b/src/__pycache__/config.cpython-311.pyc index 953269a3a35863538a009578c449dde0b95eb310..601d7db07bf36324aa96db88e71f081f2aaae3a7 100644 GIT binary patch delta 184 zcmdm@FjbLvIWI340}yOxbIn{mkynzjZKHZNvjP)Cs%VOFipd(jWlRhVtAQ8-q6||Q zgBdhUH_u>x#>Rh(JIKi;-Z3B`J~YUuN@j8tmlR7=!ox diff --git a/src/config.py b/src/config.py index ba82bac..0b46af1 100644 --- a/src/config.py +++ b/src/config.py @@ -84,6 +84,9 @@ TIMEZONE = "Asia/Jakarta" AUTH_SERVICE_API = config("AUTH_SERVICE_API", default="http://192.168.1.82:8000/auth") + RELIABILITY_APP_URL = config( "RELIABILITY_APP_URL", default="http://192.168.1.82:8000/reliability" ) + +RBD_APP_URL = config("RBD_APP_URL", default="http://192.168.1.82:8000/rbd") diff --git a/src/equipment/__pycache__/router.cpython-311.pyc b/src/equipment/__pycache__/router.cpython-311.pyc index 859b57ee0ad6032bc2a10bcbc52ef11b99286eae..5ba274eeb297784462188b9f38a11e21eb22c181 100644 GIT binary patch delta 486 zcmewzwK|%2IWI340}yyAxMl`#x+4Tgu>(uIYrT8AZs2-=p>MM0fY?*(yPzC6i^9>>0}^$114_K$L+*lR!k}lRBvesM_=$ckIc1*IiLAiYH(MMVi9!W=}H0*PN7Ho5sJ zr8%i~MN>9ERJUg_QDS6U;Q4_8NL_FZhtSDsU>ZSuWM*LUX1v1a^??bf?t|Cn5FK?! E0MvYjUH||9 delta 447 zcmZ1-{X2?xIWI340}zyab;)Gi$m=P?#S7#$1M%lelP^k0POg-Z7v*GNsAaBYsb#HY zt7Wg{sO7BXs^y-%UM7WO4a+j1PoRceM3nu@Nb!6n3Y$a#zP*eakrkDpv zC?J6%Q=q^t-t^Rx_|(GE%!1t1ypp0qpx8W+>cc?d1rRnce3h7dLQb7Ylxy-wIhn}< z@;(|$<{yN)__#hY0EsUk;)582pxy^bULLlO3_#)wi1@(3JGotc2V>D>2L(U2BA{_a z#gnHfn6g#?nTIx?Q>bKQD+h@dP4-o?XDprEtE46XQ3euC0ukkt4=bs$Rs)$AH@{Ol z$H-VSd9O+$TMa}`MOB%#9>{#V*+;d9$r9xBTPy|n#U(``D{e6tl$I2M^cH~>6(xWO za}WXY_b(2c-29Z%oK(A_Nt*>V>{(0{7}*wheqaDn7hJ<3baEP)Mi3vF8JN5ouP}Oj PU;?W9;I_F%N1YJ>)-ZR- diff --git a/src/equipment/__pycache__/schema.cpython-311.pyc b/src/equipment/__pycache__/schema.cpython-311.pyc index e62b7cd903ac9a3f86ee2051c2983fa5b61096e4..d9cb80075e06614c0f7d818452412352e190aa06 100644 GIT binary patch delta 505 zcmeyE^D>ujIWI340}xm%xMu1oZsdEf!nkbnKNWjMgEbt>m>3vV12F_d38!+WvZV@1 z!UPx)tW=g1-pPh)^0Fd8H9RQdd^SM!DJ;PZn*5V<)uyTQX60w*rKZH^CRSwT=HC*{ z&CH8WO-znY$t*6&FHX#fPtGqc(G;C5t)9kMI$2Opc=7~wK1PSh^VL77Yw530-N3S= zL=^znKIT+KBytNxmNEN zBU>d%rfPDn!6vQ-5EDc+ZMHP5V`OXq8W*nwGRjYry+{)zs|6yoK?K5v2CxlHAVCl@ zb#k<^8DrDrNydeGV3&gJZh%P_8G=|wAi@|({Nk|5%}*)KNwq6#-)v|S$Y{jH7{>U4 U0XsRt`y*K93$nb#S>cNs>?D;On$HaVX}>a>Si5H0cN=>pxPos5Mcx& zj6p;>jL6>{rL&Ncv3#rW83DHCV`AboQz?N9~iKc6TClyWxgQGi%r%s G+Y11jXj!cQ diff --git a/src/equipment/__pycache__/service.cpython-311.pyc b/src/equipment/__pycache__/service.cpython-311.pyc index 34d045da9cac6d6b229f1399cf42c269d2ff85f7..921a422f8cc64b18e62d97e3f1074dec66075e08 100644 GIT binary patch delta 3201 zcma)7eNYtV8Q;CTJ&ywf%K-<7tSH{Cgp&$`Mo>fK5b!w81LQ;NaojGjaCb}ITY@@Q zF^Nr0o6 zENGqS;r@KS-sfxI=h=@I_@AZu)j!P5wR7;>Dtgu9+UeC!{^AAKC3B^TQ*!0N4|tCI z5j>gS;9hyZ;*f(%qwG`K=-1Donkmc{nt&5`H4m%p;!W<#3!|Bg@{UQa{2k*K)@Sx_XOKJ-JtPQwzr7>so1bW zDkTY=E>jPCAOnp=G+B`(=Ay678}u$YqZjg=^z*z*`nx=-6O(K9w}@_Ue^0P^PmkLh z=-SgJvdZ0IPdK8FsBtwhCe}dOz$NrB(IMa#v96)E)>A8qO*_SHb+ybnwdIe`pvO$q zyS|qqqX68We^e^OIr{T65wZd!4$n5JtA zsMS+pzX+my_>d&(jV+#6E!!%0E#z-}`AFJqszGMnPORw zrs)@TlJFXcj~iw6Kj(L*gh`^tbz%h(DL}Zlp(uM6K(Z|JGl+3H6Cu5 zsUr_A&+fHFfL2O{+|Fm9M@gGlOW%HeV%D?A@#fO0u#WloE7pLV>Q+G2c3V zmew{o=!?Em`yauCgO!$;qs`kL#{0g5Jb#03@mFWFCVh&&ZcxR)!}>Xpc9SvZpWySE z?ypRTFiXWisql9opQO738b52C3;fh-WmY4G+V=uqf-5A&Po((A6J{XcqeZsh(HUAc zP{CiPjRTLYi1_qC)65rW(11{dunFN82)hw*_Yn<2MHok@2S`~H8qpP*e2J3h5pJT` z@;EHil%VQLEI~?9j1{qOg5=0d*(?g~mPMNf&sZn%sDs|>bI{v^?RFLl;oC;*!9HR4 z8+d~CA7d;{{oM&$QESQy1Es)RWWl2P6nlOIdR|1o9Gc(<=;81szT0pP@8tPE(F2i+ z>_4Mxc9AGO5!o;kL!MQRRg%33Sad3DRMCCf=qL=F3x({X=SM1nYD-j2=xiymWt{07 z?o09-0&Wt5D}ZcAxQc+6pWu}vcs(+G+KR6?(`wA0%_!S{>^=U|)&%=vDyr-vm7s-x zT;h$t*_=UURHV!?LHTvBMd!vV~5Ytjw$jJgA!6M+eyw=NW{O`91}-?~Ty rdo#oh!G5ZKKEL`}_Cqf#=jB(T^VvJD35{t1&C}d{c*qzVzs>b*bK delta 2201 zcma)6YfMx}6yDjr_X-OtDTFtI^77V7qaYQqU<-ANWl>m=mlc-1v$(JinO*SFRZL41 zwT;?oqc%47M{ScnZ0xo5MH`w%+O*nYtchN0YEn{3n=~ZG-}anYuwc`~8~E~_@0>Yv z&e@qcJ4tTcAoD()JJ)K0ujzi3N9sB}FPX5@`R6Ug+IpIx)zLDohT3Q;Eu*sAoPvSk z`l*!qnz6K`%?Bm6ZqDGZ(6s|yji)L!T*w2 z8`;}J(`NZwz+WT$t)jL^{cYg4Yed_4oQSBGX|1+tstd?8Fx`MVN^2ifTGxpnY;oo` zlGXuf*8h*s3BEe?xy@Q9@OlyFqB(9!)4+BJn+n?vc7w3HXqrZ8W-iQYGl-2sgdSj# z2kZ?RahTL@eO$g})nAXVkci4JCrto;k=!e9g0c_jOH!_zNsyEDefo)<2j<1=KsDwB zT~4o)1z84ImT0gqDgUpWx`WpP-TMle*9`CNI z8U`S4Sj{6Vg)?|5=i6v^Z7E-y6?iEIp!BlMBkoB?El_NPl0nQYJc!Wm>f!ZDbRhp5d8Pyhk=(%k9EYMc{+;b<%sxN`o zs&gJmW|%$N)KGEN%v|SCn96mmGDE%ywv%71JWPgld)0U5wO@f?$i9F^y|rv980!2P z+4th|WCYRLK4kL%j5ylc=h@<;{!qKm84OXzDBol&$`D5obFyI!Q(OU`FW{f9;4@o> zJPP_|{i-cRt{TBOv1j6A?8&~tj8U31L_;1QH7x$XR@To8?1@Qlqeu2SJE=D^U5=Ue z^EP|h0#VT=v~dhZtg8=dg|fF(CxZdMWnU^^1IJE2;qdCqYbT|F?=koXfLb}RA;AYA32I*R4%K#%Ja4!b3C ztA==4Q?>b9!uK>4Esb0z48|vcEkY1K)!CxDjo zuiFA-NH5rUMo~mmhd$AHjHKW@V2~bz^am|Kv$C?-)3|gye@DwCZ}JP;>RCI=bk)Eg z7*LFmi;#yPE>Q(CeA}1@!G)k96ag4YFu+1oV}GG?5aBM$Qa?PHhU^JZUy!Auj2*E? z05u_U<%Upr*cKn5N0n}nj0C>LmB5+1+A6j}u<4iFO>)HrTp?zUdrUt753k#-UJV6q z7llh=hIzkdhM8}JnJ4mIZx3nWx4q{{y?(^^EFr(~>;BA`JD6IWqBr;hfA&BZ+M;vp zlGzZjX(PIehH3&`U2yyIQFI{K(Ia_1!H{^GBGbYrkKvQV@G)Wd3b2(3cpDj>e};#Q z;h~632Y=JZe2JNV63DQoLGAEo3P$VK0(O~X@zTCheyVRxdKV$ejrjCQiy%d7rbvAw V5FXc`o{ajW=|9+@SM}c`{{Z*cE 'CM') + ) + AND ( + a.description NOT ILIKE '%U4%' + OR ( + a.description ILIKE '%U3%' + AND a.description ILIKE '%U4%' + ) + ); + """ +) async def _fetch_maximo_records( *, session: AsyncSession, assetnum: str @@ -174,6 +200,44 @@ async def _fetch_maximo_records( return [] +async def _fetch_joined_maximo_records( + *, session: AsyncSession, assetnum: str +) -> list[dict[str, Any]]: + """Fetch Joined Maximo rows with a retry to mask transient collector failures.""" + + query = JOINED_MAXIMO_SQL.bindparams(assetnum=assetnum) + + try: + result = await session.execute(query) + return result.mappings().all() + except AsyncpgInterfaceError as exc: + logger.warning( + "Collector session closed while fetching Joined Maximo data for %s. Retrying once.", + assetnum, + ) + try: + async with collector_async_session() as retry_session: + retry_result = await retry_session.execute(query) + return retry_result.mappings().all() + except Exception as retry_exc: + logger.error( + "Retrying Joined Maximo query failed for %s: %s", + assetnum, + retry_exc, + exc_info=True, + ) + except SQLAlchemyError as exc: + logger.error( + "Failed to fetch Joined Maximo data for %s: %s", assetnum, exc, exc_info=True + ) + except Exception as exc: + logger.exception( + "Unexpected error while fetching Joined Maximo data for %s", assetnum + ) + + return [] + + async def fetch_acquisition_cost_with_rollup( *, db_session: DbSession, base_category_no: str ) -> tuple[Optional[AcquisitionData], Optional[float]]: @@ -293,11 +357,14 @@ async def get_master_by_assetnum( maximo_record = await _fetch_maximo_records( session=collector_db_session, assetnum=assetnum ) + joined_maximo_record = await _fetch_joined_maximo_records( + session=collector_db_session, assetnum=assetnum + ) min_eac_disposal_cost = next( (record.eac_disposal_cost for record in records if record.tahun == min_eac_year), None, ) - print(min_eac_disposal_cost) + return ( equipment_master_record, equipment_record, @@ -307,6 +374,7 @@ async def get_master_by_assetnum( min_eac_year, last_actual_year, maximo_record, + joined_maximo_record, min_eac_disposal_cost, ) # return result.scalars().all() diff --git a/src/modules/plant/collect_results.py b/src/modules/plant/collect_results.py new file mode 100644 index 0000000..30d039a --- /dev/null +++ b/src/modules/plant/collect_results.py @@ -0,0 +1,24 @@ + +import asyncio +import logging +import sys +import os + +# Add the project root to sys.path to resolve imports +sys.path.append(os.getcwd()) + +from src.database.core import get_session +from src.modules.plant.fetch_eaf_from_rbd import fetch_simulation_results + +# Setup logging +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + +async def main(): + logger.info("Starting simulation result collection script...") + async with get_session() as session: + await fetch_simulation_results(session) + logger.info("Simulation result collection script finished.") + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/src/modules/plant/fetch_eaf_from_rbd.py b/src/modules/plant/fetch_eaf_from_rbd.py new file mode 100644 index 0000000..c1513a8 --- /dev/null +++ b/src/modules/plant/fetch_eaf_from_rbd.py @@ -0,0 +1,154 @@ + +import httpx +import logging +import os +from datetime import datetime +from sqlalchemy import select +from src.database.core import DbSession +from src.yeardata.model import Yeardata +from src.config import RBD_APP_URL + +logger = logging.getLogger(__name__) + +current_year = datetime.now().year +AUTH_APP_URL = os.getenv("AUTH_APP_URL", "http://192.168.1.82:8000/auth") + +async def sign_in(username: str = "lcca_admin", password: str = "password") -> str: + """ + Sign in to AUTH_APP_URL/sign-in using provided username/password. + Returns the access_token string if successful, else None. + """ + url = f"{AUTH_APP_URL}/sign-in" + logger.info(f"Signing in to {url}...") + async with httpx.AsyncClient() as client: + try: + resp = await client.post( + url, + json={"username": username, "password": password}, + timeout=30.0, + ) + resp.raise_for_status() + data = resp.json() + if isinstance(data, dict) and "data" in data: + token = data.get("data", {}).get("access_token") + if token: + logger.info("Sign-in successful.") + return token + except Exception as e: + logger.error(f"Sign-in failed: {e}") + return None + +async def start_simulations(db_session: DbSession): + """ + Starts simulatios on RBD app for years > current year and saves simulation IDs. + """ + # 0. Sign in first + token = await sign_in() + if not token: + logger.error("Aborting start_simulations because sign-in failed.") + return + + headers = {"Authorization": f"Bearer {token}"} + + # Get all years after current year + query = select(Yeardata).filter(Yeardata.year > current_year) + result = await db_session.execute(query) + yeardata_list = result.scalars().all() + + if not yeardata_list: + logger.info("No yeardata found for years > %s", current_year) + return + + async with httpx.AsyncClient() as client: + # Step 1: Start simulations and get IDs + for record in yeardata_list: + try: + # POST {{RBD_APP_URL}}/aeros/simulation/run/yearly + url = f"{RBD_APP_URL}/aeros/simulation/run/yearly" + payload = {"year": record.year} + logger.info(f"Starting simulation for year {record.year}: POST {url}") + + response = await client.post(url, json=payload, headers=headers, timeout=60.0) + response.raise_for_status() + + # Check JSON for ID, fallback to text if needed + try: + data = response.json() + if isinstance(data, dict): + simulation_id = data.get("id") or data.get("simulation_id") or data.get("data") + if not simulation_id or isinstance(simulation_id, dict): + simulation_id = str(data.get("id", "")) + else: + simulation_id = str(data) + except Exception: + simulation_id = response.text.strip().strip('"') + + if simulation_id: + record.rbd_simulation_id = str(simulation_id) + db_session.add(record) + logger.info(f"Saved simulation_id {simulation_id} for year {record.year}") + else: + logger.error(f"Could not extract simulation ID from response for year {record.year}: {response.text}") + + except Exception as e: + logger.error(f"Failed to start simulation for year {record.year}: {e}") + + # Commit simulation IDs + await db_session.commit() + logger.info("All simulations started and IDs saved.") + + +async def fetch_simulation_results(db_session: DbSession): + """ + Fetches EAF data using simulation IDs for years > current year. + Should be called after start_simulations. + """ + # 0. Sign in first + token = await sign_in() + if not token: + logger.error("Aborting fetch_simulation_results because sign-in failed.") + return + + headers = {"Authorization": f"Bearer {token}"} + + # Get all years after current year + query = select(Yeardata).filter(Yeardata.year > current_year) + result = await db_session.execute(query) + yeardata_list = result.scalars().all() + + if not yeardata_list: + logger.info("No yeardata found for years > %s", current_year) + return + + async with httpx.AsyncClient() as client: + # Step 2: Fetch EAF data + for record in yeardata_list: + if not record.rbd_simulation_id: + logger.warning(f"No simulation ID for year {record.year}, skipping.") + continue + + try: + # GET {{RBD_APP_URL}}/aeros/simulation/result/calc/:simulation_id/plant + url = f"{RBD_APP_URL}/aeros/simulation/result/calc/{record.rbd_simulation_id}/plant" + logger.info(f"Fetching result for year {record.year}: GET {url}") + + response = await client.get(url, headers=headers, timeout=60.0) + response.raise_for_status() + + result_data = response.json() + # Expected format: { "data": { "eaf": ... } } + eaf_value = result_data.get("data", {}).get("eaf") + + if eaf_value is not None: + record.eaf = float(eaf_value) + db_session.add(record) + logger.info(f"Saved EAF {eaf_value} for year {record.year}") + else: + logger.warning(f"No EAF value found in response for year {record.year}") + + except Exception as e: + logger.error(f"Failed to fetch EAF for year {record.year}: {e}") + + # Commit EAF updates + await db_session.commit() + logger.info("All simulation results fetched and saved.") diff --git a/src/modules/plant/trigger_simulations.py b/src/modules/plant/trigger_simulations.py new file mode 100644 index 0000000..79a9b53 --- /dev/null +++ b/src/modules/plant/trigger_simulations.py @@ -0,0 +1,24 @@ + +import asyncio +import logging +import sys +import os + +# Add the project root to sys.path to resolve imports +sys.path.append(os.getcwd()) + +from src.database.core import get_session +from src.modules.plant.fetch_eaf_from_rbd import start_simulations + +# Setup logging +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + +async def main(): + logger.info("Starting simulation trigger script...") + async with get_session() as session: + await start_simulations(session) + logger.info("Simulation trigger script finished.") + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/src/yeardata/__pycache__/model.cpython-311.pyc b/src/yeardata/__pycache__/model.cpython-311.pyc index 42306a4a40e7032833fdfb973b8b7ed88e95dd5c..b5483a110d12bbe8632066399357cbf8aee91b2d 100644 GIT binary patch delta 287 zcmdnN^MsdgIWI340}wc~xn|n1Oys-6IAh~$PsYgxjFODPAW~X{i6NCGRWyZZ4f8T4 z28Pu@3;|JMlTDZm8O1lJF=a6--eO8lyTzQEm{!CFl)fccl#~)*oS9pilUS0OpBJB* zGWi0t1EbewK^87X*BhMt9byw$7qDL7RJ+Kjc7;=|!RZEsIWI340}wPzX=H9^p2&BH(QD&tPsYhjOooiYo3}7!F;2E;abV=#+`z)c z$Z>&14v326C!c4Hmy!c&E0PBh3P9o)hfQvNN@-52U6IOU54J>WK1TZw446bm$%L2% OwjV(7|U1uQE{b}(MB3%(!~a#1Seid0C0QwPTc(H{N_EFnO2ktO7Yw89M2 z1tJ?*b_iau54j*6dQm#`igaj$bBDwP)gJi^ETKSjktOtoy3T^q6(KuZE~qeRJ9g_t}xl5azWL8axJIi~Z^3)E|`tA|CkAxI2FfX&kf@xVqIPQK0M zZ-izO8%SCWM4;Je3=#to>XT!*l^}K+15J>eJeS+w2F(<6kYW~?4y@*xfn-61_GAqn z1BiKMljC@T^}xOZI~r^!SRtxoesS33=BJeAq}mnPOuo-ksU^&4{ec0Km|*=8B>Dv* Sr89XAug>Ikysa#tAOHY$k-9Aa delta 610 zcmaDSw_S#BIWI340}wPzX=LWJZ{&+)WVG9y!Pv?qwnk(b69dC)AclY_kyO@H(aF{< z(yr(-Vjvkwm_mkB_7pK2AdiV5l{tkmg$ctXaiDsM$=NK@y67fJf@Dxkk^q^M!Whh; zDY@C1HJ_1Pld*^gC^Y#hn<1mrWOnvmM&`*A*u^LBW_M&-!nFAxyD-z_m0T)7T)?P4 zS&G}uO&+McNC8ABf(SJbp$;N6K!g^IkO4A2^8<+nIK08a(cl76t2J4k+kEm}ZVp{- zkOnkE^gv=DLIK3k0TEyu^d|4-k({i}V`hZr2sV%sWe|a8k^x8zM5s)j%%j8xHpKvF zN`?)Z2__(gEHKqr%`yhbf(VVt@w^5Qvy3NCY8IWI340}wp?=dzJokskm$$pv}< delta 20 acmZ1@w?>Y8IWI340}vd{<=M!s$PWNDYXp%1 diff --git a/src/yeardata/model.py b/src/yeardata/model.py index c15eda1..3708954 100644 --- a/src/yeardata/model.py +++ b/src/yeardata/model.py @@ -19,4 +19,5 @@ class Yeardata(Base, DefaultMixin, IdentityMixin): asset_crit_foh_forced_outage_hours = Column(Float, nullable=False) asset_crit_extra_fuel_cost = Column(Float, nullable=False) cf = Column(Float, nullable=False) - eaf = Column(Float, nullable=False) \ No newline at end of file + eaf = Column(Float, nullable=False) + rbd_simulation_id = Column(String, nullable=False) \ No newline at end of file diff --git a/src/yeardata/schema.py b/src/yeardata/schema.py index 49742d8..71c32f7 100644 --- a/src/yeardata/schema.py +++ b/src/yeardata/schema.py @@ -21,6 +21,7 @@ class YeardataBase(DefaultBase): asset_crit_extra_fuel_cost: Optional[float] = Field(None, nullable=True, ge=0, le=1_000_000_000_000_000) cf: Optional[float] = Field(None, nullable=True, ge=0, le=1_000_000_000_000_000) eaf: Optional[float] = Field(None, nullable=True, ge=0, le=1_000_000_000_000_000) + rbd_simulation_id: Optional[str] = Field(None, nullable=True) created_at: Optional[datetime] = Field(None, nullable=True) updated_at: Optional[datetime] = Field(None, nullable=True) created_by: Optional[str] = Field(None, nullable=True)