diff --git a/env-api-dev-example b/env-api-dev-example index 10fcc99..b580ab6 100644 --- a/env-api-dev-example +++ b/env-api-dev-example @@ -1,2 +1,4 @@ APP_ENV=dev DEBUG=true +# SqlAchemy Verbose +SA_ECHO=true diff --git a/poetry.lock b/poetry.lock index 3c516e7..58d1342 100644 --- a/poetry.lock +++ b/poetry.lock @@ -52,13 +52,13 @@ trio = ["trio (<0.22)"] [[package]] name = "async-timeout" -version = "4.0.2" +version = "4.0.3" description = "Timeout context manager for asyncio programs" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, - {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, + {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, + {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, ] [[package]] @@ -453,71 +453,63 @@ development = ["black", "flake8", "mypy", "pytest", "types-colorama"] [[package]] name = "coverage" -version = "7.2.7" +version = "7.3.0" description = "Code coverage measurement for Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, - {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, - {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, - {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, - {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, - {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, - {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, - {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, - {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, - {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, - {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, - {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"}, - {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"}, - {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"}, - {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, - {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, - {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, - {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, - {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, - {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, - {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, - {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, - {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, - {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, + {file = "coverage-7.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:db76a1bcb51f02b2007adacbed4c88b6dee75342c37b05d1822815eed19edee5"}, + {file = "coverage-7.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c02cfa6c36144ab334d556989406837336c1d05215a9bdf44c0bc1d1ac1cb637"}, + {file = "coverage-7.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:477c9430ad5d1b80b07f3c12f7120eef40bfbf849e9e7859e53b9c93b922d2af"}, + {file = "coverage-7.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce2ee86ca75f9f96072295c5ebb4ef2a43cecf2870b0ca5e7a1cbdd929cf67e1"}, + {file = "coverage-7.3.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68d8a0426b49c053013e631c0cdc09b952d857efa8f68121746b339912d27a12"}, + {file = "coverage-7.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b3eb0c93e2ea6445b2173da48cb548364f8f65bf68f3d090404080d338e3a689"}, + {file = "coverage-7.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:90b6e2f0f66750c5a1178ffa9370dec6c508a8ca5265c42fbad3ccac210a7977"}, + {file = "coverage-7.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:96d7d761aea65b291a98c84e1250cd57b5b51726821a6f2f8df65db89363be51"}, + {file = "coverage-7.3.0-cp310-cp310-win32.whl", hash = "sha256:63c5b8ecbc3b3d5eb3a9d873dec60afc0cd5ff9d9f1c75981d8c31cfe4df8527"}, + {file = "coverage-7.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:97c44f4ee13bce914272589b6b41165bbb650e48fdb7bd5493a38bde8de730a1"}, + {file = "coverage-7.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:74c160285f2dfe0acf0f72d425f3e970b21b6de04157fc65adc9fd07ee44177f"}, + {file = "coverage-7.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b543302a3707245d454fc49b8ecd2c2d5982b50eb63f3535244fd79a4be0c99d"}, + {file = "coverage-7.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad0f87826c4ebd3ef484502e79b39614e9c03a5d1510cfb623f4a4a051edc6fd"}, + {file = "coverage-7.3.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:13c6cbbd5f31211d8fdb477f0f7b03438591bdd077054076eec362cf2207b4a7"}, + {file = "coverage-7.3.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fac440c43e9b479d1241fe9d768645e7ccec3fb65dc3a5f6e90675e75c3f3e3a"}, + {file = "coverage-7.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3c9834d5e3df9d2aba0275c9f67989c590e05732439b3318fa37a725dff51e74"}, + {file = "coverage-7.3.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4c8e31cf29b60859876474034a83f59a14381af50cbe8a9dbaadbf70adc4b214"}, + {file = "coverage-7.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7a9baf8e230f9621f8e1d00c580394a0aa328fdac0df2b3f8384387c44083c0f"}, + {file = "coverage-7.3.0-cp311-cp311-win32.whl", hash = "sha256:ccc51713b5581e12f93ccb9c5e39e8b5d4b16776d584c0f5e9e4e63381356482"}, + {file = "coverage-7.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:887665f00ea4e488501ba755a0e3c2cfd6278e846ada3185f42d391ef95e7e70"}, + {file = "coverage-7.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d000a739f9feed900381605a12a61f7aaced6beae832719ae0d15058a1e81c1b"}, + {file = "coverage-7.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:59777652e245bb1e300e620ce2bef0d341945842e4eb888c23a7f1d9e143c446"}, + {file = "coverage-7.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9737bc49a9255d78da085fa04f628a310c2332b187cd49b958b0e494c125071"}, + {file = "coverage-7.3.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5247bab12f84a1d608213b96b8af0cbb30d090d705b6663ad794c2f2a5e5b9fe"}, + {file = "coverage-7.3.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2ac9a1de294773b9fa77447ab7e529cf4fe3910f6a0832816e5f3d538cfea9a"}, + {file = "coverage-7.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:85b7335c22455ec12444cec0d600533a238d6439d8d709d545158c1208483873"}, + {file = "coverage-7.3.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:36ce5d43a072a036f287029a55b5c6a0e9bd73db58961a273b6dc11a2c6eb9c2"}, + {file = "coverage-7.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:211a4576e984f96d9fce61766ffaed0115d5dab1419e4f63d6992b480c2bd60b"}, + {file = "coverage-7.3.0-cp312-cp312-win32.whl", hash = "sha256:56afbf41fa4a7b27f6635bc4289050ac3ab7951b8a821bca46f5b024500e6321"}, + {file = "coverage-7.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f297e0c1ae55300ff688568b04ff26b01c13dfbf4c9d2b7d0cb688ac60df479"}, + {file = "coverage-7.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac0dec90e7de0087d3d95fa0533e1d2d722dcc008bc7b60e1143402a04c117c1"}, + {file = "coverage-7.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:438856d3f8f1e27f8e79b5410ae56650732a0dcfa94e756df88c7e2d24851fcd"}, + {file = "coverage-7.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1084393c6bda8875c05e04fce5cfe1301a425f758eb012f010eab586f1f3905e"}, + {file = "coverage-7.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49ab200acf891e3dde19e5aa4b0f35d12d8b4bd805dc0be8792270c71bd56c54"}, + {file = "coverage-7.3.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a67e6bbe756ed458646e1ef2b0778591ed4d1fcd4b146fc3ba2feb1a7afd4254"}, + {file = "coverage-7.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8f39c49faf5344af36042b293ce05c0d9004270d811c7080610b3e713251c9b0"}, + {file = "coverage-7.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7df91fb24c2edaabec4e0eee512ff3bc6ec20eb8dccac2e77001c1fe516c0c84"}, + {file = "coverage-7.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:34f9f0763d5fa3035a315b69b428fe9c34d4fc2f615262d6be3d3bf3882fb985"}, + {file = "coverage-7.3.0-cp38-cp38-win32.whl", hash = "sha256:bac329371d4c0d456e8d5f38a9b0816b446581b5f278474e416ea0c68c47dcd9"}, + {file = "coverage-7.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:b859128a093f135b556b4765658d5d2e758e1fae3e7cc2f8c10f26fe7005e543"}, + {file = "coverage-7.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fc0ed8d310afe013db1eedd37176d0839dc66c96bcfcce8f6607a73ffea2d6ba"}, + {file = "coverage-7.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61260ec93f99f2c2d93d264b564ba912bec502f679793c56f678ba5251f0393"}, + {file = "coverage-7.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97af9554a799bd7c58c0179cc8dbf14aa7ab50e1fd5fa73f90b9b7215874ba28"}, + {file = "coverage-7.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3558e5b574d62f9c46b76120a5c7c16c4612dc2644c3d48a9f4064a705eaee95"}, + {file = "coverage-7.3.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37d5576d35fcb765fca05654f66aa71e2808d4237d026e64ac8b397ffa66a56a"}, + {file = "coverage-7.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:07ea61bcb179f8f05ffd804d2732b09d23a1238642bf7e51dad62082b5019b34"}, + {file = "coverage-7.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:80501d1b2270d7e8daf1b64b895745c3e234289e00d5f0e30923e706f110334e"}, + {file = "coverage-7.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4eddd3153d02204f22aef0825409091a91bf2a20bce06fe0f638f5c19a85de54"}, + {file = "coverage-7.3.0-cp39-cp39-win32.whl", hash = "sha256:2d22172f938455c156e9af2612650f26cceea47dc86ca048fa4e0b2d21646ad3"}, + {file = "coverage-7.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:60f64e2007c9144375dd0f480a54d6070f00bb1a28f65c408370544091c9bc9e"}, + {file = "coverage-7.3.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:5492a6ce3bdb15c6ad66cb68a0244854d9917478877a25671d70378bdc8562d0"}, + {file = "coverage-7.3.0.tar.gz", hash = "sha256:49dbb19cdcafc130f597d9e04a29d0a032ceedf729e41b181f51cd170e6ee865"}, ] [package.extras] @@ -709,6 +701,20 @@ six = ">=1.9.0" gmpy = ["gmpy"] gmpy2 = ["gmpy2"] +[[package]] +name = "faker" +version = "19.3.0" +description = "Faker is a Python package that generates fake data for you." +optional = false +python-versions = ">=3.8" +files = [ + {file = "Faker-19.3.0-py3-none-any.whl", hash = "sha256:bee54278d6e1289573317604ab6f4782acca724396bf261eaf1890de228e553d"}, + {file = "Faker-19.3.0.tar.gz", hash = "sha256:7d6ed00de3eef9bd57504500c67ee034cab959e4248f9c24aca33e08af82ca93"}, +] + +[package.dependencies] +python-dateutil = ">=2.4" + [[package]] name = "fastapi" version = "0.100.1" @@ -1634,6 +1640,20 @@ pytest = ">=7.3.1" [package.extras] test = ["coverage (>=7.2.7)", "pytest-mock (>=3.10)"] +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] + +[package.dependencies] +six = ">=1.5" + [[package]] name = "python-dotenv" version = "1.0.0" @@ -2279,13 +2299,13 @@ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", [[package]] name = "virtualenv" -version = "20.24.2" +version = "20.24.3" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.24.2-py3-none-any.whl", hash = "sha256:43a3052be36080548bdee0b42919c88072037d50d56c28bd3f853cbe92b953ff"}, - {file = "virtualenv-20.24.2.tar.gz", hash = "sha256:fd8a78f46f6b99a67b7ec5cf73f92357891a7b3a40fd97637c27f854aae3b9e0"}, + {file = "virtualenv-20.24.3-py3-none-any.whl", hash = "sha256:95a6e9398b4967fbcb5fef2acec5efaf9aa4972049d9ae41f95e0972a683fd02"}, + {file = "virtualenv-20.24.3.tar.gz", hash = "sha256:e5c3b4ce817b0b328af041506a2a299418c98747c4b1e68cb7527e74ced23efc"}, ] [package.dependencies] @@ -2410,4 +2430,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "c9aa673c19eeb88f8c1b6a9996d23dd6d17315cbdc9d6741a7ac2de3e7dec79c" +content-hash = "c7a78d11bbfdec346fdabd1542da4eeecf8654f8ca27fdc2dbd445856702addd" diff --git a/pyproject.toml b/pyproject.toml index 3679af5..693135d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -148,6 +148,7 @@ pytest-asyncio = "^0.21.0" poetry-types = "^0.4.0" beartype = "^0.14.1" types-redis = "^4.6.0.2" +faker = "^19.3.0" [build-system] diff --git a/src/app/app_container.py b/src/app/app_container.py index 9a7d06c..7c42e55 100644 --- a/src/app/app_container.py +++ b/src/app/app_container.py @@ -3,7 +3,8 @@ from core.di.mixins import CoreContainerMixin from filmin.di.mixins import FilminContainerMixin from infra.cache.ports import AbstractCacheRepository -from infra.database.sqlalchemy.session import AbstractDatabase +from infra.cache.redis_cache import RedisCache +from infra.database.sqlalchemy.session import AbstractDatabase, Database from utils.di import DIContainer, di_singleton @@ -12,14 +13,10 @@ class AppContainerMixin: cache_repository: AbstractCacheRepository def _get_db(self) -> AbstractDatabase: - from infra.database.sqlalchemy.session import Database - return Database() @di_singleton def _get_cache_repository(self) -> AbstractCacheRepository: - from infra.cache.redis_cache import RedisCache - return RedisCache( url=settings.REDIS_URL, user=settings.REDIS_USER, diff --git a/src/app/asgi.py b/src/app/asgi.py index 67103f6..5b5e041 100644 --- a/src/app/asgi.py +++ b/src/app/asgi.py @@ -11,7 +11,7 @@ from auth.adapters.api.http.router import router as auth_router from config import settings from core.adapters.api.http.router import router as core_router -from filmin.api.router import router as filmin_router +from filmin.adapters.api.http.router import router as filmin_router from shared.exceptions import APPExceptionError diff --git a/src/filmin/api/__init__.py b/src/filmin/adapters/__init__.py similarity index 100% rename from src/filmin/api/__init__.py rename to src/filmin/adapters/__init__.py diff --git a/src/filmin/api/schemas/__init__.py b/src/filmin/adapters/api/__init__.py similarity index 100% rename from src/filmin/api/schemas/__init__.py rename to src/filmin/adapters/api/__init__.py diff --git a/src/filmin/data/__init__.py b/src/filmin/adapters/api/http/__init__.py similarity index 100% rename from src/filmin/data/__init__.py rename to src/filmin/adapters/api/http/__init__.py diff --git a/src/filmin/api/genre.py b/src/filmin/adapters/api/http/genre.py similarity index 56% rename from src/filmin/api/genre.py rename to src/filmin/adapters/api/http/genre.py index 35d438b..e417a58 100644 --- a/src/filmin/api/genre.py +++ b/src/filmin/adapters/api/http/genre.py @@ -1,33 +1,30 @@ -from typing import Dict, List, Union - from fastapi import Depends from fastapi.responses import JSONResponse from fastapi.routing import APIRouter +from app.app_container import AppContainer from app.exceptions import EmptyPayloadExceptionError from app.schemas import Session from app.session_deps import check_access_token, is_admin_session -from filmin.api.schemas.genre import ( +from filmin.adapters.api.http.presenters.genre import GenrePagedListPresenter, GenrePresenter +from filmin.adapters.api.http.schemas.genre import ( CreateGenreRequestDTO, CreateGenreResponseDTO, + GenreListPagedResponse, + GenreResponse, UpdateGenreRequestDTO, UpdatePartialGenreRequestDTO, ) -from filmin.data.repositories.ports.genre import AbstractGenreRepository -from filmin.domain.schemas.genre import Genre +from filmin.domain.entities.genre import Genre +from filmin.domain.services.genre import GenreService +from filmin.domain.use_cases.genre import CreateGenreUseCase, GetGenresUseCase, GetGenreUseCase, UpdateGenreUseCase from filmin.schemas.genre import CreateGenreInDTO, UpdatePartialGenreInDTO -from shared.api.schemas.page import PagedResponseSchema, PageParams +from shared.api.schemas.page import PageParams router = APIRouter(prefix='/genre') -def get_genre_repository() -> AbstractGenreRepository: - from app.app_container import AppContainer - - return AppContainer().genre_repository - - @router.get( '/{code}', responses={ @@ -38,32 +35,33 @@ def get_genre_repository() -> AbstractGenreRepository: ) async def get_genre( code: str, - genre_repository: AbstractGenreRepository = Depends(get_genre_repository), + container: AppContainer = Depends(AppContainer), _: Session = Depends(check_access_token), -) -> Genre: - return await genre_repository.get_by_id(code) +) -> GenreResponse: + service = GenreService(container.genre_repository) + presenter = GenrePresenter() + usecase = GetGenreUseCase(presenter, service) + await usecase.execute(code) + return presenter.result @router.get( '', - response_model=PagedResponseSchema[Genre], responses={ 200: {'description': 'Successful Response'}, 422: {'description': 'Unprocessable Entity'}, }, ) -async def list_genre( +async def list_genres( page_params: PageParams = Depends(), - genre_repository: AbstractGenreRepository = Depends(get_genre_repository), + container: AppContainer = Depends(AppContainer), _: Session = Depends(check_access_token), -) -> Dict[str, Union[int, List[Genre]]]: - count, items = await genre_repository.get_xpage(**page_params.model_dump()) - return { - 'total': count, - 'results': items, - 'page': page_params.page, - 'size': page_params.size, - } +) -> GenreListPagedResponse: + service = GenreService(container.genre_repository) + presenter = GenrePagedListPresenter(page_params) + usecase = GetGenresUseCase(presenter, service) + await usecase.execute(page_params) + return presenter.result @router.post( @@ -78,11 +76,15 @@ async def list_genre( ) async def create_genre( request_data: CreateGenreRequestDTO, - genre_repository: AbstractGenreRepository = Depends(get_genre_repository), + container: AppContainer = Depends(AppContainer), _: Session = Depends(is_admin_session), ) -> Genre: in_dto = CreateGenreInDTO.model_validate(request_data.model_dump()) - return await genre_repository.create(in_dto) + service = GenreService(container.genre_repository) + presenter = GenrePresenter() + usecase = CreateGenreUseCase(presenter, service) + await usecase.execute(in_dto) + return presenter.result @router.put( @@ -96,12 +98,16 @@ async def create_genre( async def update_genre( code: str, request_data: UpdateGenreRequestDTO, - genre_repository: AbstractGenreRepository = Depends(get_genre_repository), + container: AppContainer = Depends(AppContainer), _: Session = Depends(is_admin_session), -) -> Genre: +) -> GenreResponse: in_data = request_data.model_dump() in_dto = UpdatePartialGenreInDTO.model_validate(in_data) - return await genre_repository.update(code, in_dto) + service = GenreService(container.genre_repository) + presenter = GenrePresenter() + usecase = UpdateGenreUseCase(presenter, service) + await usecase.execute(code, in_dto) + return presenter.result @router.patch( @@ -115,11 +121,16 @@ async def update_genre( async def update_genre_partially( code: str, request_data: UpdatePartialGenreRequestDTO, - genre_repository: AbstractGenreRepository = Depends(get_genre_repository), + container: AppContainer = Depends(AppContainer), _: Session = Depends(is_admin_session), -) -> Genre: +) -> GenreResponse: in_data = request_data.model_dump(exclude_unset=True) if not in_data.keys(): raise EmptyPayloadExceptionError() in_dto = UpdatePartialGenreInDTO.model_validate(in_data) - return await genre_repository.update(code, in_dto) + + service = GenreService(container.genre_repository) + presenter = GenrePresenter() + usecase = UpdateGenreUseCase(presenter, service) + await usecase.execute(code, in_dto) + return presenter.result diff --git a/src/filmin/adapters/api/http/movie.py b/src/filmin/adapters/api/http/movie.py new file mode 100644 index 0000000..482ee36 --- /dev/null +++ b/src/filmin/adapters/api/http/movie.py @@ -0,0 +1,145 @@ +from typing import Annotated +import uuid + +from fastapi import Depends +from fastapi.responses import JSONResponse +from fastapi.routing import APIRouter + +from app.app_container import AppContainer +from app.exceptions import EmptyPayloadExceptionError +from app.schemas import Session +from app.session_deps import is_admin_session +from filmin.adapters.api.http.presenters.movie import ( + MoviePagedListPresenter, + MoviePresenter, +) +from filmin.adapters.api.http.schemas.movie import ( + CreateMovieRequestDTO, + CreateMovieResponseDTO, + MovieListPagedResponse, + MovieResponse, + UpdateMovieRequestDTO, + UpdatePartialMovieRequestDTO, +) +from filmin.domain.entities.movie import Movie +from filmin.domain.services.movie import MovieService +from filmin.domain.use_cases.movie import ( + CreateMovieUseCase, + GetMoviesUseCase, + GetMovieUseCase, + UpdateMovieUseCase, +) +from filmin.schemas.movie import CreateMovieInDTO, UpdatePartialMovieInDTO +from shared.api.schemas.page import PageParams + + +router = APIRouter(prefix='/movie') + + +@router.get( + '/{uuid}', + responses={ + 200: {'description': 'Successful Response'}, + 404: {'description': 'Movie not found'}, + 422: {'description': 'Unprocessable Entity'}, + }, +) +async def get_movie( + uuid: Annotated[str, uuid.UUID], + container: AppContainer = Depends(AppContainer), +) -> MovieResponse: + service = MovieService(container.movie_repository) + presenter = MoviePresenter() + usecase = GetMovieUseCase(presenter, service) + await usecase.execute(uuid) + return presenter.result + + +@router.get( + '', + responses={ + 200: {'description': 'Successful Response'}, + 422: {'description': 'Unprocessable Entity'}, + }, +) +async def list_movies( + page_params: PageParams = Depends(), + container: AppContainer = Depends(AppContainer), +) -> MovieListPagedResponse: + service = MovieService(container.movie_repository) + presenter = MoviePagedListPresenter(page_params) + usecase = GetMoviesUseCase(presenter, service) + await usecase.execute(page_params) + return presenter.result + + +@router.post( + '', + response_class=JSONResponse, + response_model=CreateMovieResponseDTO, + status_code=201, + responses={ + 201: {'description': 'Item created'}, + 422: {'description': 'Unprocessable Entity'}, + }, +) +async def create_movie( + request_data: CreateMovieRequestDTO, + container: AppContainer = Depends(AppContainer), + _: Session = Depends(is_admin_session), +) -> Movie: + in_dto = CreateMovieInDTO.model_validate(request_data.model_dump()) + service = MovieService(container.movie_repository) + presenter = MoviePresenter() + usecase = CreateMovieUseCase(presenter, service) + await usecase.execute(in_dto) + return presenter.result + + +@router.put( + '/{uuid}', + responses={ + 200: {'description': 'Item updated'}, + 404: {'description': 'Item not found'}, + 422: {'description': 'Unprocessable Entity'}, + }, +) +async def update_movie( + uuid: Annotated[str, uuid.UUID], + request_data: UpdateMovieRequestDTO, + container: AppContainer = Depends(AppContainer), + _: Session = Depends(is_admin_session), +) -> MovieResponse: + in_data = request_data.model_dump() + in_dto = UpdatePartialMovieInDTO.model_validate(in_data) + service = MovieService(container.movie_repository) + presenter = MoviePresenter() + usecase = UpdateMovieUseCase(presenter, service) + await usecase.execute(uuid, in_dto) + return presenter.result + + +@router.patch( + '/{uuid}', + responses={ + 200: {'description': 'Item updated'}, + 404: {'description': 'Item not found'}, + 422: {'description': 'Unprocessable Entity'}, + }, +) +async def update_movie_partially( + uuid: Annotated[str, uuid.UUID], + request_data: UpdatePartialMovieRequestDTO, + container: AppContainer = Depends(AppContainer), + _: Session = Depends(is_admin_session), +) -> MovieResponse: + in_data = request_data.model_dump(exclude_unset=True) + if not in_data.keys(): + raise EmptyPayloadExceptionError() + in_dto = UpdatePartialMovieInDTO.model_validate(in_data) + + service = MovieService(container.movie_repository) + presenter = MoviePresenter() + usecase = UpdateMovieUseCase(presenter, service) + await usecase.execute(uuid, in_dto) + return presenter.result diff --git a/src/filmin/adapters/api/http/presenters/genre.py b/src/filmin/adapters/api/http/presenters/genre.py new file mode 100644 index 0000000..989893b --- /dev/null +++ b/src/filmin/adapters/api/http/presenters/genre.py @@ -0,0 +1,29 @@ +from typing import List + +from filmin.adapters.api.http.schemas.genre import GenreListPagedResponse, GenreResponse +from filmin.domain.entities.genre import Genre +from shared.api.schemas.page import PageParams +from shared.presenter import AbstractPresenter + + +class GenrePresenter(AbstractPresenter[Genre, GenreResponse]): + result: GenreResponse + + async def present(self, data: Genre) -> None: + self.result = GenreResponse.model_validate(data, from_attributes=True) + + +class GenrePagedListPresenter(AbstractPresenter[List[Genre], List[GenreResponse]]): + result: GenreListPagedResponse + + def __init__(self, page_params: PageParams) -> None: + self.page_params = page_params + + async def present(self, data: List[Genre]) -> None: + list_items = [GenreResponse.model_validate(item, from_attributes=True) for item in data] + self.result = GenreListPagedResponse( + results=list_items, + total=len(list_items), + page=self.page_params.page, + size=self.page_params.size, + ) diff --git a/src/filmin/adapters/api/http/presenters/movie.py b/src/filmin/adapters/api/http/presenters/movie.py new file mode 100644 index 0000000..531c009 --- /dev/null +++ b/src/filmin/adapters/api/http/presenters/movie.py @@ -0,0 +1,32 @@ +from typing import List + +from filmin.adapters.api.http.schemas.movie import ( + MovieListPagedResponse, + MovieResponse, +) +from filmin.domain.entities.movie import Movie +from shared.api.schemas.page import PageParams +from shared.presenter import AbstractPresenter + + +class MoviePresenter(AbstractPresenter[Movie, MovieResponse]): + result: MovieResponse + + async def present(self, data: Movie) -> None: + self.result = MovieResponse.model_validate(data, from_attributes=True) + + +class MoviePagedListPresenter(AbstractPresenter[List[Movie], List[MovieResponse]]): + result: MovieListPagedResponse + + def __init__(self, page_params: PageParams) -> None: + self.page_params = page_params + + async def present(self, data: List[Movie]) -> None: + list_items = [MovieResponse.model_validate(item, from_attributes=True) for item in data] + self.result = MovieListPagedResponse( + results=list_items, + total=len(list_items), + page=self.page_params.page, + size=self.page_params.size, + ) diff --git a/src/filmin/adapters/api/http/presenters/production_company.py b/src/filmin/adapters/api/http/presenters/production_company.py new file mode 100644 index 0000000..3f35384 --- /dev/null +++ b/src/filmin/adapters/api/http/presenters/production_company.py @@ -0,0 +1,32 @@ +from typing import List + +from filmin.adapters.api.http.schemas.production_company import ( + ProductionCompanyListPagedResponse, + ProductionCompanyResponse, +) +from filmin.domain.entities.production_company import ProductionCompany +from shared.api.schemas.page import PageParams +from shared.presenter import AbstractPresenter + + +class ProductionCompanyPresenter(AbstractPresenter[ProductionCompany, ProductionCompanyResponse]): + result: ProductionCompanyResponse + + async def present(self, data: ProductionCompany) -> None: + self.result = ProductionCompanyResponse.model_validate(data, from_attributes=True) + + +class ProductionCompanyPagedListPresenter(AbstractPresenter[List[ProductionCompany], List[ProductionCompanyResponse]]): + result: ProductionCompanyListPagedResponse + + def __init__(self, page_params: PageParams) -> None: + self.page_params = page_params + + async def present(self, data: List[ProductionCompany]) -> None: + list_items = [ProductionCompanyResponse.model_validate(item, from_attributes=True) for item in data] + self.result = ProductionCompanyListPagedResponse( + results=list_items, + total=len(list_items), + page=self.page_params.page, + size=self.page_params.size, + ) diff --git a/src/filmin/adapters/api/http/production_company.py b/src/filmin/adapters/api/http/production_company.py new file mode 100644 index 0000000..13524f3 --- /dev/null +++ b/src/filmin/adapters/api/http/production_company.py @@ -0,0 +1,147 @@ +from typing import Annotated +import uuid + +from fastapi import Depends +from fastapi.responses import JSONResponse +from fastapi.routing import APIRouter + +from app.app_container import AppContainer +from app.exceptions import EmptyPayloadExceptionError +from app.schemas import Session +from app.session_deps import check_access_token, is_admin_session +from filmin.adapters.api.http.presenters.production_company import ( + ProductionCompanyPagedListPresenter, + ProductionCompanyPresenter, +) +from filmin.adapters.api.http.schemas.production_company import ( + CreateProductionCompanyRequestDTO, + CreateProductionCompanyResponseDTO, + ProductionCompanyListPagedResponse, + ProductionCompanyResponse, + UpdatePartialProductionCompanyRequestDTO, + UpdateProductionCompanyRequestDTO, +) +from filmin.domain.entities.production_company import ProductionCompany +from filmin.domain.services.production_company import ProductionCompanyService +from filmin.domain.use_cases.production_company import ( + CreateProductionCompanyUseCase, + GetProductionCompaniesUseCase, + GetProductionCompanyUseCase, + UpdateProductionCompanyUseCase, +) +from filmin.schemas.production_company import CreateProductionCompanyInDTO, UpdatePartialProductionCompanyInDTO +from shared.api.schemas.page import PageParams + + +router = APIRouter(prefix='/production-company') + + +@router.get( + '/{uuid}', + responses={ + 200: {'description': 'Successful Response'}, + 404: {'description': 'ProductionCompany not found'}, + 422: {'description': 'Unprocessable Entity'}, + }, +) +async def get_production_company( + uuid: Annotated[str, uuid.UUID], + container: AppContainer = Depends(AppContainer), + _: Session = Depends(check_access_token), +) -> ProductionCompanyResponse: + service = ProductionCompanyService(container.production_company_repository) + presenter = ProductionCompanyPresenter() + usecase = GetProductionCompanyUseCase(presenter, service) + await usecase.execute(uuid) + return presenter.result + + +@router.get( + '', + responses={ + 200: {'description': 'Successful Response'}, + 422: {'description': 'Unprocessable Entity'}, + }, +) +async def list_production_companies( + page_params: PageParams = Depends(), + container: AppContainer = Depends(AppContainer), + _: Session = Depends(check_access_token), +) -> ProductionCompanyListPagedResponse: + service = ProductionCompanyService(container.production_company_repository) + presenter = ProductionCompanyPagedListPresenter(page_params) + usecase = GetProductionCompaniesUseCase(presenter, service) + await usecase.execute(page_params) + return presenter.result + + +@router.post( + '', + response_class=JSONResponse, + response_model=CreateProductionCompanyResponseDTO, + status_code=201, + responses={ + 201: {'description': 'Item created'}, + 422: {'description': 'Unprocessable Entity'}, + }, +) +async def create_production_company( + request_data: CreateProductionCompanyRequestDTO, + container: AppContainer = Depends(AppContainer), + _: Session = Depends(is_admin_session), +) -> ProductionCompany: + in_dto = CreateProductionCompanyInDTO.model_validate(request_data.model_dump()) + service = ProductionCompanyService(container.production_company_repository) + presenter = ProductionCompanyPresenter() + usecase = CreateProductionCompanyUseCase(presenter, service) + await usecase.execute(in_dto) + return presenter.result + + +@router.put( + '/{uuid}', + responses={ + 200: {'description': 'Item updated'}, + 404: {'description': 'Item not found'}, + 422: {'description': 'Unprocessable Entity'}, + }, +) +async def update_production_company( + uuid: Annotated[str, uuid.UUID], + request_data: UpdateProductionCompanyRequestDTO, + container: AppContainer = Depends(AppContainer), + _: Session = Depends(is_admin_session), +) -> ProductionCompanyResponse: + in_data = request_data.model_dump() + in_dto = UpdatePartialProductionCompanyInDTO.model_validate(in_data) + service = ProductionCompanyService(container.production_company_repository) + presenter = ProductionCompanyPresenter() + usecase = UpdateProductionCompanyUseCase(presenter, service) + await usecase.execute(uuid, in_dto) + return presenter.result + + +@router.patch( + '/{uuid}', + responses={ + 200: {'description': 'Item updated'}, + 404: {'description': 'Item not found'}, + 422: {'description': 'Unprocessable Entity'}, + }, +) +async def update_production_company_partially( + uuid: Annotated[str, uuid.UUID], + request_data: UpdatePartialProductionCompanyRequestDTO, + container: AppContainer = Depends(AppContainer), + _: Session = Depends(is_admin_session), +) -> ProductionCompanyResponse: + in_data = request_data.model_dump(exclude_unset=True) + if not in_data.keys(): + raise EmptyPayloadExceptionError() + in_dto = UpdatePartialProductionCompanyInDTO.model_validate(in_data) + + service = ProductionCompanyService(container.production_company_repository) + presenter = ProductionCompanyPresenter() + usecase = UpdateProductionCompanyUseCase(presenter, service) + await usecase.execute(uuid, in_dto) + return presenter.result diff --git a/src/filmin/api/router.py b/src/filmin/adapters/api/http/router.py similarity index 100% rename from src/filmin/api/router.py rename to src/filmin/adapters/api/http/router.py diff --git a/src/filmin/data/repositories/__init__.py b/src/filmin/adapters/api/http/schemas/__init__.py similarity index 100% rename from src/filmin/data/repositories/__init__.py rename to src/filmin/adapters/api/http/schemas/__init__.py diff --git a/src/filmin/api/schemas/genre.py b/src/filmin/adapters/api/http/schemas/genre.py similarity index 71% rename from src/filmin/api/schemas/genre.py rename to src/filmin/adapters/api/http/schemas/genre.py index c9dbb0c..6eb2f81 100644 --- a/src/filmin/api/schemas/genre.py +++ b/src/filmin/adapters/api/http/schemas/genre.py @@ -1,6 +1,16 @@ from pydantic import BaseModel, Extra from filmin.schemas.genre import CreateGenreInDTO, UpdateGenreInDTO, UpdatePartialGenreInDTO +from shared.api.schemas.page import PagedResponseSchema + + +class GenreResponse(BaseModel): + code: str + name: str + + +class GenreListPagedResponse(PagedResponseSchema[GenreResponse]): + pass class CreateGenreRequestDTO(CreateGenreInDTO): diff --git a/src/filmin/adapters/api/http/schemas/movie.py b/src/filmin/adapters/api/http/schemas/movie.py new file mode 100644 index 0000000..779aa36 --- /dev/null +++ b/src/filmin/adapters/api/http/schemas/movie.py @@ -0,0 +1,68 @@ +from datetime import date +from typing import List, Optional +import uuid as _uuid + +from pydantic import BaseModel, Extra, field_validator + +from filmin.schemas.movie import CreateMovieInDTO, UpdateMovieInDTO, UpdatePartialMovieInDTO +from shared.api.schemas.page import PagedResponseSchema + + +class Genre(BaseModel): + code: str + name: str + + +class MovieCollection(BaseModel): + uuid: str + name: str + + @field_validator('uuid', mode='before') + @classmethod + def fix_uuid(cls, v: _uuid.UUID) -> str: + return str(v) + + +class MovieResponse(BaseModel): + uuid: str + title: str + release_date: Optional[date] = None + budget: Optional[int] = None + revenue: Optional[int] = None + popularity: Optional[float] = None + runtime: Optional[int] = None + collection: Optional[MovieCollection] = None + overview: Optional[str] = None + genres: List[Genre] = [] + original_language: Optional[str] = None + + @field_validator('uuid', mode='before') + @classmethod + def fix_uuid(cls, v: _uuid.UUID) -> str: + return str(v) + + +class MovieListPagedResponse(PagedResponseSchema[MovieResponse]): + pass + + +class CreateMovieRequestDTO(CreateMovieInDTO): + class Config: + extra = Extra.forbid + populate_by_name = False + + +class CreateMovieResponseDTO(BaseModel): + uuid: str + + +class UpdateMovieRequestDTO(UpdateMovieInDTO): + class Config: + extra = Extra.forbid + populate_by_name = False + + +class UpdatePartialMovieRequestDTO(UpdatePartialMovieInDTO): + class Config: + extra = Extra.forbid + populate_by_name = False diff --git a/src/filmin/api/schemas/production_company.py b/src/filmin/adapters/api/http/schemas/production_company.py similarity index 64% rename from src/filmin/api/schemas/production_company.py rename to src/filmin/adapters/api/http/schemas/production_company.py index 4fedba3..7cbb715 100644 --- a/src/filmin/api/schemas/production_company.py +++ b/src/filmin/adapters/api/http/schemas/production_company.py @@ -1,10 +1,27 @@ from typing import Optional, Type, Union +import uuid as _uuid -from pydantic import UUID4, BaseModel, Extra, Field, validator +from pydantic import BaseModel, Extra, Field, field_validator, validator + +from shared.api.schemas.page import PagedResponseSchema + + +class ProductionCompanyResponse(BaseModel): + uuid: str + name: str + + @field_validator('uuid', mode='before') + @classmethod + def fix_uuid(cls, v: _uuid.UUID) -> str: + return str(v) + + +class ProductionCompanyListPagedResponse(PagedResponseSchema[ProductionCompanyResponse]): + pass class CreateProductionCompanyResponseDTO(BaseModel): - uuid: UUID4 + uuid: str class CreateProductionCompanyRequestDTO(BaseModel): diff --git a/src/filmin/data/repositories/ports/__init__.py b/src/filmin/adapters/spi/__init__.py similarity index 100% rename from src/filmin/data/repositories/ports/__init__.py rename to src/filmin/adapters/spi/__init__.py diff --git a/src/filmin/domain/schemas/__init__.py b/src/filmin/adapters/spi/repositories/__init__.py similarity index 100% rename from src/filmin/domain/schemas/__init__.py rename to src/filmin/adapters/spi/repositories/__init__.py diff --git a/src/filmin/data/repositories/genre.py b/src/filmin/adapters/spi/repositories/genre.py similarity index 79% rename from src/filmin/data/repositories/genre.py rename to src/filmin/adapters/spi/repositories/genre.py index f4cba6e..8d3327e 100644 --- a/src/filmin/data/repositories/genre.py +++ b/src/filmin/adapters/spi/repositories/genre.py @@ -1,7 +1,7 @@ from sqlalchemy.orm import registry -from .ports.genre import AbstractGenreRepository -from filmin.domain.schemas.genre import Genre +from filmin.domain.entities.genre import Genre +from filmin.domain.ports.repositories.genre import AbstractGenreRepository from filmin.infra.database.sqlalchemy.models.genre import genres from filmin.schemas.genre import CreateGenreInDTO, UpdatePartialGenreInDTO from shared.repository.sqlalchemy import SqlAlchemyRepository diff --git a/src/filmin/data/repositories/movie.py b/src/filmin/adapters/spi/repositories/movie.py similarity index 89% rename from src/filmin/data/repositories/movie.py rename to src/filmin/adapters/spi/repositories/movie.py index 5c6a00e..6d898a8 100644 --- a/src/filmin/data/repositories/movie.py +++ b/src/filmin/adapters/spi/repositories/movie.py @@ -3,11 +3,11 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import registry, relationship -from .ports.movie import AbstractMovieRepository -from filmin.data.repositories.ports.genre import AbstractGenreRepository -from filmin.domain.schemas.genre import Genre -from filmin.domain.schemas.movie import Movie -from filmin.domain.schemas.movie_collection import MovieCollection +from filmin.domain.entities.genre import Genre +from filmin.domain.entities.movie import Movie +from filmin.domain.entities.movie_collection import MovieCollection +from filmin.domain.ports.repositories.genre import AbstractGenreRepository +from filmin.domain.ports.repositories.movie import AbstractMovieRepository from filmin.infra.database.sqlalchemy.models.collection import movie_collection from filmin.infra.database.sqlalchemy.models.movie import movie, movie_genre from filmin.schemas.movie import CreateMovieInDTO, UpdatePartialMovieInDTO diff --git a/src/filmin/data/repositories/production_company.py b/src/filmin/adapters/spi/repositories/production_company.py similarity index 72% rename from src/filmin/data/repositories/production_company.py rename to src/filmin/adapters/spi/repositories/production_company.py index 29cebdf..cd26c55 100644 --- a/src/filmin/data/repositories/production_company.py +++ b/src/filmin/adapters/spi/repositories/production_company.py @@ -1,7 +1,7 @@ from sqlalchemy.orm import registry -from .ports.production_company import AbstractProductionCompanyRepository -from filmin.domain.schemas.production_company import ProductionCompany +from filmin.domain.entities.production_company import ProductionCompany +from filmin.domain.ports.repositories.production_company import AbstractProductionCompanyRepository from filmin.infra.database.sqlalchemy.models import production_company from filmin.schemas.production_company import CreateProductionCompanyInDTO, UpdatePartialProductionCompanyInDTO from shared.repository.sqlalchemy import SqlAlchemyRepository @@ -11,7 +11,7 @@ mapper_registry.map_imperatively( ProductionCompany, - production_company, + production_company.production_company, ) diff --git a/src/filmin/api/movie.py b/src/filmin/api/movie.py deleted file mode 100644 index 250e457..0000000 --- a/src/filmin/api/movie.py +++ /dev/null @@ -1,119 +0,0 @@ -from typing import Dict, List, Union - -from fastapi import Depends -from fastapi.responses import JSONResponse -from fastapi.routing import APIRouter - -from app.exceptions import EmptyPayloadExceptionError -from filmin.api.schemas.movie import ( - CreateMovieRequestDTO, - CreateMovieResponseDTO, - UpdatePartialMovieRequestDTO, -) -from filmin.data.repositories.ports.movie import AbstractMovieRepository -from filmin.domain.schemas.movie import Movie -from filmin.schemas.movie import CreateMovieInDTO, UpdatePartialMovieInDTO -from shared.api.schemas.page import PagedResponseSchema, PageParams - - -router = APIRouter(prefix='/movie') - - -def get_movie_repository() -> AbstractMovieRepository: - from app.app_container import AppContainer - - return AppContainer().movie_repository - - -@router.get( - '/{uuid}', - responses={ - 200: {'description': 'Successful Response'}, - 404: {'description': 'Movie not found'}, - 422: {'description': 'Unprocessable Entity'}, - }, -) -async def get_movie( - uuid: str, - movie_repository: AbstractMovieRepository = Depends(get_movie_repository), -) -> Movie: - return await movie_repository.get_by_id(uuid) - - -@router.get( - '', - response_model=PagedResponseSchema[Movie], - responses={ - 200: {'description': 'Successful Response'}, - 422: {'description': 'Unprocessable Entity'}, - }, -) -async def list_movie( - page_params: PageParams = Depends(), - movie_repository: AbstractMovieRepository = Depends(get_movie_repository), -) -> Dict[str, Union[int, List[Movie]]]: - count, items = await movie_repository.get_xpage(**page_params.model_dump()) - return { - 'total': count, - 'results': items, - 'page': page_params.page, - 'size': page_params.size, - } - - -@router.post( - '', - response_class=JSONResponse, - response_model=CreateMovieResponseDTO, - status_code=201, - responses={ - 201: {'description': 'Item created'}, - 422: {'description': 'Unprocessable Entity'}, - }, -) -async def create_movie( - request_data: CreateMovieRequestDTO, - movie_repository: AbstractMovieRepository = Depends(get_movie_repository), -) -> Movie: - in_dto = CreateMovieInDTO.model_validate(request_data.model_dump()) - return await movie_repository.create(in_dto) - - -# @router.put( -# '/{uuid}', -# status_code=204, -# responses={ -# 204: {'description': 'Item updated'}, -# 404: {'description': 'Item not found'}, -# 422: {'description': 'Unprocessable Entity'}, -# }, -# ) -# async def update_movie( -# uuid: str, -# request_data: UpdateMovieRequestDTO, -# movie_repository: AbstractMovieRepository = Depends(get_movie_repository), -# ) -> None: -# in_data = request_data.model_dump() -# in_dto = UpdatePartialMovieInDTO.model_validate(in_data) -# await movie_repository.update(uuid, in_dto) - - -@router.patch( - '/{uuid}', - status_code=200, - responses={ - 200: {'description': 'Item updated'}, - 404: {'description': 'Item not found'}, - 422: {'description': 'Unprocessable Entity'}, - }, -) -async def update_movie_partially( - uuid: str, - request_data: UpdatePartialMovieRequestDTO, - movie_repository: AbstractMovieRepository = Depends(get_movie_repository), -) -> Movie: - in_data = request_data.model_dump(exclude_unset=True) - if not in_data.keys(): - raise EmptyPayloadExceptionError() - in_dto = UpdatePartialMovieInDTO.model_validate(in_data) - return await movie_repository.update(uuid, in_dto) diff --git a/src/filmin/api/production_company.py b/src/filmin/api/production_company.py deleted file mode 100644 index 537ab99..0000000 --- a/src/filmin/api/production_company.py +++ /dev/null @@ -1,118 +0,0 @@ -from typing import Dict, List, Union - -from fastapi import Depends -from fastapi.responses import JSONResponse -from fastapi.routing import APIRouter - -from app.exceptions import EmptyPayloadExceptionError -from filmin.api.schemas.production_company import ( - CreateProductionCompanyRequestDTO, - CreateProductionCompanyResponseDTO, - UpdatePartialProductionCompanyRequestDTO, - UpdateProductionCompanyRequestDTO, -) -from filmin.data.repositories.ports.production_company import AbstractProductionCompanyRepository -from filmin.domain.schemas.production_company import ProductionCompany -from filmin.schemas.production_company import CreateProductionCompanyInDTO, UpdatePartialProductionCompanyInDTO -from shared.api.schemas.page import PagedResponseSchema, PageParams - - -router = APIRouter(prefix='/production-company') - - -def get_production_company_repository() -> AbstractProductionCompanyRepository: - from app.app_container import AppContainer - - return AppContainer().production_company_repository - - -@router.get( - '/{uuid}', - responses={ - 200: {'description': 'Successful Response'}, - 404: {'description': 'Production Company not found'}, - 422: {'description': 'Unprocessable Entity'}, - }, -) -async def get_production_company( - uuid: str, - production_company_repository: AbstractProductionCompanyRepository = Depends(get_production_company_repository), -) -> ProductionCompany: - return await production_company_repository.get_by_id(uuid) - - -@router.get( - '', - response_model=PagedResponseSchema[ProductionCompany], - responses={ - 200: {'description': 'Successful Response'}, - 422: {'description': 'Unprocessable Entity'}, - }, -) -async def list_production_company( - page_params: PageParams = Depends(), - production_company_repository: AbstractProductionCompanyRepository = Depends(get_production_company_repository), -) -> Dict[str, Union[int, List[ProductionCompany]]]: - count, items = await production_company_repository.get_xpage(**page_params.model_dump()) - return { - 'total': count, - 'results': items, - 'page': page_params.page, - 'size': page_params.size, - } - - -@router.post( - '', - response_class=JSONResponse, - response_model=CreateProductionCompanyResponseDTO, - status_code=201, - responses={ - 201: {'description': 'Item created'}, - 422: {'description': 'Unprocessable Entity'}, - }, -) -async def create_production_company( - request_data: CreateProductionCompanyRequestDTO, - production_company_repository: AbstractProductionCompanyRepository = Depends(get_production_company_repository), -) -> ProductionCompany: - in_dto = CreateProductionCompanyInDTO.model_validate(request_data.model_dump()) - return await production_company_repository.create(in_dto) - - -@router.put( - '/{uuid}', - responses={ - 200: {'description': 'Item updated'}, - 404: {'description': 'Item not found'}, - 422: {'description': 'Unprocessable Entity'}, - }, -) -async def update_production_company( - uuid: str, - request_data: UpdateProductionCompanyRequestDTO, - production_company_repository: AbstractProductionCompanyRepository = Depends(get_production_company_repository), -) -> ProductionCompany: - in_data = request_data.model_dump() - in_dto = UpdatePartialProductionCompanyInDTO.model_validate(in_data) - return await production_company_repository.update(uuid, in_dto) - - -@router.patch( - '/{uuid}', - responses={ - 200: {'description': 'Item updated'}, - 404: {'description': 'Item not found'}, - 422: {'description': 'Unprocessable Entity'}, - }, -) -async def update_production_company_partially( - uuid: str, - request_data: UpdatePartialProductionCompanyRequestDTO, - production_company_repository: AbstractProductionCompanyRepository = Depends(get_production_company_repository), -) -> ProductionCompany: - in_data = request_data.model_dump(exclude_unset=True) - if not in_data.keys(): - raise EmptyPayloadExceptionError() - in_dto = UpdatePartialProductionCompanyInDTO.model_validate(in_data) - return await production_company_repository.update(uuid, in_dto) diff --git a/src/filmin/api/schemas/movie.py b/src/filmin/api/schemas/movie.py deleted file mode 100644 index 013b749..0000000 --- a/src/filmin/api/schemas/movie.py +++ /dev/null @@ -1,25 +0,0 @@ -from pydantic import UUID4, BaseModel, Extra - -from filmin.schemas.movie import CreateMovieInDTO, UpdateMovieInDTO, UpdatePartialMovieInDTO - - -class CreateMovieRequestDTO(CreateMovieInDTO): - class Config: - extra = Extra.forbid - populate_by_name = False - - -class CreateMovieResponseDTO(BaseModel): - uuid: UUID4 - - -class UpdateMovieRequestDTO(UpdateMovieInDTO): - class Config: - extra = Extra.forbid - populate_by_name = False - - -class UpdatePartialMovieRequestDTO(UpdatePartialMovieInDTO): - class Config: - extra = Extra.forbid - populate_by_name = False diff --git a/src/filmin/di/mixins/genre.py b/src/filmin/di/mixins/genre.py index 8bc123b..98c8167 100644 --- a/src/filmin/di/mixins/genre.py +++ b/src/filmin/di/mixins/genre.py @@ -1,4 +1,5 @@ -from filmin.data.repositories.ports.genre import AbstractGenreRepository +from filmin.adapters.spi.repositories.genre import GenreRepository +from filmin.domain.ports.repositories.genre import AbstractGenreRepository from infra.database.sqlalchemy.session import AbstractDatabase @@ -7,6 +8,4 @@ class GenreContainerMixin: genre_repository: AbstractGenreRepository def _get_genre_repository(self) -> AbstractGenreRepository: - from filmin.data.repositories.genre import GenreRepository - return GenreRepository(self.db.session) diff --git a/src/filmin/di/mixins/movie.py b/src/filmin/di/mixins/movie.py index 9403d3b..8cc1109 100644 --- a/src/filmin/di/mixins/movie.py +++ b/src/filmin/di/mixins/movie.py @@ -1,5 +1,6 @@ -from filmin.data.repositories.ports.genre import AbstractGenreRepository -from filmin.data.repositories.ports.movie import AbstractMovieRepository +from filmin.adapters.spi.repositories.movie import MovieRepository +from filmin.domain.ports.repositories.genre import AbstractGenreRepository +from filmin.domain.ports.repositories.movie import AbstractMovieRepository from infra.database.sqlalchemy.session import AbstractDatabase @@ -9,6 +10,4 @@ class MovieContainerMixin: movie_repository: AbstractMovieRepository def _get_movie_repository(self) -> AbstractMovieRepository: - from filmin.data.repositories.movie import MovieRepository - return MovieRepository(self.db.session, genre_repository=self.genre_repository) diff --git a/src/filmin/di/mixins/production_company.py b/src/filmin/di/mixins/production_company.py index e2519dc..6520cca 100644 --- a/src/filmin/di/mixins/production_company.py +++ b/src/filmin/di/mixins/production_company.py @@ -1,4 +1,5 @@ -from filmin.data.repositories.ports.production_company import AbstractProductionCompanyRepository +from filmin.adapters.spi.repositories.production_company import ProductionCompanyRepository +from filmin.domain.ports.repositories.production_company import AbstractProductionCompanyRepository from infra.database.sqlalchemy.session import AbstractDatabase @@ -7,6 +8,4 @@ class ProductionCompanyContainerMixin: production_company_repository: AbstractProductionCompanyRepository def _get_production_company_repository(self) -> AbstractProductionCompanyRepository: - from filmin.data.repositories.production_company import ProductionCompanyRepository - return ProductionCompanyRepository(self.db.session) diff --git a/src/tests/filmin/api/__init__.py b/src/filmin/domain/entities/__init__.py similarity index 100% rename from src/tests/filmin/api/__init__.py rename to src/filmin/domain/entities/__init__.py diff --git a/src/filmin/domain/schemas/genre.py b/src/filmin/domain/entities/genre.py similarity index 53% rename from src/filmin/domain/schemas/genre.py rename to src/filmin/domain/entities/genre.py index dc7cb21..4316261 100644 --- a/src/filmin/domain/schemas/genre.py +++ b/src/filmin/domain/entities/genre.py @@ -1,7 +1,9 @@ from dataclasses import dataclass +from filmin.domain.entities.value_objects import GenreId + @dataclass(kw_only=True) class Genre: - code: str + code: GenreId name: str diff --git a/src/filmin/domain/schemas/movie.py b/src/filmin/domain/entities/movie.py similarity index 72% rename from src/filmin/domain/schemas/movie.py rename to src/filmin/domain/entities/movie.py index 94dd6b8..d83c49e 100644 --- a/src/filmin/domain/schemas/movie.py +++ b/src/filmin/domain/entities/movie.py @@ -3,8 +3,9 @@ from typing import List, Optional import uuid as uuid_lib -from filmin.domain.schemas.genre import Genre -from filmin.domain.schemas.movie_collection import MovieCollection +from filmin.domain.entities.genre import Genre +from filmin.domain.entities.movie_collection import MovieCollection +from filmin.domain.entities.value_objects import MovieId @dataclass(kw_only=True) @@ -23,4 +24,4 @@ class BaseMovie: @dataclass(kw_only=True) class Movie(BaseMovie): - uuid: uuid_lib.UUID = field(default_factory=uuid_lib.uuid4) + uuid: MovieId = field(default_factory=uuid_lib.uuid4) diff --git a/src/filmin/domain/schemas/movie_collection.py b/src/filmin/domain/entities/movie_collection.py similarity index 100% rename from src/filmin/domain/schemas/movie_collection.py rename to src/filmin/domain/entities/movie_collection.py diff --git a/src/filmin/domain/schemas/production_company.py b/src/filmin/domain/entities/production_company.py similarity index 60% rename from src/filmin/domain/schemas/production_company.py rename to src/filmin/domain/entities/production_company.py index 01e7312..6670a07 100644 --- a/src/filmin/domain/schemas/production_company.py +++ b/src/filmin/domain/entities/production_company.py @@ -1,6 +1,8 @@ from dataclasses import dataclass, field import uuid as uuid_lib +from filmin.domain.entities.value_objects import ProductionCompanyId + @dataclass(kw_only=True) class BaseProductionCompany: @@ -9,4 +11,4 @@ class BaseProductionCompany: @dataclass(kw_only=True) class ProductionCompany(BaseProductionCompany): - uuid: uuid_lib.UUID = field(default_factory=uuid_lib.uuid4) + uuid: ProductionCompanyId = field(default_factory=uuid_lib.uuid4) diff --git a/src/filmin/domain/entities/value_objects.py b/src/filmin/domain/entities/value_objects.py new file mode 100644 index 0000000..c090ba8 --- /dev/null +++ b/src/filmin/domain/entities/value_objects.py @@ -0,0 +1,8 @@ +import uuid + + +GenreId = str + +MovieId = uuid.UUID + +ProductionCompanyId = uuid.UUID diff --git a/src/filmin/domain/ports/__init__.py b/src/filmin/domain/ports/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/filmin/domain/ports/repositories/__init__.py b/src/filmin/domain/ports/repositories/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/filmin/data/repositories/ports/genre.py b/src/filmin/domain/ports/repositories/genre.py similarity index 84% rename from src/filmin/data/repositories/ports/genre.py rename to src/filmin/domain/ports/repositories/genre.py index b69446c..858d0a1 100644 --- a/src/filmin/data/repositories/ports/genre.py +++ b/src/filmin/domain/ports/repositories/genre.py @@ -1,4 +1,4 @@ -from filmin.domain.schemas.genre import Genre +from filmin.domain.entities.genre import Genre from filmin.schemas.genre import CreateGenreInDTO, UpdatePartialGenreInDTO from shared.repository.ports.generic import AbstractRepository diff --git a/src/filmin/data/repositories/ports/movie.py b/src/filmin/domain/ports/repositories/movie.py similarity index 84% rename from src/filmin/data/repositories/ports/movie.py rename to src/filmin/domain/ports/repositories/movie.py index cc6c194..8faca48 100644 --- a/src/filmin/data/repositories/ports/movie.py +++ b/src/filmin/domain/ports/repositories/movie.py @@ -1,4 +1,4 @@ -from filmin.domain.schemas.movie import Movie +from filmin.domain.entities.movie import Movie from filmin.schemas.movie import CreateMovieInDTO, UpdatePartialMovieInDTO from shared.repository.ports.generic import AbstractRepository diff --git a/src/filmin/data/repositories/ports/production_company.py b/src/filmin/domain/ports/repositories/production_company.py similarity index 82% rename from src/filmin/data/repositories/ports/production_company.py rename to src/filmin/domain/ports/repositories/production_company.py index 5845f25..e959359 100644 --- a/src/filmin/data/repositories/ports/production_company.py +++ b/src/filmin/domain/ports/repositories/production_company.py @@ -1,4 +1,4 @@ -from filmin.domain.schemas.production_company import ProductionCompany +from filmin.domain.entities.production_company import ProductionCompany from filmin.schemas.production_company import CreateProductionCompanyInDTO, UpdatePartialProductionCompanyInDTO from shared.repository.ports.generic import AbstractRepository diff --git a/src/filmin/domain/services/__init__.py b/src/filmin/domain/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/filmin/domain/services/genre.py b/src/filmin/domain/services/genre.py new file mode 100644 index 0000000..a41b82c --- /dev/null +++ b/src/filmin/domain/services/genre.py @@ -0,0 +1,27 @@ +from typing import List, Optional + +from filmin.domain.entities.genre import Genre +from filmin.domain.ports.repositories.genre import AbstractGenreRepository +from filmin.schemas.genre import CreateGenreInDTO, UpdateGenreInDTO +from shared.api.schemas.page import PageParams + + +class GenreService: + def __init__(self, genre_repository: AbstractGenreRepository): + self.genre_repository = genre_repository + + async def get_genre_by_id(self, code: str) -> Genre: + return await self.genre_repository.get_by_id(code) + + async def get_genres(self, *, page_params: Optional[PageParams] = None) -> List[Genre]: + if not page_params: + ret = await self.genre_repository.get_all() + else: + ret = await self.genre_repository.get_xpage(page_params.page, page_params.size) + return ret + + async def create_genre(self, in_dto: CreateGenreInDTO) -> Genre: + return await self.genre_repository.create(in_dto) + + async def update_genre(self, code: str, in_dto: UpdateGenreInDTO) -> Genre: + return await self.genre_repository.update(code, in_dto) diff --git a/src/filmin/domain/services/movie.py b/src/filmin/domain/services/movie.py new file mode 100644 index 0000000..6a83b07 --- /dev/null +++ b/src/filmin/domain/services/movie.py @@ -0,0 +1,27 @@ +from typing import List, Optional + +from filmin.domain.entities.movie import Movie +from filmin.domain.ports.repositories.movie import AbstractMovieRepository +from filmin.schemas.movie import CreateMovieInDTO, UpdateMovieInDTO +from shared.api.schemas.page import PageParams + + +class MovieService: + def __init__(self, movie_repository: AbstractMovieRepository): + self.movie_repository = movie_repository + + async def get_movie_by_id(self, uuid: str) -> Movie: + return await self.movie_repository.get_by_id(uuid) + + async def get_movies(self, *, page_params: Optional[PageParams] = None) -> List[Movie]: + if not page_params: + ret = await self.movie_repository.get_all() + else: + ret = await self.movie_repository.get_xpage(page_params.page, page_params.size) + return ret + + async def create_movie(self, in_data: CreateMovieInDTO) -> Movie: + return await self.movie_repository.create(in_data) + + async def update_movie(self, uuid: str, in_data: UpdateMovieInDTO) -> Movie: + return await self.movie_repository.update(uuid, in_data) diff --git a/src/filmin/domain/services/production_company.py b/src/filmin/domain/services/production_company.py new file mode 100644 index 0000000..015598b --- /dev/null +++ b/src/filmin/domain/services/production_company.py @@ -0,0 +1,27 @@ +from typing import List, Optional + +from filmin.domain.entities.production_company import ProductionCompany +from filmin.domain.ports.repositories.production_company import AbstractProductionCompanyRepository +from filmin.schemas.production_company import CreateProductionCompanyInDTO, UpdateProductionCompanyInDTO +from shared.api.schemas.page import PageParams + + +class ProductionCompanyService: + def __init__(self, production_company_repository: AbstractProductionCompanyRepository): + self.production_company_repository = production_company_repository + + async def get_production_company_by_id(self, uuid: str) -> ProductionCompany: + return await self.production_company_repository.get_by_id(uuid) + + async def get_production_companies(self, *, page_params: Optional[PageParams] = None) -> List[ProductionCompany]: + if page_params is None: + ret = await self.production_company_repository.get_all() + else: + ret = await self.production_company_repository.get_xpage(page_params.page, page_params.size) + return ret + + async def create_production_company(self, in_data: CreateProductionCompanyInDTO) -> ProductionCompany: + return await self.production_company_repository.create(in_data) + + async def update_production_company(self, uuid: str, in_data: UpdateProductionCompanyInDTO) -> ProductionCompany: + return await self.production_company_repository.update(uuid, in_data) diff --git a/src/filmin/domain/use_cases/__init__.py b/src/filmin/domain/use_cases/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/filmin/domain/use_cases/genre.py b/src/filmin/domain/use_cases/genre.py new file mode 100644 index 0000000..89bd1ad --- /dev/null +++ b/src/filmin/domain/use_cases/genre.py @@ -0,0 +1,40 @@ +from filmin.domain.services.genre import GenreService +from filmin.schemas.genre import CreateGenreInDTO +from shared.api.schemas.page import PageParams +from shared.presenter import AbstractPresenter + + +class GetGenresUseCase: + def __init__(self, presenter: AbstractPresenter, service: GenreService): + self.presenter = presenter + self.service = service + + async def execute(self, page_params: PageParams) -> None: + await self.presenter.present(await self.service.get_genres(page_params=page_params)) + + +class GetGenreUseCase: + def __init__(self, presenter: AbstractPresenter, service: GenreService): + self.presenter = presenter + self.service = service + + async def execute(self, code: str) -> None: + await self.presenter.present(await self.service.get_genre_by_id(code)) + + +class CreateGenreUseCase: + def __init__(self, presenter: AbstractPresenter, service: GenreService): + self.presenter = presenter + self.service = service + + async def execute(self, in_dto: CreateGenreInDTO) -> None: + await self.presenter.present(await self.service.create_genre(in_dto)) + + +class UpdateGenreUseCase: + def __init__(self, presenter: AbstractPresenter, service: GenreService): + self.presenter = presenter + self.service = service + + async def execute(self, code: str, in_data: CreateGenreInDTO) -> None: + await self.presenter.present(await self.service.update_genre(code, in_data)) diff --git a/src/filmin/domain/use_cases/movie.py b/src/filmin/domain/use_cases/movie.py new file mode 100644 index 0000000..37a17d0 --- /dev/null +++ b/src/filmin/domain/use_cases/movie.py @@ -0,0 +1,40 @@ +from filmin.domain.services.movie import MovieService +from filmin.schemas.movie import CreateMovieInDTO +from shared.api.schemas.page import PageParams +from shared.presenter import AbstractPresenter + + +class GetMoviesUseCase: + def __init__(self, presenter: AbstractPresenter, service: MovieService): + self.presenter = presenter + self.service = service + + async def execute(self, page_params: PageParams) -> None: + await self.presenter.present(await self.service.get_movies(page_params=page_params)) + + +class GetMovieUseCase: + def __init__(self, presenter: AbstractPresenter, service: MovieService): + self.presenter = presenter + self.service = service + + async def execute(self, uuid: str) -> None: + await self.presenter.present(await self.service.get_movie_by_id(uuid)) + + +class CreateMovieUseCase: + def __init__(self, presenter: AbstractPresenter, service: MovieService): + self.presenter = presenter + self.service = service + + async def execute(self, in_dto: CreateMovieInDTO) -> None: + await self.presenter.present(await self.service.create_movie(in_dto)) + + +class UpdateMovieUseCase: + def __init__(self, presenter: AbstractPresenter, service: MovieService): + self.presenter = presenter + self.service = service + + async def execute(self, uuid: str, in_data: CreateMovieInDTO) -> None: + await self.presenter.present(await self.service.update_movie(uuid, in_data)) diff --git a/src/filmin/domain/use_cases/production_company.py b/src/filmin/domain/use_cases/production_company.py new file mode 100644 index 0000000..f92dc7b --- /dev/null +++ b/src/filmin/domain/use_cases/production_company.py @@ -0,0 +1,40 @@ +from filmin.domain.services.production_company import ProductionCompanyService +from filmin.schemas.production_company import CreateProductionCompanyInDTO +from shared.api.schemas.page import PageParams +from shared.presenter import AbstractPresenter + + +class GetProductionCompaniesUseCase: + def __init__(self, presenter: AbstractPresenter, service: ProductionCompanyService): + self.presenter = presenter + self.service = service + + async def execute(self, page_params: PageParams) -> None: + await self.presenter.present(await self.service.get_production_companies(page_params=page_params)) + + +class GetProductionCompanyUseCase: + def __init__(self, presenter: AbstractPresenter, service: ProductionCompanyService): + self.presenter = presenter + self.service = service + + async def execute(self, uuid: str) -> None: + await self.presenter.present(await self.service.get_production_company_by_id(uuid)) + + +class CreateProductionCompanyUseCase: + def __init__(self, presenter: AbstractPresenter, service: ProductionCompanyService): + self.presenter = presenter + self.service = service + + async def execute(self, in_dto: CreateProductionCompanyInDTO) -> None: + await self.presenter.present(await self.service.create_production_company(in_dto)) + + +class UpdateProductionCompanyUseCase: + def __init__(self, presenter: AbstractPresenter, service: ProductionCompanyService): + self.presenter = presenter + self.service = service + + async def execute(self, uuid: str, in_data: CreateProductionCompanyInDTO) -> None: + await self.presenter.present(await self.service.update_production_company(uuid, in_data)) diff --git a/src/filmin/schemas/movie.py b/src/filmin/schemas/movie.py index 05ce2db..2b87b21 100644 --- a/src/filmin/schemas/movie.py +++ b/src/filmin/schemas/movie.py @@ -2,7 +2,7 @@ from typing import List, Optional from uuid import UUID -from pydantic import BaseModel, Field +from pydantic import BaseModel, Extra, Field class CreateMovieInDTO(BaseModel): @@ -18,6 +18,7 @@ class CreateMovieInDTO(BaseModel): class Config: populate_by_name = True + extra = Extra.forbid class UpdateMovieInDTO(BaseModel): @@ -33,6 +34,7 @@ class UpdateMovieInDTO(BaseModel): class Config: populate_by_name = True + extra = Extra.forbid class UpdatePartialMovieInDTO(BaseModel): @@ -48,3 +50,4 @@ class UpdatePartialMovieInDTO(BaseModel): class Config: populate_by_name = True + extra = Extra.forbid diff --git a/src/infra/database/sqlalchemy/session.py b/src/infra/database/sqlalchemy/session.py index a44a010..8cc456f 100644 --- a/src/infra/database/sqlalchemy/session.py +++ b/src/infra/database/sqlalchemy/session.py @@ -3,7 +3,8 @@ from contextlib import asynccontextmanager import contextvars import logging -from typing import TYPE_CHECKING, Any, AsyncContextManager, AsyncIterator, Dict, Optional, cast +import os +from typing import TYPE_CHECKING, Any, AsyncContextManager, AsyncIterator, Dict, Final, Optional, cast from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine from sqlalchemy.orm import sessionmaker @@ -22,6 +23,9 @@ db_session_context: contextvars.ContextVar = contextvars.ContextVar('db_ctx', default={'session': None, 'level': 0}) +SA_ECHO: Final[bool] = os.getenv('SA_ECHO', 'False').lower() == 'true' + + class AbstractDatabase(abc.ABC): @abc.abstractmethod def session(self) -> AsyncContextManager[AsyncSession]: @@ -31,7 +35,7 @@ def session(self) -> AsyncContextManager[AsyncSession]: @singleton class Database(AbstractDatabase): def __init__(self) -> None: - self.engine: AsyncEngine = create_async_engine(settings.DATABASE_URL, echo=True, future=True) + self.engine: AsyncEngine = create_async_engine(settings.DATABASE_URL, echo=SA_ECHO, future=True) self._session_factory: sessionmaker = sessionmaker( self.engine, class_=AsyncSession, autocommit=False, autoflush=False, expire_on_commit=False ) diff --git a/src/logging.dev.yaml b/src/logging.dev.yaml index 4cc6ddf..e67ffbc 100644 --- a/src/logging.dev.yaml +++ b/src/logging.dev.yaml @@ -48,3 +48,5 @@ loggers: level: INFO sqlalchemy.engine: level: ERROR + faker.factory: + level: ERROR diff --git a/src/pytest.ini b/src/pytest.ini index c8b1b8c..b374e7b 100644 --- a/src/pytest.ini +++ b/src/pytest.ini @@ -6,7 +6,7 @@ python_files = tests.py test_*.py env = APP_ENV=test DATABASE_URL=postgresql+asyncpg://postgres:p1234@postgres-test:5432/filmin - + SA_ECHO=false addopts = -p no:warnings diff --git a/src/shared/repository/ports/generic.py b/src/shared/repository/ports/generic.py index b4b2f82..99fa759 100644 --- a/src/shared/repository/ports/generic.py +++ b/src/shared/repository/ports/generic.py @@ -2,7 +2,7 @@ import datetime from decimal import Decimal -from typing import Dict, Generic, List, Optional, Tuple, TypeVar, Union +from typing import Dict, Generic, List, Optional, TypeVar, Union from pydantic import BaseModel @@ -24,7 +24,7 @@ async def get_all(self) -> List[EntityT]: ... @abc.abstractmethod - async def get_xpage(self, page: int, size: int) -> Tuple[int, List[EntityT]]: + async def get_xpage(self, page: int, size: int) -> List[EntityT]: ... @abc.abstractmethod diff --git a/src/shared/repository/sqlalchemy.py b/src/shared/repository/sqlalchemy.py index 36bd580..cfb23d8 100644 --- a/src/shared/repository/sqlalchemy.py +++ b/src/shared/repository/sqlalchemy.py @@ -3,8 +3,7 @@ from pydantic import BaseModel from sqlalchemy.exc import NoResultFound from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy.orm import noload -from sqlalchemy.sql import Select, func, select +from sqlalchemy.sql import Select, select from shared.exceptions import NotFoundError from shared.repository.ports.generic import AbstractRepository, FilterBy @@ -45,21 +44,17 @@ async def get_by_id(self, uuid: str) -> EntityT: return cast(EntityT, result) # From: https://github.com/lewoudar/fastapi-paginator/blob/main/fastapi_paginator/helpers.py - async def _paginate(self, query: Select, page: int, size: int) -> Tuple[int, List[EntityT]]: + async def _paginate(self, query: Select, page: int, size: int) -> List[EntityT]: # inspiration for this query comes from fastapi-pagination package async with self.session_factory() as session: - count = await session.scalar( - select(func.count()).select_from(query.order_by(None).options(noload('*')).subquery()) - ) - items = list(await session.scalars(query.limit(size).offset(page - 1))) - return count, items + return list(await session.scalars(query.limit(size).offset(page - 1))) async def get_all(self) -> List[EntityT]: query = select(self.entity) async with self.session_factory() as session: return list((await session.scalars(query)).all()) - async def get_xpage(self, page: int, size: int) -> Tuple[int, List[EntityT]]: + async def get_xpage(self, page: int, size: int) -> List[EntityT]: query = select(self.entity) return await self._paginate(query, page, size) diff --git a/src/tests/conftest.py b/src/tests/conftest.py index 28d4d83..90cc9a8 100644 --- a/src/tests/conftest.py +++ b/src/tests/conftest.py @@ -85,15 +85,27 @@ def async_session_maker(engine: AsyncEngine) -> Generator: ) +# async def init_models(): +# async with engine.begin() as conn: +# await conn.run_sync(Base.metadata.create_all, checkfirst=True) + + async def create_all(engine: AsyncEngine) -> None: async with engine.begin() as conn: await conn.run_sync(metadata.drop_all) await conn.run_sync(metadata.create_all) + await engine.dispose() async def drop_all(engine: AsyncEngine) -> None: async with engine.begin() as conn: await conn.run_sync(metadata.drop_all) + await engine.dispose() + + +# async def disconnect() -> None: +# if engine: +# await engine.dispose() @pytest_asyncio.fixture(scope='session') diff --git a/src/tests/filmin/adapters/__init__.py b/src/tests/filmin/adapters/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/tests/filmin/adapters/api/__init__.py b/src/tests/filmin/adapters/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/tests/filmin/adapters/api/http/__init__.py b/src/tests/filmin/adapters/api/http/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/tests/filmin/api/test_genre.py b/src/tests/filmin/adapters/api/http/test_genre.py similarity index 100% rename from src/tests/filmin/api/test_genre.py rename to src/tests/filmin/adapters/api/http/test_genre.py diff --git a/src/tests/filmin/api/test_movie.py b/src/tests/filmin/adapters/api/http/test_movie.py similarity index 86% rename from src/tests/filmin/api/test_movie.py rename to src/tests/filmin/adapters/api/http/test_movie.py index 73a4326..13b3295 100644 --- a/src/tests/filmin/api/test_movie.py +++ b/src/tests/filmin/adapters/api/http/test_movie.py @@ -7,7 +7,7 @@ @pytest.mark.asyncio async def test_movie_create( clean_movie: Any, # noqa: ARG001 - async_client: AsyncClient, + async_admin_client: AsyncClient, genre_comedy: Any, # noqa: ARG001 genre_animation: Any, # noqa: ARG001 collection_toy_story: str, @@ -29,7 +29,7 @@ async def test_movie_create( 'the duo eventually learns to put aside their differences.' ), } - response = await async_client.post('/filmin/movie', json=data) + response = await async_admin_client.post('/filmin/movie', json=data) assert response.status_code == 201 result = response.json() assert 'uuid' in result @@ -92,7 +92,7 @@ async def test_movie_list( ) async def test_movie_update_partial( clean_movie: Any, # noqa: ARG001 - async_client: AsyncClient, + async_admin_client: AsyncClient, movie_toy_story2: Any, collection_fake: str, # noqa: ARG001 attribute: str, @@ -101,7 +101,7 @@ async def test_movie_update_partial( data = { attribute: value, } - response = await async_client.patch(f'/filmin/movie/{movie_toy_story2}', json=data) + response = await async_admin_client.patch(f'/filmin/movie/{movie_toy_story2}', json=data) assert response.status_code == 200 result = response.json() @@ -110,10 +110,10 @@ async def test_movie_update_partial( @pytest.mark.asyncio async def test_movie_update_partial_collection( - clean_movie: Any, async_client: AsyncClient, movie_toy_story2: Any, collection_fake: str # noqa: ARG001 + clean_movie: Any, async_admin_client: AsyncClient, movie_toy_story2: Any, collection_fake: str # noqa: ARG001 ) -> None: data = {'collection': collection_fake} - response = await async_client.patch(f'/filmin/movie/{movie_toy_story2}', json=data) + response = await async_admin_client.patch(f'/filmin/movie/{movie_toy_story2}', json=data) assert response.status_code == 200 result = response.json() @@ -122,10 +122,10 @@ async def test_movie_update_partial_collection( @pytest.mark.asyncio async def test_movie_update_genres( - clean_movie: Any, async_client: AsyncClient, movie_toy_story2: Any, genre_fake: str # noqa: ARG001 + clean_movie: Any, async_admin_client: AsyncClient, movie_toy_story2: Any, genre_fake: str # noqa: ARG001 ) -> None: data = {'genres': ['animation', 'fake-genre']} - response = await async_client.patch(f'/filmin/movie/{movie_toy_story2}', json=data) + response = await async_admin_client.patch(f'/filmin/movie/{movie_toy_story2}', json=data) assert response.status_code == 200 result = response.json() diff --git a/src/tests/filmin/adapters/api/http/test_production_company.py b/src/tests/filmin/adapters/api/http/test_production_company.py new file mode 100644 index 0000000..ff443e1 --- /dev/null +++ b/src/tests/filmin/adapters/api/http/test_production_company.py @@ -0,0 +1,68 @@ +from typing import Any + +from faker import Faker +from httpx import AsyncClient +import pytest + + +fake = Faker() + + +@pytest.mark.asyncio +async def test_production_company_create( + async_admin_client: AsyncClient, + clean_production_company: Any, # noqa: ARG001 +) -> None: + data = {'name': fake.company()} + response = await async_admin_client.post('/filmin/production-company', json=data) + assert response.status_code == 201 + result = response.json() + assert 'uuid' in result + + +@pytest.mark.asyncio +async def test_production_company_list( + async_normal_client: AsyncClient, + production_company1: dict[str, str], + production_company2: dict[str, str], +) -> None: + uuids = [production_company1['uuid'], production_company2['uuid']] + names = [production_company1['name'], production_company2['name']] + response = await async_normal_client.get('/filmin/production-company') + assert response.status_code == 200 + paginated_result = response.json() + assert 'results' in paginated_result + assert len(paginated_result['results']) == 2 + for result in paginated_result['results']: + assert 'uuid' in result and result['uuid'] in uuids + assert 'name' in result and result['name'] in names + + +@pytest.mark.asyncio +async def test_production_company_detail(async_normal_client: AsyncClient, production_company1: dict[str, str]) -> None: + response = await async_normal_client.get(f"/filmin/production-company/{production_company1['uuid']}") + assert response.status_code == 200 + result = response.json() + assert 'name' in result and result['name'] == production_company1['name'] + + +@pytest.mark.asyncio +async def test_production_company_update(async_admin_client: AsyncClient, production_company1: dict[str, str]) -> None: + data = {'name': fake.company()} + response = await async_admin_client.put(f"/filmin/production-company/{production_company1['uuid']}", json=data) + assert response.status_code == 200 + result = response.json() + assert 'name' in result + assert result['name'] == data['name'] + + +@pytest.mark.asyncio +async def test_production_company_update_partial( + async_admin_client: AsyncClient, production_company1: dict[str, str] +) -> None: + data = {'name': fake.company()} + response = await async_admin_client.patch(f"/filmin/production-company/{production_company1['uuid']}", json=data) + assert response.status_code == 200 + result = response.json() + assert 'name' in result + assert result['name'] == data['name'] diff --git a/src/tests/filmin/api/conftest.py b/src/tests/filmin/conftest.py similarity index 76% rename from src/tests/filmin/api/conftest.py rename to src/tests/filmin/conftest.py index 63a5ebb..6d5a25a 100644 --- a/src/tests/filmin/api/conftest.py +++ b/src/tests/filmin/conftest.py @@ -2,6 +2,7 @@ from typing import AsyncContextManager, AsyncGenerator, Callable +from faker import Faker import pytest_asyncio from sqlalchemy.ext.asyncio import AsyncSession @@ -9,8 +10,11 @@ from filmin.infra.database.sqlalchemy.models.collection import movie_collection from filmin.infra.database.sqlalchemy.models.genre import genres from filmin.infra.database.sqlalchemy.models.movie import movie, movie_genre +from filmin.infra.database.sqlalchemy.models.production_company import production_company +fake = Faker() + AsyncSessionCtxT = Callable[[], AsyncContextManager[AsyncSession]] @@ -128,3 +132,36 @@ async def clean_movie(async_session_maker: AsyncSessionCtxT) -> AsyncGenerator: await session.execute(stmt) await session.commit() + + +@pytest_asyncio.fixture +async def clean_production_company(async_session_maker: AsyncSessionCtxT) -> AsyncGenerator: + yield + async with async_session_maker() as session: + stmt = production_company.delete() + await session.execute(stmt) + + stmt = movie.delete() + await session.execute(stmt) + + await session.commit() + + +@pytest_asyncio.fixture +async def production_company1(async_session_maker: AsyncSessionCtxT) -> dict[str, str]: + async with async_session_maker() as session: + name = fake.company() + stmt = production_company.insert().values(name=name).returning(production_company.c.uuid) + result = await session.execute(stmt) + await session.commit() + return {'uuid': str(result.scalar_one()), 'name': name} + + +@pytest_asyncio.fixture +async def production_company2(async_session_maker: AsyncSessionCtxT) -> dict[str, str]: + async with async_session_maker() as session: + name = fake.company() + stmt = production_company.insert().values(name=name).returning(production_company.c.uuid) + result = await session.execute(stmt) + await session.commit() + return {'uuid': str(result.scalar_one()), 'name': name}