From 23b3fc505e5a755a10f42f1cbe52bbe55373bcb5 Mon Sep 17 00:00:00 2001 From: largittehuna Date: Thu, 29 Jan 2026 18:57:04 +0100 Subject: [PATCH 01/16] Create login feature with password authentication in the controller : Upgrade SQLAlchemy integration --- poetry.lock | 120 +++++++++++++++++++++++++------------------------ pyproject.toml | 3 +- 2 files changed, 63 insertions(+), 60 deletions(-) diff --git a/poetry.lock b/poetry.lock index bf1794b..521da8d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -840,69 +840,71 @@ files = [ [[package]] name = "sqlalchemy" -version = "2.0.44" +version = "2.0.46" description = "Database Abstraction Library" optional = false python-versions = ">=3.7" groups = ["main"] files = [ - {file = "SQLAlchemy-2.0.44-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:471733aabb2e4848d609141a9e9d56a427c0a038f4abf65dd19d7a21fd563632"}, - {file = "SQLAlchemy-2.0.44-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48bf7d383a35e668b984c805470518b635d48b95a3c57cb03f37eaa3551b5f9f"}, - {file = "SQLAlchemy-2.0.44-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bf4bb6b3d6228fcf3a71b50231199fb94d2dd2611b66d33be0578ea3e6c2726"}, - {file = "SQLAlchemy-2.0.44-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:e998cf7c29473bd077704cea3577d23123094311f59bdc4af551923b168332b1"}, - {file = "SQLAlchemy-2.0.44-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:ebac3f0b5732014a126b43c2b7567f2f0e0afea7d9119a3378bde46d3dcad88e"}, - {file = "SQLAlchemy-2.0.44-cp37-cp37m-win32.whl", hash = "sha256:3255d821ee91bdf824795e936642bbf43a4c7cedf5d1aed8d24524e66843aa74"}, - {file = "SQLAlchemy-2.0.44-cp37-cp37m-win_amd64.whl", hash = "sha256:78e6c137ba35476adb5432103ae1534f2f5295605201d946a4198a0dea4b38e7"}, - {file = "sqlalchemy-2.0.44-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c77f3080674fc529b1bd99489378c7f63fcb4ba7f8322b79732e0258f0ea3ce"}, - {file = "sqlalchemy-2.0.44-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4c26ef74ba842d61635b0152763d057c8d48215d5be9bb8b7604116a059e9985"}, - {file = "sqlalchemy-2.0.44-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4a172b31785e2f00780eccab00bc240ccdbfdb8345f1e6063175b3ff12ad1b0"}, - {file = "sqlalchemy-2.0.44-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9480c0740aabd8cb29c329b422fb65358049840b34aba0adf63162371d2a96e"}, - {file = "sqlalchemy-2.0.44-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:17835885016b9e4d0135720160db3095dc78c583e7b902b6be799fb21035e749"}, - {file = "sqlalchemy-2.0.44-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cbe4f85f50c656d753890f39468fcd8190c5f08282caf19219f684225bfd5fd2"}, - {file = "sqlalchemy-2.0.44-cp310-cp310-win32.whl", hash = "sha256:2fcc4901a86ed81dc76703f3b93ff881e08761c63263c46991081fd7f034b165"}, - {file = "sqlalchemy-2.0.44-cp310-cp310-win_amd64.whl", hash = "sha256:9919e77403a483ab81e3423151e8ffc9dd992c20d2603bf17e4a8161111e55f5"}, - {file = "sqlalchemy-2.0.44-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0fe3917059c7ab2ee3f35e77757062b1bea10a0b6ca633c58391e3f3c6c488dd"}, - {file = "sqlalchemy-2.0.44-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:de4387a354ff230bc979b46b2207af841dc8bf29847b6c7dbe60af186d97aefa"}, - {file = "sqlalchemy-2.0.44-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3678a0fb72c8a6a29422b2732fe423db3ce119c34421b5f9955873eb9b62c1e"}, - {file = "sqlalchemy-2.0.44-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cf6872a23601672d61a68f390e44703442639a12ee9dd5a88bbce52a695e46e"}, - {file = "sqlalchemy-2.0.44-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:329aa42d1be9929603f406186630135be1e7a42569540577ba2c69952b7cf399"}, - {file = "sqlalchemy-2.0.44-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:70e03833faca7166e6a9927fbee7c27e6ecde436774cd0b24bbcc96353bce06b"}, - {file = "sqlalchemy-2.0.44-cp311-cp311-win32.whl", hash = "sha256:253e2f29843fb303eca6b2fc645aca91fa7aa0aa70b38b6950da92d44ff267f3"}, - {file = "sqlalchemy-2.0.44-cp311-cp311-win_amd64.whl", hash = "sha256:7a8694107eb4308a13b425ca8c0e67112f8134c846b6e1f722698708741215d5"}, - {file = "sqlalchemy-2.0.44-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72fea91746b5890f9e5e0997f16cbf3d53550580d76355ba2d998311b17b2250"}, - {file = "sqlalchemy-2.0.44-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:585c0c852a891450edbb1eaca8648408a3cc125f18cf433941fa6babcc359e29"}, - {file = "sqlalchemy-2.0.44-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b94843a102efa9ac68a7a30cd46df3ff1ed9c658100d30a725d10d9c60a2f44"}, - {file = "sqlalchemy-2.0.44-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:119dc41e7a7defcefc57189cfa0e61b1bf9c228211aba432b53fb71ef367fda1"}, - {file = "sqlalchemy-2.0.44-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0765e318ee9179b3718c4fd7ba35c434f4dd20332fbc6857a5e8df17719c24d7"}, - {file = "sqlalchemy-2.0.44-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2e7b5b079055e02d06a4308d0481658e4f06bc7ef211567edc8f7d5dce52018d"}, - {file = "sqlalchemy-2.0.44-cp312-cp312-win32.whl", hash = "sha256:846541e58b9a81cce7dee8329f352c318de25aa2f2bbe1e31587eb1f057448b4"}, - {file = "sqlalchemy-2.0.44-cp312-cp312-win_amd64.whl", hash = "sha256:7cbcb47fd66ab294703e1644f78971f6f2f1126424d2b300678f419aa73c7b6e"}, - {file = "sqlalchemy-2.0.44-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ff486e183d151e51b1d694c7aa1695747599bb00b9f5f604092b54b74c64a8e1"}, - {file = "sqlalchemy-2.0.44-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b1af8392eb27b372ddb783b317dea0f650241cea5bd29199b22235299ca2e45"}, - {file = "sqlalchemy-2.0.44-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b61188657e3a2b9ac4e8f04d6cf8e51046e28175f79464c67f2fd35bceb0976"}, - {file = "sqlalchemy-2.0.44-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b87e7b91a5d5973dda5f00cd61ef72ad75a1db73a386b62877d4875a8840959c"}, - {file = "sqlalchemy-2.0.44-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:15f3326f7f0b2bfe406ee562e17f43f36e16167af99c4c0df61db668de20002d"}, - {file = "sqlalchemy-2.0.44-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e77faf6ff919aa8cd63f1c4e561cac1d9a454a191bb864d5dd5e545935e5a40"}, - {file = "sqlalchemy-2.0.44-cp313-cp313-win32.whl", hash = "sha256:ee51625c2d51f8baadf2829fae817ad0b66b140573939dd69284d2ba3553ae73"}, - {file = "sqlalchemy-2.0.44-cp313-cp313-win_amd64.whl", hash = "sha256:c1c80faaee1a6c3428cecf40d16a2365bcf56c424c92c2b6f0f9ad204b899e9e"}, - {file = "sqlalchemy-2.0.44-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2fc44e5965ea46909a416fff0af48a219faefd5773ab79e5f8a5fcd5d62b2667"}, - {file = "sqlalchemy-2.0.44-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:dc8b3850d2a601ca2320d081874033684e246d28e1c5e89db0864077cfc8f5a9"}, - {file = "sqlalchemy-2.0.44-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d733dec0614bb8f4bcb7c8af88172b974f685a31dc3a65cca0527e3120de5606"}, - {file = "sqlalchemy-2.0.44-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22be14009339b8bc16d6b9dc8780bacaba3402aa7581658e246114abbd2236e3"}, - {file = "sqlalchemy-2.0.44-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:357bade0e46064f88f2c3a99808233e67b0051cdddf82992379559322dfeb183"}, - {file = "sqlalchemy-2.0.44-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4848395d932e93c1595e59a8672aa7400e8922c39bb9b0668ed99ac6fa867822"}, - {file = "sqlalchemy-2.0.44-cp38-cp38-win32.whl", hash = "sha256:2f19644f27c76f07e10603580a47278abb2a70311136a7f8fd27dc2e096b9013"}, - {file = "sqlalchemy-2.0.44-cp38-cp38-win_amd64.whl", hash = "sha256:1df4763760d1de0dfc8192cc96d8aa293eb1a44f8f7a5fbe74caf1b551905c5e"}, - {file = "sqlalchemy-2.0.44-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f7027414f2b88992877573ab780c19ecb54d3a536bef3397933573d6b5068be4"}, - {file = "sqlalchemy-2.0.44-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3fe166c7d00912e8c10d3a9a0ce105569a31a3d0db1a6e82c4e0f4bf16d5eca9"}, - {file = "sqlalchemy-2.0.44-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3caef1ff89b1caefc28f0368b3bde21a7e3e630c2eddac16abd9e47bd27cc36a"}, - {file = "sqlalchemy-2.0.44-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc2856d24afa44295735e72f3c75d6ee7fdd4336d8d3a8f3d44de7aa6b766df2"}, - {file = "sqlalchemy-2.0.44-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:11bac86b0deada30b6b5f93382712ff0e911fe8d31cb9bf46e6b149ae175eff0"}, - {file = "sqlalchemy-2.0.44-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4d18cd0e9a0f37c9f4088e50e3839fcb69a380a0ec957408e0b57cff08ee0a26"}, - {file = "sqlalchemy-2.0.44-cp39-cp39-win32.whl", hash = "sha256:9e9018544ab07614d591a26c1bd4293ddf40752cc435caf69196740516af7100"}, - {file = "sqlalchemy-2.0.44-cp39-cp39-win_amd64.whl", hash = "sha256:8e0e4e66fd80f277a8c3de016a81a554e76ccf6b8d881ee0b53200305a8433f6"}, - {file = "sqlalchemy-2.0.44-py3-none-any.whl", hash = "sha256:19de7ca1246fbef9f9d1bff8f1ab25641569df226364a0e40457dc5457c54b05"}, - {file = "sqlalchemy-2.0.44.tar.gz", hash = "sha256:0ae7454e1ab1d780aee69fd2aae7d6b8670a581d8847f2d1e0f7ddfbf47e5a22"}, + {file = "sqlalchemy-2.0.46-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:895296687ad06dc9b11a024cf68e8d9d3943aa0b4964278d2553b86f1b267735"}, + {file = "sqlalchemy-2.0.46-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab65cb2885a9f80f979b85aa4e9c9165a31381ca322cbde7c638fe6eefd1ec39"}, + {file = "sqlalchemy-2.0.46-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:52fe29b3817bd191cc20bad564237c808967972c97fa683c04b28ec8979ae36f"}, + {file = "sqlalchemy-2.0.46-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:09168817d6c19954d3b7655da6ba87fcb3a62bb575fb396a81a8b6a9fadfe8b5"}, + {file = "sqlalchemy-2.0.46-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:be6c0466b4c25b44c5d82b0426b5501de3c424d7a3220e86cd32f319ba56798e"}, + {file = "sqlalchemy-2.0.46-cp310-cp310-win32.whl", hash = "sha256:1bc3f601f0a818d27bfe139f6766487d9c88502062a2cd3a7ee6c342e81d5047"}, + {file = "sqlalchemy-2.0.46-cp310-cp310-win_amd64.whl", hash = "sha256:e0c05aff5c6b1bb5fb46a87e0f9d2f733f83ef6cbbbcd5c642b6c01678268061"}, + {file = "sqlalchemy-2.0.46-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:261c4b1f101b4a411154f1da2b76497d73abbfc42740029205d4d01fa1052684"}, + {file = "sqlalchemy-2.0.46-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:181903fe8c1b9082995325f1b2e84ac078b1189e2819380c2303a5f90e114a62"}, + {file = "sqlalchemy-2.0.46-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:590be24e20e2424a4c3c1b0835e9405fa3d0af5823a1a9fc02e5dff56471515f"}, + {file = "sqlalchemy-2.0.46-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7568fe771f974abadce52669ef3a03150ff03186d8eb82613bc8adc435a03f01"}, + {file = "sqlalchemy-2.0.46-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf7e1e78af38047e08836d33502c7a278915698b7c2145d045f780201679999"}, + {file = "sqlalchemy-2.0.46-cp311-cp311-win32.whl", hash = "sha256:9d80ea2ac519c364a7286e8d765d6cd08648f5b21ca855a8017d9871f075542d"}, + {file = "sqlalchemy-2.0.46-cp311-cp311-win_amd64.whl", hash = "sha256:585af6afe518732d9ccd3aea33af2edaae4a7aa881af5d8f6f4fe3a368699597"}, + {file = "sqlalchemy-2.0.46-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a9a72b0da8387f15d5810f1facca8f879de9b85af8c645138cba61ea147968c"}, + {file = "sqlalchemy-2.0.46-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2347c3f0efc4de367ba00218e0ae5c4ba2306e47216ef80d6e31761ac97cb0b9"}, + {file = "sqlalchemy-2.0.46-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9094c8b3197db12aa6f05c51c05daaad0a92b8c9af5388569847b03b1007fb1b"}, + {file = "sqlalchemy-2.0.46-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37fee2164cf21417478b6a906adc1a91d69ae9aba8f9533e67ce882f4bb1de53"}, + {file = "sqlalchemy-2.0.46-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b1e14b2f6965a685c7128bd315e27387205429c2e339eeec55cb75ca4ab0ea2e"}, + {file = "sqlalchemy-2.0.46-cp312-cp312-win32.whl", hash = "sha256:412f26bb4ba942d52016edc8d12fb15d91d3cd46b0047ba46e424213ad407bcb"}, + {file = "sqlalchemy-2.0.46-cp312-cp312-win_amd64.whl", hash = "sha256:ea3cd46b6713a10216323cda3333514944e510aa691c945334713fca6b5279ff"}, + {file = "sqlalchemy-2.0.46-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:93a12da97cca70cea10d4b4fc602589c4511f96c1f8f6c11817620c021d21d00"}, + {file = "sqlalchemy-2.0.46-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af865c18752d416798dae13f83f38927c52f085c52e2f32b8ab0fef46fdd02c2"}, + {file = "sqlalchemy-2.0.46-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8d679b5f318423eacb61f933a9a0f75535bfca7056daeadbf6bd5bcee6183aee"}, + {file = "sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64901e08c33462acc9ec3bad27fc7a5c2b6491665f2aa57564e57a4f5d7c52ad"}, + {file = "sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8ac45e8f4eaac0f9f8043ea0e224158855c6a4329fd4ee37c45c61e3beb518e"}, + {file = "sqlalchemy-2.0.46-cp313-cp313-win32.whl", hash = "sha256:8d3b44b3d0ab2f1319d71d9863d76eeb46766f8cf9e921ac293511804d39813f"}, + {file = "sqlalchemy-2.0.46-cp313-cp313-win_amd64.whl", hash = "sha256:77f8071d8fbcbb2dd11b7fd40dedd04e8ebe2eb80497916efedba844298065ef"}, + {file = "sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1e8cc6cc01da346dc92d9509a63033b9b1bda4fed7a7a7807ed385c7dccdc10"}, + {file = "sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96c7cca1a4babaaf3bfff3e4e606e38578856917e52f0384635a95b226c87764"}, + {file = "sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2a9f9aee38039cf4755891a1e50e1effcc42ea6ba053743f452c372c3152b1b"}, + {file = "sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:db23b1bf8cfe1f7fda19018e7207b20cdb5168f83c437ff7e95d19e39289c447"}, + {file = "sqlalchemy-2.0.46-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:56bdd261bfd0895452006d5316cbf35739c53b9bb71a170a331fa0ea560b2ada"}, + {file = "sqlalchemy-2.0.46-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33e462154edb9493f6c3ad2125931e273bbd0be8ae53f3ecd1c161ea9a1dd366"}, + {file = "sqlalchemy-2.0.46-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bcdce05f056622a632f1d44bb47dbdb677f58cad393612280406ce37530eb6d"}, + {file = "sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e84b09a9b0f19accedcbeff5c2caf36e0dd537341a33aad8d680336152dc34e"}, + {file = "sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4f52f7291a92381e9b4de9050b0a65ce5d6a763333406861e33906b8aa4906bf"}, + {file = "sqlalchemy-2.0.46-cp314-cp314-win32.whl", hash = "sha256:70ed2830b169a9960193f4d4322d22be5c0925357d82cbf485b3369893350908"}, + {file = "sqlalchemy-2.0.46-cp314-cp314-win_amd64.whl", hash = "sha256:3c32e993bc57be6d177f7d5d31edb93f30726d798ad86ff9066d75d9bf2e0b6b"}, + {file = "sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4dafb537740eef640c4d6a7c254611dca2df87eaf6d14d6a5fca9d1f4c3fc0fa"}, + {file = "sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42a1643dc5427b69aca967dae540a90b0fbf57eaf248f13a90ea5930e0966863"}, + {file = "sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ff33c6e6ad006bbc0f34f5faf941cfc62c45841c64c0a058ac38c799f15b5ede"}, + {file = "sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:82ec52100ec1e6ec671563bbd02d7c7c8d0b9e71a0723c72f22ecf52d1755330"}, + {file = "sqlalchemy-2.0.46-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6ac245604295b521de49b465bab845e3afe6916bcb2147e5929c8041b4ec0545"}, + {file = "sqlalchemy-2.0.46-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e6199143d51e3e1168bedd98cc698397404a8f7508831b81b6a29b18b051069"}, + {file = "sqlalchemy-2.0.46-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:716be5bcabf327b6d5d265dbdc6213a01199be587224eb991ad0d37e83d728fd"}, + {file = "sqlalchemy-2.0.46-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:6f827fd687fa1ba7f51699e1132129eac8db8003695513fcf13fc587e1bd47a5"}, + {file = "sqlalchemy-2.0.46-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c805fa6e5d461329fa02f53f88c914d189ea771b6821083937e79550bf31fc19"}, + {file = "sqlalchemy-2.0.46-cp38-cp38-win32.whl", hash = "sha256:3aac08f7546179889c62b53b18ebf1148b10244b3405569c93984b0388d016a7"}, + {file = "sqlalchemy-2.0.46-cp38-cp38-win_amd64.whl", hash = "sha256:0cc3117db526cad3e61074100bd2867b533e2c7dc1569e95c14089735d6fb4fe"}, + {file = "sqlalchemy-2.0.46-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:90bde6c6b1827565a95fde597da001212ab436f1b2e0c2dcc7246e14db26e2a3"}, + {file = "sqlalchemy-2.0.46-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94b1e5f3a5f1ff4f42d5daab047428cd45a3380e51e191360a35cef71c9a7a2a"}, + {file = "sqlalchemy-2.0.46-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93bb0aae40b52c57fd74ef9c6933c08c040ba98daf23ad33c3f9893494b8d3ce"}, + {file = "sqlalchemy-2.0.46-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c4e2cc868b7b5208aec6c960950b7bb821f82c2fe66446c92ee0a571765e91a5"}, + {file = "sqlalchemy-2.0.46-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:965c62be8256d10c11f8907e7a8d3e18127a4c527a5919d85fa87fd9ecc2cfdc"}, + {file = "sqlalchemy-2.0.46-cp39-cp39-win32.whl", hash = "sha256:9397b381dcee8a2d6b99447ae85ea2530dcac82ca494d1db877087a13e38926d"}, + {file = "sqlalchemy-2.0.46-cp39-cp39-win_amd64.whl", hash = "sha256:4396c948d8217e83e2c202fbdcc0389cf8c93d2c1c5e60fa5c5a955eae0e64be"}, + {file = "sqlalchemy-2.0.46-py3-none-any.whl", hash = "sha256:f9c11766e7e7c0a2767dda5acb006a118640c9fc0a4104214b96269bfb78399e"}, + {file = "sqlalchemy-2.0.46.tar.gz", hash = "sha256:cf36851ee7219c170bb0793dbc3da3e80c582e04a5437bc601bfe8c85c9216d7"}, ] [package.dependencies] @@ -985,4 +987,4 @@ email = ["email-validator"] [metadata] lock-version = "2.1" python-versions = ">=3.12" -content-hash = "dd6f01b432b1310a8eb69f942a79001d5472fd2d42e2f987db1cf0d7c714286b" +content-hash = "c8592be2ed789497781fdca83b7bb69464381ab635294da63082345abdbf3254" diff --git a/pyproject.toml b/pyproject.toml index c2d6f02..16dafe5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,8 @@ dependencies = [ "flask-uploads (>=0.2.1,<0.3.0)", "pillow (>=12.0.0,<13.0.0)", "psycopg2-binary (>=2.9.11,<3.0.0)", - "python-dotenv (>=1.2.1,<2.0.0)" + "python-dotenv (>=1.2.1,<2.0.0)", + "sqlalchemy (>=2.0.46,<3.0.0)", ] From 09990f652df06bf1fd54ef6a282394d933753042 Mon Sep 17 00:00:00 2001 From: largittehuna Date: Thu, 29 Jan 2026 19:04:37 +0100 Subject: [PATCH 02/16] Create login feature with password authentication in the controller : Create a login page with username/password authentication, we will improve the code later --- app/__init__.py | 9 +++++++++ app/controllers/auth.py | 38 ++++++++++++++++++++++++++++++++++++++ app/database.py | 6 ++++++ app/templates/login.html | 31 +++++++++++++++++++++++++++++++ run.py | 6 ++++++ 5 files changed, 90 insertions(+) create mode 100644 app/controllers/auth.py create mode 100644 app/database.py create mode 100644 app/templates/login.html create mode 100644 run.py diff --git a/app/__init__.py b/app/__init__.py index e69de29..0de2569 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -0,0 +1,9 @@ +import os +from flask import Flask +from app.controllers.auth import auth_bp + +def create_app(): + app = Flask(__name__) + app.secret_key = os.getenv("SECRET_KEY") + app.register_blueprint(auth_bp) + return app \ No newline at end of file diff --git a/app/controllers/auth.py b/app/controllers/auth.py new file mode 100644 index 0000000..5f948ea --- /dev/null +++ b/app/controllers/auth.py @@ -0,0 +1,38 @@ +from flask import Blueprint, render_template, request, redirect, url_for, session, flash +from sqlalchemy import select +from sqlalchemy.orm import Session +from app.database import engine +from app.models import Account + +auth_bp = Blueprint('auth', __name__) + +@auth_bp.route('/') +def login_page(): + return render_template('login.html') + +@auth_bp.route('/login', methods=['POST']) +def login_handler(): + username = request.form.get('username') + password = request.form.get('password') + + with Session(engine) as db_session: + stmt = select(Account).where(Account.account_username == username) + user = db_session.execute(stmt).scalar_one_or_none() + + if user and user.account_password == password: + session['user_id'] = user.account_id + session['username'] = user.account_username + return redirect(url_for('auth.dashboard')) + + flash("Identifiants incorrects") + return redirect(url_for('auth.login_page')) + +@auth_bp.route('/dashboard') +def dashboard(): + if 'user_id' not in session: return redirect(url_for('auth.login_page')) + return f"

Bienvenue {session['username']}

Déconnexion" + +@auth_bp.route('/logout') +def logout(): + session.clear() + return redirect(url_for('auth.login_page')) diff --git a/app/database.py b/app/database.py new file mode 100644 index 0000000..6b72066 --- /dev/null +++ b/app/database.py @@ -0,0 +1,6 @@ +import os +from sqlalchemy import create_engine +from dotenv import load_dotenv + +load_dotenv() +engine = create_engine(os.getenv("DATABASE_URL")) \ No newline at end of file diff --git a/app/templates/login.html b/app/templates/login.html new file mode 100644 index 0000000..d335067 --- /dev/null +++ b/app/templates/login.html @@ -0,0 +1,31 @@ + + + + Connexion Blog + + +
+

Connexion

+ {% with messages = get_flashed_messages() %} {% if messages %} +

{{ messages[0] }}

+ {% endif %} {% endwith %} +
+ + + +
+
+ + diff --git a/run.py b/run.py new file mode 100644 index 0000000..488dae9 --- /dev/null +++ b/run.py @@ -0,0 +1,6 @@ +from app import create_app + +app = create_app() + +if __name__ == "__main__": + app.run(debug=True) From a25adda13f7941a0fb097dfd59a086fc6e6cc54c Mon Sep 17 00:00:00 2001 From: raydeveloppeur-admin Date: Thu, 29 Jan 2026 21:49:33 +0100 Subject: [PATCH 03/16] Create login feature with password authentication in the controller : Add comments explaining how to include a file that is not part of the repository --- app/__init__.py | 1 + app/database.py | 1 + tests/conftest.py | 1 + 3 files changed, 3 insertions(+) diff --git a/app/__init__.py b/app/__init__.py index 0de2569..454f652 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -4,6 +4,7 @@ def create_app(): app = Flask(__name__) + # This variable must be added to the .env file for it to work app.secret_key = os.getenv("SECRET_KEY") app.register_blueprint(auth_bp) return app \ No newline at end of file diff --git a/app/database.py b/app/database.py index 6b72066..5b0e23b 100644 --- a/app/database.py +++ b/app/database.py @@ -2,5 +2,6 @@ from sqlalchemy import create_engine from dotenv import load_dotenv +# This variable must be added to the .env file for it to work load_dotenv() engine = create_engine(os.getenv("DATABASE_URL")) \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 6060013..fd4d4b3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,6 +10,7 @@ file_env = dotenv_values(".env.test") # CI (GitHub Actions) provides TEST_DATABASE_URL via environment variables. # Locally, we fall back to .env.test for a dedicated test database. +# load_dotenv() won't work if we use ".env.test" # This keeps the test configuration consistent across environments without changing the code. database_url = file_env.get("TEST_DATABASE_URL") or os.getenv("TEST_DATABASE_URL") engine = create_engine(database_url) From 83976b71b4dd1348e24a12680669417951b8d9c7 Mon Sep 17 00:00:00 2001 From: raydeveloppeur-admin Date: Thu, 29 Jan 2026 21:49:50 +0100 Subject: [PATCH 04/16] Create login feature with password authentication in the controller : Correct the lint --- app/__init__.py | 5 ++++- app/controllers/auth.py | 33 +++++++++++++++++---------------- app/database.py | 5 +++-- 3 files changed, 24 insertions(+), 19 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index 454f652..66dbcab 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,10 +1,13 @@ import os + from flask import Flask + from app.controllers.auth import auth_bp + def create_app(): app = Flask(__name__) # This variable must be added to the .env file for it to work app.secret_key = os.getenv("SECRET_KEY") app.register_blueprint(auth_bp) - return app \ No newline at end of file + return app diff --git a/app/controllers/auth.py b/app/controllers/auth.py index 5f948ea..cf74ed1 100644 --- a/app/controllers/auth.py +++ b/app/controllers/auth.py @@ -1,38 +1,39 @@ -from flask import Blueprint, render_template, request, redirect, url_for, session, flash +from flask import Blueprint, flash, redirect, render_template, request, session, url_for from sqlalchemy import select from sqlalchemy.orm import Session + from app.database import engine from app.models import Account -auth_bp = Blueprint('auth', __name__) +auth_bp = Blueprint("auth", __name__) -@auth_bp.route('/') +@auth_bp.route("/") def login_page(): - return render_template('login.html') + return render_template("login.html") -@auth_bp.route('/login', methods=['POST']) +@auth_bp.route("/login", methods=["POST"]) def login_handler(): - username = request.form.get('username') - password = request.form.get('password') + username = request.form.get("username") + password = request.form.get("password") with Session(engine) as db_session: stmt = select(Account).where(Account.account_username == username) user = db_session.execute(stmt).scalar_one_or_none() if user and user.account_password == password: - session['user_id'] = user.account_id - session['username'] = user.account_username - return redirect(url_for('auth.dashboard')) - + session["user_id"] = user.account_id + session["username"] = user.account_username + return redirect(url_for("auth.dashboard")) + flash("Identifiants incorrects") - return redirect(url_for('auth.login_page')) + return redirect(url_for("auth.login_page")) -@auth_bp.route('/dashboard') +@auth_bp.route("/dashboard") def dashboard(): - if 'user_id' not in session: return redirect(url_for('auth.login_page')) + if "user_id" not in session: return redirect(url_for("auth.login_page")) return f"

Bienvenue {session['username']}

Déconnexion" -@auth_bp.route('/logout') +@auth_bp.route("/logout") def logout(): session.clear() - return redirect(url_for('auth.login_page')) + return redirect(url_for("auth.login_page")) diff --git a/app/database.py b/app/database.py index 5b0e23b..7d85dba 100644 --- a/app/database.py +++ b/app/database.py @@ -1,7 +1,8 @@ import os -from sqlalchemy import create_engine + from dotenv import load_dotenv +from sqlalchemy import create_engine # This variable must be added to the .env file for it to work load_dotenv() -engine = create_engine(os.getenv("DATABASE_URL")) \ No newline at end of file +engine = create_engine(os.getenv("DATABASE_URL")) From 8560b6aeed2c428637ace43d5e6cb46cc1fcd4a1 Mon Sep 17 00:00:00 2001 From: raydeveloppeur-admin Date: Thu, 5 Feb 2026 00:19:48 +0100 Subject: [PATCH 05/16] Create login feature with password authentication in the controller: Refactor the code to move variables into configuration_variables.py, also Base from models.py will be used in the new file --- app/__init__.py | 6 ++---- app/configuration_variables.py | 15 +++++++++++++++ app/database.py | 11 +++++------ app/models.py | 4 ++-- 4 files changed, 24 insertions(+), 12 deletions(-) create mode 100644 app/configuration_variables.py diff --git a/app/__init__.py b/app/__init__.py index 66dbcab..9eab78f 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,13 +1,11 @@ -import os - from flask import Flask +from app.configuration_variables import ConfigurationVariables from app.controllers.auth import auth_bp def create_app(): app = Flask(__name__) - # This variable must be added to the .env file for it to work - app.secret_key = os.getenv("SECRET_KEY") + app.secret_key = ConfigurationVariables.SECRET_KEY app.register_blueprint(auth_bp) return app diff --git a/app/configuration_variables.py b/app/configuration_variables.py new file mode 100644 index 0000000..9281ff0 --- /dev/null +++ b/app/configuration_variables.py @@ -0,0 +1,15 @@ +import os + +from dotenv import load_dotenv + +load_dotenv() + +def get_required_env(name): + value = os.getenv(name) + if not value: + raise OSError(f"Missing required environment variable '{name}' in .env file.") + return value + +class ConfigurationVariables: + DATABASE_URL = get_required_env("DATABASE_URL") + SECRET_KEY = get_required_env("SECRET_KEY") diff --git a/app/database.py b/app/database.py index 7d85dba..077c2be 100644 --- a/app/database.py +++ b/app/database.py @@ -1,8 +1,7 @@ -import os - -from dotenv import load_dotenv from sqlalchemy import create_engine +from sqlalchemy.orm import declarative_base + +from app.configuration_variables import ConfigurationVariables -# This variable must be added to the .env file for it to work -load_dotenv() -engine = create_engine(os.getenv("DATABASE_URL")) +database_engine = create_engine(ConfigurationVariables.DATABASE_URL) +Base = declarative_base() diff --git a/app/models.py b/app/models.py index 647686d..a3506c5 100644 --- a/app/models.py +++ b/app/models.py @@ -7,9 +7,9 @@ Text, func, ) -from sqlalchemy.orm import declarative_base, relationship +from sqlalchemy.orm import relationship -Base = declarative_base() +from app.database import Base class Account(Base): From 04b6e065a17e89c39a209186eec5c7600df126d3 Mon Sep 17 00:00:00 2001 From: raydeveloppeur-admin Date: Thu, 5 Feb 2026 00:29:49 +0100 Subject: [PATCH 06/16] Create login feature with password authentication in the controller : Change the variable name and initialize its value inside if __name__ == "main" to improve readability --- app/__init__.py | 2 +- run.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index 9eab78f..fe96380 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -4,7 +4,7 @@ from app.controllers.auth import auth_bp -def create_app(): +def initialize_flask_application(): app = Flask(__name__) app.secret_key = ConfigurationVariables.SECRET_KEY app.register_blueprint(auth_bp) diff --git a/run.py b/run.py index 488dae9..c02d1f8 100644 --- a/run.py +++ b/run.py @@ -1,6 +1,5 @@ -from app import create_app - -app = create_app() +from app import initialize_flask_application if __name__ == "__main__": + app = initialize_flask_application() app.run(debug=True) From 55fc930835908d220b2368eaae2ed9a3901c7395 Mon Sep 17 00:00:00 2001 From: raydeveloppeur-admin Date: Thu, 5 Feb 2026 09:03:50 +0100 Subject: [PATCH 07/16] Create login feature with password authentication in the controller : Rename some functions for better readability, fix lint issues and update printed messages to English --- app/controllers/auth.py | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/app/controllers/auth.py b/app/controllers/auth.py index cf74ed1..0b7fb44 100644 --- a/app/controllers/auth.py +++ b/app/controllers/auth.py @@ -2,38 +2,46 @@ from sqlalchemy import select from sqlalchemy.orm import Session -from app.database import engine +from app.database import database_engine from app.models import Account auth_bp = Blueprint("auth", __name__) @auth_bp.route("/") -def login_page(): +def render_login_page(): return render_template("login.html") + @auth_bp.route("/login", methods=["POST"]) -def login_handler(): +def login_authentication(): username = request.form.get("username") password = request.form.get("password") - with Session(engine) as db_session: - stmt = select(Account).where(Account.account_username == username) - user = db_session.execute(stmt).scalar_one_or_none() + with Session(database_engine) as db_session: + query = select(Account).where(Account.account_username == username) + user = db_session.execute(query).scalar_one_or_none() if user and user.account_password == password: session["user_id"] = user.account_id session["username"] = user.account_username return redirect(url_for("auth.dashboard")) - flash("Identifiants incorrects") - return redirect(url_for("auth.login_page")) + flash("Incorrect username or password.") + return redirect(url_for("auth.render_login_page")) + @auth_bp.route("/dashboard") def dashboard(): - if "user_id" not in session: return redirect(url_for("auth.login_page")) - return f"

Bienvenue {session['username']}

Déconnexion" + if "user_id" not in session: + return redirect(url_for("auth.render_login_page")) + + return ( + f"

Welcome {session['username']}

" + "Logout" + ) + @auth_bp.route("/logout") def logout(): session.clear() - return redirect(url_for("auth.login_page")) + return redirect(url_for("auth.render_login_page")) From 401fd713551f05c0fb186d48d693adf353fb030e Mon Sep 17 00:00:00 2001 From: raydeveloppeur-admin Date: Thu, 5 Feb 2026 09:12:08 +0100 Subject: [PATCH 08/16] Create login feature with password authentication in the controller : Rename the file and change the blueprint name to be more readable --- app/__init__.py | 4 ++-- app/controllers/{auth.py => login.py} | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) rename app/controllers/{auth.py => login.py} (88%) diff --git a/app/__init__.py b/app/__init__.py index fe96380..90e3cfd 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,11 +1,11 @@ from flask import Flask from app.configuration_variables import ConfigurationVariables -from app.controllers.auth import auth_bp +from app.controllers.login import login_bp def initialize_flask_application(): app = Flask(__name__) app.secret_key = ConfigurationVariables.SECRET_KEY - app.register_blueprint(auth_bp) + app.register_blueprint(login_bp) return app diff --git a/app/controllers/auth.py b/app/controllers/login.py similarity index 88% rename from app/controllers/auth.py rename to app/controllers/login.py index 0b7fb44..4a8845a 100644 --- a/app/controllers/auth.py +++ b/app/controllers/login.py @@ -5,14 +5,14 @@ from app.database import database_engine from app.models import Account -auth_bp = Blueprint("auth", __name__) +login_bp = Blueprint("auth", __name__) -@auth_bp.route("/") +@login_bp.route("/") def render_login_page(): return render_template("login.html") -@auth_bp.route("/login", methods=["POST"]) +@login_bp.route("/login", methods=["POST"]) def login_authentication(): username = request.form.get("username") password = request.form.get("password") @@ -30,7 +30,7 @@ def login_authentication(): return redirect(url_for("auth.render_login_page")) -@auth_bp.route("/dashboard") +@login_bp.route("/dashboard") def dashboard(): if "user_id" not in session: return redirect(url_for("auth.render_login_page")) @@ -41,7 +41,7 @@ def dashboard(): ) -@auth_bp.route("/logout") +@login_bp.route("/logout") def logout(): session.clear() return redirect(url_for("auth.render_login_page")) From 363d11dd3006147abb65e95de18ed60832f70332 Mon Sep 17 00:00:00 2001 From: raydeveloppeur-admin Date: Thu, 5 Feb 2026 20:38:05 +0100 Subject: [PATCH 09/16] Create login feature with password authentication in the controller : Update integration tests by modifying conftest.py and refactoring factories into a dedicated file to prepare for implementing login tests --- tests/conftest.py | 62 ++++++++++++++++++++++++++++++++++---------- tests/factories.py | 43 ++++++++++++++++++++++++++++++ tests/test_models.py | 50 +++-------------------------------- 3 files changed, 94 insertions(+), 61 deletions(-) create mode 100644 tests/factories.py diff --git a/tests/conftest.py b/tests/conftest.py index fd4d4b3..a868616 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,16 +5,30 @@ from sqlalchemy import create_engine, text from sqlalchemy.orm import sessionmaker +import app.controllers.login as login_controller +import app.database as app_database +from app import initialize_flask_application from app.models import Account, Article, Base, Comment file_env = dotenv_values(".env.test") -# CI (GitHub Actions) provides TEST_DATABASE_URL via environment variables. -# Locally, we fall back to .env.test for a dedicated test database. -# load_dotenv() won't work if we use ".env.test" -# This keeps the test configuration consistent across environments without changing the code. -database_url = file_env.get("TEST_DATABASE_URL") or os.getenv("TEST_DATABASE_URL") -engine = create_engine(database_url) -SessionLocal = sessionmaker() + +def get_required_env_test(value, name): + if not value: + raise OSError(f"Missing required environment variable '{name}' in .env.test file.") + return value + +class ConfigurationVariablesTest: + DATABASE_URL = get_required_env_test( + file_env.get("TEST_DATABASE_URL") or os.getenv("TEST_DATABASE_URL"), + "TEST_DATABASE_URL" + ) + SECRET_KEY = get_required_env_test( + file_env.get("TEST_SECRET_KEY") or os.getenv("TEST_SECRET_KEY"), + "TEST_SECRET_KEY" + ) + +engine = create_engine(ConfigurationVariablesTest.DATABASE_URL) +SessionLocal = sessionmaker(bind=engine) def account_model(): return Account @@ -28,14 +42,34 @@ def comment_model(): def truncate_all_tables(connection): tables = Base.metadata.sorted_tables table_names = ", ".join(f'"{t.name}"' for t in tables) - connection.execute(text(f"TRUNCATE {table_names} RESTART IDENTITY CASCADE;")) + if table_names: + connection.execute(text(f"TRUNCATE {table_names} RESTART IDENTITY CASCADE;")) + +@pytest.fixture(scope="function") +def app(monkeypatch): + flask_app = initialize_flask_application() + flask_app.config.update({ + "TESTING": True, + "SECRET_KEY": ConfigurationVariablesTest.SECRET_KEY, + }) + + monkeypatch.setattr(app_database, "database_engine", engine) + monkeypatch.setattr(login_controller, "database_engine", engine) + + return flask_app + +@pytest.fixture(scope="function") +def client(app): + return app.test_client() @pytest.fixture(scope="function") def db_session(): - with engine.begin() as connection: + with engine.connect() as connection: truncate_all_tables(connection) - session = SessionLocal(bind=connection) - try: - yield session - finally: - session.close() + connection.commit() + + session = SessionLocal() + try: + yield session + finally: + session.close() diff --git a/tests/factories.py b/tests/factories.py new file mode 100644 index 0000000..ffbc449 --- /dev/null +++ b/tests/factories.py @@ -0,0 +1,43 @@ +from tests.conftest import account_model, article_model, comment_model + + +def make_account( + account_username="Xxx__D4RK_V4D0R__xxX", + account_password="987654321abcdefg@", + account_email="C4T@exemple.com", + account_role="user", +): + Account = account_model() + return Account( + account_username=account_username, + account_password=account_password, + account_email=account_email, + account_role=account_role, +) + +def make_article( + article_author_id, + article_title="Luke, I'm your father !", + article_content=( + 'On the platform, Darth Vader stepped forward and spoke the truth: ' + '"Luke, I am your father." Shocked, Luke backed away, unable to accept it.' + ), +): + Article = article_model() + return Article( + article_author_id=article_author_id, + article_title=article_title, + article_content=article_content, +) + +def make_comment( + comment_article_id, + comment_written_account_id, + comment_content="Bravo !", +): + Comment = comment_model() + return Comment( + comment_article_id=comment_article_id, + comment_written_account_id=comment_written_account_id, + comment_content=comment_content, +) diff --git a/tests/test_models.py b/tests/test_models.py index 234b993..009318e 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -2,48 +2,7 @@ import sqlalchemy from tests.conftest import account_model, article_model, comment_model - - -def make_account( - account_username="Xxx__D4RK_V4D0R__xxX", - account_password="987654321abcdefg@", - account_email="C4T@exemple.com", - account_role="user", -): - Account = account_model() - return Account( - account_username=account_username, - account_password=account_password, - account_email=account_email, - account_role=account_role, - ) - -def make_article( - article_author_id, - article_title="Luke, I'm your father !", - article_content=( - 'On the platform, Darth Vader stepped forward and spoke the truth: ' - '"Luke, I am your father." Shocked, Luke backed away, unable to accept it.' - ), -): - Article = article_model() - return Article( - article_author_id=article_author_id, - article_title=article_title, - article_content=article_content, - ) - -def make_comment( - comment_article_id, - comment_written_account_id, - comment_content="Bravo !", -): - Comment = comment_model() - return Comment( - comment_article_id=comment_article_id, - comment_written_account_id=comment_written_account_id, - comment_content=comment_content, - ) +from tests.factories import make_account, make_article, make_comment def test_create_account(db_session): @@ -173,8 +132,7 @@ def test_article_missing_title(db_session): author = make_account(account_role="author") db_session.add(author) db_session.commit() - Article = article_model() - article = Article(article_author_id=author.account_id, article_title=None,) + article = make_article(article_author_id=author.account_id, article_title=None) db_session.add(article) with pytest.raises(sqlalchemy.exc.IntegrityError): db_session.commit() @@ -183,9 +141,7 @@ def test_article_missing_content(db_session): author = make_account(account_role="author") db_session.add(author) db_session.commit() - Article = article_model() - article = Article(article_author_id=author.account_id, article_content=None) + article = make_article(article_author_id=author.account_id, article_content=None) db_session.add(article) with pytest.raises(sqlalchemy.exc.IntegrityError): db_session.commit() - From cca405db4a0bded34d69385b2032bf1b8917753d Mon Sep 17 00:00:00 2001 From: raydeveloppeur-admin Date: Thu, 5 Feb 2026 20:39:25 +0100 Subject: [PATCH 10/16] Create login feature with password authentication in the controller : Add login tests --- tests/test_controllers/test_login.py | 89 ++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 tests/test_controllers/test_login.py diff --git a/tests/test_controllers/test_login.py b/tests/test_controllers/test_login.py new file mode 100644 index 0000000..0f6d033 --- /dev/null +++ b/tests/test_controllers/test_login.py @@ -0,0 +1,89 @@ +from tests.factories import make_account + + +def test_render_login_page(client): + response = client.get("/") + assert response.status_code == 200 + assert b" Date: Thu, 5 Feb 2026 21:37:15 +0100 Subject: [PATCH 11/16] Create login feature with password authentication in the controller : Add PostgreSQL health checks, Poetry cache to improve CI speed and add TEST_SECRET_KEY --- .github/workflows/continuous-integration.yml | 24 ++++++++++++-------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 120a33a..690cd42 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -15,7 +15,8 @@ jobs: POSTGRES_USER: ${{ secrets.POSTGRES_USER }} POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }} POSTGRES_DB: ${{ secrets.POSTGRES_DB }} - # The TEST_DATABASE_URL variable is used in tests/conftest.py + + TEST_SECRET_KEY: ${{ secrets.TEST_SECRET_KEY }} TEST_DATABASE_URL: postgresql://${{ secrets.POSTGRES_USER }}:${{ secrets.POSTGRES_PASSWORD }}@localhost:5432/${{ secrets.POSTGRES_DB }} TEST_DATABASE_JDBC_URL: jdbc:postgresql://localhost:5432/${{ secrets.POSTGRES_DB }} @@ -28,6 +29,11 @@ jobs: POSTGRES_DB: ${{ secrets.POSTGRES_DB }} ports: - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 steps: - name: Checkout code @@ -44,17 +50,17 @@ jobs: - name: Add Poetry to PATH run: echo "$HOME/.local/bin" >> $GITHUB_PATH + - name: Cache Poetry virtualenv + uses: actions/cache@v4 + with: + path: ~/.cache/pypoetry + key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }} + restore-keys: | + ${{ runner.os }}-poetry- + - name: Install dependencies run: poetry install --no-interaction --no-root - - name: Wait for PostgreSQL to be ready - run: | - for i in {1..15}; do - PGPASSWORD="$POSTGRES_PASSWORD" psql -h localhost -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "SELECT 1;" && break - echo "Database not ready yet..." - sleep 2 - done - - name: Run Flyway migrations run: | docker run --rm \ From d0e204381b58821046ea63be164d132c03864057 Mon Sep 17 00:00:00 2001 From: raydeveloppeur-admin Date: Thu, 5 Feb 2026 21:40:58 +0100 Subject: [PATCH 12/16] Create login feature with password authentication in the controller : Prevent missing env var errors by providing dummy DATABASE_URL and SECRET_KEY during test runs --- app/configuration_variables.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/configuration_variables.py b/app/configuration_variables.py index 9281ff0..4868bd7 100644 --- a/app/configuration_variables.py +++ b/app/configuration_variables.py @@ -7,6 +7,11 @@ def get_required_env(name): value = os.getenv(name) if not value: + if os.getenv("TEST_DATABASE_URL") or os.getenv("PYTEST_CURRENT_TEST"): + if name == "DATABASE_URL": + return "postgresql://user:pass@localhost/dummy" + return "dummy_secret_key" + raise OSError(f"Missing required environment variable '{name}' in .env file.") return value From d0d636bca630df2eafc90f54b5173d5a3b82edb0 Mon Sep 17 00:00:00 2001 From: raydeveloppeur-admin Date: Thu, 5 Feb 2026 21:42:07 +0100 Subject: [PATCH 13/16] Create login feature with password authentication in the controller : Fix lint issues --- tests/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index a868616..9d78024 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,11 +19,11 @@ def get_required_env_test(value, name): class ConfigurationVariablesTest: DATABASE_URL = get_required_env_test( - file_env.get("TEST_DATABASE_URL") or os.getenv("TEST_DATABASE_URL"), + file_env.get("TEST_DATABASE_URL") or os.getenv("TEST_DATABASE_URL"), "TEST_DATABASE_URL" ) SECRET_KEY = get_required_env_test( - file_env.get("TEST_SECRET_KEY") or os.getenv("TEST_SECRET_KEY"), + file_env.get("TEST_SECRET_KEY") or os.getenv("TEST_SECRET_KEY"), "TEST_SECRET_KEY" ) From 68ce5393e533be6b22a98dc0fe283444d0017366 Mon Sep 17 00:00:00 2001 From: raydeveloppeur-admin Date: Sat, 7 Feb 2026 19:05:34 +0100 Subject: [PATCH 14/16] Create login feature with password authentication in the controller : Create a Python configuration to load environment variables from .env and .env.test, placed outside the MVC architecture --- app/configuration_variables.py | 20 ------------- configurations/configuration_variables.py | 34 +++++++++++++++++++++++ 2 files changed, 34 insertions(+), 20 deletions(-) delete mode 100644 app/configuration_variables.py create mode 100644 configurations/configuration_variables.py diff --git a/app/configuration_variables.py b/app/configuration_variables.py deleted file mode 100644 index 4868bd7..0000000 --- a/app/configuration_variables.py +++ /dev/null @@ -1,20 +0,0 @@ -import os - -from dotenv import load_dotenv - -load_dotenv() - -def get_required_env(name): - value = os.getenv(name) - if not value: - if os.getenv("TEST_DATABASE_URL") or os.getenv("PYTEST_CURRENT_TEST"): - if name == "DATABASE_URL": - return "postgresql://user:pass@localhost/dummy" - return "dummy_secret_key" - - raise OSError(f"Missing required environment variable '{name}' in .env file.") - return value - -class ConfigurationVariables: - DATABASE_URL = get_required_env("DATABASE_URL") - SECRET_KEY = get_required_env("SECRET_KEY") diff --git a/configurations/configuration_variables.py b/configurations/configuration_variables.py new file mode 100644 index 0000000..78356b5 --- /dev/null +++ b/configurations/configuration_variables.py @@ -0,0 +1,34 @@ +import os +from pathlib import Path + +from dotenv import load_dotenv + +BASE_DIR = Path(__file__).resolve().parent.parent + +load_dotenv(BASE_DIR / ".env") +load_dotenv(BASE_DIR / ".env.test") + +def get_env_variable(name: str): + value = os.getenv(name) + if not value: + raise RuntimeError(f"Missing mandatory environment variable: '{name}'") + return value + +class EnvVariablesConfig: + @property + def database_url(self): + return get_env_variable("DATABASE_URL") + + @property + def secret_key(self): + return get_env_variable("SECRET_KEY") + + @property + def test_database_url(self): + return get_env_variable("TEST_DATABASE_URL") + + @property + def test_secret_key(self): + return get_env_variable("TEST_SECRET_KEY") + +env_vars = EnvVariablesConfig() From d693d6762ec88fd28aa9bc5d792bea77070431a8 Mon Sep 17 00:00:00 2001 From: raydeveloppeur-admin Date: Sat, 7 Feb 2026 19:07:51 +0100 Subject: [PATCH 15/16] Create login feature with password authentication in the controller : Create a Python file outside the MVC architecture to connect to the production or test database. Add robust detection of pytest execution using PYTEST_CURRENT_TEST or 'pytest' in sys.modules to automatically switch to the test database in database.py and __init__.py. --- app/__init__.py | 10 ++++++++-- app/database.py | 7 ------- database/database_setup.py | 15 +++++++++++++++ 3 files changed, 23 insertions(+), 9 deletions(-) delete mode 100644 app/database.py create mode 100644 database/database_setup.py diff --git a/app/__init__.py b/app/__init__.py index 90e3cfd..36f5ab4 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,11 +1,17 @@ +import os +import sys + from flask import Flask -from app.configuration_variables import ConfigurationVariables from app.controllers.login import login_bp +from configurations.configuration_variables import env_vars def initialize_flask_application(): app = Flask(__name__) - app.secret_key = ConfigurationVariables.SECRET_KEY + if os.getenv("PYTEST_CURRENT_TEST") or "pytest" in sys.modules: + app.secret_key = env_vars.test_secret_key + else: + app.secret_key = env_vars.secret_key app.register_blueprint(login_bp) return app diff --git a/app/database.py b/app/database.py deleted file mode 100644 index 077c2be..0000000 --- a/app/database.py +++ /dev/null @@ -1,7 +0,0 @@ -from sqlalchemy import create_engine -from sqlalchemy.orm import declarative_base - -from app.configuration_variables import ConfigurationVariables - -database_engine = create_engine(ConfigurationVariables.DATABASE_URL) -Base = declarative_base() diff --git a/database/database_setup.py b/database/database_setup.py new file mode 100644 index 0000000..a5f58da --- /dev/null +++ b/database/database_setup.py @@ -0,0 +1,15 @@ +import os +import sys + +from sqlalchemy import create_engine +from sqlalchemy.orm import declarative_base + +from configurations.configuration_variables import env_vars + +if os.getenv("PYTEST_CURRENT_TEST") or "pytest" in sys.modules: + database_url = env_vars.test_database_url +else: + database_url = env_vars.database_url + +database_engine = create_engine(database_url) +Base = declarative_base() From 8d0f95305d7903edb8acb2ea6586bf750e720217 Mon Sep 17 00:00:00 2001 From: raydeveloppeur-admin Date: Sat, 7 Feb 2026 19:21:00 +0100 Subject: [PATCH 16/16] Create login feature with password authentication in the controller : Update variables to align with the new changes --- app/controllers/login.py | 2 +- app/models.py | 2 +- tests/conftest.py | 41 ++++++++-------------------------------- 3 files changed, 10 insertions(+), 35 deletions(-) diff --git a/app/controllers/login.py b/app/controllers/login.py index 4a8845a..d8c580d 100644 --- a/app/controllers/login.py +++ b/app/controllers/login.py @@ -2,8 +2,8 @@ from sqlalchemy import select from sqlalchemy.orm import Session -from app.database import database_engine from app.models import Account +from database.database_setup import database_engine login_bp = Blueprint("auth", __name__) diff --git a/app/models.py b/app/models.py index a3506c5..1dffc83 100644 --- a/app/models.py +++ b/app/models.py @@ -9,7 +9,7 @@ ) from sqlalchemy.orm import relationship -from app.database import Base +from database.database_setup import Base class Account(Base): diff --git a/tests/conftest.py b/tests/conftest.py index 9d78024..f70515b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,34 +1,13 @@ -import os - import pytest -from dotenv import dotenv_values -from sqlalchemy import create_engine, text +from sqlalchemy import text from sqlalchemy.orm import sessionmaker -import app.controllers.login as login_controller -import app.database as app_database from app import initialize_flask_application -from app.models import Account, Article, Base, Comment - -file_env = dotenv_values(".env.test") - -def get_required_env_test(value, name): - if not value: - raise OSError(f"Missing required environment variable '{name}' in .env.test file.") - return value +from app.models import Account, Article, Comment +from configurations.configuration_variables import env_vars +from database.database_setup import Base, database_engine -class ConfigurationVariablesTest: - DATABASE_URL = get_required_env_test( - file_env.get("TEST_DATABASE_URL") or os.getenv("TEST_DATABASE_URL"), - "TEST_DATABASE_URL" - ) - SECRET_KEY = get_required_env_test( - file_env.get("TEST_SECRET_KEY") or os.getenv("TEST_SECRET_KEY"), - "TEST_SECRET_KEY" - ) - -engine = create_engine(ConfigurationVariablesTest.DATABASE_URL) -SessionLocal = sessionmaker(bind=engine) +SessionLocal = sessionmaker(bind=database_engine) def account_model(): return Account @@ -46,16 +25,12 @@ def truncate_all_tables(connection): connection.execute(text(f"TRUNCATE {table_names} RESTART IDENTITY CASCADE;")) @pytest.fixture(scope="function") -def app(monkeypatch): +def app(): flask_app = initialize_flask_application() flask_app.config.update({ "TESTING": True, - "SECRET_KEY": ConfigurationVariablesTest.SECRET_KEY, + "SECRET_KEY": env_vars.test_secret_key }) - - monkeypatch.setattr(app_database, "database_engine", engine) - monkeypatch.setattr(login_controller, "database_engine", engine) - return flask_app @pytest.fixture(scope="function") @@ -64,7 +39,7 @@ def client(app): @pytest.fixture(scope="function") def db_session(): - with engine.connect() as connection: + with database_engine.connect() as connection: truncate_all_tables(connection) connection.commit()