diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml index d697eaa..734f605 100644 --- a/.github/workflows/lint-and-test.yml +++ b/.github/workflows/lint-and-test.yml @@ -15,6 +15,7 @@ jobs: lint-and-test: uses: esclient/tools/.github/workflows/lint-and-test-python.yml@v1.0.0 with: - python-version: '3.13.7' + python-version: "3.13.7" + source: "commentservice" secrets: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/Dockerfile b/Dockerfile index 714c456..d23d1cb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM python:3.13-slim -RUN apt-get update && apt-get install -y wget curl jq && \ +RUN apt-get update && apt-get install -y curl jq wget && \ wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 && \ chmod +x /usr/local/bin/yq && \ apt-get remove -y wget && apt-get autoremove -y && \ @@ -9,15 +9,14 @@ RUN apt-get update && apt-get install -y wget curl jq && \ WORKDIR /app COPY pyproject.toml pdm.lock ./ +COPY . . RUN pip install --no-cache-dir pdm && \ pdm export --without-hashes -f requirements > /tmp/req.txt && \ pip install --no-cache-dir -r /tmp/req.txt && \ pip uninstall -y pdm && \ - rm /tmp/req.txt - -COPY . . -RUN pip install --no-cache-dir -e . -RUN chmod +x tools/load_envs.sh + rm /tmp/req.txt && \ + pip install --no-cache-dir -e . && \ + chmod +x tools/load_envs.sh ENV ENV=prod ENV PYTHONPATH=src diff --git a/justfile b/justfile index 9de3706..59ae2d8 100644 --- a/justfile +++ b/justfile @@ -7,7 +7,8 @@ LOAD_ENVS_URL := 'https://raw.githubusercontent.com/esclient/tools/refs/heads/ma PROTO_TAG := 'v0.0.17' PROTO_NAME := 'comment.proto' TMP_DIR := '.proto' -OUT_DIR := 'src/commentservice/grpc' +SOURCE := 'commentservice' +OUT_DIR := 'src/' + SOURCE + '/grpc' MKDIR_TOOLS := 'mkdir -p tools' diff --git a/pdm.lock b/pdm.lock index 301df39..1abebf0 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "dev"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:d5bab202f6faef35a9f10ac6d61606aec20f4d5b719540d89425540741c7130c" +content_hash = "sha256:e3b96de7c4efceb61703b10262da72cbc0a735cbb324a8fe866247d8c513a297" [[metadata.targets]] requires_python = "~=3.13" @@ -26,9 +26,9 @@ files = [ [[package]] name = "anyio" -version = "4.9.0" +version = "4.11.0" requires_python = ">=3.9" -summary = "High level compatibility layer for multiple asynchronous event loop implementations" +summary = "High-level concurrency and networking framework on top of asyncio or Trio" groups = ["default"] dependencies = [ "exceptiongroup>=1.0.2; python_version < \"3.11\"", @@ -37,8 +37,8 @@ dependencies = [ "typing-extensions>=4.5; python_version < \"3.13\"", ] files = [ - {file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"}, - {file = "anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"}, + {file = "anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc"}, + {file = "anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4"}, ] [[package]] @@ -89,7 +89,7 @@ files = [ [[package]] name = "click" -version = "8.2.1" +version = "8.3.0" requires_python = ">=3.10" summary = "Composable command line interface toolkit" groups = ["dev"] @@ -97,8 +97,8 @@ dependencies = [ "colorama; platform_system == \"Windows\"", ] files = [ - {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"}, - {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"}, + {file = "click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc"}, + {file = "click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4"}, ] [[package]] @@ -113,6 +113,151 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "coverage" +version = "7.11.0" +requires_python = ">=3.10" +summary = "Code coverage measurement for Python" +groups = ["dev"] +files = [ + {file = "coverage-7.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cc3f49e65ea6e0d5d9bd60368684fe52a704d46f9e7fc413918f18d046ec40e1"}, + {file = "coverage-7.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f39ae2f63f37472c17b4990f794035c9890418b1b8cca75c01193f3c8d3e01be"}, + {file = "coverage-7.11.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7db53b5cdd2917b6eaadd0b1251cf4e7d96f4a8d24e174bdbdf2f65b5ea7994d"}, + {file = "coverage-7.11.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10ad04ac3a122048688387828b4537bc9cf60c0bf4869c1e9989c46e45690b82"}, + {file = "coverage-7.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4036cc9c7983a2b1f2556d574d2eb2154ac6ed55114761685657e38782b23f52"}, + {file = "coverage-7.11.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7ab934dd13b1c5e94b692b1e01bd87e4488cb746e3a50f798cb9464fd128374b"}, + {file = "coverage-7.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59a6e5a265f7cfc05f76e3bb53eca2e0dfe90f05e07e849930fecd6abb8f40b4"}, + {file = "coverage-7.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:df01d6c4c81e15a7c88337b795bb7595a8596e92310266b5072c7e301168efbd"}, + {file = "coverage-7.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8c934bd088eed6174210942761e38ee81d28c46de0132ebb1801dbe36a390dcc"}, + {file = "coverage-7.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a03eaf7ec24078ad64a07f02e30060aaf22b91dedf31a6b24d0d98d2bba7f48"}, + {file = "coverage-7.11.0-cp313-cp313-win32.whl", hash = "sha256:695340f698a5f56f795b2836abe6fb576e7c53d48cd155ad2f80fd24bc63a040"}, + {file = "coverage-7.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:2727d47fce3ee2bac648528e41455d1b0c46395a087a229deac75e9f88ba5a05"}, + {file = "coverage-7.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:0efa742f431529699712b92ecdf22de8ff198df41e43aeaaadf69973eb93f17a"}, + {file = "coverage-7.11.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:587c38849b853b157706407e9ebdca8fd12f45869edb56defbef2daa5fb0812b"}, + {file = "coverage-7.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b971bdefdd75096163dd4261c74be813c4508477e39ff7b92191dea19f24cd37"}, + {file = "coverage-7.11.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:269bfe913b7d5be12ab13a95f3a76da23cf147be7fa043933320ba5625f0a8de"}, + {file = "coverage-7.11.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:dadbcce51a10c07b7c72b0ce4a25e4b6dcb0c0372846afb8e5b6307a121eb99f"}, + {file = "coverage-7.11.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ed43fa22c6436f7957df036331f8fe4efa7af132054e1844918866cd228af6c"}, + {file = "coverage-7.11.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9516add7256b6713ec08359b7b05aeff8850c98d357784c7205b2e60aa2513fa"}, + {file = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb92e47c92fcbcdc692f428da67db33337fa213756f7adb6a011f7b5a7a20740"}, + {file = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d06f4fc7acf3cabd6d74941d53329e06bab00a8fe10e4df2714f0b134bfc64ef"}, + {file = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:6fbcee1a8f056af07ecd344482f711f563a9eb1c2cad192e87df00338ec3cdb0"}, + {file = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dbbf012be5f32533a490709ad597ad8a8ff80c582a95adc8d62af664e532f9ca"}, + {file = "coverage-7.11.0-cp313-cp313t-win32.whl", hash = "sha256:cee6291bb4fed184f1c2b663606a115c743df98a537c969c3c64b49989da96c2"}, + {file = "coverage-7.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a386c1061bf98e7ea4758e4313c0ab5ecf57af341ef0f43a0bf26c2477b5c268"}, + {file = "coverage-7.11.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f9ea02ef40bb83823b2b04964459d281688fe173e20643870bb5d2edf68bc836"}, + {file = "coverage-7.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c770885b28fb399aaf2a65bbd1c12bf6f307ffd112d6a76c5231a94276f0c497"}, + {file = "coverage-7.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a3d0e2087dba64c86a6b254f43e12d264b636a39e88c5cc0a01a7c71bcfdab7e"}, + {file = "coverage-7.11.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73feb83bb41c32811973b8565f3705caf01d928d972b72042b44e97c71fd70d1"}, + {file = "coverage-7.11.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c6f31f281012235ad08f9a560976cc2fc9c95c17604ff3ab20120fe480169bca"}, + {file = "coverage-7.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9570ad567f880ef675673992222746a124b9595506826b210fbe0ce3f0499cd"}, + {file = "coverage-7.11.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8badf70446042553a773547a61fecaa734b55dc738cacf20c56ab04b77425e43"}, + {file = "coverage-7.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a09c1211959903a479e389685b7feb8a17f59ec5a4ef9afde7650bd5eabc2777"}, + {file = "coverage-7.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:5ef83b107f50db3f9ae40f69e34b3bd9337456c5a7fe3461c7abf8b75dd666a2"}, + {file = "coverage-7.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f91f927a3215b8907e214af77200250bb6aae36eca3f760f89780d13e495388d"}, + {file = "coverage-7.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cdbcd376716d6b7fbfeedd687a6c4be019c5a5671b35f804ba76a4c0a778cba4"}, + {file = "coverage-7.11.0-cp314-cp314-win32.whl", hash = "sha256:bab7ec4bb501743edc63609320aaec8cd9188b396354f482f4de4d40a9d10721"}, + {file = "coverage-7.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:3d4ba9a449e9364a936a27322b20d32d8b166553bfe63059bd21527e681e2fad"}, + {file = "coverage-7.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:ce37f215223af94ef0f75ac68ea096f9f8e8c8ec7d6e8c346ee45c0d363f0479"}, + {file = "coverage-7.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f413ce6e07e0d0dc9c433228727b619871532674b45165abafe201f200cc215f"}, + {file = "coverage-7.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:05791e528a18f7072bf5998ba772fe29db4da1234c45c2087866b5ba4dea710e"}, + {file = "coverage-7.11.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cacb29f420cfeb9283b803263c3b9a068924474ff19ca126ba9103e1278dfa44"}, + {file = "coverage-7.11.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314c24e700d7027ae3ab0d95fbf8d53544fca1f20345fd30cd219b737c6e58d3"}, + {file = "coverage-7.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:630d0bd7a293ad2fc8b4b94e5758c8b2536fdf36c05f1681270203e463cbfa9b"}, + {file = "coverage-7.11.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e89641f5175d65e2dbb44db15fe4ea48fade5d5bbb9868fdc2b4fce22f4a469d"}, + {file = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c9f08ea03114a637dab06cedb2e914da9dc67fa52c6015c018ff43fdde25b9c2"}, + {file = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce9f3bde4e9b031eaf1eb61df95c1401427029ea1bfddb8621c1161dcb0fa02e"}, + {file = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:e4dc07e95495923d6fd4d6c27bf70769425b71c89053083843fd78f378558996"}, + {file = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:424538266794db2861db4922b05d729ade0940ee69dcf0591ce8f69784db0e11"}, + {file = "coverage-7.11.0-cp314-cp314t-win32.whl", hash = "sha256:4c1eeb3fb8eb9e0190bebafd0462936f75717687117339f708f395fe455acc73"}, + {file = "coverage-7.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b56efee146c98dbf2cf5cffc61b9829d1e94442df4d7398b26892a53992d3547"}, + {file = "coverage-7.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b5c2705afa83f49bd91962a4094b6b082f94aef7626365ab3f8f4bd159c5acf3"}, + {file = "coverage-7.11.0-py3-none-any.whl", hash = "sha256:4b7589765348d78fb4e5fb6ea35d07564e387da2fc5efff62e0222971f155f68"}, + {file = "coverage-7.11.0.tar.gz", hash = "sha256:167bd504ac1ca2af7ff3b81d245dfea0292c5032ebef9d66cc08a7d28c1b8050"}, +] + +[[package]] +name = "coverage" +version = "7.11.0" +extras = ["toml"] +requires_python = ">=3.10" +summary = "Code coverage measurement for Python" +groups = ["dev"] +dependencies = [ + "coverage==7.11.0", + "tomli; python_full_version <= \"3.11.0a6\"", +] +files = [ + {file = "coverage-7.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cc3f49e65ea6e0d5d9bd60368684fe52a704d46f9e7fc413918f18d046ec40e1"}, + {file = "coverage-7.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f39ae2f63f37472c17b4990f794035c9890418b1b8cca75c01193f3c8d3e01be"}, + {file = "coverage-7.11.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7db53b5cdd2917b6eaadd0b1251cf4e7d96f4a8d24e174bdbdf2f65b5ea7994d"}, + {file = "coverage-7.11.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10ad04ac3a122048688387828b4537bc9cf60c0bf4869c1e9989c46e45690b82"}, + {file = "coverage-7.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4036cc9c7983a2b1f2556d574d2eb2154ac6ed55114761685657e38782b23f52"}, + {file = "coverage-7.11.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7ab934dd13b1c5e94b692b1e01bd87e4488cb746e3a50f798cb9464fd128374b"}, + {file = "coverage-7.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59a6e5a265f7cfc05f76e3bb53eca2e0dfe90f05e07e849930fecd6abb8f40b4"}, + {file = "coverage-7.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:df01d6c4c81e15a7c88337b795bb7595a8596e92310266b5072c7e301168efbd"}, + {file = "coverage-7.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8c934bd088eed6174210942761e38ee81d28c46de0132ebb1801dbe36a390dcc"}, + {file = "coverage-7.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a03eaf7ec24078ad64a07f02e30060aaf22b91dedf31a6b24d0d98d2bba7f48"}, + {file = "coverage-7.11.0-cp313-cp313-win32.whl", hash = "sha256:695340f698a5f56f795b2836abe6fb576e7c53d48cd155ad2f80fd24bc63a040"}, + {file = "coverage-7.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:2727d47fce3ee2bac648528e41455d1b0c46395a087a229deac75e9f88ba5a05"}, + {file = "coverage-7.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:0efa742f431529699712b92ecdf22de8ff198df41e43aeaaadf69973eb93f17a"}, + {file = "coverage-7.11.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:587c38849b853b157706407e9ebdca8fd12f45869edb56defbef2daa5fb0812b"}, + {file = "coverage-7.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b971bdefdd75096163dd4261c74be813c4508477e39ff7b92191dea19f24cd37"}, + {file = "coverage-7.11.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:269bfe913b7d5be12ab13a95f3a76da23cf147be7fa043933320ba5625f0a8de"}, + {file = "coverage-7.11.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:dadbcce51a10c07b7c72b0ce4a25e4b6dcb0c0372846afb8e5b6307a121eb99f"}, + {file = "coverage-7.11.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ed43fa22c6436f7957df036331f8fe4efa7af132054e1844918866cd228af6c"}, + {file = "coverage-7.11.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9516add7256b6713ec08359b7b05aeff8850c98d357784c7205b2e60aa2513fa"}, + {file = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb92e47c92fcbcdc692f428da67db33337fa213756f7adb6a011f7b5a7a20740"}, + {file = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d06f4fc7acf3cabd6d74941d53329e06bab00a8fe10e4df2714f0b134bfc64ef"}, + {file = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:6fbcee1a8f056af07ecd344482f711f563a9eb1c2cad192e87df00338ec3cdb0"}, + {file = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dbbf012be5f32533a490709ad597ad8a8ff80c582a95adc8d62af664e532f9ca"}, + {file = "coverage-7.11.0-cp313-cp313t-win32.whl", hash = "sha256:cee6291bb4fed184f1c2b663606a115c743df98a537c969c3c64b49989da96c2"}, + {file = "coverage-7.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a386c1061bf98e7ea4758e4313c0ab5ecf57af341ef0f43a0bf26c2477b5c268"}, + {file = "coverage-7.11.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f9ea02ef40bb83823b2b04964459d281688fe173e20643870bb5d2edf68bc836"}, + {file = "coverage-7.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c770885b28fb399aaf2a65bbd1c12bf6f307ffd112d6a76c5231a94276f0c497"}, + {file = "coverage-7.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a3d0e2087dba64c86a6b254f43e12d264b636a39e88c5cc0a01a7c71bcfdab7e"}, + {file = "coverage-7.11.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73feb83bb41c32811973b8565f3705caf01d928d972b72042b44e97c71fd70d1"}, + {file = "coverage-7.11.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c6f31f281012235ad08f9a560976cc2fc9c95c17604ff3ab20120fe480169bca"}, + {file = "coverage-7.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9570ad567f880ef675673992222746a124b9595506826b210fbe0ce3f0499cd"}, + {file = "coverage-7.11.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8badf70446042553a773547a61fecaa734b55dc738cacf20c56ab04b77425e43"}, + {file = "coverage-7.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a09c1211959903a479e389685b7feb8a17f59ec5a4ef9afde7650bd5eabc2777"}, + {file = "coverage-7.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:5ef83b107f50db3f9ae40f69e34b3bd9337456c5a7fe3461c7abf8b75dd666a2"}, + {file = "coverage-7.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f91f927a3215b8907e214af77200250bb6aae36eca3f760f89780d13e495388d"}, + {file = "coverage-7.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cdbcd376716d6b7fbfeedd687a6c4be019c5a5671b35f804ba76a4c0a778cba4"}, + {file = "coverage-7.11.0-cp314-cp314-win32.whl", hash = "sha256:bab7ec4bb501743edc63609320aaec8cd9188b396354f482f4de4d40a9d10721"}, + {file = "coverage-7.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:3d4ba9a449e9364a936a27322b20d32d8b166553bfe63059bd21527e681e2fad"}, + {file = "coverage-7.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:ce37f215223af94ef0f75ac68ea096f9f8e8c8ec7d6e8c346ee45c0d363f0479"}, + {file = "coverage-7.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f413ce6e07e0d0dc9c433228727b619871532674b45165abafe201f200cc215f"}, + {file = "coverage-7.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:05791e528a18f7072bf5998ba772fe29db4da1234c45c2087866b5ba4dea710e"}, + {file = "coverage-7.11.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cacb29f420cfeb9283b803263c3b9a068924474ff19ca126ba9103e1278dfa44"}, + {file = "coverage-7.11.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314c24e700d7027ae3ab0d95fbf8d53544fca1f20345fd30cd219b737c6e58d3"}, + {file = "coverage-7.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:630d0bd7a293ad2fc8b4b94e5758c8b2536fdf36c05f1681270203e463cbfa9b"}, + {file = "coverage-7.11.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e89641f5175d65e2dbb44db15fe4ea48fade5d5bbb9868fdc2b4fce22f4a469d"}, + {file = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c9f08ea03114a637dab06cedb2e914da9dc67fa52c6015c018ff43fdde25b9c2"}, + {file = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce9f3bde4e9b031eaf1eb61df95c1401427029ea1bfddb8621c1161dcb0fa02e"}, + {file = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:e4dc07e95495923d6fd4d6c27bf70769425b71c89053083843fd78f378558996"}, + {file = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:424538266794db2861db4922b05d729ade0940ee69dcf0591ce8f69784db0e11"}, + {file = "coverage-7.11.0-cp314-cp314t-win32.whl", hash = "sha256:4c1eeb3fb8eb9e0190bebafd0462936f75717687117339f708f395fe455acc73"}, + {file = "coverage-7.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b56efee146c98dbf2cf5cffc61b9829d1e94442df4d7398b26892a53992d3547"}, + {file = "coverage-7.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b5c2705afa83f49bd91962a4094b6b082f94aef7626365ab3f8f4bd159c5acf3"}, + {file = "coverage-7.11.0-py3-none-any.whl", hash = "sha256:4b7589765348d78fb4e5fb6ea35d07564e387da2fc5efff62e0222971f155f68"}, + {file = "coverage-7.11.0.tar.gz", hash = "sha256:167bd504ac1ca2af7ff3b81d245dfea0292c5032ebef9d66cc08a7d28c1b8050"}, +] + +[[package]] +name = "faker" +version = "37.11.0" +requires_python = ">=3.9" +summary = "Faker is a Python package that generates fake data for you." +groups = ["dev"] +dependencies = [ + "tzdata", +] +files = [ + {file = "faker-37.11.0-py3-none-any.whl", hash = "sha256:1508d2da94dfd1e0087b36f386126d84f8583b3de19ac18e392a2831a6676c57"}, + {file = "faker-37.11.0.tar.gz", hash = "sha256:22969803849ba0618be8eee2dd01d0d9e2cd3b75e6ff1a291fa9abcdb34da5e6"}, +] + [[package]] name = "flake8" version = "7.3.0" @@ -243,24 +388,24 @@ files = [ [[package]] name = "idna" -version = "3.10" -requires_python = ">=3.6" +version = "3.11" +requires_python = ">=3.8" summary = "Internationalized Domain Names in Applications (IDNA)" groups = ["default"] files = [ - {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, - {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, + {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, + {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, ] [[package]] name = "iniconfig" -version = "2.1.0" -requires_python = ">=3.8" +version = "2.3.0" +requires_python = ">=3.10" summary = "brain-dead simple config-ini parsing" groups = ["dev"] files = [ - {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, - {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, + {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, + {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, ] [[package]] @@ -349,13 +494,13 @@ files = [ [[package]] name = "platformdirs" -version = "4.3.8" -requires_python = ">=3.9" +version = "4.5.0" +requires_python = ">=3.10" summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." groups = ["dev"] files = [ - {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"}, - {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"}, + {file = "platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3"}, + {file = "platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312"}, ] [[package]] @@ -444,47 +589,6 @@ dependencies = [ "typing-extensions!=4.7.0,>=4.6.0", ] files = [ - {file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"}, - {file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a"}, - {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac"}, - {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a"}, - {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b"}, - {file = "pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22"}, - {file = "pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640"}, - {file = "pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7"}, - {file = "pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e"}, - {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d"}, - {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30"}, - {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf"}, - {file = "pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51"}, - {file = "pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab"}, - {file = "pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65"}, - {file = "pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc"}, - {file = "pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b"}, - {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1"}, - {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6"}, - {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea"}, - {file = "pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290"}, - {file = "pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2"}, - {file = "pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab"}, {file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"}, {file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"}, {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"}, @@ -502,24 +606,6 @@ files = [ {file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"}, {file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"}, {file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1"}, {file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"}, ] @@ -581,6 +667,47 @@ files = [ {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, ] +[[package]] +name = "pytest-asyncio" +version = "0.24.0" +requires_python = ">=3.8" +summary = "Pytest support for asyncio" +groups = ["dev"] +dependencies = [ + "pytest<9,>=8.2", +] +files = [ + {file = "pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b"}, + {file = "pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276"}, +] + +[[package]] +name = "pytest-cov" +version = "5.0.0" +requires_python = ">=3.8" +summary = "Pytest plugin for measuring coverage." +groups = ["dev"] +dependencies = [ + "coverage[toml]>=5.2.1", + "pytest>=4.6", +] +files = [ + {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, + {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, +] + +[[package]] +name = "pytest-faker" +version = "2.0.0" +summary = "Faker integration with the pytest framework." +groups = ["dev"] +dependencies = [ + "Faker>=0.7.3", +] +files = [ + {file = "pytest-faker-2.0.0.tar.gz", hash = "sha256:6b37bb89d94f96552bfa51f8e8b89d32addded8ddb58a331488299ef0137d9b6"}, +] + [[package]] name = "pytest-mock" version = "3.15.1" @@ -608,13 +735,13 @@ files = [ [[package]] name = "pytokens" -version = "0.1.10" +version = "0.2.0" requires_python = ">=3.8" -summary = "A Fast, spec compliant Python 3.12+ tokenizer that runs on older Pythons." +summary = "A Fast, spec compliant Python 3.13+ tokenizer that runs on older Pythons." groups = ["dev"] files = [ - {file = "pytokens-0.1.10-py3-none-any.whl", hash = "sha256:db7b72284e480e69fb085d9f251f66b3d2df8b7166059261258ff35f50fb711b"}, - {file = "pytokens-0.1.10.tar.gz", hash = "sha256:c9a4bfa0be1d26aebce03e6884ba454e842f186a59ea43a6d3b25af58223c044"}, + {file = "pytokens-0.2.0-py3-none-any.whl", hash = "sha256:74d4b318c67f4295c13782ddd9abcb7e297ec5630ad060eb90abf7ebbefe59f8"}, + {file = "pytokens-0.2.0.tar.gz", hash = "sha256:532d6421364e5869ea57a9523bf385f02586d4662acbcc0342afd69511b4dd43"}, ] [[package]] @@ -691,18 +818,18 @@ files = [ [[package]] name = "typing-extensions" -version = "4.14.1" +version = "4.15.0" requires_python = ">=3.9" summary = "Backported and Experimental Type Hints for Python 3.9+" groups = ["default", "dev"] files = [ - {file = "typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76"}, - {file = "typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36"}, + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, ] [[package]] name = "typing-inspection" -version = "0.4.1" +version = "0.4.2" requires_python = ">=3.9" summary = "Runtime typing introspection tools" groups = ["default"] @@ -710,8 +837,19 @@ dependencies = [ "typing-extensions>=4.12.0", ] files = [ - {file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"}, - {file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"}, + {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, + {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, +] + +[[package]] +name = "tzdata" +version = "2025.2" +requires_python = ">=2" +summary = "Provider of IANA time zone data" +groups = ["dev"] +files = [ + {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, + {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index 519ef09..f367c1b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -93,6 +93,24 @@ plugins = ["pydantic.mypy"] module = "asyncpg.*" ignore_missing_imports = true +[tool.pytest.ini_options] +addopts = "--import-mode=importlib" +asyncio_mode = "strict" +testpaths = ["tests"] +python_files = ["test_*.py"] + +[tool.coverage.run] +source = ["src/commentservice"] +relative_files = true + +[tool.coverage.report] +include = [ + "src/commentservice/handler/*", + "src/commentservice/service/*", + "src/commentservice/repository/*", +] +omit = ["src/commentservice/server.py"] + [project] name = "commentservice" version = "0.0.1" @@ -122,7 +140,11 @@ dev = [ "grpc-stubs==1.53.0.6", "protobuf==6.32.1", "pytest==8.4.2", + "pytest-asyncio==0.24.0", + "pytest-cov==5.0.0", "pytest-mock==3.15.1", + "pytest-faker==2.0.0", + "Faker==37.11.0", "black==25.9.0", "isort==6.0.1", "flake8==7.3.0", diff --git a/tests/handler/test_create_comment.py b/tests/handler/test_create_comment.py index db069ee..c0d901b 100644 --- a/tests/handler/test_create_comment.py +++ b/tests/handler/test_create_comment.py @@ -1,4 +1,8 @@ +from unittest.mock import AsyncMock + import grpc +import pytest +from faker import Faker from pytest_mock import MockerFixture from commentservice.grpc.comment_pb2 import ( @@ -9,20 +13,27 @@ from commentservice.service.service import CommentService -def test_create_comment_success(mocker: MockerFixture) -> None: +@pytest.mark.asyncio +async def test_create_comment_success( + mocker: MockerFixture, faker: Faker +) -> None: ctx = mocker.Mock(spec=grpc.ServicerContext) fake_service = mocker.Mock(spec=CommentService) - fake_service.create_comment.return_value = 42 + new_id = faker.random_int(min=1, max=100000) + fake_service.create_comment = AsyncMock(return_value=new_id) + mod_id = faker.random_int(min=1, max=100000) + author_id = faker.random_int(min=1, max=100000) + text = faker.sentence() request = CreateCommentRequest( - mod_id=7, - author_id=13, - text="Test text", + mod_id=mod_id, author_id=author_id, text=text ) - response = CreateComment(fake_service, request, ctx) + response = await CreateComment(fake_service, request, ctx) assert isinstance(response, CreateCommentResponse) - assert response.comment_id == 42 + assert response.comment_id == new_id - fake_service.create_comment.assert_called_once_with(7, 13, "Test text") + fake_service.create_comment.assert_awaited_once_with( + mod_id, author_id, text + ) diff --git a/tests/handler/test_delete_comment.py b/tests/handler/test_delete_comment.py new file mode 100644 index 0000000..ee87f94 --- /dev/null +++ b/tests/handler/test_delete_comment.py @@ -0,0 +1,47 @@ +from unittest.mock import AsyncMock + +import grpc +import pytest +from faker import Faker +from pytest_mock import MockerFixture + +from commentservice.grpc.comment_pb2 import ( + DeleteCommentRequest, + DeleteCommentResponse, +) +from commentservice.handler.delete_comment import DeleteComment +from commentservice.service.service import CommentService + + +@pytest.mark.asyncio +async def test_delete_comment_success( + mocker: MockerFixture, faker: Faker +) -> None: + ctx = mocker.Mock(spec=grpc.ServicerContext) + fake_service = mocker.Mock(spec=CommentService) + fake_service.delete_comment = AsyncMock(return_value=True) + + comment_id = faker.random_int(min=1, max=100000) + request = DeleteCommentRequest(comment_id=comment_id) + + response = await DeleteComment(fake_service, request, ctx) + + assert isinstance(response, DeleteCommentResponse) + assert response.success is True + fake_service.delete_comment.assert_awaited_once_with(comment_id) + + +@pytest.mark.asyncio +async def test_delete_comment_not_found( + mocker: MockerFixture, faker: Faker +) -> None: + ctx = mocker.Mock(spec=grpc.ServicerContext) + fake_service = mocker.Mock(spec=CommentService) + fake_service.delete_comment = AsyncMock(return_value=False) + + comment_id = faker.random_int(min=1, max=100000) + request = DeleteCommentRequest(comment_id=comment_id) + response = await DeleteComment(fake_service, request, ctx) + + assert response.success is False + fake_service.delete_comment.assert_awaited_once_with(comment_id) diff --git a/tests/handler/test_edit_comment.py b/tests/handler/test_edit_comment.py new file mode 100644 index 0000000..24345d8 --- /dev/null +++ b/tests/handler/test_edit_comment.py @@ -0,0 +1,48 @@ +from unittest.mock import AsyncMock + +import grpc +import pytest +from faker import Faker +from pytest_mock import MockerFixture + +from commentservice.grpc.comment_pb2 import ( + EditCommentRequest, + EditCommentResponse, +) +from commentservice.handler.edit_comment import EditComment +from commentservice.service.service import CommentService + + +@pytest.mark.asyncio +async def test_edit_comment_success( + mocker: MockerFixture, faker: Faker +) -> None: + ctx = mocker.Mock(spec=grpc.ServicerContext) + fake_service = mocker.Mock(spec=CommentService) + fake_service.edit_comment = AsyncMock(return_value=True) + + comment_id = faker.random_int(min=1, max=100000) + new_text = faker.sentence() + request = EditCommentRequest(comment_id=comment_id, text=new_text) + response = await EditComment(fake_service, request, ctx) + + assert isinstance(response, EditCommentResponse) + assert response.success is True + fake_service.edit_comment.assert_awaited_once_with(comment_id, new_text) + + +@pytest.mark.asyncio +async def test_edit_comment_not_found( + mocker: MockerFixture, faker: Faker +) -> None: + ctx = mocker.Mock(spec=grpc.ServicerContext) + fake_service = mocker.Mock(spec=CommentService) + fake_service.edit_comment = AsyncMock(return_value=False) + + comment_id = faker.random_int(min=1, max=100000) + new_text = faker.sentence() + request = EditCommentRequest(comment_id=comment_id, text=new_text) + response = await EditComment(fake_service, request, ctx) + + assert response.success is False + fake_service.edit_comment.assert_awaited_once_with(comment_id, new_text) diff --git a/tests/handler/test_get_comments.py b/tests/handler/test_get_comments.py new file mode 100644 index 0000000..4d4937b --- /dev/null +++ b/tests/handler/test_get_comments.py @@ -0,0 +1,66 @@ +from datetime import UTC, timedelta + +import grpc +import pytest +from faker import Faker +from pytest_mock import MockerFixture + +from commentservice.grpc.comment_pb2 import ( + GetCommentsRequest, + GetCommentsResponse, +) +from commentservice.handler.get_comments import GetComments, ts_to_dt +from commentservice.repository.model import Comment +from commentservice.service.service import CommentService + + +@pytest.mark.asyncio +async def test_get_comments_success( + mocker: MockerFixture, faker: Faker +) -> None: + ctx = mocker.Mock(spec=grpc.ServicerContext) + fake_service = mocker.Mock(spec=CommentService) + + now = faker.date_time(tzinfo=UTC) + earlier = now - timedelta(hours=faker.random_int(min=1, max=12)) + + comment1 = Comment( + id=faker.random_int(min=1, max=100000), + author_id=faker.random_int(min=1, max=100000), + text=faker.sentence(), + created_at=earlier, + edited_at=None, + ) + comment2 = Comment( + id=faker.random_int(min=1, max=100000), + author_id=faker.random_int(min=1, max=100000), + text=faker.sentence(), + created_at=now, + edited_at=now, + ) + comments = [comment1, comment2] + fake_service.get_comments.return_value = comments + + mod_id = faker.random_int(min=1, max=100000) + request = GetCommentsRequest(mod_id=mod_id) + response = await GetComments(fake_service, request, ctx) + + assert isinstance(response, GetCommentsResponse) + assert response.mod_id == mod_id + assert len(response.comments) == 2 + + c1 = response.comments[0] + assert c1.id == comment1.id + assert c1.author_id == comment1.author_id + assert c1.text == comment1.text + assert not c1.HasField("edited_at") + assert ts_to_dt(c1.created_at, tz=UTC) == earlier + + c2 = response.comments[1] + assert c2.id == comment2.id + assert c2.author_id == comment2.author_id + assert c2.text == comment2.text + assert c2.HasField("edited_at") + assert ts_to_dt(c2.created_at, tz=UTC) == now + + fake_service.get_comments.assert_called_once_with(mod_id=mod_id) diff --git a/tests/handler/test_handler.py b/tests/handler/test_handler.py new file mode 100644 index 0000000..442df42 --- /dev/null +++ b/tests/handler/test_handler.py @@ -0,0 +1,130 @@ +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any +from unittest.mock import AsyncMock + +import grpc +import pytest +from faker import Faker +from pytest_mock import MockerFixture + +import commentservice.handler.handler as handler_module +from commentservice.grpc import comment_pb2 +from commentservice.handler.handler import CommentHandler +from commentservice.service.service import CommentService + + +def _build_create_pair( + faker: Faker, +) -> tuple[ + comment_pb2.CreateCommentRequest, comment_pb2.CreateCommentResponse +]: + mod_id = faker.random_int(min=1, max=100000) + author_id = faker.random_int(min=1, max=100000) + text = faker.sentence() + response = comment_pb2.CreateCommentResponse( + comment_id=faker.random_int(min=1, max=100000) + ) + request = comment_pb2.CreateCommentRequest( + mod_id=mod_id, + author_id=author_id, + text=text, + ) + return request, response + + +def _build_edit_pair( + faker: Faker, +) -> tuple[comment_pb2.EditCommentRequest, comment_pb2.EditCommentResponse]: + comment_id = faker.random_int(min=1, max=100000) + new_text = faker.sentence() + response = comment_pb2.EditCommentResponse(success=True) + request = comment_pb2.EditCommentRequest( + comment_id=comment_id, text=new_text + ) + return request, response + + +def _build_delete_pair( + faker: Faker, +) -> tuple[ + comment_pb2.DeleteCommentRequest, comment_pb2.DeleteCommentResponse +]: + comment_id = faker.random_int(min=1, max=100000) + response = comment_pb2.DeleteCommentResponse(success=True) + request = comment_pb2.DeleteCommentRequest(comment_id=comment_id) + return request, response + + +def _build_get_pair( + faker: Faker, +) -> tuple[comment_pb2.GetCommentsRequest, comment_pb2.GetCommentsResponse]: + mod_id = faker.random_int(min=1, max=100000) + response = comment_pb2.GetCommentsResponse(mod_id=mod_id) + response.comments.add( + id=faker.random_int(min=1, max=100000), + author_id=faker.random_int(min=1, max=100000), + text=faker.sentence(), + ) + request = comment_pb2.GetCommentsRequest(mod_id=mod_id) + return request, response + + +@dataclass(frozen=True) +class HandlerCase: + method_name: str + helper_attr: str + builder: Callable[[Faker], tuple[Any, Any]] + + +CASES: tuple[HandlerCase, ...] = ( + HandlerCase( + "CreateComment", + "_create_comment", + _build_create_pair, + ), + HandlerCase( + "EditComment", + "_edit_comment", + _build_edit_pair, + ), + HandlerCase( + "DeleteComment", + "_delete_comment", + _build_delete_pair, + ), + HandlerCase( + "GetComments", + "_get_comments", + _build_get_pair, + ), +) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "case", + CASES, + ids=lambda case: case.method_name, +) +async def test_comment_handler_delegates_to_helper( + mocker: MockerFixture, + faker: Faker, + case: HandlerCase, +) -> None: + service = mocker.Mock(spec=CommentService) + handler = CommentHandler(service) + request, expected_response = case.builder(faker) + + helper = mocker.patch.object( + handler_module, + case.helper_attr, + new=AsyncMock(return_value=expected_response), + ) + context = mocker.Mock(spec=grpc.ServicerContext) + + method = getattr(handler, case.method_name) + result = await method(request, context) + + assert result is expected_response + helper.assert_awaited_once_with(service, request, context) diff --git a/tests/repository/test_create_comment.py b/tests/repository/test_create_comment.py new file mode 100644 index 0000000..564d5c5 --- /dev/null +++ b/tests/repository/test_create_comment.py @@ -0,0 +1,45 @@ +import textwrap +from unittest.mock import AsyncMock + +import pytest +from faker import Faker +from pytest_mock import MockerFixture + +from commentservice.repository.repository import CommentRepository + + +@pytest.mark.asyncio +async def test_repo_create_comment_returns_id( + mocker: MockerFixture, faker: Faker +) -> None: + comment_id = faker.random_int(min=1, max=100000) + conn = mocker.Mock() + conn.fetchval = AsyncMock(return_value=comment_id) + + pool = mocker.Mock() + pool.acquire = mocker.Mock() + pool.acquire.return_value = AsyncMock() + pool.acquire.return_value.__aenter__.return_value = conn + pool.acquire.return_value.__aexit__.return_value = None + + repo = CommentRepository(pool) + mod_id = faker.random_int(min=1, max=100000) + author_id = faker.random_int(min=1, max=100000) + text = faker.sentence() + new_id = await repo.create_comment( + mod_id=mod_id, author_id=author_id, text=text + ) + + assert new_id == comment_id + assert conn.fetchval.await_count == 1 + expected_sql = """ + INSERT INTO comments (mod_id, author_id, text) + VALUES ($1, $2, $3) + RETURNING id + """ + actual_sql = conn.fetchval.await_args.args[0] + assert ( + textwrap.dedent(actual_sql).strip() + == textwrap.dedent(expected_sql).strip() + ) + assert conn.fetchval.await_args.args[1:] == (mod_id, author_id, text) diff --git a/tests/repository/test_delete_comment.py b/tests/repository/test_delete_comment.py new file mode 100644 index 0000000..b5f48c0 --- /dev/null +++ b/tests/repository/test_delete_comment.py @@ -0,0 +1,68 @@ +import textwrap +from unittest.mock import AsyncMock + +import pytest +from faker import Faker +from pytest_mock import MockerFixture + +from commentservice.repository.repository import CommentRepository + + +@pytest.mark.asyncio +async def test_repo_delete_comment_success( + mocker: MockerFixture, faker: Faker +) -> None: + conn = mocker.Mock() + conn.fetchval = AsyncMock() + pool = mocker.Mock() + pool.acquire = mocker.Mock() + pool.acquire.return_value = AsyncMock() + pool.acquire.return_value.__aenter__.return_value = conn + pool.acquire.return_value.__aexit__.return_value = None + repo = CommentRepository(pool) + + cid = faker.random_int(min=1, max=100000) + conn.fetchval.return_value = cid + + ok = await repo.delete_comment(comment_id=cid) + assert ok is True + expected_sql = """ + DELETE FROM comments + WHERE id = $1 + RETURNING id + """ + actual_sql = conn.fetchval.await_args.args[0] + assert ( + textwrap.dedent(actual_sql).strip() + == textwrap.dedent(expected_sql).strip() + ) + assert conn.fetchval.await_args.args[1:] == (cid,) + + +@pytest.mark.asyncio +async def test_repo_delete_comment_not_found( + mocker: MockerFixture, faker: Faker +) -> None: + conn = mocker.Mock() + conn.fetchval = AsyncMock(return_value=None) + pool = mocker.Mock() + pool.acquire = mocker.Mock() + pool.acquire.return_value = AsyncMock() + pool.acquire.return_value.__aenter__.return_value = conn + pool.acquire.return_value.__aexit__.return_value = None + repo = CommentRepository(pool) + + cid = faker.random_int(min=1, max=100000) + ok = await repo.delete_comment(comment_id=cid) + assert ok is False + expected_sql = """ + DELETE FROM comments + WHERE id = $1 + RETURNING id + """ + actual_sql = conn.fetchval.await_args.args[0] + assert ( + textwrap.dedent(actual_sql).strip() + == textwrap.dedent(expected_sql).strip() + ) + assert conn.fetchval.await_args.args[1:] == (cid,) diff --git a/tests/repository/test_edit_comment.py b/tests/repository/test_edit_comment.py new file mode 100644 index 0000000..198dea4 --- /dev/null +++ b/tests/repository/test_edit_comment.py @@ -0,0 +1,71 @@ +import textwrap +from unittest.mock import AsyncMock + +import pytest +from faker import Faker +from pytest_mock import MockerFixture + +from commentservice.repository.repository import CommentRepository + + +@pytest.mark.asyncio +async def test_repo_edit_comment_success( + mocker: MockerFixture, faker: Faker +) -> None: + conn = mocker.Mock() + conn.fetchval = AsyncMock() + pool = mocker.Mock() + pool.acquire = mocker.Mock() + pool.acquire.return_value = AsyncMock() + pool.acquire.return_value.__aenter__.return_value = conn + pool.acquire.return_value.__aexit__.return_value = None + repo = CommentRepository(pool) + + cid = faker.random_int(min=1, max=100000) + new_text = faker.sentence() + conn.fetchval.return_value = cid + ok = await repo.edit_comment(comment_id=cid, new_text=new_text) + assert ok is True + expected_sql = """ + UPDATE comments + SET text = $1, edited_at = NOW() + WHERE id = $2 + RETURNING id + """ + actual_sql = conn.fetchval.await_args.args[0] + assert ( + textwrap.dedent(actual_sql).strip() + == textwrap.dedent(expected_sql).strip() + ) + assert conn.fetchval.await_args.args[1:] == (new_text, cid) + + +@pytest.mark.asyncio +async def test_repo_edit_comment_not_found( + mocker: MockerFixture, faker: Faker +) -> None: + conn = mocker.Mock() + conn.fetchval = AsyncMock(return_value=None) + pool = mocker.Mock() + pool.acquire = mocker.Mock() + pool.acquire.return_value = AsyncMock() + pool.acquire.return_value.__aenter__.return_value = conn + pool.acquire.return_value.__aexit__.return_value = None + repo = CommentRepository(pool) + + cid = faker.random_int(min=1, max=100000) + new_text = faker.sentence() + ok = await repo.edit_comment(comment_id=cid, new_text=new_text) + assert ok is False + expected_sql = """ + UPDATE comments + SET text = $1, edited_at = NOW() + WHERE id = $2 + RETURNING id + """ + actual_sql = conn.fetchval.await_args.args[0] + assert ( + textwrap.dedent(actual_sql).strip() + == textwrap.dedent(expected_sql).strip() + ) + assert conn.fetchval.await_args.args[1:] == (new_text, cid) diff --git a/tests/repository/test_get_comments.py b/tests/repository/test_get_comments.py new file mode 100644 index 0000000..03981b5 --- /dev/null +++ b/tests/repository/test_get_comments.py @@ -0,0 +1,61 @@ +import textwrap +from datetime import UTC +from unittest.mock import AsyncMock + +import pytest +from faker import Faker +from pytest_mock import MockerFixture + +from commentservice.repository.repository import CommentRepository + + +@pytest.mark.asyncio +async def test_repo_get_comments_maps_rows( + mocker: MockerFixture, faker: Faker +) -> None: + rows = [ + { + "id": faker.random_int(), + "author_id": faker.random_int(), + "text": faker.sentence(), + "created_at": faker.date_time(tzinfo=UTC), + "edited_at": None, + }, + { + "id": faker.random_int(), + "author_id": faker.random_int(), + "text": faker.sentence(), + "created_at": faker.date_time(tzinfo=UTC), + "edited_at": faker.date_time(tzinfo=UTC), + }, + ] + + conn = mocker.Mock() + conn.fetch = AsyncMock(return_value=rows) + pool = mocker.Mock() + pool.acquire = mocker.Mock() + pool.acquire.return_value = AsyncMock() + pool.acquire.return_value.__aenter__.return_value = conn + pool.acquire.return_value.__aexit__.return_value = None + repo = CommentRepository(pool) + + mod_id = faker.random_int(min=1, max=100000) + + out = await repo.get_comments(mod_id=mod_id) + + assert len(out) == 2 + assert out[0].id == rows[0]["id"] and out[0].text == rows[0]["text"] + assert out[0].edited_at is None + assert out[1].id == rows[1]["id"] and out[1].text == rows[1]["text"] + assert out[1].edited_at is not None + expected_sql = """ + SELECT id, author_id, text, created_at, edited_at + FROM comments + WHERE mod_id = $1 + """ + actual_sql = conn.fetch.await_args.args[0] + assert ( + textwrap.dedent(actual_sql).strip() + == textwrap.dedent(expected_sql).strip() + ) + assert conn.fetch.await_args.args[1:] == (mod_id,) diff --git a/tests/service/test_create_comment.py b/tests/service/test_create_comment.py new file mode 100644 index 0000000..4f6e216 --- /dev/null +++ b/tests/service/test_create_comment.py @@ -0,0 +1,29 @@ +from unittest.mock import AsyncMock + +import pytest +from faker import Faker +from pytest_mock import MockerFixture + +from commentservice.repository.repository import CommentRepository +from commentservice.service.service import CommentService + + +@pytest.mark.asyncio +async def test_service_create_comment( + mocker: MockerFixture, faker: Faker +) -> None: + fake_repo = mocker.Mock(spec=CommentRepository) + new_id = faker.random_int(min=1, max=100000) + fake_repo.create_comment = AsyncMock(return_value=new_id) + + mod_id = faker.random_int(min=1, max=100000) + author_id = faker.random_int(min=1, max=100000) + text = faker.sentence() + + service = CommentService(fake_repo) + result = await service.create_comment( + mod_id=mod_id, author_id=author_id, text=text + ) + + assert result == new_id + fake_repo.create_comment.assert_awaited_once_with(mod_id, author_id, text) diff --git a/tests/service/test_delete_comment.py b/tests/service/test_delete_comment.py new file mode 100644 index 0000000..5db3a93 --- /dev/null +++ b/tests/service/test_delete_comment.py @@ -0,0 +1,24 @@ +from unittest.mock import AsyncMock + +import pytest +from faker import Faker +from pytest_mock import MockerFixture + +from commentservice.repository.repository import CommentRepository +from commentservice.service.service import CommentService + + +@pytest.mark.asyncio +async def test_service_delete_comment( + mocker: MockerFixture, faker: Faker +) -> None: + fake_repo = mocker.Mock(spec=CommentRepository) + fake_repo.delete_comment = AsyncMock(return_value=True) + + comment_id = faker.random_int(min=1, max=100000) + + service = CommentService(fake_repo) + result = await service.delete_comment(comment_id=comment_id) + + assert result is True + fake_repo.delete_comment.assert_awaited_once_with(comment_id) diff --git a/tests/service/test_edit_comment.py b/tests/service/test_edit_comment.py new file mode 100644 index 0000000..c9d750d --- /dev/null +++ b/tests/service/test_edit_comment.py @@ -0,0 +1,27 @@ +from unittest.mock import AsyncMock + +import pytest +from faker import Faker +from pytest_mock import MockerFixture + +from commentservice.repository.repository import CommentRepository +from commentservice.service.service import CommentService + + +@pytest.mark.asyncio +async def test_service_edit_comment( + mocker: MockerFixture, faker: Faker +) -> None: + fake_repo = mocker.Mock(spec=CommentRepository) + fake_repo.edit_comment = AsyncMock(return_value=True) + + comment_id = faker.random_int(min=1, max=100000) + new_text = faker.sentence() + + service = CommentService(fake_repo) + result = await service.edit_comment( + comment_id=comment_id, new_text=new_text + ) + + assert result is True + fake_repo.edit_comment.assert_awaited_once_with(comment_id, new_text) diff --git a/tests/service/test_get_comments.py b/tests/service/test_get_comments.py new file mode 100644 index 0000000..6ad2343 --- /dev/null +++ b/tests/service/test_get_comments.py @@ -0,0 +1,41 @@ +from datetime import UTC +from unittest.mock import AsyncMock + +import pytest +from faker import Faker +from pytest_mock import MockerFixture + +from commentservice.repository.model import Comment +from commentservice.repository.repository import CommentRepository +from commentservice.service.service import CommentService + + +@pytest.mark.asyncio +async def test_service_get_comments( + mocker: MockerFixture, faker: Faker +) -> None: + fake_repo = mocker.Mock(spec=CommentRepository) + comments = [ + Comment( + id=faker.random_int(), + author_id=faker.random_int(), + text=faker.sentence(), + created_at=faker.date_time(tzinfo=UTC), + edited_at=None, + ), + Comment( + id=faker.random_int(), + author_id=faker.random_int(), + text=faker.sentence(), + created_at=faker.date_time(tzinfo=UTC), + edited_at=None, + ), + ] + fake_repo.get_comments = AsyncMock(return_value=comments) + + mod_id = faker.random_int(min=1, max=100000) + service = CommentService(fake_repo) + out = await service.get_comments(mod_id=mod_id) + + assert out == comments + fake_repo.get_comments.assert_awaited_once_with(mod_id) diff --git a/tools/common.just b/tools/common.just index 883b36c..b4cdc97 100644 --- a/tools/common.just +++ b/tools/common.just @@ -39,6 +39,8 @@ lint: mypy --strict . test: - pytest + pytest \ + --cov=src/{{SOURCE}} \ + --cov-report=xml:coverage.xml prepare: format lint test diff --git a/tools/load_envs.sh b/tools/load_envs.sh index e713b7b..cbc4a4b 100644 --- a/tools/load_envs.sh +++ b/tools/load_envs.sh @@ -1,7 +1,7 @@ #!/bin/bash set -e -if [ -f .env ]; then +if [ "$ENV" = "prod" ] && [ -f .env ]; then export $(grep -v '^#' .env | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | sed 's/"//g' | sed "s/'//g" | xargs) fi