From 75bec015ea5782166c62c14f1bc664cd07556589 Mon Sep 17 00:00:00 2001 From: MrWaradana Date: Tue, 10 Feb 2026 18:25:36 +0700 Subject: [PATCH 01/16] feat: Add token parameter to simulation endpoints and remove sign-in retry logic from Prediksi module. --- .../__pycache__/router.cpython-311.pyc | Bin 16530 -> 16625 bytes src/equipment/router.py | 8 ++++---- src/modules/equipment/Prediksi.py | 12 +++--------- .../__pycache__/Prediksi.cpython-311.pyc | Bin 50439 -> 50257 bytes 4 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/equipment/__pycache__/router.cpython-311.pyc b/src/equipment/__pycache__/router.cpython-311.pyc index 7b7a0fbe53092d282b9f001928848baa2e3781e5..e3329aa1a3dd2c6a9f9ab90279bfac8db1799779 100644 GIT binary patch delta 1538 zcmZvcT})I*6oBX4pJjnAps>sGv#`4?;Bprbz7aBEC zmTDB#2zW%Aimg_IDNvhwAJp_g8&KOOO(6B5?n58?&@?7C8k@#6edwIsifz-`Ip3Z+ zGiQFznYpJ2;mIIau2?K_jFF&@1ZQ}D~`F{+T!(5)WavWpF;Fz$10h?e$ zFvNz0c2R{R=6+EX_)kRH-6V*=|Fr?~$NmuOLiK55oG#aEhHTFppI zC+ zC8nwA!=`gg%{VXi$!aF2sq1XB< zY(?EVSrZ!uP2bALg+=W^o8CkoJE>`I>+agKFQBQ>SxO^QaRg&IoRJd3dYc~(8l$#f zz;U~N!Qo$Y`0qRX3#tFD2HMXGg{id)n^WTIeBxZWuP$Gl&zHG=r`prtVs~9}jc)O- z;%jt>_Z)Jg8@FfNbIN*T-RL=J2knIYv<;b~6)Q8d65iq3Ml$tsJi|Sig*cqqjFT3R z$diV1mMyr$5j2(ZxQ`3QqGL0_x9G|qm4;(ym-A?+<9FFrj^uw|K;Riv;}2OF>laTR z#vErQPsA=~HdralaXXpH`r$n|Ifs{=-eo*DeIn0QEFwh^}DMVANO#JjGth88mHA~X};;s}eK zS|A*&1$w(QeRq$h=Tc8AA)l~^!y6Zl-5m^SJ$z75Z^O0j5>F((FH%XbGMY-KGZfO- zkZU`RxU1l%@r`>BoHv>lY_*HF+WWTJg{1$f#$Hb)*i!2hVP1)=Un$R*m((lr9Yy9k z-paKWchHobgaCmS^h(i?_{s1hpZpc@d_K)}C4{(kic64Oo}nu*&sdk&0dNFA%l{SL z$H^6Aa1@KYKXct$FaSe1UT_(X<7&kXC$L3%MQ9VyyJ7=AR*Jxn*}e^&9li!QWnA+O z0*n}CMOjcZ!e@_${WNJc1SFgxj1$fhCI}y3s5l)y#B;?>aKU&|JOQSQWcTB#lJ)Ts z^^<7(r;?R$#YipNuVsnLY{n-+rI3S`C-+;i|l(e|S^>6gtcDVA^ObdjRqD z0SSv;!MF7>R4U#uQJaq#s>p$>I9u^M?7~ZxkDwLDUY;4Co1w2K(38+Zgd>EbgdxHS z0tHvUz!8@AcL#g)E7YDQTqPVQtR>Ke_`_V?)vfk+YHRdcya`-_znK_&ECRp}%(t*q YjW4Sn3c}a9_;K}G;YP3Vp!%lx7sq*lTmS$7 delta 1480 zcmZvbdrVtZ9LLY^K1u-#<N?)iQ4J-_oi z_uTvYowGayr-nfHhE5kpXuG#_tg~uTmky5EH5HuDE=XzU<2t3%VZ-q3Yqvjmj_myroOCy`D|ztJYdH|92gNqXQE!Xy#bAczK^{g4Zn8A0&V)kHTonj;kyBd|uYmSaLiCf+zRsZLB{c&i z;0wufkb+eyEn=!wDl)dCG@H00to!=tbr(J)o24TG#~r~e=3<>wA4JE&52v7z=rplg4p zUzU`O%vVGa)Kb_Y4SmLK@T@v#T!PF6OU0t4;+Cai!Svs*zLR#E%QaTH$u)OewTHh} z?5%e4^G=cK>ksE_v5_0LxEecu!|kmx^Eb_6jU5LvZ)OOQ{h~WdYQyV+_A zP*Kl3%)PR*M3%@Pxs!}{bl2_}O zim|pb%l+|ROM!%VDZW=nNFRTE7+YRYa$ZqgMK*vK7MU;#nOU@ zcv@ByCfPmuv<4SMQ&UIZs5D}o-TuH%Dtm=yf3shy$LH-k^bKq-Qnf0k9>?4E9N3L9 zj$-{D=4fJgilGH-9qE?6Os-+rN8ySM$F>E7a<{*yE2y;LkfYEVITn#45KAMXN5Ezq zR4Umb2|sa^!JMkk9s=uvv1HL$a?4n-DVdb&A{L1f>wd-v)LkF6E+4ryLxXAGv7}yrk+)px9!Eg#^^Ue5Cz6oB& z@AIF8bLxhIGoU%o=Em{M!maTU`D%1rSX2Ph>i(iZFi$W~HHGW`g}A^R=NZQFk9Fnn z8kQA1^$|yOHJiGCfnq0IR8JRw4)JeNPnhotb|@3rTe3^T#x&y7l5Dt)_ey?&CcInv zBkaX*9-0|wVUt@K*bR%MOc`csgyAT|7{dvMa};6WP$1Z?u$?I{k58^tuF@g9S;22Lgxui);4|V~Sna4QPf*j!zu@q<_2p`1h06a4%TaqF diff --git a/src/equipment/router.py b/src/equipment/router.py index 6c5583f..258b0fb 100644 --- a/src/equipment/router.py +++ b/src/equipment/router.py @@ -75,7 +75,7 @@ async def get_maximo_record_by_assetnum(db_session: CollectorDbSession, assetnum ) @router.get("/simulate/{assetnum}") -async def simulate_equipment(db_session: DbSession, assetnum: str): +async def simulate_equipment(db_session: DbSession, assetnum: str, token: Token): """Stream progress events while running the simulation (prediksi + EAC). This endpoint returns Server-Sent Events (SSE). Each event's `data` is @@ -98,7 +98,7 @@ async def simulate_equipment(db_session: DbSession, assetnum: str): yield f"data: {json.dumps({'status':'started','step':'prediksi','message':'Menghitung prediksi'})}\n\n" try: - prediksi = await prediksi_main(assetnum=assetnum) + prediksi = await prediksi_main(assetnum=assetnum, token=token) except Exception as exc: # send error event and stop yield f"data: {json.dumps({'status':'error','step':'prediksi','message':str(exc)})}\n\n" @@ -140,7 +140,7 @@ async def simulate_equipment(db_session: DbSession, assetnum: str): @router.get("/simulate-all") -async def simulate_all_equipment(db_session: DbSession): +async def simulate_all_equipment(db_session: DbSession, token: Token): """Run simulation (prediksi + EAC) for ALL equipment. Returns SSE stream of progress. """ @@ -167,7 +167,7 @@ async def simulate_all_equipment(db_session: DbSession): await update_initial_simulation_data(db_session=db_session, assetnum=assetnum) # Prediksi - await prediksi_main(assetnum=assetnum) + await prediksi_main(assetnum=assetnum, token=token) # EAC eac = Eac() eac.hitung_eac_equipment(assetnum=assetnum) diff --git a/src/modules/equipment/Prediksi.py b/src/modules/equipment/Prediksi.py index 8ccf4c9..564325c 100644 --- a/src/modules/equipment/Prediksi.py +++ b/src/modules/equipment/Prediksi.py @@ -587,7 +587,7 @@ class Prediksi: self.refresh_token = d.get("refresh_token") return data except httpx.HTTPError as e: - print(f"Sign-in failed: {e}") + print(f"Sign-in failed for URL {self.AUTH_APP_URL}/sign-in: {type(e).__name__} - {e}") # Try to sign out if sign-in failed try: signout_url = f"{self.AUTH_APP_URL}/sign-out" @@ -595,14 +595,8 @@ class Prediksi: await client.get(signout_url, timeout=10.0) print("Signed out due to sign-in failure.") except Exception as signout_exc: - print(f"Sign-out failed: {signout_exc}") - # Try to sign in again - try: - signin_res = await self.sign_in() - if self.access_token: - return signin_res - except Exception as signin_exc: - print(f"Sign-in failed after sign-out: {signin_exc}") + print(f"Sign-out failed for URL {self.AUTH_APP_URL}/sign-out: {type(signout_exc).__name__} - {signout_exc}") + return None async def refresh_access_token(self) -> str: diff --git a/src/modules/equipment/__pycache__/Prediksi.cpython-311.pyc b/src/modules/equipment/__pycache__/Prediksi.cpython-311.pyc index 6f1e6d78f5d416ec868ae3baf9485838f504cc8d..c8d25eb97c8b7b24c9656b5d0c914eb70e304388 100644 GIT binary patch delta 1072 zcmZuwZAep57(QorSKZy5Q>HU3TWICdxk*9HK26)~0}8Dl6?L2Ks@IRJ-OV4`%s#0M zDmYMxMj>T^7S(}_hF~^lMadoP*GTB6{ac?zL3HlDEmC{Wd*1hXpZ7iA_nz;iq8BLR z-Sl(@5Gl#N#P^LpGQRWa<@gNgpEX+4lbwV0+z(hIe(g>TERri#@#%t~iAqNOz{dm@ zCnErYsY4KeVbWD;GA0qJ)CW`h!HpEOtYJAcpo)fCDt*L=t~a!KGUkqgKyVj;Kz?#8 z0EIwwb?fHr!!&OnnSsTKd7NjSx1>&4vZ8S{%H)^AEbQPREV9({4T7B)to2^Lh4WeK z+eGWWnqAf|#^F~p<<>6BYDZg|DvENI;!f@-~5O;Y7nn+ZK>Vl8oPptGHer?ITy zZRJ>2EGCDrF=$0|RfS)^VC!EImy$4^Dlf?LLYHZe$ocqYKVSNXacKbX50a~%CG8|g zFnM^qc?f#+y4(avni)f;l;yow6)V|DJkhu2m)=*FHpY1)Wr;SlA$i4 zO-b*D_)4o2_H@GpfFx4wj44~nIsVWbE89vu(YFCEuql@HFGF}8_oTu<5kkm))jK@hw);T=I^wdh4FjK`eVWymvj9s*66Xx2(+LTFh z)nWuWIu64F_;QC<2VWy}!UiYN*a-o4;*y@}Gf6pVS`3qk(CHCB8UJl$#<7ehZ$Zqhqlz1^RGf5>Rs4Lj6>A=x(!mYr?C3Sk zFA6VtdQL+o8hqAkh%@r5^8zb!epD7(58bFWRH&~cf09@wn{8+yR1Bx_%MeW$iZ7H; z$H&!sJ6Zy9Z6nvxZi8Z!u(mZ~u6)>P{AqkA)Pw=rJkbgTe1BpGbRILp(ggUw{iyI? M{>?U`RpIx)0j@JkfB*mh delta 1193 zcmZuvZ)jUp6u;-ayd>}C&)!SgtXr2ZG}&q_tJ&10&ANi-&*B8Pu@5o^rlEOD)7nH| zQZ`4@C^$9?Lgj)WIIMqggKdI5(XQKvmZWL?vc0k}OIe`|rhYR8!J!}Y-pokbdG6tN z&i&nUe(!MaTe`$voMf$6EEWL~JNocK>iL;(tq&6Tc7|sT>Tt2up_=4B!7&AjwP48Y zpk+@i!RHP5#9#|fCM8a|%oL3H?}Q(JmuzEA*Uk8DG6NPI?`++@v)O@=2PwM>{3aDE zJ~rsUrQw}=9_7)r6~`T&_^fOR3VAfbH(M1$UKnCdp#&5R6NZy0?RBHvR$wEGoEX*uJgM$ z*SX?naW@aCx`k4@Y96(oN=FF;G61N_)+i9Syy{BG7#*9aH4&f4O{aC~GFg zt_`L*389n-oEMKPaw0XFP4)b%)yE)|U1t&cm8^GBU2v(FM!yEH#J8;@0(A zGkt;p@g=jjX7*}kZ(XOi^r_=xJ}CCp5m=tUs@SSI{8zgP5ML5|YhthVls_g`XGsr@ zdd?DoWp~$={xAAJANXoObMD{bQ^K-+d(G}4pON-1x9`zfHhGckiJG+c@gw^#I!XjK zVq>JxjG#F}#FxZyO$=*Kd2^%)kZjGqed>kgYMi*qMUW(WlicI`JAwgp`DK5wi!HU- zf&qLm(_R##ZERVvMeS_a>5fRu0_q6+xP=ZI&3!^Rz%K+?njZ$54^X}$0;#V^Ld4Hi z+HH{oTxB0enFAbU{4C86bsq6Bi%uZ=i*8!K=nQ6Zx4<-Dd;Ds{QPo$x!j!)b%!ZkH?abAM^s{WH_Sy)r| ze_CVUH&y@Hqz>lQkG}L7g%x8gmX43fu^6sg%My{m_0{M0aOC$OFg?@=bHPT?PRAN? zE4-#gZoJ3CVO;(GV@9V_2Y*8#7n{d2DZ(+l{6`7y|-f6N~R-&rg4)Y1RrdV~Lc<7p1RU0VAG D&oFP? From fbab85ff506b6e0db382f3fee6aea6b1904e9418 Mon Sep 17 00:00:00 2001 From: MrWaradana Date: Wed, 11 Feb 2026 11:39:56 +0700 Subject: [PATCH 02/16] fix: Correct double backslash escaping of newlines in SSE responses. --- .../__pycache__/router.cpython-311.pyc | Bin 16625 -> 16624 bytes src/equipment/router.py | 10 +++++----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/equipment/__pycache__/router.cpython-311.pyc b/src/equipment/__pycache__/router.cpython-311.pyc index e3329aa1a3dd2c6a9f9ab90279bfac8db1799779..77bb12825b6452736035b199f7a1ac7a42e55c82 100644 GIT binary patch delta 867 zcmaLUO-vI(6bJA=3*|#94Mntw#_pCv0jaIEf>FeTi`K7*LQqgDG!>!LPD^UMNJT;j zL@G}}Lp&5UCBaC{+>IwM9^8{+PsW46&lF+|VoaP@Y&_}ge}DVtO*XqbyA*+B1hjFj zHdiIahozA&{}XLjA*}Ln;TC{K#EmgsqgYC)Jy$wdlc?Ak-cY31XuMPhu&tyF7#}Qo zo&hKEh|!qSLj4m03>!U~Q#5PFcSaMO#VpC8J4cOHqK#-rmt@%Lrm~Fa5Lj{(IqqQ4 z6YA~r1z9IXBpWm#mn87wE9p8gY>_Q`rWB--$CPx?T5Q0_ay|s`sVwUQ%7#?314=sh zO|F748cmh@uu_mpURX&7&ztT+2!EMmYe*?dMHf=iSeRdadiRdPJ2p{1m=;v56n4Rjw#Gk#dHCNO`#Qdf> zgS~>4l||K}s$Z4N+n!0!{*Qdx>;)J^b@^8qz~%B=FoZWO??v5RaSev?Q^f-q!Dyuf zqnuYB1sLP7BLWcR1J&E0I!do`kU(NTahsSRCW$HH4sjP>T8m*CKUz;ijMwj+0+_)C z+cC`yt=`8f`)-)!7wrA8X`DI@g7o*ssWF2;b{&9+SnDwB*6yKhYAfRCcbH+0PdeVf z`bVOjQ2T;jHjY=F%^I4sIKFnuFpoc--=P(k_k002zO9*ax6|Swf)TWlY>*fthKW&P pjED&m8JGRR5Syj>95GLf5cNczU^RQ6&)?bCOI*-K?k9eaD^kO?iYheofsS$ z5Sc$DGCQqw6RHSTIh^{W&M!$~h!;t$2cuRGwBfqd3cdKsItC$Z7o4V$Qj}3aNXZJ_ z73KIyCDOurnVJ}3m(&jQnl%k9ZB1%?x*|q>-{Am;1VWl)9V_3=R zt#Q}g{Ov2vM=DQkD5s}hRoILz_S2r`oRo_Dztm;?zlksGUQK{^jKAy^<{Z#zG7 zf4$}wOycL7C78llmldYvbyqXM%=*3-lY z+h9>{IPd@MrVlp268gLjUC`2;E#vDt0oHJ=Hai7(0qlkMwlWTC)9In Date: Wed, 11 Feb 2026 14:49:39 +0700 Subject: [PATCH 03/16] feat: Implement reliability-based CM cost prediction using historical cost per failure and document the new prediction logic. --- src/modules/equipment/Prediksi.py | 66 +++++++++++++++++++++++++++---- src/modules/equipment/formula.py | 8 ++++ 2 files changed, 66 insertions(+), 8 deletions(-) diff --git a/src/modules/equipment/Prediksi.py b/src/modules/equipment/Prediksi.py index 564325c..3af234e 100644 --- a/src/modules/equipment/Prediksi.py +++ b/src/modules/equipment/Prediksi.py @@ -674,6 +674,39 @@ class Prediksi: print(f"HTTP error occurred: {e}") return {} + def __get_historical_cost_per_failure(self, assetnum): + connection = None + try: + connection = get_production_connection() + if connection is None: + return 0.0 + cursor = connection.cursor() + # Optimized single-pass query: counts and sums in one scan + query = """ + SELECT + SUM(a.actmatcost) / NULLIF(COUNT(CASE WHEN a.wonum NOT LIKE 'T%%' THEN 1 END), 0) as cost_failure + FROM wo_maximo a + WHERE (a.asset_unit = '3' OR a.asset_unit = '00') + AND a.status IN ('COMP', 'CLOSE') + AND a.asset_assetnum = %s + AND a.worktype IN ('CM', 'PROACTIVE', 'EM') + AND a.wojp8 != 'S1' + AND ( + a.description NOT ILIKE '%%U4%%' + OR (a.description ILIKE '%%U3%%' AND a.description ILIKE '%%U4%%') + ) + """ + cursor.execute(query, (assetnum,)) + result = cursor.fetchone() + cost_failure = float(result[0]) if result and result[0] is not None else 0.0 + return cost_failure + except Exception as e: + print(f"Error fetching historical cost per failure for {assetnum}: {e}") + return 0.0 + finally: + if connection: + connection.close() + def __get_man_hour_rate(self, staff_level: str = "Junior"): connection = None try: @@ -755,7 +788,8 @@ class Prediksi: rate, max_year = self.__get_rate_and_max_year(assetnum) man_hour_rate = self.__get_man_hour_rate() # Defaults to 'junior' - pmt = 0 + # Pre-fetch cost per failure once per asset to avoid redundant DB queries + avg_cost_per_failure = self.__get_historical_cost_per_failure(assetnum) # Prediksi untuk setiap kolom for column in df.columns: @@ -807,16 +841,32 @@ class Prediksi: preds_list.append(cost) preds = np.array(preds_list, dtype=float) - elif recent_vals.empty: - avg = 0.0 - preds = np.repeat(float(avg), n_future) else: - avg = pd.to_numeric(recent_vals, errors="coerce").fillna(0).mean() - avg = 0.0 if pd.isna(avg) else float(avg) - preds = np.repeat(float(avg), n_future) + # Use pre-fetched cost per failure + preds_list = [] + for yr in future_years: + failures_data = await self._fetch_api_data(assetnum, yr) + # Interval from predicted number of failures + interval = 0.0 + if isinstance(failures_data, dict): + data_list = failures_data.get("data") + if isinstance(data_list, list) and len(data_list) > 0: + first_item = data_list[0] + if isinstance(first_item, dict): + num_fail = first_item.get("num_fail") + if num_fail is not None: + try: + interval = float(num_fail) + except Exception: + interval = 0.0 + + # predicted_cost = predicted_failures * avg_cost_per_failure + cost = interval * avg_cost_per_failure + preds_list.append(cost) + preds = np.array(preds_list, dtype=float) else: - # Для kolom non-cm, gunakan nilai dari last actual year bila ada, + # kolom non-cm, gunakan nilai dari last actual year bila ada, # jika tidak ada gunakan last available non-NA value, jika tidak ada pakai 0.0 if "is_actual" in df.columns and not df[df["is_actual"] == 1].empty: last_actual_year_series = df[df["is_actual"] == 1]["year"] diff --git a/src/modules/equipment/formula.py b/src/modules/equipment/formula.py index 849d964..99b41b1 100644 --- a/src/modules/equipment/formula.py +++ b/src/modules/equipment/formula.py @@ -6,6 +6,14 @@ This file consolidates the core mathematical/financial formulas used across: - `insert_actual_data.py` (aggregation formulas, man-hour conversion) - `Prediksi.py` (future value / fv wrappers) +### Prediction Logic Summary +| Category | Logic Type | Formula Basis | +| :--- | :--- | :--- | +| **CM Labor** | **Reliability-Based** | `Failures x 3.0 x 1.0 x ManPowerRate` | +| **CM Other** | **Reliability-Based** | `Failures x CostPerFailure (from Production SQL)` | +| **PM / OH / PDM** | **Last Scenario** | `Value from Last Actual Year` (Carry Forward) | +| **Total Risk Cost** | **Aggregated** | `Sum of above + Asset Criticality Multiplier` | + Keep these functions pure and well-documented to make debugging and comparisons easier. """ From 090dd1ff645ae03b5855c1654430739eed361db2 Mon Sep 17 00:00:00 2001 From: MrWaradana Date: Wed, 11 Feb 2026 14:59:08 +0700 Subject: [PATCH 04/16] feat: Implement a simulation mode for equipment prediction and EAC, and extract execution time formatting into a helper function. --- .../__pycache__/Prediksi.cpython-311.pyc | Bin 50257 -> 52571 bytes .../equipment/__pycache__/run.cpython-311.pyc | Bin 2998 -> 4437 bytes src/modules/equipment/run.py | 71 ++++++++++++------ 3 files changed, 49 insertions(+), 22 deletions(-) diff --git a/src/modules/equipment/__pycache__/Prediksi.cpython-311.pyc b/src/modules/equipment/__pycache__/Prediksi.cpython-311.pyc index c8d25eb97c8b7b24c9656b5d0c914eb70e304388..08e596875942f83b3f0925c1d41c034543b31702 100644 GIT binary patch delta 3828 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 diff --git a/src/modules/equipment/__pycache__/run.cpython-311.pyc b/src/modules/equipment/__pycache__/run.cpython-311.pyc index cd997a85fa3624a3fa582bd712f75ac7f8c56c3e..9b0cd2d6b22b0d6d713340d2fb921cbf334572e0 100644 GIT binary patch literal 4437 zcmeHKU2N3Y6~6wP@%&_x3<=33gqWX&goK$uAj_g<*^r;2v_!44qTN7u>Dw}kimlN=Y8sPNrr>__PhbyF7QC6oN=P}QRq5gP85l9A?>XWf9k3^oH8e@EKxW`;j>-}b z;g4lW;B=M}nTNFKdgPkGeUoeku72)Z@_URU{)z^5Wa%@jR-U7r-*=g zR{eixDtgE{=W|HIGt(|2;hcK}43lQf)UtxJ?N@*bcc{zg0rgutM`Q_+Oe3YyVUOBw zPec)nBOUE%4gVjzWJ#x$hv?C^Hw5||e94h_No>C?-6U?JTh!<1 zCIK<{Dz;#Cfw+YeIZCVB`Zj|{(JI+(o-1sYa>PqH{AHXT61$1f^}md9SN|H;sU@@z^@$m%q73)Z?lCS8V|lmd8U`l`&E zo{?LEl)~Pcl&UGF+qP%AdG&TekdvlQR)wUJ1o%ow7M^swOs|SZqYP$^wmFP$ilnAv znn_5e%NAKFA(r*raY;6VP_%p+Q zso=k)`!5y!!MV{AgZxL{e(ys5<`N>A`YpE3*6D%BQ+4IU;~P(IfCyx;eFe5pXZwn5 zV9r&!-a;uc&=@~i>`!Cs{m^1)&b96f7JU1cZWz8J1>cbpB0L9+?FSaFZe4SuRzC*~ z_E3R6q+9fM-Pg3`hb37`Jx}|WuRk7mGN1=~fDE>`!1n5F?~cxV&!UE=`P*OLnZJ|2 zSPWeLuIt=ir~lq&d@^BleOBoDOb=WJ%Krq$bnfVvb$c*Pq^J2Cul z49>*x<721-f9L2zV!+!3yUef^X%~s1I?i{X(@K6n9HTQa{Ky!0of!61B79F`sETgq z#L&En#9JHYv)rf>giaLPlhTA)Gq6nOgrLYNjT88oP{|<q3P*ujw|KoVgq_Pdizxb|i$xGxNK1IFsb`D3Vmk|bJlZ{64kuMs3jSp~Ej_wYYCn27!W*3}G!uGi& zym>AO%L!G2EdnoSX+CBhBduv|?ys$G?V8|xsbIh;&rv+%%}{N1eE7usi1LcS>%ejh zJDcF+QZx#?ncJ#K#iWEu^UAdwCIy?RKOhAk$gOS12K#Z@=}(js@D9hOdVrTGf*^`0 zkhiWP^5w0oTF`#!06xls< x?s?zB#f6La#}>y7wzI%?>J@rre;r;JzCXG+YOw7Ewq37KsSeR?(4#d}{{eb$4$c4o delta 1235 zcmah|U1%Id9G~6UkK2#C%Uv3iRHSRINv?#%8Y(qytwpz`q#o1{iVw1!w-a4+yBBBY zP}e=0V!#u8FwBcsf(o|v<0TM~z7{O>@vcM&gF-<9`WC4V;*&G?(b_8b=Rd!h`Tu|a z-+b)OkNdwL&0WoAO~CEt(f5NR`dV&GZh^HacUVJ~uPwuoiSx|&G{bIGMMeNg&GKN7v~^Yfj40j(L{;s!m);l@uiWTun7qePc-#Wq6)E+^^@I zh$npY`-Pj@zc?BIzWE9X5@B-a={&4l~s|rvy^tB~U7qlMxr=I(CA{dAot|d!An* zm3ik>LO;}=mJ)hh8(l~W?#bsI+=v{ENCQX6cM{zR2nyqwq&Ub`>)~<@5#)>5KoeD} z9xt4Cf}tD$za68W+Xr4qgA zOoDpE)MXq*gu$3#W-Vjn2UQ}ZsjKgMm6<^tL@}vERm7B95EG`72Hy^YH54((D@!Ib zIRO(-$TJDCLSKcedlf-=#uC%5d$wmtbLt!KyRPni?u$Rl5vosokp<=gaec;d118*aOJp8GWc z+;8f=zK1QnrPEbddbYfAx?L=DzxGbA@1e-wE}q-qnR9o&rSB+4Yw}`wwY*Z^wu&p| z*rh}HLkCYj_55-0!*SC+1aIj3+-Z1YTIFMw@w~7SM4ng5;=H&GgOdDfQkLGzA2`fE zIS`Df5+cuIkbkakz#Se0*x~S3=2%$w8#VL{F7c^&!|}I(xV_` ratn^P;rO= 3600: + hours = int(execution_time // 3600) + minutes = int((execution_time % 3600) // 60) + seconds = execution_time % 60 + return f"{hours}h {minutes}m {seconds:.2f}s." + elif execution_time >= 60: + minutes = int(execution_time // 60) + seconds = execution_time % 60 + return f"{minutes}m {seconds:.2f}s." + else: + return f"{execution_time:.2f} seconds." + +# Alternative calling function to just predict and calculate eac without inserting actual data +async def simulate(): + start_time = time.time() + print("Starting simulation (predict + eac)...") + + try: + prediction_result = await predict_run() + if prediction_result is False: + print("Prediction step failed or was skipped. Skipping EAC run.") + return + except Exception as e: + print(f"Error in predict_run: {str(e)}") + return + + try: + result = eac_run() + if asyncio.iscoroutine(result): + result = await result + print("EAC run completed.") + except Exception as e: + print(f"Error in eac_run: {str(e)}") + return + + end_time = time.time() + message = f"Simulation finished in {format_execution_time(end_time - start_time)}" + print(message) + return message + # Panggil fungsi async def main(): start_time = time.time() @@ -29,34 +70,20 @@ async def main(): result = eac_run() if asyncio.iscoroutine(result): result = await result - - if isinstance(result, (list, tuple)): - print(f"EAC run returned {len(result)} items.") - else: - print("EAC run completed.") + print("EAC run completed.") except Exception as e: print(f"Error in eac_run: {str(e)}") return end_time = time.time() - execution_time = end_time - start_time - # format execution time into h/m/s as needed - if execution_time >= 3600: - hours = int(execution_time // 3600) - minutes = int((execution_time % 3600) // 60) - seconds = execution_time % 60 - message = f"Script calculation finished in {hours}h {minutes}m {seconds:.2f}s." - elif execution_time >= 60: - minutes = int(execution_time // 60) - seconds = execution_time % 60 - message = f"Script calculation finished in {minutes}m {seconds:.2f}s." - else: - message = f"Script calculation finished in {execution_time:.2f} seconds." - + message = f"Script calculation finished in {format_execution_time(end_time - start_time)}" print(message) return message if __name__ == "__main__": - asyncio.run( - main() - ) + import sys + # Use 'simulate' argument to run without query_data + if len(sys.argv) > 1 and sys.argv[1] == "simulate": + asyncio.run(simulate()) + else: + asyncio.run(main()) From 512031bf888d9f1ce7365b3dd5d4b4b221eb3bdc Mon Sep 17 00:00:00 2001 From: MrWaradana Date: Thu, 12 Feb 2026 14:48:03 +0700 Subject: [PATCH 05/16] fix loggin level no error --- src/logging.py | 7 ++++++- src/main.py | 5 ++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/logging.py b/src/logging.py index a7f31af..42f869c 100644 --- a/src/logging.py +++ b/src/logging.py @@ -119,11 +119,16 @@ def configure_logging(): root_logger.addHandler(handler) # Reconfigure uvicorn loggers to use our JSON formatter - for logger_name in ["uvicorn", "uvicorn.access", "uvicorn.error", "fastapi"]: + for logger_name in ["uvicorn", "uvicorn.error", "fastapi"]: logger = logging.getLogger(logger_name) logger.handlers = [] logger.propagate = True + # Disable uvicorn access logs as we handle request logging in our middleware + access_logger = logging.getLogger("uvicorn.access") + access_logger.handlers = [] + access_logger.propagate = False + # sometimes the slack client can be too verbose logging.getLogger("slack_sdk.web.base_client").setLevel(logging.CRITICAL) diff --git a/src/main.py b/src/main.py index b5f803b..8f8f8c1 100644 --- a/src/main.py +++ b/src/main.py @@ -71,7 +71,10 @@ async def db_session_middleware(request: Request, call_next): collector_session = async_scoped_session(collector_async_session, scopefunc=get_request_id) request.state.collector_db = collector_session() response = await call_next(request) - log.info(f"Request completed: {response.status_code}") + if response.status_code >= 400: + log.error(f"Request completed: {response.status_code}") + else: + log.info(f"Request completed: {response.status_code}") except Exception as e: log.error(f"Request failed: {type(e).__name__} - {str(e)}") raise e from None From 783841d37ef9874695a2ccd1035e573b63dd1e09 Mon Sep 17 00:00:00 2001 From: MrWaradana Date: Thu, 12 Feb 2026 15:14:18 +0700 Subject: [PATCH 06/16] add validation errors --- src/exceptions.py | 25 ++++++++++++++++++++++++- src/main.py | 5 +++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/exceptions.py b/src/exceptions.py index 8c9b589..bab7e8d 100644 --- a/src/exceptions.py +++ b/src/exceptions.py @@ -99,7 +99,30 @@ def handle_exception(request: Request, exc: Exception): request_info = get_request_context(request) if isinstance(exc, RateLimitExceeded): - _rate_limit_exceeded_handler(request, exc) + return _rate_limit_exceeded_handler(request, exc) + + if isinstance(exc, RequestValidationError): + logging.error( + f"Validation error | Error: {str(exc.errors())} | Request: {request_info}", + extra={"error_category": "validation"}, + ) + return JSONResponse( + status_code=422, + content={ + "data": None, + "message": "Validation error", + "status": ResponseStatus.ERROR, + "errors": [ + ErrorDetail( + field=".".join(map(str, err["loc"])), + message=err["msg"], + code=err["type"], + ).model_dump() + for err in exc.errors() + ] + } + ) + if isinstance(exc, HTTPException): logging.error( f"HTTP exception | Code: {exc.status_code} | Error: {exc.detail} | Request: {request_info}", diff --git a/src/main.py b/src/main.py index 8f8f8c1..9f604b0 100644 --- a/src/main.py +++ b/src/main.py @@ -7,12 +7,14 @@ from typing import Optional, Final from fastapi import FastAPI, HTTPException, status +from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse from pydantic import ValidationError from slowapi import _rate_limit_exceeded_handler from slowapi.errors import RateLimitExceeded from sqlalchemy import inspect +from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.orm import scoped_session from sqlalchemy.ext.asyncio import async_scoped_session from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint @@ -47,6 +49,9 @@ app = FastAPI(exception_handlers=exception_handlers, openapi_url="", title="LCCA version="0.1.0") app.state.limiter = limiter app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) +app.add_exception_handler(HTTPException, handle_exception) +app.add_exception_handler(RequestValidationError, handle_exception) +app.add_exception_handler(SQLAlchemyError, handle_exception) app.add_middleware(GZipMiddleware, minimum_size=2000) From 886fdaf3d2e0691ce6e7f099027ff71ec365679c Mon Sep 17 00:00:00 2001 From: MrWaradana Date: Fri, 13 Feb 2026 13:22:27 +0700 Subject: [PATCH 07/16] configure exception handlers --- src/logging.py | 3 +++ src/main.py | 13 ++++++------- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/logging.py b/src/logging.py index 42f869c..2207241 100644 --- a/src/logging.py +++ b/src/logging.py @@ -129,6 +129,9 @@ def configure_logging(): access_logger.handlers = [] access_logger.propagate = False + # set uvicorn access log level to warning + logging.getLogger("uvicorn.access").setLevel(logging.WARNING) + # sometimes the slack client can be too verbose logging.getLogger("slack_sdk.web.base_client").setLevel(logging.CRITICAL) diff --git a/src/main.py b/src/main.py index 9f604b0..2562611 100644 --- a/src/main.py +++ b/src/main.py @@ -40,19 +40,18 @@ log = logging.getLogger(__name__) # we configure the logging level and format configure_logging() -# we define the exception handlers -exception_handlers = {Exception: handle_exception} - # we create the ASGI for the app -app = FastAPI(exception_handlers=exception_handlers, openapi_url="", title="LCCA API", +app = FastAPI(openapi_url="", title="LCCA API", description="Welcome to LCCA's API documentation!", version="0.1.0") app.state.limiter = limiter -app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) + +# we define the exception handlers +app.add_exception_handler(Exception, handle_exception) app.add_exception_handler(HTTPException, handle_exception) +app.add_exception_handler(StarletteHTTPException, handle_exception) app.add_exception_handler(RequestValidationError, handle_exception) -app.add_exception_handler(SQLAlchemyError, handle_exception) -app.add_middleware(GZipMiddleware, minimum_size=2000) +app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) from src.context import set_request_id, reset_request_id, get_request_id From 9876a4147d957252e7d9a80e7fc4ae649c37203b Mon Sep 17 00:00:00 2001 From: MrWaradana Date: Fri, 13 Feb 2026 13:23:12 +0700 Subject: [PATCH 08/16] add sql alchemy error handler --- src/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.py b/src/main.py index 2562611..cf2fb92 100644 --- a/src/main.py +++ b/src/main.py @@ -52,7 +52,7 @@ app.add_exception_handler(HTTPException, handle_exception) app.add_exception_handler(StarletteHTTPException, handle_exception) app.add_exception_handler(RequestValidationError, handle_exception) app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) - +app.add_exception_handler(SQLAlchemyError, handle_exception) from src.context import set_request_id, reset_request_id, get_request_id From f1fab9b1a36ee76fe681ccd6e1955dff3b4d7d01 Mon Sep 17 00:00:00 2001 From: MrWaradana Date: Fri, 13 Feb 2026 13:28:29 +0700 Subject: [PATCH 09/16] fix to add starlette error handler import --- src/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.py b/src/main.py index cf2fb92..b9855aa 100644 --- a/src/main.py +++ b/src/main.py @@ -15,6 +15,7 @@ from slowapi import _rate_limit_exceeded_handler from slowapi.errors import RateLimitExceeded from sqlalchemy import inspect from sqlalchemy.exc import SQLAlchemyError +from starlette.exceptions import StarletteHTTPException from sqlalchemy.orm import scoped_session from sqlalchemy.ext.asyncio import async_scoped_session from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint From 9f9ef4e99d4ceece4f7a7877fb4057ac7a512110 Mon Sep 17 00:00:00 2001 From: MrWaradana Date: Fri, 13 Feb 2026 13:34:15 +0700 Subject: [PATCH 10/16] remove starletteHTTPException --- src/main.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main.py b/src/main.py index b9855aa..407f5b0 100644 --- a/src/main.py +++ b/src/main.py @@ -15,7 +15,6 @@ from slowapi import _rate_limit_exceeded_handler from slowapi.errors import RateLimitExceeded from sqlalchemy import inspect from sqlalchemy.exc import SQLAlchemyError -from starlette.exceptions import StarletteHTTPException from sqlalchemy.orm import scoped_session from sqlalchemy.ext.asyncio import async_scoped_session from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint @@ -50,7 +49,6 @@ app.state.limiter = limiter # we define the exception handlers app.add_exception_handler(Exception, handle_exception) app.add_exception_handler(HTTPException, handle_exception) -app.add_exception_handler(StarletteHTTPException, handle_exception) app.add_exception_handler(RequestValidationError, handle_exception) app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) app.add_exception_handler(SQLAlchemyError, handle_exception) From 07e2d75cb5e9d7d4a4be42bd88b2aba9fbda0dd2 Mon Sep 17 00:00:00 2001 From: CIzz22 Date: Wed, 18 Feb 2026 08:39:00 +0000 Subject: [PATCH 11/16] Update 'Jenkinsfile' --- Jenkinsfile | 127 ++++++++++++++++++++++++---------------------------- 1 file changed, 58 insertions(+), 69 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index f1bd3b1..55d7922 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,107 +1,96 @@ pipeline { agent any - + environment { - // Replace with your Docker Hub username/organization DOCKER_HUB_USERNAME = 'aimodocker' - // Use credentials for Docker Hub - DOCKER_CREDENTIALS = credentials('aimodocker') - // Replace with your image name + // This creates DOCKER_AUTH_USR and DOCKER_AUTH_PSW + DOCKER_AUTH = credentials('aimodocker') IMAGE_NAME = 'lcca-service' - // Replace with your docker compose service name - SERVICE_NAME = 'lcca-app' - // Variable for Git commit hash - GIT_COMMIT_HASH = '' + SERVICE_NAME = 'ahm-app' + + SECURITY_PREFIX = 'security' - // Replace with the SSH credentials for development server - // SSH_CREDENTIALS = credentials('backend-server-digitaltwin') - // SSH_CREDENTIALS_USR = 'aimo' - // SSH_SERVER_IP = '192.168.1.82' + // Initialize variables to be updated in script blocks + GIT_COMMIT_HASH = "" + IMAGE_TAG = "" + SECONDARY_TAG = "" } - + stages { - stage('Checkout') { + stage('Checkout & Setup') { steps { script { - // Checkout and get git commit hash checkout scm - def commitHash = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim() - GIT_COMMIT_HASH = commitHash - echo "Git commit hash: ${GIT_COMMIT_HASH}" + GIT_COMMIT_HASH = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim() + + // Use env.BRANCH_NAME or logic to handle detached HEAD if necessary + def branch = env.BRANCH_NAME ?: 'unknown' + echo "Current Branch: ${branch}" + + if (branch == 'main') { + IMAGE_TAG = GIT_COMMIT_HASH + SECONDARY_TAG = 'latest' + } else if (branch == 'lcca_security') { + IMAGE_TAG = "${SECURITY_PREFIX}-${GIT_COMMIT_HASH}" + SECONDARY_TAG = "${SECURITY_PREFIX}-latest" + } else { + IMAGE_TAG = "temp-${GIT_COMMIT_HASH}" + SECONDARY_TAG = "" // Ensure it's empty for other branches + } + + echo "Primary Tag: ${IMAGE_TAG}" } } } - + stage('Docker Login') { steps { - sh ''' - echo ${DOCKER_CREDENTIALS_PSW} | docker login -u ${DOCKER_CREDENTIALS_USR} --password-stdin - ''' + // Fixed variable names based on the 'DOCKER_AUTH' environment key + sh "echo ${DOCKER_AUTH_PSW} | docker login -u ${DOCKER_AUTH_USR} --password-stdin" } } - - stage('Build Docker Image') { + + stage('Build & Tag') { steps { script { - // Build with commit hash tag - sh """ - docker build -t ${DOCKER_HUB_USERNAME}/${IMAGE_NAME}:latest . - docker tag ${DOCKER_HUB_USERNAME}/${IMAGE_NAME}:latest ${DOCKER_HUB_USERNAME}/${IMAGE_NAME}:${GIT_COMMIT_HASH} - """ + def fullImageName = "${DOCKER_HUB_USERNAME}/${IMAGE_NAME}" + sh "docker build -t ${fullImageName}:${IMAGE_TAG} ." + + if (SECONDARY_TAG) { + sh "docker tag ${fullImageName}:${IMAGE_TAG} ${fullImageName}:${SECONDARY_TAG}" + } } } } - + stage('Push to Docker Hub') { steps { - sh """ - # Push both tags - docker push ${DOCKER_HUB_USERNAME}/${IMAGE_NAME}:${GIT_COMMIT_HASH} - docker push ${DOCKER_HUB_USERNAME}/${IMAGE_NAME}:latest - """ + script { + def fullImageName = "${DOCKER_HUB_USERNAME}/${IMAGE_NAME}" + sh "docker push ${fullImageName}:${IMAGE_TAG}" + + if (SECONDARY_TAG) { + sh "docker push ${fullImageName}:${SECONDARY_TAG}" + } + } } } - - // stage('Deploy') { - // steps { - // script { - // sshagent(credentials: ['backend-server-digitaltwin']) { - // sh """ - // ssh -o StrictHostKeyChecking=no -p 12558 aimo@0.tcp.ap.ngrok.io ' - // cd ~/digital-twin/Docker - // sudo docker compose pull ${SERVICE_NAME} - // sudo docker compose up -d ${SERVICE_NAME} - // ' - // """ - // } - // } - // } - // } } - + post { always { - // Clean up - sh 'docker logout' - - // Clean up local images script { - try { - sh """ - # Push both tags - docker rmi ${DOCKER_HUB_USERNAME}/${IMAGE_NAME}:${GIT_COMMIT_HASH} - docker rmi ${DOCKER_HUB_USERNAME}/${IMAGE_NAME}:latest - """ - } catch (err) { - echo "Failed to clean up images: ${err}" + sh 'docker logout' + def fullImageName = "${DOCKER_HUB_USERNAME}/${IMAGE_NAME}" + // Clean up images to save agent disk space + sh "docker rmi ${fullImageName}:${IMAGE_TAG} || true" + if (SECONDARY_TAG) { + sh "docker rmi ${fullImageName}:${SECONDARY_TAG} || true" } } } success { - echo "Successfully built, pushed, and deployed Docker image with tags: latest and ${GIT_COMMIT_HASH}" - } - failure { - echo 'Failed to build/push/deploy Docker image!' + echo "Successfully processed ${env.BRANCH_NAME}." } } } \ No newline at end of file From f0142044cd3fe3b826323b2ab56365cd2a34c420 Mon Sep 17 00:00:00 2001 From: MrWaradana Date: Fri, 20 Feb 2026 10:22:52 +0700 Subject: [PATCH 12/16] update replace cost equipment logic --- docs/updated_acquisition_algorithm.md | 59 ++++++ src/database/service.py | 2 +- .../__pycache__/service.cpython-311.pyc | Bin 42461 -> 38426 bytes src/equipment/service.py | 194 +++++++----------- .../insert_actual_data.cpython-311.pyc | Bin 53957 -> 54461 bytes .../equipment/__pycache__/run.cpython-311.pyc | Bin 4437 -> 5349 bytes src/modules/equipment/insert_actual_data.py | 152 ++++---------- src/modules/equipment/run.py | 32 +-- 8 files changed, 189 insertions(+), 250 deletions(-) create mode 100644 docs/updated_acquisition_algorithm.md diff --git a/docs/updated_acquisition_algorithm.md b/docs/updated_acquisition_algorithm.md new file mode 100644 index 0000000..72006b1 --- /dev/null +++ b/docs/updated_acquisition_algorithm.md @@ -0,0 +1,59 @@ +# Updated Equipment Acquisition & Simulation Algorithm + +This document outlines the refactored logic for equipment acquisition cost calculation and simulation forecasting, implemented in February 2026. + +## 1. Timeline Definitions + +The simulation follows a strict temporal alignment to ensure consistency across the fleet: + +| Parameter | Value | Description | +| :--- | :--- | :--- | +| **Base Year** | `2015` | The target year for all "Value of Money" (Net Present Value) calculations. | +| **Forecasting Start** | `2015` | The year from which future predictions and Economic Life reports begin. | +| **Calculation Start** | `2014` | The technical sequence start ($seq = 0$) used to establish an initial state. | + +--- + +## 2. Capital Cost Adjustment (Value of Money) + +To account for the time value of money, both the **Initial Acquisition Cost** and the **Replacement Cost** are normalized to the **2015 Base Year** using the project's inflation rate. + +### 2.1 Adjustment Formula + +The value of any cost $V$ at a specific $Year$ is adjusted to its equivalent value in $2015$ using the following formula: + +$$V_{2015} = \frac{V_{Year}}{(1 + r)^{(Year - 2015)}}$$ + +Where: +- $V_{2015}$ = Adjusted value in 2015 terms. +- $V_{Year}$ = Raw cost recorded in the database or Maximo. +- $r$ = Inflation rate (from `lcc_ms_master`, defaults to $0.05$ if undefined). +- $Year$ = The year the cost was recorded ($Y_{acq}$ or $Y_{replace}$). + +### 2.2 Total Acquisition Cost + +The total capital cost $C_{total}$ stored in the master data is the sum of the adjusted initial cost and the adjusted first detected replacement cost: + +$$C_{total} = \frac{C_{initial}}{(1+r)^{(Y_{acq} - 2015)}} + \frac{C_{replace}}{(1+r)^{(Y_{replace} - 2015)}}$$ + +--- + +## 3. Maintenance Cost Suppression Logic + +A specific business rule is applied to prevent "double counting" or distorted maintenance records during major equipment replacement years: + +### 3.1 Replacement Year Rule +In the **first year** where a `replace_cost > 0` is detected in Maximo ($Y_{replace}$): +- All **Material Costs** are set to $0.0$. +- All **Labor Costs** (and labor hours) are set to $0.0$. + +### 3.2 Logic Rationale +The replacement cost is treated as a capital expenditure (CAPEX) that restarts the equipment's life cycle. Standard maintenance (OPEX) for that specific year is ignored because the replacement action supersedes regular repair tasks. + +--- + +## 4. Implementation Reference + +The logic is primarily contained in: +- `src/equipment/service.py`: `check_and_update_acquisition_data()` (Cost adjustments). +- `src/modules/equipment/insert_actual_data.py`: `query_data()` (Timeline and cost suppression). diff --git a/src/database/service.py b/src/database/service.py index b797248..baa61e7 100644 --- a/src/database/service.py +++ b/src/database/service.py @@ -134,7 +134,7 @@ async def search_filter_sort_paginate( # Get total count count_query = Select(func.count()).select_from(query.subquery()) total = await db_session.scalar(count_query) - if all: + if all or items_per_page == -1: result = await db_session.execute(query) items = _extract_result_items(result) return { diff --git a/src/equipment/__pycache__/service.cpython-311.pyc b/src/equipment/__pycache__/service.cpython-311.pyc index 158ac2fc34a620c484ed9bdb28589eb0dac8e2df..190e00cf52f799942706641d6dbb87a69a55c7c0 100644 GIT binary patch delta 2522 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 diff --git a/src/equipment/service.py b/src/equipment/service.py index e3080c9..0651595 100644 --- a/src/equipment/service.py +++ b/src/equipment/service.py @@ -670,9 +670,9 @@ async def delete(*, db_session: DbSession, equipment_id: str): async def check_and_update_acquisition_data(db_session: DbSession, assetnum: str) -> bool: """ - Check if acquisition year/cost in Maximo differs from local DB. - If changed, archive history, delete transaction data, update master, and return True. - Otherwise return False. + Check if acquisition cost in Maximo differs from local DB. + Updates master acquisition_cost (initial + replacement) and sets forecasting_start_year to 2015. + Returns True if master record was updated, False otherwise. """ conn = get_production_connection() first_year = None @@ -680,7 +680,7 @@ async def check_and_update_acquisition_data(db_session: DbSession, assetnum: str if conn: try: cursor = conn.cursor() - # Query the oldest year from wo_maximo to detect the original acquisition + # Query the oldest year from wo_maximo to detect the original replacement cost query = """ select DATE_PART('year', a.reportdate) AS year, a.asset_replacecost AS cost from wo_maximo a @@ -697,7 +697,7 @@ async def check_and_update_acquisition_data(db_session: DbSession, assetnum: str cursor.close() conn.close() except Exception as e: - print(f"Error fetching acquisition year for {assetnum}: {e}") + print(f"Error fetching replacement data for {assetnum}: {e}") if conn: try: conn.close() @@ -706,123 +706,75 @@ async def check_and_update_acquisition_data(db_session: DbSession, assetnum: str updates_performed = False - if first_year: - # Fetch equipment to update - eq = await get_by_assetnum(db_session=db_session, assetnum=assetnum) - if eq: - # Check if forecasting_target_year matches the "default" logic (acquisition + design_life) - # using the OLD acquisition year. - current_acq = eq.acquisition_year - current_life = eq.design_life - current_target = eq.forecasting_target_year - current_acq_cost = eq.acquisition_cost - - # If current_target is logically "default", we update it. - # If user changed it to something else, we might want to preserve it - # if it currently holds the default value (based on old acq year). - is_valid_default = False - if current_acq and current_life and current_target: - is_valid_default = current_target == (current_acq + current_life) - - # Check for changes - change_year = (eq.acquisition_year != first_year) - change_cost = (first_cost is not None and eq.acquisition_cost != first_cost) - - # We only archive transaction history if the acquisition year itself changed. - # This prevents redundant history entries for cost-only updates. - if change_year: - print(f"Acquisition year change detected for {assetnum}: {current_acq}->{first_year}. Archiving history.") - - acq_year_ref = f"{current_acq}_{current_target}" + # Fetch equipment to update + eq = await get_by_assetnum(db_session=db_session, assetnum=assetnum) + if eq: + # Check if forecasting_target_year matches the "default" logic (acquisition + design_life) + # using the OLD acquisition year. + current_acq = eq.acquisition_year + current_life = eq.design_life + current_target = eq.forecasting_target_year + + is_valid_default = False + if current_acq and current_life and current_target: + is_valid_default = current_target == (current_acq + current_life) + + # Fetch inflation rate from lcc_ms_master for value-of-money adjustment + inflation_rate = 0.05 # Default fallback + try: + rate_query = text("SELECT value_num / 100.0 FROM lcc_ms_master WHERE name = 'inflation_rate'") + rate_result = (await db_session.execute(rate_query)).scalar() + if rate_result is not None: + inflation_rate = float(rate_result) + except Exception as e: + print(f"Warning: Could not fetch inflation_rate for {assetnum}: {e}") - # --- ARCHIVE HISTORICAL DATA --- - - # Check for existing identical archive to prevent duplicates (after calculation failures/retries) - check_hist_query = text("SELECT 1 FROM lcc_ms_equipment_historical_data WHERE assetnum = :assetnum AND acquisition_year_ref = :acq_year_ref LIMIT 1") - hist_exists = (await db_session.execute(check_hist_query, {"assetnum": assetnum, "acq_year_ref": acq_year_ref})).fetchone() - - if not hist_exists: - # 1. Copy old equipment master data to history - history_ms_query = text(""" - INSERT INTO lcc_ms_equipment_historical_data ( - id, assetnum, acquisition_year, acquisition_cost, capital_cost_record_time, design_life, - forecasting_start_year, forecasting_target_year, manhours_rate, created_at, created_by, - updated_at, updated_by, min_eac_info, harga_saat_ini, minimum_eac_seq, minimum_eac_year, - minimum_eac, minimum_npv, minimum_pmt, minimum_pmt_aq_cost, minimum_is_actual, - efdh_equivalent_forced_derated_hours, foh_forced_outage_hours, category_no, proportion, - acquisition_year_ref - ) - SELECT - uuid_generate_v4(), assetnum, acquisition_year, acquisition_cost, capital_cost_record_time, design_life, - forecasting_start_year, forecasting_target_year, manhours_rate, created_at, created_by, - updated_at, updated_by, min_eac_info, harga_saat_ini, minimum_eac_seq, minimum_eac_year, - minimum_eac, minimum_npv, minimum_pmt, minimum_pmt_aq_cost, minimum_is_actual, - efdh_equivalent_forced_derated_hours, foh_forced_outage_hours, category_no, proportion, - :acq_year_ref - FROM lcc_ms_equipment_data - WHERE assetnum = :assetnum - """) - await db_session.execute(history_ms_query, {"acq_year_ref": acq_year_ref, "assetnum": assetnum}) - - # 2. Copy old transaction data to lcc_equipment_historical_tr_data - history_tr_query = text(""" - INSERT INTO lcc_equipment_historical_tr_data ( - id, assetnum, tahun, seq, is_actual, - raw_cm_interval, raw_cm_material_cost, raw_cm_labor_time, raw_cm_labor_human, - raw_pm_interval, raw_pm_material_cost, raw_pm_labor_time, raw_pm_labor_human, - raw_oh_interval, raw_oh_material_cost, raw_oh_labor_time, raw_oh_labor_human, - raw_predictive_interval, raw_predictive_material_cost, raw_predictive_labor_time, raw_predictive_labor_human, - raw_project_task_material_cost, "raw_loss_output_MW", raw_loss_output_price, - raw_operational_cost, raw_maintenance_cost, - rc_cm_material_cost, rc_cm_labor_cost, - rc_pm_material_cost, rc_pm_labor_cost, - rc_oh_material_cost, rc_oh_labor_cost, - rc_predictive_labor_cost, - rc_project_material_cost, rc_lost_cost, rc_operation_cost, rc_maintenance_cost, - rc_total_cost, - eac_npv, eac_annual_mnt_cost, eac_annual_acq_cost, eac_disposal_cost, eac_eac, - efdh_equivalent_forced_derated_hours, foh_forced_outage_hours, - created_by, created_at, acquisition_year_ref - ) - SELECT - uuid_generate_v4(), assetnum, tahun, seq, is_actual, - raw_cm_interval, raw_cm_material_cost, raw_cm_labor_time, raw_cm_labor_human, - raw_pm_interval, raw_pm_material_cost, raw_pm_labor_time, raw_pm_labor_human, - raw_oh_interval, raw_oh_material_cost, raw_oh_labor_time, raw_oh_labor_human, - raw_predictive_interval, raw_predictive_material_cost, raw_predictive_labor_time, raw_predictive_labor_human, - raw_project_task_material_cost, "raw_loss_output_MW", raw_loss_output_price, - raw_operational_cost, raw_maintenance_cost, - rc_cm_material_cost, rc_cm_labor_cost, - rc_pm_material_cost, rc_pm_labor_cost, - rc_oh_material_cost, rc_oh_labor_cost, - rc_predictive_labor_cost, - rc_project_material_cost, rc_lost_cost, rc_operation_cost, rc_maintenance_cost, - rc_total_cost, - eac_npv, eac_annual_mnt_cost, eac_annual_acq_cost, eac_disposal_cost, eac_eac, - efdh_equivalent_forced_derated_hours, foh_forced_outage_hours, - created_by, NOW(), :acq_year_ref - FROM lcc_equipment_tr_data - WHERE assetnum = :assetnum - """) - await db_session.execute(history_tr_query, {"acq_year_ref": acq_year_ref, "assetnum": assetnum}) - - # 3. Delete old data - del_query = text("DELETE FROM lcc_equipment_tr_data WHERE assetnum = :assetnum") - await db_session.execute(del_query, {"assetnum": assetnum}) - - # Update Equipment Master regardless of if archive was needed/skipped - if change_year or change_cost: - if first_cost is not None and eq.acquisition_cost != first_cost: - eq.acquisition_cost = first_cost - - if eq.acquisition_year != first_year: - eq.acquisition_year = first_year - eq.forecasting_start_year = first_year # Align start with acquisition - if is_valid_default and current_life: - eq.forecasting_target_year = first_year + current_life - - await db_session.commit() - updates_performed = True + # Calculate initial cost from category/proportion (base acquisition cost) + initial_cost = 0.0 + if eq.category_no and eq.proportion: + _, aggregated_cost = await fetch_acquisition_cost_with_rollup( + db_session=db_session, base_category_no=eq.category_no + ) + if aggregated_cost: + initial_cost = (eq.proportion * 0.01) * aggregated_cost + + # Adjust initial cost to 2015 value (Base Year) + # Formula: Value_2015 = Value_Year / (1 + rate)^(Year - 2015) + adj_initial_cost = initial_cost + if current_acq and current_acq != 2015: + adj_initial_cost = initial_cost / ((1 + inflation_rate) ** (current_acq - 2015)) + + # Adjust replace cost to 2015 value (Base Year) + adj_replace_cost = (first_cost or 0.0) + if first_year and first_year != 2015: + adj_replace_cost = (first_cost or 0.0) / ((1 + inflation_rate) ** (first_year - 2015)) + + # Total cost is adjusted initial cost plus the adjusted replacement cost + total_cost = adj_initial_cost + adj_replace_cost + + change_cost = (eq.acquisition_cost != total_cost) + # Requirement: forecasting_start_year always starts from 2015 + change_start = (eq.forecasting_start_year != 2015) + + # Note: acquisition_year itself is no longer updated as per requirements. + + if change_cost or change_start: + if change_cost: + print( + f"Acquisition cost update for {assetnum}: {eq.acquisition_cost} -> {total_cost} " + f"(Adj. Initial: {adj_initial_cost} + Adj. Replacement: {adj_replace_cost} | Rate: {inflation_rate})" + ) + eq.acquisition_cost = total_cost + + if change_start: + print(f"Aligning forecasting_start_year to 2015 for {assetnum}") + eq.forecasting_start_year = 2015 + # If target was default, we update it to 2015 + design_life + if is_valid_default and current_life: + eq.forecasting_target_year = 2015 + current_life + + await db_session.commit() + updates_performed = True return updates_performed diff --git a/src/modules/equipment/__pycache__/insert_actual_data.cpython-311.pyc b/src/modules/equipment/__pycache__/insert_actual_data.cpython-311.pyc index 11b06a39bc62d0850ec148e6fcba929eb903acad..ca6f66e1b503b73a760ce3447ad94d05d9481f2e 100644 GIT binary patch delta 3661 zcmc&$Yj9J?72drMNxpj6)?1ck$&&oGVBlv3V>X;{fD}KBc0RjQmgh6`YQOBAibe(gK&}b zSIv0=u8>Yz208dSuFgII-;}OrFJ=hIbhg1$QlGPqXMO>sQ{L+kO48o^=T*!Uz&Cuq zWbOg!Xt~HKu9KRf2NGBFDWjXp^2Iny65|Sqsav6jo21oG{NBP`hmzriw>ah~l-_@F zpOd-7NK5ugEUc04zgDkPR2c#IcCAz~be&}tZDvVz@EBBx=7`5)QVx6+Zyp8PtX*u{=)DcQDp;oXRdi;FBCzs@mVEZZez1$(4EkZX z{!tRjM+K-56`|t&wa9%s|D=-aHgYGgh!1&#Rub`cCw6gAzYT~5Z^1}8DqjiO)R78Q zfqbukATJgoAF5~|X@eS68k$2e@g`v1(fVjD$|EI&?nKQXBdWl^{Se=Y5l(G5*OO8*5UnC1I zmazsITOwmiWo+3a)|oBe?&N^6&u?@)(#~62{Hqa1^ebI~`8D{;RseuDvIqVjE0B0| z?Ra#=Zqd#6TZi(9MaGNd4Kah$xcB%)rEL%SwnZw*(eK@Y(i5Z#se>w%fz*4{0Eo-? zkQbS}7Rw{mVk5GTfa6T024#z?ZjSEWph0{BWzT!bmtx+lLN(%wq&o2}slmMmZJ8?* z$G|F7Lk@%0sB+Xr4-r$-2q&&VTH;%abOhI-s$|4NBTu4g@hS3bK5jS($q7L2 zzE`6-L;gVoOYqrqKTHWp|1-qA&tfEf_IL*KAuFBxX(lWvN)HauA@)-kr0`t|LlnM8 z;UI-Kar(~--~#F6pY<_LAN5pHSViHt6tXBxQ<%Zc=N)hX?mqvCKE0=}Z)bZb><@<9 z>2gh)@iRX$;|(9|wM{C6fe>x~q^`3o7)q=}LW8e-uvRyjMpvZ0JFq)63A?fNLJ_HW zuDT4zs@1svf}yaP^yYK&JqvxB4#0ysFh~AnPGp*1T{mH|$2jAJ(-Y&e<4g#@cVQLW zB|W&10bvMl_@))-e)JF3%WPUVb054c*)RSD7HfdhGw^g=0knoU)*o0Ov-@LWPpmf* z>)1P{iC)u0W18qxUQJBn#;<>THJSmm<{KKvxW*9&N@qq~2MqQh`~7sKHs>=yfOrXH z0E_E}xp3TEIA$)sW-cCRny9FYIU0|*j9N}*zMXZX{aE{O`}x*^^;3Goz=|2GBbHNh zw&&7@u?4HgtWDRfO>wY>(dOJw;1D;F;T)|T%kU0K-@0@FK4}F;cN}OlD@Mgrz2o@} zvHXTOla_0agNHD*Hf{tt1#!TbDiSaZC+yjy!tk=;WfP_4!|O)Bex&)qgvBixKB-nQ zTMc;4Y5?)0%Qn-xYW}j1r=Dt_dKNY*2pL;e^=uV4UI~dkUL~~H zmE#Sj7PIndCY4?_^Tc!2-c(7*O|9{3Zsuk#mELpet$uVJUW#+T}usfCc*^@3l)-)0$N z-{v8;6)d&WXrbF`s=2Lcq_e-Hhk+t+#~^I2WA8XJwpOus%6RIj;;AQE#}aZ^XYBBR zyB?vVg1uV~Db^UpG%%eeh-P-$1Sw`2#d0uRE{F~6^kPVG1{ecoFk==1UUtRGkN|IU8^hRj@z delta 3265 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 YC{JuF9`CN=tr)FlL&O6vDgPt+2gAKmGXMYp diff --git a/src/modules/equipment/insert_actual_data.py b/src/modules/equipment/insert_actual_data.py index 88f1764..d26b38a 100644 --- a/src/modules/equipment/insert_actual_data.py +++ b/src/modules/equipment/insert_actual_data.py @@ -39,72 +39,6 @@ def get_recursive_query(cursor, assetnum, worktype="CM"): Fungsi untuk menjalankan query rekursif berdasarkan assetnum dan worktype. worktype memiliki nilai default 'CM'. """ - # query = f""" - # SELECT - # ROW_NUMBER() OVER (ORDER BY tbl.assetnum, tbl.year, tbl.worktype) AS seq, - # * - # FROM ( - # SELECT - # a.worktype, - # a.assetnum, - # EXTRACT(YEAR FROM a.reportdate) AS year, - # COUNT(a.wonum) AS raw_corrective_failure_interval, - # SUM(a.total_cost_max) AS raw_corrective_material_cost, - # ROUND( - # SUM( - # EXTRACT(EPOCH FROM ( - # a.actfinish - - # a.actstart - # )) - # ) / 3600 - # , 2) AS raw_corrective_labor_time_jam, - # SUM(a.jumlah_labor) AS raw_corrective_labor_technician - # FROM - # public.wo_staging_3 AS a - # WHERE - # a.unit = '3' - # GROUP BY - # a.worktype, - # a.assetnum, - # EXTRACT(YEAR FROM a.reportdate) - # ) AS tbl - # WHERE - # tbl.worktype = '{worktype}' - # AND tbl.assetnum = '{assetnum}' - # ORDER BY - # tbl.assetnum, - # tbl.year, - # tbl.worktype - # """ -# query = f""" -# select d.tahun, SUM(d.actmatcost) AS raw_corrective_material_cost, sum(d.man_hour) as man_hour_peryear from -# ( -# SELECT -# a.wonum, -# a.actmatcost, -# DATE_PART('year', a.reportdate) AS tahun, -# ( -# ROUND(SUM(EXTRACT(EPOCH FROM (a.actfinish - a.actstart)) / 3600), 2) -# ) AS man_hour, -# CASE -# WHEN COUNT(b.laborcode) = 0 THEN 3 -# ELSE COUNT(b.laborcode) -# END AS man_count -# FROM public.wo_maximo AS a -# LEFT JOIN public.wo_maximo_labtrans AS b -# ON b.wonum = a.wonum -# WHERE -# a.asset_unit = '3' -# AND a.worktype = '{worktype}' -# AND a.asset_assetnum = '{assetnum}' -# and a.wonum not like 'T%' -# GROUP BY -# a.wonum, -# a.actmatcost, -# DATE_PART('year', a.reportdate) -# ) as d group by d.tahun -# ; -# """ where_query = get_where_query_sql(assetnum, worktype) query = f""" @@ -360,48 +294,11 @@ def _build_tr_row_values( ) rc_cm_material_cost = raw_cm_material_cost_total - # rc_cm_labor_cost = ( - # data_cm_row.get("raw_cm_labor_time") - # * data_cm_row.get("rc_cm_labor_human") - # * man_hour_value - # if data_cm_row - # and data_cm_row.get("rc_cm_labor_cost") - # and data_cm_row.get("rc_cm_labor_human") - # and man_hour_value is not None - # else 0 - # ) rc_pm_material_cost = raw_pm_material_cost - # rc_pm_labor_cost = ( - # data_pm_row.get("raw_pm_labor_time") - # * data_pm_row.get("rc_pm_labor_human") - # * man_hour_value - # if data_pm_row - # and data_pm_row.get("rc_pm_labor_cost") - # and data_pm_row.get("rc_pm_labor_human") - # and man_hour_value is not None - # else 0 - # ) rc_oh_material_cost = raw_oh_material_cost - # rc_oh_labor_cost = ( - # data_oh_row.get("raw_oh_labor_time") - # * data_oh_row.get("rc_oh_labor_human") - # * man_hour_value - # if data_oh_row - # and data_oh_row.get("rc_oh_labor_cost") - # and data_oh_row.get("rc_oh_labor_human") - # and man_hour_value is not None - # else 0 - # ) - - # rc_predictive_labor_cost = ( - # data_predictive_row.get("raw_predictive_labor_human") * man_hour_value - # if data_predictive_row - # and data_predictive_row.get("rc_predictive_labor_cost") - # and man_hour_value is not None - # else 0 - # ) + if labour_cost_lookup and year is not None: cm_lookup = labour_cost_lookup.get("CM", {}) @@ -987,18 +884,14 @@ async def query_data(target_assetnum: str = None): print(f"Error checking acquisition data for {assetnum}: {exc}") - forecasting_start_year_db = row.get("forecasting_start_year") - acquisition_year = row.get("acquisition_year") + # Calculation start is always 2014 (forecasting start is 2015) + # Forecasting and calculation start configuration + loop_start_year = 2014 + + # Delete data before calculation start (2014) + cursor.execute("DELETE FROM lcc_equipment_tr_data WHERE assetnum = %s AND tahun < %s", (assetnum, loop_start_year)) - if acquisition_year: - # Remove data before acquisition_year - cursor.execute("DELETE FROM lcc_equipment_tr_data WHERE assetnum = %s AND tahun < %s", (assetnum, acquisition_year)) - forecasting_start_year = acquisition_year - elif forecasting_start_year_db: - # If no acquisition_year but forecasting_start_year defined in DB - forecasting_start_year = forecasting_start_year_db - else: - forecasting_start_year = 2014 + forecasting_start_year = loop_start_year asset_start = datetime.now() processed_assets += 1 @@ -1024,6 +917,18 @@ async def query_data(target_assetnum: str = None): "OH": get_labour_cost_totals(cursor_wo, assetnum, "OH"), } + # Find first year with replace_cost > 0 in Maximo (Requirement: ignore costs in this year) + cursor_wo.execute(""" + select DATE_PART('year', a.reportdate) AS year + from wo_maximo a + where a.asset_replacecost > 0 + and a.asset_assetnum = %s + order by a.reportdate asc + limit 1; + """, (assetnum,)) + res_rep = cursor_wo.fetchone() + first_rep_year = int(res_rep[0]) if res_rep else None + seq = 0 # Looping untuk setiap tahun for year in range(forecasting_start_year, current_year + 1): @@ -1074,6 +979,23 @@ async def query_data(target_assetnum: str = None): year=year, labour_cost_lookup=labour_cost_lookup, ) + + # Requirement: At the first year of the replace cost detected > 0, + # The material cost/ labor cost is ignored. + if first_rep_year and year == first_rep_year: + cost_keys = [ + "raw_cm_material_cost", "raw_cm_labor_time", + "raw_pm_material_cost", "raw_pm_labor_time", + "raw_oh_material_cost", "raw_oh_labor_time", + "raw_predictive_material_cost", "raw_predictive_labor_time", + "rc_cm_material_cost", "rc_cm_labor_cost", + "rc_pm_material_cost", "rc_pm_labor_cost", + "rc_oh_material_cost", "rc_oh_labor_cost", + "rc_predictive_labor_cost" + ] + for k in cost_keys: + if k in row_values: + row_values[k] = 0.0 if not data_exists: cursor.execute( insert_query, diff --git a/src/modules/equipment/run.py b/src/modules/equipment/run.py index 4c7637d..6795c94 100644 --- a/src/modules/equipment/run.py +++ b/src/modules/equipment/run.py @@ -20,12 +20,12 @@ def format_execution_time(execution_time): return f"{execution_time:.2f} seconds." # Alternative calling function to just predict and calculate eac without inserting actual data -async def simulate(): +async def simulate(assetnum: str = None): start_time = time.time() - print("Starting simulation (predict + eac)...") + print(f"Starting simulation (predict + eac) {'for ' + assetnum if assetnum else 'for all assets'}...") try: - prediction_result = await predict_run() + prediction_result = await predict_run(assetnum=assetnum) if prediction_result is False: print("Prediction step failed or was skipped. Skipping EAC run.") return @@ -34,7 +34,7 @@ async def simulate(): return try: - result = eac_run() + result = eac_run(assetnum=assetnum) if asyncio.iscoroutine(result): result = await result print("EAC run completed.") @@ -48,17 +48,18 @@ async def simulate(): return message # Panggil fungsi -async def main(): +async def main(assetnum: str = None): start_time = time.time() + print(f"Starting calculation workflow {'for ' + assetnum if assetnum else 'for all assets'}...") try: - await query_data() + await query_data(target_assetnum=assetnum) except Exception as e: print(f"Error in query_data: {str(e)}") return try: - prediction_result = await predict_run() + prediction_result = await predict_run(assetnum=assetnum) if prediction_result is False: print("Prediction step failed or was skipped. Skipping EAC run.") return @@ -67,7 +68,7 @@ async def main(): return try: - result = eac_run() + result = eac_run(assetnum=assetnum) if asyncio.iscoroutine(result): result = await result print("EAC run completed.") @@ -81,9 +82,14 @@ async def main(): return message if __name__ == "__main__": - import sys - # Use 'simulate' argument to run without query_data - if len(sys.argv) > 1 and sys.argv[1] == "simulate": - asyncio.run(simulate()) + import argparse + parser = argparse.ArgumentParser(description="Run LCCA Simulation") + parser.add_argument("mode", nargs="?", choices=["main", "simulate"], default="main", help="Mode to run: 'main' (full) or 'simulate' (no data refresh)") + parser.add_argument("--assetnum", type=str, help="Specific asset number to process") + + args = parser.parse_args() + + if args.mode == "simulate": + asyncio.run(simulate(assetnum=args.assetnum)) else: - asyncio.run(main()) + asyncio.run(main(assetnum=args.assetnum)) From b0b68b06d30a7d30008e8e7037fad1d67e7d0798 Mon Sep 17 00:00:00 2001 From: MrWaradana Date: Mon, 23 Feb 2026 14:51:27 +0700 Subject: [PATCH 13/16] unit test --- Jenkinsfile | 12 ++ src/__pycache__/__init__.cpython-311.pyc | Bin 143 -> 139 bytes src/__pycache__/api.cpython-311.pyc | Bin 5606 -> 5602 bytes src/__pycache__/config.cpython-311.pyc | Bin 4836 -> 4832 bytes src/__pycache__/enums.cpython-311.pyc | Bin 1065 -> 1061 bytes src/__pycache__/exceptions.cpython-311.pyc | Bin 7437 -> 8455 bytes src/__pycache__/logging.cpython-311.pyc | Bin 4872 -> 4987 bytes src/__pycache__/main.cpython-311.pyc | Bin 5506 -> 5990 bytes src/__pycache__/models.cpython-311.pyc | Bin 7224 -> 7220 bytes src/__pycache__/rate_limiter.cpython-311.pyc | Bin 335 -> 331 bytes src/auth/__pycache__/__init__.cpython-311.pyc | Bin 148 -> 144 bytes src/auth/__pycache__/model.cpython-311.pyc | Bin 535 -> 531 bytes src/auth/__pycache__/service.cpython-311.pyc | Bin 3809 -> 3805 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 152 -> 148 bytes src/database/__pycache__/core.cpython-311.pyc | Bin 8688 -> 8684 bytes .../__pycache__/service.cpython-311.pyc | Bin 6538 -> 6534 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 153 -> 149 bytes .../__pycache__/model.cpython-311.pyc | Bin 10376 -> 10372 bytes .../__pycache__/router.cpython-311.pyc | Bin 16624 -> 16620 bytes .../__pycache__/schema.cpython-311.pyc | Bin 16483 -> 16479 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 160 -> 156 bytes .../__pycache__/model.cpython-311.pyc | Bin 2597 -> 2593 bytes .../__pycache__/router.cpython-311.pyc | Bin 2436 -> 2432 bytes .../__pycache__/schema.cpython-311.pyc | Bin 3755 -> 3751 bytes .../__pycache__/service.cpython-311.pyc | Bin 2918 -> 2914 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 154 -> 150 bytes .../__pycache__/model.cpython-311.pyc | Bin 1086 -> 1082 bytes .../__pycache__/router.cpython-311.pyc | Bin 6440 -> 6436 bytes .../__pycache__/schema.cpython-311.pyc | Bin 4220 -> 4216 bytes .../__pycache__/service.cpython-311.pyc | Bin 14452 -> 14448 bytes .../__pycache__/config.cpython-311.pyc | Bin 2416 -> 2412 bytes .../equipment/__pycache__/Eac.cpython-311.pyc | Bin 15665 -> 15661 bytes .../__pycache__/Prediksi.cpython-311.pyc | Bin 52571 -> 52567 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 160 -> 156 bytes .../__pycache__/model.cpython-311.pyc | Bin 2635 -> 2631 bytes .../__pycache__/router.cpython-311.pyc | Bin 4715 -> 4711 bytes .../__pycache__/schema.cpython-311.pyc | Bin 7964 -> 7960 bytes .../__pycache__/service.cpython-311.pyc | Bin 3703 -> 3699 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 166 -> 162 bytes .../__pycache__/model.cpython-311.pyc | Bin 5383 -> 5379 bytes .../__pycache__/router.cpython-311.pyc | Bin 8338 -> 8334 bytes .../__pycache__/schema.cpython-311.pyc | Bin 11852 -> 11848 bytes .../__pycache__/service.cpython-311.pyc | Bin 14712 -> 14708 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 152 -> 148 bytes .../__pycache__/model.cpython-311.pyc | Bin 1764 -> 1760 bytes .../__pycache__/router.cpython-311.pyc | Bin 4627 -> 4623 bytes .../__pycache__/schema.cpython-311.pyc | Bin 4368 -> 4364 bytes .../__pycache__/service.cpython-311.pyc | Bin 3883 -> 3879 bytes test_masterdata_output.txt | 111 +++++++++++++ test_masterdata_output_2.txt | 155 ++++++++++++++++++ test_output.txt | 38 +++++ test_output_e2e.txt | 141 ++++++++++++++++ tests/conftest.py | 90 ++++++---- tests/database.py | 3 - tests/e2e/test_acquisition_cost.py | 23 +++ tests/e2e/test_equipment.py | 26 +++ tests/e2e/test_equipment_master.py | 8 + tests/e2e/test_healthcheck.py | 8 + tests/e2e/test_masterdata.py | 97 +++++++++++ tests/e2e/test_masterdata_simulations.py | 8 + tests/e2e/test_plant_fs_transaction.py | 16 ++ tests/e2e/test_plant_masterdata.py | 20 +++ tests/e2e/test_plant_transaction.py | 18 ++ tests/e2e/test_simulation.py | 19 +++ tests/e2e/test_yeardata.py | 18 ++ tests/factories.py | 33 ---- tests/unit/test_masterdata_logic.py | 24 +++ tests/unit/test_masterdata_service.py | 37 +++++ 68 files changed, 837 insertions(+), 68 deletions(-) create mode 100644 test_masterdata_output.txt create mode 100644 test_masterdata_output_2.txt create mode 100644 test_output.txt create mode 100644 test_output_e2e.txt delete mode 100644 tests/database.py create mode 100644 tests/e2e/test_acquisition_cost.py create mode 100644 tests/e2e/test_equipment.py create mode 100644 tests/e2e/test_equipment_master.py create mode 100644 tests/e2e/test_healthcheck.py create mode 100644 tests/e2e/test_masterdata.py create mode 100644 tests/e2e/test_masterdata_simulations.py create mode 100644 tests/e2e/test_plant_fs_transaction.py create mode 100644 tests/e2e/test_plant_masterdata.py create mode 100644 tests/e2e/test_plant_transaction.py create mode 100644 tests/e2e/test_simulation.py create mode 100644 tests/e2e/test_yeardata.py delete mode 100644 tests/factories.py create mode 100644 tests/unit/test_masterdata_logic.py create mode 100644 tests/unit/test_masterdata_service.py diff --git a/Jenkinsfile b/Jenkinsfile index 55d7922..f9a3e7e 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -43,6 +43,18 @@ pipeline { } } + stage('Run Unit Tests') { + steps { + sh 'poetry run pytest tests/unit' + } + } + + // stage('Run E2E Tests') { + // steps { + // sh 'poetry run pytest tests/e2e' + // } + // } + stage('Docker Login') { steps { // Fixed variable names based on the 'DOCKER_AUTH' environment key diff --git a/src/__pycache__/__init__.cpython-311.pyc b/src/__pycache__/__init__.cpython-311.pyc index 426c2483b7abe0c04ad706bbaffb96541c02573f..a55b27f4dbeabf2de4a24ecd56e5fd495264d78f 100644 GIT binary patch delta 44 ycmeBY>}KRy&dbZi00j2C)@4lOF%gk-wu(tfEsIG?)y+vxPK+roN{*QrVGaNR#0?<; delta 48 zcmeBX>}TXz&dbZi00akfc`_&Rm`Et;XXNLm>L+IA=IbY=>gFUTC+Zg$CF@TNF$VxD Chz-X8 diff --git a/src/__pycache__/api.cpython-311.pyc b/src/__pycache__/api.cpython-311.pyc index dc55969c6178fe27322c40735f0f575b97b6f50a..7e81a85ee4cf0c43e16c93ad03996bc91e175123 100644 GIT binary patch delta 49 zcmaE+{YaZ{IWI340}x0?cxSo@Y~ diff --git a/src/__pycache__/config.cpython-311.pyc b/src/__pycache__/config.cpython-311.pyc index 95f6f552f3e4b6dac98333382238072dd72464b6..825efb63fb99b45f35909f72fea6908f1af1d2ed 100644 GIT binary patch delta 49 zcmaE&`aqR$IWI340}$-|(4EQ6yOHlOi-?S~RZL21Sxi!@ZccJ?VoY&Sa?IwJEWddH Dg?bS7 delta 53 zcmaE$`b3p)IWI340}z-WZOdHFvytyGi-dxHMt*Lpeqv^BzJ5}wZccJ?qJD8vvi|0$ HEWddH!2S{O diff --git a/src/__pycache__/enums.cpython-311.pyc b/src/__pycache__/enums.cpython-311.pyc index 1c7f55dffce2941e40ae018e187ca20210650e2f..8d88bbfe917939fd7341d0902b3cba778417b212 100644 GIT binary patch delta 48 zcmZ3%IWI340}$BnT9;wBkvEu0MB3RZCMC5jCMi`nCpkGWrno3MW^)D8Mn(WZ CwGN;F delta 52 zcmZ3=v66##IWI340}vd{<;hgv$Q#TgA+Mj2pPQ7%Q6rS~Zy&M1U#{bTb^QY88F(^a=J5fM|P;rTbtVW`^jf~gCwl=XDFI5_C zY7P}h&`Jq|_-O<60YU&*1cd_J~jAU7g#Dyzt)U-&wz?(ShI4Hn@k>{IvGjHB} z{@&ZUm3aHS^9zT=ilDvz?yda&;J3~ztS9nZA|+?9re4jCDVa<P>_?z(}D7{2GxCaiyL>>=5@VXoQ8vr<13AO~GRG9m^Zw`)wnVYtAJ z4zQgac8h@p7&wm^^oQI2GJl$U!AX7-edA|yQ~Qmu2%T}vFS<4vpbNd#s&1-;u)uWi z9;Ce4>*@&6q&VwVUF3VI*L9+jc+9bP(k$~D``<(~wsz*V6UMchCg23U)smoYq1&(u z?Hqt`5|GiP5UF){RlJ7BDyMNcu5qgt%~J3gtJv9To!;A~dw#>S5NOYW#%z@bk5JnZ zG!|~})_6dVCP2AxWWp2ldXLsmr12RAG$s50pEjX?(k2=p4?WXjXfCbf<+PeFE=95! z7crUjMl05RKagn`0sKu`+*I=WKLUJ+7LbN6Us+|;4j&m+ zJ1lTJZYpQYkc3oA+kh;}Opv4{KmE{RV42$T(mnRXs+l)gr~v4#RyEVAtgM@E&L>fn zd&I<=tWwN0*~Rig)2v=AWuq*n`NK4Im~bbsDN%J}Q*d)yDNn=1 za4W;HauMK7q$55ob$}fOdmh-3H`3@ExSPA3`@DSb$!&If<~!@&=wy9#a<6Zy-Z#~V z3{_`7vo?AoRo})dI&b#+4iLa0g+B!3A9$~@?or-Xbla)$c!0d)9VZjsVMo)PQ`J(G zIg)_<;9Yn*jGqQd_!$cHxx@5{!Sv&Zd z4-mj11)ahJ0l9k`f$%SZof)1w(r~iVWRdxxMnSwxQ_`(CmQbb2rq&dbZi00eT9&6$Q`8~NgxB~g5M6yb6D1JO@1LD&nP!}J(mPaQC*4R}|V74w}0y+tZAs`s!M?n}bO9;jSh8!=@8LLE;>xE%rwX8L) zMV2+JwQMzPSt2meB0gjus@w*U96UU;;EvA{hv}|m2da>O^Ehglfj0Biumef{8V+Dg zu`<;%Pqq^h7Zm_YA;Lru940-2verT=>^00KaKjlGutyQDkdeX_GMd7Z`NZrdUyx9l zoGvCW%#u-3QlQCri#4^Pq$tr(ld(u*@;nKn$xM=IVUk4_KtB}Og9v^Q!3QEh>8{8L zL|B3dFCcM?EhV)iF*B!#8zdqJB6vZB28aN8;TBhJeoAUid`f9n>eoR^o20SI=r_Gab^ZsfCL5|MSbib+W=i%Ck=%}Gv9j43Wkj@g{Tw3L(a z$!0E|WG1;<_H>3C_IR*9klqv)AWsOy0)feld}2%~tdryU{CU~@G?{O)mzHHF=NIKo zKFud3zz5~%B_=1Q78kose!_Q{kz?`-epmZjyb!VYoc#3k)S_YzpkWG7P$UlI+!9RA z&r8cpFD*(1tI5nuFDe6y#{hA$|700~tBm%OKMNQth{{|MQN5sMc2UIqiimlG?+sb` t2Hz_zQa9w3LA3N_MZwjKWs}be$}-kWel7S%QIpY+@dE>PvPcYQJOFI0RhR$( delta 217 zcmeyZ)}h9?oR^o20SIQD?#k2_*vMzcB%!FEk)NBYpO~4Oub-5vo0FWJs9#)^tiL&d zX(=b;g3W9^$xM^i@;S<~_-V4-VlOSrOwKRLtKx$)^b(VkQ;Uly|K^jLtj2$ck$v(@ zepmfscAzB+5KtrzWZx1@&d*EBOfM};jnBzXPtVLtFDeC!#{hA$?&Ms7t4x86lbr<( iCr=k#!B{%kK}eRddUBl5A4L^LKgJIX*vTR>pmqR2wm%;L diff --git a/src/__pycache__/main.cpython-311.pyc b/src/__pycache__/main.cpython-311.pyc index e7365addd0b79f7d3356f3c5df8b69dc7f303aa5..a58a2ef150129a984cfcfacae53b86d449837f72 100644 GIT binary patch delta 1911 zcmaJ?O>7fK6rQo;#IYUQaef>-BzEHbunBRXh@Y5*;247w(gcdiVVlZqypwd(UE9pA z3xQUVs-9>oKyxTUJk zQMYS?C|G=}I;0JY!`cpUhc+UPSXi4H*G9!ri^nP!F`LD`YnB*N_iFpZeHI^9_iJO~ zn3fO|+5z!^g>|aq+Jrb^@!Qmc+N3y%kqr^Na)=ko7fG&Tgl@vAE^!L2}X2n^n8p|h$ z$mG`MSi0A%@#OxkI(dM^mTdCiM|E49C(gm`UhT8}e?IfzbNp$)_HE4rkBLtrL14%2 z^TGBv$e9w+jZ>1U$daKH@@Yy7lznbr^7~Gnm`$p=cZjxXfw%v)pSIa{*4>usui?J; z>sNfW2axODwVsg1jQSde&~-mAz>Z`Gi#U65FS}D8!<_Gr^ln=9A$s(KhL`%)vsI^H zZ!j>s;0&`n&LOtec$r;jyff^aJ0F}8kzdM+; zr3L#v!2!a(xE&Pt6G>O5jy-AmwqqC6MpK+*c$Ib_(P)&;u^IH@&9r!H3T1=rkQS#cIuWhrkcIqKnJgRU0Nf;Da8lJ}_;xB9D7F9%)v z8C9xI7v+NlR?h&Qg!*3u3sj@c9BhByl0Ejhae)2fi`2J(lD4uQe@Cc|+ciVQR3(N% z;4_n&nrb57w|IHB&^0BV|G+fU`;$uwtI8?k!KBtyslI7hb^lL=txo!r?-_CKZAeU5fkd{E? z{|5R`EEG8B-(#XJn_UVl_=iliW#9fYa33c_4^Zgph$)P1phOuZOq6(t?7tumo8g`2 zYf~F2RYoZjrJmteN6p~_8)&?Y#!WQNzG+>Ib>h|=!N&+bo8&J$vUQYk@Ue}<2ia)b Id>ya-KUpR9t^fc4 delta 1487 zcmaJ>O>7%Q6rS0&6R#6HUfaY@Qpa}PG;u<`ia^beo1~`saUe~EC_-9MqV4V^*=X16 z%&yghOC`hsdVy#TNF1tgNs(FvQtog_;XpYQDdJ#>8-h!b!i7T*6>se34heAOF?VHDU%^Q*Ja1vkG?*BSrr_>Zi1~S4{ubo!Yjdh2${bxRB z52yonUd=b4m^Ek*sY8u5wy=u9C2p}{+{u=3`i^~By=;%DBMrXG8nwsNvBtXF8n=sT z5hDQ+yncnBfHyL+ixGMNquJC+;FVoa9vndkBQizCOsrl7E=aB#J;pgBW%NEw@A|#rRF%fm-2Qrfx3vO=s2kvNI&3g8B#*&uvrXPUDLA~_@TF#B&FQao?X z*t6=aJ*UpuWwm_blR%_+HiZT^-;8JE&T_^Dl3o#vz7Kt8Q&eAvGyK2L#sB!c0X_>a zvU=t0K5*suEtKzPPsJtnwol)C?7JxlzleK-t)F9-k#g9_o(Ho28#&}<*t(R$G7lso z!_V$Xd9lUAY)9&5ze$5wVqXNe*}dR*7h4zKkIq+g$2Kdg8A>*4#P!BA&u}J>>yoQG zRkBj6=cNtiB*kBWX!zDFVuKZHubxdfZ;_-aIf{b-4c zS-vyRGn2{CGR*b3W*EyZ(QDN7wwBk3W)SMq1)$zx1CcVm&Nd>8cjOffE~07{C~>P! z#U(D~Uu!*3-7>U_XX><#tF5l(+y*n+&b9Cht>rWAghsht6i!7g;&}v4)_8cA#yBCLBAl8Bl|kd+ zpT;k-r*f*VliS2j*&L^p;IWfjbg@FTr`*jgC3wC9snl$j_Hez8>rh3jJnlJ;L^wjB zRF*B%Ha*BV#o<;C^9$)^+tI{(X~3GrmE~->5%#E_TzdW(v`xdX$Zd@hn&Y6fO1uTw z58_Vo7_V}{|5mh*)1p?b(oxnD8_n_xr@S1S^&31DiI*N-V9T-2-s^nH-~UwA3+ diff --git a/src/__pycache__/models.cpython-311.pyc b/src/__pycache__/models.cpython-311.pyc index 09d2e86c8473e01d49e824724c65fd96840a2e82..e07e086c134dd78ea5fb97f72972013a30735686 100644 GIT binary patch delta 48 zcmdmCvBiRSIWI340}$-|(4F~vBdWvUOiF55Oj4?DPI7W$OmR_i%;pl7BjNyp C;t-Dj delta 52 zcmdmDvBQFQIWI340}xmTRb)1Af-(0|Q GL>vHzQxEz8 diff --git a/src/__pycache__/rate_limiter.cpython-311.pyc b/src/__pycache__/rate_limiter.cpython-311.pyc index 75029b1f710337947d585bfe414201a482ed9190..d9d7738dd1592f0b3b03859790c8a44035846eee 100644 GIT binary patch delta 45 zcmX@lbef5GIWI340}$BnT9;8bk#~=XlCxDzN@`h5QmSrFa&lr!aZz&2#D{qRITa6Y delta 49 zcmX@jbe@TKIWI340}vd{<;kp^$h${EO+O<)H&s6|GdEv9DOEQoIXO|kxF}hF;@vy| DX9W-t diff --git a/src/auth/__pycache__/__init__.cpython-311.pyc b/src/auth/__pycache__/__init__.cpython-311.pyc index e42b5d501a8fb9db5018a502a71ae35baf18f743..224f452c1b83dc15720bd262e530484613ff9cf6 100644 GIT binary patch delta 49 zcmbQjIDwI8IWI340}$BnT9+}A$3#-u*(xR_wJatnRW~O&IWeZVC^;svv?L>DVyrm; DO*;=r delta 53 zcmbQhIE9gCIWI340}vd{<;k4LVk(ZZIQo-3OCMC5jCMi`nCpkGWrno3MCb6_6BWAKW G;{yP9D-cHj delta 56 zcmbQtGM$BYIWI340}vd{<;l$6$ji$ptD>KgpPQnEk^<|HR4>K7L!>!&1^ QBqk*mr|NHZWpxw*03I0hX4Qo delta 58 zcmbQrIFpfQIWI340}vd{<;k4LW1^&^pOK%Ns-Kvdo3Edgs+*IXoTy)1l&qgxSejXo Mo0?amKQYc60MqFb00000 diff --git a/src/equipment/__pycache__/model.cpython-311.pyc b/src/equipment/__pycache__/model.cpython-311.pyc index b10150a5e8961e44f74b454b614e814b45458810..468b9f5ced71b0dad5404a3f46e006a91e93f4d8 100644 GIT binary patch delta 59 zcmeAOYzgFB&dbZi00cWfbZ0iGY~-_KlT&rJib+W=i%Ck=%}Gv9j43Wkj!7*n%`C`G N%`1u7oXIAy1OP0H6Py45 delta 63 zcmZn(>nEk^<|HR4>K7L!>!%i$ RW)|e8=9TDgPGyr<0suSi6IB2J diff --git a/src/equipment/__pycache__/router.cpython-311.pyc b/src/equipment/__pycache__/router.cpython-311.pyc index 77bb12825b6452736035b199f7a1ac7a42e55c82..2b8e1a6a13064e710a837d06ed752cad4213bbce 100644 GIT binary patch delta 61 zcmey+$oQs_k#9LKFBbz4m~!`I9yQv?w~uhT>NGL- diff --git a/src/equipment_master/__pycache__/model.cpython-311.pyc b/src/equipment_master/__pycache__/model.cpython-311.pyc index abfe1c6d3d3737415b894065f1a0b49516343d25..ae571cccbaea35e02ae9eaec87299b1ad8837478 100644 GIT binary patch delta 66 zcmZ1~vQUI?IWI340}$BnT9?7kv5_x{RaM8?DkdehEG8*cHzzqcF{ZdEIVQERG_xQ# UHLoN-H?g=RwJ2uuOx7M20Et5t-~a#s delta 70 zcmZ1|vQ&g`IWI340}x!|iOW30zL779RntH}BR@A)KQS{mUq2~THzzqcQNOq-SwFR~ YG_xQ#HLoN-H?g=RwMc*SRMs9A0OsQt7XSbN diff --git a/src/equipment_master/__pycache__/router.cpython-311.pyc b/src/equipment_master/__pycache__/router.cpython-311.pyc index 7ddee05d29c5b4f08a2cbdfe481fe39abe346a59..fad7adaf6844502c7411512e23feaf59529b4fb3 100644 GIT binary patch delta 65 zcmZn>ZV={O&dbZi00h^3D>D5y@_uJl)pfRtNl7h>NlMktNls3TDK1KmNi8hREXYmG TD~ZodEG|hcirK8o63z+$ltmS} delta 69 zcmZn=ZV~2P&dbZi00ePv6`67ydA~Dj8tP}{=cei>X6EMWC#CA2KC#311NZU6uP delta 70 zcmZ23yIPiSIWI340}#ZyRb<{_-N+ZttZAs9k)NBYpO~4Oub-5vo0FWJs9#)^te;v~ Ynpu#WnpYB^n^;_uTBN_Zi&>T%0Oso!q5uE@ diff --git a/src/equipment_master/__pycache__/service.cpython-311.pyc b/src/equipment_master/__pycache__/service.cpython-311.pyc index a923155c29bd3079316aae24bc38619ec7dd087d..6d7dc6c6a8b4af66bb17325233b65c0ff0606d0e 100644 GIT binary patch delta 65 zcmaDR_DGC(IWI340}$BnT9=Wxk=L6^RnOTfCMC5jCMi`nCpkGWrno3MCbh6MvmiG$ TuOvPo&G diff --git a/src/masterdata/__pycache__/model.cpython-311.pyc b/src/masterdata/__pycache__/model.cpython-311.pyc index 892b12a129b5f9502e020012a0ebbb13e4536700..ae67aeb30de10edd73fa669148af84c76dc16daf 100644 GIT binary patch delta 59 zcmdnTv5SLuIWI340}x0?cxSHK$m_=>ujXtOlag8%la#8PlboCwQ(TlBlbcvvl3J9K NSdtjCxsK@oBLMXJ67&E7 delta 63 zcmdnRv5$jyIWI340}w=gGtZo{k=Kt&SxY}7KQ~oBF*7$`KPgo=CpkG$zqlw_KR2 delta 64 zcmZ2tw8DsQIWI340}vc-?98ke+{nkvrmU@>k)NBYpO~4Oub-5vo0FWJs9#)^te=}$ ST#{Opl30?czuAiIl@I`Kz7)Rz diff --git a/src/masterdata/__pycache__/schema.cpython-311.pyc b/src/masterdata/__pycache__/schema.cpython-311.pyc index 1531dca8e1731ef53d13b2fa164043c90ec175f2..1ebe3df49bd98813b37dd5b66cf18d4357e73a21 100644 GIT binary patch delta 59 zcmeyP@I!%jIWI340}x#Et;jsRk@p>oyt=bhOiF55Oj4?DPI7W$OmR_iOm1RvNor9_ NVo74mW;xb4ZUA4M6pjD@ delta 63 zcmeyN@JE4nIWI340}#ZyRb+N=nCMy6x_Y*<@ diff --git a/src/modules/equipment/__pycache__/Eac.cpython-311.pyc b/src/modules/equipment/__pycache__/Eac.cpython-311.pyc index 337131275f9427471eee3f353aa109abb7b1b23b..6b8e5e2095b13574c9cf05763081b2c34c8d508e 100644 GIT binary patch delta 67 zcmdm3wYG|HIWI340}$-|(4G0fWh0+IlbW`(RZL21Sxi!@ZccJ?VoY&Sa!hW1N@-4N VaZGAqX=XugYFta$JOWIWI340}$BnT9V delta 69 zcmX>ua$1CUIWI340}vd{<;mQ!kvEr3(?CBXKQ~oBF*7$`KPgo=CpkG$zqlw_zaS?u XuOvP?IWI340}$-|(4ASZkvEA|RoB@nCMC5jCMi`nCpkGWrno3MrXVLVuOvP< TvA86)C?&BZF=q1&*2_Eq+%OlR delta 69 zcmaE^@>+#=IWI340}!}`c4qQyfFUU#E XD~ZodEG|hcN=Yn9)ZaXX^)e3t-`N)s diff --git a/src/plant_masterdata/__pycache__/schema.cpython-311.pyc b/src/plant_masterdata/__pycache__/schema.cpython-311.pyc index 511e8aaa4c13f3ca9db9ba6a66740e6e61090784..49fa0130b7955d7d0c1cf97d604790d4a78e5e7a 100644 GIT binary patch delta 65 zcmbPZH^Yv1IWI340}z-VT$gcaBX0n=s;;wDOiF55Oj4?DPI7W$OmR_iOhHa!UP*jz TVsS}oQA%P-V$9|??rFjRvxyeE delta 69 zcmbPXH^+{5IWI340}vd{<;kqx$Q!_|X{eu(pPQ5%^fpZt+m Mj8T6x2j6ct0Op?&wg3PC diff --git a/src/plant_transaction_data/__pycache__/router.cpython-311.pyc b/src/plant_transaction_data/__pycache__/router.cpython-311.pyc index a2ae08184d22148d4a8bd6d734744c05a38ba3a4..08b112e53cf6181e0dd9ea53754b5c86cccdfcff 100644 GIT binary patch delta 72 zcmbQ_*yqT%oR^o20SI<}=+0a(xsflHUEA2%DkdehEG8*cHzzqcF{ZdEIi?^dF|Q=P aq$n}3I5D{-Ge0jrC9xziX7eoe9})m;l^Qw# delta 59 zcmeBkoaD&2oR^o20SH_|J2US}Y~+h&moV4Q$j?pHPt45C*H22-%}Gv9)Gsbd)}LI@ NF2<<8c^dl<2>|}55;gz; diff --git a/src/plant_transaction_data/__pycache__/schema.cpython-311.pyc b/src/plant_transaction_data/__pycache__/schema.cpython-311.pyc index aaf073cbb71248845dacf9dd789df3260367eca0..b3bdb63b8c6ec42ca7374d0c560f2f66c418dffa 100644 GIT binary patch delta 54 zcmX>Tb0UU!IWI340}$-|(4BdABkyVn5o2eon3UAAn50zQoaE%hnBt=3n8_C;#28~X JKbJ@q1ppDg6MO&w delta 75 zcmX>Rb0&s&IWI340}#C0)R>vJk$1I(zPWxzer~FMVrFi>ep0G#PI7XhesNK@ diff --git a/src/yeardata/__pycache__/model.cpython-311.pyc b/src/yeardata/__pycache__/model.cpython-311.pyc index b5483a110d12bbe8632066399357cbf8aee91b2d..b53a3aa069e188e8ad58cc4989b54d18d3328f4d 100644 GIT binary patch delta 57 zcmaFD`+%2sIWI340}x0?cxMJ}NlMktNls3TDK1KmsZ32QN=Yn9 LjM<#QQo{rQ&*u@U delta 61 zcmaFB`-GQwIWI340}wc~xn|mIk!&rQ`&%*@T#PfFFzNls4GFD^>fuS`uW PN=Yn9)Zd)KQo{rQ238VF diff --git a/src/yeardata/__pycache__/router.cpython-311.pyc b/src/yeardata/__pycache__/router.cpython-311.pyc index 07b7983ecfe9fbd8b44398bb1126004ff20df722..e17ef971fef8ed51f7540dd8197989c170ec7f90 100644 GIT binary patch delta 57 zcmbQN(yzk1oR^o20SI<}=+4}+k=Kn?R@K=mCMC5jCMi`nCpkGWrno3MrZP3LC?&BZ LF=lfK>wF#n2xSvo delta 61 zcmeBInXJOQoR^o20SH_|J2N9U^187qYU*d?=cei>X6EMWC#CATfP!ozDXR4Vn_r diff --git a/src/yeardata/__pycache__/schema.cpython-311.pyc b/src/yeardata/__pycache__/schema.cpython-311.pyc index 8b3a86aca2a607127715bf7a8f468be13ebaddf6..f67b454b5342e3c8963f9385b66a1935c5cacdcc 100644 GIT binary patch delta 72 zcmbQB)T6|^oR^o20SK=7R%9OD$h(DIL)Fep0G#PI7XhesNKCQ|U(OVr diff --git a/test_masterdata_output.txt b/test_masterdata_output.txt new file mode 100644 index 0000000..96ab605 --- /dev/null +++ b/test_masterdata_output.txt @@ -0,0 +1,111 @@ +Traceback (most recent call last): + File "", line 198, in _run_module_as_main + File "", line 88, in _run_code + File "C:\dev\be-lcca\venv\Lib\site-packages\pytest\__main__.py", line 9, in + raise SystemExit(pytest.console_main()) + ^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\config\__init__.py", line 201, in console_main + code = main() + ^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\config\__init__.py", line 156, in main + config = _prepareconfig(args, plugins) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\config\__init__.py", line 341, in _prepareconfig + config = pluginmanager.hook.pytest_cmdline_parse( + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\pluggy\_hooks.py", line 513, in __call__ + return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\pluggy\_manager.py", line 120, in _hookexec + return self._inner_hookexec(hook_name, methods, kwargs, firstresult) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\pluggy\_callers.py", line 139, in _multicall + raise exception.with_traceback(exception.__traceback__) + File "C:\dev\be-lcca\venv\Lib\site-packages\pluggy\_callers.py", line 122, in _multicall + teardown.throw(exception) # type: ignore[union-attr] + ^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\helpconfig.py", line 105, in pytest_cmdline_parse + config = yield + ^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\pluggy\_callers.py", line 103, in _multicall + res = hook_impl.function(*args) + ^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\config\__init__.py", line 1140, in pytest_cmdline_parse + self.parse(args) + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\config\__init__.py", line 1494, in parse + self._preparse(args, addopts=addopts) + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\config\__init__.py", line 1398, in _preparse + self.hook.pytest_load_initial_conftests( + File "C:\dev\be-lcca\venv\Lib\site-packages\pluggy\_hooks.py", line 513, in __call__ + return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\pluggy\_manager.py", line 120, in _hookexec + return self._inner_hookexec(hook_name, methods, kwargs, firstresult) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\pluggy\_callers.py", line 139, in _multicall + raise exception.with_traceback(exception.__traceback__) + File "C:\dev\be-lcca\venv\Lib\site-packages\pluggy\_callers.py", line 122, in _multicall + teardown.throw(exception) # type: ignore[union-attr] + ^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\warnings.py", line 151, in pytest_load_initial_conftests + return (yield) + ^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\pluggy\_callers.py", line 122, in _multicall + teardown.throw(exception) # type: ignore[union-attr] + ^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\capture.py", line 154, in pytest_load_initial_conftests + yield + File "C:\dev\be-lcca\venv\Lib\site-packages\pluggy\_callers.py", line 103, in _multicall + res = hook_impl.function(*args) + ^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\config\__init__.py", line 1222, in pytest_load_initial_conftests + self.pluginmanager._set_initial_conftests( + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\config\__init__.py", line 581, in _set_initial_conftests + self._try_load_conftest( + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\config\__init__.py", line 619, in _try_load_conftest + self._loadconftestmodules( + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\config\__init__.py", line 659, in _loadconftestmodules + mod = self._importconftest( + ^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\config\__init__.py", line 710, in _importconftest + mod = import_path( + ^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\pathlib.py", line 587, in import_path + importlib.import_module(module_name) + File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.11_3.11.2544.0_x64__qbz5n2kfra8p0\Lib\importlib\__init__.py", line 126, in import_module + return _bootstrap._gcd_import(name[level:], package, level) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "", line 1204, in _gcd_import + File "", line 1176, in _find_and_load + File "", line 1147, in _find_and_load_unlocked + File "", line 690, in _load_unlocked + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\assertion\rewrite.py", line 184, in exec_module + exec(co, module.__dict__) + File "C:\dev\be-lcca\tests\conftest.py", line 20, in + from fastapi import Request + File "C:\dev\be-lcca\venv\Lib\site-packages\fastapi\__init__.py", line 7, in + from .applications import FastAPI as FastAPI + File "C:\dev\be-lcca\venv\Lib\site-packages\fastapi\applications.py", line 16, in + from fastapi import routing + File "C:\dev\be-lcca\venv\Lib\site-packages\fastapi\routing.py", line 34, in + from fastapi.dependencies.models import Dependant + File "C:\dev\be-lcca\venv\Lib\site-packages\fastapi\dependencies\models.py", line 5, in + from fastapi.security.base import SecurityBase + File "C:\dev\be-lcca\venv\Lib\site-packages\fastapi\security\__init__.py", line 1, in + from .api_key import APIKeyCookie as APIKeyCookie + File "C:\dev\be-lcca\venv\Lib\site-packages\fastapi\security\api_key.py", line 6, in + from starlette.requests import Request + File "C:\dev\be-lcca\venv\Lib\site-packages\starlette\requests.py", line 12, in + from starlette.formparsers import FormParser, MultiPartException, MultiPartParser + File "C:\dev\be-lcca\venv\Lib\site-packages\starlette\formparsers.py", line 17, in + import python_multipart as multipart + File "C:\dev\be-lcca\venv\Lib\site-packages\python_multipart\__init__.py", line 7, in + from .multipart import ( + File "C:\dev\be-lcca\venv\Lib\site-packages\python_multipart\multipart.py", line 115, in + class MultipartState(IntEnum): + File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.11_3.11.2544.0_x64__qbz5n2kfra8p0\Lib\enum.py", line 647, in __new__ + delattr(enum_class, '_singles_mask_') + File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.11_3.11.2544.0_x64__qbz5n2kfra8p0\Lib\enum.py", line 752, in __delattr__ + super().__delattr__(attr) + ^^^^^^^ +KeyboardInterrupt diff --git a/test_masterdata_output_2.txt b/test_masterdata_output_2.txt new file mode 100644 index 0000000..67fb7e0 --- /dev/null +++ b/test_masterdata_output_2.txt @@ -0,0 +1,155 @@ +Traceback (most recent call last): + File "", line 198, in _run_module_as_main + File "", line 88, in _run_code + File "C:\dev\be-lcca\venv\Lib\site-packages\pytest\__main__.py", line 9, in + raise SystemExit(pytest.console_main()) + ^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\config\__init__.py", line 201, in console_main + code = main() + ^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\config\__init__.py", line 156, in main + config = _prepareconfig(args, plugins) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\config\__init__.py", line 341, in _prepareconfig + config = pluginmanager.hook.pytest_cmdline_parse( + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\pluggy\_hooks.py", line 513, in __call__ + return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\pluggy\_manager.py", line 120, in _hookexec + return self._inner_hookexec(hook_name, methods, kwargs, firstresult) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\pluggy\_callers.py", line 139, in _multicall + raise exception.with_traceback(exception.__traceback__) + File "C:\dev\be-lcca\venv\Lib\site-packages\pluggy\_callers.py", line 122, in _multicall + teardown.throw(exception) # type: ignore[union-attr] + ^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\helpconfig.py", line 105, in pytest_cmdline_parse + config = yield + ^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\pluggy\_callers.py", line 103, in _multicall + res = hook_impl.function(*args) + ^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\config\__init__.py", line 1140, in pytest_cmdline_parse + self.parse(args) + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\config\__init__.py", line 1494, in parse + self._preparse(args, addopts=addopts) + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\config\__init__.py", line 1398, in _preparse + self.hook.pytest_load_initial_conftests( + File "C:\dev\be-lcca\venv\Lib\site-packages\pluggy\_hooks.py", line 513, in __call__ + return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\pluggy\_manager.py", line 120, in _hookexec + return self._inner_hookexec(hook_name, methods, kwargs, firstresult) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\pluggy\_callers.py", line 139, in _multicall + raise exception.with_traceback(exception.__traceback__) + File "C:\dev\be-lcca\venv\Lib\site-packages\pluggy\_callers.py", line 122, in _multicall + teardown.throw(exception) # type: ignore[union-attr] + ^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\warnings.py", line 151, in pytest_load_initial_conftests + return (yield) + ^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\pluggy\_callers.py", line 122, in _multicall + teardown.throw(exception) # type: ignore[union-attr] + ^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\capture.py", line 154, in pytest_load_initial_conftests + yield + File "C:\dev\be-lcca\venv\Lib\site-packages\pluggy\_callers.py", line 103, in _multicall + res = hook_impl.function(*args) + ^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\config\__init__.py", line 1222, in pytest_load_initial_conftests + self.pluginmanager._set_initial_conftests( + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\config\__init__.py", line 581, in _set_initial_conftests + self._try_load_conftest( + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\config\__init__.py", line 619, in _try_load_conftest + self._loadconftestmodules( + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\config\__init__.py", line 659, in _loadconftestmodules + mod = self._importconftest( + ^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\config\__init__.py", line 710, in _importconftest + mod = import_path( + ^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\pathlib.py", line 587, in import_path + importlib.import_module(module_name) + File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.11_3.11.2544.0_x64__qbz5n2kfra8p0\Lib\importlib\__init__.py", line 126, in import_module + return _bootstrap._gcd_import(name[level:], package, level) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "", line 1204, in _gcd_import + File "", line 1176, in _find_and_load + File "", line 1147, in _find_and_load_unlocked + File "", line 690, in _load_unlocked + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\assertion\rewrite.py", line 184, in exec_module + exec(co, module.__dict__) + File "C:\dev\be-lcca\tests\conftest.py", line 22, in + from src.main import app + File "C:\dev\be-lcca\src\main.py", line 33, in + from src.api import api_router + File "C:\dev\be-lcca\src\api.py", line 22, in + from src.simulations.router import router as simulations_router + File "C:\dev\be-lcca\src\simulations\__init__.py", line 1, in + from .router import router + File "C:\dev\be-lcca\src\simulations\router.py", line 17, in + from src.simulations.service import create, delete, get, get_all, run_simulation, update + File "C:\dev\be-lcca\src\simulations\service.py", line 34, in + column.key for column in sa_inspect(MasterData).mapper.column_attrs if column.key != "id" + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\sqlalchemy\util\langhelpers.py", line 1257, in __get__ + obj.__dict__[self.__name__] = result = self.fget(obj) + ^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\sqlalchemy\orm\mapper.py", line 3172, in column_attrs + return self._filter_properties(properties.ColumnProperty) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\sqlalchemy\orm\mapper.py", line 3225, in _filter_properties + self._check_configure() + File "C:\dev\be-lcca\venv\Lib\site-packages\sqlalchemy\orm\mapper.py", line 2401, in _check_configure + _configure_registries({self.registry}, cascade=True) + File "C:\dev\be-lcca\venv\Lib\site-packages\sqlalchemy\orm\mapper.py", line 4213, in _configure_registries + _do_configure_registries(registries, cascade) + File "C:\dev\be-lcca\venv\Lib\site-packages\sqlalchemy\orm\mapper.py", line 4254, in _do_configure_registries + mapper._post_configure_properties() + File "C:\dev\be-lcca\venv\Lib\site-packages\sqlalchemy\orm\mapper.py", line 2421, in _post_configure_properties + prop.post_instrument_class(self) + File "C:\dev\be-lcca\venv\Lib\site-packages\sqlalchemy\orm\interfaces.py", line 1113, in post_instrument_class + self.strategy.init_class_attribute(mapper) + File "C:\dev\be-lcca\venv\Lib\site-packages\sqlalchemy\orm\strategies.py", line 254, in init_class_attribute + _register_attribute( + File "C:\dev\be-lcca\venv\Lib\site-packages\sqlalchemy\orm\strategies.py", line 126, in _register_attribute + desc = attributes.register_attribute_impl( + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\sqlalchemy\orm\attributes.py", line 2605, in register_attribute_impl + "_Dispatch[QueryableAttribute[Any]]", manager[key].dispatch + ^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\sqlalchemy\event\base.py", line 465, in __get__ + if hasattr(obj, "_slots_dispatch"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\sqlalchemy\orm\attributes.py", line 472, in __getattr__ + return getattr(self.comparator, key) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\sqlalchemy\util\langhelpers.py", line 1332, in __getattr__ + return self._fallback_getattr(key) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\sqlalchemy\orm\properties.py", line 472, in _fallback_getattr + return getattr(self.__clause_element__(), key) + ^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\sqlalchemy\util\langhelpers.py", line 1319, in oneshot + result = fn(*args, **kw) + ^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\sqlalchemy\orm\properties.py", line 439, in _memoized_method___clause_element__ + return self._orm_annotate_column(self.prop.columns[0]) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\sqlalchemy\orm\properties.py", line 425, in _orm_annotate_column + return col._annotate(annotations)._set_propagate_attrs( + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\sqlalchemy\sql\annotation.py", line 129, in _annotate + return Annotated._as_annotated_instance(self, values) # type: ignore + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\sqlalchemy\sql\annotation.py", line 277, in _as_annotated_instance + return cls(element, values) + ^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\sqlalchemy\sql\elements.py", line 5313, in __init__ + Annotated.__init__(self, element, values) + File "C:\dev\be-lcca\venv\Lib\site-packages\sqlalchemy\sql\annotation.py", line 289, in __init__ + self.__dict__ = element.__dict__.copy() + ^^^^^^^^^^^^^^^^^^^^^^^ +KeyboardInterrupt diff --git a/test_output.txt b/test_output.txt new file mode 100644 index 0000000..5acd846 --- /dev/null +++ b/test_output.txt @@ -0,0 +1,38 @@ +C:\dev\be-lcca\venv\Lib\site-packages\pytest_asyncio\plugin.py:247: PytestDeprecationWarning: The configuration option "asyncio_default_fixture_loop_scope" is unset. +The event loop scope for asynchronous fixtures will default to the fixture caching scope. Future versions of pytest-asyncio will default the loop scope for asynchronous fixtures to function scope. Set the default fixture loop scope explicitly in order to avoid unexpected behavior in the future. Valid fixture loop scopes are: "function", "class", "module", "package", "session" + + warnings.warn(PytestDeprecationWarning(_DEFAULT_FIXTURE_LOOP_SCOPE_UNSET)) +============================= test session starts ============================= +platform win32 -- Python 3.11.9, pytest-8.3.4, pluggy-1.5.0 -- C:\dev\be-lcca\venv\Scripts\python.exe +cachedir: .pytest_cache +rootdir: C:\dev\be-lcca +configfile: pyproject.toml +plugins: anyio-4.8.0, Faker-30.10.0, asyncio-1.3.0 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collecting ... collected 1 item + +tests/test_healthcheck.py::test_healthcheck PASSED [100%] + +============================== warnings summary =============================== +venv\Lib\site-packages\pydantic\_internal\_config.py:295 + C:\dev\be-lcca\venv\Lib\site-packages\pydantic\_internal\_config.py:295: PydanticDeprecatedSince20: Support for class-based `config` is deprecated, use ConfigDict instead. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.10/migration/ + warnings.warn(DEPRECATION_MESSAGE, DeprecationWarning) + +venv\Lib\site-packages\pydantic\fields.py:1042: 473 warnings + C:\dev\be-lcca\venv\Lib\site-packages\pydantic\fields.py:1042: PydanticDeprecatedSince20: Using extra keyword arguments on `Field` is deprecated and will be removed. Use `json_schema_extra` instead. (Extra keys: 'nullable'). Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.10/migration/ + warn( + +venv\Lib\site-packages\pydantic\_internal\_generate_schema.py:297: 115 warnings + C:\dev\be-lcca\venv\Lib\site-packages\pydantic\_internal\_generate_schema.py:297: PydanticDeprecatedSince20: `json_encoders` is deprecated. See https://docs.pydantic.dev/2.10/concepts/serialization/#custom-serializers for alternatives. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.10/migration/ + warnings.warn( + +src\database\core.py:115 + C:\dev\be-lcca\src\database\core.py:115: MovedIn20Warning: The ``declarative_base()`` function is now available as sqlalchemy.orm.declarative_base(). (deprecated since: 2.0) (Background on SQLAlchemy 2.0 at: https://sqlalche.me/e/b8d9) + Base = declarative_base(cls=CustomBase) + +tests/test_healthcheck.py::test_healthcheck + C:\dev\be-lcca\venv\Lib\site-packages\httpx\_client.py:1437: DeprecationWarning: The 'app' shortcut is now deprecated. Use the explicit style 'transport=ASGITransport(app=...)' instead. + warnings.warn(message, DeprecationWarning) + +-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html +======================= 1 passed, 591 warnings in 0.95s ======================= diff --git a/test_output_e2e.txt b/test_output_e2e.txt new file mode 100644 index 0000000..2291833 --- /dev/null +++ b/test_output_e2e.txt @@ -0,0 +1,141 @@ +Traceback (most recent call last): + File "", line 198, in _run_module_as_main + File "", line 88, in _run_code + File "C:\dev\be-lcca\venv\Lib\site-packages\pytest\__main__.py", line 9, in + raise SystemExit(pytest.console_main()) + ^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\config\__init__.py", line 201, in console_main + code = main() + ^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\config\__init__.py", line 156, in main + config = _prepareconfig(args, plugins) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\config\__init__.py", line 341, in _prepareconfig + config = pluginmanager.hook.pytest_cmdline_parse( + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\pluggy\_hooks.py", line 513, in __call__ + return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\pluggy\_manager.py", line 120, in _hookexec + return self._inner_hookexec(hook_name, methods, kwargs, firstresult) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\pluggy\_callers.py", line 139, in _multicall + raise exception.with_traceback(exception.__traceback__) + File "C:\dev\be-lcca\venv\Lib\site-packages\pluggy\_callers.py", line 122, in _multicall + teardown.throw(exception) # type: ignore[union-attr] + ^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\helpconfig.py", line 105, in pytest_cmdline_parse + config = yield + ^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\pluggy\_callers.py", line 103, in _multicall + res = hook_impl.function(*args) + ^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\config\__init__.py", line 1140, in pytest_cmdline_parse + self.parse(args) + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\config\__init__.py", line 1494, in parse + self._preparse(args, addopts=addopts) + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\config\__init__.py", line 1398, in _preparse + self.hook.pytest_load_initial_conftests( + File "C:\dev\be-lcca\venv\Lib\site-packages\pluggy\_hooks.py", line 513, in __call__ + return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\pluggy\_manager.py", line 120, in _hookexec + return self._inner_hookexec(hook_name, methods, kwargs, firstresult) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\pluggy\_callers.py", line 139, in _multicall + raise exception.with_traceback(exception.__traceback__) + File "C:\dev\be-lcca\venv\Lib\site-packages\pluggy\_callers.py", line 122, in _multicall + teardown.throw(exception) # type: ignore[union-attr] + ^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\warnings.py", line 151, in pytest_load_initial_conftests + return (yield) + ^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\pluggy\_callers.py", line 122, in _multicall + teardown.throw(exception) # type: ignore[union-attr] + ^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\capture.py", line 154, in pytest_load_initial_conftests + yield + File "C:\dev\be-lcca\venv\Lib\site-packages\pluggy\_callers.py", line 103, in _multicall + res = hook_impl.function(*args) + ^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\config\__init__.py", line 1222, in pytest_load_initial_conftests + self.pluginmanager._set_initial_conftests( + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\config\__init__.py", line 581, in _set_initial_conftests + self._try_load_conftest( + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\config\__init__.py", line 619, in _try_load_conftest + self._loadconftestmodules( + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\config\__init__.py", line 659, in _loadconftestmodules + mod = self._importconftest( + ^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\config\__init__.py", line 710, in _importconftest + mod = import_path( + ^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\pathlib.py", line 587, in import_path + importlib.import_module(module_name) + File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.11_3.11.2544.0_x64__qbz5n2kfra8p0\Lib\importlib\__init__.py", line 126, in import_module + return _bootstrap._gcd_import(name[level:], package, level) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "", line 1204, in _gcd_import + File "", line 1176, in _find_and_load + File "", line 1147, in _find_and_load_unlocked + File "", line 690, in _load_unlocked + File "C:\dev\be-lcca\venv\Lib\site-packages\_pytest\assertion\rewrite.py", line 184, in exec_module + exec(co, module.__dict__) + File "C:\dev\be-lcca\tests\conftest.py", line 22, in + from src.main import app + File "C:\dev\be-lcca\src\main.py", line 33, in + from src.api import api_router + File "C:\dev\be-lcca\src\api.py", line 18, in + from src.acquisition_cost.router import router as acquisition_data_router + File "C:\dev\be-lcca\src\acquisition_cost\router.py", line 6, in + from src.acquisition_cost.schema import AcquisitionCostDataPagination, AcquisitionCostDataRead, AcquisitionCostDataCreate, AcquisitionCostDataUpdate, ListQueryParams + File "C:\dev\be-lcca\src\acquisition_cost\schema.py", line 20, in + class AcquisitionCostDataCreate(AcquisitionCostDataBase): + File "C:\dev\be-lcca\venv\Lib\site-packages\pydantic\_internal\_model_construction.py", line 224, in __new__ + complete_model_class( + File "C:\dev\be-lcca\venv\Lib\site-packages\pydantic\_internal\_model_construction.py", line 602, in complete_model_class + schema = cls.__get_pydantic_core_schema__(cls, handler) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\pydantic\main.py", line 702, in __get_pydantic_core_schema__ + return handler(source) + ^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\pydantic\_internal\_schema_generation_shared.py", line 84, in __call__ + schema = self._handler(source_type) + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\pydantic\_internal\_generate_schema.py", line 610, in generate_schema + schema = self._generate_schema_inner(obj) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\pydantic\_internal\_generate_schema.py", line 879, in _generate_schema_inner + return self._model_schema(obj) + ^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\pydantic\_internal\_generate_schema.py", line 691, in _model_schema + {k: self._generate_md_field_schema(k, v, decorators) for k, v in fields.items()}, + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\pydantic\_internal\_generate_schema.py", line 691, in + {k: self._generate_md_field_schema(k, v, decorators) for k, v in fields.items()}, + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\pydantic\_internal\_generate_schema.py", line 1071, in _generate_md_field_schema + common_field = self._common_field_schema(name, field_info, decorators) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\pydantic\_internal\_generate_schema.py", line 1263, in _common_field_schema + schema = self._apply_annotations( + ^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\pydantic\_internal\_generate_schema.py", line 2056, in _apply_annotations + schema = get_inner_schema(source_type) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\pydantic\_internal\_schema_generation_shared.py", line 84, in __call__ + schema = self._handler(source_type) + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\pydantic\_internal\_generate_schema.py", line 2040, in inner_handler + metadata_js_function = _extract_get_pydantic_json_schema(obj, schema) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\pydantic\_internal\_generate_schema.py", line 2403, in _extract_get_pydantic_json_schema + return _extract_get_pydantic_json_schema(tp.__origin__, schema) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\dev\be-lcca\venv\Lib\site-packages\pydantic\_internal\_generate_schema.py", line 2402, in _extract_get_pydantic_json_schema + if hasattr(tp, '__origin__') and not _typing_extra.is_annotated(tp): + ^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.11_3.11.2544.0_x64__qbz5n2kfra8p0\Lib\typing.py", line 470, in __getattr__ + def __getattr__(self, item): + +KeyboardInterrupt diff --git a/tests/conftest.py b/tests/conftest.py index 5ce6ed2..c6eb1a2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,18 +1,44 @@ +import os + +# Set dummy environment variables for testing +os.environ["DATABASE_HOSTNAME"] = "localhost" +os.environ["DATABASE_CREDENTIAL_USER"] = "test" +os.environ["DATABASE_CREDENTIAL_PASSWORD"] = "test" +os.environ["COLLECTOR_CREDENTIAL_USER"] = "test" +os.environ["COLLECTOR_CREDENTIAL_PASSWORD"] = "test" +os.environ["DEV_USERNAME"] = "test" +os.environ["DEV_PASSWORD"] = "test" + import asyncio from typing import AsyncGenerator, Generator import pytest -from httpx import AsyncClient +import pytest_asyncio +from httpx import AsyncClient, ASGITransport from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine from sqlalchemy.orm import sessionmaker from sqlalchemy.pool import StaticPool +from fastapi import Request -import pytest -from sqlalchemy_utils import drop_database, database_exists -from starlette.config import environ -from starlette.testclient import TestClient +from src.main import app +from src.database.core import Base, get_db, get_collector_db +from src.auth.service import JWTBearer +from src.auth.model import UserBase -# from src.database import Base, get_db -# from src.main import app +# Import all models to register them with Base +import src.acquisition_cost.model +import src.equipment.model +import src.equipment_master.model +import src.manpower_cost.model +import src.manpower_master.model +import src.masterdata.model +import src.masterdata_simulations.model +import src.plant_fs_transaction_data.model +import src.plant_masterdata.model +import src.plant_transaction_data.model +import src.plant_transaction_data_simulations.model +import src.simulations.model +import src.uploaded_file.model +import src.yeardata.model # Test database URL TEST_DATABASE_URL = "sqlite+aiosqlite:///:memory:" @@ -23,7 +49,7 @@ engine = create_async_engine( poolclass=StaticPool, ) -async_session = sessionmaker( +TestingSessionLocal = sessionmaker( engine, class_=AsyncSession, expire_on_commit=False, @@ -31,39 +57,39 @@ async_session = sessionmaker( autoflush=False, ) - -async def override_get_db() -> AsyncGenerator[AsyncSession, None]: - async with async_session() as session: - try: - yield session - await session.commit() - except Exception: - await session.rollback() - raise - finally: - await session.close() - - -app.dependency_overrides[get_db] = override_get_db - - @pytest.fixture(scope="session") -def event_loop() -> Generator: - loop = asyncio.get_event_loop_policy().new_event_loop() +def event_loop(): + try: + loop = asyncio.get_running_loop() + except RuntimeError: + loop = asyncio.new_event_loop() yield loop - loop.close() - + # loop.close() # Avoid closing if it might be shared -@pytest.fixture(autouse=True) -async def setup_db() -> AsyncGenerator[None, None]: +@pytest_asyncio.fixture(autouse=True) +async def setup_db(): async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) yield async with engine.begin() as conn: await conn.run_sync(Base.metadata.drop_all) +async def override_get_db(request: Request = None): + async with TestingSessionLocal() as session: + yield session + +app.dependency_overrides[get_db] = override_get_db +app.dependency_overrides[get_collector_db] = override_get_db + +@pytest.fixture(autouse=True) +def mock_auth(monkeypatch): + async def mock_call(self, request: Request): + user = UserBase(user_id="test-id", name="test-user", role="admin") + request.state.user = user + return user + monkeypatch.setattr(JWTBearer, "__call__", mock_call) -@pytest.fixture +@pytest_asyncio.fixture async def client() -> AsyncGenerator[AsyncClient, None]: - async with AsyncClient(app=app, base_url="http://test") as client: + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: yield client \ No newline at end of file diff --git a/tests/database.py b/tests/database.py deleted file mode 100644 index 89b84ae..0000000 --- a/tests/database.py +++ /dev/null @@ -1,3 +0,0 @@ -from sqlalchemy.orm import scoped_session, sessionmaker - -Session = scoped_session(sessionmaker()) diff --git a/tests/e2e/test_acquisition_cost.py b/tests/e2e/test_acquisition_cost.py new file mode 100644 index 0000000..e748bbd --- /dev/null +++ b/tests/e2e/test_acquisition_cost.py @@ -0,0 +1,23 @@ +import pytest +from httpx import AsyncClient + +@pytest.mark.asyncio +async def test_get_acquisition_costs(client: AsyncClient): + response = await client.get("/acquisition-data") + assert response.status_code == 200 + assert response.json()["message"] == "Data retrieved successfully" + +@pytest.mark.asyncio +async def test_create_acquisition_cost(client: AsyncClient): + payload = { + "assetnum": "TEST-ASSET", + "acquisition_cost": 1000.0, + "acquisition_year": 2024, + "residual_value": 100.0, + "useful_life": 10 + } + response = await client.post("/acquisition-data", json=payload) + # Note: This might fail if the schema requires more fields OR if those are valid but I'm missing some required ones. + # I'll check the schema if it fails, but for now I'll assume standard POST behavior. + assert response.status_code == 200 + assert response.json()["message"] == "Data created successfully" diff --git a/tests/e2e/test_equipment.py b/tests/e2e/test_equipment.py new file mode 100644 index 0000000..53eede2 --- /dev/null +++ b/tests/e2e/test_equipment.py @@ -0,0 +1,26 @@ +import pytest +from httpx import AsyncClient + +@pytest.mark.asyncio +async def test_get_equipments(client: AsyncClient): + response = await client.get("/equipment") + assert response.status_code == 200 + assert response.json()["message"] == "Data retrieved successfully" + +@pytest.mark.asyncio +async def test_get_top_10_replacement_priorities(client: AsyncClient): + response = await client.get("/equipment/top-10-replacement-priorities") + assert response.status_code == 200 + assert response.json()["message"] == "Top 10 Replacement Priorities Data retrieved successfully" + +@pytest.mark.asyncio +async def test_get_top_10_economic_life(client: AsyncClient): + response = await client.get("/equipment/top-10-economic-life") + assert response.status_code == 200 + assert response.json()["message"] == "Top 10 Economic Life Data retrieved successfully" + +@pytest.mark.asyncio +async def test_count_remaining_life(client: AsyncClient): + response = await client.get("/equipment/count-remaining-life") + assert response.status_code == 200 + assert response.json()["message"] == "Count remaining life retrieved successfully" diff --git a/tests/e2e/test_equipment_master.py b/tests/e2e/test_equipment_master.py new file mode 100644 index 0000000..a75f10f --- /dev/null +++ b/tests/e2e/test_equipment_master.py @@ -0,0 +1,8 @@ +import pytest +from httpx import AsyncClient + +@pytest.mark.asyncio +async def test_get_equipment_masters(client: AsyncClient): + response = await client.get("/equipment-master") + assert response.status_code == 200 + assert response.json()["message"] == "Data retrieved successfully" diff --git a/tests/e2e/test_healthcheck.py b/tests/e2e/test_healthcheck.py new file mode 100644 index 0000000..0908cd7 --- /dev/null +++ b/tests/e2e/test_healthcheck.py @@ -0,0 +1,8 @@ +import pytest +from httpx import AsyncClient + +@pytest.mark.asyncio +async def test_healthcheck(client: AsyncClient): + response = await client.get("/healthcheck") + assert response.status_code == 200 + assert response.json() == {"status": "ok"} diff --git a/tests/e2e/test_masterdata.py b/tests/e2e/test_masterdata.py new file mode 100644 index 0000000..aa4c215 --- /dev/null +++ b/tests/e2e/test_masterdata.py @@ -0,0 +1,97 @@ +import pytest +from httpx import AsyncClient +import uuid + +@pytest.mark.asyncio +async def test_create_masterdata(client: AsyncClient): + payload = { + "name": "Test Master Data", + "description": "Test Description", + "unit_of_measurement": "unit", + "value_num": 100.0, + "value_str": "100", + "seq": 1 + } + response = await client.post("/masterdata", json=payload) + assert response.status_code == 200 + data = response.json() + assert data["message"] == "Data created successfully" + assert data["data"]["name"] == "Test Master Data" + assert "id" in data["data"] + return data["data"]["id"] + +@pytest.mark.asyncio +async def test_get_masterdatas(client: AsyncClient): + # First create one + await client.post("/masterdata", json={ + "name": "Data 1", + "description": "Desc 1", + "unit_of_measurement": "u", + "value_num": 1.0, + "seq": 1 + }) + + response = await client.get("/masterdata") + assert response.status_code == 200 + data = response.json() + assert data["message"] == "Data retrieved successfully" + assert len(data["data"]["items"]) >= 1 + +@pytest.mark.asyncio +async def test_get_masterdata_by_id(client: AsyncClient): + # Create one + create_resp = await client.post("/masterdata", json={ + "name": "Data By ID", + "description": "Desc", + "unit_of_measurement": "u", + "value_num": 2.0, + "seq": 2 + }) + masterdata_id = create_resp.json()["data"]["id"] + + response = await client.get(f"/masterdata/{masterdata_id}") + assert response.status_code == 200 + assert response.json()["data"]["name"] == "Data By ID" + +@pytest.mark.asyncio +async def test_update_masterdata(client: AsyncClient): + # Create one + create_resp = await client.post("/masterdata", json={ + "name": "Old Name", + "description": "Desc", + "unit_of_measurement": "u", + "value_num": 3.0, + "seq": 3 + }) + masterdata_id = create_resp.json()["data"]["id"] + + # Update it + update_payload = { + "name": "New Name", + "value_num": 4.0 + } + response = await client.post(f"/masterdata/update/{masterdata_id}", json=update_payload) + assert response.status_code == 200 + assert response.json()["data"]["name"] == "New Name" + assert response.json()["data"]["value_num"] == 4.0 + +@pytest.mark.asyncio +async def test_delete_masterdata(client: AsyncClient): + # Create one + create_resp = await client.post("/masterdata", json={ + "name": "To Be Deleted", + "description": "Desc", + "unit_of_measurement": "u", + "value_num": 5.0, + "seq": 5 + }) + masterdata_id = create_resp.json()["data"]["id"] + + # Delete it + response = await client.post(f"/masterdata/delete/{masterdata_id}") + assert response.status_code == 200 + assert response.json()["message"] == "Data deleted successfully" + + # Verify it's gone + get_resp = await client.get(f"/masterdata/{masterdata_id}") + assert get_resp.status_code == 404 diff --git a/tests/e2e/test_masterdata_simulations.py b/tests/e2e/test_masterdata_simulations.py new file mode 100644 index 0000000..3037264 --- /dev/null +++ b/tests/e2e/test_masterdata_simulations.py @@ -0,0 +1,8 @@ +import pytest +from httpx import AsyncClient + +@pytest.mark.asyncio +async def test_get_masterdata_simulations(client: AsyncClient): + response = await client.get("/masterdata-simulations") + assert response.status_code == 200 + assert response.json()["message"] == "Data retrieved successfully" diff --git a/tests/e2e/test_plant_fs_transaction.py b/tests/e2e/test_plant_fs_transaction.py new file mode 100644 index 0000000..dbdc94e --- /dev/null +++ b/tests/e2e/test_plant_fs_transaction.py @@ -0,0 +1,16 @@ +import pytest +from httpx import AsyncClient + +@pytest.mark.asyncio +async def test_list_fs_transactions(client: AsyncClient): + response = await client.get("/plant-fs-transaction-data") + assert response.status_code == 200 + assert response.json()["message"] == "Data retrieved successfully" + +@pytest.mark.asyncio +async def test_get_fs_charts(client: AsyncClient): + response = await client.get("/plant-fs-transaction-data/charts") + if response.status_code == 200: + assert "items" in response.json()["data"] + else: + assert response.status_code == 404 diff --git a/tests/e2e/test_plant_masterdata.py b/tests/e2e/test_plant_masterdata.py new file mode 100644 index 0000000..ea5f9a4 --- /dev/null +++ b/tests/e2e/test_plant_masterdata.py @@ -0,0 +1,20 @@ +import pytest +from httpx import AsyncClient + +@pytest.mark.asyncio +async def test_get_plant_masterdatas(client: AsyncClient): + response = await client.get("/plant-masterdata") + assert response.status_code == 200 + assert response.json()["message"] == "Data retrieved successfully" + +@pytest.mark.asyncio +async def test_create_plant_masterdata(client: AsyncClient): + payload = { + "name": "Plant Parameter", + "description": "Plant Desc", + "unit_of_measurement": "unit", + "value_num": 10.5 + } + response = await client.post("/plant-masterdata", json=payload) + assert response.status_code == 200 + assert response.json()["message"] == "Data created successfully" diff --git a/tests/e2e/test_plant_transaction.py b/tests/e2e/test_plant_transaction.py new file mode 100644 index 0000000..5518e4a --- /dev/null +++ b/tests/e2e/test_plant_transaction.py @@ -0,0 +1,18 @@ +import pytest +from httpx import AsyncClient + +@pytest.mark.asyncio +async def test_get_plant_transactions(client: AsyncClient): + response = await client.get("/plant-transaction-data") + assert response.status_code == 200 + assert response.json()["message"] == "Data retrieved successfully" + +@pytest.mark.asyncio +async def test_get_plant_charts(client: AsyncClient): + # This might return 404 if no data exists, but with my setup_db it should be empty + response = await client.get("/plant-transaction-data/charts") + # Actually, the service might raise 404 if it's empty + if response.status_code == 200: + assert "items" in response.json()["data"] + else: + assert response.status_code == 404 diff --git a/tests/e2e/test_simulation.py b/tests/e2e/test_simulation.py new file mode 100644 index 0000000..e44ba9f --- /dev/null +++ b/tests/e2e/test_simulation.py @@ -0,0 +1,19 @@ +import pytest +from httpx import AsyncClient + +@pytest.mark.asyncio +async def test_get_simulations(client: AsyncClient): + response = await client.get("/simulations") + assert response.status_code == 200 + assert response.json()["message"] == "Data retrieved successfully" + +@pytest.mark.asyncio +async def test_create_simulation(client: AsyncClient): + payload = { + "label": "Test Simulation", + "description": "Test Desc", + "version": 1 + } + response = await client.post("/simulations", json=payload) + assert response.status_code == 200 + assert response.json()["data"]["label"] == "Test Simulation" diff --git a/tests/e2e/test_yeardata.py b/tests/e2e/test_yeardata.py new file mode 100644 index 0000000..b187bdd --- /dev/null +++ b/tests/e2e/test_yeardata.py @@ -0,0 +1,18 @@ +import pytest +from httpx import AsyncClient + +@pytest.mark.asyncio +async def test_get_yeardatas(client: AsyncClient): + response = await client.get("/yeardata") + assert response.status_code == 200 + assert response.json()["message"] == "Data retrieved successfully" + +@pytest.mark.asyncio +async def test_create_yeardata(client: AsyncClient): + payload = { + "year": 2024, + "description": "Test Year Data" + } + response = await client.post("/yeardata", json=payload) + assert response.status_code == 200 + assert response.json()["message"] == "Data created successfully" diff --git a/tests/factories.py b/tests/factories.py deleted file mode 100644 index 52ccd3d..0000000 --- a/tests/factories.py +++ /dev/null @@ -1,33 +0,0 @@ -import uuid -from datetime import datetime - -from factory import ( - LazyAttribute, - LazyFunction, - Sequence, - SubFactory, - post_generation, - SelfAttribute, -) -from factory.alchemy import SQLAlchemyModelFactory -from factory.fuzzy import FuzzyChoice, FuzzyDateTime, FuzzyInteger, FuzzyText -from faker import Faker -from faker.providers import misc -# from pytz import UTC - - -from .database import Session - -fake = Faker() -fake.add_provider(misc) - - -class BaseFactory(SQLAlchemyModelFactory): - """Base Factory.""" - - class Meta: - """Factory configuration.""" - - abstract = True - sqlalchemy_session = Session - sqlalchemy_session_persistence = "commit" diff --git a/tests/unit/test_masterdata_logic.py b/tests/unit/test_masterdata_logic.py new file mode 100644 index 0000000..fbb1da8 --- /dev/null +++ b/tests/unit/test_masterdata_logic.py @@ -0,0 +1,24 @@ +import pytest +from src.masterdata.service import calculate_pmt + +def test_calculate_pmt_zero_rate(): + # PMT = -PV / nper when rate is 0 + pv = 1000 + nper = 10 + rate = 0 + result = calculate_pmt(rate, nper, pv) + assert result == -100 + +def test_calculate_pmt_standard(): + # Example: Loan 1000, 5% rate, 2 periods + # PMT = -1000 * (0.05 * (1.05)^2) / ((1.05)^2 - 1) + # PMT = -1000 * (0.05 * 1.1025) / (0.1025) + # PMT = -1000 * (0.055125) / (0.1025) = -537.8048... + result = calculate_pmt(5, 2, 1000) + assert round(result, 2) == -537.80 + +def test_calculate_pmt_percentage(): + # If rate > 1, it divides by 100 + result_5 = calculate_pmt(5, 10, 1000) + result_05 = calculate_pmt(0.05, 10, 1000) + assert result_5 == result_05 diff --git a/tests/unit/test_masterdata_service.py b/tests/unit/test_masterdata_service.py new file mode 100644 index 0000000..b549f26 --- /dev/null +++ b/tests/unit/test_masterdata_service.py @@ -0,0 +1,37 @@ +import pytest +from unittest.mock import AsyncMock, MagicMock +from src.masterdata.service import create, get +from src.masterdata.schema import MasterDataCreate + +@pytest.mark.asyncio +async def test_create_masterdata_service(): + mock_db = AsyncMock() + masterdata_in = MasterDataCreate( + name="Test", + description="Desc", + unit_of_measurement="unit", + value_num=10.0, + seq=1 + ) + + result = await create(db_session=mock_db, masterdata_in=masterdata_in) + + assert result.name == "Test" + mock_db.add.assert_called_once() + mock_db.commit.assert_called_once() + +@pytest.mark.asyncio +async def test_get_masterdata_service(): + mock_db = AsyncMock() + mock_result = MagicMock() + mock_masterdata = MagicMock() + mock_masterdata.id = "test-id" + + # Mock behavior of db_session.execute().scalars().one_or_none() + mock_result.scalars.return_value.one_or_none.return_value = mock_masterdata + mock_db.execute.return_value = mock_result + + result = await get(db_session=mock_db, masterdata_id="test-id") + + assert result.id == "test-id" + mock_db.execute.assert_called_once() From 8d9ce1ae909bb1870a75447832625d142f60d635 Mon Sep 17 00:00:00 2001 From: Cizz22 Date: Mon, 23 Feb 2026 15:46:13 +0700 Subject: [PATCH 14/16] feat: Improve logging with user context and error IDs, enhance request tracing, and strengthen security middleware with RCE/path traversal detection and pagination validation. --- src/auth/service.py | 13 ++++- src/config.py | 2 +- src/context.py | 34 ++++++++++++ src/exceptions.py | 122 ++++++++++++++++++++++++-------------------- src/logging.py | 45 ++++++++++------ src/main.py | 70 ++++++++++++++++++++++--- src/middleware.py | 60 +++++++++++++++++++--- 7 files changed, 261 insertions(+), 85 deletions(-) diff --git a/src/auth/service.py b/src/auth/service.py index dc49e59..2d06f00 100644 --- a/src/auth/service.py +++ b/src/auth/service.py @@ -28,6 +28,17 @@ class JWTBearer(HTTPBearer): ) request.state.user = user_info + + from src.context import set_user_id, set_username, set_role + if hasattr(user_info, "user_id"): + set_user_id(str(user_info.user_id)) + if hasattr(user_info, "username"): + set_username(user_info.username) + elif hasattr(user_info, "name"): + set_username(user_info.name) + if hasattr(user_info, "role"): + set_role(user_info.role) + return user_info else: raise HTTPException(status_code=403, detail="Invalid authorization code.") @@ -46,7 +57,7 @@ class JWTBearer(HTTPBearer): return UserBase(**user_data["data"]) except Exception as e: - print(f"Token verification error: {str(e)}") + logging.error(f"Token verification error: {str(e)}") return None diff --git a/src/config.py b/src/config.py index 03ab027..b92f09f 100644 --- a/src/config.py +++ b/src/config.py @@ -51,7 +51,7 @@ def get_config(): config = get_config() -LOG_LEVEL = config("LOG_LEVEL", default=logging.WARNING) +LOG_LEVEL = config("LOG_LEVEL", default="INFO") ENV = config("ENV", default="local") PORT = config("PORT", cast=int, default=8000) HOST = config("HOST", default="localhost") diff --git a/src/context.py b/src/context.py index 4c968a2..47e0e62 100644 --- a/src/context.py +++ b/src/context.py @@ -2,8 +2,18 @@ from contextvars import ContextVar from typing import Optional, Final REQUEST_ID_CTX_KEY: Final[str] = "request_id" +USER_ID_CTX_KEY: Final[str] = "user_id" +USERNAME_CTX_KEY: Final[str] = "username" +ROLE_CTX_KEY: Final[str] = "role" + _request_id_ctx_var: ContextVar[Optional[str]] = ContextVar( REQUEST_ID_CTX_KEY, default=None) +_user_id_ctx_var: ContextVar[Optional[str]] = ContextVar( + USER_ID_CTX_KEY, default=None) +_username_ctx_var: ContextVar[Optional[str]] = ContextVar( + USERNAME_CTX_KEY, default=None) +_role_ctx_var: ContextVar[Optional[str]] = ContextVar( + ROLE_CTX_KEY, default=None) def get_request_id() -> Optional[str]: @@ -16,3 +26,27 @@ def set_request_id(request_id: str): def reset_request_id(token): _request_id_ctx_var.reset(token) + + +def get_user_id() -> Optional[str]: + return _user_id_ctx_var.get() + + +def set_user_id(user_id: str): + return _user_id_ctx_var.set(user_id) + + +def get_username() -> Optional[str]: + return _username_ctx_var.get() + + +def set_username(username: str): + return _username_ctx_var.set(username) + + +def get_role() -> Optional[str]: + return _role_ctx_var.get() + + +def set_role(role: str): + return _role_ctx_var.set(role) diff --git a/src/exceptions.py b/src/exceptions.py index bab7e8d..18377cb 100644 --- a/src/exceptions.py +++ b/src/exceptions.py @@ -96,58 +96,86 @@ def handle_exception(request: Request, exc: Exception): """ Global exception handler for Fastapi application. """ + import uuid + error_id = str(uuid.uuid1()) request_info = get_request_context(request) + + # Store error_id in request.state for middleware/logging + request.state.error_id = error_id if isinstance(exc, RateLimitExceeded): - return _rate_limit_exceeded_handler(request, exc) + logging.warning( + f"Rate limit exceeded | Error ID: {error_id}", + extra={ + "error_id": error_id, + "error_category": "rate_limit", + "request": request_info, + "detail": str(exc.description) if hasattr(exc, "description") else str(exc), + }, + ) + return JSONResponse( + status_code=429, + content={ + "data": None, + "message": "Rate limit exceeded", + "status": ResponseStatus.ERROR, + "error_id": error_id + } + ) if isinstance(exc, RequestValidationError): - logging.error( - f"Validation error | Error: {str(exc.errors())} | Request: {request_info}", - extra={"error_category": "validation"}, + logging.warning( + f"Validation error occurred | Error ID: {error_id}", + extra={ + "error_id": error_id, + "error_category": "validation", + "errors": exc.errors(), + "request": request_info, + }, ) return JSONResponse( status_code=422, content={ - "data": None, - "message": "Validation error", + "data": exc.errors(), + "message": "Validation Error", "status": ResponseStatus.ERROR, - "errors": [ - ErrorDetail( - field=".".join(map(str, err["loc"])), - message=err["msg"], - code=err["type"], - ).model_dump() - for err in exc.errors() - ] - } + "error_id": error_id + }, ) if isinstance(exc, HTTPException): logging.error( - f"HTTP exception | Code: {exc.status_code} | Error: {exc.detail} | Request: {request_info}", - extra={"error_category": "http"}, + f"HTTP exception occurred | Error ID: {error_id}", + extra={ + "error_id": error_id, + "error_category": "http", + "status_code": exc.status_code, + "detail": exc.detail if hasattr(exc, "detail") else str(exc), + "request": request_info, + }, ) return JSONResponse( status_code=exc.status_code, content={ "data": None, - "message": str(exc.detail), + "message": str(exc.detail) if hasattr(exc, "detail") else str(exc), "status": ResponseStatus.ERROR, - "errors": [ - ErrorDetail( - message=str(exc.detail) - ).model_dump() - ] - } + "error_id": error_id + }, ) if isinstance(exc, SQLAlchemyError): error_message, status_code = handle_sqlalchemy_error(exc) logging.error( - f"Database Error | Error: {str(error_message)} | Request: {request_info}", - extra={"error_category": "database"}, + f"Database error occurred | Error ID: {error_id}", + extra={ + "error_id": error_id, + "error_category": "database", + "error_message": error_message, + "request": request_info, + "exception": str(exc), + }, ) return JSONResponse( @@ -156,42 +184,28 @@ def handle_exception(request: Request, exc: Exception): "data": None, "message": error_message, "status": ResponseStatus.ERROR, - "errors": [ - ErrorDetail( - message=error_message - ).model_dump() - ] - } + "error_id": error_id + }, ) # Log unexpected errors - error_message = f"{exc.__class__.__name__}: {str(exc)}" - error_traceback = exc.__traceback__ - - # Get file and line info if available - if error_traceback: - tb = error_traceback - while tb.tb_next: - tb = tb.tb_next - file_name = tb.tb_frame.f_code.co_filename - line_num = tb.tb_lineno - error_message = f"{error_message}\nFile {file_name}, line {line_num}" - logging.error( - f"Unexpected Error | Error: {error_message} | Request: {request_info}", - extra={"error_category": "unexpected"}, + f"Unexpected error occurred | Error ID: {error_id}", + extra={ + "error_id": error_id, + "error_category": "unexpected", + "error_message": str(exc), + "request": request_info, + }, + exc_info=True, ) - + return JSONResponse( status_code=500, content={ "data": None, - "message": error_message, + "message": "An unexpected error occurred", "status": ResponseStatus.ERROR, - "errors": [ - ErrorDetail( - message=error_message - ).model_dump() - ] - } + "error_id": error_id + }, ) diff --git a/src/logging.py b/src/logging.py index 2207241..b9fc41c 100644 --- a/src/logging.py +++ b/src/logging.py @@ -35,29 +35,45 @@ class JSONFormatter(logging.Formatter): Custom formatter to output logs in JSON format. """ def format(self, record): - from src.context import get_request_id - + from src.context import get_request_id, get_user_id, get_username, get_role request_id = None + user_id = None + username = None + role = None + try: request_id = get_request_id() + user_id = get_user_id() + username = get_username() + role = get_role() except Exception: pass + # Standard fields from requirements log_record = { - "timestamp": datetime.datetime.fromtimestamp(record.created).astimezone().isoformat(), + "timestamp": datetime.datetime.fromtimestamp(record.created).strftime("%Y-%m-%d %H:%M:%S"), "level": record.levelname, + "name": record.name, "message": record.getMessage(), - "logger_name": record.name, - "location": f"{record.module}:{record.funcName}:{record.lineno}", - "module": record.module, - "funcName": record.funcName, - "lineno": record.lineno, - "pid": os.getpid(), - "request_id": request_id or "SYSTEM", # request id assigned per request or SYSTEM for system logs } - + # Add Context information if available + if user_id: + log_record["user_id"] = user_id + if username: + log_record["username"] = username + if role: + log_record["role"] = role + if request_id: + log_record["request_id"] = request_id + + # Add Error context if available + if hasattr(record, "error_id"): + log_record["error_id"] = record.error_id + elif "error_id" in record.__dict__: + log_record["error_id"] = record.error_id + # Capture exception info if available if record.exc_info: log_record["exception"] = self.formatException(record.exc_info) @@ -67,18 +83,17 @@ class JSONFormatter(logging.Formatter): log_record["stack_trace"] = self.formatStack(record.stack_info) # Add any extra attributes passed to the log call - # We skip standard and internal uvicorn/fastapi attributes to avoid duplication or mess standard_attrs = { "args", "asctime", "created", "exc_info", "exc_text", "filename", "funcName", "levelname", "levelno", "lineno", "module", "msecs", "message", "msg", "name", "pathname", "process", "processName", - "relativeCreated", "stack_info", "thread", "threadName", + "relativeCreated", "stack_info", "thread", "threadName", "error_id", "color_message", "request", "scope" } for key, value in record.__dict__.items(): - if key not in standard_attrs: + if key not in standard_attrs and not key.startswith("_"): log_record[key] = value - + log_json = json.dumps(log_record) # Apply color if the output is a terminal diff --git a/src/main.py b/src/main.py index 407f5b0..265deee 100644 --- a/src/main.py +++ b/src/main.py @@ -50,7 +50,7 @@ app.state.limiter = limiter app.add_exception_handler(Exception, handle_exception) app.add_exception_handler(HTTPException, handle_exception) app.add_exception_handler(RequestValidationError, handle_exception) -app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) +app.add_exception_handler(RateLimitExceeded, handle_exception) app.add_exception_handler(SQLAlchemyError, handle_exception) from src.context import set_request_id, reset_request_id, get_request_id @@ -68,18 +68,74 @@ async def db_session_middleware(request: Request, call_next): try: - log.info(f"Incoming request: {request.method} {request.url.path}") + start_time = time.time() session = async_scoped_session(async_session, scopefunc=get_request_id) request.state.db = session() collector_session = async_scoped_session(collector_async_session, scopefunc=get_request_id) request.state.collector_db = collector_session() + response = await call_next(request) - if response.status_code >= 400: - log.error(f"Request completed: {response.status_code}") - else: - log.info(f"Request completed: {response.status_code}") + process_time = (time.time() - start_time) * 1000 + + from src.context import get_username, get_role, get_user_id, set_user_id, set_username, set_role + + # Pull from context or fallback to request.state.user + username = get_username() + role = get_role() + user_id = get_user_id() + + user_obj = getattr(request.state, "user", None) + if user_obj: + # UserBase in this project + u_id = getattr(user_obj, "user_id", None) + u_name = getattr(user_obj, "name", None) or getattr(user_obj, "username", None) + u_role = getattr(user_obj, "role", None) + + if not user_id and u_id: + user_id = str(u_id) + set_user_id(user_id) + if not username and u_name: + username = u_name + set_username(username) + if not role and u_role: + role = u_role + set_role(role) + + user_info_str = "" + if username: + user_info_str = f" | User: {username}" + if role: + user_info_str += f" ({role})" + + log.info( + f"HTTP {request.method} {request.url.path} completed in {round(process_time, 2)}ms{user_info_str}", + extra={ + "method": request.method, + "path": request.url.path, + "status_code": response.status_code, + "duration_ms": round(process_time, 2), + "user_id": user_id, + "role": role, + }, + ) except Exception as e: - log.error(f"Request failed: {type(e).__name__} - {str(e)}") + # Generate an error_id here if it hasn't been generated yet + error_id = getattr(request.state, "error_id", None) + if not error_id: + import uuid + error_id = str(uuid.uuid1()) + request.state.error_id = error_id + + log.error( + f"Request failed | Error ID: {error_id}", + extra={ + "method": request.method, + "path": request.url.path, + "error": str(e), + "error_id": error_id, + }, + exc_info=True, + ) raise e from None finally: await request.state.db.close() diff --git a/src/middleware.py b/src/middleware.py index 3127020..5599a59 100644 --- a/src/middleware.py +++ b/src/middleware.py @@ -18,13 +18,35 @@ MAX_QUERY_PARAMS = 50 MAX_QUERY_LENGTH = 2000 MAX_JSON_BODY_SIZE = 1024 * 100 # 100 KB -# Very targeted patterns. Avoid catastrophic regex nonsense. -XSS_PATTERN_STR = r"( 50: + raise HTTPException(status_code=400, detail=f"Pagination size '{key}' cannot exceed 50") + if size_val % 5 != 0: + raise HTTPException(status_code=400, detail=f"Pagination size '{key}' must be a multiple of 5") + except ValueError: + raise HTTPException(status_code=400, detail=f"Pagination size '{key}' must be an integer") # ------------------------- # 4. Content-Type sanity From 20786afa065ed917d58d1efbe76a14bf88dbb570 Mon Sep 17 00:00:00 2001 From: MrWaradana Date: Mon, 23 Feb 2026 16:23:46 +0700 Subject: [PATCH 15/16] refactor: Update Pydantic schemas by removing redundant `nullable` attributes and add comprehensive test execution documentation. --- docs/test.md | 85 +++++++ poetry.lock | 184 +++++++++++++-- pyproject.toml | 6 + src/__pycache__/__init__.cpython-311.pyc | Bin 139 -> 143 bytes src/__pycache__/api.cpython-311.pyc | Bin 5602 -> 5606 bytes src/__pycache__/config.cpython-311.pyc | Bin 4832 -> 4836 bytes src/__pycache__/enums.cpython-311.pyc | Bin 1061 -> 1065 bytes src/__pycache__/exceptions.cpython-311.pyc | Bin 8455 -> 8459 bytes src/__pycache__/logging.cpython-311.pyc | Bin 4987 -> 5130 bytes src/__pycache__/main.cpython-311.pyc | Bin 5990 -> 5916 bytes src/__pycache__/models.cpython-311.pyc | Bin 7220 -> 6595 bytes src/__pycache__/rate_limiter.cpython-311.pyc | Bin 331 -> 335 bytes src/acquisition_cost/schema.py | 16 +- src/auth/__pycache__/__init__.cpython-311.pyc | Bin 144 -> 148 bytes src/auth/__pycache__/model.cpython-311.pyc | Bin 531 -> 535 bytes src/auth/__pycache__/service.cpython-311.pyc | Bin 3805 -> 3809 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 148 -> 152 bytes src/database/__pycache__/core.cpython-311.pyc | Bin 8684 -> 8688 bytes .../__pycache__/service.cpython-311.pyc | Bin 6534 -> 6565 bytes src/database/core.py | 4 +- .../__pycache__/__init__.cpython-311.pyc | Bin 149 -> 153 bytes .../__pycache__/model.cpython-311.pyc | Bin 10372 -> 10376 bytes .../__pycache__/router.cpython-311.pyc | Bin 16620 -> 16624 bytes .../__pycache__/schema.cpython-311.pyc | Bin 16479 -> 15553 bytes src/equipment/schema.py | 216 +++++++++--------- .../__pycache__/__init__.cpython-311.pyc | Bin 156 -> 160 bytes .../__pycache__/model.cpython-311.pyc | Bin 2593 -> 2597 bytes .../__pycache__/router.cpython-311.pyc | Bin 2432 -> 2436 bytes .../__pycache__/schema.cpython-311.pyc | Bin 3751 -> 3601 bytes .../__pycache__/service.cpython-311.pyc | Bin 2914 -> 2918 bytes src/equipment_master/schema.py | 22 +- src/manpower_cost/schema.py | 16 +- src/manpower_master/schema.py | 16 +- .../__pycache__/__init__.cpython-311.pyc | Bin 150 -> 154 bytes .../__pycache__/model.cpython-311.pyc | Bin 1082 -> 1086 bytes .../__pycache__/router.cpython-311.pyc | Bin 6436 -> 6440 bytes .../__pycache__/schema.cpython-311.pyc | Bin 4216 -> 4034 bytes .../__pycache__/service.cpython-311.pyc | Bin 14448 -> 14452 bytes src/masterdata/schema.py | 32 +-- src/masterdata_simulations/schema.py | 18 +- src/models.py | 23 +- .../__pycache__/config.cpython-311.pyc | Bin 2412 -> 2416 bytes .../equipment/__pycache__/Eac.cpython-311.pyc | Bin 15661 -> 15665 bytes .../__pycache__/Prediksi.cpython-311.pyc | Bin 52567 -> 52571 bytes src/plant_fs_transaction_data/schema.py | 52 ++--- .../__pycache__/__init__.cpython-311.pyc | Bin 156 -> 160 bytes .../__pycache__/model.cpython-311.pyc | Bin 2631 -> 2635 bytes .../__pycache__/router.cpython-311.pyc | Bin 4711 -> 4715 bytes .../__pycache__/schema.cpython-311.pyc | Bin 7960 -> 7676 bytes .../__pycache__/service.cpython-311.pyc | Bin 3699 -> 3703 bytes src/plant_masterdata/schema.py | 120 +++++----- .../__pycache__/__init__.cpython-311.pyc | Bin 162 -> 166 bytes .../__pycache__/model.cpython-311.pyc | Bin 5379 -> 5383 bytes .../__pycache__/router.cpython-311.pyc | Bin 8334 -> 8338 bytes .../__pycache__/schema.cpython-311.pyc | Bin 11848 -> 11353 bytes .../__pycache__/service.cpython-311.pyc | Bin 14708 -> 14712 bytes src/plant_transaction_data/schema.py | 162 ++++++------- .../schema.py | 208 ++++++++--------- src/simulations/schema.py | 26 +-- src/uploaded_file/schema.py | 20 +- .../__pycache__/__init__.cpython-311.pyc | Bin 148 -> 152 bytes .../__pycache__/model.cpython-311.pyc | Bin 1760 -> 1764 bytes .../__pycache__/router.cpython-311.pyc | Bin 4623 -> 4627 bytes .../__pycache__/schema.cpython-311.pyc | Bin 4364 -> 4204 bytes .../__pycache__/service.cpython-311.pyc | Bin 3879 -> 3883 bytes src/yeardata/schema.py | 38 +-- tests/conftest.py | 34 ++- tests/unit/test_masterdata_service.py | 2 + 68 files changed, 772 insertions(+), 528 deletions(-) create mode 100644 docs/test.md diff --git a/docs/test.md b/docs/test.md new file mode 100644 index 0000000..674da1e --- /dev/null +++ b/docs/test.md @@ -0,0 +1,85 @@ +# Panduan Menjalankan Script Testing di BE LCCA Digital Twin + +Proyek ini menggunakan **Pytest** sebagai framework pengujian. Infrastruktur testing terletak di direktori `tests/` dan dikonfigurasi untuk menangani sifat asynchronous dari aplikasi FastAPI serta isolasi database. + +--- + +## **1. Persiapan Lingkungan (Environment Setup)** +Pastikan Anda berada di root direktori proyek dan environment sudah siap. + +### **Opsi A: Menggunakan Virtual Environment (Direkomendasikan)** +Aktifkan `venv` sebelum menjalankan perintah apapun: +```bash +source venv/bin/activate +``` + +### **Opsi B: Menggunakan Poetry** +Jika Anda lebih suka menggunakan Poetry secara langsung tanpa aktivasi manual: +```bash +poetry run pytest +``` + +--- + +## **2. Menjalankan Pengujian** + +| Tujuan | Perintah | +| :--- | :--- | +| **Jalankan Unit Tests** | `pytest tests/unit` | +| **Jalankan E2E Tests** | `pytest tests/e2e` | +| **Jalankan semua test** | `pytest` | +| **Tampilkan statement print** | `pytest -s` | +| **Berhenti di kegagalan pertama** | `pytest -x` | +| **Jalankan file spesifik** | `pytest tests/unit/test_example.py` | + +> **Catatan**: Verbose output (`-v`) sudah aktif secara default di konfigurasi `pyproject.toml`. + +--- + +## **3. Peringatan Penting (Caution for E2E Tests)** + +⚠️ **PENTING**: Saat menjalankan pengujian **End-to-End (E2E)**, pastikan Anda menggunakan **Testing Database**. + +* **JANGAN PERNAH** menjalankan E2E tests menggunakan database **Production** atau **Development**. +* Pengujian E2E seringkali melakukan operasi manipulasi data (create, update, delete) dan pembersihan database secara otomatis yang dapat mengakibatkan **kehilangan data permanen**. +* Selalu gunakan database terpisah (misalnya PostgreSQL instance khusus testing atau SQLite) yang aman untuk dihapus isinya sewaktu-waktu. + +--- + +## **4. Gambaran Infrastruktur Testing** +Direktori `tests/` berisi beberapa utility script yang memudahkan proses testing: + +* **`conftest.py`**: Berisi fixture global. Sudah terkonfigurasi dengan: + * `client`: `AsyncClient` untuk simulasi request API ke aplikasi FastAPI Anda. + * `setup_db`: Secara otomatis membuat dan menghapus database test (SQLite in-memory) untuk setiap sesi pengujian. +* **`factories.py`**: Menggunakan `factory-boy` untuk menghasilkan mock data untuk model Anda. +* **`database.py`**: Mengonfigurasi session database untuk kebutuhan pengujian. + +--- + +## **5. Menulis Test Pertama Anda** +Agar `pytest` mengenali sebuah file sebagai test, file tersebut harus dinamai dengan format `test_*.py` atau `*_test.py`. + +**Contoh (`tests/test_api.py`):** +```python +import pytest + +@pytest.mark.asyncio +async def test_api_status(client): + """Contoh pengujian menggunakan fixture 'client' dari conftest.py""" + response = await client.get("/") + assert response.status_code == 200 +``` + +--- + +## **6. Tips Troubleshooting** +* **Masalah Module Path**: Jika Anda menemui error `ModuleNotFoundError`, jalankan test dengan menambahkan direktori saat ini ke `PYTHONPATH`: + ```bash + export PYTHONPATH=$PYTHONPATH:. + pytest + ``` +* **Menjalankan Test yang Gagal Saja**: Untuk menghemat waktu, jalankan hanya test yang gagal pada sesi sebelumnya: + ```bash + pytest --lf + ``` \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 5415881..75a7063 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,20 @@ -# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. + +[[package]] +name = "aiosqlite" +version = "0.22.1" +description = "asyncio bridge to the standard sqlite3 module" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "aiosqlite-0.22.1-py3-none-any.whl", hash = "sha256:21c002eb13823fad740196c5a2e9d8e62f6243bd9e7e4a1f87fb5e44ecb4fceb"}, + {file = "aiosqlite-0.22.1.tar.gz", hash = "sha256:043e0bd78d32888c0a9ca90fc788b38796843360c855a7262a532813133a0650"}, +] + +[package.extras] +dev = ["attribution (==1.8.0)", "black (==25.11.0)", "build (>=1.2)", "coverage[toml] (==7.10.7)", "flake8 (==7.3.0)", "flake8-bugbear (==24.12.12)", "flit (==3.12.0)", "mypy (==1.19.0)", "ufmt (==2.8.0)", "usort (==1.0.8.post1)"] +docs = ["sphinx (==8.1.3)", "sphinx-mdinclude (==0.6.2)"] [[package]] name = "annotated-types" @@ -6,6 +22,7 @@ version = "0.7.0" description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, @@ -17,6 +34,7 @@ version = "4.8.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a"}, {file = "anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a"}, @@ -30,7 +48,7 @@ typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""] trio = ["trio (>=0.26.1)"] [[package]] @@ -39,6 +57,8 @@ version = "5.0.1" description = "Timeout context manager for asyncio programs" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "python_version == \"3.10\"" files = [ {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, @@ -50,6 +70,7 @@ version = "0.30.0" description = "An asyncio PostgreSQL driver" optional = false python-versions = ">=3.8.0" +groups = ["main"] files = [ {file = "asyncpg-0.30.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bfb4dd5ae0699bad2b233672c8fc5ccbd9ad24b89afded02341786887e37927e"}, {file = "asyncpg-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc1f62c792752a49f88b7e6f774c26077091b44caceb1983509edc18a2222ec0"}, @@ -107,8 +128,21 @@ async-timeout = {version = ">=4.0.3", markers = "python_version < \"3.11.0\""} [package.extras] docs = ["Sphinx (>=8.1.3,<8.2.0)", "sphinx-rtd-theme (>=1.2.2)"] -gssauth = ["gssapi", "sspilib"] -test = ["distro (>=1.9.0,<1.10.0)", "flake8 (>=6.1,<7.0)", "flake8-pyi (>=24.1.0,<24.2.0)", "gssapi", "k5test", "mypy (>=1.8.0,<1.9.0)", "sspilib", "uvloop (>=0.15.3)"] +gssauth = ["gssapi ; platform_system != \"Windows\"", "sspilib ; platform_system == \"Windows\""] +test = ["distro (>=1.9.0,<1.10.0)", "flake8 (>=6.1,<7.0)", "flake8-pyi (>=24.1.0,<24.2.0)", "gssapi ; platform_system == \"Linux\"", "k5test ; platform_system == \"Linux\"", "mypy (>=1.8.0,<1.9.0)", "sspilib ; platform_system == \"Windows\"", "uvloop (>=0.15.3) ; platform_system != \"Windows\" and python_version < \"3.14.0\""] + +[[package]] +name = "backports-asyncio-runner" +version = "1.2.0" +description = "Backport of asyncio.Runner, a context manager that controls event loop life cycle." +optional = false +python-versions = "<3.11,>=3.8" +groups = ["main"] +markers = "python_version == \"3.10\"" +files = [ + {file = "backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5"}, + {file = "backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162"}, +] [[package]] name = "certifi" @@ -116,6 +150,7 @@ version = "2025.1.31" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, @@ -127,6 +162,7 @@ version = "3.4.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, @@ -228,6 +264,7 @@ version = "8.1.8" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, @@ -242,6 +279,8 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main"] +markers = "platform_system == \"Windows\" or sys_platform == \"win32\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -253,6 +292,7 @@ version = "1.3.1" description = "Python library for calculating contours of 2D quadrilateral grids" optional = false python-versions = ">=3.10" +groups = ["main"] files = [ {file = "contourpy-1.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a045f341a77b77e1c5de31e74e966537bba9f3c4099b35bf4c2e3939dd54cdab"}, {file = "contourpy-1.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:500360b77259914f7805af7462e41f9cb7ca92ad38e9f94d6c8641b089338124"}, @@ -326,6 +366,7 @@ version = "0.12.1" description = "Composable style cycles" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30"}, {file = "cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c"}, @@ -341,6 +382,7 @@ version = "1.2.18" description = "Python @deprecated decorator to deprecate old python classes, functions or methods." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +groups = ["main"] files = [ {file = "Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec"}, {file = "deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d"}, @@ -350,7 +392,7 @@ files = [ wrapt = ">=1.10,<2" [package.extras] -dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "setuptools", "tox"] +dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "setuptools ; python_version >= \"3.12\"", "tox"] [[package]] name = "dnspython" @@ -358,6 +400,7 @@ version = "2.7.0" description = "DNS toolkit" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86"}, {file = "dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1"}, @@ -378,6 +421,7 @@ version = "2.2.0" description = "A robust email address syntax and deliverability validation library." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631"}, {file = "email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7"}, @@ -393,6 +437,7 @@ version = "2.0.0" description = "An implementation of lxml.xmlfile for the standard library" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa"}, {file = "et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54"}, @@ -404,6 +449,8 @@ version = "1.2.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" +groups = ["main"] +markers = "python_version == \"3.10\"" files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, @@ -418,6 +465,7 @@ version = "3.3.1" description = "A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "factory_boy-3.3.1-py2.py3-none-any.whl", hash = "sha256:7b1113c49736e1e9995bc2a18f4dbf2c52cf0f841103517010b1d825712ce3ca"}, {file = "factory_boy-3.3.1.tar.gz", hash = "sha256:8317aa5289cdfc45f9cae570feb07a6177316c82e34d14df3c2e1f22f26abef0"}, @@ -436,6 +484,7 @@ version = "30.10.0" description = "Faker is a Python package that generates fake data for you." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "Faker-30.10.0-py3-none-any.whl", hash = "sha256:5f05ee92ddf0e1736d95dca41b2a16ee06d987b736fa4ddecdb047abf2e9024b"}, {file = "faker-30.10.0.tar.gz", hash = "sha256:c2e627d3becec67f7a45400d3670018b5abb3f0728b7dfaa06c135b7df1ce3fb"}, @@ -451,6 +500,7 @@ version = "0.115.8" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "fastapi-0.115.8-py3-none-any.whl", hash = "sha256:753a96dd7e036b34eeef8babdfcfe3f28ff79648f86551eb36bfc1b0bf4a8cbf"}, {file = "fastapi-0.115.8.tar.gz", hash = "sha256:0ce9111231720190473e222cdf0f07f7206ad7e53ea02beb1d2dc36e2f0741e9"}, @@ -477,6 +527,7 @@ version = "0.0.7" description = "Run and manage FastAPI apps from the command line with FastAPI CLI. 🚀" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "fastapi_cli-0.0.7-py3-none-any.whl", hash = "sha256:d549368ff584b2804336c61f192d86ddea080c11255f375959627911944804f4"}, {file = "fastapi_cli-0.0.7.tar.gz", hash = "sha256:02b3b65956f526412515907a0793c9094abd4bfb5457b389f645b0ea6ba3605e"}, @@ -496,6 +547,7 @@ version = "4.56.0" description = "Tools to manipulate font files" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "fonttools-4.56.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:331954d002dbf5e704c7f3756028e21db07097c19722569983ba4d74df014000"}, {file = "fonttools-4.56.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8d1613abd5af2f93c05867b3a3759a56e8bf97eb79b1da76b2bc10892f96ff16"}, @@ -550,18 +602,18 @@ files = [ ] [package.extras] -all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "pycairo", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.1.0)", "xattr", "zopfli (>=0.1.4)"] +all = ["brotli (>=1.0.1) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\"", "fs (>=2.2.0,<3)", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres ; platform_python_implementation == \"PyPy\"", "pycairo", "scipy ; platform_python_implementation != \"PyPy\"", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.1.0) ; python_version <= \"3.12\"", "xattr ; sys_platform == \"darwin\"", "zopfli (>=0.1.4)"] graphite = ["lz4 (>=1.7.4.2)"] -interpolatable = ["munkres", "pycairo", "scipy"] +interpolatable = ["munkres ; platform_python_implementation == \"PyPy\"", "pycairo", "scipy ; platform_python_implementation != \"PyPy\""] lxml = ["lxml (>=4.0)"] pathops = ["skia-pathops (>=0.5.0)"] plot = ["matplotlib"] repacker = ["uharfbuzz (>=0.23.0)"] symfont = ["sympy"] -type1 = ["xattr"] +type1 = ["xattr ; sys_platform == \"darwin\""] ufo = ["fs (>=2.2.0,<3)"] -unicode = ["unicodedata2 (>=15.1.0)"] -woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] +unicode = ["unicodedata2 (>=15.1.0) ; python_version <= \"3.12\""] +woff = ["brotli (>=1.0.1) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\"", "zopfli (>=0.1.4)"] [[package]] name = "greenlet" @@ -569,6 +621,8 @@ version = "3.1.1" description = "Lightweight in-process concurrent programming" optional = false python-versions = ">=3.7" +groups = ["main"] +markers = "python_version < \"3.14\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")" files = [ {file = "greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563"}, {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83"}, @@ -655,6 +709,7 @@ version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, @@ -666,6 +721,7 @@ version = "1.0.7" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"}, {file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"}, @@ -687,6 +743,7 @@ version = "0.6.4" description = "A collection of framework independent HTTP protocol utils." optional = false python-versions = ">=3.8.0" +groups = ["main"] files = [ {file = "httptools-0.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3c73ce323711a6ffb0d247dcd5a550b8babf0f757e86a52558fe5b86d6fefcc0"}, {file = "httptools-0.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:345c288418f0944a6fe67be8e6afa9262b18c7626c3ef3c28adc5eabc06a68da"}, @@ -742,6 +799,7 @@ version = "0.27.2" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"}, {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"}, @@ -755,7 +813,7 @@ idna = "*" sniffio = "*" [package.extras] -brotli = ["brotli", "brotlicffi"] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] @@ -767,6 +825,7 @@ version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -781,6 +840,7 @@ version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, @@ -792,6 +852,7 @@ version = "3.1.5" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"}, {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, @@ -809,6 +870,7 @@ version = "1.4.2" description = "Lightweight pipelining with Python functions" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "joblib-1.4.2-py3-none-any.whl", hash = "sha256:06d478d5674cbc267e7496a410ee875abd68e4340feff4490bcb7afb88060ae6"}, {file = "joblib-1.4.2.tar.gz", hash = "sha256:2382c5816b2636fbd20a09e0f4e9dad4736765fdfb7dca582943b9c1366b3f0e"}, @@ -820,6 +882,7 @@ version = "1.4.8" description = "A fast implementation of the Cassowary constraint solver" optional = false python-versions = ">=3.10" +groups = ["main"] files = [ {file = "kiwisolver-1.4.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88c6f252f6816a73b1f8c904f7bbe02fd67c09a69f7cb8a0eecdbf5ce78e63db"}, {file = "kiwisolver-1.4.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c72941acb7b67138f35b879bbe85be0f6c6a70cab78fe3ef6db9c024d9223e5b"}, @@ -909,6 +972,7 @@ version = "4.0.1" description = "Rate limiting utilities" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "limits-4.0.1-py3-none-any.whl", hash = "sha256:67667e669f570cf7be4e2c2bc52f763b3f93bdf66ea945584360bc1a3f251901"}, {file = "limits-4.0.1.tar.gz", hash = "sha256:a54f5c058dfc965319ae3ee78faf222294659e371b46d22cd7456761f7e46d5a"}, @@ -920,9 +984,9 @@ packaging = ">=21,<25" typing-extensions = "*" [package.extras] -all = ["aetcd", "coredis (>=3.4.0,<5)", "emcache (>=0.6.1)", "emcache (>=1)", "etcd3", "motor (>=3,<4)", "pymemcache (>3,<5.0.0)", "pymongo (>4.1,<5)", "redis (>3,!=4.5.2,!=4.5.3,<6.0.0)", "redis (>=4.2.0,!=4.5.2,!=4.5.3)"] +all = ["aetcd", "coredis (>=3.4.0,<5)", "emcache (>=0.6.1) ; python_version < \"3.11\"", "emcache (>=1) ; python_version >= \"3.11\" and python_version < \"3.13.0\"", "etcd3", "motor (>=3,<4)", "pymemcache (>3,<5.0.0)", "pymongo (>4.1,<5)", "redis (>3,!=4.5.2,!=4.5.3,<6.0.0)", "redis (>=4.2.0,!=4.5.2,!=4.5.3)"] async-etcd = ["aetcd"] -async-memcached = ["emcache (>=0.6.1)", "emcache (>=1)"] +async-memcached = ["emcache (>=0.6.1) ; python_version < \"3.11\"", "emcache (>=1) ; python_version >= \"3.11\" and python_version < \"3.13.0\""] async-mongodb = ["motor (>=3,<4)"] async-redis = ["coredis (>=3.4.0,<5)"] etcd = ["etcd3"] @@ -937,6 +1001,7 @@ version = "3.0.0" description = "Python port of markdown-it. Markdown parsing, done right!" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, @@ -961,6 +1026,7 @@ version = "3.0.2" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, @@ -1031,6 +1097,7 @@ version = "3.10.0" description = "Python plotting package" optional = false python-versions = ">=3.10" +groups = ["main"] files = [ {file = "matplotlib-3.10.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2c5829a5a1dd5a71f0e31e6e8bb449bc0ee9dbfb05ad28fc0c6b55101b3a4be6"}, {file = "matplotlib-3.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2a43cbefe22d653ab34bb55d42384ed30f611bcbdea1f8d7f431011a2e1c62e"}, @@ -1088,6 +1155,7 @@ version = "0.1.2" description = "Markdown URL utilities" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, @@ -1099,6 +1167,7 @@ version = "2.2.3" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.10" +groups = ["main"] files = [ {file = "numpy-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cbc6472e01952d3d1b2772b720428f8b90e2deea8344e854df22b0618e9cce71"}, {file = "numpy-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdfe0c22692a30cd830c0755746473ae66c4a8f2e7bd508b35fb3b6a0813d787"}, @@ -1163,6 +1232,7 @@ version = "1.0.0" description = "Simple financial functions" optional = false python-versions = ">=3.5" +groups = ["main"] files = [ {file = "numpy-financial-1.0.0.tar.gz", hash = "sha256:f84341bc62b2485d5604a73d5fac7e91975b4b9cd5f4a5a9cf608902ea00cb40"}, {file = "numpy_financial-1.0.0-py3-none-any.whl", hash = "sha256:bae534b357516f12258862d1f0181d911032d0467f215bfcd1c264b4da579047"}, @@ -1177,6 +1247,7 @@ version = "3.1.5" description = "A Python library to read/write Excel 2010 xlsx/xlsm files" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2"}, {file = "openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050"}, @@ -1191,6 +1262,7 @@ version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, @@ -1202,6 +1274,7 @@ version = "2.2.3" description = "Powerful data structures for data analysis, time series, and statistics" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5"}, {file = "pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348"}, @@ -1288,6 +1361,7 @@ version = "1.0.1" description = "A Python package for describing statistical models and for building design matrices." optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "patsy-1.0.1-py2.py3-none-any.whl", hash = "sha256:751fb38f9e97e62312e921a1954b81e1bb2bcda4f5eeabaf94db251ee791509c"}, {file = "patsy-1.0.1.tar.gz", hash = "sha256:e786a9391eec818c054e359b737bbce692f051aee4c661f4141cc88fb459c0c4"}, @@ -1305,6 +1379,7 @@ version = "11.1.0" description = "Python Imaging Library (Fork)" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "pillow-11.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:e1abe69aca89514737465752b4bcaf8016de61b3be1397a8fc260ba33321b3a8"}, {file = "pillow-11.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c640e5a06869c75994624551f45e5506e4256562ead981cce820d5ab39ae2192"}, @@ -1384,7 +1459,7 @@ docs = ["furo", "olefile", "sphinx (>=8.1)", "sphinx-copybutton", "sphinx-inline fpx = ["olefile"] mic = ["olefile"] tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "trove-classifiers (>=2024.10.12)"] -typing = ["typing-extensions"] +typing = ["typing-extensions ; python_version < \"3.10\""] xmp = ["defusedxml"] [[package]] @@ -1393,6 +1468,7 @@ version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -1408,6 +1484,7 @@ version = "2.9.10" description = "psycopg2 - Python-PostgreSQL Database Adapter" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2"}, {file = "psycopg2_binary-2.9.10-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:0ea8e3d0ae83564f2fc554955d327fa081d065c8ca5cc6d2abb643e2c9c1200f"}, @@ -1456,6 +1533,7 @@ files = [ {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909"}, {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1"}, {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567"}, + {file = "psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142"}, {file = "psycopg2_binary-2.9.10-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:eb09aa7f9cecb45027683bb55aebaaf45a0df8bf6de68801a6afdc7947bb09d4"}, {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b73d6d7f0ccdad7bc43e6d34273f70d587ef62f824d7261c4ae9b8b1b6af90e8"}, {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce5ab4bf46a211a8e924d307c1b1fcda82368586a19d0a24f8ae166f5c784864"}, @@ -1484,6 +1562,7 @@ version = "2.10.6" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584"}, {file = "pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236"}, @@ -1496,7 +1575,7 @@ typing-extensions = ">=4.12.2" [package.extras] email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] [[package]] name = "pydantic-core" @@ -1504,6 +1583,7 @@ version = "2.27.2" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"}, {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"}, @@ -1616,6 +1696,7 @@ version = "2.19.1" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, @@ -1630,6 +1711,7 @@ version = "3.2.1" description = "pyparsing module - Classes and methods to define and execute parsing grammars" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "pyparsing-3.2.1-py3-none-any.whl", hash = "sha256:506ff4f4386c4cec0590ec19e6302d3aedb992fdc02c761e90416f158dacf8e1"}, {file = "pyparsing-3.2.1.tar.gz", hash = "sha256:61980854fd66de3a90028d679a954d5f2623e83144b5afe5ee86f43d762e5f0a"}, @@ -1644,6 +1726,7 @@ version = "8.3.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, @@ -1660,12 +1743,34 @@ tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5"}, + {file = "pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5"}, +] + +[package.dependencies] +backports-asyncio-runner = {version = ">=1.1,<2", markers = "python_version < \"3.11\""} +pytest = ">=8.2,<10" +typing-extensions = {version = ">=4.12", markers = "python_version < \"3.13\""} + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] + [[package]] name = "python-dateutil" version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -1680,6 +1785,7 @@ version = "1.0.1" description = "Read key-value pairs from a .env file and set them as environment variables" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, @@ -1694,6 +1800,7 @@ version = "0.0.20" description = "A streaming multipart parser for Python" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104"}, {file = "python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13"}, @@ -1705,6 +1812,7 @@ version = "2024.2" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"}, {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, @@ -1716,6 +1824,7 @@ version = "6.0.2" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, @@ -1778,6 +1887,7 @@ version = "2.32.3" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, @@ -1799,6 +1909,7 @@ version = "13.9.4" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.8.0" +groups = ["main"] files = [ {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, @@ -1818,6 +1929,7 @@ version = "0.13.2" description = "Rich toolkit for building command-line applications" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "rich_toolkit-0.13.2-py3-none-any.whl", hash = "sha256:f3f6c583e5283298a2f7dbd3c65aca18b7f818ad96174113ab5bec0b0e35ed61"}, {file = "rich_toolkit-0.13.2.tar.gz", hash = "sha256:fea92557530de7c28f121cbed572ad93d9e0ddc60c3ca643f1b831f2f56b95d3"}, @@ -1834,6 +1946,7 @@ version = "1.6.1" description = "A set of python modules for machine learning and data mining" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "scikit_learn-1.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d056391530ccd1e501056160e3c9673b4da4805eb67eb2bdf4e983e1f9c9204e"}, {file = "scikit_learn-1.6.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:0c8d036eb937dbb568c6242fa598d551d88fb4399c0344d95c001980ec1c7d36"}, @@ -1888,6 +2001,7 @@ version = "1.15.1" description = "Fundamental algorithms for scientific computing in Python" optional = false python-versions = ">=3.10" +groups = ["main"] files = [ {file = "scipy-1.15.1-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:c64ded12dcab08afff9e805a67ff4480f5e69993310e093434b10e85dc9d43e1"}, {file = "scipy-1.15.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:5b190b935e7db569960b48840e5bef71dc513314cc4e79a1b7d14664f57fd4ff"}, @@ -1937,7 +2051,7 @@ numpy = ">=1.23.5,<2.5" [package.extras] dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy (==1.10.0)", "pycodestyle", "pydevtool", "rich-click", "ruff (>=0.0.292)", "types-psutil", "typing_extensions"] doc = ["intersphinx_registry", "jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.16.5)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0,<8.0.0)", "sphinx-copybutton", "sphinx-design (>=0.4.0)"] -test = ["Cython", "array-api-strict (>=2.0,<2.1.1)", "asv", "gmpy2", "hypothesis (>=6.30)", "meson", "mpmath", "ninja", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] +test = ["Cython", "array-api-strict (>=2.0,<2.1.1)", "asv", "gmpy2", "hypothesis (>=6.30)", "meson", "mpmath", "ninja ; sys_platform != \"emscripten\"", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] [[package]] name = "shellingham" @@ -1945,6 +2059,7 @@ version = "1.5.4" description = "Tool to Detect Surrounding Shell" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, @@ -1956,6 +2071,7 @@ version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, @@ -1967,6 +2083,7 @@ version = "0.1.9" description = "A rate limiting extension for Starlette and Fastapi" optional = false python-versions = ">=3.7,<4.0" +groups = ["main"] files = [ {file = "slowapi-0.1.9-py3-none-any.whl", hash = "sha256:cfad116cfb84ad9d763ee155c1e5c5cbf00b0d47399a769b227865f5df576e36"}, {file = "slowapi-0.1.9.tar.gz", hash = "sha256:639192d0f1ca01b1c6d95bf6c71d794c3a9ee189855337b4821f7f457dddad77"}, @@ -1984,6 +2101,7 @@ version = "1.3.1" description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, @@ -1995,6 +2113,7 @@ version = "2.0.37" description = "Database Abstraction Library" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "SQLAlchemy-2.0.37-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da36c3b0e891808a7542c5c89f224520b9a16c7f5e4d6a1156955605e54aef0e"}, {file = "SQLAlchemy-2.0.37-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e7402ff96e2b073a98ef6d6142796426d705addd27b9d26c3b32dbaa06d7d069"}, @@ -2090,6 +2209,7 @@ version = "0.13.0" description = "A library to filter SQLAlchemy queries." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "sqlalchemy-filters-0.13.0.tar.gz", hash = "sha256:40f2daead93c4db2409cf5e5abf67a420179f9e5c1df5c15fa1b474f6533b105"}, {file = "sqlalchemy_filters-0.13.0-py3-none-any.whl", hash = "sha256:aa4595b90d152eb76fa312a3e03d5d675f0c2e16762751f340f5449468689d9a"}, @@ -2110,6 +2230,7 @@ version = "0.41.2" description = "Various utility functions for SQLAlchemy." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "SQLAlchemy-Utils-0.41.2.tar.gz", hash = "sha256:bc599c8c3b3319e53ce6c5c3c471120bd325d0071fb6f38a10e924e3d07b9990"}, {file = "SQLAlchemy_Utils-0.41.2-py3-none-any.whl", hash = "sha256:85cf3842da2bf060760f955f8467b87983fb2e30f1764fd0e24a48307dc8ec6e"}, @@ -2127,8 +2248,8 @@ intervals = ["intervals (>=0.7.1)"] password = ["passlib (>=1.6,<2.0)"] pendulum = ["pendulum (>=2.0.5)"] phone = ["phonenumbers (>=5.9.2)"] -test = ["Jinja2 (>=2.3)", "Pygments (>=1.2)", "backports.zoneinfo", "docutils (>=0.10)", "flake8 (>=2.4.0)", "flexmock (>=0.9.7)", "isort (>=4.2.2)", "pg8000 (>=1.12.4)", "psycopg (>=3.1.8)", "psycopg2 (>=2.5.1)", "psycopg2cffi (>=2.8.1)", "pymysql", "pyodbc", "pytest (==7.4.4)", "python-dateutil (>=2.6)", "pytz (>=2014.2)"] -test-all = ["Babel (>=1.3)", "Jinja2 (>=2.3)", "Pygments (>=1.2)", "arrow (>=0.3.4)", "backports.zoneinfo", "colour (>=0.0.4)", "cryptography (>=0.6)", "docutils (>=0.10)", "flake8 (>=2.4.0)", "flexmock (>=0.9.7)", "furl (>=0.4.1)", "intervals (>=0.7.1)", "isort (>=4.2.2)", "passlib (>=1.6,<2.0)", "pendulum (>=2.0.5)", "pg8000 (>=1.12.4)", "phonenumbers (>=5.9.2)", "psycopg (>=3.1.8)", "psycopg2 (>=2.5.1)", "psycopg2cffi (>=2.8.1)", "pymysql", "pyodbc", "pytest (==7.4.4)", "python-dateutil", "python-dateutil (>=2.6)", "pytz (>=2014.2)"] +test = ["Jinja2 (>=2.3)", "Pygments (>=1.2)", "backports.zoneinfo ; python_version < \"3.9\"", "docutils (>=0.10)", "flake8 (>=2.4.0)", "flexmock (>=0.9.7)", "isort (>=4.2.2)", "pg8000 (>=1.12.4)", "psycopg (>=3.1.8)", "psycopg2 (>=2.5.1)", "psycopg2cffi (>=2.8.1)", "pymysql", "pyodbc", "pytest (==7.4.4)", "python-dateutil (>=2.6)", "pytz (>=2014.2)"] +test-all = ["Babel (>=1.3)", "Jinja2 (>=2.3)", "Pygments (>=1.2)", "arrow (>=0.3.4)", "backports.zoneinfo ; python_version < \"3.9\"", "colour (>=0.0.4)", "cryptography (>=0.6)", "docutils (>=0.10)", "flake8 (>=2.4.0)", "flexmock (>=0.9.7)", "furl (>=0.4.1)", "intervals (>=0.7.1)", "isort (>=4.2.2)", "passlib (>=1.6,<2.0)", "pendulum (>=2.0.5)", "pg8000 (>=1.12.4)", "phonenumbers (>=5.9.2)", "psycopg (>=3.1.8)", "psycopg2 (>=2.5.1)", "psycopg2cffi (>=2.8.1)", "pymysql", "pyodbc", "pytest (==7.4.4)", "python-dateutil", "python-dateutil (>=2.6)", "pytz (>=2014.2)"] timezone = ["python-dateutil"] url = ["furl (>=0.4.1)"] @@ -2138,6 +2259,7 @@ version = "0.45.3" description = "The little ASGI library that shines." optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "starlette-0.45.3-py3-none-any.whl", hash = "sha256:dfb6d332576f136ec740296c7e8bb8c8a7125044e7c6da30744718880cdd059d"}, {file = "starlette-0.45.3.tar.gz", hash = "sha256:2cbcba2a75806f8a41c722141486f37c28e30a0921c5f6fe4346cb0dcee1302f"}, @@ -2155,6 +2277,7 @@ version = "0.14.4" description = "Statistical computations and models for Python" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "statsmodels-0.14.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7a62f1fc9086e4b7ee789a6f66b3c0fc82dd8de1edda1522d30901a0aa45e42b"}, {file = "statsmodels-0.14.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:46ac7ddefac0c9b7b607eed1d47d11e26fe92a1bc1f4d9af48aeed4e21e87981"}, @@ -2197,7 +2320,7 @@ scipy = ">=1.8,<1.9.2 || >1.9.2" [package.extras] build = ["cython (>=3.0.10)"] -develop = ["colorama", "cython (>=3.0.10)", "cython (>=3.0.10,<4)", "flake8", "isort", "joblib", "matplotlib (>=3)", "pytest (>=7.3.0,<8)", "pytest-cov", "pytest-randomly", "pytest-xdist", "pywinpty", "setuptools-scm[toml] (>=8.0,<9.0)"] +develop = ["colorama", "cython (>=3.0.10)", "cython (>=3.0.10,<4)", "flake8", "isort", "joblib", "matplotlib (>=3)", "pytest (>=7.3.0,<8)", "pytest-cov", "pytest-randomly", "pytest-xdist", "pywinpty ; os_name == \"nt\"", "setuptools-scm[toml] (>=8.0,<9.0)"] docs = ["ipykernel", "jupyter-client", "matplotlib", "nbconvert", "nbformat", "numpydoc", "pandas-datareader", "sphinx"] [[package]] @@ -2206,6 +2329,7 @@ version = "3.5.0" description = "threadpoolctl" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "threadpoolctl-3.5.0-py3-none-any.whl", hash = "sha256:56c1e26c150397e58c4926da8eeee87533b1e32bef131bd4bf6a2f45f3185467"}, {file = "threadpoolctl-3.5.0.tar.gz", hash = "sha256:082433502dd922bf738de0d8bcc4fdcbf0979ff44c42bd40f5af8a282f6fa107"}, @@ -2217,6 +2341,8 @@ version = "2.2.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "python_version == \"3.10\"" files = [ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, @@ -2258,6 +2384,7 @@ version = "0.15.1" description = "Typer, build great CLIs. Easy to code. Based on Python type hints." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "typer-0.15.1-py3-none-any.whl", hash = "sha256:7994fb7b8155b64d3402518560648446072864beefd44aa2dc36972a5972e847"}, {file = "typer-0.15.1.tar.gz", hash = "sha256:a0588c0a7fa68a1978a069818657778f86abe6ff5ea6abf472f940a08bfe4f0a"}, @@ -2275,6 +2402,7 @@ version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, @@ -2286,6 +2414,7 @@ version = "2025.1" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" +groups = ["main"] files = [ {file = "tzdata-2025.1-py2.py3-none-any.whl", hash = "sha256:7e127113816800496f027041c570f50bcd464a020098a3b6b199517772303639"}, {file = "tzdata-2025.1.tar.gz", hash = "sha256:24894909e88cdb28bd1636c6887801df64cb485bd593f2fd83ef29075a81d694"}, @@ -2297,13 +2426,14 @@ version = "2.3.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] @@ -2314,6 +2444,7 @@ version = "0.32.1" description = "The lightning-fast ASGI server." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "uvicorn-0.32.1-py3-none-any.whl", hash = "sha256:82ad92fd58da0d12af7482ecdb5f2470a04c9c9a53ced65b9bbb4a205377602e"}, {file = "uvicorn-0.32.1.tar.gz", hash = "sha256:ee9519c246a72b1c084cea8d3b44ed6026e78a4a309cbedae9c37e4cb9fbb175"}, @@ -2327,12 +2458,12 @@ httptools = {version = ">=0.6.3", optional = true, markers = "extra == \"standar python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} pyyaml = {version = ">=5.1", optional = true, markers = "extra == \"standard\""} typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} -uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "(sys_platform != \"win32\" and sys_platform != \"cygwin\") and platform_python_implementation != \"PyPy\" and extra == \"standard\""} +uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\" and extra == \"standard\""} watchfiles = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} websockets = {version = ">=10.4", optional = true, markers = "extra == \"standard\""} [package.extras] -standard = ["colorama (>=0.4)", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] +standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"] [[package]] name = "uvloop" @@ -2340,6 +2471,8 @@ version = "0.21.0" description = "Fast implementation of asyncio event loop on top of libuv" optional = false python-versions = ">=3.8.0" +groups = ["main"] +markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"" files = [ {file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f"}, {file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d"}, @@ -2391,6 +2524,7 @@ version = "1.0.4" description = "Simple, modern and high performance file watching and code reload in python." optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "watchfiles-1.0.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ba5bb3073d9db37c64520681dd2650f8bd40902d991e7b4cfaeece3e32561d08"}, {file = "watchfiles-1.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f25d0ba0fe2b6d2c921cf587b2bf4c451860086534f40c384329fb96e2044d1"}, @@ -2474,6 +2608,7 @@ version = "14.2" description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "websockets-14.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e8179f95323b9ab1c11723e5d91a89403903f7b001828161b480a7810b334885"}, {file = "websockets-14.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0d8c3e2cdb38f31d8bd7d9d28908005f6fa9def3324edb9bf336d7e4266fd397"}, @@ -2552,6 +2687,7 @@ version = "1.17.2" description = "Module for decorators, wrappers and monkey patching." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "wrapt-1.17.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d57c572081fed831ad2d26fd430d565b76aa277ed1d30ff4d40670b1c0dd984"}, {file = "wrapt-1.17.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5e251054542ae57ac7f3fba5d10bfff615b6c2fb09abeb37d2f1463f841ae22"}, @@ -2635,6 +2771,6 @@ files = [ ] [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "^3.10" -content-hash = "8d70f1df8b24fbd51e128ed36fbf43c4ccfdcd3b7dbd1f0f718870cab0c4d568" +content-hash = "a67faa975147cf6652ac87a3767b499a2c0e344cb4d3f7c47d0526e81fe2bbd0" diff --git a/pyproject.toml b/pyproject.toml index 053e01d..294260e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,12 @@ pandas = "^2.2.3" numpy-financial = "^1.0.0" numpy = "^2.2.3" statsmodels = "^0.14.4" +pytest-asyncio = "^1.3.0" +aiosqlite = "^0.22.1" + +[tool.pytest.ini_options] +asyncio_default_fixture_loop_scope = "function" +addopts = "-v" [build-system] diff --git a/src/__pycache__/__init__.cpython-311.pyc b/src/__pycache__/__init__.cpython-311.pyc index a55b27f4dbeabf2de4a24ecd56e5fd495264d78f..426c2483b7abe0c04ad706bbaffb96541c02573f 100644 GIT binary patch delta 48 zcmeBX>}TXz&dbZi00akfc`_&Rm`Et;XXNLm>L+IA=IbY=>gFUTC+Zg$CF@TNF$VxD Chz-X8 delta 44 ycmeBY>}KRy&dbZi00j2C)@4lOF%gk-wu(tfEsIG?)y+vxPK+roN{*QrVGaNR#0?<; diff --git a/src/__pycache__/api.cpython-311.pyc b/src/__pycache__/api.cpython-311.pyc index 7e81a85ee4cf0c43e16c93ad03996bc91e175123..dc55969c6178fe27322c40735f0f575b97b6f50a 100644 GIT binary patch delta 53 zcmaE){Y;y0IWI340}#aSx6eGnzmabTtAwn6Mt*Lpeqv^BzJ5}wZccJ?qJD8vvi{~< HtQ-OWzlRX> delta 49 zcmaE+{YaZ{IWI340}x0?cxSo@Y~%IWI340}$BnT9;wBkvEu0MB3RZCMC5jCMi`nCpkGWrno3MW^)D8Mn(WZ CwGN;F diff --git a/src/__pycache__/exceptions.cpython-311.pyc b/src/__pycache__/exceptions.cpython-311.pyc index 08447c596d0ffffda77c5f4d15555c697a806468..8b0dca67de4dd6b5c6a4e96f765a8d8115088558 100644 GIT binary patch delta 52 zcmZp7>UQE?&dbZi00ch|_GRjAf-`v35 GE(-vlW)O`4 delta 48 zcmeBnYIovY&dbZi00f2Iy_ugk^2RfZC^%ciq@f-<-g- zl#|Jgc{4lDbf(EseEgHw^4U!GNlMktNls3TDK1Km*_^_(l#}TR z(`Ig-=}ePZ_>CrC;gg)~&mX}kGI<66!pVLDos9mQZwMGNZq^mr!>Fjq=*Reh0XtbF H2DAYZ9C4QG&9pJEK|I&BoacRy^pz zo1nrV6!Fl5#6!K5D7`5Nf;Uqw2sw!bPa=Z1o}5V4x_s#cUzh|Chk}m=Q zFQCzT|Im1{2Y^l13`5!mzhA8;UvtNKfV^-YjKT>RdpX$_0a&3qh*}Y4QSru^VXtO~ z2o;yll1V-k0cZ{t+B{X!90Ui3EKzw`*xn=$cHqN1@ESiJh5$~P=d4n#WR+`Wt5U1< zA34t~NJ1E+kIXZy02ps`)QsjJ;C>Xp#>2#Yu}udn!!f8;=@I|eO#h=~gz z6{SsG*Axfq3VGw77%9`uLg$-*w{TRU5T9mcjuuCtB^Ha}68-kZGze7(=`C!yx79k* zAi@rwr_>d4V6?F=gG#a|uf=I=7%O%awNVLj0cJ!7f zZ`rVAqLbL6E&DglT8D%*-m@!s@A}fza)kusc;uG^IPnXJd;*cBnQkJx4sshH*9AFJ kl4r>=ITBR5sT1oUzX9@HkpD_n<-O#sERhfLQ<1s-0X!JE9smFU delta 793 zcmaJ;O=uHA6rR~lnv$fcY?G7*v}voxTCyQZ6l!dhw%R8B(Ssr(u&g_y-I7hhB+;s% zMZ8H+n2Qt-9wZ_yVhJ9+^(2K}wo*{`R1iEDJP4kA(;6%YK6u}L@6CMkzQ?{)zpJq~ z9UWc-L7vvl1qGpRqG*NZ5Baga7W;t5=@>n4;@r4|sF#kD!{i9*CDGNsh8v+3$owGV5KIbzN@Jvdb)X@^9LW8^h#+#b zFlA+O87rI1TGP2{j9L;Nk-K2V49G+AV+{;S$o(M4>A)?C#Gbes&{%1;QEnF0|5Kd! zM==MAD|<5{!aO95-1xJFbPnR0iw|;%fQzO>V#+&YSfrny{FAnoq#7c3r8CN**S@? zHZL4{AVn@hgPjpDDj){n$a=ZVhWVxvdE^AdGnPqUrMo(#jt_jOMyXh(i&YjE65*O9 zgwFf-9uqG2ra1w#XcTIMYOGYNQuY?HIdLXT{%TKKB~mNU5mtjz{IAL;Y_Th2jCYW4 zscCjylX|+Nc3;vCowAX#+lD-WrQ3Gj>2+men12cl@MO53`@^dGB4H1m+d}DWl(td& nE0Vt;UfZejqr90V zHpG;J2fYFBAO{mnNU8>J#k&cK@!-LD+Km1KCdNSE#G&$_jI+ORA@9+2hzW04^ z9*#dcmiaoJPH^Jx1wa+f9MMj6Ll3Jp%;#S!nR0o+XVGY?8wZd}(_a!p)0lL0V;*82W$H1AVn{%TO8=<1 zp4ms4<&Z>8D=$s_3i*pa#fQnOLM0U>rd(&qT%I;{f9t#u6-EYV%x}4;5c#=WUvX_z z*G${3H_;JG*R{Hb^*UJ#zb#j(#e%})3jHd45H-+}*0OvvGfH^@50fCgnl6Jfgp2fp zYvjGi30@|jMdtV_`7PqeIVuiB%k+HYV5Ji$q+6Z`HOp8grnpE=e-e);YY-s?kS5V+ zZSo)`gOulMzNy>DUvUi;>>6Bfy2?Ec~~RRv

