diff --git a/Dockerfile b/Dockerfile index 6e11ee3b..35478a88 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,10 +8,6 @@ ENV BIN_DIR="$HOME_DIR/.local/bin" ENV PATH="$PATH:$BIN_DIR" ENV DATA_DIR="$HOME_DIR/data" ENV APP_DIR="$HOME_DIR/github-webhook-server" -ENV UV_PYTHON=python3.13 -ENV UV_COMPILE_BYTECODE=1 -ENV UV_NO_SYNC=1 -ENV UV_CACHE_DIR=${APP_DIR}/.cache RUN dnf -y install dnf-plugins-core \ && dnf -y update \ @@ -21,6 +17,10 @@ RUN dnf -y install dnf-plugins-core \ unzip \ gcc \ python3-devel \ + python3.10-devel \ + python3.11-devel \ + python3.12-devel \ + python3.13-devel \ clang \ cargo \ && dnf clean all \ @@ -41,6 +41,11 @@ RUN usermod --add-subuids 100000-165535 --add-subgids 100000-165535 $USERNAME \ USER $USERNAME WORKDIR $HOME_DIR +ENV UV_PYTHON=python3.13 +ENV UV_COMPILE_BYTECODE=1 +ENV UV_NO_SYNC=1 +ENV UV_CACHE_DIR=${APP_DIR}/.cache + COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx ${BIN_DIR}/ RUN set -x \ diff --git a/README.md b/README.md index 431222bb..83983dee 100644 --- a/README.md +++ b/README.md @@ -242,15 +242,8 @@ approvers: - myakove - rnetser reviewers: - any: # will be added to all pull requests - - myakove - - rnetser - files: # will be added to pull requests if files in the list are changed - Dockerfile: - - myakove - folders: # will be added to pull requests if folders in the list are changed - webhook_server_container/libs: # path is relative to the repository root - - myakove + - myakove + - rnetser ``` ### Supported user actions via adding comment diff --git a/pyproject.toml b/pyproject.toml index b1745247..6605be22 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,13 @@ +[tool.coverage.run] +omit = ["tests/*"] + +[tool.coverage.report] +fail_under = 0 +skip_empty = true + +[tool.coverage.html] +directory = ".tests_coverage" + [tool.ruff] preview = true line-length = 120 @@ -25,7 +35,7 @@ dev-dependencies = ["ipdb>=0.13.13", "ipython>=8.12.3"] [project] name = "github-webhook-server" version = "2.0.0" -requires-python = ">=3.9" +requires-python = ">=3.10" description = "A webhook server to manage Github reposotories and pull requests." readme = "README.md" license = "Apache-2.0" @@ -41,6 +51,7 @@ dependencies = [ "jira>=3.8.0", "pygithub>=2.4.0", "pyhelper-utils>=0.0.42", + "pytest-cov>=6.0.0", "pytest-mock>=3.14.0", "pytest>=8.3.3", "python-simple-logger>=1.0.40", diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..fcf52b9a --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +addopts = + --cov-config=pyproject.toml --cov-report=html --cov-report=term --cov=github_webhook_server diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 6515ce72..00000000 --- a/tox.ini +++ /dev/null @@ -1,15 +0,0 @@ -[tox] -envlist = unused-code, pytest -skipsdist = True - -[testenv:unused-code] -deps = - python-utility-scripts -commands = - pyutils-unusedcode --exclude-function-prefixes 'process_webhook' - -[testenv:pytest] -deps = - uv -commands = - uv run pytest webhook_server_container/tests diff --git a/tox.toml b/tox.toml new file mode 100644 index 00000000..7cabf92a --- /dev/null +++ b/tox.toml @@ -0,0 +1,17 @@ +skipsdist = true + +envlist = ["unused-code", "unittests"] + +[env.unused-code] +deps = ["python-utility-scripts"] +commands = [ + [ + "pyutils-unusedcode", + "--exclude-function-prefixes", + "'process_webhook'", + ], +] + +[env.unittests] +deps = ["uv"] +commands = [["uv", "run", "pytest", "-s", "webhook_server_container/tests"]] diff --git a/uv.lock b/uv.lock index 945b76ca..a9479950 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -requires-python = ">=3.9" +requires-python = ">=3.10" resolution-markers = [ "python_full_version < '3.11'", "python_full_version >= '3.11'", @@ -153,18 +153,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 }, { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, - { url = "https://files.pythonhosted.org/packages/b9/ea/8bb50596b8ffbc49ddd7a1ad305035daa770202a6b782fc164647c2673ad/cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16", size = 182220 }, - { url = "https://files.pythonhosted.org/packages/ae/11/e77c8cd24f58285a82c23af484cf5b124a376b32644e445960d1a4654c3a/cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36", size = 178605 }, - { url = "https://files.pythonhosted.org/packages/ed/65/25a8dc32c53bf5b7b6c2686b42ae2ad58743f7ff644844af7cdb29b49361/cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", size = 424910 }, - { url = "https://files.pythonhosted.org/packages/42/7a/9d086fab7c66bd7c4d0f27c57a1b6b068ced810afc498cc8c49e0088661c/cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", size = 447200 }, - { url = "https://files.pythonhosted.org/packages/da/63/1785ced118ce92a993b0ec9e0d0ac8dc3e5dbfbcaa81135be56c69cabbb6/cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", size = 454565 }, - { url = "https://files.pythonhosted.org/packages/74/06/90b8a44abf3556599cdec107f7290277ae8901a58f75e6fe8f970cd72418/cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", size = 435635 }, - { url = "https://files.pythonhosted.org/packages/bd/62/a1f468e5708a70b1d86ead5bab5520861d9c7eacce4a885ded9faa7729c3/cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", size = 445218 }, - { url = "https://files.pythonhosted.org/packages/5b/95/b34462f3ccb09c2594aa782d90a90b045de4ff1f70148ee79c69d37a0a5a/cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", size = 460486 }, - { url = "https://files.pythonhosted.org/packages/fc/fc/a1e4bebd8d680febd29cf6c8a40067182b64f00c7d105f8f26b5bc54317b/cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", size = 437911 }, - { url = "https://files.pythonhosted.org/packages/e6/c3/21cab7a6154b6a5ea330ae80de386e7665254835b9e98ecc1340b3a7de9a/cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", size = 460632 }, - { url = "https://files.pythonhosted.org/packages/cb/b5/fd9f8b5a84010ca169ee49f4e4ad6f8c05f4e3545b72ee041dbbcb159882/cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7", size = 171820 }, - { url = "https://files.pythonhosted.org/packages/8c/52/b08750ce0bce45c143e1b5d7357ee8c55341b52bdef4b0f081af1eb248c2/cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", size = 181290 }, ] [[package]] @@ -233,21 +221,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d8/90/6af4cd042066a4adad58ae25648a12c09c879efa4849c705719ba1b23d8c/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482", size = 144970 }, { url = "https://files.pythonhosted.org/packages/cc/67/e5e7e0cbfefc4ca79025238b43cdf8a2037854195b37d6417f3d0895c4c2/charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", size = 94973 }, { url = "https://files.pythonhosted.org/packages/65/97/fc9bbc54ee13d33dc54a7fcf17b26368b18505500fc01e228c27b5222d80/charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", size = 102308 }, - { url = "https://files.pythonhosted.org/packages/54/2f/28659eee7f5d003e0f5a3b572765bf76d6e0fe6601ab1f1b1dd4cba7e4f1/charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa", size = 196326 }, - { url = "https://files.pythonhosted.org/packages/d1/18/92869d5c0057baa973a3ee2af71573be7b084b3c3d428fe6463ce71167f8/charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a", size = 125614 }, - { url = "https://files.pythonhosted.org/packages/d6/27/327904c5a54a7796bb9f36810ec4173d2df5d88b401d2b95ef53111d214e/charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0", size = 120450 }, - { url = "https://files.pythonhosted.org/packages/a4/23/65af317914a0308495133b2d654cf67b11bbd6ca16637c4e8a38f80a5a69/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a", size = 140135 }, - { url = "https://files.pythonhosted.org/packages/f2/41/6190102ad521a8aa888519bb014a74251ac4586cde9b38e790901684f9ab/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242", size = 150413 }, - { url = "https://files.pythonhosted.org/packages/7b/ab/f47b0159a69eab9bd915591106859f49670c75f9a19082505ff16f50efc0/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b", size = 142992 }, - { url = "https://files.pythonhosted.org/packages/28/89/60f51ad71f63aaaa7e51a2a2ad37919985a341a1d267070f212cdf6c2d22/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62", size = 144871 }, - { url = "https://files.pythonhosted.org/packages/0c/48/0050550275fea585a6e24460b42465020b53375017d8596c96be57bfabca/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0", size = 146756 }, - { url = "https://files.pythonhosted.org/packages/dc/b5/47f8ee91455946f745e6c9ddbb0f8f50314d2416dd922b213e7d5551ad09/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd", size = 141034 }, - { url = "https://files.pythonhosted.org/packages/84/79/5c731059ebab43e80bf61fa51666b9b18167974b82004f18c76378ed31a3/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be", size = 149434 }, - { url = "https://files.pythonhosted.org/packages/ca/f3/0719cd09fc4dc42066f239cb3c48ced17fc3316afca3e2a30a4756fe49ab/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d", size = 152443 }, - { url = "https://files.pythonhosted.org/packages/f7/0e/c6357297f1157c8e8227ff337e93fd0a90e498e3d6ab96b2782204ecae48/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3", size = 150294 }, - { url = "https://files.pythonhosted.org/packages/54/9a/acfa96dc4ea8c928040b15822b59d0863d6e1757fba8bd7de3dc4f761c13/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742", size = 145314 }, - { url = "https://files.pythonhosted.org/packages/73/1c/b10a63032eaebb8d7bcb8544f12f063f41f5f463778ac61da15d9985e8b6/charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2", size = 94724 }, - { url = "https://files.pythonhosted.org/packages/c5/77/3a78bf28bfaa0863f9cfef278dbeadf55efe064eafff8c7c424ae3c4c1bf/charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca", size = 102159 }, { url = "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", size = 49446 }, ] @@ -297,6 +270,70 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/06/00/a17a5657bf090b9dffdb310ac273c553a38f9252f60224da9fe62d9b60e9/Columnar-1.4.1-py3-none-any.whl", hash = "sha256:8efb692a7e6ca07dcc8f4ea889960421331a5dffa8e5af81f0a67ad8ea1fc798", size = 11845 }, ] +[[package]] +name = "coverage" +version = "7.6.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/75/aecfd0a3adbec6e45753976bc2a9fed62b42cea9a206d10fd29244a77953/coverage-7.6.8.tar.gz", hash = "sha256:8b2b8503edb06822c86d82fa64a4a5cb0760bb8f31f26e138ec743f422f37cfc", size = 801425 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/86/6ed22e101badc8eedf181f0c2f65500df5929c44c79991cf45b9bf741424/coverage-7.6.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b39e6011cd06822eb964d038d5dff5da5d98652b81f5ecd439277b32361a3a50", size = 206988 }, + { url = "https://files.pythonhosted.org/packages/3b/04/16853c58bacc02b3ff5405193dfc6c66632442d931b23dd7b9452dc55cf3/coverage-7.6.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:63c19702db10ad79151a059d2d6336fe0c470f2e18d0d4d1a57f7f9713875dcf", size = 207418 }, + { url = "https://files.pythonhosted.org/packages/f8/eb/8a91520d04215eb549d6a7d7d3a79cbb1d78b5dd0814f4b23bf97521d580/coverage-7.6.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3985b9be361d8fb6b2d1adc9924d01dec575a1d7453a14cccd73225cb79243ee", size = 235860 }, + { url = "https://files.pythonhosted.org/packages/00/10/bf1ede5b54ae1bbf39921a5dd4cc84aee79041ed301ec8955064785ddb90/coverage-7.6.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:644ec81edec0f4ad17d51c838a7d01e42811054543b76d4ba2c5d6af741ce2a6", size = 233766 }, + { url = "https://files.pythonhosted.org/packages/5c/ea/741d9233eb502906e0d18ccf4c15c4fb74ff0e85fd8ee967590194b889a1/coverage-7.6.8-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f188a2402f8359cf0c4b1fe89eea40dc13b52e7b4fd4812450da9fcd210181d", size = 234924 }, + { url = "https://files.pythonhosted.org/packages/18/43/b2cfd4413a5b64ab27c289228b0c45b4527d1b99381cc9d6a00bfd515da4/coverage-7.6.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e19122296822deafce89a0c5e8685704c067ae65d45e79718c92df7b3ec3d331", size = 234019 }, + { url = "https://files.pythonhosted.org/packages/8e/95/8b2fbb9d1a79277963b6095cd51a90fb7088cd3618faf75550038331f78b/coverage-7.6.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:13618bed0c38acc418896005732e565b317aa9e98d855a0e9f211a7ffc2d6638", size = 232481 }, + { url = "https://files.pythonhosted.org/packages/4d/d7/9e939508a39ef67605b715ca89c6522214aceb27c2db9152ae3ae1cf8626/coverage-7.6.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:193e3bffca48ad74b8c764fb4492dd875038a2f9925530cb094db92bb5e47bed", size = 233609 }, + { url = "https://files.pythonhosted.org/packages/ba/e2/1c5fb52eafcffeebaa9db084bff47e7c3cf4f97db752226c232cee4d530b/coverage-7.6.8-cp310-cp310-win32.whl", hash = "sha256:3988665ee376abce49613701336544041f2117de7b7fbfe91b93d8ff8b151c8e", size = 209669 }, + { url = "https://files.pythonhosted.org/packages/31/31/6a56469609a252549dd4b090815428d5521edd4642440d987573a450c069/coverage-7.6.8-cp310-cp310-win_amd64.whl", hash = "sha256:f56f49b2553d7dd85fd86e029515a221e5c1f8cb3d9c38b470bc38bde7b8445a", size = 210509 }, + { url = "https://files.pythonhosted.org/packages/ab/9f/e98211980f6e2f439e251737482aa77906c9b9c507824c71a2ce7eea0402/coverage-7.6.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:86cffe9c6dfcfe22e28027069725c7f57f4b868a3f86e81d1c62462764dc46d4", size = 207093 }, + { url = "https://files.pythonhosted.org/packages/fd/c7/8bab83fb9c20f7f8163c5a20dcb62d591b906a214a6dc6b07413074afc80/coverage-7.6.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d82ab6816c3277dc962cfcdc85b1efa0e5f50fb2c449432deaf2398a2928ab94", size = 207536 }, + { url = "https://files.pythonhosted.org/packages/1e/d6/00243df625f1b282bb25c83ce153ae2c06f8e7a796a8d833e7235337b4d9/coverage-7.6.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13690e923a3932e4fad4c0ebfb9cb5988e03d9dcb4c5150b5fcbf58fd8bddfc4", size = 239482 }, + { url = "https://files.pythonhosted.org/packages/1e/07/faf04b3eeb55ffc2a6f24b65dffe6e0359ec3b283e6efb5050ea0707446f/coverage-7.6.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4be32da0c3827ac9132bb488d331cb32e8d9638dd41a0557c5569d57cf22c9c1", size = 236886 }, + { url = "https://files.pythonhosted.org/packages/43/23/c79e497bf4d8fcacd316bebe1d559c765485b8ec23ac4e23025be6bfce09/coverage-7.6.8-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44e6c85bbdc809383b509d732b06419fb4544dca29ebe18480379633623baafb", size = 238749 }, + { url = "https://files.pythonhosted.org/packages/b5/e5/791bae13be3c6451e32ef7af1192e711c6a319f3c597e9b218d148fd0633/coverage-7.6.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:768939f7c4353c0fac2f7c37897e10b1414b571fd85dd9fc49e6a87e37a2e0d8", size = 237679 }, + { url = "https://files.pythonhosted.org/packages/05/c6/bbfdfb03aada601fb8993ced17468c8c8e0b4aafb3097026e680fabb7ce1/coverage-7.6.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e44961e36cb13c495806d4cac67640ac2866cb99044e210895b506c26ee63d3a", size = 236317 }, + { url = "https://files.pythonhosted.org/packages/67/f9/f8e5a4b2ce96d1b0e83ae6246369eb8437001dc80ec03bb51c87ff557cd8/coverage-7.6.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ea8bb1ab9558374c0ab591783808511d135a833c3ca64a18ec927f20c4030f0", size = 237084 }, + { url = "https://files.pythonhosted.org/packages/f0/70/b05328901e4debe76e033717e1452d00246c458c44e9dbd893e7619c2967/coverage-7.6.8-cp311-cp311-win32.whl", hash = "sha256:629a1ba2115dce8bf75a5cce9f2486ae483cb89c0145795603d6554bdc83e801", size = 209638 }, + { url = "https://files.pythonhosted.org/packages/70/55/1efa24f960a2fa9fbc44a9523d3f3c50ceb94dd1e8cd732168ab2dc41b07/coverage-7.6.8-cp311-cp311-win_amd64.whl", hash = "sha256:fb9fc32399dca861584d96eccd6c980b69bbcd7c228d06fb74fe53e007aa8ef9", size = 210506 }, + { url = "https://files.pythonhosted.org/packages/76/ce/3edf581c8fe429ed8ced6e6d9ac693c25975ef9093413276dab6ed68a80a/coverage-7.6.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e683e6ecc587643f8cde8f5da6768e9d165cd31edf39ee90ed7034f9ca0eefee", size = 207285 }, + { url = "https://files.pythonhosted.org/packages/09/9c/cf102ab046c9cf8895c3f7aadcde6f489a4b2ec326757e8c6e6581829b5e/coverage-7.6.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1defe91d41ce1bd44b40fabf071e6a01a5aa14de4a31b986aa9dfd1b3e3e414a", size = 207522 }, + { url = "https://files.pythonhosted.org/packages/39/06/42aa6dd13dbfca72e1fd8ffccadbc921b6e75db34545ebab4d955d1e7ad3/coverage-7.6.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7ad66e8e50225ebf4236368cc43c37f59d5e6728f15f6e258c8639fa0dd8e6d", size = 240543 }, + { url = "https://files.pythonhosted.org/packages/a0/20/2932971dc215adeca8eeff446266a7fef17a0c238e881ffedebe7bfa0669/coverage-7.6.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fe47da3e4fda5f1abb5709c156eca207eacf8007304ce3019eb001e7a7204cb", size = 237577 }, + { url = "https://files.pythonhosted.org/packages/ac/85/4323ece0cd5452c9522f4b6e5cc461e6c7149a4b1887c9e7a8b1f4e51146/coverage-7.6.8-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:202a2d645c5a46b84992f55b0a3affe4f0ba6b4c611abec32ee88358db4bb649", size = 239646 }, + { url = "https://files.pythonhosted.org/packages/77/52/b2537487d8f36241e518e84db6f79e26bc3343b14844366e35b090fae0d4/coverage-7.6.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4674f0daa1823c295845b6a740d98a840d7a1c11df00d1fd62614545c1583787", size = 239128 }, + { url = "https://files.pythonhosted.org/packages/7c/99/7f007762012186547d0ecc3d328da6b6f31a8c99f05dc1e13dcd929918cd/coverage-7.6.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:74610105ebd6f33d7c10f8907afed696e79c59e3043c5f20eaa3a46fddf33b4c", size = 237434 }, + { url = "https://files.pythonhosted.org/packages/97/53/e9b5cf0682a1cab9352adfac73caae0d77ae1d65abc88975d510f7816389/coverage-7.6.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37cda8712145917105e07aab96388ae76e787270ec04bcb9d5cc786d7cbb8443", size = 239095 }, + { url = "https://files.pythonhosted.org/packages/0c/50/054f0b464fbae0483217186478eefa2e7df3a79917ed7f1d430b6da2cf0d/coverage-7.6.8-cp312-cp312-win32.whl", hash = "sha256:9e89d5c8509fbd6c03d0dd1972925b22f50db0792ce06324ba069f10787429ad", size = 209895 }, + { url = "https://files.pythonhosted.org/packages/df/d0/09ba870360a27ecf09e177ca2ff59d4337fc7197b456f22ceff85cffcfa5/coverage-7.6.8-cp312-cp312-win_amd64.whl", hash = "sha256:379c111d3558272a2cae3d8e57e6b6e6f4fe652905692d54bad5ea0ca37c5ad4", size = 210684 }, + { url = "https://files.pythonhosted.org/packages/9a/84/6f0ccf94a098ac3d6d6f236bd3905eeac049a9e0efcd9a63d4feca37ac4b/coverage-7.6.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0b0c69f4f724c64dfbfe79f5dfb503b42fe6127b8d479b2677f2b227478db2eb", size = 207313 }, + { url = "https://files.pythonhosted.org/packages/db/2b/e3b3a3a12ebec738c545897ac9f314620470fcbc368cdac88cf14974ba20/coverage-7.6.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c15b32a7aca8038ed7644f854bf17b663bc38e1671b5d6f43f9a2b2bd0c46f63", size = 207574 }, + { url = "https://files.pythonhosted.org/packages/db/c0/5bf95d42b6a8d21dfce5025ce187f15db57d6460a59b67a95fe8728162f1/coverage-7.6.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63068a11171e4276f6ece913bde059e77c713b48c3a848814a6537f35afb8365", size = 240090 }, + { url = "https://files.pythonhosted.org/packages/57/b8/d6fd17d1a8e2b0e1a4e8b9cb1f0f261afd422570735899759c0584236916/coverage-7.6.8-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f4548c5ead23ad13fb7a2c8ea541357474ec13c2b736feb02e19a3085fac002", size = 237237 }, + { url = "https://files.pythonhosted.org/packages/d4/e4/a91e9bb46809c8b63e68fc5db5c4d567d3423b6691d049a4f950e38fbe9d/coverage-7.6.8-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b4b4299dd0d2c67caaaf286d58aef5e75b125b95615dda4542561a5a566a1e3", size = 239225 }, + { url = "https://files.pythonhosted.org/packages/31/9c/9b99b0591ec4555b7292d271e005f27b465388ce166056c435b288db6a69/coverage-7.6.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9ebfb2507751f7196995142f057d1324afdab56db1d9743aab7f50289abd022", size = 238888 }, + { url = "https://files.pythonhosted.org/packages/a6/85/285c2df9a04bc7c31f21fd9d4a24d19e040ec5e2ff06e572af1f6514c9e7/coverage-7.6.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c1b4474beee02ede1eef86c25ad4600a424fe36cff01a6103cb4533c6bf0169e", size = 236974 }, + { url = "https://files.pythonhosted.org/packages/cb/a1/95ec8522206f76cdca033bf8bb61fff56429fb414835fc4d34651dfd29fc/coverage-7.6.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d9fd2547e6decdbf985d579cf3fc78e4c1d662b9b0ff7cc7862baaab71c9cc5b", size = 238815 }, + { url = "https://files.pythonhosted.org/packages/8d/ac/687e9ba5e6d0979e9dab5c02e01c4f24ac58260ef82d88d3b433b3f84f1e/coverage-7.6.8-cp313-cp313-win32.whl", hash = "sha256:8aae5aea53cbfe024919715eca696b1a3201886ce83790537d1c3668459c7146", size = 209957 }, + { url = "https://files.pythonhosted.org/packages/2f/a3/b61cc8e3fcf075293fb0f3dee405748453c5ba28ac02ceb4a87f52bdb105/coverage-7.6.8-cp313-cp313-win_amd64.whl", hash = "sha256:ae270e79f7e169ccfe23284ff5ea2d52a6f401dc01b337efb54b3783e2ce3f28", size = 210711 }, + { url = "https://files.pythonhosted.org/packages/ee/4b/891c8b9acf1b62c85e4a71dac142ab9284e8347409b7355de02e3f38306f/coverage-7.6.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:de38add67a0af869b0d79c525d3e4588ac1ffa92f39116dbe0ed9753f26eba7d", size = 208053 }, + { url = "https://files.pythonhosted.org/packages/18/a9/9e330409b291cc002723d339346452800e78df1ce50774ca439ade1d374f/coverage-7.6.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b07c25d52b1c16ce5de088046cd2432b30f9ad5e224ff17c8f496d9cb7d1d451", size = 208329 }, + { url = "https://files.pythonhosted.org/packages/9c/0d/33635fd429f6589c6e1cdfc7bf581aefe4c1792fbff06383f9d37f59db60/coverage-7.6.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62a66ff235e4c2e37ed3b6104d8b478d767ff73838d1222132a7a026aa548764", size = 251052 }, + { url = "https://files.pythonhosted.org/packages/23/32/8a08da0e46f3830bbb9a5b40614241b2e700f27a9c2889f53122486443ed/coverage-7.6.8-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09b9f848b28081e7b975a3626e9081574a7b9196cde26604540582da60235fdf", size = 246765 }, + { url = "https://files.pythonhosted.org/packages/56/3f/3b86303d2c14350fdb1c6c4dbf9bc76000af2382f42ca1d4d99c6317666e/coverage-7.6.8-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:093896e530c38c8e9c996901858ac63f3d4171268db2c9c8b373a228f459bbc5", size = 249125 }, + { url = "https://files.pythonhosted.org/packages/36/cb/c4f081b9023f9fd8646dbc4ef77be0df090263e8f66f4ea47681e0dc2cff/coverage-7.6.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a7b8ac36fd688c8361cbc7bf1cb5866977ece6e0b17c34aa0df58bda4fa18a4", size = 248615 }, + { url = "https://files.pythonhosted.org/packages/32/ee/53bdbf67760928c44b57b2c28a8c0a4bf544f85a9ee129a63ba5c78fdee4/coverage-7.6.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:38c51297b35b3ed91670e1e4efb702b790002e3245a28c76e627478aa3c10d83", size = 246507 }, + { url = "https://files.pythonhosted.org/packages/57/49/5a57910bd0af6d8e802b4ca65292576d19b54b49f81577fd898505dee075/coverage-7.6.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2e4e0f60cb4bd7396108823548e82fdab72d4d8a65e58e2c19bbbc2f1e2bfa4b", size = 247785 }, + { url = "https://files.pythonhosted.org/packages/bd/37/e450c9f6b297c79bb9858407396ed3e084dcc22990dd110ab01d5ceb9770/coverage-7.6.8-cp313-cp313t-win32.whl", hash = "sha256:6535d996f6537ecb298b4e287a855f37deaf64ff007162ec0afb9ab8ba3b8b71", size = 210605 }, + { url = "https://files.pythonhosted.org/packages/44/79/7d0c7dd237c6905018e2936cd1055fe1d42e7eba2ebab3c00f4aad2a27d7/coverage-7.6.8-cp313-cp313t-win_amd64.whl", hash = "sha256:c79c0685f142ca53256722a384540832420dff4ab15fec1863d7e5bc8691bdcc", size = 211777 }, + { url = "https://files.pythonhosted.org/packages/32/df/0d2476121cd0bfb9ca2413efe02289c474b82c4b134863bef4b89ec7bcfa/coverage-7.6.8-pp39.pp310-none-any.whl", hash = "sha256:5c52a036535d12590c32c49209e79cabaad9f9ad8aa4cbd875b68c4d67a9cbce", size = 199230 }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + [[package]] name = "cryptography" version = "43.0.3" @@ -328,10 +365,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/93/90/116edd5f8ec23b2dc879f7a42443e073cdad22950d3c8ee834e3b8124543/cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3", size = 3679828 }, { url = "https://files.pythonhosted.org/packages/d8/32/1e1d78b316aa22c0ba6493cc271c1c309969e5aa5c22c830a1d7ce3471e6/cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83", size = 3908132 }, { url = "https://files.pythonhosted.org/packages/91/bb/cd2c13be3332e7af3cdf16154147952d39075b9f61ea5e6b5241bf4bf436/cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7", size = 2988811 }, - { url = "https://files.pythonhosted.org/packages/cc/fc/ff7c76afdc4f5933b5e99092528d4783d3d1b131960fc8b31eb38e076ca8/cryptography-43.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664", size = 3146844 }, - { url = "https://files.pythonhosted.org/packages/d7/29/a233efb3e98b13d9175dcb3c3146988ec990896c8fa07e8467cce27d5a80/cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08", size = 3681997 }, - { url = "https://files.pythonhosted.org/packages/c0/cf/c9eea7791b961f279fb6db86c3355cfad29a73141f46427af71852b23b95/cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa", size = 3905208 }, - { url = "https://files.pythonhosted.org/packages/21/ea/6c38ca546d5b6dab3874c2b8fc6b1739baac29bacdea31a8c6c0513b3cfa/cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff", size = 2989787 }, ] [[package]] @@ -409,6 +442,7 @@ dependencies = [ { name = "pygithub" }, { name = "pyhelper-utils" }, { name = "pytest" }, + { name = "pytest-cov" }, { name = "pytest-mock" }, { name = "python-simple-logger" }, { name = "pyyaml" }, @@ -437,6 +471,7 @@ requires-dist = [ { name = "pygithub", specifier = ">=2.4.0" }, { name = "pyhelper-utils", specifier = ">=0.0.42" }, { name = "pytest", specifier = ">=8.3.3" }, + { name = "pytest-cov", specifier = ">=6.0.0" }, { name = "pytest-mock", specifier = ">=3.14.0" }, { name = "python-simple-logger", specifier = ">=1.0.40" }, { name = "pyyaml", specifier = ">=6.0.2" }, @@ -510,7 +545,7 @@ wheels = [ [[package]] name = "ipython" -version = "8.18.1" +version = "8.29.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -518,16 +553,16 @@ dependencies = [ { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "jedi" }, { name = "matplotlib-inline" }, - { name = "pexpect", marker = "sys_platform != 'win32'" }, + { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, { name = "prompt-toolkit" }, { name = "pygments" }, { name = "stack-data" }, { name = "traitlets" }, - { name = "typing-extensions", marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/b9/3ba6c45a6df813c09a48bac313c22ff83efa26cbb55011218d925a46e2ad/ipython-8.18.1.tar.gz", hash = "sha256:ca6f079bb33457c66e233e4580ebfc4128855b4cf6370dddd73842a9563e8a27", size = 5486330 } +sdist = { url = "https://files.pythonhosted.org/packages/85/e0/a3f36dde97e12121106807d80485423ae4c5b27ce60d40d4ab0bab18a9db/ipython-8.29.0.tar.gz", hash = "sha256:40b60e15b22591450eef73e40a027cf77bd652e757523eebc5bd7c7c498290eb", size = 5497513 } wheels = [ - { url = "https://files.pythonhosted.org/packages/47/6b/d9fdcdef2eb6a23f391251fde8781c38d42acd82abe84d054cb74f7863b0/ipython-8.18.1-py3-none-any.whl", hash = "sha256:e8267419d72d81955ec1177f8a29aaa90ac80ad647499201119e2f05e99aa397", size = 808161 }, + { url = "https://files.pythonhosted.org/packages/c5/a5/c15ed187f1b3fac445bb42a2dedd8dec1eee1718b35129242049a13a962f/ipython-8.29.0-py3-none-any.whl", hash = "sha256:0188a1bd83267192123ccea7f4a8ed0a78910535dbaa3f37671dca76ebd429c8", size = 819911 }, ] [[package]] @@ -722,17 +757,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d6/b9/fb620dd47fc7cc9678af8f8bd8c772034ca4977237049287e99dda360b66/pillow-11.0.0-cp313-cp313t-win32.whl", hash = "sha256:607bbe123c74e272e381a8d1957083a9463401f7bd01287f50521ecb05a313f8", size = 2253197 }, { url = "https://files.pythonhosted.org/packages/df/86/25dde85c06c89d7fc5db17940f07aae0a56ac69aa9ccb5eb0f09798862a8/pillow-11.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c39ed17edea3bc69c743a8dd3e9853b7509625c2462532e62baa0732163a904", size = 2572169 }, { url = "https://files.pythonhosted.org/packages/51/85/9c33f2517add612e17f3381aee7c4072779130c634921a756c97bc29fb49/pillow-11.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:75acbbeb05b86bc53cbe7b7e6fe00fbcf82ad7c684b3ad82e3d711da9ba287d3", size = 2256828 }, - { url = "https://files.pythonhosted.org/packages/f3/8b/01849a820686bf309b7d79a935d57bcafbfd016f1d78fc3d37ed2ba00f96/pillow-11.0.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2e46773dc9f35a1dd28bd6981332fd7f27bec001a918a72a79b4133cf5291dba", size = 3154738 }, - { url = "https://files.pythonhosted.org/packages/35/e8/ff71a40ca8e24cfd6bb333cc4ca8cc24ebecb6942bb4ad1e5ec61f33d1b8/pillow-11.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2679d2258b7f1192b378e2893a8a0a0ca472234d4c2c0e6bdd3380e8dfa21b6a", size = 2979272 }, - { url = "https://files.pythonhosted.org/packages/09/4f/2280ad43f5639174a0227920a59664fb78c5096a0b3fd865fee5184d4526/pillow-11.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eda2616eb2313cbb3eebbe51f19362eb434b18e3bb599466a1ffa76a033fb916", size = 4179756 }, - { url = "https://files.pythonhosted.org/packages/14/b1/c8f428bae932a27ce9c87e7b21aba8ea3e820aa11413c5a795868c37e039/pillow-11.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ec184af98a121fb2da42642dea8a29ec80fc3efbaefb86d8fdd2606619045d", size = 4280488 }, - { url = "https://files.pythonhosted.org/packages/78/66/7c5e44ab2c0123710a5d4692a4ee5931ac438efd7730ac395e305902346e/pillow-11.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:8594f42df584e5b4bb9281799698403f7af489fba84c34d53d1c4bfb71b7c4e7", size = 4192772 }, - { url = "https://files.pythonhosted.org/packages/36/5d/a9a00f8251ce93144f0250c0f0aece31b83ff33ffc243cdf987a8d584818/pillow-11.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:c12b5ae868897c7338519c03049a806af85b9b8c237b7d675b8c5e089e4a618e", size = 4363533 }, - { url = "https://files.pythonhosted.org/packages/fd/21/d8182fc1f3233078eb744f9f2950992f537655174febb8b3f7bdc61847b1/pillow-11.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:70fbbdacd1d271b77b7721fe3cdd2d537bbbd75d29e6300c672ec6bb38d9672f", size = 4275415 }, - { url = "https://files.pythonhosted.org/packages/c9/ee/93e02e8c29210ba7383843405b8b39bd19a164770f14d8569096dd123781/pillow-11.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5178952973e588b3f1360868847334e9e3bf49d19e169bbbdfaf8398002419ae", size = 4407081 }, - { url = "https://files.pythonhosted.org/packages/6e/77/8cda03af2b5177a18d645ad4a7446cda6c1292d1a2fb6e772a06fa9fc86b/pillow-11.0.0-cp39-cp39-win32.whl", hash = "sha256:8c676b587da5673d3c75bd67dd2a8cdfeb282ca38a30f37950511766b26858c4", size = 2249213 }, - { url = "https://files.pythonhosted.org/packages/9f/e4/c90bf7889489f3a14803bd00d3645945dd476020ab67579985af8233ab30/pillow-11.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:94f3e1780abb45062287b4614a5bc0874519c86a777d4a7ad34978e86428b8dd", size = 2566862 }, - { url = "https://files.pythonhosted.org/packages/27/a6/77d2ed085055237581d6276ac1e85f562f1b1848614647d8427e49d83c03/pillow-11.0.0-cp39-cp39-win_arm64.whl", hash = "sha256:290f2cc809f9da7d6d622550bbf4c1e57518212da51b6a30fe8e0a270a5b78bd", size = 2254605 }, { url = "https://files.pythonhosted.org/packages/36/57/42a4dd825eab762ba9e690d696d894ba366e06791936056e26e099398cda/pillow-11.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1187739620f2b365de756ce086fdb3604573337cc28a0d3ac4a01ab6b2d2a6d2", size = 3119239 }, { url = "https://files.pythonhosted.org/packages/98/f7/25f9f9e368226a1d6cf3507081a1a7944eddd3ca7821023377043f5a83c8/pillow-11.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fbbcb7b57dc9c794843e3d1258c0fbf0f48656d46ffe9e09b63bbd6e8cd5d0a2", size = 2950803 }, { url = "https://files.pythonhosted.org/packages/59/01/98ead48a6c2e31e6185d4c16c978a67fe3ccb5da5c2ff2ba8475379bb693/pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d203af30149ae339ad1b4f710d9844ed8796e97fda23ffbc4cc472968a47d0b", size = 3281098 }, @@ -740,10 +764,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/75/689b4ec0483c42bfc7d1aacd32ade7a226db4f4fac57c6fdcdf90c0731e3/pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:73853108f56df97baf2bb8b522f3578221e56f646ba345a372c78326710d3830", size = 3310533 }, { url = "https://files.pythonhosted.org/packages/3d/30/38bd6149cf53da1db4bad304c543ade775d225961c4310f30425995cb9ec/pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e58876c91f97b0952eb766123bfef372792ab3f4e3e1f1a2267834c2ab131734", size = 3414886 }, { url = "https://files.pythonhosted.org/packages/ec/3d/c32a51d848401bd94cabb8767a39621496491ee7cd5199856b77da9b18ad/pillow-11.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:224aaa38177597bb179f3ec87eeefcce8e4f85e608025e9cfac60de237ba6316", size = 2567508 }, - { url = "https://files.pythonhosted.org/packages/67/21/fbb4222399f72d6e9c828818ff4ef8391c1e8e71623368295c8dbc789bd1/pillow-11.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5bd2d3bdb846d757055910f0a59792d33b555800813c3b39ada1829c372ccb06", size = 2950706 }, - { url = "https://files.pythonhosted.org/packages/a2/b6/6aeb6e018b705ea4076db50aac078c9db8715a901f4c65698edc31375d0f/pillow-11.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:375b8dd15a1f5d2feafff536d47e22f69625c1aa92f12b339ec0b2ca40263273", size = 3323524 }, - { url = "https://files.pythonhosted.org/packages/48/26/36cc90e9932c5fe7c8876c32d6091ef5a09e8137e8e0633045bd35085fdd/pillow-11.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:daffdf51ee5db69a82dd127eabecce20729e21f7a3680cf7cbb23f0829189790", size = 3414787 }, - { url = "https://files.pythonhosted.org/packages/44/5c/089154029fcca7729ae142ac820057f74ca4b0b59617734276c31281af15/pillow-11.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7326a1787e3c7b0429659e0a944725e1b03eeaa10edd945a86dead1913383944", size = 2567664 }, ] [[package]] @@ -872,19 +892,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/d8/c080592d80edd3441ab7f88f865f51dae94a157fc64283c680e9f32cf6da/pydantic_core-2.27.1-cp313-none-win32.whl", hash = "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05", size = 1833713 }, { url = "https://files.pythonhosted.org/packages/83/84/5ab82a9ee2538ac95a66e51f6838d6aba6e0a03a42aa185ad2fe404a4e8f/pydantic_core-2.27.1-cp313-none-win_amd64.whl", hash = "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337", size = 1987897 }, { url = "https://files.pythonhosted.org/packages/df/c3/b15fb833926d91d982fde29c0624c9f225da743c7af801dace0d4e187e71/pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5", size = 1882983 }, - { url = "https://files.pythonhosted.org/packages/bc/6a/d741ce0c7da75ce9b394636a406aace00ad992ae417935ef2ad2e67fb970/pydantic_core-2.27.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:e9386266798d64eeb19dd3677051f5705bf873e98e15897ddb7d76f477131967", size = 1898376 }, - { url = "https://files.pythonhosted.org/packages/bd/68/6ba18e30f10c7051bc55f1dffeadbee51454b381c91846104892a6d3b9cd/pydantic_core-2.27.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4228b5b646caa73f119b1ae756216b59cc6e2267201c27d3912b592c5e323b60", size = 1777246 }, - { url = "https://files.pythonhosted.org/packages/36/b8/6f1b7c5f068c00dfe179b8762bc1d32c75c0e9f62c9372174b1b64a74aa8/pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3dfe500de26c52abe0477dde16192ac39c98f05bf2d80e76102d394bd13854", size = 1832148 }, - { url = "https://files.pythonhosted.org/packages/d9/83/83ff64d599847f080a93df119e856e3bd93063cced04b9a27eb66d863831/pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aee66be87825cdf72ac64cb03ad4c15ffef4143dbf5c113f64a5ff4f81477bf9", size = 1856371 }, - { url = "https://files.pythonhosted.org/packages/72/e9/974e6c73f59627c446833ecc306cadd199edab40abcfa093372a5a5c0156/pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b748c44bb9f53031c8cbc99a8a061bc181c1000c60a30f55393b6e9c45cc5bd", size = 2038686 }, - { url = "https://files.pythonhosted.org/packages/5e/bb/5e912d02dcf29aebb2da35e5a1a26088c39ffc0b1ea81242ee9db6f1f730/pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ca038c7f6a0afd0b2448941b6ef9d5e1949e999f9e5517692eb6da58e9d44be", size = 2785725 }, - { url = "https://files.pythonhosted.org/packages/85/d7/936846087424c882d89c853711687230cd60179a67c79c34c99b64f92625/pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e0bd57539da59a3e4671b90a502da9a28c72322a4f17866ba3ac63a82c4498e", size = 2135177 }, - { url = "https://files.pythonhosted.org/packages/82/72/5a386e5ce8d3e933c3f283e61357474181c39383f38afffc15a6152fa1c5/pydantic_core-2.27.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac6c2c45c847bbf8f91930d88716a0fb924b51e0c6dad329b793d670ec5db792", size = 1989877 }, - { url = "https://files.pythonhosted.org/packages/ce/5c/b1c417a5fd67ce132d78d16a6ba7629dc7f188dbd4f7c30ef58111ee5147/pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b94d4ba43739bbe8b0ce4262bcc3b7b9f31459ad120fb595627eaeb7f9b9ca01", size = 1996006 }, - { url = "https://files.pythonhosted.org/packages/dd/04/4e18f2c42b29929882f30e4c09a3a039555158995a4ac730a73585198a66/pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:00e6424f4b26fe82d44577b4c842d7df97c20be6439e8e685d0d715feceb9fb9", size = 2091441 }, - { url = "https://files.pythonhosted.org/packages/06/84/5a332345b7efb5ab361f916eaf7316ef010e72417e8c7dd3d34462ee9840/pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:38de0a70160dd97540335b7ad3a74571b24f1dc3ed33f815f0880682e6880131", size = 2144471 }, - { url = "https://files.pythonhosted.org/packages/54/58/23caa58c35d36627156789c0fb562264c12cfdb451c75eb275535188a96f/pydantic_core-2.27.1-cp39-none-win32.whl", hash = "sha256:7ccebf51efc61634f6c2344da73e366c75e735960b5654b63d7e6f69a5885fa3", size = 1816563 }, - { url = "https://files.pythonhosted.org/packages/f7/9c/e83f08adc8e222b43c7f11d98b27eba08f21bcb259bcbf74743ce903c49c/pydantic_core-2.27.1-cp39-none-win_amd64.whl", hash = "sha256:a57847b090d7892f123726202b7daa20df6694cbd583b67a592e856bff603d6c", size = 1983137 }, { url = "https://files.pythonhosted.org/packages/7c/60/e5eb2d462595ba1f622edbe7b1d19531e510c05c405f0b87c80c1e89d5b1/pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3fa80ac2bd5856580e242dbc202db873c60a01b20309c8319b5c5986fbe53ce6", size = 1894016 }, { url = "https://files.pythonhosted.org/packages/61/20/da7059855225038c1c4326a840908cc7ca72c7198cb6addb8b92ec81c1d6/pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d950caa237bb1954f1b8c9227b5065ba6875ac9771bb8ec790d956a699b78676", size = 1771648 }, { url = "https://files.pythonhosted.org/packages/8f/fc/5485cf0b0bb38da31d1d292160a4d123b5977841ddc1122c671a30b76cfd/pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e4216e64d203e39c62df627aa882f02a2438d18a5f21d7f721621f7a5d3611d", size = 1826929 }, @@ -894,15 +901,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7a/d2/8ce2b074d6835f3c88d85f6d8a399790043e9fdb3d0e43455e72d19df8cc/pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e173486019cc283dc9778315fa29a363579372fe67045e971e89b6365cc035ed", size = 2079616 }, { url = "https://files.pythonhosted.org/packages/65/71/af01033d4e58484c3db1e5d13e751ba5e3d6b87cc3368533df4c50932c8b/pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:af52d26579b308921b73b956153066481f064875140ccd1dfd4e77db89dbb12f", size = 2133265 }, { url = "https://files.pythonhosted.org/packages/33/72/f881b5e18fbb67cf2fb4ab253660de3c6899dbb2dba409d0b757e3559e3d/pydantic_core-2.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:981fb88516bd1ae8b0cbbd2034678a39dedc98752f264ac9bc5839d3923fa04c", size = 2001864 }, - { url = "https://files.pythonhosted.org/packages/85/3e/f6f75ba36678fee11dd07a7729e9ed172ecf31e3f50a5d636e9605eee2af/pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5fde892e6c697ce3e30c61b239330fc5d569a71fefd4eb6512fc6caec9dd9e2f", size = 1894250 }, - { url = "https://files.pythonhosted.org/packages/d3/2d/a40578918e2eb5b4ee0d206a4fb6c4040c2bf14e28d29fba9bd7e7659d16/pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:816f5aa087094099fff7edabb5e01cc370eb21aa1a1d44fe2d2aefdfb5599b31", size = 1772035 }, - { url = "https://files.pythonhosted.org/packages/7f/ee/0377e9f4ca5a47e8885f670a65c0a647ddf9ce98d50bf7547cf8e1ee5771/pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c10c309e18e443ddb108f0ef64e8729363adbfd92d6d57beec680f6261556f3", size = 1827025 }, - { url = "https://files.pythonhosted.org/packages/fe/0b/a24d9ef762d05bebdfafd6d5d176b990728fa9ec8ea7b6040d6fb5f3caaa/pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98476c98b02c8e9b2eec76ac4156fd006628b1b2d0ef27e548ffa978393fd154", size = 1980927 }, - { url = "https://files.pythonhosted.org/packages/00/bd/deadc1722eb7dfdf787a3bbcd32eabbdcc36931fd48671a850e1b9f2cd77/pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3027001c28434e7ca5a6e1e527487051136aa81803ac812be51802150d880dd", size = 1980918 }, - { url = "https://files.pythonhosted.org/packages/f0/05/5d09d0b0e92053d538927308ea1d35cb25ab543d9c3e2eb2d7653bc73690/pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:7699b1df36a48169cdebda7ab5a2bac265204003f153b4bd17276153d997670a", size = 1989990 }, - { url = "https://files.pythonhosted.org/packages/5b/7e/f7191346d1c3ac66049f618ee331359f8552a8b68a2daf916003c30b6dc8/pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1c39b07d90be6b48968ddc8c19e7585052088fd7ec8d568bb31ff64c70ae3c97", size = 2079871 }, - { url = "https://files.pythonhosted.org/packages/f3/65/2caf4f7ad65413a137d43cb9578c54d1abd3224be786ad840263c1bf9e0f/pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:46ccfe3032b3915586e469d4972973f893c0a2bb65669194a5bdea9bacc088c2", size = 2133569 }, - { url = "https://files.pythonhosted.org/packages/fd/ab/718d9a1c41bb8d3e0e04d15b68b8afc135f8fcf552705b62f226225065c7/pydantic_core-2.27.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:62ba45e21cf6571d7f716d903b5b7b6d2617e2d5d67c0923dc47b9d41369f840", size = 2002035 }, ] [[package]] @@ -1004,6 +1002,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2", size = 342341 }, ] +[[package]] +name = "pytest-cov" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/45/9b538de8cef30e17c7b45ef42f538a94889ed6a16f2387a6c89e73220651/pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0", size = 66945 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 }, +] + [[package]] name = "pytest-mock" version = "3.14.0" @@ -1079,15 +1090,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, - { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777 }, - { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318 }, - { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891 }, - { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614 }, - { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360 }, - { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006 }, - { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577 }, - { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593 }, - { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312 }, ] [[package]] @@ -1225,7 +1227,6 @@ version = "0.41.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, - { name = "typing-extensions", marker = "python_full_version < '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1a/4c/9b5764bd22eec91c4039ef4c55334e9187085da2d8a2df7bd570869aae18/starlette-0.41.3.tar.gz", hash = "sha256:0e4ab3d16522a255be6b28260b938eae2482f98ce5cc934cb08dce8dc3ba5835", size = 2574159 } wheels = [ @@ -1380,15 +1381,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f7/16/9f3ac99fe1f6caaa789d67b4e3c562898b532c250769f5255fa8b8b93983/wrapt-1.17.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:914f66f3b6fc7b915d46c1cc424bc2441841083de01b90f9e81109c9759e43ab", size = 106347 }, { url = "https://files.pythonhosted.org/packages/64/85/c77a331b2c06af49a687f8b926fc2d111047a51e6f0b0a4baa01ff3a673a/wrapt-1.17.0-cp313-cp313t-win32.whl", hash = "sha256:a4192b45dff127c7d69b3bdfb4d3e47b64179a0b9900b6351859f3001397dabf", size = 37971 }, { url = "https://files.pythonhosted.org/packages/05/9b/b2469f8be9efed24283fd7b9eeb8e913e9bc0715cf919ea8645e428ab7af/wrapt-1.17.0-cp313-cp313t-win_amd64.whl", hash = "sha256:4f643df3d4419ea3f856c5c3f40fec1d65ea2e89ec812c83f7767c8730f9827a", size = 40755 }, - { url = "https://files.pythonhosted.org/packages/89/03/518069f0708573c02cbba3a3e452be3642dc7d984d0a03a47e0850e2fb05/wrapt-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d751300b94e35b6016d4b1e7d0e7bbc3b5e1751e2405ef908316c2a9024008a1", size = 38765 }, - { url = "https://files.pythonhosted.org/packages/60/01/12dd81522f8c1c953e98e2cbf356ff44fbb06ef0f7523cd622ac06ad7f03/wrapt-1.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7264cbb4a18dc4acfd73b63e4bcfec9c9802614572025bdd44d0721983fc1d9c", size = 83012 }, - { url = "https://files.pythonhosted.org/packages/c4/2d/9853fe0009271b2841f839eb0e707c6b4307d169375f26c58812ecf4fd71/wrapt-1.17.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33539c6f5b96cf0b1105a0ff4cf5db9332e773bb521cc804a90e58dc49b10578", size = 74759 }, - { url = "https://files.pythonhosted.org/packages/94/5c/03c911442b01b50e364572581430e12f82c3f5ea74d302907c1449d7ba36/wrapt-1.17.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c30970bdee1cad6a8da2044febd824ef6dc4cc0b19e39af3085c763fdec7de33", size = 82540 }, - { url = "https://files.pythonhosted.org/packages/52/e0/ef637448514295a6b3a01cf1dff417e081e7b8cf1eb712839962459af1f6/wrapt-1.17.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:bc7f729a72b16ee21795a943f85c6244971724819819a41ddbaeb691b2dd85ad", size = 81461 }, - { url = "https://files.pythonhosted.org/packages/7f/44/8b7d417c3aae3a35ccfe361375ee3e452901c91062e5462e1aeef98255e8/wrapt-1.17.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:6ff02a91c4fc9b6a94e1c9c20f62ea06a7e375f42fe57587f004d1078ac86ca9", size = 74380 }, - { url = "https://files.pythonhosted.org/packages/af/a9/e65406a9c3a99162055efcb6bf5e0261924381228c0a7608066805da03df/wrapt-1.17.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2dfb7cff84e72e7bf975b06b4989477873dcf160b2fd89959c629535df53d4e0", size = 81057 }, - { url = "https://files.pythonhosted.org/packages/55/0c/111d42fb658a2f9ed7024cd5e57c08521d61646a256a3946db7d500c1551/wrapt-1.17.0-cp39-cp39-win32.whl", hash = "sha256:2399408ac33ffd5b200480ee858baa58d77dd30e0dd0cab6a8a9547135f30a88", size = 36415 }, - { url = "https://files.pythonhosted.org/packages/00/33/e7b14a7c06cedfaae064f34e95c95350de7cc10187ac173743e30a956b30/wrapt-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:4f763a29ee6a20c529496a20a7bcb16a73de27f5da6a843249c7047daf135977", size = 38742 }, { url = "https://files.pythonhosted.org/packages/4b/d9/a8ba5e9507a9af1917285d118388c5eb7a81834873f45df213a6fe923774/wrapt-1.17.0-py3-none-any.whl", hash = "sha256:d2c63b93548eda58abf5188e505ffed0229bf675f7c3090f8e36ad55b8cbc371", size = 23592 }, ] diff --git a/webhook_server_container/OWNERS b/webhook_server_container/OWNERS deleted file mode 100644 index 0456f047..00000000 --- a/webhook_server_container/OWNERS +++ /dev/null @@ -1,6 +0,0 @@ -approvers: - - myakove - - dbasunag -reviewers: - - rnetser - - dbasunag diff --git a/webhook_server_container/app.py b/webhook_server_container/app.py index 26999192..024f8002 100644 --- a/webhook_server_container/app.py +++ b/webhook_server_container/app.py @@ -44,7 +44,8 @@ async def process_webhook(request: Request) -> Dict[str, Any]: api.process() return {"status": requests.codes.ok, "message": "process success", "log_prefix": delivery_headers} - except Exception as _: + except Exception as exp: + logger.error(f"Error: {exp}") exc_type, exc_obj, exc_tb = sys.exc_info() # noqa: F841 msg = f"Error: {exc_type}" diff --git a/webhook_server_container/libs/github_api.py b/webhook_server_container/libs/github_api.py index 2a8ced00..4ccb21d4 100644 --- a/webhook_server_container/libs/github_api.py +++ b/webhook_server_container/libs/github_api.py @@ -11,6 +11,7 @@ import time from concurrent.futures import Future, ThreadPoolExecutor, as_completed from typing import Any, Callable, Dict, Generator, List, Optional, Set, Tuple +from github.CheckRun import CheckRun from stringcolor import cs from github.Branch import Branch @@ -192,7 +193,6 @@ def process(self) -> None: return event_log: str = f"Event type: {self.github_event}. event ID: {self.x_github_delivery}" - self.owners_content = self.get_owners_content() try: self.pull_request = self._get_pull_request() @@ -201,7 +201,11 @@ def process(self) -> None: self.last_commit = self._get_last_commit() self.parent_committer = self.pull_request.user.login self.last_committer = getattr(self.last_commit.committer, "login", self.parent_committer) + self.changed_files = self.list_changed_commit_files() self.pull_request_branch = self.pull_request.base.ref + self.approvers_and_reviewers = self.get_approvers_and_reviewers() + self.all_approvers = self.get_all_approvers() + self.all_reviewers = self.get_all_reviewers() if self.jira_enabled_repository: self.set_jira_in_pull_request() @@ -302,20 +306,22 @@ def _get_random_color(_colors: List[str], _json: Dict[str, str]) -> str: def prepare_log_prefix(self, pull_request: Optional[PullRequest] = None) -> str: _repository_color = self._get_reposiroty_color_for_log_prefix() - _id = self.x_github_delivery.split("-", 1)[-1] return ( - f"{_repository_color}[{self.github_event}][{_id}][PR {pull_request.number}]:" + f"{_repository_color}[{self.github_event}][{self.x_github_delivery}][PR {pull_request.number}]:" if pull_request - else f"{_repository_color}[{self.github_event}][{_id}]:" + else f"{_repository_color}[{self.github_event}][{self.x_github_delivery}]:" ) def process_pull_request_check_run_webhook_data(self) -> None: _check_run: Dict[str, Any] = self.hook_data["check_run"] - if _check_run.get("action", "") != "completed": - self.logger.debug(f"{self.log_prefix} check run action is not completed, skipping") + check_run_name: str = _check_run["name"] + + if self.hook_data.get("action", "") != "completed": + self.logger.debug( + f"{self.log_prefix} check run {check_run_name} action is {self.hook_data.get('action', 'N/A')} and not completed, skipping" + ) return - check_run_name: str = _check_run["name"] check_run_status: str = _check_run["status"] check_run_conclusion: str = _check_run["conclusion"] check_run_head_sha: str = _check_run["head_sha"] @@ -531,7 +537,7 @@ def _error(_out: str, _err: str) -> None: self.logger.error(f"{self.log_prefix} {err} - {_err}, {_out}") self.repository.create_issue( title=_err, - assignee=self.approvers[0] if self.approvers else "", + assignee=self.root_approvers[0] if self.root_approvers else "", body=f""" stdout: `{_out}` stderr: `{_err}` @@ -575,9 +581,22 @@ def _error(_out: str, _err: str) -> None: """ self.send_slack_message(message=message, webhook_url=self.slack_webhook_url) - def get_owners_content(self) -> Dict[str, Any]: + def get_owners_content(self, folder_path: str = "") -> Dict[str, Any]: + if folder_path: + # Normalize path and check for directory traversal + norm_path = os.path.normpath(folder_path) + if ( + norm_path.startswith("/") + or norm_path.startswith("\\") + or ".." in norm_path + or not all(part.isalnum() or part in "-_" for part in norm_path.split(os.path.sep)) + ): + self.logger.error(f"{self.log_prefix} Invalid folder path: {folder_path}") + return {} + try: - owners_content: list[ContentFile] | ContentFile = self.repository.get_contents("OWNERS") + owners_path = f"{folder_path}/OWNERS" if folder_path else "OWNERS" + owners_content: list[ContentFile] | ContentFile = self.repository.get_contents(owners_path) if isinstance(owners_content, list): self.logger.debug(f"{self.log_prefix} Found more than one OWNERS file, using the first one") owners_content = owners_content[0] @@ -591,51 +610,24 @@ def get_owners_content(self) -> Dict[str, Any]: return {} @property - def reviewers(self) -> List[str]: - bc_reviewers: List[str] = self.owners_content.get("reviewers", []) - if isinstance(bc_reviewers, dict): - _reviewers: List[str] = self.owners_content.get("reviewers", {}).get("any", []) - else: - _reviewers = bc_reviewers - - self.logger.debug(f"{self.log_prefix} Reviewers: {_reviewers}") + def root_reviewers(self) -> List[str]: + _reviewers = self.approvers_and_reviewers.get(".", {}).get("reviewers", []) + self.logger.debug(f"{self.log_prefix} ROOT Reviewers: {_reviewers}") return _reviewers @property - def files_reviewers(self) -> Dict[str, str]: - _reviewers = self.owners_content.get("reviewers", {}) - if isinstance(_reviewers, dict): - return _reviewers.get("files", {}) - - return {} - - @property - def folders_reviewers(self) -> Dict[str, str]: - _reviewers = self.owners_content.get("reviewers", {}) - if isinstance(_reviewers, dict): - return _reviewers.get("folders", {}) - - return {} - - @property - def approvers(self) -> List[str]: - return self.owners_content.get("approvers", []) + def root_approvers(self) -> List[str]: + _approvers = self.approvers_and_reviewers.get(".", {}).get("approvers", []) + self.logger.debug(f"{self.log_prefix} ROOT Approvers: {_approvers}") + return _approvers def list_changed_commit_files(self) -> list[str]: return [fd["filename"] for fd in self.last_commit.raw_data["files"]] def assign_reviewers(self) -> None: self.logger.info(f"{self.log_prefix} Assign reviewers") - changed_files = self.list_changed_commit_files() - reviewers_to_add = self.reviewers - for _file, _file_reviewers in self.files_reviewers.items(): - if _file in changed_files: - reviewers_to_add.extend(_file_reviewers) - for _folder, _folder_reviewers in self.folders_reviewers.items(): - if any(cf for cf in changed_files if _folder in str(Path(cf).parent)): - reviewers_to_add.extend(_folder_reviewers) - - _to_add: List[str] = list(set(reviewers_to_add)) + + _to_add: List[str] = list(set(self.all_reviewers)) self.logger.debug(f"{self.log_prefix} Reviewers to add: {', '.join(_to_add)}") for reviewer in _to_add: @@ -856,7 +848,7 @@ def delete_remote_tag_for_merged_or_closed_pr(self) -> None: self.logger.error(f"{self.log_prefix} Failed to delete tag: {repository_full_tag}. OUT:{out}. ERR:{err}") def process_comment_webhook_data(self) -> None: - if comment_action := self.hook_data["action"] in ("action", "deleted"): + if comment_action := self.hook_data["action"] in ("edited", "deleted"): self.logger.debug(f"{self.log_prefix} Not processing comment. action is {comment_action}") return @@ -903,10 +895,12 @@ def process_pull_request_webhook_data(self) -> None: pull_request_opened_futures.append(executor.submit(self.process_opened_or_synchronize_pull_request)) if self.jira_track_pr: pull_request_opened_futures.append(executor.submit(self.create_jira_when_open_pull_reques)) + pull_request_opened_futures.append(executor.submit(self.set_pull_request_automerge)) - for _ in as_completed(pull_request_opened_futures): - pass + for result in as_completed(pull_request_opened_futures): + if _exp := result.exception(): + self.logger.error(f"{self.log_prefix} {_exp}") if hook_action == "synchronize": pull_request_synchronize_futures: List[Future] = [] @@ -919,6 +913,10 @@ def process_pull_request_webhook_data(self) -> None: if self.jira_track_pr: pull_request_synchronize_futures.append(executor.submit(self.update_jira_when_pull_request_sync)) + for result in as_completed(pull_request_synchronize_futures): + if _exp := result.exception(): + self.logger.error(f"{self.log_prefix} {_exp}") + if hook_action == "closed": self.close_issue_for_merged_or_closed_pr(hook_action=hook_action) self.delete_remote_tag_for_merged_or_closed_pr() @@ -960,7 +958,7 @@ def process_pull_request_webhook_data(self) -> None: _reviewer = labeled.split(CHANGED_REQUESTED_BY_LABEL_PREFIX)[-1] _approved_output: Dict[str, Any] = {"title": "Approved", "summary": "", "text": ""} - if _reviewer in self.approvers: + if _reviewer in self.all_approvers: _check_for_merge = True _approved_output["text"] += f"Approved by {_reviewer}.\n" @@ -1016,7 +1014,7 @@ def manage_reviewed_by_label(self, review_state: str, action: str, reviewed_user label_prefix: str = "" label_to_remove: str = "" - if reviewed_user in self.approvers: + if reviewed_user in self.all_approvers: approved_lgtm_label = APPROVED_BY_LABEL_PREFIX else: approved_lgtm_label = LGTM_BY_LABEL_PREFIX @@ -1175,7 +1173,7 @@ def user_commands(self, command: str, reviewed_user: str, issue_comment_id: int) elif _command == HOLD_LABEL_STR: self.create_comment_reaction(issue_comment_id=issue_comment_id, reaction=REACTIONS.ok) - if reviewed_user not in self.approvers: + if reviewed_user not in self.all_approvers: self.pull_request.create_issue_comment( f"{reviewed_user} is not part of the approver, only approvers can mark pull request as hold" ) @@ -1323,85 +1321,38 @@ def check_if_can_be_merged(self) -> None: failure_output = "" try: - self.all_required_status_checks = self.get_all_required_status_checks() self.logger.info(f"{self.log_prefix} Check if {CAN_BE_MERGED_STR}.") self.set_merge_check_queued() last_commit_check_runs = list(self.last_commit.get_check_runs()) - self.logger.debug(f"{self.log_prefix} Check if any required check runs in progress.") - check_runs_in_progress = [ - check_run.name - for check_run in last_commit_check_runs - if check_run.status == IN_PROGRESS_STR - and check_run.name != CAN_BE_MERGED_STR - and check_run.name in self.all_required_status_checks - ] - if check_runs_in_progress: - self.logger.debug( - f"{self.log_prefix} Some required check runs in progress {check_runs_in_progress}, " - f"skipping check if {CAN_BE_MERGED_STR}." - ) - failure_output += f"Some required check runs in progress {', '.join(check_runs_in_progress)}\n" - _labels = self.pull_request_labels_names() - is_hold = HOLD_LABEL_STR in _labels - is_wip = WIP_STR in _labels - if is_hold or is_wip: - if is_hold: - failure_output += "Hold label exists.\n" - - if is_wip: - failure_output += "WIP label exists.\n" + self.logger.debug(f"{self.log_prefix} check if can be merged. PR labels are: {_labels}") if not self.pull_request.mergeable: failure_output += "PR is not mergeable: {self.pull_request.mergeable_state}\n" - failed_check_runs = [] - for check_run in last_commit_check_runs: - if ( - check_run.name == CAN_BE_MERGED_STR - or check_run.conclusion == SUCCESS_STR - or check_run.conclusion == QUEUED_STR - or check_run.name not in self.all_required_status_checks - ): - continue - - failed_check_runs.append(check_run.name) - - if failed_check_runs: - exclude_in_progress = [ - failed_check_run - for failed_check_run in failed_check_runs - if failed_check_run not in check_runs_in_progress - ] - failure_output += f"Some check runs failed: {', '.join(exclude_in_progress)}\n" - - self.logger.debug(f"{self.log_prefix} check if can be merged. PR labels are: {_labels}") - - for _label in _labels: - if CHANGED_REQUESTED_BY_LABEL_PREFIX.lower() in _label.lower(): - change_request_user = _label.split("-")[-1] - if change_request_user in self.approvers: - failure_output += "PR has changed requests from approvers\n" + required_check_in_progress_failure_output, check_runs_in_progress = self._required_check_in_progress( + last_commit_check_runs=last_commit_check_runs + ) + if required_check_in_progress_failure_output: + failure_output += required_check_in_progress_failure_output - missing_required_labels = [] - for _req_label in self.can_be_merged_required_labels: - if _req_label not in _labels: - missing_required_labels.append(_req_label) + labels_failure_output = self._wip_or_hold_lables_exists(labels=_labels) + if labels_failure_output: + failure_output += labels_failure_output - if missing_required_labels: - failure_output += f"Missing required labels: {', '.join(missing_required_labels)}\n" + required_check_failed_failure_output = self._required_check_failed( + last_commit_check_runs=last_commit_check_runs, check_runs_in_progress=check_runs_in_progress + ) + if required_check_failed_failure_output: + failure_output += required_check_failed_failure_output - pr_approved = False - for _label in _labels: - if APPROVED_BY_LABEL_PREFIX.lower() in _label.lower(): - approved_user = _label.split("-")[-1] - if approved_user in self.approvers and self.parent_committer != approved_user: - pr_approved = True - break + lables_failue_output = self._check_lables_for_can_be_merged(labels=_labels) + if lables_failue_output: + failure_output += lables_failue_output - if not pr_approved: - missing_approvers = [approver for approver in self.approvers if approver != self.parent_committer] - failure_output += f"Missing lgtm/approved from approvers: {', '.join(missing_approvers)}\n" + pr_approvered_failure_output = self._check_if_pr_approved(labels=_labels) + if pr_approvered_failure_output: + failure_output += pr_approvered_failure_output if not failure_output: self._add_label(label=CAN_BE_MERGED_STR) @@ -1800,7 +1751,7 @@ def set_wip_label_based_on_title(self) -> None: def set_jira_in_pull_request(self) -> None: if self.jira_enabled_repository: - reviewers_and_approvers = self.reviewers + self.approvers + reviewers_and_approvers = self.root_reviewers + self.root_approvers if self.parent_committer in reviewers_and_approvers: self.jira_assignee = self.jira_user_mapping.get(self.parent_committer) if not self.jira_assignee: @@ -2014,9 +1965,12 @@ def add_pull_request_owner_as_assingee(self) -> None: try: self.logger.info(f"{self.log_prefix} Adding PR owner as assignee") self.pull_request.add_to_assignees() - except Exception: - if self.approvers: - self.pull_request.add_to_assignees(self.approvers[0]) + except Exception as exp: + self.logger.debug(f"{self.log_prefix} Exception while adding PR owner as assignee: {exp}") + + if self.root_approvers: + self.logger.debug(f"{self.log_prefix} Falling back to first approver as assignee") + self.pull_request.add_to_assignees(self.root_approvers[0]) def set_pull_request_automerge(self) -> None: if self.parent_committer in self.auto_verified_and_merged_users: @@ -2071,3 +2025,210 @@ def run_podman_command(self, command: str, pipe: bool = False) -> Tuple[bool, st return run_command(command=command, log_prefix=self.log_prefix, pipe=pipe) return rc, out, err + + def get_approvers_and_reviewers(self) -> dict[str, dict[str, list[str]]]: + # Dictionary mapping OWNERS file paths to their approvers and reviewers + _owners: dict[str, dict[str, list[str]]] = {} + + max_owners_files = 1000 # Configurable limit + owners_count = 0 + + tree = self.repository.get_git_tree(self.pull_request_branch, recursive=True) + for element in tree.tree: + if element.type == "blob" and element.path.endswith("OWNERS"): + owners_count += 1 + if owners_count > max_owners_files: + self.logger.error(f"{self.log_prefix} Too many OWNERS files (>{max_owners_files})") + break + + content_path = element.path + _path = self.repository.get_contents(content_path) + if isinstance(_path, list): + _path = _path[0] + + try: + content = yaml.safe_load(_path.decoded_content) + if self._validate_owners_content(content, content_path): + # Use Path for consistent path handling + parent_path = str(Path(content_path).parent) + if not parent_path: + parent_path = "." + _owners[parent_path] = content + + except yaml.YAMLError as exp: + self.logger.error(f"{self.log_prefix} Invalid OWNERS file {content_path}: {exp}") + continue + + self.logger.debug(f"{self.log_prefix} Owners file mapping: {_owners}") + return _owners + + def get_all_approvers(self) -> list[str]: + _approvers: list[str] = [] + for list_of_approvers in self.owners_data_for_changed_files()["approvers"]: + for _approver in list_of_approvers: + _approvers.append(_approver) + + reviewers = list(set(self.root_approvers + _approvers)) + reviewers.sort() + return reviewers + + def get_all_reviewers(self) -> list[str]: + _reviewers: list[str] = [] + for list_of_reviewers in self.owners_data_for_changed_files()["reviewers"]: + for _approver in list_of_reviewers: + _reviewers.append(_approver) + + approvers = list(set(self.root_reviewers + _reviewers)) + approvers.sort() + return approvers + + def owners_data_for_changed_files(self) -> dict[str, list[list[str]]]: + data: dict[str, list[list[str]]] = {"approvers": [], "reviewers": []} + + changed_folders = {Path(cf).parent for cf in self.changed_files} + + for changed_folder_path in changed_folders: + for owners_dir, owners_data in self.approvers_and_reviewers.items(): + _owners_dir = Path(owners_dir) + + if _owners_dir == changed_folder_path or _owners_dir in changed_folder_path.parents: + _reviewers = owners_data.get("reviewers", []) + self.logger.debug(f"{self.log_prefix} Found reviewers for {owners_dir}: {_reviewers}") + data["reviewers"].append(_reviewers) + + _approvers = owners_data.get("approvers", []) + self.logger.debug(f"{self.log_prefix} Found approvers for {owners_dir}: {_approvers}") + data["approvers"].append(_approvers) + + data["reviewers"].sort() + data["approvers"].sort() + return data + + def _validate_owners_content(self, content: Any, path: str) -> bool: + """Validate OWNERS file content structure.""" + try: + if not isinstance(content, dict): + raise ValueError("OWNERS file must contain a dictionary") + + for key in ["approvers", "reviewers"]: + if key in content: + if not isinstance(content[key], list): + raise ValueError(f"{key} must be a list") + + if not all(isinstance(_elm, str) for _elm in content[key]): + raise ValueError(f"All {key} must be strings") + + return True + + except ValueError as e: + self.logger.error(f"{self.log_prefix} Invalid OWNERS file {path}: {e}") + return False + + def _required_check_in_progress(self, last_commit_check_runs: list[CheckRun]) -> tuple[str, list[str]]: + self.all_required_status_checks = self.get_all_required_status_checks() + last_commit_check_runs = list(self.last_commit.get_check_runs()) + self.logger.debug(f"{self.log_prefix} Check if any required check runs in progress.") + check_runs_in_progress = [ + check_run.name + for check_run in last_commit_check_runs + if check_run.status == IN_PROGRESS_STR + and check_run.name != CAN_BE_MERGED_STR + and check_run.name in self.all_required_status_checks + ] + if check_runs_in_progress: + self.logger.debug( + f"{self.log_prefix} Some required check runs in progress {check_runs_in_progress}, " + f"skipping check if {CAN_BE_MERGED_STR}." + ) + return f"Some required check runs in progress {', '.join(check_runs_in_progress)}\n", check_runs_in_progress + return "", [] + + def _required_check_failed(self, last_commit_check_runs: list[CheckRun], check_runs_in_progress: list[str]) -> str: + failed_check_runs = [] + for check_run in last_commit_check_runs: + if ( + check_run.name == CAN_BE_MERGED_STR + or check_run.conclusion == SUCCESS_STR + or check_run.conclusion == QUEUED_STR + or check_run.name not in self.all_required_status_checks + ): + continue + + failed_check_runs.append(check_run.name) + + if failed_check_runs: + exclude_in_progress = [ + failed_check_run + for failed_check_run in failed_check_runs + if failed_check_run not in check_runs_in_progress + ] + return f"Some check runs failed: {', '.join(exclude_in_progress)}\n" + + return "" + + def _wip_or_hold_lables_exists(self, labels: list[str]) -> str: + failure_output = "" + is_hold = HOLD_LABEL_STR in labels + is_wip = WIP_STR in labels + + if is_hold or is_wip: + if is_hold: + failure_output += "Hold label exists.\n" + + if is_wip: + failure_output += "WIP label exists.\n" + + return failure_output + + def _check_lables_for_can_be_merged(self, labels: list[str]) -> str: + failure_output = "" + + for _label in labels: + if CHANGED_REQUESTED_BY_LABEL_PREFIX.lower() in _label.lower(): + change_request_user = _label.split("-")[-1] + if change_request_user in self.all_approvers: + failure_output += "PR has changed requests from approvers\n" + + missing_required_labels = [] + for _req_label in self.can_be_merged_required_labels: + if _req_label not in labels: + missing_required_labels.append(_req_label) + + if missing_required_labels: + failure_output += f"Missing required labels: {', '.join(missing_required_labels)}\n" + + return failure_output + + def _check_if_pr_approved(self, labels: list[str]) -> str: + _pr_approvers: list[str] = [] + all_needed_approvers = [] + for approvers_list in self.owners_data_for_changed_files()["approvers"]: + if approvers_list not in all_needed_approvers: + all_needed_approvers.append(approvers_list) + + # all_needed_approvers is [['approver1', 'approver2'], ['approver3', 'approver4']] + # To mark PR as approved we need at least one lgtm/approved from each nested list inside all_needed_approvers + approved_by = [] + for _label in labels: + if APPROVED_BY_LABEL_PREFIX.lower() in _label.lower(): + approved_user = _label.split("-")[-1] + if self.parent_committer == approved_user: + continue + + approved_by.append(approved_user) + + missing_approvers = self.all_approvers.copy() + + for owners_data in self.approvers_and_reviewers.values(): + _approvers = owners_data.get("approvers", []) + for approver in _approvers: + if approver in approved_by: + _pr_approvers.append(approver) + # Once we found approver in approved_by list, we remove all approvers from missing_approvers list for this owners file + {missing_approvers.remove(_approver) for _approver in _approvers if _approver in missing_approvers} # type: ignore + break + + if missing_approvers: + return f"Missing lgtm/approved from approvers: {', '.join(missing_approvers)}\n" + + return "" diff --git a/webhook_server_container/tests/test_github_api.py b/webhook_server_container/tests/test_github_api.py index 61288948..392f9c4d 100644 --- a/webhook_server_container/tests/test_github_api.py +++ b/webhook_server_container/tests/test_github_api.py @@ -3,19 +3,72 @@ from simple_logger.logger import logging from stringcolor.ops import os +import yaml from webhook_server_container.libs.github_api import ProcessGithubWehook -from webhook_server_container.utils.constants import SIZE_LABEL_PREFIX +from webhook_server_container.utils.constants import APPROVED_BY_LABEL_PREFIX, SIZE_LABEL_PREFIX + + +class Label: + def __init__(self, name: str): + self.name = name + + +class Tree: + def __init__(self, path: str): + self.type = "blob" + self.path = path + + @property + def tree(self): + trees = [] + for _path in ["OWNERS", "test1/OWNERS", "code/file.py", "README.md"]: + trees.append(Tree(_path)) + return trees + + +class ContentFile: + def __init__(self, content: str): + self.content = content + + @property + def decoded_content(self): + return self.content class Repository: def __init__(self): self.name = "test-repo" + def get_git_tree(self, sha: str, recursive: bool): + return Tree("") + + def get_contents(self, path: str): + owners_data = yaml.dump({"approvers": ["approver1", "approver2"], "reviewers": ["reviewer1", "reviewer2"]}) + + test1_owners_data = yaml.dump({ + "approvers": ["approver3", "approver4"], + "reviewers": ["reviewer3", "reviewer4"], + }) + + if path == "OWNERS": + return ContentFile(owners_data) + elif path == "test1/OWNERS": + return ContentFile(test1_owners_data) + class PullRequest: - def __init__(self, additions: int, deletions: int): + def __init__(self, additions: int, deletions: int, labels: list[str] | None = None): self.additions = additions self.deletions = deletions + self.labels = labels or [] + + @property + def lables(self) -> list[Label]: + _lables = [] + for label in self.labels: + _lables.append(Label(label)) + + return _lables @pytest.fixture(scope="function") @@ -28,9 +81,26 @@ def process_github_webhook(mocker): mocker.patch(f"{base_import_path}.get_api_with_highest_rate_limit", return_value=("API", "TOKEN")) mocker.patch(f"{base_import_path}.get_github_repo_api", return_value=Repository()) - return ProcessGithubWehook( + process_github_webhook = ProcessGithubWehook( {"repository": {"name": Repository().name}}, Headers({"X-GitHub-Event": "test-event"}), logging.getLogger() ) + process_github_webhook.pull_request_branch = "main" + process_github_webhook.changed_files = ["OWNERS", "test1/OWNERS", "code/file.py", "README.md"] + return process_github_webhook + + +@pytest.fixture(scope="function") +def approvers_and_reviewers(process_github_webhook): + process_github_webhook.approvers_and_reviewers = { + ".": {"approvers": ["approver1", "approver2"], "reviewers": ["reviewer1", "reviewer2"]}, + "test1": {"approvers": ["approver3", "approver4"], "reviewers": ["reviewer3", "reviewer4"]}, + } + + +@pytest.fixture(scope="function") +def all_approvers_reviewers(process_github_webhook): + process_github_webhook.all_approvers = ["approver1", "approver2", "approver3", "approver4"] + process_github_webhook.all_reviewers = ["reviewer1", "reviewer2", "reviewer3", "reviewer4"] @pytest.mark.parametrize( @@ -50,3 +120,73 @@ def test_get_size_thresholds(process_github_webhook, additions, deletions, expec result = process_github_webhook.get_size() assert result == f"{SIZE_LABEL_PREFIX}{expected_label}" + + +def test_get_approvers_and_reviewers(process_github_webhook, approvers_and_reviewers): + process_github_webhook.repository = Repository() + read_owners_result = process_github_webhook.get_approvers_and_reviewers() + assert read_owners_result == process_github_webhook.approvers_and_reviewers + + +def test_owners_data_for_changed_files(process_github_webhook, approvers_and_reviewers): + owners_data_chaged_files_result = process_github_webhook.owners_data_for_changed_files() + owners_data_chaged_files_expected = { + "approvers": [ + ["approver1", "approver2"], + ["approver1", "approver2"], + ["approver3", "approver4"], + ["approver1", "approver2"], + ], + "reviewers": [ + ["reviewer1", "reviewer2"], + ["reviewer1", "reviewer2"], + ["reviewer3", "reviewer4"], + ["reviewer1", "reviewer2"], + ], + } + owners_data_chaged_files_expected["approvers"].sort() + owners_data_chaged_files_expected["reviewers"].sort() + assert owners_data_chaged_files_result == owners_data_chaged_files_expected + + +def test_all_approvers_reviewers(process_github_webhook, approvers_and_reviewers, all_approvers_reviewers): + all_approvers = process_github_webhook.get_all_approvers() + assert all_approvers == process_github_webhook.all_approvers + + all_reviewers = process_github_webhook.get_all_reviewers() + assert all_reviewers == process_github_webhook.all_reviewers + + +def test_check_if_pr_approved(process_github_webhook, approvers_and_reviewers, all_approvers_reviewers): + pr_approved_all_result = process_github_webhook._check_if_pr_approved( + labels=[ + f"{APPROVED_BY_LABEL_PREFIX}approver1", + f"{APPROVED_BY_LABEL_PREFIX}approver2", + f"{APPROVED_BY_LABEL_PREFIX}approver3", + f"{APPROVED_BY_LABEL_PREFIX}approver4", + ] + ) + assert pr_approved_all_result == "" + + pr_approved_minimum_result = process_github_webhook._check_if_pr_approved( + labels=[ + f"{APPROVED_BY_LABEL_PREFIX}approver1", + f"{APPROVED_BY_LABEL_PREFIX}approver3", + ] + ) + assert pr_approved_minimum_result == "" + + pr_not_approved_result = process_github_webhook._check_if_pr_approved( + labels=[ + f"{APPROVED_BY_LABEL_PREFIX}approver1", + ] + ) + assert pr_not_approved_result == "Missing lgtm/approved from approvers: approver3, approver4\n" + + pr_partial_approved_result = process_github_webhook._check_if_pr_approved( + labels=[ + f"{APPROVED_BY_LABEL_PREFIX}approver1", + f"{APPROVED_BY_LABEL_PREFIX}approver2", + ] + ) + assert pr_partial_approved_result == "Missing lgtm/approved from approvers: approver3, approver4\n"