From 094c139ee3f425223d2202f606270dc0e2e40f4c Mon Sep 17 00:00:00 2001 From: LocalT0aster <90502400+LocalT0aster@users.noreply.github.com> Date: Wed, 11 Feb 2026 14:48:31 +0000 Subject: [PATCH 1/9] feat: switch to poetry --- app_python/.flake8 | 4 + app_python/poetry.lock | 552 +++++++++++++++++++++++++++ app_python/pyproject.toml | 23 ++ app_python/requirements.txt | 12 - app_python/src/flask_instance.py | 16 + app_python/src/main.py | 24 ++ app_python/{app.py => src/router.py} | 73 ++-- 7 files changed, 641 insertions(+), 63 deletions(-) create mode 100644 app_python/.flake8 create mode 100644 app_python/poetry.lock create mode 100644 app_python/pyproject.toml delete mode 100644 app_python/requirements.txt create mode 100644 app_python/src/flask_instance.py create mode 100644 app_python/src/main.py rename app_python/{app.py => src/router.py} (70%) diff --git a/app_python/.flake8 b/app_python/.flake8 new file mode 100644 index 0000000000..63c477b455 --- /dev/null +++ b/app_python/.flake8 @@ -0,0 +1,4 @@ +[flake8] +max-line-length = 100 +max-complexity = 10 +exclude = .*,docs,*/__pycache__ diff --git a/app_python/poetry.lock b/app_python/poetry.lock new file mode 100644 index 0000000000..54b4217fd9 --- /dev/null +++ b/app_python/poetry.lock @@ -0,0 +1,552 @@ +# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. + +[[package]] +name = "blinker" +version = "1.9.0" +description = "Fast, simple object-to-object and broadcast signaling" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"}, + {file = "blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf"}, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c"}, + {file = "certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +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.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-win32.whl", hash = "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50"}, + {file = "charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f"}, + {file = "charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a"}, +] + +[[package]] +name = "click" +version = "8.3.1" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"}, + {file = "click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +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", "dev"] +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] +markers = {main = "platform_system == \"Windows\"", dev = "sys_platform == \"win32\""} + +[[package]] +name = "flake8" +version = "7.3.0" +description = "the modular source code checker: pep8 pyflakes and co" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e"}, + {file = "flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872"}, +] + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.14.0,<2.15.0" +pyflakes = ">=3.4.0,<3.5.0" + +[[package]] +name = "flask" +version = "3.1.2" +description = "A simple framework for building complex web applications." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c"}, + {file = "flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87"}, +] + +[package.dependencies] +blinker = ">=1.9.0" +click = ">=8.1.3" +itsdangerous = ">=2.2.0" +jinja2 = ">=3.1.2" +markupsafe = ">=2.1.1" +werkzeug = ">=3.1.0" + +[package.extras] +async = ["asgiref (>=3.2)"] +dotenv = ["python-dotenv"] + +[[package]] +name = "idna" +version = "3.11" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, + {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "iniconfig" +version = "2.3.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, + {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +description = "Safely pass data to untrusted environments and back." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, + {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "markupsafe" +version = "3.0.3" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"}, + {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1"}, + {file = "markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a"}, + {file = "markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b"}, + {file = "markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12"}, + {file = "markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe"}, + {file = "markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d"}, + {file = "markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"}, + {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"}, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "packaging" +version = "26.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"}, + {file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"}, +] + +[[package]] +name = "pep8-naming" +version = "0.15.1" +description = "Check PEP-8 naming conventions, plugin for flake8" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pep8_naming-0.15.1-py3-none-any.whl", hash = "sha256:eb63925e7fd9e028c7f7ee7b1e413ec03d1ee5de0e627012102ee0222c273c86"}, + {file = "pep8_naming-0.15.1.tar.gz", hash = "sha256:f6f4a499aba2deeda93c1f26ccc02f3da32b035c8b2db9696b730ef2c9639d29"}, +] + +[package.dependencies] +flake8 = ">=5.0.0" + +[[package]] +name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["coverage", "pytest", "pytest-benchmark"] + +[[package]] +name = "pycodestyle" +version = "2.14.0" +description = "Python style guide checker" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d"}, + {file = "pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783"}, +] + +[[package]] +name = "pyflakes" +version = "3.4.0" +description = "passive checker of Python programs" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f"}, + {file = "pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58"}, +] + +[[package]] +name = "pygments" +version = "2.19.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pytest" +version = "9.0.2" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b"}, + {file = "pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11"}, +] + +[package.dependencies] +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +iniconfig = ">=1.0.1" +packaging = ">=22" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "requests" +version = "2.32.5" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, + {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset_normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "urllib3" +version = "2.6.3" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, + {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, +] + +[package.extras] +brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] + +[[package]] +name = "werkzeug" +version = "3.1.5" +description = "The comprehensive WSGI web application library." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc"}, + {file = "werkzeug-3.1.5.tar.gz", hash = "sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67"}, +] + +[package.dependencies] +markupsafe = ">=2.1.1" + +[package.extras] +watchdog = ["watchdog (>=2.3)"] + +[metadata] +lock-version = "2.1" +python-versions = ">=3.13" +content-hash = "f3aa0b4f8b89c9bd078f27aa615d33e6ab1d6762210f9eab9131569294a31603" diff --git a/app_python/pyproject.toml b/app_python/pyproject.toml new file mode 100644 index 0000000000..39c9f3364d --- /dev/null +++ b/app_python/pyproject.toml @@ -0,0 +1,23 @@ +[tool.poetry] +name = "devops-info-service" +version = "0.1.0" +description = "" +authors = ["LocalT0aster"] +readme = "README.md" +packages = [{ include = "src" }] + +[tool.poetry.dependencies] +python = ">=3.13" +flask = ">=3.1.2,<4.0.0" +requests = ">=2.32.5,<3.0.0" + +[build-system] +requires = ["poetry-core>=2.0.0,<3.0.0"] +build-backend = "poetry.core.masonry.api" + +[dependency-groups] +dev = [ + "pytest (>=9.0.2,<10.0.0)", + "flake8 (>=7.3.0,<8.0.0)", + "pep8-naming (>=0.15.1,<0.16.0)" +] diff --git a/app_python/requirements.txt b/app_python/requirements.txt deleted file mode 100644 index 91714b4eb2..0000000000 --- a/app_python/requirements.txt +++ /dev/null @@ -1,12 +0,0 @@ -blinker==1.9.0 -certifi==2026.1.4 -charset-normalizer==3.4.4 -click==8.3.1 -Flask==3.1.2 -idna==3.11 -itsdangerous==2.2.0 -Jinja2==3.1.6 -MarkupSafe==3.0.3 -requests==2.32.5 -urllib3==2.6.3 -Werkzeug==3.1.5 diff --git a/app_python/src/flask_instance.py b/app_python/src/flask_instance.py new file mode 100644 index 0000000000..7f3267ba03 --- /dev/null +++ b/app_python/src/flask_instance.py @@ -0,0 +1,16 @@ +""" +Flask app instance and shared process-level state. +""" + +from datetime import datetime, timezone +import logging + +from flask import Flask + +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger(__name__) + +app = Flask("DevOps Info Service") +START_TIME = datetime.now(timezone.utc) # Application start time (UTC). diff --git a/app_python/src/main.py b/app_python/src/main.py new file mode 100644 index 0000000000..be666268ab --- /dev/null +++ b/app_python/src/main.py @@ -0,0 +1,24 @@ +""" +DevOps Info Service +Application runtime entrypoint. +""" + +import os + +from flask_instance import app, logger +import router # noqa: F401 +logger.info("b") + +HOST = os.getenv("HOST", "0.0.0.0") +PORT = int(os.getenv("PORT", 5000)) +DEBUG = os.getenv("DEBUG", "False").lower() == "true" + + +def run() -> None: + """Run development server.""" + logger.info("Application starting...") + app.run(host=HOST, port=PORT, debug=DEBUG) + + +if __name__ == "__main__": + run() diff --git a/app_python/app.py b/app_python/src/router.py similarity index 70% rename from app_python/app.py rename to app_python/src/router.py index ffce15ef58..e8d0404858 100644 --- a/app_python/app.py +++ b/app_python/src/router.py @@ -1,40 +1,21 @@ """ -DevOps Info Service -Main application module +Route handlers and response helpers. """ -__version__ = "1.0.0" - -# Basics -import os from datetime import datetime, timezone -import logging - -logging.basicConfig( - level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" -) -logger = logging.getLogger(__name__) - -# Metadata gathering +import inspect from multiprocessing import cpu_count import platform -import inspect - -# Web import socket -from flask import Flask, jsonify, request - -# Configuration -HOST = os.getenv("HOST", "0.0.0.0") -PORT = int(os.getenv("PORT", 5000)) -DEBUG = os.getenv("DEBUG", "False").lower() == "true" +from flask import jsonify, request +from flask_instance import START_TIME, app, logger -app = Flask(__name__) +__version__ = "1.0.0" def get_service_info() -> dict[str, str]: - """Collect info about service""" + """Collect info about service.""" return { "name": "devops-info-service", "version": __version__, @@ -44,11 +25,11 @@ def get_service_info() -> dict[str, str]: def get_platform_info() -> dict[str, str | int]: - """Collect system information""" + """Collect system information.""" def _platform_version() -> str: """Return a human-friendly OS version string.""" - match (platform.system().lower()): + match platform.system().lower(): case "linux": return platform.freedesktop_os_release()["PRETTY_NAME"] case "windows": @@ -66,7 +47,7 @@ def _platform_version() -> str: } -def get_uptime(): +def get_uptime() -> dict[str, str | int]: """Return uptime in seconds and a simple human string.""" delta = datetime.now(tz=timezone.utc) - START_TIME up_seconds = int(delta.total_seconds()) @@ -78,7 +59,7 @@ def get_uptime(): } -def get_runtime(): +def get_runtime() -> dict[str, str | int]: """Return current runtime metadata (uptime + UTC timestamp).""" up = get_uptime() return { @@ -89,13 +70,13 @@ def get_runtime(): } -def get_request_info(request): +def get_request_info(req) -> dict[str, str | None]: """Return basic request metadata for debugging/telemetry.""" return { - "client_ip": request.remote_addr, - "user_agent": request.headers.get("User-Agent"), - "method": request.method, - "path": request.path, + "client_ip": req.remote_addr, + "user_agent": req.headers.get("User-Agent"), + "method": req.method, + "path": req.path, } @@ -104,13 +85,11 @@ def list_routes() -> list[dict[str, str]]: out: list[dict[str, str]] = [] for rule in sorted(app.url_map.iter_rules(), key=lambda r: (r.rule, r.endpoint)): - # Skip Flask's built-in static handler if rule.endpoint == "static": continue view = app.view_functions.get(rule.endpoint) - # Description is pulled from docstring's brief (first line) desc = "" if view is not None: desc = inspect.getdoc(view) or "" @@ -129,8 +108,8 @@ def list_routes() -> list[dict[str, str]]: @app.route("/") def index(): - """Service information""" - logger.debug(f"Request: {request.method} {request.path}") + """Service information.""" + logger.debug("Request: %s %s", request.method, request.path) return jsonify( { "service": get_service_info(), @@ -144,8 +123,8 @@ def index(): @app.route("/health") def health(): - """Health check""" - logger.debug(f"Request: {request.method} {request.path}") + """Health check.""" + logger.debug("Request: %s %s", request.method, request.path) return jsonify( { "status": "healthy", @@ -156,14 +135,14 @@ def health(): @app.errorhandler(404) -def not_found(error): +def not_found(error): # noqa: ARG001 """Return a JSON 404 payload.""" - logger.debug(f"Request: {request.method} {request.path}") + logger.debug("Request: %s %s", request.method, request.path) return jsonify({"error": "Not Found", "message": "Endpoint does not exist"}), 404 @app.errorhandler(500) -def internal_error(error): +def internal_error(error): # noqa: ARG001 """Return a JSON 500 payload.""" return ( jsonify( @@ -174,11 +153,3 @@ def internal_error(error): ), 500, ) - - -START_TIME = datetime.now(timezone.utc) # Application start time (UTC). -logger.info("Application starting...") - -# TODO use WSGI in production. -if __name__ == "__main__": - app.run(host=HOST, port=PORT, debug=DEBUG) From bcb29cea364a1d871461bc5e3d07ab1e4250de95 Mon Sep 17 00:00:00 2001 From: LocalT0aster <90502400+LocalT0aster@users.noreply.github.com> Date: Wed, 11 Feb 2026 16:37:03 +0000 Subject: [PATCH 2/9] feat: add gunicorn as prod. ready server --- app_python/poetry.lock | 27 +++++++++++++++++++++++++-- app_python/pyproject.toml | 1 + app_python/src/main.py | 9 ++++++--- app_python/src/router.py | 6 +++++- 4 files changed, 37 insertions(+), 6 deletions(-) diff --git a/app_python/poetry.lock b/app_python/poetry.lock index 54b4217fd9..ff50722257 100644 --- a/app_python/poetry.lock +++ b/app_python/poetry.lock @@ -216,6 +216,29 @@ werkzeug = ">=3.1.0" async = ["asgiref (>=3.2)"] dotenv = ["python-dotenv"] +[[package]] +name = "gunicorn" +version = "25.0.3" +description = "WSGI HTTP Server for UNIX" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "gunicorn-25.0.3-py3-none-any.whl", hash = "sha256:aca364c096c81ca11acd4cede0aaeea91ba76ca74e2c0d7f879154db9d890f35"}, + {file = "gunicorn-25.0.3.tar.gz", hash = "sha256:b53a7fff1a07b825b962af320554de44ae77a26abfa373711ff3f83d57d3506d"}, +] + +[package.dependencies] +packaging = "*" + +[package.extras] +eventlet = ["eventlet (>=0.40.3)"] +gevent = ["gevent (>=24.10.1)"] +http2 = ["h2 (>=4.1.0)"] +setproctitle = ["setproctitle"] +testing = ["coverage", "eventlet (>=0.40.3)", "gevent (>=24.10.1)", "h2 (>=4.1.0)", "httpx[http2]", "pytest", "pytest-asyncio", "pytest-cov", "uvloop (>=0.19.0)"] +tornado = ["tornado (>=6.5.0)"] + [[package]] name = "idna" version = "3.11" @@ -390,7 +413,7 @@ version = "26.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"}, {file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"}, @@ -549,4 +572,4 @@ watchdog = ["watchdog (>=2.3)"] [metadata] lock-version = "2.1" python-versions = ">=3.13" -content-hash = "f3aa0b4f8b89c9bd078f27aa615d33e6ab1d6762210f9eab9131569294a31603" +content-hash = "7b72c24d1c07f9d14e6432a09c4eef233ab58ea34077eca255e1394fb0cc6b3c" diff --git a/app_python/pyproject.toml b/app_python/pyproject.toml index 39c9f3364d..e1cbb72954 100644 --- a/app_python/pyproject.toml +++ b/app_python/pyproject.toml @@ -10,6 +10,7 @@ packages = [{ include = "src" }] python = ">=3.13" flask = ">=3.1.2,<4.0.0" requests = ">=2.32.5,<3.0.0" +gunicorn = "^25.0.3" [build-system] requires = ["poetry-core>=2.0.0,<3.0.0"] diff --git a/app_python/src/main.py b/app_python/src/main.py index be666268ab..cc959d4262 100644 --- a/app_python/src/main.py +++ b/app_python/src/main.py @@ -5,9 +5,12 @@ import os -from flask_instance import app, logger -import router # noqa: F401 -logger.info("b") +try: + from .flask_instance import app, logger + from . import router # noqa: F401 +except ImportError: # pragma: no cover - allows `python src/main.py` + from flask_instance import app, logger + import router # noqa: F401 HOST = os.getenv("HOST", "0.0.0.0") PORT = int(os.getenv("PORT", 5000)) diff --git a/app_python/src/router.py b/app_python/src/router.py index e8d0404858..994b1340e4 100644 --- a/app_python/src/router.py +++ b/app_python/src/router.py @@ -9,7 +9,11 @@ import socket from flask import jsonify, request -from flask_instance import START_TIME, app, logger + +try: + from .flask_instance import START_TIME, app, logger +except ImportError: # pragma: no cover - allows `python src/main.py` + from flask_instance import START_TIME, app, logger __version__ = "1.0.0" From 98fb1ec4883fbc0319055dcb0c7cb6c4477a2e61 Mon Sep 17 00:00:00 2001 From: LocalT0aster <90502400+LocalT0aster@users.noreply.github.com> Date: Thu, 12 Feb 2026 18:55:28 +0300 Subject: [PATCH 3/9] feat: actions --- .github/workflows/app_python.yml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/app_python.yml diff --git a/.github/workflows/app_python.yml b/.github/workflows/app_python.yml new file mode 100644 index 0000000000..54c6f0d88e --- /dev/null +++ b/.github/workflows/app_python.yml @@ -0,0 +1,32 @@ +on: + push: + paths: + - app_python/** + - .github/workflows/app_python.yml + +jobs: + test: + strategy: + fail-fast: false + matrix: + python-version: [3.14] + poetry-version: [2.3.2] + os: [ubuntu-latest] + runs-on: ${{ matrix.os }} + defaults: + run: + working-directory: ./app_python + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - uses: snok/install-poetry@v1 + with: + version: ${{ matrix.poetry-version }} + - name: Install dependencies + run: poetry install + - name: Lint with flake8 + run: poetry run flake8 src tests + - name: Test using pytest + run: poetry run pytest From 761ff53279ac2c318f11580d1aeb69445cba9937 Mon Sep 17 00:00:00 2001 From: LocalT0aster <90502400+LocalT0aster@users.noreply.github.com> Date: Thu, 12 Feb 2026 20:54:37 +0300 Subject: [PATCH 4/9] Testing --- .github/workflows/app_python.yml | 32 ------ .github/workflows/python-ci.yml | 54 ++++++++++ app_python/.dockerignore | 6 +- app_python/Dockerfile | 22 ++-- app_python/README.md | 42 ++++++-- app_python/docs/LAB03.md | 87 ++++++++++++++++ app_python/poetry.lock | 141 +++++++++++++++++++++++++- app_python/pyproject.toml | 1 + app_python/src/main.py | 2 +- app_python/tests/conftest.py | 14 +++ app_python/tests/test_endpoints.py | 118 +++++++++++++++++++++ app_python/tests/test_unit_helpers.py | 93 +++++++++++++++++ 12 files changed, 562 insertions(+), 50 deletions(-) delete mode 100644 .github/workflows/app_python.yml create mode 100644 .github/workflows/python-ci.yml create mode 100644 app_python/docs/LAB03.md create mode 100644 app_python/tests/conftest.py create mode 100644 app_python/tests/test_endpoints.py create mode 100644 app_python/tests/test_unit_helpers.py diff --git a/.github/workflows/app_python.yml b/.github/workflows/app_python.yml deleted file mode 100644 index 54c6f0d88e..0000000000 --- a/.github/workflows/app_python.yml +++ /dev/null @@ -1,32 +0,0 @@ -on: - push: - paths: - - app_python/** - - .github/workflows/app_python.yml - -jobs: - test: - strategy: - fail-fast: false - matrix: - python-version: [3.14] - poetry-version: [2.3.2] - os: [ubuntu-latest] - runs-on: ${{ matrix.os }} - defaults: - run: - working-directory: ./app_python - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - uses: snok/install-poetry@v1 - with: - version: ${{ matrix.poetry-version }} - - name: Install dependencies - run: poetry install - - name: Lint with flake8 - run: poetry run flake8 src tests - - name: Test using pytest - run: poetry run pytest diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml new file mode 100644 index 0000000000..6fd5f93ba3 --- /dev/null +++ b/.github/workflows/python-ci.yml @@ -0,0 +1,54 @@ +name: Python CI + +on: + push: + paths: + - app_python/** + - .github/workflows/python-ci.yml + pull_request: + paths: + - app_python/** + - .github/workflows/python-ci.yml + +jobs: + test: + strategy: + fail-fast: false + matrix: + python-version: [3.14] + poetry-version: [2.3.2] + os: [ubuntu-latest] + runs-on: ${{ matrix.os }} + defaults: + run: + working-directory: ./app_python + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + cache: poetry + cache-dependency-path: app_python/poetry.lock + - uses: snok/install-poetry@v1 + with: + version: ${{ matrix.poetry-version }} + - name: Install dependencies + run: poetry install + - name: Lint with flake8 + run: poetry run flake8 src tests + - name: Test using pytest with coverage report + run: | + mkdir -p test-results + poetry run pytest \ + --junitxml=test-results/pytest-report.xml \ + --cov=src \ + --cov-report=term-missing \ + --cov-report=xml:test-results/coverage.xml + - name: Upload pytest and coverage reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: python-test-reports + path: | + app_python/test-results/pytest-report.xml + app_python/test-results/coverage.xml diff --git a/app_python/.dockerignore b/app_python/.dockerignore index 2944c29a24..511a810855 100644 --- a/app_python/.dockerignore +++ b/app_python/.dockerignore @@ -1,4 +1,4 @@ * -!app.py -!requirements.txt -!tests/* +!src/** +!pyproject.toml +!poetry.lock diff --git a/app_python/Dockerfile b/app_python/Dockerfile index 5b1a868024..e9b02ed024 100644 --- a/app_python/Dockerfile +++ b/app_python/Dockerfile @@ -1,13 +1,23 @@ FROM python:3.14-alpine -RUN addgroup appgroup && adduser --disabled-password --gecos "" --no-create-home -s /bin/sh appuser -G appgroup + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 +ENV POETRY_VERSION=2.3.2 + +RUN pip install --no-cache-dir "poetry==$POETRY_VERSION" \ + && addgroup appgroup \ + && adduser --disabled-password --gecos "" --no-create-home -s /bin/sh appuser -G appgroup + WORKDIR /app -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt -COPY . . -RUN rm requirements.txt + +COPY pyproject.toml poetry.lock ./ +RUN poetry config virtualenvs.create false \ + && poetry install --only main --no-interaction --no-ansi --no-root + +COPY src ./src ENV PORT=5000 ENV HOST="0.0.0.0" USER appuser -CMD ["python", "app.py"] +CMD ["sh", "-c", "gunicorn --bind ${HOST:-0.0.0.0}:${PORT:-5000} src.flask_instance:app"] diff --git a/app_python/README.md b/app_python/README.md index 4258b0b0ee..7fc47993f7 100644 --- a/app_python/README.md +++ b/app_python/README.md @@ -1,20 +1,20 @@ # DevOps Info Service +[![Python CI](https://github.com/LocalT0aster/DevOps-Core-S25/actions/workflows/python-ci.yml/badge.svg)](https://github.com/LocalT0aster/DevOps-Core-S25/actions/workflows/python-ci.yml) + ## Overview Small Flask web service that reports service metadata, system information, runtime uptime, and basic request details. Includes a simple health check endpoint for monitoring. ## Prerequisites -- Python 3.14 -- Dependencies from `requirements.txt` +- Python 3.13+ +- Poetry ## Installation ```bash -python -m venv .venv -source .venv/bin/activate -pip install -r requirements.txt +poetry install ``` ### Docker @@ -27,13 +27,19 @@ pip install -r requirements.txt ```bash docker build -t localt0aster/devops-app-py . ``` + The Docker build installs dependencies with: + ```bash + poetry install --only main --no-root + ``` ## Running the Application +Production-style local run with Gunicorn: + ```bash -python app.py +poetry run gunicorn --bind 0.0.0.0:5000 src.flask_instance:app # Or with custom config -PORT=8080 HOST=127.0.0.1 python app.py +HOST=127.0.0.1 PORT=8080 poetry run gunicorn --bind 127.0.0.1:8080 src.flask_instance:app ``` ### Docker @@ -55,3 +61,25 @@ PORT=8080 HOST=127.0.0.1 python app.py | `HOST` | `0.0.0.0` | Bind address for the server | | `PORT` | `5000` | Port to listen on | | `DEBUG` | `False` | Enable Flask debug mode (`true`/`false`) | + +## Testing + +The project uses `pytest` for unit tests. + +```bash +poetry install --with dev +poetry run pytest --cov=src --cov-report=term-missing +``` + +## Linting + +```bash +poetry run flake8 src tests +``` + +Current test coverage includes: + +- `GET /` successful response schema and types +- `GET /health` successful response schema and types +- `404` JSON error handling for unknown routes +- `500` JSON error handling for simulated internal failures diff --git a/app_python/docs/LAB03.md b/app_python/docs/LAB03.md new file mode 100644 index 0000000000..a7148f0c24 --- /dev/null +++ b/app_python/docs/LAB03.md @@ -0,0 +1,87 @@ +# LAB03 - Continuous Integration (Python) + +## 1. Overview + +**Testing framework used:** `pytest` + +**Why this choice:** + +- concise assertions and clear failure output +- fixtures simplify Flask test-client setup +- `monkeypatch` enables controlled error-path testing + +**What is covered by tests:** + +- endpoint tests for `GET /` and `GET /health` (success + error behavior) +- JSON schema/type assertions +- helper/unit tests for runtime/platform/request metadata +- entrypoint behavior test for `main.run()` argument wiring + +**Current CI trigger configuration:** + +- workflow file: `.github/workflows/app_python.yml` +- trigger: `push` with path filters for `app_python/**` and workflow file changes + +**Versioning strategy (SemVer/CalVer):** + +- `TODO`: not documented/implemented yet for Docker image tagging in CI + +## 2. Workflow Evidence + +Provide links/terminal output for: + +- Tests passing locally (terminal output below) +- Successful workflow run link (GitHub Actions): `TODO` +- Docker image on Docker Hub (link): `TODO` +- Status badge in `app_python/README.md`: + - + +```text +$ poetry run pytest --cov=src --cov-report=term-missing +========================= test session starts ========================= +platform linux -- Python 3.14.2, pytest-9.0.2, pluggy-1.6.0 +rootdir: /home/t0ast/Repos/DevOps-Core-S26/app_python +configfile: pyproject.toml +plugins: anyio-4.12.1, mock-3.15.1, cov-7.0.0 +collected 10 items + +tests/test_endpoints.py ..... [ 50%] +tests/test_unit_helpers.py ..... [100%] + +=========================== tests coverage ============================ +___________ coverage: platform linux, python 3.14.2-final-0 ___________ + +Name Stmts Miss Cover Missing +----------------------------------------------------- +src/flask_instance.py 7 0 100% +src/main.py 10 0 100% +src/router.py 60 0 100% +----------------------------------------------------- +TOTAL 77 0 100% +========================= 10 passed in 0.06s ========================== +``` + +Coverage note: + +- The output above was captured before excluding launcher-only code. +- `src/main.py` line 27 (`if __name__ == "__main__":`) is now marked with `# pragma: no cover`. + +## 3. Best Practices Implemented + +- **Practice 1: Path-based trigger filtering**: avoids running Python CI when unrelated folders change. +- **Practice 2: Lint + test stages in CI**: catches style and functional issues early. +- **Practice 3: Coverage reporting in CI command**: makes test quality visible, not just pass/fail. +- **Caching**: `actions/setup-python` Poetry cache enabled with lockfile-based invalidation. +- **Snyk**: `TODO` (not integrated/documented yet). + +## 4. Key Decisions + +- **Versioning Strategy:** `TODO` (SemVer or CalVer not yet implemented for Docker tags). +- **Docker Tags:** `TODO` (version tags + `latest` not yet shown). +- **Workflow Triggers:** path-filtered `push` trigger to reduce unnecessary runs in a monorepo. +- **Test Coverage:** endpoint and helper logic are covered; launcher-only code is excluded with pragma. + +## 5. Challenges (Optional) + +- Moving from endpoint-only tests to helper-level unit tests increased meaningful coverage. +- Local and CI environments may have different tool availability; Poetry-based commands are used for reproducibility. diff --git a/app_python/poetry.lock b/app_python/poetry.lock index ff50722257..e4918ed237 100644 --- a/app_python/poetry.lock +++ b/app_python/poetry.lock @@ -175,6 +175,125 @@ files = [ ] markers = {main = "platform_system == \"Windows\"", dev = "sys_platform == \"win32\""} +[[package]] +name = "coverage" +version = "7.13.4" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "coverage-7.13.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fc31c787a84f8cd6027eba44010517020e0d18487064cd3d8968941856d1415"}, + {file = "coverage-7.13.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a32ebc02a1805adf637fc8dec324b5cdacd2e493515424f70ee33799573d661b"}, + {file = "coverage-7.13.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e24f9156097ff9dc286f2f913df3a7f63c0e333dcafa3c196f2c18b4175ca09a"}, + {file = "coverage-7.13.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8041b6c5bfdc03257666e9881d33b1abc88daccaf73f7b6340fb7946655cd10f"}, + {file = "coverage-7.13.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a09cfa6a5862bc2fc6ca7c3def5b2926194a56b8ab78ffcf617d28911123012"}, + {file = "coverage-7.13.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:296f8b0af861d3970c2a4d8c91d48eb4dd4771bcef9baedec6a9b515d7de3def"}, + {file = "coverage-7.13.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e101609bcbbfb04605ea1027b10dc3735c094d12d40826a60f897b98b1c30256"}, + {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aa3feb8db2e87ff5e6d00d7e1480ae241876286691265657b500886c98f38bda"}, + {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4fc7fa81bbaf5a02801b65346c8b3e657f1d93763e58c0abdf7c992addd81a92"}, + {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:33901f604424145c6e9c2398684b92e176c0b12df77d52db81c20abd48c3794c"}, + {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:bb28c0f2cf2782508a40cec377935829d5fcc3ad9a3681375af4e84eb34b6b58"}, + {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d107aff57a83222ddbd8d9ee705ede2af2cc926608b57abed8ef96b50b7e8f9"}, + {file = "coverage-7.13.4-cp310-cp310-win32.whl", hash = "sha256:a6f94a7d00eb18f1b6d403c91a88fd58cfc92d4b16080dfdb774afc8294469bf"}, + {file = "coverage-7.13.4-cp310-cp310-win_amd64.whl", hash = "sha256:2cb0f1e000ebc419632bbe04366a8990b6e32c4e0b51543a6484ffe15eaeda95"}, + {file = "coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053"}, + {file = "coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11"}, + {file = "coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa"}, + {file = "coverage-7.13.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7"}, + {file = "coverage-7.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00"}, + {file = "coverage-7.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef"}, + {file = "coverage-7.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903"}, + {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f"}, + {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299"}, + {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505"}, + {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6"}, + {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9"}, + {file = "coverage-7.13.4-cp311-cp311-win32.whl", hash = "sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9"}, + {file = "coverage-7.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f"}, + {file = "coverage-7.13.4-cp311-cp311-win_arm64.whl", hash = "sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f"}, + {file = "coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459"}, + {file = "coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3"}, + {file = "coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634"}, + {file = "coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3"}, + {file = "coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa"}, + {file = "coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3"}, + {file = "coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a"}, + {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7"}, + {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc"}, + {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47"}, + {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985"}, + {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0"}, + {file = "coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246"}, + {file = "coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126"}, + {file = "coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d"}, + {file = "coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9"}, + {file = "coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac"}, + {file = "coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea"}, + {file = "coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b"}, + {file = "coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525"}, + {file = "coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242"}, + {file = "coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148"}, + {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a"}, + {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23"}, + {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80"}, + {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea"}, + {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a"}, + {file = "coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d"}, + {file = "coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd"}, + {file = "coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af"}, + {file = "coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d"}, + {file = "coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12"}, + {file = "coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b"}, + {file = "coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9"}, + {file = "coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092"}, + {file = "coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9"}, + {file = "coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26"}, + {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2"}, + {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940"}, + {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c"}, + {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0"}, + {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b"}, + {file = "coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9"}, + {file = "coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd"}, + {file = "coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997"}, + {file = "coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601"}, + {file = "coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689"}, + {file = "coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c"}, + {file = "coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129"}, + {file = "coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552"}, + {file = "coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a"}, + {file = "coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356"}, + {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71"}, + {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5"}, + {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98"}, + {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5"}, + {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0"}, + {file = "coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb"}, + {file = "coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505"}, + {file = "coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2"}, + {file = "coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056"}, + {file = "coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc"}, + {file = "coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9"}, + {file = "coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf"}, + {file = "coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55"}, + {file = "coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72"}, + {file = "coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a"}, + {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6"}, + {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3"}, + {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750"}, + {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39"}, + {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0"}, + {file = "coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea"}, + {file = "coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932"}, + {file = "coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b"}, + {file = "coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0"}, + {file = "coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91"}, +] + +[package.extras] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] + [[package]] name = "flake8" version = "7.3.0" @@ -511,6 +630,26 @@ pygments = ">=2.7.2" [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pytest-cov" +version = "7.0.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861"}, + {file = "pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1"}, +] + +[package.dependencies] +coverage = {version = ">=7.10.6", extras = ["toml"]} +pluggy = ">=1.2" +pytest = ">=7" + +[package.extras] +testing = ["process-tests", "pytest-xdist", "virtualenv"] + [[package]] name = "requests" version = "2.32.5" @@ -572,4 +711,4 @@ watchdog = ["watchdog (>=2.3)"] [metadata] lock-version = "2.1" python-versions = ">=3.13" -content-hash = "7b72c24d1c07f9d14e6432a09c4eef233ab58ea34077eca255e1394fb0cc6b3c" +content-hash = "5bcb333e951818ca4706d50bae307ab22a95462b6e393691b1a6d0992e4ffc41" diff --git a/app_python/pyproject.toml b/app_python/pyproject.toml index e1cbb72954..1e8b7fba15 100644 --- a/app_python/pyproject.toml +++ b/app_python/pyproject.toml @@ -19,6 +19,7 @@ build-backend = "poetry.core.masonry.api" [dependency-groups] dev = [ "pytest (>=9.0.2,<10.0.0)", + "pytest-cov (>=7.0.0,<8.0.0)", "flake8 (>=7.3.0,<8.0.0)", "pep8-naming (>=0.15.1,<0.16.0)" ] diff --git a/app_python/src/main.py b/app_python/src/main.py index cc959d4262..d2d7356067 100644 --- a/app_python/src/main.py +++ b/app_python/src/main.py @@ -23,5 +23,5 @@ def run() -> None: app.run(host=HOST, port=PORT, debug=DEBUG) -if __name__ == "__main__": +if __name__ == "__main__": # pragma: no cover run() diff --git a/app_python/tests/conftest.py b/app_python/tests/conftest.py new file mode 100644 index 0000000000..eef52e6a63 --- /dev/null +++ b/app_python/tests/conftest.py @@ -0,0 +1,14 @@ +"""Shared pytest fixtures for app endpoint tests.""" + +import pytest + +from src.flask_instance import app +import src.router # noqa: F401 # Ensure route decorators are loaded. + + +@pytest.fixture() +def client(): + """Return a Flask test client without starting a real HTTP server.""" + app.config.update(TESTING=True, PROPAGATE_EXCEPTIONS=False) + with app.test_client() as test_client: + yield test_client diff --git a/app_python/tests/test_endpoints.py b/app_python/tests/test_endpoints.py new file mode 100644 index 0000000000..d0aca40338 --- /dev/null +++ b/app_python/tests/test_endpoints.py @@ -0,0 +1,118 @@ +"""Unit tests for HTTP endpoints and error handling.""" + +from datetime import datetime + +import src.router as router + + +def _raise_runtime_error() -> None: + raise RuntimeError("simulated failure") + + +def test_index_returns_expected_json_structure_and_types(client): + """GET / should return the expected nested schema with stable field types.""" + response = client.get( + "/", + headers={"User-Agent": "pytest-suite/1.0"}, + environ_overrides={"REMOTE_ADDR": "203.0.113.7"}, + ) + + assert response.status_code == 200 + payload = response.get_json() + assert payload is not None + + assert {"service", "system", "runtime", "request", "endpoints"} <= payload.keys() + + service = payload["service"] + assert service["name"] == "devops-info-service" + assert service["framework"] == "Flask" + assert isinstance(service["version"], str) + assert isinstance(service["description"], str) + + system = payload["system"] + assert isinstance(system["hostname"], str) + assert system["hostname"] + assert isinstance(system["platform"], str) + assert isinstance(system["platform_version"], str) + assert isinstance(system["architecture"], str) + assert isinstance(system["cpu_count"], int) + assert system["cpu_count"] >= 1 + assert isinstance(system["python_version"], str) + + runtime = payload["runtime"] + assert isinstance(runtime["seconds"], int) + assert runtime["seconds"] >= 0 + assert isinstance(runtime["human"], str) + + request = payload["request"] + assert request["client_ip"] == "203.0.113.7" + assert request["user_agent"] == "pytest-suite/1.0" + assert request["method"] == "GET" + assert request["path"] == "/" + + endpoints = payload["endpoints"] + assert isinstance(endpoints, list) + assert endpoints + for endpoint in endpoints: + assert {"path", "method", "description"} <= endpoint.keys() + assert isinstance(endpoint["path"], str) + assert isinstance(endpoint["method"], str) + assert isinstance(endpoint["description"], str) + + route_index = {(endpoint["method"], endpoint["path"]) for endpoint in endpoints} + assert ("GET", "/") in route_index + assert ("GET", "/health") in route_index + + +def test_health_returns_expected_json_structure_and_types(client): + """GET /health should report healthy status and typed runtime metadata.""" + response = client.get("/health") + + assert response.status_code == 200 + payload = response.get_json() + assert payload is not None + + assert {"status", "timestamp", "uptime_seconds"} <= payload.keys() + assert payload["status"] == "healthy" + assert isinstance(payload["uptime_seconds"], int) + assert payload["uptime_seconds"] >= 0 + + parsed_timestamp = datetime.fromisoformat(payload["timestamp"]) + assert parsed_timestamp.tzinfo is not None + + +def test_unknown_endpoint_returns_json_404(client): + """Unknown routes should be handled by JSON 404 error handler.""" + response = client.get("/definitely-does-not-exist") + + assert response.status_code == 404 + assert response.get_json() == { + "error": "Not Found", + "message": "Endpoint does not exist", + } + + +def test_index_returns_json_500_when_platform_probe_fails(client, monkeypatch): + """GET / should return JSON 500 when an internal helper crashes.""" + monkeypatch.setattr(router, "get_platform_info", _raise_runtime_error) + + response = client.get("/") + + assert response.status_code == 500 + assert response.get_json() == { + "error": "Internal Server Error", + "message": "An unexpected error occurred", + } + + +def test_health_returns_json_500_when_uptime_probe_fails(client, monkeypatch): + """GET /health should return JSON 500 when uptime collection crashes.""" + monkeypatch.setattr(router, "get_uptime", _raise_runtime_error) + + response = client.get("/health") + + assert response.status_code == 500 + assert response.get_json() == { + "error": "Internal Server Error", + "message": "An unexpected error occurred", + } diff --git a/app_python/tests/test_unit_helpers.py b/app_python/tests/test_unit_helpers.py new file mode 100644 index 0000000000..a17f071a61 --- /dev/null +++ b/app_python/tests/test_unit_helpers.py @@ -0,0 +1,93 @@ +"""Unit tests for helper functions and app entrypoint behavior.""" + +from datetime import datetime +from unittest.mock import Mock + +from flask import request + +from src.flask_instance import app +import src.main as main +import src.router as router + + +def test_run_calls_flask_app_with_configured_host_port_debug(monkeypatch): + """main.run should log startup and pass module config into app.run.""" + run_mock = Mock() + info_mock = Mock() + + monkeypatch.setattr(main, "HOST", "127.0.0.1") + monkeypatch.setattr(main, "PORT", 5050) + monkeypatch.setattr(main, "DEBUG", True) + monkeypatch.setattr(main.app, "run", run_mock) + monkeypatch.setattr(main.logger, "info", info_mock) + + main.run() + + info_mock.assert_called_once_with("Application starting...") + run_mock.assert_called_once_with(host="127.0.0.1", port=5050, debug=True) + + +def test_get_runtime_maps_uptime_payload(monkeypatch): + """get_runtime should map uptime fields and produce UTC timestamp text.""" + monkeypatch.setattr( + router, + "get_uptime", + lambda: {"seconds": 42, "human": "0 hours, 0 minutes"}, + ) + + runtime = router.get_runtime() + + assert runtime["uptime_seconds"] == 42 + assert runtime["uptime_human"] == "0 hours, 0 minutes" + assert runtime["timezone"] == "UTC" + assert runtime["current_time"].endswith("Z") + datetime.strptime(runtime["current_time"], "%Y-%m-%dT%H:%M:%SZ") + + +def test_get_platform_info_windows_platform_version_branch(monkeypatch): + """Windows branch should format platform_version from win32 metadata.""" + monkeypatch.setattr(router.platform, "system", lambda: "Windows") + monkeypatch.setattr(router.platform, "win32_ver", lambda: ("", "11", "", "")) + monkeypatch.setattr(router.platform, "machine", lambda: "AMD64") + monkeypatch.setattr(router.platform, "python_version", lambda: "3.14.2") + monkeypatch.setattr(router.socket, "gethostname", lambda: "test-host") + monkeypatch.setattr(router, "cpu_count", lambda: 8) + + payload = router.get_platform_info() + + assert payload["platform"] == "Windows" + assert payload["platform_version"] == "Windows 11" + assert payload["hostname"] == "test-host" + assert payload["cpu_count"] == 8 + + +def test_get_platform_info_default_platform_version_branch(monkeypatch): + """Non-Linux and non-Windows branch should use platform.version().""" + monkeypatch.setattr(router.platform, "system", lambda: "Darwin") + monkeypatch.setattr(router.platform, "version", lambda: "Darwin Kernel 25.0") + monkeypatch.setattr(router.platform, "machine", lambda: "arm64") + monkeypatch.setattr(router.platform, "python_version", lambda: "3.14.2") + monkeypatch.setattr(router.socket, "gethostname", lambda: "mac-host") + monkeypatch.setattr(router, "cpu_count", lambda: 10) + + payload = router.get_platform_info() + + assert payload["platform"] == "Darwin" + assert payload["platform_version"] == "Darwin Kernel 25.0" + + +def test_get_request_info_returns_none_when_user_agent_missing(): + """Missing User-Agent header should map to None without crashing.""" + with app.test_request_context( + "/diagnostic", + method="POST", + environ_base={"REMOTE_ADDR": "198.51.100.9"}, + ): + info = router.get_request_info(request) + + assert info == { + "client_ip": "198.51.100.9", + "user_agent": None, + "method": "POST", + "path": "/diagnostic", + } From 781b786037bbadfec3378b8103b7999d0ac5e164 Mon Sep 17 00:00:00 2001 From: LocalT0aster <90502400+LocalT0aster@users.noreply.github.com> Date: Thu, 12 Feb 2026 21:02:04 +0300 Subject: [PATCH 5/9] fix: the actions --- .github/workflows/python-ci.yml | 15 ++++++++++++--- app_python/docs/LAB03.md | 4 ++-- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index 6fd5f93ba3..57ca288c85 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -27,13 +27,22 @@ jobs: - uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - cache: poetry - cache-dependency-path: app_python/poetry.lock - uses: snok/install-poetry@v1 with: version: ${{ matrix.poetry-version }} + - name: Configure Poetry virtualenv location + run: poetry config virtualenvs.in-project true + - name: Cache Poetry dependencies + uses: actions/cache@v4 + with: + path: | + ~/.cache/pypoetry + app_python/.venv + key: ${{ runner.os }}-py${{ matrix.python-version }}-poetry${{ matrix.poetry-version }}-${{ hashFiles('app_python/poetry.lock') }} + restore-keys: | + ${{ runner.os }}-py${{ matrix.python-version }}-poetry${{ matrix.poetry-version }}- - name: Install dependencies - run: poetry install + run: poetry install --with dev --no-interaction --no-ansi - name: Lint with flake8 run: poetry run flake8 src tests - name: Test using pytest with coverage report diff --git a/app_python/docs/LAB03.md b/app_python/docs/LAB03.md index a7148f0c24..a9ab50b39b 100644 --- a/app_python/docs/LAB03.md +++ b/app_python/docs/LAB03.md @@ -36,7 +36,7 @@ Provide links/terminal output for: - Status badge in `app_python/README.md`: - -```text +```log $ poetry run pytest --cov=src --cov-report=term-missing ========================= test session starts ========================= platform linux -- Python 3.14.2, pytest-9.0.2, pluggy-1.6.0 @@ -71,7 +71,7 @@ Coverage note: - **Practice 1: Path-based trigger filtering**: avoids running Python CI when unrelated folders change. - **Practice 2: Lint + test stages in CI**: catches style and functional issues early. - **Practice 3: Coverage reporting in CI command**: makes test quality visible, not just pass/fail. -- **Caching**: `actions/setup-python` Poetry cache enabled with lockfile-based invalidation. +- **Caching**: `actions/cache` stores `~/.cache/pypoetry` and `app_python/.venv` using a `poetry.lock`-based key. - **Snyk**: `TODO` (not integrated/documented yet). ## 4. Key Decisions From 6b359d943d9e9e0bae0e7995e2ad5e8d6dbc2d73 Mon Sep 17 00:00:00 2001 From: LocalT0aster <90502400+LocalT0aster@users.noreply.github.com> Date: Thu, 12 Feb 2026 21:38:13 +0300 Subject: [PATCH 6/9] snyk --- .github/actions/python-setup/action.yml | 57 +++++++++++++++++++++++++ .github/workflows/python-ci.yml | 27 +++++------- .github/workflows/python-docker.yml | 44 +++++++++++++++++++ .github/workflows/python-snyk.yml | 34 +++++++++++++++ app_python/docs/LAB03.md | 27 ++++++++---- 5 files changed, 163 insertions(+), 26 deletions(-) create mode 100644 .github/actions/python-setup/action.yml create mode 100644 .github/workflows/python-docker.yml create mode 100644 .github/workflows/python-snyk.yml diff --git a/.github/actions/python-setup/action.yml b/.github/actions/python-setup/action.yml new file mode 100644 index 0000000000..25c87fa7c7 --- /dev/null +++ b/.github/actions/python-setup/action.yml @@ -0,0 +1,57 @@ +name: Python Poetry Setup +description: Set up Python + Poetry, cache dependencies, and install project deps + +inputs: + python-version: + description: Python version to install + required: false + default: "3.14" + poetry-version: + description: Poetry version to install + required: false + default: "2.3.2" + working-directory: + description: Project directory containing pyproject.toml + required: false + default: "app_python" + lockfile-path: + description: Path to poetry.lock for cache key invalidation + required: false + default: "app_python/poetry.lock" + install-args: + description: Extra arguments passed to poetry install + required: false + default: "--with dev --no-interaction --no-ansi" + +runs: + using: composite + steps: + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: ${{ inputs.python-version }} + + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + version: ${{ inputs.poetry-version }} + + - name: Configure Poetry virtualenv location + shell: bash + working-directory: ${{ inputs.working-directory }} + run: poetry config virtualenvs.in-project true + + - name: Cache Poetry dependencies + uses: actions/cache@v4 + with: + path: | + ~/.cache/pypoetry + ${{ inputs.working-directory }}/.venv + key: ${{ runner.os }}-py${{ inputs.python-version }}-poetry${{ inputs.poetry-version }}-${{ hashFiles(inputs.lockfile-path) }} + restore-keys: | + ${{ runner.os }}-py${{ inputs.python-version }}-poetry${{ inputs.poetry-version }}- + + - name: Install dependencies + shell: bash + working-directory: ${{ inputs.working-directory }} + run: poetry install ${{ inputs.install-args }} diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index 57ca288c85..712fe39c42 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -4,10 +4,14 @@ on: push: paths: - app_python/** + - .github/actions/python-setup/** - .github/workflows/python-ci.yml pull_request: + branches: + - master paths: - app_python/** + - .github/actions/python-setup/** - .github/workflows/python-ci.yml jobs: @@ -24,25 +28,14 @@ jobs: working-directory: ./app_python steps: - uses: actions/checkout@v2 - - uses: actions/setup-python@v4 + - name: Setup Python tooling and dependencies + uses: ./.github/actions/python-setup with: python-version: ${{ matrix.python-version }} - - uses: snok/install-poetry@v1 - with: - version: ${{ matrix.poetry-version }} - - name: Configure Poetry virtualenv location - run: poetry config virtualenvs.in-project true - - name: Cache Poetry dependencies - uses: actions/cache@v4 - with: - path: | - ~/.cache/pypoetry - app_python/.venv - key: ${{ runner.os }}-py${{ matrix.python-version }}-poetry${{ matrix.poetry-version }}-${{ hashFiles('app_python/poetry.lock') }} - restore-keys: | - ${{ runner.os }}-py${{ matrix.python-version }}-poetry${{ matrix.poetry-version }}- - - name: Install dependencies - run: poetry install --with dev --no-interaction --no-ansi + poetry-version: ${{ matrix.poetry-version }} + working-directory: app_python + lockfile-path: app_python/poetry.lock + install-args: --with dev --no-interaction --no-ansi - name: Lint with flake8 run: poetry run flake8 src tests - name: Test using pytest with coverage report diff --git a/.github/workflows/python-docker.yml b/.github/workflows/python-docker.yml new file mode 100644 index 0000000000..6ba4be8770 --- /dev/null +++ b/.github/workflows/python-docker.yml @@ -0,0 +1,44 @@ +name: Python Docker Publish + +on: + pull_request: + branches: + - master + types: + - closed + paths: + - app_python/** + - .github/workflows/python-docker.yml + +jobs: + build-and-push: + if: github.event.pull_request.merged == true + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Derive lab version tag from merged branch + id: version + run: | + source_branch="${{ github.event.pull_request.head.ref }}" + if [[ "$source_branch" =~ ([0-9]+) ]]; then + lab_number="${BASH_REMATCH[1]}" + lab_number=$((10#$lab_number)) + echo "version_tag=1.${lab_number}" >> "$GITHUB_OUTPUT" + else + echo "Failed to extract lab number from merged branch: $source_branch" >&2 + exit 1 + fi + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build and push Docker image + uses: docker/build-push-action@v6 + with: + context: ./app_python + file: ./app_python/Dockerfile + push: true + tags: | + ${{ secrets.DOCKERHUB_USERNAME }}/devops-app-py:${{ steps.version.outputs.version_tag }} + ${{ secrets.DOCKERHUB_USERNAME }}/devops-app-py:latest diff --git a/.github/workflows/python-snyk.yml b/.github/workflows/python-snyk.yml new file mode 100644 index 0000000000..41ba73f4ff --- /dev/null +++ b/.github/workflows/python-snyk.yml @@ -0,0 +1,34 @@ +name: Python Snyk Scan + +on: + push: + paths: + - app_python/** + - .github/actions/python-setup/** + - .github/workflows/python-snyk.yml + pull_request: + branches: + - master + paths: + - app_python/** + - .github/actions/python-setup/** + - .github/workflows/python-snyk.yml + +jobs: + snyk: + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./app_python + steps: + - uses: actions/checkout@v4 + - name: Setup Python tooling and dependencies + uses: ./.github/actions/python-setup + - name: Setup Snyk CLI + uses: snyk/actions/setup@master + - name: Run Snyk dependency scan + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + run: snyk test --severity-threshold=high + - name: Snyk scan skipped + run: echo "SNYK_TOKEN secret not set; skipping Snyk dependency scan." diff --git a/app_python/docs/LAB03.md b/app_python/docs/LAB03.md index a9ab50b39b..851b966cb4 100644 --- a/app_python/docs/LAB03.md +++ b/app_python/docs/LAB03.md @@ -19,12 +19,18 @@ **Current CI trigger configuration:** -- workflow file: `.github/workflows/app_python.yml` -- trigger: `push` with path filters for `app_python/**` and workflow file changes +- workflow files: + - `.github/workflows/python-ci.yml` (lint + tests + coverage reports) + - `.github/workflows/python-snyk.yml` (security scan) + - `.github/workflows/python-docker.yml` (container publish) +- triggers: + - CI/Snyk: `push` + `pull_request` with path filters + - Docker publish: `pull_request` on `master`, `types: [closed]`, then gated by `merged == true` **Versioning strategy (SemVer/CalVer):** -- `TODO`: not documented/implemented yet for Docker image tagging in CI +- SemVer-style lab release tags: `1.` + `latest` +- lab number is extracted from merged branch name (example: `lab03` -> `1.3`) ## 2. Workflow Evidence @@ -63,23 +69,26 @@ TOTAL 77 0 100% Coverage note: -- The output above was captured before excluding launcher-only code. -- `src/main.py` line 27 (`if __name__ == "__main__":`) is now marked with `# pragma: no cover`. +- `src/main.py` launcher-only branch is excluded with `# pragma: no cover`. ## 3. Best Practices Implemented - **Practice 1: Path-based trigger filtering**: avoids running Python CI when unrelated folders change. - **Practice 2: Lint + test stages in CI**: catches style and functional issues early. - **Practice 3: Coverage reporting in CI command**: makes test quality visible, not just pass/fail. +- **Practice 4: Pipeline separation by concern**: test, security, and deploy concerns run independently for clearer failure diagnosis. +- **Practice 5: Reusable setup action**: shared Python/Poetry setup is centralized in `.github/actions/python-setup/action.yml` to avoid duplication. - **Caching**: `actions/cache` stores `~/.cache/pypoetry` and `app_python/.venv` using a `poetry.lock`-based key. -- **Snyk**: `TODO` (not integrated/documented yet). +- **Snyk**: integrated via `snyk/actions/setup` + `snyk test --severity-threshold=high`. +- **Snyk token handling**: workflow skips Snyk step if `SNYK_TOKEN` secret is missing. ## 4. Key Decisions -- **Versioning Strategy:** `TODO` (SemVer or CalVer not yet implemented for Docker tags). -- **Docker Tags:** `TODO` (version tags + `latest` not yet shown). -- **Workflow Triggers:** path-filtered `push` trigger to reduce unnecessary runs in a monorepo. +- **Versioning Strategy:** SemVer-style `1.` because releases happen once per lab and are easy to map back to coursework milestones. +- **Docker Tags:** each merged lab release pushes two tags: `1.` and `latest`. +- **Workflow Triggers:** path-filtered pushes/PRs for CI and Snyk, with container publishing gated on merged PRs to `master`. - **Test Coverage:** endpoint and helper logic are covered; launcher-only code is excluded with pragma. +- **Snyk policy:** CI fails only for vulnerabilities at `high` severity or above. ## 5. Challenges (Optional) From d8be54034a07786931d454696dcfff43a4c5abe9 Mon Sep 17 00:00:00 2001 From: LocalT0aster <90502400+LocalT0aster@users.noreply.github.com> Date: Thu, 12 Feb 2026 21:58:05 +0300 Subject: [PATCH 7/9] fix: disable snyk skip message --- .github/workflows/python-snyk.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/python-snyk.yml b/.github/workflows/python-snyk.yml index 41ba73f4ff..c3297eccc1 100644 --- a/.github/workflows/python-snyk.yml +++ b/.github/workflows/python-snyk.yml @@ -26,9 +26,12 @@ jobs: uses: ./.github/actions/python-setup - name: Setup Snyk CLI uses: snyk/actions/setup@master - - name: Run Snyk dependency scan + - name: Run Snyk dependency scan (or skip) env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} - run: snyk test --severity-threshold=high - - name: Snyk scan skipped - run: echo "SNYK_TOKEN secret not set; skipping Snyk dependency scan." + run: | + if [ -z "${SNYK_TOKEN:-}" ]; then + echo "SNYK_TOKEN secret not set; skipping Snyk dependency scan." + exit 0 + fi + snyk test --severity-threshold=high From d4ae1cef4a0ef1ade2c94305e4aa271209b5270c Mon Sep 17 00:00:00 2001 From: LocalT0aster <90502400+LocalT0aster@users.noreply.github.com> Date: Thu, 12 Feb 2026 22:25:49 +0300 Subject: [PATCH 8/9] docker build every push --- .github/workflows/python-docker.yml | 38 +++++++++++++++++++++++++++++ app_python/docs/LAB03.md | 6 +++-- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-docker.yml b/.github/workflows/python-docker.yml index 6ba4be8770..dde175a645 100644 --- a/.github/workflows/python-docker.yml +++ b/.github/workflows/python-docker.yml @@ -1,6 +1,12 @@ name: Python Docker Publish on: + push: + branches: + - "lab*" + paths: + - app_python/** + - .github/workflows/python-docker.yml pull_request: branches: - master @@ -11,6 +17,38 @@ on: - .github/workflows/python-docker.yml jobs: + build-and-push-branch: + if: github.event_name == 'push' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Derive lab+sha tag from branch + id: version + run: | + source_branch="${{ github.ref_name }}" + if [[ "$source_branch" =~ ([0-9]+) ]]; then + lab_number="${BASH_REMATCH[1]}" + lab_number=$((10#$lab_number)) + short_sha="${GITHUB_SHA::7}" + echo "branch_tag=1.${lab_number}.${short_sha}" >> "$GITHUB_OUTPUT" + else + echo "Failed to extract lab number from branch: $source_branch" >&2 + exit 1 + fi + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build and push Docker image (branch) + uses: docker/build-push-action@v6 + with: + context: ./app_python + file: ./app_python/Dockerfile + push: true + tags: | + ${{ secrets.DOCKERHUB_USERNAME }}/devops-app-py:${{ steps.version.outputs.branch_tag }} + build-and-push: if: github.event.pull_request.merged == true runs-on: ubuntu-latest diff --git a/app_python/docs/LAB03.md b/app_python/docs/LAB03.md index 851b966cb4..a8e350f14d 100644 --- a/app_python/docs/LAB03.md +++ b/app_python/docs/LAB03.md @@ -25,7 +25,9 @@ - `.github/workflows/python-docker.yml` (container publish) - triggers: - CI/Snyk: `push` + `pull_request` with path filters - - Docker publish: `pull_request` on `master`, `types: [closed]`, then gated by `merged == true` + - Docker publish: + - branch pushes to `lab*` publish `1..` + - merged PRs to `master` publish `1.` + `latest` **Versioning strategy (SemVer/CalVer):** @@ -85,7 +87,7 @@ Coverage note: ## 4. Key Decisions - **Versioning Strategy:** SemVer-style `1.` because releases happen once per lab and are easy to map back to coursework milestones. -- **Docker Tags:** each merged lab release pushes two tags: `1.` and `latest`. +- **Docker Tags:** branch builds publish `1..`; merged lab releases publish `1.` and `latest`. - **Workflow Triggers:** path-filtered pushes/PRs for CI and Snyk, with container publishing gated on merged PRs to `master`. - **Test Coverage:** endpoint and helper logic are covered; launcher-only code is excluded with pragma. - **Snyk policy:** CI fails only for vulnerabilities at `high` severity or above. From 44c4d4ad84bb0dd47a9932d2fff9ddc30bde2f22 Mon Sep 17 00:00:00 2001 From: LocalT0aster <90502400+LocalT0aster@users.noreply.github.com> Date: Thu, 12 Feb 2026 22:38:50 +0300 Subject: [PATCH 9/9] upd: docs --- app_python/README.md | 2 +- app_python/docs/LAB03.md | 38 ++++++++++++++++++++++++++++++++------ 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/app_python/README.md b/app_python/README.md index 7fc47993f7..1979c45e82 100644 --- a/app_python/README.md +++ b/app_python/README.md @@ -1,6 +1,6 @@ # DevOps Info Service -[![Python CI](https://github.com/LocalT0aster/DevOps-Core-S25/actions/workflows/python-ci.yml/badge.svg)](https://github.com/LocalT0aster/DevOps-Core-S25/actions/workflows/python-ci.yml) +[![Python CI](https://github.com/LocalT0aster/DevOps-Core-S26/actions/workflows/python-ci.yml/badge.svg)](https://github.com/LocalT0aster/DevOps-Core-S26/actions/workflows/python-ci.yml) ## Overview diff --git a/app_python/docs/LAB03.md b/app_python/docs/LAB03.md index a8e350f14d..5f12e800e1 100644 --- a/app_python/docs/LAB03.md +++ b/app_python/docs/LAB03.md @@ -23,6 +23,7 @@ - `.github/workflows/python-ci.yml` (lint + tests + coverage reports) - `.github/workflows/python-snyk.yml` (security scan) - `.github/workflows/python-docker.yml` (container publish) +- note: Docker login/build/push is intentionally separated into `python-docker.yml` rather than embedded in `python-ci.yml`. - triggers: - CI/Snyk: `push` + `pull_request` with path filters - Docker publish: @@ -39,10 +40,18 @@ Provide links/terminal output for: - Tests passing locally (terminal output below) -- Successful workflow run link (GitHub Actions): `TODO` -- Docker image on Docker Hub (link): `TODO` +- Successful workflow run links (GitHub Actions): + - Python CI: + - Python Docker Publish: + - Python Snyk Scan: +- Docker image on Docker Hub (links): + - Tags page: + - Example pushed tag (`1.3.d4ae1ce`): - Status badge in `app_python/README.md`: - - + - + +
+pytest output log ```log $ poetry run pytest --cov=src --cov-report=term-missing @@ -69,9 +78,7 @@ TOTAL 77 0 100% ========================= 10 passed in 0.06s ========================== ``` -Coverage note: - -- `src/main.py` launcher-only branch is excluded with `# pragma: no cover`. +
## 3. Best Practices Implemented @@ -84,6 +91,25 @@ Coverage note: - **Snyk**: integrated via `snyk/actions/setup` + `snyk test --severity-threshold=high`. - **Snyk token handling**: workflow skips Snyk step if `SNYK_TOKEN` secret is missing. +
+Snyk result (run #21961075835) + +``` +Testing /home/runner/work/DevOps-Core-S26/DevOps-Core-S26/app_python... + +Organization: localt0aster +Package manager: poetry +Target file: pyproject.toml +Project name: devops-info-service +Open source: no +Project path: /home/runner/work/DevOps-Core-S26/DevOps-Core-S26/app_python +Licenses: enabled + +✔ Tested 15 dependencies for known issues, no vulnerable paths found. +``` + +
+ ## 4. Key Decisions - **Versioning Strategy:** SemVer-style `1.` because releases happen once per lab and are easy to map back to coursework milestones.