From fa22434416d19df27ee68e97bf778bb4e32fc14e Mon Sep 17 00:00:00 2001 From: MrWaradana Date: Tue, 24 Feb 2026 16:03:34 +0700 Subject: [PATCH 01/14] clean uneccesary testing --- src/__pycache__/__init__.cpython-311.pyc | Bin 143 -> 139 bytes src/__pycache__/config.cpython-311.pyc | Bin 4836 -> 4817 bytes src/__pycache__/enums.cpython-311.pyc | Bin 1065 -> 1061 bytes src/__pycache__/models.cpython-311.pyc | Bin 6595 -> 6591 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 -> 4486 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 6565 -> 6561 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 154 -> 150 bytes .../__pycache__/model.cpython-311.pyc | Bin 1086 -> 1082 bytes .../__pycache__/schema.cpython-311.pyc | Bin 4034 -> 4030 bytes .../__pycache__/service.cpython-311.pyc | Bin 14452 -> 14448 bytes test_run.log | 51 +++++++++ tests/conftest.py | 18 ++-- 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 ---- 27 files changed, 60 insertions(+), 270 deletions(-) create mode 100644 test_run.log delete mode 100644 tests/e2e/test_acquisition_cost.py delete mode 100644 tests/e2e/test_equipment.py delete mode 100644 tests/e2e/test_equipment_master.py delete mode 100644 tests/e2e/test_healthcheck.py delete mode 100644 tests/e2e/test_masterdata.py delete mode 100644 tests/e2e/test_masterdata_simulations.py delete mode 100644 tests/e2e/test_plant_fs_transaction.py delete mode 100644 tests/e2e/test_plant_masterdata.py delete mode 100644 tests/e2e/test_plant_transaction.py delete mode 100644 tests/e2e/test_simulation.py delete mode 100644 tests/e2e/test_yeardata.py 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__/config.cpython-311.pyc b/src/__pycache__/config.cpython-311.pyc index 95f6f552f3e4b6dac98333382238072dd72464b6..33be3f6f1032b51ecbc82526ea0ee54fad2f9c9f 100644 GIT binary patch delta 871 zcmaE&dQp{cIWI340}wQ(&&_1woyaG_xL~6C#rhP!HT=t%7#LOqF$6@3F)^fyrU;}6 zV#tVtWrR{CQiRutEMo@h0%E8xNwAz~iWr72DX@(AGDe_!Acm@!28&utpb5)>g(cBM zWr3nBS#mJVK%QW#?BomVGWAjNU=30!(irwCfMsM-WHDqE!7_3{f5>C_LkTRW0F-0E zkW&WBDW)jFO{|A`MM0s<%`m-c zU>QxIm0B29s)OaU(ZWLmEUL4NQ3)kXHG#tD!K4WarnD$6unt`;!K4jzxdzy^T40x} zf!XR{murJvu9Kn{r3==tpJIUFdOfg=VT#cj{>cfdsb%$a#5MFt?Q5r{CEe1tba)EvaL1QFIC!WKl>O}6Bd zWpe~EohHZd#W1=~-pMB`>IM>U2N511!V^SzO@7U%#&}}05I;L37uy9M{fj*MS9tU{ Js|xIA0ssu3rO5yQ delta 897 zcmcbp`b3p)IWI340}z-WZOdHFGm%e%an(fii}k4tSz;hz5Lm;vjER9^H4sBUlsFSZ zs%Q#-iU5X;1XxBeRWe0rjqoyNpe`VW>XHJ>iKK{P=#mD@h%I9Tss~~qa0TKh8L+T8 zk}ybA7Az`(CMpLMWyz8U=>-8GPcT((@;fHkdIhk0$rLFJYZbvV(kU_+GD=_>S)e!M zFub7*mXim{F<{84faMfY6cHxYLmU*Pnxd2{JDVXzIaL8js-((**-AiCHC1so!(6T? zHLzy26m^7Vs9trjj0Vt3O$;kFz;aq>!J!Ej)dq?x!h#fNR+JV{7(JA~I~h!*x@1qx8V~^-}cL@J%jY7ELrrF~snO0a%SuiZOX6EMWC#CA%IWI340}$BnT9;wBkvEu0MB3RZCMC5jCMi`nCpkGWrno3MW^)D8Mn(WZ CwGN;F delta 52 zcmZ3=v66##IWI340}vd{<;hgv$Q#TgA+Mj2pPQDVyrm; DO*;=r delta 53 zcmbQhIE9gCIWI340}vd{<;k4LVk(ZZIQo-3OCMC5jCMi`nCpkGWrno3MCb6_6BWAKW G;{yP9D-cHj delta 56 zcmbQtGM$BYIWI340}vd{<;l$6$ji$ptD>KgpPQ^x|&1N3M5OdQmKUl5+JV0fnL4R9-@^ZLH~iKD&oKe=4CM`frzJ>-@dQ; z&CGkVPh#I5Z2BV@3?M2u^0x}Sc(>_+r@edPqm-&Gq%LYnJ)6y>44h46D&|~@Jn?)X z@-+DyI{%;Zkk8oy^Dp_E{c~CMsy^Lc3@mX)afvTVOI`+<`+?Y%MOGD7JqZ0prz<$- zL5rLjYuE@avh$&awaU<3I$hamy~XTbUEWC@rqn#8llLMh#djSpu^40TdiM>I4uT`@GAE~t*wFiM%C<_KWn zvaUJ4MuGs!c93%s!sG?l$wWveKfO%paCRVDE}7aD6GMA&2LLp|@y%t7jA>$rHB5{< z!G8cC4$wtG5ir~aw*h=W!STBrmvZG@WH>^@hCm_i%5~g=P#418)OD@-%=y`IErT^Y zkWObax}Hu$wAyQP+RqCO|e1=JCxW$Oz`lAyuT{mnyC5h z_^{P6Vt0&K@~AD3*5y$*xz7qE?ND+HvBBhq+)B*QTs+ zza2i_53G&En-?rOVatiSoN&nwSs}#^DUhRV$kD2>(tfkkl6!5rw=VZSCSQ3cnWJRQ z`zU;5i8EinLxgI6@)O&_sa~^{{ObFi5y?s6lbBC0=ktY9-r+Tj%NY03awxo8?gaT+ z=#Te6;ac7lZb8F^+|~5_m*xPJZjJmS{BR)hlR}VH<$m65n;EQEd)7?V`VHxazVU;6JR4~>`<~jNkY~RP>XJG?83Ms7s o^bw-x!pD@OqH8DcIdUgh?HggDTj*`52L1!@(>EcVj^l3rGVHf3{$p9Lb!?7(V$h^D2=V=3s^rPLN>`kiwM0HhCkfFeCfqqpWF@ z%h==?IVVqLb2rjtEfNAMDG~z_azNr1XK8V2QG8}zT7Ho*kYCISBou(4p-2RzMtX7_ zyA0D^hRHST{)~K+&#;#=-kAKC!TP<@q%S9pk1YGP4pkzQG9 zQD$0Yd{%jhGEk;?@8o9g9%;b~Lgs6WHyExgTT=#P0?|btiz_@97kDft>+$ID@-iP( zmUrS{KE%Q5#5*~i$68bxsJX}pM1TUVh#N%6PTtOwrz6A~&bT1`0|SUAKx#}54IC zK-MiTU?l1#=jWwmrWff<5)ico=`R8)y2b5WT2z#pR}u=02C!>yv4-Slr{)!Df|Q7Y t2v9`);;_lhPbtkwwJY+T93(J7OPx{T0|O>8f$Jkk^b0~NY%-5v6#(F4oxuPA diff --git a/src/database/__pycache__/__init__.cpython-311.pyc b/src/database/__pycache__/__init__.cpython-311.pyc index 72fb57c1819d074cdb374c36908e0aa65964ccf6..4badd8cf6a0fba5f59c388c289357c5970b1acde 100644 GIT binary patch delta 53 zcmbQiIE9gCIWI340}$BnT9+}A$3#}$*(xR_wJatnRW~O&IWeZVC^;r2u_Q4mu{bqm HVxlh($ delta 62 zcmaFk{K1)TIWI340}zCA&B^>DxREcFMNwTpBR@A)KQS{mUq2~THzzqcQNOq-SwAJQ QBrz$mI8}f11eO?C0BD*NN&o-= diff --git a/src/database/__pycache__/service.cpython-311.pyc b/src/database/__pycache__/service.cpython-311.pyc index 0166c2ff33728c92e35d5aa9d248769ea05be96a..e62533cc09f2e24f725078c92cc062a75d3399b2 100644 GIT binary patch delta 58 zcmZ2#ywI3$IWI340}!lYo0DlJvXM`URaVW}DkdehEG8*cHzzqcF{ZdEIVL5sBrz$m MI5lRoCu@ud0NXnervLx| delta 62 zcmZ2zywsR)IWI340}!}eoR--yypd0eRZ&YnBR@A)KQS{mUq2~THzzqcQNOq-SwAJQ QBrz$mI8}eMD{G7h06ygtKL7v# diff --git a/src/masterdata/__pycache__/__init__.cpython-311.pyc b/src/masterdata/__pycache__/__init__.cpython-311.pyc index 32396611605057ee0aad64bf3abdcab039cabf15..7e1190cfa2c2107bddfe5f665eb7f68d481f302a 100644 GIT binary patch delta 55 zcmbQmIE|5KIWI340}$BnT9+}A$3$M!*(xR_wJatnRW~O&IWeZVC^;rKvA86)C?&BZ JF=k@2IRLhc5!wI% delta 59 zcmbQnIE#^IIWI340}vd{<;k4LW1_69pOK%Ns-Kvdo3Edgs+*IXoTy)1l&qhdSX`1? Nl#*DIs6R2@902B-5>o&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_KR2kzfYcbIWI340}wQ(&&|BOk#`%5yt=bhOiF55Oj4?DPI7W$OmR_iOm1RvNor9_ NVo74m=4UK-xBx!86&?Tp delta 63 zcmdlde@LEpIWI340}xE+nv*$uBkwjAWo`Y8{M=Oi#LV1${iIagoaE$0{o + from src.masterdata.service import calculate_pmt +src\masterdata\service.py:6: in + from src.database.service import search_filter_sort_paginate +src\database\service.py:7: in + from .core import DbSession +src\database\core.py:19: in + from src.config import SQLALCHEMY_DATABASE_URI, COLLECTOR_URI +src\config.py:99: in + DEV_USERNAME = config("DEV_USERNAME") +venv\Lib\site-packages\starlette\config.py:90: in __call__ + return self.get(key, cast, default) +venv\Lib\site-packages\starlette\config.py:107: in get + raise KeyError(f"Config '{key}' is missing, and has no default.") +E KeyError: "Config 'DEV_USERNAME' is missing, and has no default." +___________ ERROR collecting tests/unit/test_masterdata_service.py ____________ +tests\unit\test_masterdata_service.py:3: in + from src.masterdata.service import create, get +src\masterdata\service.py:6: in + from src.database.service import search_filter_sort_paginate +src\database\service.py:7: in + from .core import DbSession +src\database\core.py:19: in + from src.config import SQLALCHEMY_DATABASE_URI, COLLECTOR_URI +src\config.py:99: in + DEV_USERNAME = config("DEV_USERNAME") +venv\Lib\site-packages\starlette\config.py:90: in __call__ + return self.get(key, cast, default) +venv\Lib\site-packages\starlette\config.py:107: in get + raise KeyError(f"Config '{key}' is missing, and has no default.") +E KeyError: "Config 'DEV_USERNAME' is missing, and has no default." +=========================== short test summary info =========================== +ERROR tests/unit/test_masterdata_logic.py - KeyError: "Config 'DEV_USERNAME' ... +ERROR tests/unit/test_masterdata_service.py - KeyError: "Config 'DEV_USERNAME... +!!!!!!!!!!!!!!!!!!! Interrupted: 2 errors during collection !!!!!!!!!!!!!!!!!!! +============================== 2 errors in 0.67s ============================== diff --git a/tests/conftest.py b/tests/conftest.py index 29f621b..3360e93 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,13 +1,13 @@ -# import os +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" +# 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 diff --git a/tests/e2e/test_acquisition_cost.py b/tests/e2e/test_acquisition_cost.py deleted file mode 100644 index e748bbd..0000000 --- a/tests/e2e/test_acquisition_cost.py +++ /dev/null @@ -1,23 +0,0 @@ -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 deleted file mode 100644 index 53eede2..0000000 --- a/tests/e2e/test_equipment.py +++ /dev/null @@ -1,26 +0,0 @@ -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 deleted file mode 100644 index a75f10f..0000000 --- a/tests/e2e/test_equipment_master.py +++ /dev/null @@ -1,8 +0,0 @@ -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 deleted file mode 100644 index 0908cd7..0000000 --- a/tests/e2e/test_healthcheck.py +++ /dev/null @@ -1,8 +0,0 @@ -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 deleted file mode 100644 index aa4c215..0000000 --- a/tests/e2e/test_masterdata.py +++ /dev/null @@ -1,97 +0,0 @@ -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 deleted file mode 100644 index 3037264..0000000 --- a/tests/e2e/test_masterdata_simulations.py +++ /dev/null @@ -1,8 +0,0 @@ -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 deleted file mode 100644 index dbdc94e..0000000 --- a/tests/e2e/test_plant_fs_transaction.py +++ /dev/null @@ -1,16 +0,0 @@ -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 deleted file mode 100644 index ea5f9a4..0000000 --- a/tests/e2e/test_plant_masterdata.py +++ /dev/null @@ -1,20 +0,0 @@ -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 deleted file mode 100644 index 5518e4a..0000000 --- a/tests/e2e/test_plant_transaction.py +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index e44ba9f..0000000 --- a/tests/e2e/test_simulation.py +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index b187bdd..0000000 --- a/tests/e2e/test_yeardata.py +++ /dev/null @@ -1,18 +0,0 @@ -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" From b11edfd98c60fa3613f378707d418c871dbf3307 Mon Sep 17 00:00:00 2001 From: MrWaradana Date: Tue, 24 Feb 2026 16:16:17 +0700 Subject: [PATCH 02/14] jenkins --- Jenkinsfile | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 2559cb0..2d43bd4 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -4,7 +4,6 @@ pipeline { environment { DOCKER_HUB_USERNAME = 'aimodocker' // This creates DOCKER_AUTH_USR and DOCKER_AUTH_PSW - DOCKER_AUTH = credentials('aimodocker') IMAGE_NAME = 'lcca-service' SERVICE_NAME = 'ahm-app' @@ -55,13 +54,6 @@ pipeline { // } // } - stage('Docker Login') { - steps { - // 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 & Tag') { steps { script { @@ -75,14 +67,19 @@ pipeline { } } - stage('Push to Docker Hub') { + stage('Docker Login & Push') { steps { script { def fullImageName = "${DOCKER_HUB_USERNAME}/${IMAGE_NAME}" - sh "docker push ${fullImageName}:${IMAGE_TAG}" - - if (SECONDARY_TAG) { - sh "docker push ${fullImageName}:${SECONDARY_TAG}" + withCredentials([usernamePassword(credentialsId: 'aimodocker', passwordVariable: 'DOCKER_PSW', usernameVariable: 'DOCKER_USR')]) { + // Use single quotes to prevent Groovy from interpolating the secret in logs + sh 'echo $DOCKER_PSW | docker login -u $DOCKER_USR --password-stdin' + + sh "docker push ${fullImageName}:${IMAGE_TAG}" + + if (SECONDARY_TAG) { + sh "docker push ${fullImageName}:${SECONDARY_TAG}" + } } } } From 18df242c6b883afb1c671a3043dd8c9ac8fa99f0 Mon Sep 17 00:00:00 2001 From: MrWaradana Date: Wed, 25 Feb 2026 10:51:08 +0700 Subject: [PATCH 03/14] feat: route for export all data --- src/equipment/router.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/equipment/router.py b/src/equipment/router.py index 4a49db6..a6ea5c2 100644 --- a/src/equipment/router.py +++ b/src/equipment/router.py @@ -211,6 +211,18 @@ async def get_calculated_top_10_replacement_priorities(db_session: DbSession, co message="Top 10 Replacement Priorities Data retrieved successfully", ) +@router.get( + "/top-10-replacement-priorities-export-all", + response_model=StandardResponse[EquipmentTop10Pagination], +) +async def get_calculated_top_10_replacement_priorities_all(db_session: DbSession, common: CommonParameters): + common["all"] = True + equipment_data = await get_top_10_replacement_priorities(db_session=db_session, common=common) + return StandardResponse( + data=equipment_data, + message="All Replacement Priorities Data retrieved successfully", + ) + @router.get( "/top-10-economic-life", response_model=StandardResponse[EquipmentTop10Pagination], @@ -224,6 +236,18 @@ async def get_calculated_top_10_economic_life(db_session: DbSession, common: Com message="Top 10 Economic Life Data retrieved successfully", ) +@router.get( + "/top-10-economic-life-export-all", + response_model=StandardResponse[EquipmentTop10Pagination], +) +async def get_calculated_top_10_economic_life_all(db_session: DbSession, common: CommonParameters): + common["all"] = True + equipment_data = await get_top_10_economic_life(db_session=db_session, common=common) + return StandardResponse( + data=equipment_data, + message="All Economic Life Data retrieved successfully", + ) + @router.get("/tree", response_model=StandardResponse[EquipmentRead]) async def get_equipment_tree(): From 7a4050ee4a396a769c5abbc482906e26962137af Mon Sep 17 00:00:00 2001 From: MrWaradana Date: Wed, 25 Feb 2026 10:59:23 +0700 Subject: [PATCH 04/14] feat: Add request validation middleware to enforce security and data integrity checks on items_per_page limitation --- src/middleware.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/middleware.py b/src/middleware.py index 5599a59..b226813 100644 --- a/src/middleware.py +++ b/src/middleware.py @@ -152,17 +152,26 @@ class RequestValidationMiddleware(BaseHTTPMiddleware): for key, value in params: if value: inspect_value(value, f"query param '{key}'") - + # Pagination constraint: multiples of 5, max 50 if key in pagination_size_keys and value: try: size_val = int(value) if size_val > 50: - raise HTTPException(status_code=400, detail=f"Pagination size '{key}' cannot exceed 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") + 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") + raise HTTPException( + status_code=400, + detail=f"Pagination size '{key}' must be an integer", + ) # ------------------------- # 4. Content-Type sanity From 6e479406b9aa219990dacea5b0ff56e3c4805c0f Mon Sep 17 00:00:00 2001 From: MrWaradana Date: Wed, 25 Feb 2026 16:57:46 +0700 Subject: [PATCH 05/14] add export route and add image data on response for equipment master --- src/acquisition_cost/router.py | 17 +++++++++++++++++ src/equipment/router.py | 17 +++++++++++++++++ src/equipment_master/model.py | 3 ++- src/equipment_master/router.py | 15 +++++++++++++++ src/manpower_cost/router.py | 17 +++++++++++++++++ src/manpower_master/router.py | 17 +++++++++++++++++ src/masterdata/router.py | 17 +++++++++++++++++ src/plant_masterdata/router.py | 17 +++++++++++++++++ src/plant_transaction_data/router.py | 17 +++++++++++++++++ .../router.py | 19 +++++++++++++++++++ src/simulations/router.py | 16 ++++++++++++++++ src/uploaded_file/router.py | 17 +++++++++++++++++ src/yeardata/router.py | 17 +++++++++++++++++ 13 files changed, 205 insertions(+), 1 deletion(-) diff --git a/src/acquisition_cost/router.py b/src/acquisition_cost/router.py index 194de28..7fd49a7 100644 --- a/src/acquisition_cost/router.py +++ b/src/acquisition_cost/router.py @@ -33,6 +33,23 @@ async def get_yeardatas( message="Data retrieved successfully", ) +@router.get("/export-all", response_model=StandardResponse[AcquisitionCostDataPagination]) +async def get_yeardatas_export_all( + db_session: DbSession, + common: CommonParameters, +): + """Get all acquisition_cost_data for export.""" + common["all"] = True + get_acquisition_cost_data = await get_all( + db_session=db_session, + items_per_page=-1, + common=common, + ) + return StandardResponse( + data=get_acquisition_cost_data, + message="All Acquisition Cost Data retrieved successfully", + ) + @router.get("/{acquisition_cost_data_id}", response_model=StandardResponse[AcquisitionCostDataRead]) async def get_acquisition_cost_data(db_session: DbSession, acquisition_cost_data_id: str): diff --git a/src/equipment/router.py b/src/equipment/router.py index a6ea5c2..6927e5c 100644 --- a/src/equipment/router.py +++ b/src/equipment/router.py @@ -62,6 +62,23 @@ async def get_equipments( message="Data retrieved successfully", ) +@router.get("/export-all", response_model=StandardResponse[EquipmentPagination]) +async def get_equipments_export_all( + db_session: DbSession, + common: CommonParameters, +): + """Get all equipment for export.""" + common["all"] = True + equipment_data = await get_all( + db_session=db_session, + items_per_page=-1, + common=common, + ) + return StandardResponse( + data=equipment_data, + message="All Equipment Data retrieved successfully", + ) + @router.get("/maximo/{assetnum}", response_model=StandardResponse[List[dict]]) diff --git a/src/equipment_master/model.py b/src/equipment_master/model.py index f9d64ba..c86e64b 100644 --- a/src/equipment_master/model.py +++ b/src/equipment_master/model.py @@ -31,7 +31,8 @@ class EquipmentMaster(Base, DefaultMixin): system_tag = Column(String, nullable=True) assetnum = Column(String, nullable=True) location_tag = Column(String, nullable=True) - + image_name = Column(String, nullable=True) + description = Column(String, nullable=True) # Relationship definitions # Define both sides of the relationship # parent = relationship( diff --git a/src/equipment_master/router.py b/src/equipment_master/router.py index a201d76..e843dbb 100644 --- a/src/equipment_master/router.py +++ b/src/equipment_master/router.py @@ -28,6 +28,21 @@ async def get_all_equipment_master_tree( data=equipment_masters, message="Data retrieved successfully" ) +@router.get("/export-all", response_model=StandardResponse[EquipmentMasterPaginated]) +async def get_all_equipment_master_tree_export_all( + db_session: DbSession, + common: CommonParameters, +): + common["all"] = True + equipment_masters = await get_all_master( + db_session=db_session, + common=common, + ) + + return StandardResponse( + data=equipment_masters, message="All Equipment Master Data retrieved successfully" + ) + @router.get( "/{equipment_master_id}", response_model=StandardResponse[EquipmentMasterRead] diff --git a/src/manpower_cost/router.py b/src/manpower_cost/router.py index d34a809..6eaa8da 100644 --- a/src/manpower_cost/router.py +++ b/src/manpower_cost/router.py @@ -32,6 +32,23 @@ async def get_yeardatas( message="Data retrieved successfully", ) +@router.get("/export-all", response_model=StandardResponse[ManpowerCostPagination]) +async def get_yeardatas_export_all( + db_session: DbSession, + common: CommonParameters, +): + """Get all manpower_cost_data for export.""" + common["all"] = True + get_acquisition_cost_data = await get_all( + db_session=db_session, + items_per_page=-1, + common=common, + ) + return StandardResponse( + data=get_acquisition_cost_data, + message="All Manpower Cost Data retrieved successfully", + ) + @router.get("/{acquisition_cost_data_id}", response_model=StandardResponse[ManpowerCostRead]) async def get_acquisition_cost_data(db_session: DbSession, acquisition_cost_data_id: str): diff --git a/src/manpower_master/router.py b/src/manpower_master/router.py index d34a809..69b4929 100644 --- a/src/manpower_master/router.py +++ b/src/manpower_master/router.py @@ -32,6 +32,23 @@ async def get_yeardatas( message="Data retrieved successfully", ) +@router.get("/export-all", response_model=StandardResponse[ManpowerCostPagination]) +async def get_yeardatas_export_all( + db_session: DbSession, + common: CommonParameters, +): + """Get all manpower_master_data for export.""" + common["all"] = True + get_acquisition_cost_data = await get_all( + db_session=db_session, + items_per_page=-1, + common=common, + ) + return StandardResponse( + data=get_acquisition_cost_data, + message="All Manpower Master Data retrieved successfully", + ) + @router.get("/{acquisition_cost_data_id}", response_model=StandardResponse[ManpowerCostRead]) async def get_acquisition_cost_data(db_session: DbSession, acquisition_cost_data_id: str): diff --git a/src/masterdata/router.py b/src/masterdata/router.py index b351ee3..13d1deb 100644 --- a/src/masterdata/router.py +++ b/src/masterdata/router.py @@ -40,6 +40,23 @@ async def get_masterdatas( message="Data retrieved successfully", ) +@router.get("/export-all", response_model=StandardResponse[MasterDataPagination]) +async def get_masterdatas_export_all( + db_session: DbSession, + common: CommonParameters, +): + """Get all documents for export.""" + common["all"] = True + master_datas = await get_all( + db_session=db_session, + items_per_page=-1, + common=common, + ) + return StandardResponse( + data=master_datas, + message="All Master Data retrieved successfully", + ) + @router.get("/{masterdata_id}", response_model=StandardResponse[MasterDataRead]) async def get_masterdata(db_session: DbSession, masterdata_id: str): diff --git a/src/plant_masterdata/router.py b/src/plant_masterdata/router.py index f696acd..6064ce2 100644 --- a/src/plant_masterdata/router.py +++ b/src/plant_masterdata/router.py @@ -38,6 +38,23 @@ async def get_masterdatas( message="Data retrieved successfully", ) +@router.get("/export-all", response_model=StandardResponse[PlantMasterDataPagination]) +async def get_masterdatas_export_all( + db_session: DbSession, + common: CommonParameters, +): + """Get all documents for export.""" + common["all"] = True + master_datas = await get_all( + db_session=db_session, + items_per_page=-1, + common=common, + ) + return StandardResponse( + data=master_datas, + message="All Plant Master Data retrieved successfully", + ) + @router.get("/{masterdata_id}", response_model=StandardResponse[PlantMasterDataRead]) async def get_masterdata(db_session: DbSession, masterdata_id: str): diff --git a/src/plant_transaction_data/router.py b/src/plant_transaction_data/router.py index bd4f02c..bac841f 100644 --- a/src/plant_transaction_data/router.py +++ b/src/plant_transaction_data/router.py @@ -49,6 +49,23 @@ async def get_transaction_datas( message="Data retrieved successfully", ) +@router.get("/export-all", response_model=StandardResponse[PlantTransactionDataPagination]) +async def get_transaction_datas_export_all( + db_session: DbSession, + common: CommonParameters, +): + """Get all transaction_data for export.""" + common["all"] = True + plant_transaction_data = await get_all( + db_session=db_session, + items_per_page=-1, + common=common, + ) + return StandardResponse( + data=plant_transaction_data, + message="All Plant Transaction Data retrieved successfully", + ) + @router.get("/charts", response_model=StandardResponse[PlantChartData]) async def get_chart_data(db_session: DbSession, common: CommonParameters): chart_data, bep_year, bep_total_lcc = await get_charts( diff --git a/src/plant_transaction_data_simulations/router.py b/src/plant_transaction_data_simulations/router.py index bd6a16c..afe05cf 100644 --- a/src/plant_transaction_data_simulations/router.py +++ b/src/plant_transaction_data_simulations/router.py @@ -52,6 +52,25 @@ async def get_transaction_datas( message="Data retrieved successfully", ) +@router.get("/export-all", response_model=StandardResponse[PlantTransactionDataSimulationsPagination]) +async def get_transaction_datas_export_all( + db_session: DbSession, + common: CommonParameters, + simulation_id: UUID = Query(..., description="Simulation identifier"), +): + """Get all transaction_data for export.""" + common["all"] = True + plant_transaction_data = await get_all( + db_session=db_session, + items_per_page=-1, + common=common, + simulation_id=simulation_id, + ) + return StandardResponse( + data=plant_transaction_data, + message="All Plant Transaction Data Simulations retrieved successfully", + ) + @router.get("/charts", response_model=StandardResponse[PlantChartDataSimulations]) async def get_chart_data( db_session: DbSession, diff --git a/src/simulations/router.py b/src/simulations/router.py index 50b1d3b..26c3faf 100644 --- a/src/simulations/router.py +++ b/src/simulations/router.py @@ -36,6 +36,22 @@ async def get_simulations( ) return StandardResponse(data=simulations, message="Data retrieved successfully") +@router.get("/export-all", response_model=StandardResponse[SimulationPagination]) +async def get_simulations_export_all( + db_session: DbSession, + common: CommonParameters, + current_user: CurrentUser, +): + """Get all simulations for export.""" + common["all"] = True + simulations = await get_all( + db_session=db_session, + items_per_page=-1, + common=common, + owner=current_user.name, + ) + return StandardResponse(data=simulations, message="All Simulations Data retrieved successfully") + @router.get("/{simulation_id}", response_model=StandardResponse[SimulationRead]) async def get_simulation( diff --git a/src/uploaded_file/router.py b/src/uploaded_file/router.py index d03b1b0..e3232dc 100644 --- a/src/uploaded_file/router.py +++ b/src/uploaded_file/router.py @@ -36,6 +36,23 @@ async def get_uploaded_files( message="Data retrieved successfully", ) +@router.get("/export-all", response_model=StandardResponse[UploadedFileDataPagination]) +async def get_uploaded_files_export_all( + db_session: DbSession, + common: CommonParameters, +): + """Get all uploaded files for export.""" + common["all"] = True + uploaded_files = await get_all( + db_session=db_session, + items_per_page=-1, + common=common, + ) + return StandardResponse( + data=uploaded_files, + message="All Uploaded Files Data retrieved successfully", + ) + @router.get("/{uploaded_file_id}", response_model=StandardResponse[UploadedFileDataRead]) async def get_uploaded_file(db_session: DbSession, uploaded_file_id: str): diff --git a/src/yeardata/router.py b/src/yeardata/router.py index b92d443..8a536aa 100644 --- a/src/yeardata/router.py +++ b/src/yeardata/router.py @@ -33,6 +33,23 @@ async def get_yeardatas( message="Data retrieved successfully", ) +@router.get("/export-all", response_model=StandardResponse[YeardataPagination]) +async def get_yeardatas_export_all( + db_session: DbSession, + common: CommonParameters, +): + """Get all yeardata for export.""" + common["all"] = True + year_data = await get_all( + db_session=db_session, + items_per_page=-1, + common=common, + ) + return StandardResponse( + data=year_data, + message="All Year Data retrieved successfully", + ) + @router.get("/{yeardata_id}", response_model=StandardResponse[YeardataRead]) async def get_yeardata(db_session: DbSession, yeardata_id: str): From 589d5f099f5e40fba546650d81e5c94f55fbdfc1 Mon Sep 17 00:00:00 2001 From: MrWaradana Date: Wed, 25 Feb 2026 17:11:29 +0700 Subject: [PATCH 06/14] add equipment master schema --- src/equipment/schema.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/equipment/schema.py b/src/equipment/schema.py index 50fc188..5df5fab 100644 --- a/src/equipment/schema.py +++ b/src/equipment/schema.py @@ -34,9 +34,17 @@ class EquipmentBase(DefaultBase): updated_by: Optional[str] = Field(None) class EquipmentMasterBase(DefaultBase): - location_tag: Optional[str] = Field(None) - assetnum: Optional[str] = Field(None) + id: Optional[UUID] = Field(None) name: Optional[str] = Field(None) + parent_id: Optional[UUID] = Field(None) + equipment_tree_id: Optional[UUID] = Field(None) + category_id: Optional[UUID] = Field(None) + system_tag: Optional[str] = Field(None) + assetnum: Optional[str] = Field(None) + location_tag: Optional[str] = Field(None) + image_name: Optional[str] = Field(None) + description: Optional[str] = Field(None) + class MasterBase(DefaultBase): assetnum: Optional[str] = Field(None) From 8fc47edc1b34be34342fb1fbdd032dd40cbbfcc5 Mon Sep 17 00:00:00 2001 From: MrWaradana Date: Mon, 2 Mar 2026 13:45:21 +0700 Subject: [PATCH 07/14] security revision --- src/manpower_cost/schema.py | 13 ++- src/masterdata_simulations/schema.py | 11 +- src/middleware.py | 108 +++++++++++++++--- src/plant_fs_transaction_data/router.py | 15 +-- src/plant_fs_transaction_data/schema.py | 17 ++- src/plant_masterdata/router.py | 10 +- src/plant_masterdata/schema.py | 17 ++- src/plant_transaction_data/router.py | 10 +- src/plant_transaction_data/schema.py | 17 ++- .../router.py | 13 +-- .../schema.py | 21 +++- src/simulations/router.py | 10 +- src/simulations/schema.py | 17 ++- src/uploaded_file/router.py | 11 +- src/uploaded_file/schema.py | 16 ++- src/yeardata/router.py | 11 +- src/yeardata/schema.py | 17 ++- 17 files changed, 261 insertions(+), 73 deletions(-) diff --git a/src/manpower_cost/schema.py b/src/manpower_cost/schema.py index 91eda64..99ca8c3 100644 --- a/src/manpower_cost/schema.py +++ b/src/manpower_cost/schema.py @@ -34,5 +34,14 @@ class ManpowerCostPagination(Pagination): class QueryParams(CommonParams): - items_per_page: Optional[int] = Field(5) - search: Optional[str] = Field(None) \ No newline at end of file + items_per_page: Optional[int] = Field( + default=5, + ge=1, + le=1000, + description="Number of items per page", + alias="itemsPerPage", + ) + search: Optional[str] = Field( + default=None, + description="Search keyword", + ) diff --git a/src/masterdata_simulations/schema.py b/src/masterdata_simulations/schema.py index b5726e0..8c0f048 100644 --- a/src/masterdata_simulations/schema.py +++ b/src/masterdata_simulations/schema.py @@ -46,8 +46,13 @@ class QueryParams(CommonParams): description="Simulation identifier", ) items_per_page: Optional[int] = Field( - 5, + default=5, ge=1, - description="Items per page" + le=1000, + description="Items per page", + alias="itemsPerPage", + ) + search: Optional[str] = Field( + default=None, + description="Search keyword", ) - search: Optional[str] = Field(None) \ No newline at end of file diff --git a/src/middleware.py b/src/middleware.py index b226813..f428ef5 100644 --- a/src/middleware.py +++ b/src/middleware.py @@ -14,6 +14,37 @@ ALLOWED_MULTI_PARAMS = { "exclude[]", } +# Whitelist of ALL allowed query parameter names across the application. +# Any param NOT in this set will be rejected. +ALLOWED_QUERY_PARAMS = { + # CommonParameters (from database/service.py common_parameters) + "currentUser", + "page", + "itemsPerPage", + "q", + "filter", + "sortBy[]", + "descending[]", + "all", + # ListQueryParams / QueryParams used across routers + "items_per_page", + "search", + # equipment_master specific + "parent_id", + # masterdata_simulations / plant_transaction_data_simulations specific + "simulation_id", + # exclude + "exclude[]", +} + +# Query params that are ONLY allowed for "write" operations (read operations use ALLOWED_QUERY_PARAMS). +# For GET/POST/PUT/etc, whitelisting still applies. +WRITE_METHOD_ALLOWED_PARAMS = { + # Only auth/session params are allowed in query for write methods. + # Data values (like simulation_id) must be in the JSON body for these methods. + "currentUser", +} + MAX_QUERY_PARAMS = 50 MAX_QUERY_LENGTH = 2000 MAX_JSON_BODY_SIZE = 1024 * 100 # 100 KB @@ -62,31 +93,31 @@ def has_control_chars(value: str) -> bool: def inspect_value(value: str, source: str): if XSS_PATTERN.search(value): raise HTTPException( - status_code=400, + status_code=422, detail=f"Potential XSS payload detected in {source}", ) if SQLI_PATTERN.search(value): raise HTTPException( - status_code=400, + status_code=422, detail=f"Potential SQL injection payload detected in {source}", ) if RCE_PATTERN.search(value): raise HTTPException( - status_code=400, + status_code=422, detail=f"Potential RCE payload detected in {source}", ) if TRAVERSAL_PATTERN.search(value): raise HTTPException( - status_code=400, + status_code=422, detail=f"Potential Path Traversal payload detected in {source}", ) if has_control_chars(value): raise HTTPException( - status_code=400, + status_code=422, detail=f"Invalid control characters detected in {source}", ) @@ -96,7 +127,7 @@ def inspect_json(obj, path="body"): for key, value in obj.items(): if key in FORBIDDEN_JSON_KEYS: raise HTTPException( - status_code=400, + status_code=422, detail=f"Forbidden JSON key detected: {path}.{key}", ) inspect_json(value, f"{path}.{key}") @@ -126,12 +157,28 @@ class RequestValidationMiddleware(BaseHTTPMiddleware): if len(params) > MAX_QUERY_PARAMS: raise HTTPException( - status_code=400, + status_code=422, detail="Too many query parameters", ) # ------------------------- - # 2. Duplicate parameters + # 2. Query param whitelist + # ------------------------- + # For GET, we allow data parameters like page, search, etc. + # For POST, PUT, DELETE, PATCH, we ONLY allow auth/session params. + active_whitelist = ALLOWED_QUERY_PARAMS if request.method == "GET" else WRITE_METHOD_ALLOWED_PARAMS + + unknown_params = [ + key for key, _ in params if key not in active_whitelist + ] + if unknown_params: + raise HTTPException( + status_code=422, + detail=f"Unknown query parameters are not allowed for {request.method} request: {unknown_params}", + ) + + # ------------------------- + # 3. Duplicate parameters # ------------------------- counter = Counter(key for key, _ in params) duplicates = [ @@ -141,12 +188,40 @@ class RequestValidationMiddleware(BaseHTTPMiddleware): if duplicates: raise HTTPException( - status_code=400, + status_code=422, detail=f"Duplicate query parameters are not allowed: {duplicates}", ) # ------------------------- - # 3. Query param inspection + # 4. Single source enforcement + # Ensuring data comes from ONLY one source (Query OR Body). + # ------------------------- + content_type = request.headers.get("content-type", "") + has_json_body = content_type.startswith("application/json") + + # Check for data parameters in query (anything whitelisted as 'data' but not 'session/auth') + data_params_in_query = [ + key for key, _ in params + if key in ALLOWED_QUERY_PARAMS and key not in WRITE_METHOD_ALLOWED_PARAMS + ] + + if has_json_body: + # If sending JSON body, we forbid any data in query string (one source only) + if data_params_in_query: + raise HTTPException( + status_code=422, + detail=f"Single source enforcement: Data received from both JSON body and query string ({data_params_in_query}). Use only one source.", + ) + + # Special case: GET with body is discouraged/forbidden in many strict security contexts + if request.method == "GET": + raise HTTPException( + status_code=422, + detail="GET requests must use query parameters, not JSON body.", + ) + + # ------------------------- + # 5. Query param inspection # ------------------------- pagination_size_keys = {"size", "itemsPerPage", "per_page", "limit", "items_per_page"} for key, value in params: @@ -159,24 +234,23 @@ class RequestValidationMiddleware(BaseHTTPMiddleware): size_val = int(value) if size_val > 50: raise HTTPException( - status_code=400, + status_code=422, detail=f"Pagination size '{key}' cannot exceed 50", ) if size_val % 5 != 0: raise HTTPException( - status_code=400, + status_code=422, detail=f"Pagination size '{key}' must be a multiple of 5", ) except ValueError: raise HTTPException( - status_code=400, + status_code=422, detail=f"Pagination size '{key}' must be an integer", ) # ------------------------- - # 4. Content-Type sanity + # 6. Content-Type sanity # ------------------------- - content_type = request.headers.get("content-type", "") if content_type and not any( content_type.startswith(t) for t in ( @@ -191,7 +265,7 @@ class RequestValidationMiddleware(BaseHTTPMiddleware): ) # ------------------------- - # 5. JSON body inspection + # 7. JSON body inspection # ------------------------- if content_type.startswith("application/json"): body = await request.body() @@ -207,7 +281,7 @@ class RequestValidationMiddleware(BaseHTTPMiddleware): payload = json.loads(body) except json.JSONDecodeError: raise HTTPException( - status_code=400, + status_code=422, detail="Invalid JSON body", ) diff --git a/src/plant_fs_transaction_data/router.py b/src/plant_fs_transaction_data/router.py index 80a126b..93e7bd3 100644 --- a/src/plant_fs_transaction_data/router.py +++ b/src/plant_fs_transaction_data/router.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Annotated, List, Optional from uuid import UUID from fastapi import APIRouter, HTTPException, Query, status @@ -16,6 +16,7 @@ from .schema import ( PlantFSTransactionDataRead, PlantFSTransactionDataUpdate, PlantFSChartData, + ListQueryParams, ) from .service import create, delete, get, get_all, update, update_fs_charts_from_matrix, get_charts @@ -28,15 +29,14 @@ router = APIRouter() async def list_fs_transactions( db_session: DbSession, common: CommonParameters, - items_per_page: Optional[int] = Query(5), - search: Optional[str] = Query(None), + params: Annotated[ListQueryParams, Query()], ): """Return paginated financial statement transaction data.""" records = await get_all( db_session=db_session, - items_per_page=items_per_page, - search=search, + items_per_page=params.items_per_page, + search=params.search, common=common, ) @@ -166,8 +166,3 @@ async def delete_fs_transaction( await delete(db_session=db_session, fs_transaction_id=str(fs_transaction_id)) return StandardResponse(data=record, message="Data deleted successfully") - - - - - diff --git a/src/plant_fs_transaction_data/schema.py b/src/plant_fs_transaction_data/schema.py index 8af5592..d2497e4 100644 --- a/src/plant_fs_transaction_data/schema.py +++ b/src/plant_fs_transaction_data/schema.py @@ -4,7 +4,7 @@ from uuid import UUID from pydantic import Field -from src.models import DefaultBase, Pagination +from src.models import CommonParams, DefaultBase, Pagination class PlantFSTransactionDataBase(DefaultBase): @@ -100,3 +100,18 @@ class PlantFSChartData(DefaultBase): bep_year: Optional[int] = Field(None, ge=0, le=9999) bep_total_lcc: Optional[float] = Field(None, ge=0, le=1_000_000_000_000_000) + +class ListQueryParams(CommonParams): + items_per_page: Optional[int] = Field( + default=5, + ge=1, + le=1000, + description="Number of items per page", + alias="itemsPerPage", + ) + search: Optional[str] = Field( + default=None, + description="Search keyword", + ) + + diff --git a/src/plant_masterdata/router.py b/src/plant_masterdata/router.py index 6064ce2..f12f7c7 100644 --- a/src/plant_masterdata/router.py +++ b/src/plant_masterdata/router.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Annotated, Optional from fastapi import APIRouter, HTTPException, status, Query from .model import PlantMasterData @@ -7,6 +7,7 @@ from .schema import ( PlantMasterDataRead, PlantMasterDataCreate, PlantMasterDataUpdate, + ListQueryParams, ) from .service import get, get_all, create, update, delete @@ -22,15 +23,14 @@ router = APIRouter() async def get_masterdatas( db_session: DbSession, common: CommonParameters, - items_per_page: Optional[int] = Query(5), - search: Optional[str] = Query(None), + params: Annotated[ListQueryParams, Query()], ): """Get all documents.""" # return master_datas = await get_all( db_session=db_session, - items_per_page=items_per_page, - search=search, + items_per_page=params.items_per_page, + search=params.search, common=common, ) return StandardResponse( diff --git a/src/plant_masterdata/schema.py b/src/plant_masterdata/schema.py index 016003d..c938a6b 100644 --- a/src/plant_masterdata/schema.py +++ b/src/plant_masterdata/schema.py @@ -3,7 +3,7 @@ from typing import List, Optional from uuid import UUID from pydantic import Field -from src.models import DefaultBase, Pagination +from src.models import CommonParams, DefaultBase, Pagination from src.auth.service import CurrentUser @@ -85,3 +85,18 @@ class PlantMasterDataRead(PlantMasterdataBase): class PlantMasterDataPagination(Pagination): items: List[PlantMasterDataRead] = [] + + +class ListQueryParams(CommonParams): + items_per_page: Optional[int] = Field( + default=5, + ge=1, + le=1000, + description="Number of items per page", + alias="itemsPerPage", + ) + search: Optional[str] = Field( + default=None, + description="Search keyword", + ) + diff --git a/src/plant_transaction_data/router.py b/src/plant_transaction_data/router.py index bac841f..7baeee0 100644 --- a/src/plant_transaction_data/router.py +++ b/src/plant_transaction_data/router.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Annotated, List, Optional from fastapi import APIRouter, HTTPException, status, Query from .model import PlantTransactionData @@ -10,6 +10,7 @@ from .schema import ( PlantTransactionDataCreate, PlantTransactionDataUpdate, PlantTransactionFSImport, + ListQueryParams, ) from .service import ( get, @@ -33,14 +34,13 @@ router = APIRouter() async def get_transaction_datas( db_session: DbSession, common: CommonParameters, - items_per_page: Optional[int] = Query(5), - search: Optional[str] = Query(None), + params: Annotated[ListQueryParams, Query()], ): """Get all transaction_data pagination.""" plant_transaction_data = await get_all( db_session=db_session, - items_per_page=items_per_page, - search=search, + items_per_page=params.items_per_page, + search=params.search, common=common, ) # return diff --git a/src/plant_transaction_data/schema.py b/src/plant_transaction_data/schema.py index a9db71f..9813a80 100644 --- a/src/plant_transaction_data/schema.py +++ b/src/plant_transaction_data/schema.py @@ -3,7 +3,7 @@ from typing import Any, List, Optional from uuid import UUID from pydantic import Field -from src.models import DefaultBase, Pagination +from src.models import CommonParams, DefaultBase, Pagination class PlantTransactionDataBase(DefaultBase): @@ -117,3 +117,18 @@ class PlantTransactionDataRead(PlantTransactionDataBase): class PlantTransactionDataPagination(Pagination): items: List[PlantTransactionDataRead] = [] + + +class ListQueryParams(CommonParams): + items_per_page: Optional[int] = Field( + default=5, + ge=1, + le=1000, + description="Number of items per page", + alias="itemsPerPage", + ) + search: Optional[str] = Field( + default=None, + description="Search keyword", + ) + diff --git a/src/plant_transaction_data_simulations/router.py b/src/plant_transaction_data_simulations/router.py index afe05cf..b0b845c 100644 --- a/src/plant_transaction_data_simulations/router.py +++ b/src/plant_transaction_data_simulations/router.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Annotated, List, Optional from uuid import UUID from fastapi import APIRouter, HTTPException, status, Query @@ -11,6 +11,7 @@ from src.plant_transaction_data_simulations.schema import ( PlantTransactionDataSimulationsCreate, PlantTransactionDataSimulationsUpdate, PlantTransactionFSImportSimulations, + ListQueryParams, ) from src.plant_transaction_data_simulations.service import ( get, @@ -34,17 +35,15 @@ router = APIRouter() async def get_transaction_datas( db_session: DbSession, common: CommonParameters, - simulation_id: UUID = Query(..., description="Simulation identifier"), - items_per_page: Optional[int] = Query(5), - search: Optional[str] = Query(None), + params: Annotated[ListQueryParams, Query()], ): """Get all transaction_data pagination.""" plant_transaction_data = await get_all( db_session=db_session, - items_per_page=items_per_page, - search=search, + items_per_page=params.items_per_page, + search=params.search, common=common, - simulation_id=simulation_id, + simulation_id=params.simulation_id, ) # return return StandardResponse( diff --git a/src/plant_transaction_data_simulations/schema.py b/src/plant_transaction_data_simulations/schema.py index 0668854..939ad68 100644 --- a/src/plant_transaction_data_simulations/schema.py +++ b/src/plant_transaction_data_simulations/schema.py @@ -3,7 +3,7 @@ from typing import Any, List, Optional from uuid import UUID from pydantic import Field -from src.models import DefaultBase, Pagination +from src.models import CommonParams, DefaultBase, Pagination class PlantTransactionDataSimulationsBase(DefaultBase): @@ -140,3 +140,22 @@ class PlantTransactionDataSimulationsRead(PlantTransactionDataSimulationsBase): class PlantTransactionDataSimulationsPagination(Pagination): items: List[PlantTransactionDataSimulationsRead] = [] + + +class ListQueryParams(CommonParams): + simulation_id: UUID = Field( + ..., + description="Simulation identifier", + ) + items_per_page: Optional[int] = Field( + default=5, + ge=1, + le=1000, + description="Number of items per page", + alias="itemsPerPage", + ) + search: Optional[str] = Field( + default=None, + description="Search keyword", + ) + diff --git a/src/simulations/router.py b/src/simulations/router.py index 26c3faf..0f4c7ad 100644 --- a/src/simulations/router.py +++ b/src/simulations/router.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Annotated, Optional from fastapi import APIRouter, HTTPException, Query, status @@ -13,6 +13,7 @@ from src.simulations.schema import ( SimulationRead, SimulationRunPayload, SimulationUpdate, + ListQueryParams, ) from src.simulations.service import create, delete, get, get_all, run_simulation, update @@ -24,13 +25,12 @@ async def get_simulations( db_session: DbSession, common: CommonParameters, current_user: CurrentUser, - items_per_page: Optional[int] = Query(5), - search: Optional[str] = Query(None), + params: Annotated[ListQueryParams, Query()], ): simulations = await get_all( db_session=db_session, - items_per_page=items_per_page, - search=search, + items_per_page=params.items_per_page, + search=params.search, common=common, owner=current_user.name, ) diff --git a/src/simulations/schema.py b/src/simulations/schema.py index 7b5a7cd..c4780bf 100644 --- a/src/simulations/schema.py +++ b/src/simulations/schema.py @@ -4,7 +4,7 @@ from uuid import UUID from pydantic import Field -from src.models import DefaultBase, Pagination +from src.models import CommonParams, DefaultBase, Pagination from src.masterdata_simulations.schema import MasterDataSimulationRead from src.plant_transaction_data_simulations.schema import ( PlantTransactionDataSimulationsRead, @@ -51,3 +51,18 @@ class MasterDataOverride(DefaultBase): class SimulationRunPayload(DefaultBase): label: Optional[str] = Field(None) overrides: List[MasterDataOverride] = Field(default_factory=list) + + +class ListQueryParams(CommonParams): + items_per_page: Optional[int] = Field( + default=5, + ge=1, + le=1000, + description="Number of items per page", + alias="itemsPerPage", + ) + search: Optional[str] = Field( + default=None, + description="Search keyword", + ) + diff --git a/src/uploaded_file/router.py b/src/uploaded_file/router.py index e3232dc..142af32 100644 --- a/src/uploaded_file/router.py +++ b/src/uploaded_file/router.py @@ -1,8 +1,8 @@ -from typing import Optional +from typing import Annotated, Optional from fastapi import APIRouter, Form, HTTPException, status, Query, UploadFile, File from .model import UploadedFileData -from src.uploaded_file.schema import UploadedFileDataCreate, UploadedFileDataUpdate, UploadedFileDataRead, UploadedFileDataPagination +from src.uploaded_file.schema import UploadedFileDataCreate, UploadedFileDataUpdate, UploadedFileDataRead, UploadedFileDataPagination, ListQueryParams from src.uploaded_file.service import get, get_all, create, update, delete from src.database.service import CommonParameters, search_filter_sort_paginate @@ -20,14 +20,13 @@ router = APIRouter() async def get_uploaded_files( db_session: DbSession, common: CommonParameters, - items_per_page: Optional[int] = Query(5), - search: Optional[str] = Query(None), + params: Annotated[ListQueryParams, Query()], ): """Get all uploaded files pagination.""" uploaded_files = await get_all( db_session=db_session, - items_per_page=items_per_page, - search=search, + items_per_page=params.items_per_page, + search=params.search, common=common, ) # return diff --git a/src/uploaded_file/schema.py b/src/uploaded_file/schema.py index baf5a44..1750ea7 100644 --- a/src/uploaded_file/schema.py +++ b/src/uploaded_file/schema.py @@ -3,7 +3,7 @@ from typing import List, Optional from uuid import UUID from pydantic import Field -from src.models import DefaultBase, Pagination +from src.models import CommonParams, DefaultBase, Pagination class UploadedFileDataBase(DefaultBase): filename: str = Field(...) @@ -28,3 +28,17 @@ class UploadedFileDataRead(UploadedFileDataBase): class UploadedFileDataPagination(Pagination): items: List[UploadedFileDataRead] = [] + + +class ListQueryParams(CommonParams): + items_per_page: Optional[int] = Field( + default=5, + ge=1, + le=1000, + description="Number of items per page", + alias="itemsPerPage", + ) + search: Optional[str] = Field( + default=None, + description="Search keyword", + ) diff --git a/src/yeardata/router.py b/src/yeardata/router.py index 8a536aa..4b6efa5 100644 --- a/src/yeardata/router.py +++ b/src/yeardata/router.py @@ -1,8 +1,8 @@ -from typing import Optional +from typing import Annotated, Optional from fastapi import APIRouter, HTTPException, status, Query from .model import Yeardata -from .schema import YeardataPagination, YeardataRead, YeardataCreate, YeardataUpdate +from .schema import YeardataPagination, YeardataRead, YeardataCreate, YeardataUpdate, ListQueryParams from .service import get, get_all, create, update, delete from src.database.service import CommonParameters, search_filter_sort_paginate @@ -17,14 +17,13 @@ router = APIRouter() async def get_yeardatas( db_session: DbSession, common: CommonParameters, - items_per_page: Optional[int] = Query(5), - search: Optional[str] = Query(None), + params: Annotated[ListQueryParams, Query()], ): """Get all yeardata pagination.""" year_data = await get_all( db_session=db_session, - items_per_page=items_per_page, - search=search, + items_per_page=params.items_per_page, + search=params.search, common=common, ) # return diff --git a/src/yeardata/schema.py b/src/yeardata/schema.py index 039a4e7..778888d 100644 --- a/src/yeardata/schema.py +++ b/src/yeardata/schema.py @@ -3,7 +3,7 @@ from typing import List, Optional from uuid import UUID from pydantic import Field, field_validator -from src.models import DefaultBase, Pagination +from src.models import CommonParams, DefaultBase, Pagination class YeardataBase(DefaultBase): @@ -61,3 +61,18 @@ class YeardataRead(YeardataBase): class YeardataPagination(Pagination): items: List[YeardataRead] = [] + + +class ListQueryParams(CommonParams): + items_per_page: Optional[int] = Field( + default=5, + ge=1, + le=1000, + description="Number of items per page", + alias="itemsPerPage", + ) + search: Optional[str] = Field( + default=None, + description="Search keyword", + ) + From aaabe1b8c4e07a2f6667fc8ff75157494ff5ff16 Mon Sep 17 00:00:00 2001 From: MrWaradana Date: Mon, 2 Mar 2026 16:25:57 +0700 Subject: [PATCH 08/14] fix middleware --- src/middleware.py | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/src/middleware.py b/src/middleware.py index f428ef5..65c9aa3 100644 --- a/src/middleware.py +++ b/src/middleware.py @@ -193,27 +193,35 @@ class RequestValidationMiddleware(BaseHTTPMiddleware): ) # ------------------------- - # 4. Single source enforcement + # 4. JSON body inspection & Single source enforcement # Ensuring data comes from ONLY one source (Query OR Body). # ------------------------- content_type = request.headers.get("content-type", "") - has_json_body = content_type.startswith("application/json") + has_json_header = content_type.startswith("application/json") + # Read body now so we can check if it's actually empty + body = b"" + if has_json_header: + body = await request.body() + + # We consider it a "JSON body" source ONLY if it's not empty and not just "{}" + has_actual_json_body = has_json_header and body and body.strip() != b"{}" + # Check for data parameters in query (anything whitelisted as 'data' but not 'session/auth') data_params_in_query = [ key for key, _ in params if key in ALLOWED_QUERY_PARAMS and key not in WRITE_METHOD_ALLOWED_PARAMS ] - if has_json_body: - # If sending JSON body, we forbid any data in query string (one source only) + if has_actual_json_body: + # If sending actual JSON body, we forbid any data in query string (one source only) if data_params_in_query: raise HTTPException( status_code=422, detail=f"Single source enforcement: Data received from both JSON body and query string ({data_params_in_query}). Use only one source.", ) - # Special case: GET with body is discouraged/forbidden in many strict security contexts + # Special case: GET with actual body is discouraged/forbidden if request.method == "GET": raise HTTPException( status_code=422, @@ -265,17 +273,9 @@ class RequestValidationMiddleware(BaseHTTPMiddleware): ) # ------------------------- - # 7. JSON body inspection + # 7. JSON body inspection & Re-injection # ------------------------- - if content_type.startswith("application/json"): - body = await request.body() - - #if len(body) > MAX_JSON_BODY_SIZE: - # raise HTTPException( - # status_code=413, - # detail="JSON body too large", - # ) - + if has_json_header: if body: try: payload = json.loads(body) @@ -284,13 +284,11 @@ class RequestValidationMiddleware(BaseHTTPMiddleware): status_code=422, detail="Invalid JSON body", ) - inspect_json(payload) # Re-inject body for downstream handlers async def receive(): return {"type": "http.request", "body": body} - request._receive = receive # noqa: protected-access return await call_next(request) From 53cf29822bf75603aa15e0f9d8302c85e3ed310d Mon Sep 17 00:00:00 2001 From: MrWaradana Date: Thu, 5 Mar 2026 15:24:51 +0700 Subject: [PATCH 09/14] refactor: Update `QueryParams` to inherit from `DefaultBase` with an `itemsPerPage` alias and inject it as a dependency in the masterdata router. --- src/masterdata/router.py | 4 ++-- src/masterdata/schema.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/masterdata/router.py b/src/masterdata/router.py index 13d1deb..c0238d9 100644 --- a/src/masterdata/router.py +++ b/src/masterdata/router.py @@ -3,7 +3,7 @@ from fastapi import APIRouter, HTTPException, status, Query from sqlalchemy import Select -from src.manpower_cost.schema import QueryParams +from .schema import QueryParams from .model import MasterData from .schema import ( MasterDataPagination, @@ -25,7 +25,7 @@ router = APIRouter() async def get_masterdatas( db_session: DbSession, common: CommonParameters, - params: Annotated[QueryParams, Query()], + params: Annotated[QueryParams, Depends()], ): """Get all documents.""" # return diff --git a/src/masterdata/schema.py b/src/masterdata/schema.py index b2fe898..7a2d57d 100644 --- a/src/masterdata/schema.py +++ b/src/masterdata/schema.py @@ -52,10 +52,11 @@ class MasterDataPagination(Pagination): items: List[MasterDataRead] = [] -class QueryParams(BaseModel): +class QueryParams(DefaultBase): items_per_page: Optional[int] = Field( 5, ge=1, + alias="itemsPerPage", description="Items per page" ) search: Optional[str] = Field( From 33e85cface052a1d710fe3116aaf5b46b67acb37 Mon Sep 17 00:00:00 2001 From: MrWaradana Date: Fri, 6 Mar 2026 14:32:19 +0700 Subject: [PATCH 10/14] fix master data Depends dependecies --- src/masterdata/router.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/masterdata/router.py b/src/masterdata/router.py index c0238d9..29bb2b0 100644 --- a/src/masterdata/router.py +++ b/src/masterdata/router.py @@ -1,5 +1,5 @@ from typing import Annotated, Optional, List -from fastapi import APIRouter, HTTPException, status, Query +from fastapi import APIRouter, HTTPException, status, Query, Depends from sqlalchemy import Select From fca4d18d2b916d1c5aa5216fc2a8674dcc619b65 Mon Sep 17 00:00:00 2001 From: MrWaradana Date: Fri, 6 Mar 2026 15:33:03 +0700 Subject: [PATCH 11/14] fix items_per_page problem --- src/database/service.py | 18 ++++++++++++++---- src/equipment_master/schema.py | 1 - src/manpower_master/schema.py | 1 - src/masterdata/schema.py | 19 ++++++++++++++++--- src/models.py | 18 +++++++++++++++--- src/plant_fs_transaction_data/schema.py | 7 ------- src/plant_masterdata/schema.py | 7 ------- src/simulations/schema.py | 7 ------- 8 files changed, 45 insertions(+), 33 deletions(-) diff --git a/src/database/service.py b/src/database/service.py index baa61e7..9313812 100644 --- a/src/database/service.py +++ b/src/database/service.py @@ -18,9 +18,11 @@ QueryStr = constr(pattern=r"^[ -~]+$", min_length=1) def common_parameters( db_session: DbSession, # type: ignore - current_user: QueryStr = Query(None, alias="currentUser"), # type: ignore + current_user: Optional[str] = Query(None, alias="currentUser"), # type: ignore + current_user_snake: Optional[str] = Query(None, alias="current_user"), # type: ignore page: int = Query(1, gt=0, lt=2147483647), - items_per_page: int = Query(5, alias="itemsPerPage", gt=-2, lt=2147483647), + items_per_page: Optional[int] = Query(None, alias="items_per_page", gt=-2, lt=2147483647), + items_per_page_camel: Optional[int] = Query(None, alias="itemsPerPage", gt=-2, lt=2147483647), query_str: QueryStr = Query(None, alias="q"), # type: ignore filter_spec: QueryStr = Query(None, alias="filter"), # type: ignore sort_by: List[str] = Query([], alias="sortBy[]"), @@ -28,15 +30,23 @@ def common_parameters( all: int = Query(0), # role: QueryStr = Depends(get_current_role), ): + # Support both snake_case and camelCase for pagination size + final_items_per_page = items_per_page_camel if items_per_page_camel is not None else ( + items_per_page if items_per_page is not None else 5 + ) + + # Support both snake_case and camelCase for current user + final_current_user = current_user or current_user_snake + return { "db_session": db_session, "page": page, - "items_per_page": items_per_page, + "items_per_page": final_items_per_page, "query_str": query_str, "filter_spec": filter_spec, "sort_by": sort_by, "descending": descending, - "current_user": current_user, + "current_user": final_current_user, # "role": role, "all": bool(all), } diff --git a/src/equipment_master/schema.py b/src/equipment_master/schema.py index ae94d76..54eed2a 100644 --- a/src/equipment_master/schema.py +++ b/src/equipment_master/schema.py @@ -46,5 +46,4 @@ class EquipmentMasterPaginated(Pagination): class EquipmentMasterQuery(CommonParams): parent_id : Optional[str] = None - items_per_page : Optional[int] = 5 search : Optional[str] = None \ No newline at end of file diff --git a/src/manpower_master/schema.py b/src/manpower_master/schema.py index c945b86..26f010d 100644 --- a/src/manpower_master/schema.py +++ b/src/manpower_master/schema.py @@ -33,5 +33,4 @@ class ManpowerCostPagination(Pagination): items: List[ManpowerCostRead] = [] class QueryParams(CommonParams): - items_per_page: Optional[int] = Field(5) search: Optional[str] = Field(None) \ No newline at end of file diff --git a/src/masterdata/schema.py b/src/masterdata/schema.py index 7a2d57d..3c77dba 100644 --- a/src/masterdata/schema.py +++ b/src/masterdata/schema.py @@ -2,7 +2,7 @@ from datetime import datetime from typing import List, Optional from uuid import UUID -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, model_validator from src.models import DefaultBase, Pagination from src.auth.service import CurrentUser @@ -53,13 +53,26 @@ class MasterDataPagination(Pagination): class QueryParams(DefaultBase): - items_per_page: Optional[int] = Field( + items_per_page: int = Field( 5, ge=1, - alias="itemsPerPage", + alias="items_per_page", description="Items per page" ) + itemsPerPage: Optional[int] = Field( + None, + ge=1, + description="Alias for items_per_page" + ) search: Optional[str] = Field( None, description="Search keyword" ) + + @model_validator(mode="before") + @classmethod + def resolve_aliases(cls, data: any) -> any: + if isinstance(data, dict): + if "itemsPerPage" in data and data["itemsPerPage"] is not None: + data.setdefault("items_per_page", data["itemsPerPage"]) + return data diff --git a/src/models.py b/src/models.py index 9fdaee8..b03adf8 100644 --- a/src/models.py +++ b/src/models.py @@ -2,7 +2,7 @@ from datetime import datetime from typing import Generic, List, Optional, TypeVar import uuid -from pydantic import BaseModel, Field, SecretStr, ConfigDict +from pydantic import BaseModel, Field, SecretStr, ConfigDict, model_validator from sqlalchemy import Column, DateTime, String, func, event from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import Mapped, mapped_column @@ -100,9 +100,11 @@ class StandardResponse(BaseModel, Generic[T]): class CommonParams(DefaultBase): # This ensures no extra query params are allowed - current_user: Optional[str] = Field(None, alias="currentUser") + current_user: Optional[str] = Field(None, alias="current_user") + currentUser: Optional[str] = Field(None, description="Alias for current_user") page: int = Field(1, gt=0, lt=2147483647) - items_per_page: int = Field(5, gt=-2, lt=2147483647, alias="itemsPerPage") + items_per_page: int = Field(5, gt=-2, lt=2147483647, alias="items_per_page") + itemsPerPage: Optional[int] = Field(None, description="Alias for items_per_page") query_str: Optional[str] = Field(None, alias="q") filter_spec: Optional[str] = Field(None, alias="filter") sort_by: List[str] = Field(default_factory=list, alias="sortBy[]") @@ -110,6 +112,16 @@ class CommonParams(DefaultBase): exclude: List[str] = Field(default_factory=list, alias="exclude[]") all_params: int = Field(0, alias="all") + @model_validator(mode="before") + @classmethod + def resolve_aliases(cls, data: any) -> any: + if isinstance(data, dict): + if "itemsPerPage" in data and data["itemsPerPage"] is not None: + data.setdefault("items_per_page", data["itemsPerPage"]) + if "currentUser" in data and data["currentUser"] is not None: + data.setdefault("current_user", data["currentUser"]) + return data + # Property to mirror your original return dict's bool conversion @property def is_all(self) -> bool: diff --git a/src/plant_fs_transaction_data/schema.py b/src/plant_fs_transaction_data/schema.py index d2497e4..2341a18 100644 --- a/src/plant_fs_transaction_data/schema.py +++ b/src/plant_fs_transaction_data/schema.py @@ -102,13 +102,6 @@ class PlantFSChartData(DefaultBase): class ListQueryParams(CommonParams): - items_per_page: Optional[int] = Field( - default=5, - ge=1, - le=1000, - description="Number of items per page", - alias="itemsPerPage", - ) search: Optional[str] = Field( default=None, description="Search keyword", diff --git a/src/plant_masterdata/schema.py b/src/plant_masterdata/schema.py index c938a6b..58c22d4 100644 --- a/src/plant_masterdata/schema.py +++ b/src/plant_masterdata/schema.py @@ -88,13 +88,6 @@ class PlantMasterDataPagination(Pagination): class ListQueryParams(CommonParams): - items_per_page: Optional[int] = Field( - default=5, - ge=1, - le=1000, - description="Number of items per page", - alias="itemsPerPage", - ) search: Optional[str] = Field( default=None, description="Search keyword", diff --git a/src/simulations/schema.py b/src/simulations/schema.py index c4780bf..fe09cd0 100644 --- a/src/simulations/schema.py +++ b/src/simulations/schema.py @@ -54,13 +54,6 @@ class SimulationRunPayload(DefaultBase): class ListQueryParams(CommonParams): - items_per_page: Optional[int] = Field( - default=5, - ge=1, - le=1000, - description="Number of items per page", - alias="itemsPerPage", - ) search: Optional[str] = Field( default=None, description="Search keyword", From 85eca71bf81aa4b8435bfc0fb8143e0c610c0ec2 Mon Sep 17 00:00:00 2001 From: MrWaradana Date: Fri, 6 Mar 2026 15:39:07 +0700 Subject: [PATCH 12/14] fix optional dependencies --- src/database/service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/database/service.py b/src/database/service.py index 9313812..62e29df 100644 --- a/src/database/service.py +++ b/src/database/service.py @@ -1,5 +1,5 @@ import logging -from typing import Annotated, List +from typing import Annotated, List, Optional from sqlalchemy import desc, func, or_, Select from sqlalchemy_filters import apply_pagination From 3a7124386b73eb3af814b940ef2ac33d74f4dcae Mon Sep 17 00:00:00 2001 From: MrWaradana Date: Fri, 6 Mar 2026 15:58:55 +0700 Subject: [PATCH 13/14] refactor: Centralize `search` and `items_per_page` query parameters into `CommonParams` and remove redundant definitions from specific schema query classes. --- src/acquisition_cost/schema.py | 11 +------- src/equipment/schema.py | 12 +------- src/manpower_cost/schema.py | 12 +------- src/masterdata/schema.py | 28 ++----------------- src/masterdata_simulations/schema.py | 11 -------- src/models.py | 1 + src/plant_transaction_data/schema.py | 12 +------- .../schema.py | 11 -------- src/uploaded_file/schema.py | 12 +------- src/yeardata/schema.py | 12 +------- 10 files changed, 10 insertions(+), 112 deletions(-) diff --git a/src/acquisition_cost/schema.py b/src/acquisition_cost/schema.py index 23dbdd8..c455e26 100644 --- a/src/acquisition_cost/schema.py +++ b/src/acquisition_cost/schema.py @@ -34,13 +34,4 @@ class AcquisitionCostDataPagination(Pagination): class ListQueryParams(CommonParams): - items_per_page: Optional[int] = Field( - default=5, - ge=1, - le=1000, - description="Number of items per page" - ) - search: Optional[str] = Field( - default=None, - description="Search keyword" - ) \ No newline at end of file + pass \ No newline at end of file diff --git a/src/equipment/schema.py b/src/equipment/schema.py index 5df5fab..0d6b449 100644 --- a/src/equipment/schema.py +++ b/src/equipment/schema.py @@ -170,15 +170,5 @@ class CountRemainingLifeResponse(DefaultBase): critical: int class ListQueryParams(CommonParams): - items_per_page: Optional[int] = Field( - default=5, - ge=1, - le=1000, - description="Number of items per page", - alias="itemsPerPage" - ) - search: Optional[str] = Field( - default=None, - description="Search keyword" - ) + pass diff --git a/src/manpower_cost/schema.py b/src/manpower_cost/schema.py index 99ca8c3..cf420a5 100644 --- a/src/manpower_cost/schema.py +++ b/src/manpower_cost/schema.py @@ -34,14 +34,4 @@ class ManpowerCostPagination(Pagination): class QueryParams(CommonParams): - items_per_page: Optional[int] = Field( - default=5, - ge=1, - le=1000, - description="Number of items per page", - alias="itemsPerPage", - ) - search: Optional[str] = Field( - default=None, - description="Search keyword", - ) + pass diff --git a/src/masterdata/schema.py b/src/masterdata/schema.py index 3c77dba..43dea94 100644 --- a/src/masterdata/schema.py +++ b/src/masterdata/schema.py @@ -3,7 +3,7 @@ from typing import List, Optional from uuid import UUID from pydantic import BaseModel, Field, model_validator -from src.models import DefaultBase, Pagination +from src.models import CommonParams, DefaultBase, Pagination from src.auth.service import CurrentUser @@ -52,27 +52,5 @@ class MasterDataPagination(Pagination): items: List[MasterDataRead] = [] -class QueryParams(DefaultBase): - items_per_page: int = Field( - 5, - ge=1, - alias="items_per_page", - description="Items per page" - ) - itemsPerPage: Optional[int] = Field( - None, - ge=1, - description="Alias for items_per_page" - ) - search: Optional[str] = Field( - None, - description="Search keyword" - ) - - @model_validator(mode="before") - @classmethod - def resolve_aliases(cls, data: any) -> any: - if isinstance(data, dict): - if "itemsPerPage" in data and data["itemsPerPage"] is not None: - data.setdefault("items_per_page", data["itemsPerPage"]) - return data +class QueryParams(CommonParams): + pass diff --git a/src/masterdata_simulations/schema.py b/src/masterdata_simulations/schema.py index 8c0f048..561b7f2 100644 --- a/src/masterdata_simulations/schema.py +++ b/src/masterdata_simulations/schema.py @@ -45,14 +45,3 @@ class QueryParams(CommonParams): ..., description="Simulation identifier", ) - items_per_page: Optional[int] = Field( - default=5, - ge=1, - le=1000, - description="Items per page", - alias="itemsPerPage", - ) - search: Optional[str] = Field( - default=None, - description="Search keyword", - ) diff --git a/src/models.py b/src/models.py index b03adf8..00135bd 100644 --- a/src/models.py +++ b/src/models.py @@ -106,6 +106,7 @@ class CommonParams(DefaultBase): items_per_page: int = Field(5, gt=-2, lt=2147483647, alias="items_per_page") itemsPerPage: Optional[int] = Field(None, description="Alias for items_per_page") query_str: Optional[str] = Field(None, alias="q") + search: Optional[str] = Field(None, description="Search keyword") filter_spec: Optional[str] = Field(None, alias="filter") sort_by: List[str] = Field(default_factory=list, alias="sortBy[]") descending: List[bool] = Field(default_factory=list, alias="descending[]") diff --git a/src/plant_transaction_data/schema.py b/src/plant_transaction_data/schema.py index 9813a80..b31e11c 100644 --- a/src/plant_transaction_data/schema.py +++ b/src/plant_transaction_data/schema.py @@ -120,15 +120,5 @@ class PlantTransactionDataPagination(Pagination): class ListQueryParams(CommonParams): - items_per_page: Optional[int] = Field( - default=5, - ge=1, - le=1000, - description="Number of items per page", - alias="itemsPerPage", - ) - search: Optional[str] = Field( - default=None, - description="Search keyword", - ) + pass diff --git a/src/plant_transaction_data_simulations/schema.py b/src/plant_transaction_data_simulations/schema.py index 939ad68..0d8f13e 100644 --- a/src/plant_transaction_data_simulations/schema.py +++ b/src/plant_transaction_data_simulations/schema.py @@ -147,15 +147,4 @@ class ListQueryParams(CommonParams): ..., description="Simulation identifier", ) - items_per_page: Optional[int] = Field( - default=5, - ge=1, - le=1000, - description="Number of items per page", - alias="itemsPerPage", - ) - search: Optional[str] = Field( - default=None, - description="Search keyword", - ) diff --git a/src/uploaded_file/schema.py b/src/uploaded_file/schema.py index 1750ea7..ba32598 100644 --- a/src/uploaded_file/schema.py +++ b/src/uploaded_file/schema.py @@ -31,14 +31,4 @@ class UploadedFileDataPagination(Pagination): class ListQueryParams(CommonParams): - items_per_page: Optional[int] = Field( - default=5, - ge=1, - le=1000, - description="Number of items per page", - alias="itemsPerPage", - ) - search: Optional[str] = Field( - default=None, - description="Search keyword", - ) + pass diff --git a/src/yeardata/schema.py b/src/yeardata/schema.py index 778888d..cf2abc8 100644 --- a/src/yeardata/schema.py +++ b/src/yeardata/schema.py @@ -64,15 +64,5 @@ class YeardataPagination(Pagination): class ListQueryParams(CommonParams): - items_per_page: Optional[int] = Field( - default=5, - ge=1, - le=1000, - description="Number of items per page", - alias="itemsPerPage", - ) - search: Optional[str] = Field( - default=None, - description="Search keyword", - ) + pass From f9c8b0d749d10ad309c82cd1fc2dba6d6e188c9e Mon Sep 17 00:00:00 2001 From: MrWaradana Date: Fri, 6 Mar 2026 16:27:21 +0700 Subject: [PATCH 14/14] fix pydantic error list --- src/__pycache__/__init__.cpython-311.pyc | Bin 139 -> 143 bytes src/__pycache__/config.cpython-311.pyc | Bin 4817 -> 4821 bytes src/__pycache__/enums.cpython-311.pyc | Bin 1061 -> 1065 bytes src/__pycache__/models.cpython-311.pyc | Bin 6591 -> 7638 bytes 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 4486 -> 4490 bytes src/models.py | 6 +++--- 8 files changed, 3 insertions(+), 3 deletions(-) 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__/config.cpython-311.pyc b/src/__pycache__/config.cpython-311.pyc index 33be3f6f1032b51ecbc82526ea0ee54fad2f9c9f..e587d90b465622c50f6bd10a9df98be39cbc8f89 100644 GIT binary patch delta 53 zcmcbpdR3KgIWI340}%WWU6?tKXCvQE76}FYjQreG{lv`NeEpHmT$ZO DWjPQB diff --git a/src/__pycache__/enums.cpython-311.pyc b/src/__pycache__/enums.cpython-311.pyc index 8d88bbfe917939fd7341d0902b3cba778417b212..1c7f55dffce2941e40ae018e187ca20210650e2f 100644 GIT binary patch delta 52 zcmZ3=v66##IWI340}vd{<;hgv$Q#TgA+Mj2pPQ%IWI340}$BnT9;wBkvEu0MB3RZCMC5jCMi`nCpkGWrno3MW^)D8Mn(WZ CwGN;F diff --git a/src/__pycache__/models.cpython-311.pyc b/src/__pycache__/models.cpython-311.pyc index 0af1c154077d17bf089befe5caf0dbce1a4ea397..cda0557bce59e37d15713ad8b192d20dcf7008c0 100644 GIT binary patch delta 3557 zcma)8-A^0Y6`!%k24iCzunh(~@fQK7T}YF(5ea0s!Gt8;uoU8|akf~-a|upo?BtG} zlqibnDq3ZwJdoT-i&RxYs>IS=s#5K1rK(c@fbrJq(s_#d*7k+%wo;XcrsrG}z(Xq4 zzVn-V&-tBm?$?}qe|PFn=iHyPx3>xK``~Av7N*@#-94iIbYQZox-u?NkOkSMw5#rn zTlHi-syE|RJ2D+=XQq?u>`IsF%lLS`Me(bFOhE0ZAw@@k~ty@7GYA5osR_BMUJjp1>rMzgcj_Qkcm9q309+=;Q@ws-2!@e8ox}8 zYdS#FNg^b=E`b)FU2U9eyFlwBt$UuiGLzssKj;D^Vbb+;T{q}@(2hM8+Xs$W1Y(ne z>mo#eXE(wCXZ8ZKkF*jC8DPH^gW*9g4S_U#$aj+cLmU%r?1UxRVR+P%OqA^1oKlc; z~lcfwx4#!&_g?dn{~HD-#!La!=;sTFJ=!BXfRVoG9upyo{ECtydZ3O8fY}%1_M0gc|>6QTdgJU%|292p?>;DenVGr9v ze$xOhpq~ru*KOAu1GvE4&Y9$!x@j_;M8iq;ku%mXoMgXo{`ri_&$ch=;ku}y=%NNBy6)6?f{FoSI#zjGpvPT+3;kuxni+|OI0dr*{s>&EIOQJU$mzh z4zbN0e%n2I?j2}MCGF-pI*&HE0`xk9X{0w$c7tuY2OA^3!3_6Cy18#OizGY3`v}}m z4r+!~D^uEywU*iR`1r)^ES*P&Daahk=Gd2>A4DU(3Xq3q?}xte_w_RioAQoSEu+wY z57{bkux?l5eLTcO$ znmXhq9j5u~J;3kV3|37BonX+_G~&6Ak9>I4b+Bs$8bQdrAt}( z+U~n&Nsi_hBR?if_exYY>>6(y_Jx8{Ce&~)uunS!Efmu94kA%sJ zf6a!Sl5&L>^A@ICeedc({#YpES+e_kAeQ$|Z$W6@4|G}VxfX*JpkTA>Ih zUNKJ$NiO8eMw>>;yT>k^xI@Urtft{0BXEAg%kWU5m6W?An^UT=>O{LAI=D-Q_SIZl zM*AgzW#M0T!4+7Mf1av2qjhI=N01y7;g>P&N`959`Oz>&LHbdwm~!o_X}h ziLJ!e!}`gKHSc8IJE?mow>_aXN%th6Z1}#hq1Kb5dh*gPf6cumZT+ZzV!Y-})x9a* zo7(ntJ$CZLnjPz!i1OQm#gOJpC57IHPT_KEL|NFoAUx@ch*kEhuD)sn)gkja;Q%S0 zzec9V{M^X&d}47=(;p)6$;ORIDT0PjLAZ;6+n(M-c!cm11ndrVA)Em)s@7a_iGG09 z*4wlM-$i+eeu!$*v9W+M1Okr+mqeK9{r`*Dz*_%?v^Ay2#%r$do|&AVf<+k9)Mb_95BG+w;U;VY|P?|5_rUkRY^NI=@@ zWS{s?I3uqYB6*zs#UB&TGg}}!z?T$0tUF3h$uAOhY4_5JWDkNDp@UrtB&z%d=-#Ie zm8eR)Q11f(Cp15_dxyeXH4UTGi_%=Byf_3&`EDUkXaL0y6q8~_)f!h1dujd_$(I|~ z)e*G5fWYr?{025FoP{>O>Aa5`ejVYPt9iq{Y#oIJSy9M&`V4rO4YgI)-+g)TsxJIr d<@1&9e~V{AV*ieC7*@`Gh0=e%LB+r0{{mv;GAjT8 delta 2404 zcmZ`)O>7fK7~Qqk|MuFBW8<~s#Bm70QlU_2o03pOQf!34Lis6W8%Q1RB$)2nVRnt0 zqLLtSXsfCU3-XVVJ^SL>CoSXWz*zUa`M){vF^kipwPT>@r>eYO*PZMQP^UHoM zAP2Od9AunbZP7wmEuVs5ofOUMbH zvv65X@to%rFKS<~a@Vllh8j6vZCex8M}9+D`gnKw5E>-f$;**Y*;hBsXkBdN>0@iIe1_{Wwj1Xy57S1O+dEk8E>v?&+pY)2kbKqgc?8 zaiXj+0n<}Z^Sa)MY=!)G0GKRJ0$-LuNHw!|)a3L5b=bRt%DdKfWvF9Og8^Cse z7XaAML9)jc4mSz*0^LiBu2DM^>?J?AZVdO+XfEx<|End^1k+u>C~qKTd}79QSEiLl zyLrxgVTirtC->3%5PM0FXDIz&(oJ*AKw*Y_;YrmCGvtovch9Oqa?QJ;c|rT3Xg@K% zgY}~Qq|5j5nxb!fvF4&7C>kOUe6Q7uhRBQJXx|yhU54RW~q>K+7>aIyks*co^p)qiK+15IaVe{jb*t zIYtf!B+IuvnF_ozE5N@;UoNX+Q|yXkvH8_>A{TNfu9Yme;!zxmchT4Mdb?SkxRS>! z{zYq(O?Z$E*(Ew&{h+#m!>bB{>k7Q<3R<4#BcBplY;77qM1JyFFdCy|WE~kmeiS6N zU}Tok(RFkSrPoeQdO8+;FtK%W3$X>qAqTZ8Z74WtQ4;5c1=_#bSRlvAVTB*&((QDE zl{KwgI-JLOZC20GcejEuDjB0X!qic0d9|3=O>ZMQUZLUp;0zc{w)o`9li9Szv`iVM zMK$mUjor5a{6iT$Ok31=PSP8G8(@lpDHaW+>EqJ~k5A{PkjbAiU8gDt&y2&iA%BEA zL#DrBWaE?hf>FjZc#8O157{v?D!4?H$i7ZyY8F$ND3?|I4h+Pbf{xV572moON%ELH zh{R{X64H_87r+HGIsw48@C3Bj7KT;eBLF3UX#h;Y6u>310ex5`t^k|?I1K=^FrCx5 zOqXWN;P)T~JO1D*=R*(#fJ?{pNzSeM-Ana+-hD;5eyExnsHFy$QUh}b=Qm&8acN6c z7@%iO7$7at=c2dHR%5wZEVmTP&E@6?t3r;RH6cgNL^qP}qdm$M!YJpAw$oakhz;kT^_x@ld2?gwM1?yk()a(FU*fza<3@emD(?dsY+i}%GRXp zl9YX>lC28anvf+qsl&2tCpV>S9%j0*q;#76DW&+`BpB;VnQr}*npX=akT%o!Zou-v zb+I2HK#s;XZU+-G#b>Eo%UHuLz=wcw)34puLb)_qoWk%?#BJnSEHWDg&IKGQRW!YB zH}>+xNmMZEADCW9X12{Nm{}^bNM>%#vX}$0|75K-J$CkMWu>B`SMdi_1vb2Xk?#0q zE6?-yxI~pBgYkayb-Y8Ua{YJ7jd+sWj=OHw;=k}O`}yu=ZavKRJ%s3A|3R@0@-KBF B76||V 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 d361941f00a25842406cebf321c02b458c64fe86..496ac297fb23a7ed3965d71a5dd595d4fbc1ddcd 100644 GIT binary patch delta 58 zcmZou?o#Gk&dbZi00ezv3p2NJZRFd=B&(*Mk)NBYpO~4Oub-5vo0FWJs9#)^te;p~ MlA*u(F4Iar0O~suiU0rr delta 54 zcmeBDZd2x4&dbZi00fQcb2C}FH}Y*`l2mfGib+W=i%Ck=%}Gv9j43Wkj!7&n$%xtf IkZC0!0GUJ)HUIzs diff --git a/src/models.py b/src/models.py index 00135bd..456374b 100644 --- a/src/models.py +++ b/src/models.py @@ -108,9 +108,9 @@ class CommonParams(DefaultBase): query_str: Optional[str] = Field(None, alias="q") search: Optional[str] = Field(None, description="Search keyword") filter_spec: Optional[str] = Field(None, alias="filter") - sort_by: List[str] = Field(default_factory=list, alias="sortBy[]") - descending: List[bool] = Field(default_factory=list, alias="descending[]") - exclude: List[str] = Field(default_factory=list, alias="exclude[]") + sort_by: List[str] = Field(default=[], alias="sortBy[]") + descending: List[bool] = Field(default=[], alias="descending[]") + exclude: List[str] = Field(default=[], alias="exclude[]") all_params: int = Field(0, alias="all") @model_validator(mode="before")