X^2R4F~?Hb5x=&TQW*Y7Pl1qL`r zhj)U)&E2=%U%7R%T^wo;$!&S0U8%J*QoB@noQ@8NPdEzC0B*hmZ>Iqu(4LAz9C7ne zazEQTy>|hFkm>kAw$acW$Auo&2Emy!vOD+_j6}DI}9wjnwQA;xrxb1%FHTa z?+%{=Kjst6@u;hcVhpeUDW1Ny?9+M-|AZt8D>RpdGgj@k&)ii-()j%5f zB7g(127oC9VYoD~4?JXvTL3ozt^+_8L2M1XYY6-6_!E#p@r8d~1PFuxXzO?B2A^bS z0(eCs&EoJhY!Z#JZfFj@V{i=2S)%a2!b#93yla+b=_|-y@7_(I$^c{ma^z8QWP@E4 znHQ#GF1B$W==&+qAXp#kc0XLccnG99kZLV|Wz0kPx~U^v0C60|$Z6SLXMaVvxr%hZ zbE}PmH@hBYfP!wP-FcECYbn+wY|mNgdz;~TY=WM{mW58^2zhj)j3RXR^{b%7 delta 2586 zcmaJ?S!^3c7~WYQaeU{nox@4&G;ZpmghG#?5<=AU;A(&z57~yM&hDgj*tL^cyH$GN zfFe|JNDcE)9(X7`q*OehDt!P6p-M=ok8V||(F#$Xka&TV_Jtx*{(qdKaSChCw=@6$ z&wuRv|35$My!M>`vd`z1;Q4m>!}Q1~8v!L? z1eKuRIrS=|TB#QE3cbdtRceip5)x~d9yTIMM9gh^ol*z7D)oA!L1_>)x87((l_-&9 z>5!y(<|NHan-*-6^c4*8*d(QSe!0HTA|wJJ5nPag52N%bF~O+exeC_-Bp* z(#-#GWMTmzTTVS~=4gg(E9tSRx@lrGG1kJ*Igf(qPtIMQ7*Lo8!OI&f;xD$r)bg6S zR4%O=G=zTAi;7aozR7(KswDXFijj z&Qr^k$?JM*T&MlXO4f;cpGMe$@C<@*CkQjkp3kQ>%YnGdlG9qb(JmD4;zO0gPGPi* ze_eTX@Ohy2Ctd%o+lRW{Dx;|!)yBqWEO&lNTbY-e=tUE~e9=8pGSSPcJp;)nxn1@_ z4&cTE{9{jRLHLGi6~@}Ln!NY%gf2={QivugvPdWb2_=^mfK9UvN=dur*c`y3bg`JY#LGQFdQ(Pnygg7+eIri+nhw*nte`6%Zf*HfPUx0>c{kV_GL2@>ElC| zb0W*e)0((j;9z$J3o{e`>u)V`aszT2_BVyy3mHe1T!4RK-55pg{WlZz6>Rf}=)grl`k@|%hJ zBUbP@&5gk+FnFCz>3PZy!;_nOXIb_-+{@BtxQDa{YmBOOnUeiO zJm#%^v}c!!Py#u|Mf7 zj|;tK*sWL!p{xt2NJALq*ZgfIbt?ba|K-R==h$%+bRuX7LRT7QmJP~)g>Y6eINaBF zU~rI4qC}aY38W!t{BB^NWN2csE4W>bzXjCa90>nBdi02m!VK;X6J9KbFvZ`i9wJlx zPW7j8k=U|jAJ1lW_AW{i%WhJAVl}Dxg_;;S#jn;hinf*vmw%5+R5^_xqInx#!{I_L@+y%>GkL=-osly;K1tPF={gkGQ!nno{4Qm?$S;u<;+(~? z3GXqa^@yOy8Cfl_(;oH-FbE9uBWO&^HbTfRQd3dldm`QZRwT^tMx1;q(#8uB=i>K~ cDze>2pl&w9h2%Y?K3HeP_+NED+VNZc3v|UQ`~Uy| diff --git a/src/__pycache__/rate_limiter.cpython-311.pyc b/src/__pycache__/rate_limiter.cpython-311.pyc index d9d7738dd1592f0b3b03859790c8a44035846eee..75029b1f710337947d585bfe414201a482ed9190 100644 GIT binary patch delta 49 zcmX@jbe@TKIWI340}vd{<;kp^$h${EO+O<)H&s6|GdEv9DOEQoIXO|kxF}hF;@vy| DX9W-t delta 45 zcmX@lbef5GIWI340}$BnT9;8bk#~=XlCxDzN@`h5QmSrFa&lr!aZz&2#D{qRITa6Y diff --git a/src/acquisition_cost/schema.py b/src/acquisition_cost/schema.py index ec995a3..23dbdd8 100644 --- a/src/acquisition_cost/schema.py +++ b/src/acquisition_cost/schema.py @@ -7,14 +7,14 @@ from src.models import CommonParams, DefaultBase, Pagination class AcquisitionCostDataBase(DefaultBase): - category_no: Optional[str] = Field(None, nullable=True) - name: Optional[str] = Field(None, nullable=True) - cost_unit_3_n_4: Optional[float] = Field(None, nullable=True) - cost_unit_3: Optional[float] = 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) - updated_by: Optional[str] = Field(None, nullable=True) + category_no: Optional[str] = Field(None) + name: Optional[str] = Field(None) + cost_unit_3_n_4: Optional[float] = Field(None) + cost_unit_3: Optional[float] = Field(None) + created_at: Optional[datetime] = Field(None) + updated_at: Optional[datetime] = Field(None) + created_by: Optional[str] = Field(None) + updated_by: Optional[str] = Field(None) class AcquisitionCostDataCreate(AcquisitionCostDataBase): diff --git a/src/auth/__pycache__/__init__.cpython-311.pyc b/src/auth/__pycache__/__init__.cpython-311.pyc index 224f452c1b83dc15720bd262e530484613ff9cf6..e42b5d501a8fb9db5018a502a71ae35baf18f743 100644 GIT binary patch delta 53 zcmbQhIE9gCIWI340}vd{<;k4LVDVyrm; DO*;=r diff --git a/src/auth/__pycache__/model.cpython-311.pyc b/src/auth/__pycache__/model.cpython-311.pyc index e8edab19542908b0688046baa3f2f6a7d49b1d06..628300a98685d725385f12350a82f596998e1b83 100644 GIT binary patch delta 56 zcmbQtGM$BYIWI340}vd{<;l$6$ji$ptD>KgpPQk(ZZIQo-3OCMC5jCMi`nCpkGWrno3MCb6_6BWAKW G;{yP9D-cHj diff --git a/src/auth/__pycache__/service.cpython-311.pyc b/src/auth/__pycache__/service.cpython-311.pyc index 9eeaf7f6c26ad4f6e9d93d12beaf9d3ca9adc7bf..daf1c8333770a8e7bec23a46b02343c1f8d32543 100644 GIT binary patch delta 57 zcmcaB`%so|IWI340}wPzX=Fa<*vPkyNmfliBR@A)KQS{mUq2~THzzqcQNOq-SwFF~ LBtw7kT_zp??7$Ks delta 53 zcmaDTdsmikIWI340}!0KWss@Exsh)hlcbWfRZL21Sxi!@ZccJ?VoY&Sa!g`rNk+`# HhfF*Gx@r-V diff --git a/src/database/__pycache__/__init__.cpython-311.pyc b/src/database/__pycache__/__init__.cpython-311.pyc index 4badd8cf6a0fba5f59c388c289357c5970b1acde..72fb57c1819d074cdb374c36908e0aa65964ccf6 100644 GIT binary patch delta 57 zcmbQjID?UAIWI340}vd{<;k4LW1^_7pOK%Ns-Kvdo3Edgs+*IXoTy)1l&qhUSdy5O LSe&XqG1eRazX%an delta 53 zcmbQiIE9gCIWI340}$BnT9+}A$3#}$*(xR_wJatnRW~O&IWeZVC^;r2u_Q4mu{bqm HVxlDIFT=eJC`epi;*FPA%!((W9}SAMy|_jS(X}~lvtdq$+-D0(=e+FT&Jl+ncD0s~HTgTd_rHniDOemN5W1*uBzy2Ph+flq0(hsXy;8Ibf!h9XUnmFhs^7l%!5eoARhs$EgR<`6L> FW&o=#K4<^{ delta 188 zcmZ2#+-A(ToR^o20SF``yfc+VHu6ca%BneA#iXQ`#U!Qb<|HR4#uOJN$D|~dBqk*m zr^amdWOe3ZY}wq)@6X6mWK_d2xq(M~GP9rqJ8KGS4Q~qDWG_K^M%Kw$f(yl2tAIw= zuvRevNfwaY9M)h4P4>x3LP?W5h0ZaCPL2?E<%$E^o(IImeUq09Z`+(M@_|tXB({>F cNE1jEsRM~$95%W6DWy57c18J{i^YtX0a{!!<^TWy diff --git a/src/database/core.py b/src/database/core.py index 8202f9f..a0209e0 100644 --- a/src/database/core.py +++ b/src/database/core.py @@ -2,8 +2,8 @@ from starlette.requests import Request from sqlalchemy_utils import get_mapper from sqlalchemy.sql.expression import true -from sqlalchemy.orm import object_session, sessionmaker, Session -from sqlalchemy.ext.declarative import declarative_base, declared_attr +from sqlalchemy.orm import object_session, sessionmaker, Session, declarative_base +from sqlalchemy.ext.declarative import declared_attr from sqlalchemy import create_engine, inspect from pydantic import BaseModel from fastapi import Depends diff --git a/src/equipment/__pycache__/__init__.cpython-311.pyc b/src/equipment/__pycache__/__init__.cpython-311.pyc index 207a0bf16d6d0f392e7255aca446a2e7e614f11f..419a16b9eea66fb130f3e40122f6129468859461 100644 GIT binary patch delta 58 zcmbQrIFpfQIWI340}vd{<;k4LW1^&^pOK%Ns-Kvdo3Edgs+*IXoTy)1l&qgxSejXo Mo0?amKQYc60MqFb00000 delta 54 zcmbQqIF*rSIWI340}$BnT9+}A$3#xU*(xR_wJatnRW~O&IWeZVC^;szur#wEH#M&$ IW@3^#0Hl=>hX4Qo diff --git a/src/equipment/__pycache__/model.cpython-311.pyc b/src/equipment/__pycache__/model.cpython-311.pyc index 468b9f5ced71b0dad5404a3f46e006a91e93f4d8..b10150a5e8961e44f74b454b614e814b45458810 100644 GIT binary patch delta 63 zcmZn(>nEk^<|HR4>K7L!>!%i$ RW)|e8=9TDgPGyr<0suSi6IB2J delta 59 zcmeAOYzgFB&dbZi00cWfbZ0iGY~-_KlT&rJib+W=i%Ck=%}Gv9j43Wkj!7*n%`C`G N%`1u7oXIAy1OP0H6Py45 diff --git a/src/equipment/__pycache__/router.cpython-311.pyc b/src/equipment/__pycache__/router.cpython-311.pyc index 2b8e1a6a13064e710a837d06ed752cad4213bbce..28b9e73544dc9b732e4e3a80cc9fdc0dd92070f7 100644 GIT binary patch delta 65 zcmaFU$oQd=k#9LKFBbz4cyae+<{NJ0+sLJ)rJs?Xo2s9fnVYYll&YJPoSdj%T$HSz TT3DJ{keixUqQChr7rz+*n9>zl delta 61 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 delta 61 zcmZ3$IERsEIWI340}$BnT9+}A$3#`%*(xR_wJatnRW~O&IWeZVC^;szur#wEH#M&$ PJ~y$rB(*4JVx~C&F;oX6EMWC#CA2KC#31ZV={O&dbZi00h^3D>D5y@_uJl)pfRtNl7h>NlMktNls3TDK1KmNi8hREXYmG TD~ZodEG|hcirK8o63z+$ltmS} diff --git a/src/equipment_master/__pycache__/schema.cpython-311.pyc b/src/equipment_master/__pycache__/schema.cpython-311.pyc index 307c7b6285ab618f05c0d77b874448406b26c289..ef16ea4dff3f48610c94086e8953486974354d5e 100644 GIT binary patch literal 3601 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 diff --git a/src/equipment_master/__pycache__/service.cpython-311.pyc b/src/equipment_master/__pycache__/service.cpython-311.pyc index 6d7dc6c6a8b4af66bb17325233b65c0ff0606d0e..a923155c29bd3079316aae24bc38619ec7dd087d 100644 GIT binary patch delta 69 zcmaDP_Dqa-IWI340}x!|iOV$K$m`9dX{4W#pPQo&G delta 55 zcmbQmIE|5KIWI340}$BnT9+}A$3$M!*(xR_wJatnRW~O&IWeZVC^;rKvA86)C?&BZ JF=k@2IRLhc5!wI% diff --git a/src/masterdata/__pycache__/model.cpython-311.pyc b/src/masterdata/__pycache__/model.cpython-311.pyc index ae67aeb30de10edd73fa669148af84c76dc16daf..892b12a129b5f9502e020012a0ebbb13e4536700 100644 GIT binary patch delta 63 zcmdnRv5$jyIWI340}w=gGtZo{k=Kt&SxY}7KQ~oBF*7$`KPgo=CpkG$zqlw_KR2ujXtOlag8%la#8PlboCwQ(TlBlbcvvl3J9K NSdtjCxsK@oBLMXJ67&E7 diff --git a/src/masterdata/__pycache__/router.cpython-311.pyc b/src/masterdata/__pycache__/router.cpython-311.pyc index 4be777cda6f028ee92146a0172e24aa023d87c44..6f989a245658fd58b3c76e93919b01f4879a5810 100644 GIT binary patch delta 64 zcmZ2tw8DsQIWI340}vc-?98ke+{nkvrmU@>k)NBYpO~4Oub-5vo0FWJs9#)^te=}$ ST#{Opl30?czuAiIl@I`Kz7)Rz delta 60 zcmZ2sw8V&SIWI340}$-|(4EO9w2_aOO diff --git a/src/masterdata/__pycache__/schema.cpython-311.pyc b/src/masterdata/__pycache__/schema.cpython-311.pyc index 1ebe3df49bd98813b37dd5b66cf18d4357e73a21..f6c2f5a02e67f628560d24c98b898bd3cd34f945 100644 GIT binary patch delta 1840 zcmaJ7%Q6yCMR?#BP7HF2_Owp*NyQ|vN-57-Ah zt$M_u?JA_5V8?7?&_2Xow}~M#U}eWc=RF&AvC%5zpBX4Gdp`hH7-Ihfc4UZk+r^-3 zh0PO;+@OEkukbt-NtO4}9Wq(- zL#`S?I0w*ie7aVWDryLcSCO!sO*_BdJ$x5rmQuY(& z(psS)Wy@+ITkLP1Ef-dlqLlgaYt@U@2>4dD@-=vZ`mtYE;KWEX-X^>0R=U2pIitP4 zbx9|22!q7UsYE-}uI!2XV*PzB+>AEP>tq(fAhTvNa}YYH+!r5;IvH(-+tC(?IS7N~ zSfMF+^kAZEw@o__Ie;UM{_cp@cB`#w{rcutP5agzounWPk}}6GG(*kGuGkXm3!7mr zx&_T*5C(~v@l^ZPL2fU%001jPSo)4HEm7iQ-V4@{qs#`WdDVIlZq7Oh4xN+Q9Vv zq!Gmhn+)tP^z7f)-K@#L@>boWc%hz|Ulzc+?oOa*-9@nOf~rUrC8PSFo$BXmfXuQ% zT)%>V=DjhnZPKTTNKYV~M-UKL{RR9tiEz^UY6Q8y^VKO_P9sDSVh9NaYk=6f{42n5 z@0x*-=Fke;u7(So8`a4;gh9s5(Ftw6xwO5}*Z|j_)yWiuL8i>fSaZF-w7b#TK*xfR zgfK{w6_|os55#F#NMG5`H}c@nvQ8op28o#dbDQhh&F`vPRd6R1zTBzzr+4B6{di>I znq>}_3(eq#5H8|A_FB!*e@0evmR9J(4v|+2mEJflty3VYDf+D|>tq8= z(O+C2Wh_0Mn8Y5#Qk_8|L+9OzUUi1b?$72e)vsX^9z$j6r}ddbb&fhcxn7?+`i|!@ z@BhJj@iyMJ<18zZT3AWCPBqLtRyz=w8`uVK;F6iha^xHeIeN$YeQ&EA_4-Ql%rb1u zY#{8Q>`*NIWQ*s*)nZ9rUsK*v-=;tN=!dgi&d*yx%O4_pG>~EfFLt?M(7riD_NXt> K9sgehU*iR2#FK3R literal 4216 zcma)9&2JmW6`v)Sx~$Pz8ewoFPQ6R+Dy0>rfX2qH96QaP=a4@nnRD6!^_Wg<&T z*Vtt^@;~&l3lPC#FD=lMiyr7&C!g}(tmNfa<*a6Z znwj5kcHhjr_jdSgG%6CfzQ6lL`5z%d{*8@`D{x+Uag!tDYoZW^Q%Qvs2#0l_%2j*? zU&UYWv$kL5D}h3Q)p<2o2^B&Wp&+n!Kn+*Kf>?#Rj-h#>Ih43~*N%HwxSp8tHrA7~{r( z8>gW@Zd^%j6Y1(RG+W{vL6Hq=lq*#7IsC87y5R^P9UA3>s;mOb@9y5&04DsNtkb&( z3RRuJ2W6@%&==XD_vM;uAmxZ3%lqZ3j00g`-Q zkb-X)7MA)IPVqhQf8&E?h1NdR%@y%-&3i>SLF(IA_~<@TA8__GghmXjpD)mVui|j zO`{d?tN-%MA^vjb<=Z#NW1kj>rsIS0;fJzXqs3~ia#7U{&GGB>b4M&`6kJ^?%7!D> z4$fI#Uh$hEIMMbSUFH?D%MAe#m{T9>N1)DZ`KkB=-TimcwM|$ zl&jSPgGGa0ENY|3UqQHvqQHIiKsK}q>_}mUzg#sm+(8Y~iFOU)I>HTr$K(YddHLr0 z%AP_W?%ktrsil&o6(xNyL0W+okX9%=<*I!2<*E7vri$68Y zYkBKhzI`oU-#EV2SUh>lP09Pzi8)fw{}nS-(}6*nw48?=ho^UHiTxXotQECH7FLp_MGOc{xrTlBFZ;ZC;)lrWv78cn-@=iU_$N>LlD;umQPXodxIIjuyi; zVgcnv%^3ELBVhb&d66Tcc{BH0KuO05kuAz2Bjosb}aW=&p#V)2qaaidXdZlC7Oi3Mw7p*^tx!82y^ z3s5Y6!A@tvzqg;}&GeF$UTUY8FmQl}|4SCX#D?2Kq@kGUJOoV&gC>PRlfs~>4F=5B z>y6!J{xoYQ=dI*?J2?+B-Q?$?Sp2*lA3v@&cEH-WWW}X+T!LVM*PUDIR@}Mcw0m3= zatFI!;I4SC$x^wmd;4}-KHZZ1{rl0ku(#nk8p4}!>zKB^g|&I1zTp~ziXKD!(1F(r z+#b)6T{tyRzU$i;P&oFj@qvH&o$>c->Svyhxkfr6H~H&ZQn24z9-i*-O1WgXp3`Uj zPl47Dege0Se&ktSCw6qSkv%EEZbGpbLdBgB4jMioO!oZ~>Y2p(2NQ$jQz|QtudHlI zfqu(_@R*@CFCc+154Vm>)nl_TQs1m^{t@o_rfVu{hntGK(rxNKDZuK}K-c;QdrTkq z*4{fFI)So5E3o>Z{yF5~`5JBkvv=rh7Li53TSq7AnaxbL{{C^k5o<@%CZC03F@%ac zHyqV7w^!#;4Vdn^U7Rl!{}aCN{5N@EKyIdfRC$(%W?*yNVkTROowx7s0t z(D==B>^<|StiavwkU?nt{yFxZdDK;o3wOvMpb~n|JWAlW6e|0n`7SC`s63}Y8AWA3 oG=7W9C@Rk>Q07qC56u-+=1_S~31JRAY7iRIbL>4ELNRCi0mo^gO8@`> diff --git a/src/masterdata/__pycache__/service.cpython-311.pyc b/src/masterdata/__pycache__/service.cpython-311.pyc index 0f5f22e0dc22e566318f59707d115d1da5ba09f4..79d928cc1dc42ffdaa62d793eb714cd68bf315c1 100644 GIT binary patch delta 64 zcmexR@TGunIWI340}ynCMy6x_Y*<@ delta 56 zcmew$^hStxIWI340}$-|(4FbKk@ps}jEb{WOiF55Oj4?DPI7W$OmR_iOm2QkX-;Z! K%w~3$OjZCFpc5JZ diff --git a/src/modules/equipment/__pycache__/Eac.cpython-311.pyc b/src/modules/equipment/__pycache__/Eac.cpython-311.pyc index 6b8e5e2095b13574c9cf05763081b2c34c8d508e..337131275f9427471eee3f353aa109abb7b1b23b 100644 GIT binary patch delta 71 zcmZ2mwXuqCIWI340}$-cYRHUn-pJ?Aq@}N)k)NBYpO~4Oub-5vo0FWJs9#)^te=~o ZQks)mte;v~npu#WnpdK~xtVE|6#z0R7*hZM delta 67 zcmdm3wYG|HIWI340}$-|(4G0fWh0+IlbW`(RZL21Sxi!@ZccJ?VoY&Sa!hW1N@-4N VaZGAqX=XugYFua$1CUIWI340}vd{<;mQ!kvEr3(?CBXKQ~oBF*7$`KPgo=CpkG$zqlw_zaS?u XuOvPta$JOWIWI340}$BnT9V diff --git a/src/plant_masterdata/__pycache__/router.cpython-311.pyc b/src/plant_masterdata/__pycache__/router.cpython-311.pyc index 0637c71177b435478ae4891ce2d4f7abf5e70bef..cef3673d16c4a2bedf9bfe438620f58d26e5a773 100644 GIT binary patch delta 69 zcmaE^@>+#=IWI340}!}`c4qQyfFUU#E XD~ZodEG|hcN=Yn9)ZaXX^)e3t-`N)s delta 65 zcmaE@@?3>?IWI340}$-|(4ASZkvEA|RoB@nCMC5jCMi`nCpkGWrno3MrXVLVuOvP< TvA86)C?&BZF=q1&*2_Eq+%OlR diff --git a/src/plant_masterdata/__pycache__/schema.cpython-311.pyc b/src/plant_masterdata/__pycache__/schema.cpython-311.pyc index 49fa0130b7955d7d0c1cf97d604790d4a78e5e7a..af0a54a2964115715e362414552301b7207090ba 100644 GIT binary patch literal 7676 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 literal 7960 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 Mj8T6x2j6ct0Op?&wg3PC delta 71 zcmZqIYS!Xi&dbZi00h^3D>5%^|}55;gz; delta 72 zcmbQ_*yqT%oR^o20SI<}=+0a(xsflHUEA2%DkdehEG8*cHzzqcF{ZdEIi?^dF|Q=P aq$n}3I5D{-Ge0jrC9xziX7eoe9})m;l^Qw# diff --git a/src/plant_transaction_data/__pycache__/schema.cpython-311.pyc b/src/plant_transaction_data/__pycache__/schema.cpython-311.pyc index b3bdb63b8c6ec42ca7374d0c560f2f66c418dffa..37b55b8517ddb673b755f5b7a0810de5e96e13c1 100644 GIT binary patch literal 11353 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 delta 53 zcmbQiIE9gCIWI340}$BnT9+}A$3#}$*(xR_wJatnRW~O&IWeZVC^@DwHL)lqu_Q5O HVxlk!&rQ`&%*@T#PfFFzNls4GFD^>fuS`uW PN=Yn9)Zd)KQo{rQ238VF delta 57 zcmaFD`+%2sIWI340}x0?cxMJ}NlMktNls3TDK1KmsZ32QN=Yn9 LjM<#QQo{rQ&*u@U diff --git a/src/yeardata/__pycache__/router.cpython-311.pyc b/src/yeardata/__pycache__/router.cpython-311.pyc index e17ef971fef8ed51f7540dd8197989c170ec7f90..07b7983ecfe9fbd8b44398bb1126004ff20df722 100644 GIT binary patch delta 61 zcmeBInXJOQoR^o20SH_|J2N9U^187qYU*d?=cei>X6EMWC#CATfP!ozDXR4Vn_r delta 57 zcmbQN(yzk1oR^o20SI<}=+4}+k=Kn?R@K=mCMC5jCMi`nCpkGWrno3MrZP3LC?&BZ LF=lfK>wF#n2xSvo diff --git a/src/yeardata/__pycache__/schema.cpython-311.pyc b/src/yeardata/__pycache__/schema.cpython-311.pyc index f67b454b5342e3c8963f9385b66a1935c5cacdcc..15b320e826aaab1cd6c7c75e7092188cd8de0b36 100644 GIT binary patch delta 1855 zcma)5%}-)i6zAhIqI`*pB8UPlTF2o-MukzUtreV%(l)8np@K2Mdo3mk@d1g=w1uS$ z7bMpO3(`CjVo6F9CesC*ChR)Pdy}_#Ax+aiV7h8oUG-k5&d89qmvetP@BYsDox^?S zo!ehpwclwp0}`^}K>6^C{%6{hRQ9JFej^=K*5p?*iR613{8noI1gw%6nS_-yQbxht zsr6mS2}gEk?I~FW+dqE&C}H~;<$q)hz+a>m)oWUCS!Ny^=$!>qGP6z9o4YQ$r-7$3 zi{UlJ8%7IjvfH%=rSSRS$FL4IPH$J&Z9&k=FV^l_m}@X*iGu&NzJWXMa!Yl>#HS`lFGY;qb$MPe{p~9o0vShf>ArK;lLFj)*SslEBXR0S? z@e#6aW9znH-7Y*l%O{Ju^E3k6#4y+v{qHsu4W@a5LUH6zV1GjJClGj>PgZjk5=Q(4 z1_{yWtv;?VG~P!WPmnW=ongTlMqs?;=L6++1c)ETAS})=)eZG*Q;LESG#|zDQDHuc zfTeU=Rh07x5I-NqAS(J+>$%2mGlq6z$QQ@HxZsN;@D?AgvXuh_h#$uwE_xR0iaOIc zL4iHwiC|Ae@I(-pC?)w^nKVcI2nG>xYp0oev5P)^f}$sAi^E%-u*D&8zaDO|KN30O za~N>q@_NIOZ-N(k^bv!WS-i{&%Pa!ZRb|~!BNt4376VrF`s$7bXzI~c6nXctcVF=C zBQVJ;tA+{*BmO=H`=V*8DfcORlaa?$8hqg)XHPdbR>>+UYxOg%zTZ^mk>i(;(1}q|i%ejS_3{ckMz)XAREi k!mjq8Z8zJl(+;}bz+cMM<1~{yV%NCM=RfEoQb61I8*xSOTmS$7 delta 2015 zcma)7&rcgi6!xy4VH5l#G;shMgX`kZq&NX5!PW}mLP#1_O_iwY6dWsw!)ze13H5p% zC5OiDsT??1og9$DVGoE}4V7G_2Op7o;ndDZt7?VR{smRlsy+2kb;bk~ezjfCe)IOd z?|pu6X2<>|yb$qz>G2$Pu2SPo;w2&2;TmEr z(V;q#fJD@UnpdS2U{S!|3qc$d-JTuZFT^0eD!SWtIQbXxE7O7>ilGByZfuI7(*AiK z)ukTl*c4%?HY3kLWo?l%-aR?q|IG1WaJF{%4&vcHXAnPe?rIs|V%`^fycOpC$$8L9 z9L0Y-4_b+1xF0$@cdqt!TYlaaz60$t5?7C}h-@p^y?JGhvtZkfb!vOPmn}Yi3PFGS zzLBpWh(j)^)Y$GV{HKO-rHZMLCPqN7=h-Oq-m{WefUb_Q@;7EPmdM1s^^mb+@##{0-MV zZ7yfvThh>k-o+ixf}!i(T$&vXhaTqeCWi^zg;=I5F;?m28Cfe(rZSmP&3v9a``5SWN9R8 zMY84hE7GF>nU6r04GP)+%?n3Omx*dhJx9hSNH{}F;fxi|l&{QBSBeXB1Tt(;$k@r@ zx~q=X6>{TalFgH(PLsNo)Cu&M)79cKV=`N(pxd!{HC;Qm*0Vl9uHPfEDH@xyVp9Y< z7L(?sr4#{XPf?h%2L@|?)(KkAlk^N3n56@=)<9{NKxpxC)wNV2!0cHHvvy*rR;(-Q zVR)C|W@{9*k{#$?jG9n2>H@j&5eXgdPivQh}Annz+ZW6I-h+? zfY~DyM(pS*GhIDb?;+7ribhjbG}U;e+N~oI0&L4t6jHB|4(4pVPb4hUuxy3phR)Gv z9s)8O6lD9~DGd%@X;Fnf>~>$i9OFmiG>0=Bb{(KW?&A<=_a8n6uuDYdJ`N4FB!6lgQ5k>j>A)e+;OSjOG;zuRhh0kir%WHhpLtF+H57)J z!gO+1_Pdep0G#PI7XhesNKCQ|U(OVr delta 57 zcmZ22w_J{QIWI340}x#Et;l?_kvET7R?XQeCMC5jCMi`nCpkGWrno3MrZP3LC?&BZ LF=q1|=6-Gf1Zfjf diff --git a/src/yeardata/schema.py b/src/yeardata/schema.py index ee20353..039a4e7 100644 --- a/src/yeardata/schema.py +++ b/src/yeardata/schema.py @@ -7,25 +7,25 @@ from src.models import DefaultBase, Pagination class YeardataBase(DefaultBase): - year: Optional[int] = Field(None, nullable=True, ge=1900) - rp_per_kwh: Optional[float] = Field(None, nullable=True, ge=0, le=1_000_000_000_000_000) - total_lost: Optional[float] = Field(None, nullable=True, ge=0, le=1_000_000_000_000_000) - man_hour: Optional[float] = Field(None, nullable=True, ge=0, le=1_000_000_000_000_000) - asset_crit_ens_energy_not_served: Optional[float] = Field(None, nullable=True, ge=0, le=1_000_000_000_000_000) - asset_crit_bpp_system: Optional[float] = Field(None, nullable=True, ge=0, le=1_000_000_000_000_000) - asset_crit_bpp_pembangkit: Optional[float] = Field(None, nullable=True, ge=0, le=1_000_000_000_000_000) - asset_crit_dmn_daya_mampu_netto: Optional[float] = Field(None, nullable=True, ge=0, le=1_000_000_000_000_000) - asset_crit_marginal_cost: Optional[float] = Field(None, nullable=True, ge=0, le=1_000_000_000_000_000) - asset_crit_efdh_equivalent_forced_derated_hours: Optional[float] = Field(None, nullable=True, ge=0, le=1_000_000_000_000_000) - asset_crit_foh_forced_outage_hours: Optional[float] = Field(None, nullable=True, ge=0, le=1_000_000_000_000_000) - 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) - updated_by: Optional[str] = Field(None, nullable=True) + year: Optional[int] = Field(None, ge=1900) + rp_per_kwh: Optional[float] = Field(None, ge=0, le=1_000_000_000_000_000) + total_lost: Optional[float] = Field(None, ge=0, le=1_000_000_000_000_000) + man_hour: Optional[float] = Field(None, ge=0, le=1_000_000_000_000_000) + asset_crit_ens_energy_not_served: Optional[float] = Field(None, ge=0, le=1_000_000_000_000_000) + asset_crit_bpp_system: Optional[float] = Field(None, ge=0, le=1_000_000_000_000_000) + asset_crit_bpp_pembangkit: Optional[float] = Field(None, ge=0, le=1_000_000_000_000_000) + asset_crit_dmn_daya_mampu_netto: Optional[float] = Field(None, ge=0, le=1_000_000_000_000_000) + asset_crit_marginal_cost: Optional[float] = Field(None, ge=0, le=1_000_000_000_000_000) + asset_crit_efdh_equivalent_forced_derated_hours: Optional[float] = Field(None, ge=0, le=1_000_000_000_000_000) + asset_crit_foh_forced_outage_hours: Optional[float] = Field(None, ge=0, le=1_000_000_000_000_000) + asset_crit_extra_fuel_cost: Optional[float] = Field(None, ge=0, le=1_000_000_000_000_000) + cf: Optional[float] = Field(None, ge=0, le=1_000_000_000_000_000) + eaf: Optional[float] = Field(None, ge=0, le=1_000_000_000_000_000) + rbd_simulation_id: Optional[str] = Field(None) + created_at: Optional[datetime] = Field(None) + updated_at: Optional[datetime] = Field(None) + created_by: Optional[str] = Field(None) + updated_by: Optional[str] = Field(None) @field_validator( "asset_crit_ens_energy_not_served", diff --git a/tests/conftest.py b/tests/conftest.py index c6eb1a2..04fa40a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -57,14 +57,34 @@ TestingSessionLocal = sessionmaker( autoflush=False, ) -@pytest.fixture(scope="session") -def event_loop(): +def pytest_sessionfinish(session, exitstatus): + """ + Called after whole test run finished, right before returning the exit status to the system. + Used here to dispose of all SQLAlchemy engines to prevent hanging. + """ + from src.database.core import engine as db_engine, collector_engine + + async def dispose_all(): + # Dispose of both test engine and production engines + await engine.dispose() + await db_engine.dispose() + await collector_engine.dispose() + try: - loop = asyncio.get_running_loop() - except RuntimeError: - loop = asyncio.new_event_loop() - yield loop - # loop.close() # Avoid closing if it might be shared + loop = asyncio.get_event_loop() + if loop.is_running(): + # If the loop is already running, we create a task + loop.create_task(dispose_all()) + else: + loop.run_until_complete(dispose_all()) + except Exception: + # Fallback for environment where no loop is available or loop is closed + try: + asyncio.run(dispose_all()) + except Exception: + pass + +# Removed custom event_loop fixture @pytest_asyncio.fixture(autouse=True) async def setup_db(): diff --git a/tests/unit/test_masterdata_service.py b/tests/unit/test_masterdata_service.py index b549f26..bfcf680 100644 --- a/tests/unit/test_masterdata_service.py +++ b/tests/unit/test_masterdata_service.py @@ -6,6 +6,7 @@ from src.masterdata.schema import MasterDataCreate @pytest.mark.asyncio async def test_create_masterdata_service(): mock_db = AsyncMock() + mock_db.add = MagicMock() masterdata_in = MasterDataCreate( name="Test", description="Desc", @@ -23,6 +24,7 @@ async def test_create_masterdata_service(): @pytest.mark.asyncio async def test_get_masterdata_service(): mock_db = AsyncMock() + mock_db.add = MagicMock() mock_result = MagicMock() mock_masterdata = MagicMock() mock_masterdata.id = "test-id" From 99023ef2950f9895778d643721296cdeb71fb7ee Mon Sep 17 00:00:00 2001 From: MrWaradana Date: Tue, 24 Feb 2026 10:01:53 +0700 Subject: [PATCH 16/16] add test guide markdown --- docs/test.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/test.md b/docs/test.md index 674da1e..1f4e185 100644 --- a/docs/test.md +++ b/docs/test.md @@ -10,7 +10,10 @@ Pastikan Anda berada di root direktori proyek dan environment sudah siap. ### **Opsi A: Menggunakan Virtual Environment (Direkomendasikan)** Aktifkan `venv` sebelum menjalankan perintah apapun: ```bash +python -m venv venv source venv/bin/activate +pip install poetry +poetry install ``` ### **Opsi B: Menggunakan Poetry**