From 6b56760f0dc2f8dd75e3d7f40e964b00a53278e8 Mon Sep 17 00:00:00 2001 From: Gaston Valvassori Date: Sun, 30 Mar 2025 15:31:49 -0300 Subject: [PATCH 01/20] feat: github actions CI pipeline --- .coverage | Bin 0 -> 53248 bytes .github/worflows/python-app.yml | 74 +++++++++++++++++++++++++ poetry.lock | 94 +++++++++++++++++++++++++++++--- pyproject.toml | 1 + 4 files changed, 160 insertions(+), 9 deletions(-) create mode 100644 .coverage create mode 100644 .github/worflows/python-app.yml diff --git a/.coverage b/.coverage new file mode 100644 index 0000000000000000000000000000000000000000..74af7f984d96058178eed528e3a5045837e06b15 GIT binary patch literal 53248 zcmeI)?{C{g7zc1W+2STm(3kIGm6E8Rt$+6O@C$SpvLPy7FspEv5 zG(Ye4yaDA()DaaijyzvR7vcwX(Ml8KS4-4+k{zHzly?+x3bB`GZ;D6_Ripw}#aS)( zyg{?peDPDwayB>3OO>`sMlSuVFSLdZ6isoNB%v62LE6k9_7X1)ggRI5BvFGK8`xqry%FAgYpASe+@NP!&iq<)Iz{Fia^}hN z%tNYN5Ng&UBVU>_r*#e{exJy{pZH_$9LkYhI0={KT$VQp^Txh*-`vobMSB;zQ&63=$33qdnGyd-q^{PMWrQL-2$Jc>pm5f0NK!?{J)ITX$F zW9EUzkP#0w5#|Z0rf)RbxUy6S(I18V#aK%tCRQ{+XfDvnhLgbMp%QZ&-0)OI@9&W`!Knubx{TltH%5JI{qZ|dmgeAW1rVHYAStozhm$0YUw)g0C$Bw4eX|0J_u8IPHPhrYyuS?N z*rR2Ym#z3ieN;4#Wz4fs+z|aR94Q%OYXjwY6w@`mSfq8hzAAlOlo4NW8Aqp$v`%_) z;c5Pm2Av*XwB!3S806~}FT+`>H23r101V zeL2X6J9o-liD$>^T5;KG^tnt_;`vH&lC&~uV4cZWxRIihw3ap-=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] -test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] +test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""] trio = ["trio (>=0.26.1)"] [[package]] @@ -126,8 +126,8 @@ files = [ [package.extras] docs = ["Sphinx (>=8.1.3,<8.2.0)", "sphinx-rtd-theme (>=1.2.2)"] -gssauth = ["gssapi", "sspilib"] -test = ["distro (>=1.9.0,<1.10.0)", "flake8 (>=6.1,<7.0)", "flake8-pyi (>=24.1.0,<24.2.0)", "gssapi", "k5test", "mypy (>=1.8.0,<1.9.0)", "sspilib", "uvloop (>=0.15.3)"] +gssauth = ["gssapi ; platform_system != \"Windows\"", "sspilib ; platform_system == \"Windows\""] +test = ["distro (>=1.9.0,<1.10.0)", "flake8 (>=6.1,<7.0)", "flake8-pyi (>=24.1.0,<24.2.0)", "gssapi ; platform_system == \"Linux\"", "k5test ; platform_system == \"Linux\"", "mypy (>=1.8.0,<1.9.0)", "sspilib ; platform_system == \"Windows\"", "uvloop (>=0.15.3) ; platform_system != \"Windows\" and python_version < \"3.14.0\""] [[package]] name = "bcrypt" @@ -279,6 +279,82 @@ files = [ ] markers = {main = "platform_system == \"Windows\"", dev = "platform_system == \"Windows\" or sys_platform == \"win32\""} +[[package]] +name = "coverage" +version = "7.7.1" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "coverage-7.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:553ba93f8e3c70e1b0031e4dfea36aba4e2b51fe5770db35e99af8dc5c5a9dfe"}, + {file = "coverage-7.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:44683f2556a56c9a6e673b583763096b8efbd2df022b02995609cf8e64fc8ae0"}, + {file = "coverage-7.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02fad4f8faa4153db76f9246bc95c1d99f054f4e0a884175bff9155cf4f856cb"}, + {file = "coverage-7.7.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c181ceba2e6808ede1e964f7bdc77bd8c7eb62f202c63a48cc541e5ffffccb6"}, + {file = "coverage-7.7.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80b5b207a8b08c6a934b214e364cab2fa82663d4af18981a6c0a9e95f8df7602"}, + {file = "coverage-7.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:25fe40967717bad0ce628a0223f08a10d54c9d739e88c9cbb0f77b5959367542"}, + {file = "coverage-7.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:881cae0f9cbd928c9c001487bb3dcbfd0b0af3ef53ae92180878591053be0cb3"}, + {file = "coverage-7.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c90e9141e9221dd6fbc16a2727a5703c19443a8d9bf7d634c792fa0287cee1ab"}, + {file = "coverage-7.7.1-cp310-cp310-win32.whl", hash = "sha256:ae13ed5bf5542d7d4a0a42ff5160e07e84adc44eda65ddaa635c484ff8e55917"}, + {file = "coverage-7.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:171e9977c6a5d2b2be9efc7df1126fd525ce7cad0eb9904fe692da007ba90d81"}, + {file = "coverage-7.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1165490be0069e34e4f99d08e9c5209c463de11b471709dfae31e2a98cbd49fd"}, + {file = "coverage-7.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:44af11c00fd3b19b8809487630f8a0039130d32363239dfd15238e6d37e41a48"}, + {file = "coverage-7.7.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fbba59022e7c20124d2f520842b75904c7b9f16c854233fa46575c69949fb5b9"}, + {file = "coverage-7.7.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:af94fb80e4f159f4d93fb411800448ad87b6039b0500849a403b73a0d36bb5ae"}, + {file = "coverage-7.7.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eae79f8e3501133aa0e220bbc29573910d096795882a70e6f6e6637b09522133"}, + {file = "coverage-7.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e33426a5e1dc7743dd54dfd11d3a6c02c5d127abfaa2edd80a6e352b58347d1a"}, + {file = "coverage-7.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b559adc22486937786731dac69e57296cb9aede7e2687dfc0d2696dbd3b1eb6b"}, + {file = "coverage-7.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b838a91e84e1773c3436f6cc6996e000ed3ca5721799e7789be18830fad009a2"}, + {file = "coverage-7.7.1-cp311-cp311-win32.whl", hash = "sha256:2c492401bdb3a85824669d6a03f57b3dfadef0941b8541f035f83bbfc39d4282"}, + {file = "coverage-7.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:1e6f867379fd033a0eeabb1be0cffa2bd660582b8b0c9478895c509d875a9d9e"}, + {file = "coverage-7.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:eff187177d8016ff6addf789dcc421c3db0d014e4946c1cc3fbf697f7852459d"}, + {file = "coverage-7.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2444fbe1ba1889e0b29eb4d11931afa88f92dc507b7248f45be372775b3cef4f"}, + {file = "coverage-7.7.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:177d837339883c541f8524683e227adcaea581eca6bb33823a2a1fdae4c988e1"}, + {file = "coverage-7.7.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15d54ecef1582b1d3ec6049b20d3c1a07d5e7f85335d8a3b617c9960b4f807e0"}, + {file = "coverage-7.7.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75c82b27c56478d5e1391f2e7b2e7f588d093157fa40d53fd9453a471b1191f2"}, + {file = "coverage-7.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:315ff74b585110ac3b7ab631e89e769d294f303c6d21302a816b3554ed4c81af"}, + {file = "coverage-7.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4dd532dac197d68c478480edde74fd4476c6823355987fd31d01ad9aa1e5fb59"}, + {file = "coverage-7.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:385618003e3d608001676bb35dc67ae3ad44c75c0395d8de5780af7bb35be6b2"}, + {file = "coverage-7.7.1-cp312-cp312-win32.whl", hash = "sha256:63306486fcb5a827449464f6211d2991f01dfa2965976018c9bab9d5e45a35c8"}, + {file = "coverage-7.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:37351dc8123c154fa05b7579fdb126b9f8b1cf42fd6f79ddf19121b7bdd4aa04"}, + {file = "coverage-7.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:eebd927b86761a7068a06d3699fd6c20129becf15bb44282db085921ea0f1585"}, + {file = "coverage-7.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2a79c4a09765d18311c35975ad2eb1ac613c0401afdd9cb1ca4110aeb5dd3c4c"}, + {file = "coverage-7.7.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b1c65a739447c5ddce5b96c0a388fd82e4bbdff7251396a70182b1d83631019"}, + {file = "coverage-7.7.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:392cc8fd2b1b010ca36840735e2a526fcbd76795a5d44006065e79868cc76ccf"}, + {file = "coverage-7.7.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9bb47cc9f07a59a451361a850cb06d20633e77a9118d05fd0f77b1864439461b"}, + {file = "coverage-7.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b4c144c129343416a49378e05c9451c34aae5ccf00221e4fa4f487db0816ee2f"}, + {file = "coverage-7.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bc96441c9d9ca12a790b5ae17d2fa6654da4b3962ea15e0eabb1b1caed094777"}, + {file = "coverage-7.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d03287eb03186256999539d98818c425c33546ab4901028c8fa933b62c35c3a"}, + {file = "coverage-7.7.1-cp313-cp313-win32.whl", hash = "sha256:8fed429c26b99641dc1f3a79179860122b22745dd9af36f29b141e178925070a"}, + {file = "coverage-7.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:092b134129a8bb940c08b2d9ceb4459af5fb3faea77888af63182e17d89e1cf1"}, + {file = "coverage-7.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3154b369141c3169b8133973ac00f63fcf8d6dbcc297d788d36afbb7811e511"}, + {file = "coverage-7.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:264ff2bcce27a7f455b64ac0dfe097680b65d9a1a293ef902675fa8158d20b24"}, + {file = "coverage-7.7.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba8480ebe401c2f094d10a8c4209b800a9b77215b6c796d16b6ecdf665048950"}, + {file = "coverage-7.7.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:520af84febb6bb54453e7fbb730afa58c7178fd018c398a8fcd8e269a79bf96d"}, + {file = "coverage-7.7.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88d96127ae01ff571d465d4b0be25c123789cef88ba0879194d673fdea52f54e"}, + {file = "coverage-7.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0ce92c5a9d7007d838456f4b77ea159cb628187a137e1895331e530973dcf862"}, + {file = "coverage-7.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0dab4ef76d7b14f432057fdb7a0477e8bffca0ad39ace308be6e74864e632271"}, + {file = "coverage-7.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7e688010581dbac9cab72800e9076e16f7cccd0d89af5785b70daa11174e94de"}, + {file = "coverage-7.7.1-cp313-cp313t-win32.whl", hash = "sha256:e52eb31ae3afacdacfe50705a15b75ded67935770c460d88c215a9c0c40d0e9c"}, + {file = "coverage-7.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a6b6b3bd121ee2ec4bd35039319f3423d0be282b9752a5ae9f18724bc93ebe7c"}, + {file = "coverage-7.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:34a3bf6b92e6621fc4dcdaab353e173ccb0ca9e4bfbcf7e49a0134c86c9cd303"}, + {file = "coverage-7.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d6874929d624d3a670f676efafbbc747f519a6121b581dd41d012109e70a5ebd"}, + {file = "coverage-7.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ba5ff236c87a7b7aa1441a216caf44baee14cbfbd2256d306f926d16b026578"}, + {file = "coverage-7.7.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:452735fafe8ff5918236d5fe1feac322b359e57692269c75151f9b4ee4b7e1bc"}, + {file = "coverage-7.7.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5f99a93cecf799738e211f9746dc83749b5693538fbfac279a61682ba309387"}, + {file = "coverage-7.7.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:11dd6f52c2a7ce8bf0a5f3b6e4a8eb60e157ffedc3c4b4314a41c1dfbd26ce58"}, + {file = "coverage-7.7.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:b52edb940d087e2a96e73c1523284a2e94a4e66fa2ea1e2e64dddc67173bad94"}, + {file = "coverage-7.7.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d2e73e2ac468536197e6b3ab79bc4a5c9da0f078cd78cfcc7fe27cf5d1195ef0"}, + {file = "coverage-7.7.1-cp39-cp39-win32.whl", hash = "sha256:18f544356bceef17cc55fcf859e5664f06946c1b68efcea6acdc50f8f6a6e776"}, + {file = "coverage-7.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:d66ff48ab3bb6f762a153e29c0fc1eb5a62a260217bc64470d7ba602f5886d20"}, + {file = "coverage-7.7.1-pp39.pp310.pp311-none-any.whl", hash = "sha256:5b7b02e50d54be6114cc4f6a3222fec83164f7c42772ba03b520138859b5fde1"}, + {file = "coverage-7.7.1-py3-none-any.whl", hash = "sha256:822fa99dd1ac686061e1219b67868e25d9757989cf2259f735a4802497d6da31"}, + {file = "coverage-7.7.1.tar.gz", hash = "sha256:199a1272e642266b90c9f40dec7fd3d307b51bf639fa0d15980dc0b3246c1393"}, +] + +[package.extras] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] + [[package]] name = "dnspython" version = "2.7.0" @@ -547,7 +623,7 @@ httpcore = "==1.*" idna = "*" [package.extras] -brotli = ["brotli", "brotlicffi"] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] @@ -697,7 +773,7 @@ pyyaml = ">=5.2" [package.extras] dev = ["jupyter (>=1.0.0)", "libcst[dev-without-jupyter]", "nbsphinx (>=0.4.2)"] -dev-without-jupyter = ["Sphinx (>=5.1.1)", "black (==24.8.0)", "build (>=0.10.0)", "coverage[toml] (>=4.5.4)", "fixit (==2.1.0)", "flake8 (==7.1.2)", "hypothesis (>=4.36.0)", "hypothesmith (>=0.0.4)", "jinja2 (==3.1.5)", "maturin (>=1.7.0,<1.8)", "prompt-toolkit (>=2.0.9)", "pyre-check (==0.9.18)", "setuptools-rust (>=1.5.2)", "setuptools_scm (>=6.0.1)", "slotscheck (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "ufmt (==2.8.0)", "usort (==1.0.8.post1)"] +dev-without-jupyter = ["Sphinx (>=5.1.1)", "black (==24.8.0)", "build (>=0.10.0)", "coverage[toml] (>=4.5.4)", "fixit (==2.1.0)", "flake8 (==7.1.2)", "hypothesis (>=4.36.0)", "hypothesmith (>=0.0.4)", "jinja2 (==3.1.5)", "maturin (>=1.7.0,<1.8)", "prompt-toolkit (>=2.0.9)", "pyre-check (==0.9.18) ; platform_system != \"Windows\"", "setuptools-rust (>=1.5.2)", "setuptools_scm (>=6.0.1)", "slotscheck (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "ufmt (==2.8.0)", "usort (==1.0.8.post1)"] [[package]] name = "mako" @@ -1131,7 +1207,7 @@ typing-extensions = ">=4.12.2" [package.extras] email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] [[package]] name = "pydantic-core" @@ -1760,7 +1836,7 @@ click = ">=7.0" h11 = ">=0.8" [package.extras] -standard = ["colorama (>=0.4)", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] +standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"] [[package]] name = "wcwidth" @@ -1795,4 +1871,4 @@ email = ["email-validator"] [metadata] lock-version = "2.1" python-versions = "^3.13" -content-hash = "4c0aacbb5b043820cf2b113cbbf0cddd031ec84fa694e74427a68c0bc13690fa" +content-hash = "764503cd6bdf89019148e97a8a389851a4a5665a5ae9a5989de0908c69a45f5e" diff --git a/pyproject.toml b/pyproject.toml index 796faa1..925f8f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ python = "^3.13" alembic = "^1.15.1" asyncpg = "^0.30.0" bcrypt = "4.3.0" +coverage = "^7.7.1" email-validator = "^2.2.0" fastapi = "^0.115.11" fastapi-pagination = "^0.12.26" From 8eeb0fa1305dfbc33947c6aec3e20f5cacc54160 Mon Sep 17 00:00:00 2001 From: Gaston Valvassori Date: Sun, 30 Mar 2025 15:36:02 -0300 Subject: [PATCH 02/20] fix: github actions wip, run on all branches --- .github/worflows/python-app.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/worflows/python-app.yml b/.github/worflows/python-app.yml index de2b85c..b7356a6 100644 --- a/.github/worflows/python-app.yml +++ b/.github/worflows/python-app.yml @@ -3,10 +3,10 @@ name: CI Pipeline Template 'on': push: branches: - - feat/github_actions + - '**' pull_request: branches: - - feat/github_actions + - '**' jobs: lint-and-format: @@ -58,7 +58,7 @@ jobs: - name: Test coverage run: | - poetry run coverage run -m pytest src + poetry run coverage run -m pytest poetry run coverage report -m --fail-under=80 docker-build: From 1f37105c9f106eaf158d7b1c29dd105503410daa Mon Sep 17 00:00:00 2001 From: Gaston Valvassori Date: Sun, 30 Mar 2025 15:37:12 -0300 Subject: [PATCH 03/20] fix: typo in workflows folder --- .github/{worflows => workflows}/python-app.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/{worflows => workflows}/python-app.yml (100%) diff --git a/.github/worflows/python-app.yml b/.github/workflows/python-app.yml similarity index 100% rename from .github/worflows/python-app.yml rename to .github/workflows/python-app.yml From fb0d86f6c5f67a44bfc6955971a63cfa81c18ba6 Mon Sep 17 00:00:00 2001 From: Gaston Valvassori Date: Sun, 30 Mar 2025 15:40:45 -0300 Subject: [PATCH 04/20] fix: remove pure pytest step and only use coverage --- .github/workflows/python-app.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index b7356a6..dd52990 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -53,10 +53,6 @@ jobs: poetry install --no-root --with dev - name: Run tests with coverage - run: | - poetry run pytest --cov=src --cov-report=xml --cov-fail-under=80 - - - name: Test coverage run: | poetry run coverage run -m pytest poetry run coverage report -m --fail-under=80 From 0f7d9c540b828bcba70054d7637a02eb5ce3ce5f Mon Sep 17 00:00:00 2001 From: Gaston Valvassori Date: Mon, 31 Mar 2025 09:42:41 -0300 Subject: [PATCH 05/20] fix: wip run tests --- .coverage | Bin 53248 -> 53248 bytes .github/workflows/python-app.yml | 6 +++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.coverage b/.coverage index 74af7f984d96058178eed528e3a5045837e06b15..83bf6ecfcdf336774482cc93127045c2a19c0589 100644 GIT binary patch literal 53248 zcmeI4Uu+yl9mjWX*SoiScRT05XZvE`nvyzh>|9KR(xjnD92=aHsFa2xK=iUcukCH` zZk^k;V@JxLYbsKKDrg^?k`}@Pg%?BwQVUePKm`&J63PPH#v8d?9sb;$pI_d^zz=*-(mb0yiXp z1dsp{`2QwQdoU?yy1MwskGaM~$#knm(X4u}slA^b+jDSCKe%UP|CsI_(>JL)Y(qo( z9=&QG(Pz!7K4q0m-KtDlMZ>i!)4Drj2D#@PbCOPUa15q8nsEy&Pg4bJ5=yz|G)T-= zt+G*F)E_e!2U!OH-CT3Q^$viDX-!vX4tSR=l@RwLt8#qv}%DyQOD%#aa>itC3s1#>RXHYkmYFM}_wg%)f8FI*eYIz%gU$Sdi;SBOr%@C{1(+5N&(;9SKH zu+|IGw}!&Z=gM4p0+2sCSDvMf10}pScJ0+lu2$By{mR~Q+uWcn>-AmMoB?v$qF$-F zigkF-Tx$G5ya-FN0UHxiW_v5oK~$Ox&fyYVZxxI=*Jk?#X#B!3yd6Urk(Y%~(2_S8MuSsj z({Sdh!9a4eK{6;&A0>l6;-7))49QYvPj%-YCInGHX~>=&~>ddozGeGfoiXJ>(i- z*`a%X%61$Jrd2v^IXi-*djE{!&{4?g*C%YdWEvH3Zh#W|9C%O5(U}j|&q}-M8qOls zn4a%vJ_VBt9p(>0rwjTQB+6x@GU?A(bQ<Mnl&j@MJ%P{?se~et=$QAO>^;tpm8VMi)B!C2v01`j~NB{{S0VIF~kN^@0 z2qc9bKKK+MrV3fb{}6zF|KBHnm?J+Y$H+GAUF`>2S-UIs=hU;Q{V6{Aqok7@OuUtN zF;PhLsDDt;s1K_|`K@wR`J!@1{Ehh2@lVLtir!}%)Qw+VMK z=Hb}`Rk%BGOa~u6bJm2%8&ryxNh`jq9b|-P&by}LI{691F+pIY zEqc4nm?d}uh?9rAh?x_+TS22KniFMv(kv}?*WE1*EBMho+6)S*hI|M`@$PhVKLQ%? zzzN{Fmw-Ydnje8e(Qv$o9MeD~VsHc^q1ue3Kqg|Jga@LSv|$ZDKE{)v(b$me-5I^q z#`_YW6mbyr$D2UOwI4HSw;Na2>{KuaDxlQZAQ(6m0zMiCwTMCJS23irM+Ox+S_n~< zki>`t5{!GT8WE9t0~~!V_?H8F zW1teTS9&TTS9bCOcOqJ|gi6!z|NG>P9C=lFUfZmuQ-6v7G5$<^T;7;ElRA)!C4Vgc zSAJQxwfD3iCS9$P978sL zV*TH-TdWBYo%b$m{q;Klq75r#!853)%#(W4xadQKH z1T?($e|l7`B_jF}C@ii2Ndv(Vh=gjRHPE&z*Z--;$D3e*?61L+4Ge<*coQfE>;J^s zo$|v*ZF~?6oC*O~8VEwaiXoNwh**;&h7eT=Nyv@1d}#eIHP9E9um77G^&(Ugu>qvl zum6PxdgXQN|JY99M8wZjLKVU1|KSxb5ihgKmter2_OL^fCP{L5x5))=j)&2 zp5(ay{DUMiu-}PT;t! z&2L|R`n)_Yj-EVuUVbv2j>W~^Ts#SxT9OxjEz1d7KJn~yT&0K93yvfykkS-Qic=fs zC7D{8Z;`;>DgEnX&%a}nLy~whE~eEcIM&g0_T2HyiX_r}BHt;1{fIDs?%y$8lul+l zxl5hN&R?Fa2~v#ai1AIls3;oG?*AuQj=WFaBYz{8$s6SNXN4AlzWPp4KByd9lNB{{S0VIF~kN^@u0!RP} zAOR$>9tqH;Hs9CBjIJ}2%Q4g2%S=xXGaEKAlg%>I-OWr_7c-rm%w#gmbaXJ&-p)*0 z8#Aq~%(SqjJ>T5S_R?u)2w_Ikm`SCWNhX;|B$!cEW)xWZgS+uKGqTK#Br(&}#Ed91 zBM8jIVj_G1z`p;LuHcIY({Kmter2_OL^fCP{L5 Date: Mon, 31 Mar 2025 09:56:05 -0300 Subject: [PATCH 06/20] fix: wip run tests with postgres image --- .github/workflows/python-app.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index dd7d6c4..ae1d4d1 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -51,11 +51,12 @@ jobs: run: | pip install poetry poetry install --no-root --with dev - + - name: Load DB run: | + echo "DATABASE_URL=postgresql://dev:dev@postgres:5432/dev" > .env docker compose -f .devcontainer/docker-compose.yaml -p python-template up postgres - + - name: Run tests with coverage run: | poetry run coverage run -m pytest @@ -71,4 +72,4 @@ jobs: - name: Build Docker image run: | - docker build -t python-template:latest . \ No newline at end of file + docker build -t python-template:latest . From 526853bbfdb3d7fbe45645908e92a7aa564e3e0d Mon Sep 17 00:00:00 2001 From: Gaston Valvassori Date: Mon, 31 Mar 2025 10:02:33 -0300 Subject: [PATCH 07/20] fix: wip run tests with postgres image in the background --- .github/workflows/python-app.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index ae1d4d1..fcd14cc 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -52,13 +52,10 @@ jobs: pip install poetry poetry install --no-root --with dev - - name: Load DB - run: | - echo "DATABASE_URL=postgresql://dev:dev@postgres:5432/dev" > .env - docker compose -f .devcontainer/docker-compose.yaml -p python-template up postgres - - name: Run tests with coverage run: | + echo "DATABASE_URL=postgresql://dev:dev@postgres:5432/dev" > .env + docker compose -f .devcontainer/docker-compose.yaml -p python-template up -d postgres poetry run coverage run -m pytest poetry run coverage report -m --fail-under=80 From d8924e3db8d6d683b37dadd8f631f32bf13b321d Mon Sep 17 00:00:00 2001 From: Gaston Valvassori Date: Mon, 31 Mar 2025 10:05:28 -0300 Subject: [PATCH 08/20] fix: make settings optional --- src/core/config.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/core/config.py b/src/core/config.py index ff8977f..9082076 100755 --- a/src/core/config.py +++ b/src/core/config.py @@ -15,19 +15,19 @@ class LogLevel(str, Enum): class Settings(BaseSettings): # Database database_url: PostgresDsn - async_database_url: PostgresDsn - database_pool_pre_ping: bool - database_pool_size: int - database_pool_recycle: int - database_max_overflow: int + async_database_url: PostgresDsn | None = None + database_pool_pre_ping: bool | None = None + database_pool_size: int | None = None + database_pool_recycle: int | None = None + database_max_overflow: int | None = None # Logging log_level: LogLevel = LogLevel.debug - server_url: str + server_url: str | None = None # Auth - access_token_expire_minutes: float - jwt_signing_key: str + access_token_expire_minutes: float | None = None + jwt_signing_key: str | None = None accept_cookie: bool = True accept_token: bool = True From c94209951ae5e26cd6673b4090076e76c9297fb2 Mon Sep 17 00:00:00 2001 From: Gaston Valvassori Date: Mon, 31 Mar 2025 10:08:42 -0300 Subject: [PATCH 09/20] fix: add all env vars (wip test step) --- .github/workflows/python-app.yml | 14 +++++++++++++- src/core/config.py | 16 ++++++++-------- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index fcd14cc..bc9aeb6 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -54,7 +54,19 @@ jobs: - name: Run tests with coverage run: | - echo "DATABASE_URL=postgresql://dev:dev@postgres:5432/dev" > .env + echo "PROJECT_NAME=python-template" > .env + echo "DATABASE_URL=postgresql://dev:dev@postgres:5432/dev" >> .env + echo "ASYNC_DATABASE_URL=postgresql+asyncpg://dev:dev@postgres:5432/dev" >> .env + echo "DATABASE_POOL_PRE_PING=True" >> .env + echo "DATABASE_POOL_SIZE=5" >> .env + echo "DATABASE_POOL_RECYCLE=3600" >> .env + echo "DATABASE_MAX_OVERFLOW=10" >> .env + echo "LOG_LEVEL=DEBUG" >> .env + echo "SERVER_URL=example.com" >> .env + echo "ACCESS_TOKEN_EXPIRE_MINUTES=15" >> .env + echo "JWT_SIGNING_KEY=" >> .env + echo "CELERY_BROKER_URL=amqp://guest:guest@rabbitmq:5672" >> .env + echo "CELERY_RESULT_BACKEND=redis://redis:6379/0" >> .env docker compose -f .devcontainer/docker-compose.yaml -p python-template up -d postgres poetry run coverage run -m pytest poetry run coverage report -m --fail-under=80 diff --git a/src/core/config.py b/src/core/config.py index 9082076..ff8977f 100755 --- a/src/core/config.py +++ b/src/core/config.py @@ -15,19 +15,19 @@ class LogLevel(str, Enum): class Settings(BaseSettings): # Database database_url: PostgresDsn - async_database_url: PostgresDsn | None = None - database_pool_pre_ping: bool | None = None - database_pool_size: int | None = None - database_pool_recycle: int | None = None - database_max_overflow: int | None = None + async_database_url: PostgresDsn + database_pool_pre_ping: bool + database_pool_size: int + database_pool_recycle: int + database_max_overflow: int # Logging log_level: LogLevel = LogLevel.debug - server_url: str | None = None + server_url: str # Auth - access_token_expire_minutes: float | None = None - jwt_signing_key: str | None = None + access_token_expire_minutes: float + jwt_signing_key: str accept_cookie: bool = True accept_token: bool = True From 354398b531a9063fec64d1a83aa432ec88834049 Mon Sep 17 00:00:00 2001 From: Gaston Valvassori Date: Mon, 31 Mar 2025 10:12:08 -0300 Subject: [PATCH 10/20] fix: still wip in test step --- .github/workflows/python-app.yml | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index bc9aeb6..af7ab42 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -55,21 +55,22 @@ jobs: - name: Run tests with coverage run: | echo "PROJECT_NAME=python-template" > .env - echo "DATABASE_URL=postgresql://dev:dev@postgres:5432/dev" >> .env - echo "ASYNC_DATABASE_URL=postgresql+asyncpg://dev:dev@postgres:5432/dev" >> .env - echo "DATABASE_POOL_PRE_PING=True" >> .env - echo "DATABASE_POOL_SIZE=5" >> .env - echo "DATABASE_POOL_RECYCLE=3600" >> .env - echo "DATABASE_MAX_OVERFLOW=10" >> .env - echo "LOG_LEVEL=DEBUG" >> .env - echo "SERVER_URL=example.com" >> .env - echo "ACCESS_TOKEN_EXPIRE_MINUTES=15" >> .env - echo "JWT_SIGNING_KEY=" >> .env - echo "CELERY_BROKER_URL=amqp://guest:guest@rabbitmq:5672" >> .env - echo "CELERY_RESULT_BACKEND=redis://redis:6379/0" >> .env - docker compose -f .devcontainer/docker-compose.yaml -p python-template up -d postgres - poetry run coverage run -m pytest - poetry run coverage report -m --fail-under=80 + echo "DATABASE_URL=postgresql://dev:dev@postgres:5432/dev" >> .env + echo "ASYNC_DATABASE_URL=postgresql+asyncpg://dev:dev@postgres:5432/dev" >> .env + echo "DATABASE_POOL_PRE_PING=True" >> .env + echo "DATABASE_POOL_SIZE=5" >> .env + echo "DATABASE_POOL_RECYCLE=3600" >> .env + echo "DATABASE_MAX_OVERFLOW=10" >> .env + echo "LOG_LEVEL=DEBUG" >> .env + echo "SERVER_URL=example.com" >> .env + echo "ACCESS_TOKEN_EXPIRE_MINUTES=15" >> .env + echo "JWT_SIGNING_KEY=your-signing-key" >> .env + echo "CELERY_BROKER_URL=amqp://guest:guest@rabbitmq:5672" >> .env + echo "CELERY_RESULT_BACKEND=redis://redis:6379/0" >> .env + docker compose -f .devcontainer/docker-compose.yaml -p python-template up -d postgres + export $(cat .env | xargs) + poetry run coverage run -m pytest + poetry run coverage report -m --fail-under=80 docker-build: name: Build Docker Image From 341b57cf4263272d0d99ee127ae712685327949e Mon Sep 17 00:00:00 2001 From: Gaston Valvassori Date: Mon, 31 Mar 2025 13:50:08 -0300 Subject: [PATCH 11/20] fix: testing preinstalled postgresql db --- .github/workflows/python-app.yml | 41 +++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index af7ab42..07c4062 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -38,6 +38,12 @@ jobs: name: Run Tests runs-on: ubuntu-latest needs: lint-and-format + timeout-minutes: 15 + env: + PGHOST: postgres + PGDATABASE: dev + PGUSERNAME: dev + PGPASSWORD: dev steps: - name: Checkout code uses: actions/checkout@v3 @@ -51,7 +57,39 @@ jobs: run: | pip install poetry poetry install --no-root --with dev + + - name: Add PostgreSQL binaries to PATH + shell: bash + run: | + if [ "$RUNNER_OS" == "Windows" ]; then + echo "$PGBIN" >> $GITHUB_PATH + elif [ "$RUNNER_OS" == "Linux" ]; then + echo "$(pg_config --bindir)" >> $GITHUB_PATH + fi + - name: Start preinstalled PostgreSQL + shell: bash + run: | + echo "Initializing database cluster..." + + export PGHOST="${RUNNER_TEMP//\\//}/postgres" + export PGDATA="$PGHOST/pgdata" + mkdir -p "$PGDATA" + + export PWFILE="$RUNNER_TEMP/pwfile" + echo "postgres" > $PWFILE + initdb --pgdata="$PGDATA" --username="postgres" --pwfile="$PWFILE" + + echo "Starting PostgreSQL..." + echo "unix_socket_directories = '$PGHOST'" >> "$PGDATA/postgresql.conf" + pg_ctl start + + echo "Creating user..." + psql --host "$PGHOST" --username="postgres" --dbname="postgres" --command="CREATE USER $PGUSERNAME PASSWORD '$PGPASSWORD'" --command="\du" + + echo "Creating database..." + createdb --owner="$PGUSERNAME" --username="postgres" "$PGDATABASE" + - name: Run tests with coverage run: | echo "PROJECT_NAME=python-template" > .env @@ -65,9 +103,6 @@ jobs: echo "SERVER_URL=example.com" >> .env echo "ACCESS_TOKEN_EXPIRE_MINUTES=15" >> .env echo "JWT_SIGNING_KEY=your-signing-key" >> .env - echo "CELERY_BROKER_URL=amqp://guest:guest@rabbitmq:5672" >> .env - echo "CELERY_RESULT_BACKEND=redis://redis:6379/0" >> .env - docker compose -f .devcontainer/docker-compose.yaml -p python-template up -d postgres export $(cat .env | xargs) poetry run coverage run -m pytest poetry run coverage report -m --fail-under=80 From f2413f040a99e838258632db1f673bca4bd26552 Mon Sep 17 00:00:00 2001 From: Gaston Valvassori Date: Mon, 31 Mar 2025 14:00:31 -0300 Subject: [PATCH 12/20] fix: testing postgresql service --- .github/workflows/python-app.yml | 77 +++++++++++--------------------- 1 file changed, 27 insertions(+), 50 deletions(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 07c4062..df6b01e 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -39,11 +39,32 @@ jobs: runs-on: ubuntu-latest needs: lint-and-format timeout-minutes: 15 + services: + postgres: + image: postgres:16 + env: + POSTGRES_USER: dev + POSTGRES_PASSWORD: dev + POSTGRES_DB: dev + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U dev -d dev" + --health-interval 10s + --health-timeout 5s + --health-retries 5 env: - PGHOST: postgres - PGDATABASE: dev - PGUSERNAME: dev - PGPASSWORD: dev + PROJECT_NAME: python-template + DATABASE_URL: postgresql://dev:dev@postgres:5432/dev + ASYNC_DATABASE_URL: postgresql+asyncpg://dev:dev@postgres:5432/dev + DATABASE_POOL_PRE_PING: True + DATABASE_POOL_SIZE: 5 + DATABASE_POOL_RECYCLE: 3600 + DATABASE_MAX_OVERFLOW: 10 + LOG_LEVEL: DEBUG + SERVER_URL: example.com + ACCESS_TOKEN_EXPIRE_MINUTES: 15 + JWT_SIGNING_KEY: your-signing-key steps: - name: Checkout code uses: actions/checkout@v3 @@ -58,54 +79,10 @@ jobs: pip install poetry poetry install --no-root --with dev - - - name: Add PostgreSQL binaries to PATH - shell: bash - run: | - if [ "$RUNNER_OS" == "Windows" ]; then - echo "$PGBIN" >> $GITHUB_PATH - elif [ "$RUNNER_OS" == "Linux" ]; then - echo "$(pg_config --bindir)" >> $GITHUB_PATH - fi - - name: Start preinstalled PostgreSQL - shell: bash - run: | - echo "Initializing database cluster..." - - export PGHOST="${RUNNER_TEMP//\\//}/postgres" - export PGDATA="$PGHOST/pgdata" - mkdir -p "$PGDATA" - - export PWFILE="$RUNNER_TEMP/pwfile" - echo "postgres" > $PWFILE - initdb --pgdata="$PGDATA" --username="postgres" --pwfile="$PWFILE" - - echo "Starting PostgreSQL..." - echo "unix_socket_directories = '$PGHOST'" >> "$PGDATA/postgresql.conf" - pg_ctl start - - echo "Creating user..." - psql --host "$PGHOST" --username="postgres" --dbname="postgres" --command="CREATE USER $PGUSERNAME PASSWORD '$PGPASSWORD'" --command="\du" - - echo "Creating database..." - createdb --owner="$PGUSERNAME" --username="postgres" "$PGDATABASE" - - name: Run tests with coverage run: | - echo "PROJECT_NAME=python-template" > .env - echo "DATABASE_URL=postgresql://dev:dev@postgres:5432/dev" >> .env - echo "ASYNC_DATABASE_URL=postgresql+asyncpg://dev:dev@postgres:5432/dev" >> .env - echo "DATABASE_POOL_PRE_PING=True" >> .env - echo "DATABASE_POOL_SIZE=5" >> .env - echo "DATABASE_POOL_RECYCLE=3600" >> .env - echo "DATABASE_MAX_OVERFLOW=10" >> .env - echo "LOG_LEVEL=DEBUG" >> .env - echo "SERVER_URL=example.com" >> .env - echo "ACCESS_TOKEN_EXPIRE_MINUTES=15" >> .env - echo "JWT_SIGNING_KEY=your-signing-key" >> .env - export $(cat .env | xargs) - poetry run coverage run -m pytest - poetry run coverage report -m --fail-under=80 + poetry run coverage run -m pytest + poetry run coverage report -m --fail-under=80 docker-build: name: Build Docker Image From 6db68dcc76dc38228d3f66b8c9e69280c82b2bea Mon Sep 17 00:00:00 2001 From: Gaston Valvassori Date: Mon, 31 Mar 2025 14:07:18 -0300 Subject: [PATCH 13/20] fix: testing postgresql service v2 --- .github/workflows/python-app.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index df6b01e..24299cd 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -55,8 +55,8 @@ jobs: --health-retries 5 env: PROJECT_NAME: python-template - DATABASE_URL: postgresql://dev:dev@postgres:5432/dev - ASYNC_DATABASE_URL: postgresql+asyncpg://dev:dev@postgres:5432/dev + DATABASE_URL: postgresql://dev:dev@localhost:5432/dev + ASYNC_DATABASE_URL: postgresql+asyncpg://dev:dev@localhost:5432/dev DATABASE_POOL_PRE_PING: True DATABASE_POOL_SIZE: 5 DATABASE_POOL_RECYCLE: 3600 From a691519070dc612916a29e2e2684cd2c260adc01 Mon Sep 17 00:00:00 2001 From: Gaston Valvassori Date: Thu, 3 Apr 2025 08:11:27 -0300 Subject: [PATCH 14/20] fix: missing env var --- .github/workflows/python-app.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index c1e93fa..4d71077 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -64,6 +64,7 @@ jobs: LOG_LEVEL: DEBUG SERVER_URL: example.com ACCESS_TOKEN_EXPIRE_MINUTES: 15 + JWT_SIGNING_KEY: your-signing-key steps: - name: Checkout code uses: actions/checkout@v3 From 16f1730c32340cadc21e777e6ff02090bf5fbeea Mon Sep 17 00:00:00 2001 From: Gaston Valvassori Date: Thu, 3 Apr 2025 08:15:22 -0300 Subject: [PATCH 15/20] fix: missing celery env vars --- .github/workflows/python-app.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 4d71077..179d034 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -65,6 +65,8 @@ jobs: SERVER_URL: example.com ACCESS_TOKEN_EXPIRE_MINUTES: 15 JWT_SIGNING_KEY: your-signing-key + CELERY_BROKER_URL: amqp://guest:guest@rabbitmq:5672 + CELERY_RESULT_BACKEND: redis://redis:6379/0 steps: - name: Checkout code uses: actions/checkout@v3 From 417a1a455e612db8bc99e2fcf94b00ea79e6ee12 Mon Sep 17 00:00:00 2001 From: Gaston Valvassori Date: Fri, 4 Apr 2025 14:46:33 -0300 Subject: [PATCH 16/20] feat: add ruff and pre-commit, also add cache to the GA docker build step --- .docs/images/format.png | Bin 36360 -> 25083 bytes .docs/images/pre-commit.png | Bin 0 -> 23889 bytes .flake8 | 6 - .github/workflows/python-app.yml | 43 +- .isort.cfg | 3 - .pre-commit-config.yaml | 32 ++ CODEOWNERS | 2 +- README.md | 24 +- mypy.ini | 4 - poetry.lock | 448 ++++++------------ pyproject.toml | 62 ++- scripts/format.sh | 16 +- src/admin.py | 4 +- src/alembic/env.py | 6 +- .../versions/2023-07-12-5d07fd610995_.py | 2 +- src/api/v1/router.py | 4 +- src/api/v1/routers/item.py | 8 +- src/api/v1/schemas/task.py | 8 +- src/controllers/item.py | 13 +- src/controllers/user.py | 4 +- src/core/database.py | 28 +- src/core/security.py | 16 +- src/tests/test_user.py | 12 +- 23 files changed, 313 insertions(+), 432 deletions(-) create mode 100644 .docs/images/pre-commit.png delete mode 100755 .flake8 delete mode 100755 .isort.cfg create mode 100644 .pre-commit-config.yaml delete mode 100755 mypy.ini diff --git a/.docs/images/format.png b/.docs/images/format.png index 409de27949c95a333075a0006cf9410f845fdae9..7dd3784e294cc10e943b7bcf927b1731a6fc1926 100644 GIT binary patch literal 25083 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
  • wbu*CO$$Q-|y+@8@c+i+y?6 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 literal 36360 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 z EFYkBtM*si- diff --git a/.docs/images/pre-commit.png b/.docs/images/pre-commit.png new file mode 100644 index 0000000000000000000000000000000000000000..32a203f4a3f37bb3c8edf678c109fe2f8bc6525a GIT binary patch literal 23889 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.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] -test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""] +test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] trio = ["trio (>=0.26.1)"] [[package]] @@ -141,8 +141,8 @@ files = [ [package.extras] docs = ["Sphinx (>=8.1.3,<8.2.0)", "sphinx-rtd-theme (>=1.2.2)"] -gssauth = ["gssapi ; platform_system != \"Windows\"", "sspilib ; platform_system == \"Windows\""] -test = ["distro (>=1.9.0,<1.10.0)", "flake8 (>=6.1,<7.0)", "flake8-pyi (>=24.1.0,<24.2.0)", "gssapi ; platform_system == \"Linux\"", "k5test ; platform_system == \"Linux\"", "mypy (>=1.8.0,<1.9.0)", "sspilib ; platform_system == \"Windows\"", "uvloop (>=0.15.3) ; platform_system != \"Windows\" and python_version < \"3.14.0\""] +gssauth = ["gssapi", "sspilib"] +test = ["distro (>=1.9.0,<1.10.0)", "flake8 (>=6.1,<7.0)", "flake8-pyi (>=24.1.0,<24.2.0)", "gssapi", "k5test", "mypy (>=1.8.0,<1.9.0)", "sspilib", "uvloop (>=0.15.3)"] [[package]] name = "bcrypt" @@ -221,51 +221,6 @@ files = [ {file = "billiard-4.2.1.tar.gz", hash = "sha256:12b641b0c539073fc8d3f5b8b7be998956665c4233c7c1fcd66a7e677c4fb36f"}, ] -[[package]] -name = "black" -version = "25.1.0" -description = "The uncompromising code formatter." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32"}, - {file = "black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da"}, - {file = "black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7"}, - {file = "black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9"}, - {file = "black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0"}, - {file = "black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299"}, - {file = "black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096"}, - {file = "black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2"}, - {file = "black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b"}, - {file = "black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc"}, - {file = "black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f"}, - {file = "black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba"}, - {file = "black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f"}, - {file = "black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3"}, - {file = "black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171"}, - {file = "black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18"}, - {file = "black-25.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1ee0a0c330f7b5130ce0caed9936a904793576ef4d2b98c40835d6a65afa6a0"}, - {file = "black-25.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3df5f1bf91d36002b0a75389ca8663510cf0531cca8aa5c1ef695b46d98655f"}, - {file = "black-25.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6827d563a2c820772b32ce8a42828dc6790f095f441beef18f96aa6f8294e"}, - {file = "black-25.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:bacabb307dca5ebaf9c118d2d2f6903da0d62c9faa82bd21a33eecc319559355"}, - {file = "black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717"}, - {file = "black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666"}, -] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -packaging = ">=22.0" -pathspec = ">=0.9.0" -platformdirs = ">=2" - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.10)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - [[package]] name = "celery" version = "5.4.0" @@ -293,32 +248,32 @@ vine = ">=5.1.0,<6.0" arangodb = ["pyArango (>=2.0.2)"] auth = ["cryptography (==42.0.5)"] azureblockblob = ["azure-storage-blob (>=12.15.0)"] -brotli = ["brotli (>=1.0.0) ; platform_python_implementation == \"CPython\"", "brotlipy (>=0.7.0) ; platform_python_implementation == \"PyPy\""] +brotli = ["brotli (>=1.0.0)", "brotlipy (>=0.7.0)"] cassandra = ["cassandra-driver (>=3.25.0,<4)"] consul = ["python-consul2 (==0.1.5)"] cosmosdbsql = ["pydocumentdb (==2.3.5)"] -couchbase = ["couchbase (>=3.0.0) ; platform_python_implementation != \"PyPy\" and (platform_system != \"Windows\" or python_version < \"3.10\")"] +couchbase = ["couchbase (>=3.0.0)"] couchdb = ["pycouchdb (==1.14.2)"] django = ["Django (>=2.2.28)"] dynamodb = ["boto3 (>=1.26.143)"] elasticsearch = ["elastic-transport (<=8.13.0)", "elasticsearch (<=8.13.0)"] -eventlet = ["eventlet (>=0.32.0) ; python_version < \"3.10\""] +eventlet = ["eventlet (>=0.32.0)"] gcs = ["google-cloud-storage (>=2.10.0)"] gevent = ["gevent (>=1.5.0)"] -librabbitmq = ["librabbitmq (>=2.0.0) ; python_version < \"3.11\""] -memcache = ["pylibmc (==1.6.3) ; platform_system != \"Windows\""] +librabbitmq = ["librabbitmq (>=2.0.0)"] +memcache = ["pylibmc (==1.6.3)"] mongodb = ["pymongo[srv] (>=4.0.2)"] msgpack = ["msgpack (==1.0.8)"] pymemcache = ["python-memcached (>=1.61)"] -pyro = ["pyro4 (==4.82) ; python_version < \"3.11\""] +pyro = ["pyro4 (==4.82)"] pytest = ["pytest-celery[all] (>=1.0.0)"] redis = ["redis (>=4.5.2,!=4.5.5,<6.0.0)"] s3 = ["boto3 (>=1.26.143)"] slmq = ["softlayer-messaging (>=1.0.3)"] -solar = ["ephem (==4.1.5) ; platform_python_implementation != \"PyPy\""] +solar = ["ephem (==4.1.5)"] sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"] -sqs = ["boto3 (>=1.26.143)", "kombu[sqs] (>=5.3.4)", "pycurl (>=7.43.0.5) ; sys_platform != \"win32\" and platform_python_implementation == \"CPython\"", "urllib3 (>=1.26.16)"] -tblib = ["tblib (>=1.3.0) ; python_version < \"3.8.0\"", "tblib (>=1.5.0) ; python_version >= \"3.8.0\""] +sqs = ["boto3 (>=1.26.143)", "kombu[sqs] (>=5.3.4)", "pycurl (>=7.43.0.5)", "urllib3 (>=1.26.16)"] +tblib = ["tblib (>=1.3.0)", "tblib (>=1.5.0)"] yaml = ["PyYAML (>=3.10)"] zookeeper = ["kazoo (>=1.3.1)"] zstd = ["zstandard (==0.22.0)"] @@ -350,6 +305,18 @@ files = [ {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, ] +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + [[package]] name = "click" version = "8.1.8" @@ -504,7 +471,19 @@ files = [ ] [package.extras] -toml = ["tomli ; python_full_version <= \"3.11.0a6\""] +toml = ["tomli"] + +[[package]] +name = "distlib" +version = "0.3.9" +description = "Distribution utilities" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, + {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, +] [[package]] name = "dnspython" @@ -617,21 +596,21 @@ sqlmodel = ["sqlakeyset (>=2.0.1680321678,<3.0.0)", "sqlmodel (>=0.0.22)"] tortoise = ["tortoise-orm (>=0.22.0)"] [[package]] -name = "flake8" -version = "7.1.2" -description = "the modular source code checker: pep8 pyflakes and co" +name = "filelock" +version = "3.18.0" +description = "A platform independent file lock." optional = false -python-versions = ">=3.8.1" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "flake8-7.1.2-py2.py3-none-any.whl", hash = "sha256:1cbc62e65536f65e6d754dfe6f1bada7f5cf392d6f5db3c2b85892466c3e7c1a"}, - {file = "flake8-7.1.2.tar.gz", hash = "sha256:c586ffd0b41540951ae41af572e6790dbd49fc12b3aa2541685d253d9bd504bd"}, + {file = "filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de"}, + {file = "filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2"}, ] -[package.dependencies] -mccabe = ">=0.7.0,<0.8.0" -pycodestyle = ">=2.12.0,<2.13.0" -pyflakes = ">=3.2.0,<3.3.0" +[package.extras] +docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"] +typing = ["typing-extensions (>=4.12.2)"] [[package]] name = "flower" @@ -793,7 +772,7 @@ httpcore = "==1.*" idna = "*" [package.extras] -brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] +brotli = ["brotli", "brotlicffi"] cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] @@ -814,6 +793,21 @@ files = [ [package.extras] tests = ["freezegun", "pytest", "pytest-cov"] +[[package]] +name = "identify" +version = "2.6.9" +description = "File identification library for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "identify-2.6.9-py2.py3-none-any.whl", hash = "sha256:c98b4322da415a8e5a70ff6e51fbc2d2932c015532d77e9f8537b4ba7813b150"}, + {file = "identify-2.6.9.tar.gz", hash = "sha256:d40dfe3142a1421d8518e3d3985ef5ac42890683e32306ad614a29490abeb6bf"}, +] + +[package.extras] +license = ["ukkonen"] + [[package]] name = "idna" version = "3.10" @@ -841,22 +835,6 @@ files = [ {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, ] -[[package]] -name = "isort" -version = "6.0.1" -description = "A Python utility / library to sort Python imports." -optional = false -python-versions = ">=3.9.0" -groups = ["dev"] -files = [ - {file = "isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615"}, - {file = "isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450"}, -] - -[package.extras] -colors = ["colorama"] -plugins = ["setuptools"] - [[package]] name = "itsdangerous" version = "2.2.0" @@ -929,7 +907,7 @@ azureservicebus = ["azure-servicebus (>=7.10.0)"] azurestoragequeues = ["azure-identity (>=1.12.0)", "azure-storage-queue (>=12.6.0)"] confluentkafka = ["confluent-kafka (>=2.2.0)"] consul = ["python-consul2 (==0.1.5)"] -librabbitmq = ["librabbitmq (>=2.0.0) ; python_version < \"3.11\""] +librabbitmq = ["librabbitmq (>=2.0.0)"] mongodb = ["pymongo (>=4.1.1)"] msgpack = ["msgpack (==1.1.0)"] pyro = ["pyro4 (==4.82)"] @@ -937,63 +915,10 @@ qpid = ["qpid-python (>=0.26)", "qpid-tools (>=0.26)"] redis = ["redis (>=4.5.2,!=4.5.5,!=5.0.2)"] slmq = ["softlayer-messaging (>=1.0.3)"] sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"] -sqs = ["boto3 (>=1.26.143)", "pycurl (>=7.43.0.5) ; sys_platform != \"win32\" and platform_python_implementation == \"CPython\"", "urllib3 (>=1.26.16)"] +sqs = ["boto3 (>=1.26.143)", "pycurl (>=7.43.0.5)", "urllib3 (>=1.26.16)"] yaml = ["PyYAML (>=3.10)"] zookeeper = ["kazoo (>=2.8.0)"] -[[package]] -name = "libcst" -version = "1.7.0" -description = "A concrete syntax tree with AST-like properties for Python 3.0 through 3.13 programs." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "libcst-1.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:340054c57abcd42953248af18ed278be651a03b1c2a1616f7e1f1ef90b6018ce"}, - {file = "libcst-1.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdae6e632d222d8db7cb98d7cecb45597c21b8e3841d0c98d4fca79c49dad04b"}, - {file = "libcst-1.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71a8f59f3472fe8c0f6e2fad457825ea2ccad8c4c713cca55a91ff2cbfa9bc03"}, - {file = "libcst-1.7.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1560598f5c56681adbd32f4b08e9cffcd45a021921d1d784370a7d4d9a2fac11"}, - {file = "libcst-1.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9cd5ab15b12a37f0e9994d8847d5670da936a93d98672c442a956fab34ea0c15"}, - {file = "libcst-1.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5d5ba9314569865effd5baff3a58ceb2cced52228e181824759c68486a7ec8f4"}, - {file = "libcst-1.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:3d2ec10015e86a4402c3d2084ede6c7c9268faea1ecb99592fe9e291c515aaa2"}, - {file = "libcst-1.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8f6e693281d6e9a62414205fb300ec228ddc902ca9cb965a09f11561dc10aa94"}, - {file = "libcst-1.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e635eadb6043d5f967450af27125811c6ccc7eeb4d8c5fd4f1bece9d96418781"}, - {file = "libcst-1.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c568e14d29489f09faf4915af18235f805d5aa60fa194023b4fadf3209f0c94"}, - {file = "libcst-1.7.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9add619a825d6f176774110d79dc3137f353a236c1e3bcd6e063ca6d93d6e0ae"}, - {file = "libcst-1.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:57a6bcfc8ca8a0bb9e89a2dbf63ee8f0c7e8353a130528dcb47c9e59c2dc8c94"}, - {file = "libcst-1.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5e22738ec2855803f8242e6bf78057389d10f8954db34bf7079c82abab1b8b95"}, - {file = "libcst-1.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:fa519d4391326329f37860c2f2aaf80cb11a6122d14afa2f4f00dde6fcfa7ae4"}, - {file = "libcst-1.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b52692a28d0d958ebfabcf8bfce5fcf2c8582967310d35e6111a6e2d4db96659"}, - {file = "libcst-1.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61bfc90c8a4594296f8b68702f494dfdfec6e745a4abc0cfa8069d7f22061424"}, - {file = "libcst-1.7.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9370c23a3f609280c3f2296d61d34dd32afd7a1c9b19e4e29cc35cb2e2544363"}, - {file = "libcst-1.7.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e50e6960ecc3ed67f39fec63aa329e772d5d27f8e2334e30f19a94aa14489f1"}, - {file = "libcst-1.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ca4e91aa854758040fa6fe7036fbe7f90a36a7d283fa1df8587b6f73084fc997"}, - {file = "libcst-1.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d894c48f682b0061fdb2c983d5e64c30334db6ce0783560dbbb9df0163179c0c"}, - {file = "libcst-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:14e5c1d427c33d50df75be6bc999a7b2d7c6b7840e2361a18a6f354db50cb18e"}, - {file = "libcst-1.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:93417d36c2a1b70d651d0e970ff73339e8dcd64d341672b68823fa0039665022"}, - {file = "libcst-1.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6523731bfbdbc045ff8649130fe14a46b31ad6925f67acdc0e0d80a0c61719fd"}, - {file = "libcst-1.7.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a252fa03ea00986f03100379f11e15d381103a09667900fb0fa2076cec19081a"}, - {file = "libcst-1.7.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09a5530b40a15dbe6fac842ef2ad87ad561760779380ccf3ade6850854d81406"}, - {file = "libcst-1.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0456381c939169c4f11caecdb30f7aca6f234640731f8f965849c1631930536b"}, - {file = "libcst-1.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c8d6176a667d2db0132d133dad6bbf965f915f3071559342ca2cdbbec537ed12"}, - {file = "libcst-1.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:6137fe549bfbb017283c3cf85419eb0dfaa20a211ad6d525538a2494e248a84b"}, - {file = "libcst-1.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3923a341a787c1f454909e726a6213dd59c3db26c6e56d0a1fc4f2f7e96b45d7"}, - {file = "libcst-1.7.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7d9a796c2f3d5b71dd06b7578e8d1fb1c031d2eb8d59e7b40e288752ae1b210"}, - {file = "libcst-1.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:932a4c4508bd4cf5248c99b7218bb86af97d87fefa2bdab7ea8a0c28c270724a"}, - {file = "libcst-1.7.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d12ffe199ff677a37abfb6b21aba1407eb02246dc7e6bcaf4f8e24a195ec4ad6"}, - {file = "libcst-1.7.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:81036e820249937608db7e72d0799180122d40d76d0c0414c454f8aa2ffa9c51"}, - {file = "libcst-1.7.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:94acd51ea1206460c20dea764c59222e62c45ae8a486f22024f063d11a7bca88"}, - {file = "libcst-1.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:c3445dce908fd4971ce9bb5fef5742e26c984027676e3dcf24875fbed1ff7e4c"}, - {file = "libcst-1.7.0.tar.gz", hash = "sha256:a63f44ffa81292f183656234c7f2848653ff45c17d867db83c9335119e28aafa"}, -] - -[package.dependencies] -pyyaml = ">=5.2" - -[package.extras] -dev = ["jupyter (>=1.0.0)", "libcst[dev-without-jupyter]", "nbsphinx (>=0.4.2)"] -dev-without-jupyter = ["Sphinx (>=5.1.1)", "black (==24.8.0)", "build (>=0.10.0)", "coverage[toml] (>=4.5.4)", "fixit (==2.1.0)", "flake8 (==7.1.2)", "hypothesis (>=4.36.0)", "hypothesmith (>=0.0.4)", "jinja2 (==3.1.5)", "maturin (>=1.7.0,<1.8)", "prompt-toolkit (>=2.0.9)", "pyre-check (==0.9.18) ; platform_system != \"Windows\"", "setuptools-rust (>=1.5.2)", "setuptools_scm (>=6.0.1)", "slotscheck (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "ufmt (==2.8.0)", "usort (==1.0.8.post1)"] - [[package]] name = "mako" version = "1.3.9" @@ -1014,31 +939,6 @@ babel = ["Babel"] lingua = ["lingua"] testing = ["pytest"] -[[package]] -name = "markdown-it-py" -version = "3.0.0" -description = "Python port of markdown-it. Markdown parsing, done right!" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, - {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, -] - -[package.dependencies] -mdurl = ">=0.1,<1.0" - -[package.extras] -benchmarking = ["psutil", "pytest", "pytest-benchmark"] -code-style = ["pre-commit (>=3.0,<4.0)"] -compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] -linkify = ["linkify-it-py (>=1,<3)"] -plugins = ["mdit-py-plugins"] -profiling = ["gprof2dot"] -rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] - [[package]] name = "markupsafe" version = "3.0.2" @@ -1110,30 +1010,6 @@ files = [ {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, ] -[[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 = "mdurl" -version = "0.1.2" -description = "Markdown URL utilities" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, - {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, -] - [[package]] name = "mock" version = "5.2.0" @@ -1216,6 +1092,18 @@ files = [ {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] +[[package]] +name = "nodeenv" +version = "1.9.1" +description = "Node.js virtual environment builder" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +files = [ + {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, + {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, +] + [[package]] name = "packaging" version = "24.2" @@ -1262,18 +1150,6 @@ bcrypt = ["bcrypt (>=3.1.0)"] build-docs = ["cloud-sptheme (>=1.10.1)", "sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)"] totp = ["cryptography"] -[[package]] -name = "pathspec" -version = "0.12.1" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, -] - [[package]] name = "platformdirs" version = "4.3.7" @@ -1307,6 +1183,25 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "pre-commit" +version = "4.2.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd"}, + {file = "pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + [[package]] name = "prometheus-client" version = "0.21.1" @@ -1391,37 +1286,6 @@ files = [ {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"}, ] -[[package]] -name = "pycln" -version = "2.5.0" -description = "A formatter for finding and removing unused import statements." -optional = false -python-versions = "<4,>=3.8" -groups = ["dev"] -files = [ - {file = "pycln-2.5.0-py3-none-any.whl", hash = "sha256:6aec7a5b8df47e23399842b1f8470da4164956e26391f9b86c5edced5344da92"}, - {file = "pycln-2.5.0.tar.gz", hash = "sha256:f3a64486f813cd29da07940c4c2bb412080a23b9b0df9b0b1576c8e39ac79c44"}, -] - -[package.dependencies] -libcst = ">=0.3.10" -pathspec = ">=0.9.0" -pyyaml = ">=5.3.1" -tomlkit = ">=0.11.1" -typer = ">=0.4.1" - -[[package]] -name = "pycodestyle" -version = "2.12.1" -description = "Python style guide checker" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3"}, - {file = "pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521"}, -] - [[package]] name = "pydantic" version = "2.10.6" @@ -1441,7 +1305,7 @@ typing-extensions = ">=4.12.2" [package.extras] email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] +timezone = ["tzdata"] [[package]] name = "pydantic-core" @@ -1577,25 +1441,13 @@ azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0 toml = ["tomli (>=2.0.1)"] yaml = ["pyyaml (>=6.0.1)"] -[[package]] -name = "pyflakes" -version = "3.2.0" -description = "passive checker of Python programs" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, - {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, -] - [[package]] name = "pygments" version = "2.19.1" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, @@ -1781,25 +1633,6 @@ files = [ hiredis = ["hiredis (>=3.0.0)"] ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==23.2.1)", "requests (>=2.31.0)"] -[[package]] -name = "rich" -version = "13.9.4" -description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -optional = false -python-versions = ">=3.8.0" -groups = ["dev"] -files = [ - {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, - {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, -] - -[package.dependencies] -markdown-it-py = ">=2.2.0" -pygments = ">=2.13.0,<3.0.0" - -[package.extras] -jupyter = ["ipywidgets (>=7.5.1,<9)"] - [[package]] name = "rsa" version = "4.9" @@ -1816,15 +1649,31 @@ files = [ pyasn1 = ">=0.1.3" [[package]] -name = "shellingham" -version = "1.5.4" -description = "Tool to Detect Surrounding Shell" +name = "ruff" +version = "0.11.3" +description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, - {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, + {file = "ruff-0.11.3-py3-none-linux_armv6l.whl", hash = "sha256:cb893a5eedff45071d52565300a20cd4ac088869e156b25e0971cb98c06f5dd7"}, + {file = "ruff-0.11.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:58edd48af0e201e2f494789de80f5b2f2b46c9a2991a12ea031254865d5f6aa3"}, + {file = "ruff-0.11.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:520f6ade25cea98b2e5cb29eb0906f6a0339c6b8e28a024583b867f48295f1ed"}, + {file = "ruff-0.11.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1ca4405a93ebbc05e924358f872efceb1498c3d52a989ddf9476712a5480b16"}, + {file = "ruff-0.11.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4341d38775a6be605ce7cd50e951b89de65cbd40acb0399f95b8e1524d604c8"}, + {file = "ruff-0.11.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72bf5b49e4b546f4bea6c05448ab71919b09cf75363adf5e3bf5276124afd31c"}, + {file = "ruff-0.11.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:9fa791ee6c3629ba7f9ba2c8f2e76178b03f3eaefb920e426302115259819237"}, + {file = "ruff-0.11.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c81d3fe718f4d303aaa4ccdcd0f43e23bb2127da3353635f718394ca9b26721"}, + {file = "ruff-0.11.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e4c38e9b6c01caaba46b6d8e732791f4c78389a9923319991d55b298017ce02"}, + {file = "ruff-0.11.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9686f5d1a2b4c918b5a6e9876bfe7f47498a990076624d41f57d17aadd02a4dd"}, + {file = "ruff-0.11.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4800ddc4764d42d8961ce4cb972bcf5cc2730d11cca3f11f240d9f7360460408"}, + {file = "ruff-0.11.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e63a2808879361aa9597d88d86380d8fb934953ef91f5ff3dafe18d9cb0b1e14"}, + {file = "ruff-0.11.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8f8b1c4ae62638cc220df440140c21469232d8f2cb7f5059f395f7f48dcdb59e"}, + {file = "ruff-0.11.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3ea2026be50f6b1fbedd2d1757d004e1e58bd0f414efa2a6fa01235468d4c82a"}, + {file = "ruff-0.11.3-py3-none-win32.whl", hash = "sha256:73d8b90d12674a0c6e98cd9e235f2dcad09d1a80e559a585eac994bb536917a3"}, + {file = "ruff-0.11.3-py3-none-win_amd64.whl", hash = "sha256:faf1bfb0a51fb3a82aa1112cb03658796acef978e37c7f807d3ecc50b52ecbf6"}, + {file = "ruff-0.11.3-py3-none-win_arm64.whl", hash = "sha256:67f8b68d7ab909f08af1fb601696925a89d65083ae2bb3ab286e572b5dc456aa"}, + {file = "ruff-0.11.3.tar.gz", hash = "sha256:8d5fcdb3bb359adc12b757ed832ee743993e7474b9de714bb9ea13c4a8458bf9"}, ] [[package]] @@ -1987,18 +1836,6 @@ anyio = ">=3.6.2,<5" [package.extras] full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] -[[package]] -name = "tomlkit" -version = "0.13.2" -description = "Style preserving TOML library" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, - {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, -] - [[package]] name = "tornado" version = "6.4.2" @@ -2020,24 +1857,6 @@ files = [ {file = "tornado-6.4.2.tar.gz", hash = "sha256:92bad5b4746e9879fd7bf1eb21dce4e3fc5128d71601f80005afa39237ad620b"}, ] -[[package]] -name = "typer" -version = "0.15.2" -description = "Typer, build great CLIs. Easy to code. Based on Python type hints." -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc"}, - {file = "typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5"}, -] - -[package.dependencies] -click = ">=8.0.0" -rich = ">=10.11.0" -shellingham = ">=1.3.0" -typing-extensions = ">=3.7.4.3" - [[package]] name = "types-mock" version = "5.2.0.20250306" @@ -2146,7 +1965,7 @@ click = ">=7.0" h11 = ">=0.8" [package.extras] -standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"] +standard = ["colorama (>=0.4)", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] [[package]] name = "vine" @@ -2160,6 +1979,27 @@ files = [ {file = "vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0"}, ] +[[package]] +name = "virtualenv" +version = "20.30.0" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "virtualenv-20.30.0-py3-none-any.whl", hash = "sha256:e34302959180fca3af42d1800df014b35019490b119eba981af27f2fa486e5d6"}, + {file = "virtualenv-20.30.0.tar.gz", hash = "sha256:800863162bcaa5450a6e4d721049730e7f2dae07720e0902b0e4040bd6f9ada8"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + [[package]] name = "wcwidth" version = "0.2.13" @@ -2192,5 +2032,5 @@ email = ["email-validator"] [metadata] lock-version = "2.1" -python-versions = "^3.13" -content-hash = "29a3085693afe62316fe044cfab6632ce77f5cbfc6e248dc536f3e72da15a54e" +python-versions = ">=3.13.0,<4.0.0" +content-hash = "c951875c614543f731f12bc92d55185263749e66dd62db290a0814a62c4cb286" diff --git a/pyproject.toml b/pyproject.toml index 1c23f7d..41dde33 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ version = "0.1.0" description = "Xmartlabs' Python project template" authors = [{ name = "Xmartlabs", email = "getintouch@xmartlabs.com" }] readme = "README.md" -requires-python = "^3.13" +requires-python = ">=3.13.0,<4.0.0" [tool.poetry] # TODO(remer): this can be removed when the source files are moved to project name folder within src @@ -13,7 +13,7 @@ requires-python = "^3.13" packages = [{ include = "*", from = "src" }] [tool.poetry.dependencies] -python = "^3.13" +python = ">=3.13.0,<4.0.0" alembic = "^1.15.1" asyncpg = "^0.30.0" @@ -37,15 +37,13 @@ sqlalchemy = "^2.0.39" uvicorn = "^0.34.0" [tool.poetry.group.dev.dependencies] -black = "^25.1.0" -flake8 = "^7.1.2" flower = "^2.0.1" -isort = "^6.0.1" +mock = "^5.2.0" mypy = "^1.15.0" mypy-extensions = "^1.0.0" -pycln = "^2.5.0" pytest = "^8.3.5" -mock = "^5.2.0" +pre-commit = "^4.2.0" +ruff = "^0.11.3" [tool.poetry.group.types.dependencies] celery-types = "^0.23.0" @@ -59,3 +57,53 @@ typing-extensions = "^4.12.2" [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" + +[tool.ruff] +line-length = 130 +force-exclude = true # Ensure exclusions are respected by the pre-commit hook +extend-exclude = ["src/alembic/versions", "__pycache__", "scripts"] + +[tool.ruff.lint] +extend-select = [ # Defaults: [ "E4", "E7", "E9", "F" ] (https://docs.astral.sh/ruff/rules/#error-e) + "E501", # line-too-long + "I001", # unsorted-imports + "I002", # missing-required-import +] + +[tool.ruff.lint.per-file-ignores] +"__init__.py" = ["F401"] + +[tool.ruff.lint.isort] +known-first-party = ["src"] +known-third-party = ["fastapi", "sqlalchemy", "pydantic"] +force-single-line = false +combine-as-imports = true + +[tool.mypy] +plugins = ["pydantic.mypy", "sqlalchemy.ext.mypy.plugin"] +ignore_missing_imports = true +disallow_untyped_defs = true +warn_unused_ignores = false +no_implicit_optional = true +implicit_reexport = true +explicit_package_bases = true +namespace_packages = true +follow_imports = "silent" +warn_redundant_casts = true +check_untyped_defs = true +no_implicit_reexport = true +disable_error_code = ["name-defined", "call-arg", "attr-defined"] + +[[tool.mypy.overrides]] +module = "starlette_context.plugins" +implicit_reexport = true + +[[tool.mypy.overrides]] +module = "app.middlewares.logging_middleware" +warn_unused_ignores = false + +[tool.pydantic-mypy] +init_forbid_extra = true +init_typed = true +warn_required_dynamic_aliases = false +warn_untyped_fields = true diff --git a/scripts/format.sh b/scripts/format.sh index 2087206..bb090f5 100755 --- a/scripts/format.sh +++ b/scripts/format.sh @@ -1,16 +1,10 @@ #!/bin/bash -printf "Runing pycln...\n" -poetry run python -m pycln src --exclude __init__.py --all - -printf "\nRunning isort...\n" -poetry run python -m isort src - -printf "\nRunning flake8...\n" -poetry run python -m flake8 src - printf "\nRunning mypy...\n" poetry run python -m mypy src -printf "\nRunning black...\n" -poetry run python -m black src --exclude alembic +printf "\nRunning ruff check...\n" +ruff check --fix + +printf "\nRunning ruff format...\n" +ruff format diff --git a/src/admin.py b/src/admin.py index 9f72641..c5c0e6a 100755 --- a/src/admin.py +++ b/src/admin.py @@ -29,9 +29,7 @@ async def logout(self, request: Request) -> bool: return True async def authenticate(self, request: Request) -> RedirectResponse | bool: - failed_auth_response = RedirectResponse( - request.url_for("admin:login"), status_code=302 - ) + failed_auth_response = RedirectResponse(request.url_for("admin:login"), status_code=302) manager = AuthManager() token = request.session.get(AdminAuth.cookie_name) if not token: diff --git a/src/alembic/env.py b/src/alembic/env.py index 6eaa610..2c658e0 100755 --- a/src/alembic/env.py +++ b/src/alembic/env.py @@ -1,8 +1,8 @@ from logging.config import fileConfig +from alembic import context from sqlalchemy import engine_from_config, pool -from alembic import context from src import models # noqa F401 from src.core.config import settings from src.core.database import SQLBase @@ -59,9 +59,7 @@ def run_migrations_online(): ) with connectable.connect() as connection: - context.configure( - connection=connection, target_metadata=target_metadata, compare_type=True - ) + context.configure(connection=connection, target_metadata=target_metadata, compare_type=True) with context.begin_transaction(): context.run_migrations() diff --git a/src/alembic/versions/2023-07-12-5d07fd610995_.py b/src/alembic/versions/2023-07-12-5d07fd610995_.py index a68bfe5..dff84a7 100755 --- a/src/alembic/versions/2023-07-12-5d07fd610995_.py +++ b/src/alembic/versions/2023-07-12-5d07fd610995_.py @@ -1,7 +1,7 @@ """empty message Revision ID: 5d07fd610995 -Revises: +Revises: Create Date: 2023-07-12 18:37:52.159059 """ diff --git a/src/api/v1/router.py b/src/api/v1/router.py index bcff8c9..bd8eef5 100755 --- a/src/api/v1/router.py +++ b/src/api/v1/router.py @@ -5,6 +5,4 @@ v1_router = APIRouter() v1_router.include_router(user.router, prefix="/users") v1_router.include_router(item.router, prefix="/items") -v1_router.include_router( - task.router, tags=["Distributed Tasks Queue - Celery"], prefix="/tasks" -) +v1_router.include_router(task.router, tags=["Distributed Tasks Queue - Celery"], prefix="/tasks") diff --git a/src/api/v1/routers/item.py b/src/api/v1/routers/item.py index 309d453..bbdcf84 100755 --- a/src/api/v1/routers/item.py +++ b/src/api/v1/routers/item.py @@ -14,9 +14,7 @@ @router.get("", response_model=Page[Item]) -def get_items( - user: User = Depends(get_user), session: Session = Depends(db_session) -) -> Any: +def get_items(user: User = Depends(get_user), session: Session = Depends(db_session)) -> Any: return paginate(session, user.get_items()) @@ -45,6 +43,4 @@ async def create_item_async( async_session: AsyncSession = Depends(async_db_session), ) -> Any: """Create items asynchronously.""" - return await ItemController.bulk_create( - items_data=item_data.items, owner_id=user.id, async_session=async_session - ) + return await ItemController.bulk_create(items_data=item_data.items, owner_id=user.id, async_session=async_session) diff --git a/src/api/v1/schemas/task.py b/src/api/v1/schemas/task.py index 5c49c90..9fe9231 100644 --- a/src/api/v1/schemas/task.py +++ b/src/api/v1/schemas/task.py @@ -10,13 +10,9 @@ class TaskCreate(BaseModel): class Task(BaseModel): - task_id: UUID = Field( - ..., description="Task ID", examples=["7ce6afd6-bc66-4db1-bf31-b99e6daa0f11"] - ) + task_id: UUID = Field(..., description="Task ID", examples=["7ce6afd6-bc66-4db1-bf31-b99e6daa0f11"]) class TaskResult(Task): - task_status: str = Field( - ..., description="Task status", examples=["SUCCESS", "FAILURE"] - ) + task_status: str = Field(..., description="Task status", examples=["SUCCESS", "FAILURE"]) task_result: int | None = Field(None, description="Task result", examples=[6, None]) diff --git a/src/controllers/item.py b/src/controllers/item.py index f5f387a..ecda40f 100755 --- a/src/controllers/item.py +++ b/src/controllers/item.py @@ -8,9 +8,7 @@ class ItemController: @staticmethod - def create( - item_data: schemas.ItemCreate, owner_id: UUID, session: Session - ) -> models.Item: + def create(item_data: schemas.ItemCreate, owner_id: UUID, session: Session) -> models.Item: item_data = schemas.Item(owner_id=owner_id, **item_data.model_dump()) item = models.Item.objects(session).create(item_data.model_dump()) return item @@ -21,13 +19,8 @@ async def bulk_create( owner_id: UUID, async_session: AsyncSession, ) -> Sequence[models.Item]: - items_data = [ - schemas.Item(owner_id=owner_id, **item_data.model_dump()) - for item_data in items_data - ] - items = await models.Item.async_objects(async_session).bulk_create( - [item_data.model_dump() for item_data in items_data] - ) + items_data = [schemas.Item(owner_id=owner_id, **item_data.model_dump()) for item_data in items_data] + items = await models.Item.async_objects(async_session).bulk_create([item_data.model_dump() for item_data in items_data]) for item in items: await async_session.refresh(item) diff --git a/src/controllers/user.py b/src/controllers/user.py index 741efdc..3bd57aa 100755 --- a/src/controllers/user.py +++ b/src/controllers/user.py @@ -19,9 +19,7 @@ def create(user_data: UserCreate, session: Session) -> User: @staticmethod def login(user_data: UserCreate, session: Session) -> User: - login_exception = HTTPException( - status_code=401, detail="Invalid email or password" - ) + login_exception = HTTPException(status_code=401, detail="Invalid email or password") user = User.objects(session).get(User.email == user_data.email) if not user: raise login_exception diff --git a/src/core/database.py b/src/core/database.py index de2fc9c..97fa20d 100755 --- a/src/core/database.py +++ b/src/core/database.py @@ -51,9 +51,7 @@ def _on_handle_error(context: ExceptionContext) -> None: Returns: None: this returns nothing. """ - logging.warning( - f"handle_error event triggered for PostgreSQL engine: {context.sqlalchemy_exception}" - ) + logging.warning(f"handle_error event triggered for PostgreSQL engine: {context.sqlalchemy_exception}") if "Can't connect to PostgreSQL server on" in str(context.sqlalchemy_exception): # Setting is_disconnect to True should tell SQLAlchemy treat this as a connection error and retry context.is_disconnect = True # type: ignore @@ -74,9 +72,7 @@ def _on_handle_error(context: ExceptionContext) -> None: def async_session_generator() -> async_sessionmaker[AsyncSession]: - return async_sessionmaker( - autocommit=False, autoflush=False, bind=async_engine, class_=AsyncSession - ) + return async_sessionmaker(autocommit=False, autoflush=False, bind=async_engine, class_=AsyncSession) class SQLBase(DeclarativeBase): @@ -91,9 +87,7 @@ def objects(cls: Type["_Model"], session: Session) -> "Objects[_Model]": return Objects(cls, session) @classmethod - def async_objects( - cls: Type["_Model"], session: AsyncSession - ) -> "AsyncObjects[_Model]": + def async_objects(cls: Type["_Model"], session: AsyncSession) -> "AsyncObjects[_Model]": return AsyncObjects(cls, session) @@ -130,9 +124,7 @@ def get(self, *where_clause: Any) -> _Model | None: def get_or_404(self, *where_clause: Any) -> _Model: obj = self.get(*where_clause) if obj is None: - raise HTTPException( - status_code=404, detail=f"{self.cls.__name__} not found" - ) + raise HTTPException(status_code=404, detail=f"{self.cls.__name__} not found") return obj def get_all(self, *where_clause: Any) -> Sequence[_Model]: @@ -188,9 +180,7 @@ async def get(self, *where_clause: Any) -> _Model | None: async def get_or_404(self, *where_clause: Any) -> _Model: obj = await self.get(*where_clause) if obj is None: - raise HTTPException( - status_code=404, detail=f"{self.cls.__name__} not found" - ) + raise HTTPException(status_code=404, detail=f"{self.cls.__name__} not found") return obj async def get_all(self, *where_clause: Any) -> Sequence[_Model]: @@ -224,14 +214,10 @@ async def bulk_create(self, data: Sequence[Dict[str, Any]]) -> Sequence[_Model]: @declarative_mixin class TableIdMixin: - id: Mapped[uuid.UUID] = mapped_column( - primary_key=True, server_default=random_uuid() - ) + id: Mapped[uuid.UUID] = mapped_column(primary_key=True, server_default=random_uuid()) @declarative_mixin class DatedTableMixin(TableIdMixin): created_at: Mapped[datetime] = mapped_column(server_default=utcnow()) - updated_at: Mapped[datetime] = mapped_column( - server_default=utcnow(), onupdate=datetime.now(timezone.utc) - ) + updated_at: Mapped[datetime] = mapped_column(server_default=utcnow(), onupdate=datetime.now(timezone.utc)) diff --git a/src/core/security.py b/src/core/security.py index 6a443f6..e2b29e7 100755 --- a/src/core/security.py +++ b/src/core/security.py @@ -38,16 +38,10 @@ class AuthManager: accept_header = settings.accept_token @classmethod - def create_access_token( - cls, user: User, expires_delta: timedelta | None = None - ) -> Tuple[str, datetime]: - expires = datetime.now(timezone.utc) + ( - expires_delta or timedelta(minutes=settings.access_token_expire_minutes) - ) + def create_access_token(cls, user: User, expires_delta: timedelta | None = None) -> Tuple[str, datetime]: + expires = datetime.now(timezone.utc) + (expires_delta or timedelta(minutes=settings.access_token_expire_minutes)) claims = {"exp": expires, "user_id": str(user.id)} - token = jwt.encode( - claims=claims, key=settings.jwt_signing_key, algorithm=cls.algorithm - ) + token = jwt.encode(claims=claims, key=settings.jwt_signing_key, algorithm=cls.algorithm) return token, expires @classmethod @@ -65,9 +59,7 @@ def process_login(cls, user: User, response: Response) -> Token | None: def get_user_from_token(self, token: str, session: Session) -> User: try: - payload = jwt.decode( - token=token, key=settings.jwt_signing_key, algorithms=self.algorithm - ) + payload = jwt.decode(token=token, key=settings.jwt_signing_key, algorithms=self.algorithm) token_data = TokenPayload(**payload) except (JWTError, ValidationError): raise self.credentials_exception diff --git a/src/tests/test_user.py b/src/tests/test_user.py index 755c47e..3416cf4 100755 --- a/src/tests/test_user.py +++ b/src/tests/test_user.py @@ -48,9 +48,7 @@ def check_me_response(self, response: Response) -> None: @reset_database def test_signup(self) -> None: - expected_expire = datetime.now(timezone.utc) + timedelta( - minutes=settings.access_token_expire_minutes - ) + expected_expire = datetime.now(timezone.utc) + timedelta(minutes=settings.access_token_expire_minutes) response = client.post(self.SIGNUP_URL, json=self.TEST_PAYLOAD) assert response.status_code == 201 data = response.json() @@ -75,9 +73,7 @@ def test_signup_dup_emails(self) -> None: @reset_database def test_login(self) -> None: client.post(self.SIGNUP_URL, json=self.TEST_PAYLOAD) - expected_expire = datetime.now(timezone.utc) + timedelta( - minutes=settings.access_token_expire_minutes - ) + expected_expire = datetime.now(timezone.utc) + timedelta(minutes=settings.access_token_expire_minutes) response = client.post(self.LOGIN_URL, json=self.TEST_PAYLOAD) assert response.status_code == 200 data = response.json() @@ -127,9 +123,7 @@ def test_me_unauthenticated(self) -> None: def test_me_bad_access_token(self) -> None: client.post(self.SIGNUP_URL, json=self.TEST_PAYLOAD) client.cookies.clear() - response = client.get( - self.ME_URL, headers={AuthManager.header_name: self.BAD_TOKEN} - ) + response = client.get(self.ME_URL, headers={AuthManager.header_name: self.BAD_TOKEN}) assert response.status_code == 401 @reset_database From 3175776b87ac1a05d10012c8f177f2cb5a2ba605 Mon Sep 17 00:00:00 2001 From: Gaston Valvassori Date: Fri, 4 Apr 2025 14:48:22 -0300 Subject: [PATCH 17/20] fix: indentation --- .github/workflows/python-app.yml | 46 ++++++++++++++++---------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 26b7bde..dbc9c72 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -85,30 +85,30 @@ jobs: poetry run coverage report -m --fail-under=80 docker-build: - name: Build Docker Image - runs-on: ubuntu-latest - needs: tests + name: Build Docker Image + runs-on: ubuntu-latest + needs: tests - steps: - - name: Checkout code - uses: actions/checkout@v3 + steps: + - name: Checkout code + uses: actions/checkout@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 - - name: Cache Docker layers - uses: actions/cache@v4 - with: - path: /tmp/.buildx-cache - key: ${{ runner.os }}-docker-${{ github.sha }} - restore-keys: | - ${{ runner.os }}-docker- + - name: Cache Docker layers + uses: actions/cache@v4 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-docker-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-docker- - - name: Build image with cache - uses: docker/build-push-action@v5 - with: - context: . - push: false - tags: python-template:latest - cache-from: type=local,src=/tmp/.buildx-cache - cache-to: type=local,dest=/tmp/.buildx-cache,mode=max + - name: Build image with cache + uses: docker/build-push-action@v5 + with: + context: . + push: false + tags: python-template:latest + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache,mode=max From c178b9394912775ab9dcc7481f88bb74e76fc52b Mon Sep 17 00:00:00 2001 From: Gaston Valvassori Date: Mon, 7 Apr 2025 16:08:34 -0300 Subject: [PATCH 18/20] chore: PR feedback changes --- .github/workflows/python-app.yml | 9 +++---- .gitignore | 1 + poetry.lock | 46 ++++++++++++++++---------------- pyproject.toml | 2 +- 4 files changed, 29 insertions(+), 29 deletions(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index dbc9c72..9deaa82 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -25,12 +25,11 @@ jobs: run: | pip install poetry poetry install --no-root --with dev - + + - name: Install pre-commit + run: pip install pre-commit - name: Run linters and formatters - run: | - poetry run python -m ruff check --fix src - poetry run python -m ruff format --check src - poetry run python -m mypy src --exclude 'src/alembic/' + run: pre-commit run --all-files tests: name: Run Tests diff --git a/.gitignore b/.gitignore index 2f9a178..be2fe66 100755 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ logs/* .ptpython-history .mypy_cache .pytest_cache +.coverage .devcontainer/commandhistory !.devcontainer/commandhistory/.gitkeep diff --git a/poetry.lock b/poetry.lock index f91af28..025730f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. [[package]] name = "alembic" @@ -65,7 +65,7 @@ sniffio = ">=1.1" [package.extras] doc = ["Sphinx (>=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] -test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] +test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""] trio = ["trio (>=0.26.1)"] [[package]] @@ -141,8 +141,8 @@ files = [ [package.extras] docs = ["Sphinx (>=8.1.3,<8.2.0)", "sphinx-rtd-theme (>=1.2.2)"] -gssauth = ["gssapi", "sspilib"] -test = ["distro (>=1.9.0,<1.10.0)", "flake8 (>=6.1,<7.0)", "flake8-pyi (>=24.1.0,<24.2.0)", "gssapi", "k5test", "mypy (>=1.8.0,<1.9.0)", "sspilib", "uvloop (>=0.15.3)"] +gssauth = ["gssapi ; platform_system != \"Windows\"", "sspilib ; platform_system == \"Windows\""] +test = ["distro (>=1.9.0,<1.10.0)", "flake8 (>=6.1,<7.0)", "flake8-pyi (>=24.1.0,<24.2.0)", "gssapi ; platform_system == \"Linux\"", "k5test ; platform_system == \"Linux\"", "mypy (>=1.8.0,<1.9.0)", "sspilib ; platform_system == \"Windows\"", "uvloop (>=0.15.3) ; platform_system != \"Windows\" and python_version < \"3.14.0\""] [[package]] name = "bcrypt" @@ -248,32 +248,32 @@ vine = ">=5.1.0,<6.0" arangodb = ["pyArango (>=2.0.2)"] auth = ["cryptography (==42.0.5)"] azureblockblob = ["azure-storage-blob (>=12.15.0)"] -brotli = ["brotli (>=1.0.0)", "brotlipy (>=0.7.0)"] +brotli = ["brotli (>=1.0.0) ; platform_python_implementation == \"CPython\"", "brotlipy (>=0.7.0) ; platform_python_implementation == \"PyPy\""] cassandra = ["cassandra-driver (>=3.25.0,<4)"] consul = ["python-consul2 (==0.1.5)"] cosmosdbsql = ["pydocumentdb (==2.3.5)"] -couchbase = ["couchbase (>=3.0.0)"] +couchbase = ["couchbase (>=3.0.0) ; platform_python_implementation != \"PyPy\" and (platform_system != \"Windows\" or python_version < \"3.10\")"] couchdb = ["pycouchdb (==1.14.2)"] django = ["Django (>=2.2.28)"] dynamodb = ["boto3 (>=1.26.143)"] elasticsearch = ["elastic-transport (<=8.13.0)", "elasticsearch (<=8.13.0)"] -eventlet = ["eventlet (>=0.32.0)"] +eventlet = ["eventlet (>=0.32.0) ; python_version < \"3.10\""] gcs = ["google-cloud-storage (>=2.10.0)"] gevent = ["gevent (>=1.5.0)"] -librabbitmq = ["librabbitmq (>=2.0.0)"] -memcache = ["pylibmc (==1.6.3)"] +librabbitmq = ["librabbitmq (>=2.0.0) ; python_version < \"3.11\""] +memcache = ["pylibmc (==1.6.3) ; platform_system != \"Windows\""] mongodb = ["pymongo[srv] (>=4.0.2)"] msgpack = ["msgpack (==1.0.8)"] pymemcache = ["python-memcached (>=1.61)"] -pyro = ["pyro4 (==4.82)"] +pyro = ["pyro4 (==4.82) ; python_version < \"3.11\""] pytest = ["pytest-celery[all] (>=1.0.0)"] redis = ["redis (>=4.5.2,!=4.5.5,<6.0.0)"] s3 = ["boto3 (>=1.26.143)"] slmq = ["softlayer-messaging (>=1.0.3)"] -solar = ["ephem (==4.1.5)"] +solar = ["ephem (==4.1.5) ; platform_python_implementation != \"PyPy\""] sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"] -sqs = ["boto3 (>=1.26.143)", "kombu[sqs] (>=5.3.4)", "pycurl (>=7.43.0.5)", "urllib3 (>=1.26.16)"] -tblib = ["tblib (>=1.3.0)", "tblib (>=1.5.0)"] +sqs = ["boto3 (>=1.26.143)", "kombu[sqs] (>=5.3.4)", "pycurl (>=7.43.0.5) ; sys_platform != \"win32\" and platform_python_implementation == \"CPython\"", "urllib3 (>=1.26.16)"] +tblib = ["tblib (>=1.3.0) ; python_version < \"3.8.0\"", "tblib (>=1.5.0) ; python_version >= \"3.8.0\""] yaml = ["PyYAML (>=3.10)"] zookeeper = ["kazoo (>=1.3.1)"] zstd = ["zstandard (==0.22.0)"] @@ -403,7 +403,7 @@ version = "7.7.1" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["dev"] files = [ {file = "coverage-7.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:553ba93f8e3c70e1b0031e4dfea36aba4e2b51fe5770db35e99af8dc5c5a9dfe"}, {file = "coverage-7.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:44683f2556a56c9a6e673b583763096b8efbd2df022b02995609cf8e64fc8ae0"}, @@ -471,7 +471,7 @@ files = [ ] [package.extras] -toml = ["tomli"] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "distlib" @@ -610,7 +610,7 @@ files = [ [package.extras] docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"] -typing = ["typing-extensions (>=4.12.2)"] +typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] [[package]] name = "flower" @@ -772,7 +772,7 @@ httpcore = "==1.*" idna = "*" [package.extras] -brotli = ["brotli", "brotlicffi"] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] @@ -907,7 +907,7 @@ azureservicebus = ["azure-servicebus (>=7.10.0)"] azurestoragequeues = ["azure-identity (>=1.12.0)", "azure-storage-queue (>=12.6.0)"] confluentkafka = ["confluent-kafka (>=2.2.0)"] consul = ["python-consul2 (==0.1.5)"] -librabbitmq = ["librabbitmq (>=2.0.0)"] +librabbitmq = ["librabbitmq (>=2.0.0) ; python_version < \"3.11\""] mongodb = ["pymongo (>=4.1.1)"] msgpack = ["msgpack (==1.1.0)"] pyro = ["pyro4 (==4.82)"] @@ -915,7 +915,7 @@ qpid = ["qpid-python (>=0.26)", "qpid-tools (>=0.26)"] redis = ["redis (>=4.5.2,!=4.5.5,!=5.0.2)"] slmq = ["softlayer-messaging (>=1.0.3)"] sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"] -sqs = ["boto3 (>=1.26.143)", "pycurl (>=7.43.0.5)", "urllib3 (>=1.26.16)"] +sqs = ["boto3 (>=1.26.143)", "pycurl (>=7.43.0.5) ; sys_platform != \"win32\" and platform_python_implementation == \"CPython\"", "urllib3 (>=1.26.16)"] yaml = ["PyYAML (>=3.10)"] zookeeper = ["kazoo (>=2.8.0)"] @@ -1305,7 +1305,7 @@ typing-extensions = ">=4.12.2" [package.extras] email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] [[package]] name = "pydantic-core" @@ -1965,7 +1965,7 @@ click = ">=7.0" h11 = ">=0.8" [package.extras] -standard = ["colorama (>=0.4)", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] +standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"] [[package]] name = "vine" @@ -1998,7 +1998,7 @@ platformdirs = ">=3.9.1,<5" [package.extras] docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"GraalVM\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] [[package]] name = "wcwidth" @@ -2033,4 +2033,4 @@ email = ["email-validator"] [metadata] lock-version = "2.1" python-versions = ">=3.13.0,<4.0.0" -content-hash = "c951875c614543f731f12bc92d55185263749e66dd62db290a0814a62c4cb286" +content-hash = "e0c1203f186e5d3a792b3e7c64b2202ed74ad131e2b1b2877ceb165cbb5dd72a" diff --git a/pyproject.toml b/pyproject.toml index 41dde33..85b9950 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,6 @@ python = ">=3.13.0,<4.0.0" alembic = "^1.15.1" asyncpg = "^0.30.0" bcrypt = "4.3.0" -coverage = "^7.7.1" email-validator = "^2.2.0" celery = "^5.4.0" fastapi = "^0.115.11" @@ -37,6 +36,7 @@ sqlalchemy = "^2.0.39" uvicorn = "^0.34.0" [tool.poetry.group.dev.dependencies] +coverage = "^7.7.1" flower = "^2.0.1" mock = "^5.2.0" mypy = "^1.15.0" From c959516db712be32931ddf557a6343b6c6bbe8e9 Mon Sep 17 00:00:00 2001 From: Gaston Valvassori Date: Mon, 7 Apr 2025 16:16:30 -0300 Subject: [PATCH 19/20] chore: remove .coverage file --- .coverage | Bin 53248 -> 0 bytes .gitignore | 1 + 2 files changed, 1 insertion(+) delete mode 100644 .coverage diff --git a/.coverage b/.coverage deleted file mode 100644 index a5e7db1fec97d70ea3689080579d350946b0ad46..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53248 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 J0`Cfe{{!>3SiJxM diff --git a/.gitignore b/.gitignore index be2fe66..b073d48 100755 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ logs/* .mypy_cache .pytest_cache .coverage +*.coverage .devcontainer/commandhistory !.devcontainer/commandhistory/.gitkeep From 85104380c74c357a7da6ebae62b0ca1d8feaa606 Mon Sep 17 00:00:00 2001 From: Gaston Valvassori Date: Mon, 21 Apr 2025 10:44:39 -0300 Subject: [PATCH 20/20] style: run pre-commit --- src/alembic/env.py | 7 ++--- src/api/dependencies.py | 4 +-- src/api/v1/routers/item.py | 12 ++------ src/api/v1/routers/user.py | 4 +-- src/controllers/item.py | 13 ++------- src/controllers/user.py | 4 +-- src/core/database.py | 20 ++++--------- src/tests/base.py | 4 +-- src/tests/conftest.py | 4 +-- src/tests/test_user.py | 60 ++++++++++---------------------------- 10 files changed, 33 insertions(+), 99 deletions(-) diff --git a/src/alembic/env.py b/src/alembic/env.py index 1f32e5c..041b4a7 100755 --- a/src/alembic/env.py +++ b/src/alembic/env.py @@ -3,15 +3,12 @@ from alembic import context from sqlalchemy import engine_from_config, pool +from sqlalchemy.engine import Connection +from sqlalchemy.ext.asyncio import async_engine_from_config from src import models # noqa F401 from src.core.config import settings from src.core.database import SQLBase -from sqlalchemy import pool -from sqlalchemy.engine import Connection -from sqlalchemy.ext.asyncio import async_engine_from_config - -from alembic import context # this is the Alembic Config object, which provides # access to the values within the .ini file in use. diff --git a/src/api/dependencies.py b/src/api/dependencies.py index 8f5d330..7289ab8 100755 --- a/src/api/dependencies.py +++ b/src/api/dependencies.py @@ -19,8 +19,6 @@ async def db_session() -> AsyncIterator[AsyncSession]: await session.close() -async def get_user( - request: Request, session: AsyncSession = Depends(db_session) -) -> User: +async def get_user(request: Request, session: AsyncSession = Depends(db_session)) -> User: manager = AuthManager() return await manager(request=request, session=session) diff --git a/src/api/v1/routers/item.py b/src/api/v1/routers/item.py index 53e723d..1c47584 100755 --- a/src/api/v1/routers/item.py +++ b/src/api/v1/routers/item.py @@ -14,9 +14,7 @@ @router.get("", response_model=Page[Item]) -async def get_items( - user: User = Depends(get_user), session: AsyncSession = Depends(db_session) -) -> Any: +async def get_items(user: User = Depends(get_user), session: AsyncSession = Depends(db_session)) -> Any: return await paginate(session, user.get_items()) @@ -26,9 +24,7 @@ async def create_item( user: User = Depends(get_user), session: AsyncSession = Depends(db_session), ) -> Any: - return await ItemController.create( - item_data=item_data, owner_id=user.id, session=session - ) + return await ItemController.create(item_data=item_data, owner_id=user.id, session=session) @router.post("/bulk", response_model=list[Item], status_code=201) @@ -38,6 +34,4 @@ async def create_item_async( session: AsyncSession = Depends(db_session), ) -> Any: """Create items asynchronously.""" - return await ItemController.bulk_create( - items_data=item_data.items, owner_id=user.id, async_session=session - ) + return await ItemController.bulk_create(items_data=item_data.items, owner_id=user.id, async_session=session) diff --git a/src/api/v1/routers/user.py b/src/api/v1/routers/user.py index 2c42b0c..a7f7b52 100755 --- a/src/api/v1/routers/user.py +++ b/src/api/v1/routers/user.py @@ -42,8 +42,6 @@ def me(user: models.User = Depends(get_user)) -> Any: @router.get("/{user_id}/items", response_model=Page[Item]) -async def get_public_items( - user_id: UUID, session: AsyncSession = Depends(db_session) -) -> Any: +async def get_public_items(user_id: UUID, session: AsyncSession = Depends(db_session)) -> Any: user = await models.User.objects(session).get_or_404(models.User.id == user_id) return await paginate(session, user.get_public_items()) diff --git a/src/controllers/item.py b/src/controllers/item.py index 45d8dbb..ebe96eb 100755 --- a/src/controllers/item.py +++ b/src/controllers/item.py @@ -8,9 +8,7 @@ class ItemController: @staticmethod - async def create( - item_data: schemas.ItemCreate, owner_id: UUID, session: AsyncSession - ) -> models.Item: + async def create(item_data: schemas.ItemCreate, owner_id: UUID, session: AsyncSession) -> models.Item: item_data = schemas.Item(owner_id=owner_id, **item_data.model_dump()) item = await models.Item.objects(session).create(item_data.model_dump()) await session.refresh(item) @@ -22,13 +20,8 @@ async def bulk_create( owner_id: UUID, async_session: AsyncSession, ) -> Sequence[models.Item]: - items_data = [ - schemas.Item(owner_id=owner_id, **item_data.model_dump()) - for item_data in items_data - ] - items = await models.Item.objects(async_session).bulk_create( - [item_data.model_dump() for item_data in items_data] - ) + items_data = [schemas.Item(owner_id=owner_id, **item_data.model_dump()) for item_data in items_data] + items = await models.Item.objects(async_session).bulk_create([item_data.model_dump() for item_data in items_data]) for item in items: await async_session.refresh(item) diff --git a/src/controllers/user.py b/src/controllers/user.py index 950d3f5..d505bd6 100755 --- a/src/controllers/user.py +++ b/src/controllers/user.py @@ -20,9 +20,7 @@ async def create(user_data: UserCreate, session: AsyncSession) -> User: @staticmethod async def login(user_data: UserCreate, session: AsyncSession) -> User: - login_exception = HTTPException( - status_code=401, detail="Invalid email or password" - ) + login_exception = HTTPException(status_code=401, detail="Invalid email or password") user = await User.objects(session).get(User.email == user_data.email) if not user: raise login_exception diff --git a/src/core/database.py b/src/core/database.py index d7031b3..79c0001 100755 --- a/src/core/database.py +++ b/src/core/database.py @@ -50,9 +50,7 @@ def _on_handle_error(context: ExceptionContext) -> None: Returns: None: this returns nothing. """ - logging.warning( - f"handle_error event triggered for PostgreSQL engine: {context.sqlalchemy_exception}" - ) + logging.warning(f"handle_error event triggered for PostgreSQL engine: {context.sqlalchemy_exception}") if "Can't connect to PostgreSQL server on" in str(context.sqlalchemy_exception): # Setting is_disconnect to True should tell SQLAlchemy treat this as a connection error and retry context.is_disconnect = True # type: ignore @@ -61,9 +59,7 @@ def _on_handle_error(context: ExceptionContext) -> None: def async_session_generator() -> async_sessionmaker[AsyncSession]: - return async_sessionmaker( - autocommit=False, autoflush=False, bind=engine, class_=AsyncSession - ) + return async_sessionmaker(autocommit=False, autoflush=False, bind=engine, class_=AsyncSession) class SQLBase(AsyncAttrs, DeclarativeBase): @@ -113,9 +109,7 @@ async def get(self, *where_clause: Any) -> _Model | None: async def get_or_404(self, *where_clause: Any) -> _Model: obj = await self.get(*where_clause) if obj is None: - raise HTTPException( - status_code=404, detail=f"{self.cls.__name__} not found" - ) + raise HTTPException(status_code=404, detail=f"{self.cls.__name__} not found") return obj async def get_all(self, *where_clause: Any) -> Sequence[_Model]: @@ -149,14 +143,10 @@ async def bulk_create(self, data: Sequence[Dict[str, Any]]) -> Sequence[_Model]: @declarative_mixin class TableIdMixin: - id: Mapped[uuid.UUID] = mapped_column( - primary_key=True, server_default=random_uuid() - ) + id: Mapped[uuid.UUID] = mapped_column(primary_key=True, server_default=random_uuid()) @declarative_mixin class DatedTableMixin(TableIdMixin): created_at: Mapped[datetime] = mapped_column(server_default=utcnow()) - updated_at: Mapped[datetime] = mapped_column( - server_default=utcnow(), onupdate=datetime.now(timezone.utc) - ) + updated_at: Mapped[datetime] = mapped_column(server_default=utcnow(), onupdate=datetime.now(timezone.utc)) diff --git a/src/tests/base.py b/src/tests/base.py index 3dcd891..b43601d 100755 --- a/src/tests/base.py +++ b/src/tests/base.py @@ -21,9 +21,7 @@ def async_session_generator() -> async_sessionmaker[AsyncSession]: - return async_sessionmaker( - autocommit=False, autoflush=False, bind=async_engine, class_=AsyncSession - ) + return async_sessionmaker(autocommit=False, autoflush=False, bind=async_engine, class_=AsyncSession) async def override_get_db() -> AsyncIterator[AsyncSession]: diff --git a/src/tests/conftest.py b/src/tests/conftest.py index 59b412a..5fb4ae8 100644 --- a/src/tests/conftest.py +++ b/src/tests/conftest.py @@ -22,9 +22,7 @@ async def reset_database() -> AsyncGenerator: @pytest.fixture async def async_client() -> AsyncGenerator: - async with AsyncClient( - transport=ASGITransport(app=app), base_url="http://test" - ) as ac: + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac: yield ac diff --git a/src/tests/test_user.py b/src/tests/test_user.py index fc55f25..91a0259 100755 --- a/src/tests/test_user.py +++ b/src/tests/test_user.py @@ -50,12 +50,8 @@ def check_me_response(self, response: Response) -> None: assert data["is_active"] == True # noqa: E712 assert data["is_superuser"] == False # noqa: E712 - async def test_signup( - self, reset_database: AsyncGenerator, async_client: AsyncClient - ) -> None: - expected_expire = datetime.now(timezone.utc) + timedelta( - minutes=settings.access_token_expire_minutes - ) + async def test_signup(self, reset_database: AsyncGenerator, async_client: AsyncClient) -> None: + expected_expire = datetime.now(timezone.utc) + timedelta(minutes=settings.access_token_expire_minutes) response = await async_client.post(self.SIGNUP_URL, json=self.TEST_PAYLOAD) assert response.status_code == 201 @@ -70,22 +66,16 @@ async def test_signup_wrong_shema(self, async_client: AsyncClient) -> None: response = await async_client.post(self.SIGNUP_URL) assert response.status_code == 422 - async def test_signup_dup_emails( - self, reset_database: AsyncGenerator, async_client: AsyncClient - ) -> None: + async def test_signup_dup_emails(self, reset_database: AsyncGenerator, async_client: AsyncClient) -> None: await async_client.post(self.SIGNUP_URL, json=self.TEST_PAYLOAD) response = await async_client.post(self.SIGNUP_URL, json=self.TEST_PAYLOAD) assert response.status_code == 409 data = response.json() assert data["detail"] == "Email address already in use" - async def test_login( - self, reset_database: AsyncGenerator, async_client: AsyncClient - ) -> None: + async def test_login(self, reset_database: AsyncGenerator, async_client: AsyncClient) -> None: await async_client.post(self.SIGNUP_URL, json=self.TEST_PAYLOAD) - expected_expire = datetime.now(timezone.utc) + timedelta( - minutes=settings.access_token_expire_minutes - ) + expected_expire = datetime.now(timezone.utc) + timedelta(minutes=settings.access_token_expire_minutes) response = await async_client.post(self.LOGIN_URL, json=self.TEST_PAYLOAD) assert response.status_code == 200 data = response.json() @@ -99,58 +89,40 @@ async def test_login_wrong_shema(self, async_client: AsyncClient) -> None: response = await async_client.post(self.LOGIN_URL) assert response.status_code == 422 - async def test_login_fail( - self, reset_database: AsyncGenerator, async_client: AsyncClient - ) -> None: + async def test_login_fail(self, reset_database: AsyncGenerator, async_client: AsyncClient) -> None: response = await async_client.post(self.LOGIN_URL, json=self.TEST_PAYLOAD) self.check_login_fail(response=response) - async def test_login_bad_password( - self, reset_database: AsyncGenerator, async_client: AsyncClient - ) -> None: + async def test_login_bad_password(self, reset_database: AsyncGenerator, async_client: AsyncClient) -> None: await async_client.post(self.SIGNUP_URL, json=self.TEST_PAYLOAD) payload = {"email": self.TEST_EMAIL, "password": "oops"} response = await async_client.post(self.LOGIN_URL, json=payload) self.check_login_fail(response=response) - async def test_me_header( - self, reset_database: AsyncGenerator, async_client: AsyncClient - ) -> None: + async def test_me_header(self, reset_database: AsyncGenerator, async_client: AsyncClient) -> None: sign_up_resp = await async_client.post(self.SIGNUP_URL, json=self.TEST_PAYLOAD) data = sign_up_resp.json() token = f"Bearer {data['access_token']}" async_client.cookies.clear() - response = await async_client.get( - self.ME_URL, headers={AuthManager.header_name: token} - ) + response = await async_client.get(self.ME_URL, headers={AuthManager.header_name: token}) self.check_me_response(response=response) - async def test_me_cookie( - self, reset_database: AsyncGenerator, async_client: AsyncClient - ) -> None: + async def test_me_cookie(self, reset_database: AsyncGenerator, async_client: AsyncClient) -> None: await async_client.post(self.SIGNUP_URL, json=self.TEST_PAYLOAD) response = await async_client.get(self.ME_URL) self.check_me_response(response=response) - async def test_me_unauthenticated( - self, reset_database: AsyncGenerator, async_client: AsyncClient - ) -> None: + async def test_me_unauthenticated(self, reset_database: AsyncGenerator, async_client: AsyncClient) -> None: response = await async_client.get(self.ME_URL) assert response.status_code == 401 - async def test_me_bad_access_token( - self, reset_database: AsyncGenerator, async_client: AsyncClient - ) -> None: + async def test_me_bad_access_token(self, reset_database: AsyncGenerator, async_client: AsyncClient) -> None: await async_client.post(self.SIGNUP_URL, json=self.TEST_PAYLOAD) async_client.cookies.clear() - response = await async_client.get( - self.ME_URL, headers={AuthManager.header_name: self.BAD_TOKEN} - ) + response = await async_client.get(self.ME_URL, headers={AuthManager.header_name: self.BAD_TOKEN}) assert response.status_code == 401 - async def test_me_bad_cookie( - self, reset_database: AsyncGenerator, async_client: AsyncClient - ) -> None: + async def test_me_bad_cookie(self, reset_database: AsyncGenerator, async_client: AsyncClient) -> None: await async_client.post(self.SIGNUP_URL, json=self.TEST_PAYLOAD) async_client.cookies.clear() async_client.cookies.set(name=AuthManager.cookie_name, value=self.BAD_TOKEN) @@ -158,9 +130,7 @@ async def test_me_bad_cookie( assert response.status_code == 401 @patch.object(settings, "access_token_expire_minutes", 0.02) - async def test_expired_token( - self, reset_database: AsyncGenerator, async_client: AsyncClient - ) -> None: + async def test_expired_token(self, reset_database: AsyncGenerator, async_client: AsyncClient) -> None: await async_client.post(self.SIGNUP_URL, json=self.TEST_PAYLOAD) time.sleep(3) response = await async_client.get(self.ME_URL)