diff --git a/poetry.lock b/poetry.lock index 362db16..28926b8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "annotated-types" @@ -9,8 +9,6 @@ python-versions = ">=3.8" files = [ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, - {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, - {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, ] [[package]] @@ -22,8 +20,6 @@ python-versions = ">=3.8" files = [ {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"}, {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"}, - {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"}, - {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"}, ] [package.dependencies] @@ -128,15 +124,12 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "certifi" version = "2024.7.4" -version = "2024.7.4" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, - {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, - {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, ] [[package]] @@ -550,36 +543,32 @@ tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipyth [[package]] name = "filelock" version = "3.15.4" -version = "3.15.4" description = "A platform independent file lock." optional = false python-versions = ">=3.8" files = [ {file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"}, {file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"}, - {file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"}, - {file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"}, ] [package.extras] docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"] typing = ["typing-extensions (>=4.8)"] [[package]] name = "githubkit" -version = "0.11.7" +version = "0.11.13" description = "GitHub SDK for Python" optional = false python-versions = "<4.0,>=3.8" files = [ - {file = "githubkit-0.11.7-py3-none-any.whl", hash = "sha256:1dc3f1b8a5b63509962e14eaa66af561bfefa31c8bc9d106d875475f4ccd029f"}, - {file = "githubkit-0.11.7.tar.gz", hash = "sha256:f7ff916d1def03b62d52bfe3097dd531cef0fa8e75dcc563c8f6a457e43d88e3"}, + {file = "githubkit-0.11.13-py3-none-any.whl", hash = "sha256:a1d52ed31cbcebaba7c6b2d6ba54b316ca00cbca6c2429ec88715a263a7c2fdc"}, + {file = "githubkit-0.11.13.tar.gz", hash = "sha256:0d17ab5bffe98ba521fabd570c39be5d0301b9d3bf8508a89a6cd0bbc080d542"}, ] [package.dependencies] -hishel = ">=0.0.21,<=0.0.30" +hishel = ">=0.0.21,<=0.0.33" httpx = ">=0.23.0,<1.0.0" pydantic = ">=1.9.1,<2.5.0 || >2.5.0,<2.5.1 || >2.5.1,<3.0.0" PyJWT = {version = ">=2.4.0,<3.0.0", extras = ["crypto"], optional = true, markers = "extra == \"jwt\" or extra == \"auth-app\" or extra == \"auth\" or extra == \"all\""} @@ -672,15 +661,12 @@ socks = ["socksio (==1.*)"] [[package]] name = "identify" version = "2.6.0" -version = "2.6.0" description = "File identification library for Python" optional = false python-versions = ">=3.8" files = [ {file = "identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0"}, {file = "identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf"}, - {file = "identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0"}, - {file = "identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf"}, ] [package.extras] @@ -732,15 +718,12 @@ typing-extensions = "*" [[package]] name = "jinja2" version = "3.1.4" -version = "3.1.4" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" files = [ {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, - {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, - {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, ] [package.dependencies] @@ -984,16 +967,12 @@ files = [ [[package]] name = "nodeenv" version = "1.9.1" -version = "1.9.1" description = "Node.js virtual environment builder" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, - {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, - {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, ] [[package]] @@ -1060,11 +1039,9 @@ typing-extensions = ">=4.0.0,<5.0.0" [[package]] name = "packaging" version = "24.1" -version = "24.1" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" -python-versions = ">=3.8" files = [ {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, @@ -1084,15 +1061,12 @@ files = [ [[package]] name = "platformdirs" version = "4.2.2" -version = "4.2.2" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" files = [ {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, - {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, - {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, ] [package.extras] @@ -1147,15 +1121,12 @@ files = [ [[package]] name = "pydantic" version = "2.8.2" -version = "2.8.2" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" files = [ {file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"}, {file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"}, - {file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"}, - {file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"}, ] [package.dependencies] @@ -1165,11 +1136,6 @@ typing-extensions = [ {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, {version = ">=4.6.1", markers = "python_version < \"3.13\""}, ] -pydantic-core = "2.20.1" -typing-extensions = [ - {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, - {version = ">=4.6.1", markers = "python_version < \"3.13\""}, -] [package.extras] email = ["email-validator (>=2.0.0)"] @@ -1177,7 +1143,6 @@ email = ["email-validator (>=2.0.0)"] [[package]] name = "pydantic-core" version = "2.20.1" -version = "2.20.1" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" @@ -1271,95 +1236,6 @@ files = [ {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a"}, {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7"}, {file = "pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4"}, - {file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"}, - {file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a"}, - {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a"}, - {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840"}, - {file = "pydantic_core-2.20.1-cp310-none-win32.whl", hash = "sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250"}, - {file = "pydantic_core-2.20.1-cp310-none-win_amd64.whl", hash = "sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c"}, - {file = "pydantic_core-2.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312"}, - {file = "pydantic_core-2.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b"}, - {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27"}, - {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b"}, - {file = "pydantic_core-2.20.1-cp311-none-win32.whl", hash = "sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a"}, - {file = "pydantic_core-2.20.1-cp311-none-win_amd64.whl", hash = "sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2"}, - {file = "pydantic_core-2.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231"}, - {file = "pydantic_core-2.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24"}, - {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1"}, - {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd"}, - {file = "pydantic_core-2.20.1-cp312-none-win32.whl", hash = "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688"}, - {file = "pydantic_core-2.20.1-cp312-none-win_amd64.whl", hash = "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d"}, - {file = "pydantic_core-2.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686"}, - {file = "pydantic_core-2.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83"}, - {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203"}, - {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0"}, - {file = "pydantic_core-2.20.1-cp313-none-win32.whl", hash = "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e"}, - {file = "pydantic_core-2.20.1-cp313-none-win_amd64.whl", hash = "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20"}, - {file = "pydantic_core-2.20.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:4745f4ac52cc6686390c40eaa01d48b18997cb130833154801a442323cc78f91"}, - {file = "pydantic_core-2.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a8ad4c766d3f33ba8fd692f9aa297c9058970530a32c728a2c4bfd2616d3358b"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41e81317dd6a0127cabce83c0c9c3fbecceae981c8391e6f1dec88a77c8a569a"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04024d270cf63f586ad41fff13fde4311c4fc13ea74676962c876d9577bcc78f"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eaad4ff2de1c3823fddf82f41121bdf453d922e9a238642b1dedb33c4e4f98ad"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26ab812fa0c845df815e506be30337e2df27e88399b985d0bb4e3ecfe72df31c"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c5ebac750d9d5f2706654c638c041635c385596caf68f81342011ddfa1e5598"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2aafc5a503855ea5885559eae883978c9b6d8c8993d67766ee73d82e841300dd"}, - {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4868f6bd7c9d98904b748a2653031fc9c2f85b6237009d475b1008bfaeb0a5aa"}, - {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa2f457b4af386254372dfa78a2eda2563680d982422641a85f271c859df1987"}, - {file = "pydantic_core-2.20.1-cp38-none-win32.whl", hash = "sha256:225b67a1f6d602de0ce7f6c1c3ae89a4aa25d3de9be857999e9124f15dab486a"}, - {file = "pydantic_core-2.20.1-cp38-none-win_amd64.whl", hash = "sha256:6b507132dcfc0dea440cce23ee2182c0ce7aba7054576efc65634f080dbe9434"}, - {file = "pydantic_core-2.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b03f7941783b4c4a26051846dea594628b38f6940a2fdc0df00b221aed39314c"}, - {file = "pydantic_core-2.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1eedfeb6089ed3fad42e81a67755846ad4dcc14d73698c120a82e4ccf0f1f9f6"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635fee4e041ab9c479e31edda27fcf966ea9614fff1317e280d99eb3e5ab6fe2"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:77bf3ac639c1ff567ae3b47f8d4cc3dc20f9966a2a6dd2311dcc055d3d04fb8a"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ed1b0132f24beeec5a78b67d9388656d03e6a7c837394f99257e2d55b461611"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6514f963b023aeee506678a1cf821fe31159b925c4b76fe2afa94cc70b3222b"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10d4204d8ca33146e761c79f83cc861df20e7ae9f6487ca290a97702daf56006"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d036c7187b9422ae5b262badb87a20a49eb6c5238b2004e96d4da1231badef1"}, - {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ebfef07dbe1d93efb94b4700f2d278494e9162565a54f124c404a5656d7ff09"}, - {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6b9d9bb600328a1ce523ab4f454859e9d439150abb0906c5a1983c146580ebab"}, - {file = "pydantic_core-2.20.1-cp39-none-win32.whl", hash = "sha256:784c1214cb6dd1e3b15dd8b91b9a53852aed16671cc3fbe4786f4f1db07089e2"}, - {file = "pydantic_core-2.20.1-cp39-none-win_amd64.whl", hash = "sha256:d2fe69c5434391727efa54b47a1e7986bb0186e72a41b203df8f5b0a19a4f669"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7"}, - {file = "pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4"}, ] [package.dependencies] @@ -1368,15 +1244,12 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pydantic-extra-types" version = "2.9.0" -version = "2.9.0" description = "Extra Pydantic types." optional = false python-versions = ">=3.8" files = [ {file = "pydantic_extra_types-2.9.0-py3-none-any.whl", hash = "sha256:f0bb975508572ba7bf3390b7337807588463b7248587e69f43b1ad7c797530d0"}, {file = "pydantic_extra_types-2.9.0.tar.gz", hash = "sha256:e061c01636188743bb69f368dcd391f327b8cfbfede2fe1cbb1211b06601ba3b"}, - {file = "pydantic_extra_types-2.9.0-py3-none-any.whl", hash = "sha256:f0bb975508572ba7bf3390b7337807588463b7248587e69f43b1ad7c797530d0"}, - {file = "pydantic_extra_types-2.9.0.tar.gz", hash = "sha256:e061c01636188743bb69f368dcd391f327b8cfbfede2fe1cbb1211b06601ba3b"}, ] [package.dependencies] @@ -1458,15 +1331,12 @@ dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments [[package]] name = "pytest-asyncio" version = "0.23.8" -version = "0.23.8" description = "Pytest support for asyncio" optional = false python-versions = ">=3.8" files = [ {file = "pytest_asyncio-0.23.8-py3-none-any.whl", hash = "sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2"}, {file = "pytest_asyncio-0.23.8.tar.gz", hash = "sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3"}, - {file = "pytest_asyncio-0.23.8-py3-none-any.whl", hash = "sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2"}, - {file = "pytest_asyncio-0.23.8.tar.gz", hash = "sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3"}, ] [package.dependencies] @@ -1633,16 +1503,12 @@ files = [ [[package]] name = "requests" version = "2.32.3" -version = "2.32.3" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" -python-versions = ">=3.8" files = [ {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, - {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, - {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, ] [package.dependencies] @@ -1734,15 +1600,12 @@ files = [ [[package]] name = "typing-extensions" version = "4.12.2" -version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, - {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, - {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] [[package]] @@ -1759,15 +1622,12 @@ files = [ [[package]] name = "urllib3" version = "2.2.2" -version = "2.2.2" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" files = [ {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, - {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, - {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, ] [package.extras] @@ -1779,15 +1639,12 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" version = "20.26.3" -version = "20.26.3" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ {file = "virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589"}, {file = "virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a"}, - {file = "virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589"}, - {file = "virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a"}, ] [package.dependencies] @@ -1919,4 +1776,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "9de4ef2194c3c40d0579c0b30f7788fb83698bf554f2c864518dfb579a4a53ba" +content-hash = "4f64b2a64fc9db7eb792d2bc2e167c66aedded52b1b2cd5e6513878d1c24c379" diff --git a/src/plugins/github/constants.py b/src/plugins/github/constants.py index ef6fd0a..28ae3f8 100644 --- a/src/plugins/github/constants.py +++ b/src/plugins/github/constants.py @@ -15,3 +15,5 @@ ISSUE_PATTERN = r"### {}\s+([^\s#].*?)(?=(?:\s+###|$))" ISSUE_FIELD_TEMPLATE = "### {}" ISSUE_FIELD_PATTERN = r"### {}\s+" + +SKIP_COMMENT = "/skip" diff --git a/src/plugins/github/models/__init__.py b/src/plugins/github/models/__init__.py index dd9c36f..99a033f 100644 --- a/src/plugins/github/models/__init__.py +++ b/src/plugins/github/models/__init__.py @@ -1,20 +1,4 @@ -from typing import Literal -from githubkit.rest import Issue, PullRequestSimple -from nonebot import logger -from pydantic import ( - BaseModel, - ConfigDict, - Field, - model_validator, -) -from nonebot.adapters.github import Bot - -from githubkit.exception import RequestFailed -from githubkit.utils import UNSET -from githubkit.typing import Missing - -from src.plugins.github.constants import NONEFLOW_MARKER -from src.plugins.github.utils import run_shell_command +from pydantic import BaseModel class RepoInfo(BaseModel): @@ -24,276 +8,8 @@ class RepoInfo(BaseModel): repo: str -class GitHandler(BaseModel): - """Git 操作""" - - def commit_and_push(self, message: str, branch_name: str, author: str): - run_shell_command(["git", "config", "--global", "user.name", author]) - user_email = f"{author}@users.noreply.github.com" - run_shell_command(["git", "config", "--global", "user.email", user_email]) - run_shell_command(["git", "add", "-A"]) - try: - run_shell_command(["git", "commit", "-m", message]) - except Exception: - # 如果提交失败,因为是 pre-commit hooks 格式化代码导致的,所以需要再次提交 - run_shell_command(["git", "add", "-A"]) - run_shell_command(["git", "commit", "-m", message]) - - try: - run_shell_command(["git", "fetch", "origin"]) - r = run_shell_command(["git", "diff", f"origin/{branch_name}", branch_name]) - if r.stdout: - raise Exception - else: - logger.info("检测到本地分支与远程分支一致,跳过推送") - except Exception: - logger.info("检测到本地分支与远程分支不一致,尝试强制推送") - run_shell_command(["git", "push", "origin", branch_name, "-f"]) - - def delete_origin_branch(self, branch_name: str): - """删除远程分支""" - run_shell_command(["git", "push", "origin", "--delete", branch_name]) - - -class GithubHandler(GitHandler): - """Bot 相关的 Github 操作""" - - model_config = ConfigDict(arbitrary_types_allowed=True) - - bot: Bot - repo_info: RepoInfo - - async def create_dispatch_event( - self, repo: RepoInfo | None, event_type: str, client_payload: dict - ): - if repo is None: - repo = self.repo_info - await self.bot.rest.repos.async_create_dispatch_event( - repo=repo.repo, - owner=repo.owner, - event_type=event_type, - client_payload=client_payload, # type: ignore - ) - - async def list_comments(self, issue_number: int): - return ( - await self.bot.rest.issues.async_list_comments( - **self.repo_info.model_dump(), issue_number=issue_number - ) - ).parsed_data - - async def comment_issue(self, comment: str, issue_number: int): - """发布评论 - 若之前发布过评论,则修改之前的评论 - """ - logger.info("开始发布评论") - - # 重复利用评论 - # 如果发现之前评论过,直接修改之前的评论 - comments = ( - await self.bot.rest.issues.async_list_comments( - **self.repo_info.model_dump(), issue_number=issue_number - ) - ).parsed_data - reusable_comment = next( - filter(lambda x: NONEFLOW_MARKER in (x.body if x.body else ""), comments), - None, - ) - - # comment = await render_comment(result, bool(reusable_comment)) - if reusable_comment: - logger.info(f"发现已有评论 {reusable_comment.id},正在修改") - if reusable_comment.body != comment: - await self.bot.rest.issues.async_update_comment( - **self.repo_info.model_dump(), - comment_id=reusable_comment.id, - body=comment, - ) - logger.info("评论修改完成") - else: - logger.info("评论内容无变化,跳过修改") - else: - await self.bot.rest.issues.async_create_comment( - **self.repo_info.model_dump(), - issue_number=issue_number, - body=comment, - ) - logger.info("评论创建完成") - - async def get_pull_requests_by_label(self, label: str) -> list[PullRequestSimple]: - """根据标签获取拉取请求""" - pulls = ( - await self.bot.rest.pulls.async_list( - **self.repo_info.model_dump(), state="open" - ) - ).parsed_data - return [ - pull for pull in pulls if label in [label.name for label in pull.labels] - ] - - async def pull_request_to_draft(self, branch_name: str): - """ - 将拉取请求转换为草稿 - """ - pulls = ( - await self.bot.rest.pulls.async_list( - **self.repo_info.model_dump(), - head=f"{self.repo_info.owner}:{branch_name}", - ) - ).parsed_data - if pulls and (pull := pulls[0]) and not pull.draft: - await self.bot.async_graphql( - query="""mutation convertPullRequestToDraft($pullRequestId: ID!) { - convertPullRequestToDraft(input: {pullRequestId: $pullRequestId}) { - clientMutationId - } - }""", - variables={"pullRequestId": pull.node_id}, - ) - logger.info("删除没通过检查,已将之前的拉取请求转换为草稿") - else: - logger.info("没通过检查,暂不创建拉取请求") - - async def merge_pull_request( - self, - pull_number: int, - merge_method: Missing[Literal["merge", "squash", "rebase"]] = UNSET, - ): - """合并拉取请求""" - await self.bot.rest.pulls.async_merge( - **self.repo_info.model_dump(), - pull_number=pull_number, - merge_method=merge_method, - ) - logger.info(f"拉取请求 #{pull_number} 已合并") - - async def create_pull_request( - self, - base_branch: str, - title: str, - branch_name: str, - label: str | list[str], - issue_number: int, - ): - """创建拉取请求""" - body = f"resolve #{issue_number}" - - try: - # 创建拉取请求 - resp = await self.bot.rest.pulls.async_create( - **self.repo_info.model_dump(), - title=title, - body=body, - base=base_branch, - head=branch_name, - ) - pull = resp.parsed_data - - # 自动给拉取请求添加标签 - await self.bot.rest.issues.async_add_labels( - **self.repo_info.model_dump(), - issue_number=pull.number, - labels=[label] if isinstance(label, str) else label, - ) - logger.info("拉取请求创建完毕") - except RequestFailed: - logger.info("该分支的拉取请求已创建,请前往查看") - - pull = ( - await self.bot.rest.pulls.async_list( - **self.repo_info.model_dump(), - head=f"{self.repo_info.owner}:{branch_name}", - ) - ).parsed_data[0] - if pull.title != title: - await self.bot.rest.pulls.async_update( - **self.repo_info.model_dump(), pull_number=pull.number, title=title - ) - logger.info(f"拉取请求标题已修改为 {title}") - if pull.draft: - await self.bot.async_graphql( - query="""mutation markPullRequestReadyForReview($pullRequestId: ID!) { - markPullRequestReadyForReview(input: {pullRequestId: $pullRequestId}) { - clientMutationId - } - }""", - variables={"pullRequestId": pull.node_id}, - ) - logger.info("拉取请求已标记为可评审") - - -class IssueHandler(GithubHandler): - """Issue 的相关 Github/Git 操作""" - - model_config = ConfigDict(arbitrary_types_allowed=True) - - issue: Issue - issue_number: int - author_id: int = 0 - author: str = "" - - @model_validator(mode="before") - @classmethod - def issuehandler_validator(cls, data): - if data.get("author_id") is None and data.get("issue"): - issue = data["issue"] - data["author_id"] = issue.user.id if issue.user else 0 - if data.get("author") is None and data.get("issue"): - issue = data["issue"] - data["author"] = issue.user.login if issue.user else "" - if data.get("issue_number") is None and data.get("issue"): - issue = data["issue"] - data["issue_number"] = issue.number - return data - - async def update_issue_title( - self, - title: str, - ): - if self.issue and self.issue.title != title: - await self.bot.rest.issues.async_update( - **self.repo_info.model_dump(), - issue_number=self.issue_number, - title=title, - ) - logger.info(f"标题已修改为 {title}") - - async def update_issue_content(self, body: str): - """编辑议题内容""" - await self.bot.rest.issues.async_update( - **self.repo_info.model_dump(), - issue_number=self.issue_number, - body=body, - ) - logger.info("议题内容已修改") - - async def close_issue(self, reason: str): - """关闭议题""" - if self.issue and self.issue.state == "open": - logger.info(f"正在关闭议题 #{self.issue_number}") - await self.bot.rest.issues.async_update( - **self.repo_info.model_dump(), - issue_number=self.issue_number, - state="closed", - state_reason=reason, - ) - - async def comment_issue(self, comment: str): - return await super().comment_issue(comment, self.issue_number) - - async def create_pull_request( - self, - base_branch: str, - title: str, - branch_name: str, - label: str | list[str], - ): - return await super().create_pull_request( - base_branch, title, branch_name, label, self.issue_number - ) - - async def list_comments(self): - return await super().list_comments(self.issue_number) +from .git import GitHandler as GitHandler +from .github import GithubHandler as GithubHandler +from .issue import IssueHandler as IssueHandler - def commit_and_push(self, message: str, branch_name: str): - return super().commit_and_push(message, branch_name, self.author) +__all__ = ["GitHandler", "GithubHandler", "IssueHandler", "RepoInfo"] diff --git a/src/plugins/github/models/git.py b/src/plugins/github/models/git.py new file mode 100644 index 0000000..22cb8a5 --- /dev/null +++ b/src/plugins/github/models/git.py @@ -0,0 +1,38 @@ +from nonebot import logger +from pydantic import BaseModel + +from src.plugins.github.utils import run_shell_command + + +class GitHandler(BaseModel): + """Git 操作""" + + def commit_and_push(self, message: str, branch_name: str, author: str): + """提交并推送""" + + run_shell_command(["git", "config", "--global", "user.name", author]) + user_email = f"{author}@users.noreply.github.com" + run_shell_command(["git", "config", "--global", "user.email", user_email]) + run_shell_command(["git", "add", "-A"]) + try: + run_shell_command(["git", "commit", "-m", message]) + except Exception: + # 如果提交失败,因为是 pre-commit hooks 格式化代码导致的,所以需要再次提交 + run_shell_command(["git", "add", "-A"]) + run_shell_command(["git", "commit", "-m", message]) + + try: + run_shell_command(["git", "fetch", "origin"]) + r = run_shell_command(["git", "diff", f"origin/{branch_name}", branch_name]) + if r.stdout: + raise Exception + else: + logger.info("检测到本地分支与远程分支一致,跳过推送") + except Exception: + logger.info("检测到本地分支与远程分支不一致,尝试强制推送") + run_shell_command(["git", "push", "origin", branch_name, "-f"]) + + def delete_origin_branch(self, branch_name: str): + """删除远程分支""" + + run_shell_command(["git", "push", "origin", "--delete", branch_name]) diff --git a/src/plugins/github/models/github.py b/src/plugins/github/models/github.py new file mode 100644 index 0000000..3211b69 --- /dev/null +++ b/src/plugins/github/models/github.py @@ -0,0 +1,200 @@ +from typing import Literal +from nonebot import logger +from pydantic import ConfigDict + + +from nonebot.adapters.github import Bot +from githubkit.exception import RequestFailed +from githubkit.utils import UNSET +from githubkit.typing import Missing +from githubkit.rest import PullRequestSimple + +from src.plugins.github.constants import NONEFLOW_MARKER +from src.plugins.github.models import GitHandler, RepoInfo + + +class GithubHandler(GitHandler): + """Bot 相关的 Github 操作""" + + model_config = ConfigDict(arbitrary_types_allowed=True) + + bot: Bot + repo_info: RepoInfo + + async def create_dispatch_event( + self, repo: RepoInfo | None, event_type: str, client_payload: dict + ): + if repo is None: + repo = self.repo_info + await self.bot.rest.repos.async_create_dispatch_event( + repo=repo.repo, + owner=repo.owner, + event_type=event_type, + client_payload=client_payload, # type: ignore + ) + + async def list_comments(self, issue_number: int): + return ( + await self.bot.rest.issues.async_list_comments( + **self.repo_info.model_dump(), issue_number=issue_number + ) + ).parsed_data + + async def comment_issue(self, comment: str, issue_number: int): + """发布评论 + 若之前发布过评论,则修改之前的评论 + """ + logger.info("开始发布评论") + + # 重复利用评论 + # 如果发现之前评论过,直接修改之前的评论 + comments = ( + await self.bot.rest.issues.async_list_comments( + **self.repo_info.model_dump(), issue_number=issue_number + ) + ).parsed_data + reusable_comment = next( + filter(lambda x: NONEFLOW_MARKER in (x.body if x.body else ""), comments), + None, + ) + + # comment = await render_comment(result, bool(reusable_comment)) + if reusable_comment: + logger.info(f"发现已有评论 {reusable_comment.id},正在修改") + if reusable_comment.body != comment: + await self.bot.rest.issues.async_update_comment( + **self.repo_info.model_dump(), + comment_id=reusable_comment.id, + body=comment, + ) + logger.info("评论修改完成") + else: + logger.info("评论内容无变化,跳过修改") + else: + await self.bot.rest.issues.async_create_comment( + **self.repo_info.model_dump(), + issue_number=issue_number, + body=comment, + ) + logger.info("评论创建完成") + + async def get_pull_requests_by_label(self, label: str) -> list[PullRequestSimple]: + """根据标签获取拉取请求""" + pulls = ( + await self.bot.rest.pulls.async_list( + **self.repo_info.model_dump(), state="open" + ) + ).parsed_data + return [ + pull for pull in pulls if label in [label.name for label in pull.labels] + ] + + async def pull_request_to_draft(self, branch_name: str): + """ + 将拉取请求转换为草稿 + """ + pulls = ( + await self.bot.rest.pulls.async_list( + **self.repo_info.model_dump(), + head=f"{self.repo_info.owner}:{branch_name}", + ) + ).parsed_data + if pulls and (pull := pulls[0]) and not pull.draft: + await self.bot.async_graphql( + query="""mutation convertPullRequestToDraft($pullRequestId: ID!) { + convertPullRequestToDraft(input: {pullRequestId: $pullRequestId}) { + clientMutationId + } + }""", + variables={"pullRequestId": pull.node_id}, + ) + logger.info("删除没通过检查,已将之前的拉取请求转换为草稿") + else: + logger.info("没通过检查,暂不创建拉取请求") + + async def merge_pull_request( + self, + pull_number: int, + merge_method: Missing[Literal["merge", "squash", "rebase"]] = UNSET, + ): + """合并拉取请求""" + await self.bot.rest.pulls.async_merge( + **self.repo_info.model_dump(), + pull_number=pull_number, + merge_method=merge_method, + ) + logger.info(f"拉取请求 #{pull_number} 已合并") + + async def create_pull_request( + self, + base_branch: str, + title: str, + branch_name: str, + label: str | list[str], + issue_number: int, + ): + """创建拉取请求""" + body = f"resolve #{issue_number}" + + try: + # 创建拉取请求 + resp = await self.bot.rest.pulls.async_create( + **self.repo_info.model_dump(), + title=title, + body=body, + base=base_branch, + head=branch_name, + ) + pull = resp.parsed_data + + # 自动给拉取请求添加标签 + await self.bot.rest.issues.async_add_labels( + **self.repo_info.model_dump(), + issue_number=pull.number, + labels=[label] if isinstance(label, str) else label, + ) + logger.info("拉取请求创建完毕") + except RequestFailed: + logger.info("该分支的拉取请求已创建,请前往查看") + + pull = ( + await self.bot.rest.pulls.async_list( + **self.repo_info.model_dump(), + head=f"{self.repo_info.owner}:{branch_name}", + ) + ).parsed_data[0] + if pull.title != title: + await self.bot.rest.pulls.async_update( + **self.repo_info.model_dump(), pull_number=pull.number, title=title + ) + logger.info(f"拉取请求标题已修改为 {title}") + if pull.draft: + await self.bot.async_graphql( + query="""mutation markPullRequestReadyForReview($pullRequestId: ID!) { + markPullRequestReadyForReview(input: {pullRequestId: $pullRequestId}) { + clientMutationId + } + }""", + variables={"pullRequestId": pull.node_id}, + ) + logger.info("拉取请求已标记为可评审") + + async def get_user_name(self, account_id: int): + """根据用户 ID 获取用户名""" + return ( + await self.bot.rest.users.async_get_by_id(account_id=account_id) + ).parsed_data.login + + async def get_user_id(self, account_name: str): + """根据用户名获取用户 ID""" + return ( + await self.bot.rest.users.async_get_by_username(username=account_name) + ).parsed_data.id + + async def get_issue(self, issue_number: int): + """获取议题""" + return ( + await self.bot.rest.issues.async_get( + **self.repo_info.model_dump(), issue_number=issue_number + ) + ).parsed_data diff --git a/src/plugins/github/models/issue.py b/src/plugins/github/models/issue.py new file mode 100644 index 0000000..2588ab8 --- /dev/null +++ b/src/plugins/github/models/issue.py @@ -0,0 +1,105 @@ +from typing import Literal +from githubkit.rest import Issue +from nonebot import logger +from pydantic import ( + ConfigDict, + model_validator, +) + +from src.plugins.github.models import GithubHandler +from src.plugins.github.constants import SKIP_COMMENT + + +class IssueHandler(GithubHandler): + """Issue 的相关 Github/Git 操作""" + + model_config = ConfigDict(arbitrary_types_allowed=True) + + issue: Issue + issue_number: int + author_id: int = 0 + author: str = "" + + @model_validator(mode="before") + @classmethod + def issuehandler_validator(cls, data): + if data.get("author_id") is None and data.get("issue"): + issue = data["issue"] + data["author_id"] = issue.user.id if issue.user else 0 + if data.get("author") is None and data.get("issue"): + issue = data["issue"] + data["author"] = issue.user.login if issue.user else "" + if data.get("issue_number") is None and data.get("issue"): + issue = data["issue"] + data["issue_number"] = issue.number + return data + + async def update_issue_title( + self, + title: str, + ): + if self.issue and self.issue.title != title: + await self.bot.rest.issues.async_update( + **self.repo_info.model_dump(), + issue_number=self.issue_number, + title=title, + ) + logger.info(f"标题已修改为 {title}") + + async def update_issue_content(self, body: str): + """编辑议题内容""" + await self.bot.rest.issues.async_update( + **self.repo_info.model_dump(), + issue_number=self.issue_number, + body=body, + ) + logger.info("议题内容已修改") + + async def close_issue( + self, reason: Literal["completed", "not_planned", "reopened"] + ): + """关闭议题""" + if self.issue and self.issue.state == "open": + logger.info(f"正在关闭议题 #{self.issue_number}") + await self.bot.rest.issues.async_update( + **self.repo_info.model_dump(), + issue_number=self.issue_number, + state="closed", + state_reason=reason, + ) + + async def comment_issue(self, comment: str): + return await super().comment_issue(comment, self.issue_number) + + async def create_pull_request( + self, + base_branch: str, + title: str, + branch_name: str, + label: str | list[str], + ): + return await super().create_pull_request( + base_branch, title, branch_name, label, self.issue_number + ) + + async def list_comments(self): + return await super().list_comments(self.issue_number) + + async def should_skip_plugin_test(self) -> bool: + """判断评论是否包含跳过的标记""" + comments = ( + await self.bot.rest.issues.async_list_comments( + **self.repo_info.model_dump(), issue_number=self.issue_number + ) + ).parsed_data + for comment in comments: + author_association = comment.author_association + if comment.body == SKIP_COMMENT and author_association in [ + "OWNER", + "MEMBER", + ]: + return True + return False + + def commit_and_push(self, message: str, branch_name: str): + return super().commit_and_push(message, branch_name, self.author) diff --git a/src/plugins/github/plugins/publish/__init__.py b/src/plugins/github/plugins/publish/__init__.py index 6bab3dd..51f885d 100644 --- a/src/plugins/github/plugins/publish/__init__.py +++ b/src/plugins/github/plugins/publish/__init__.py @@ -182,7 +182,7 @@ async def handle_publish_plugin_check( # 设置拉取请求与议题的标题 # 限制标题长度,过长的标题不好看 - title = f"{PublishType.PLUGIN}: {result.name[:TITLE_MAX_LENGTH]}" + title = f"{publish_type}: {result.name[:TITLE_MAX_LENGTH]}" # 分支命名示例 publish/issue123 branch_name = f"{BRANCH_NAME_PREFIX}{issue_number}" diff --git a/src/plugins/github/plugins/publish/depends.py b/src/plugins/github/plugins/publish/depends.py index 7cb6452..be06a4e 100644 --- a/src/plugins/github/plugins/publish/depends.py +++ b/src/plugins/github/plugins/publish/depends.py @@ -1,32 +1,17 @@ -from githubkit.rest import ( - PullRequestPropLabelsItems, - PullRequestSimple, - WebhookIssueCommentCreatedPropIssueAllof0PropLabelsItems, - WebhookIssuesEditedPropIssuePropLabelsItems, - WebhookIssuesOpenedPropIssuePropLabelsItems, - WebhookIssuesReopenedPropIssueMergedLabels, - WebhookPullRequestReviewSubmittedPropPullRequestPropLabelsItems, -) -from githubkit.typing import Missing -from nonebot.adapters.github import Bot +from githubkit.rest import PullRequestSimple +from nonebot.adapters.github import Bot from nonebot.params import Depends from src.plugins.github.depends import get_issue_title, get_labels, get_repo_info from src.plugins.github.plugins.publish import utils from src.providers.validation.models import PublishType from src.plugins.github.models import RepoInfo +from src.plugins.github.typing import LabelsItems def get_type_by_labels( - labels: list[PullRequestPropLabelsItems] - | list[WebhookPullRequestReviewSubmittedPropPullRequestPropLabelsItems] - | Missing[list[WebhookIssuesOpenedPropIssuePropLabelsItems]] - | Missing[list[WebhookIssuesReopenedPropIssueMergedLabels]] - | Missing[list[WebhookIssuesEditedPropIssuePropLabelsItems]] - | list[WebhookIssueCommentCreatedPropIssueAllof0PropLabelsItems] = Depends( - get_labels - ), + labels: LabelsItems = Depends(get_labels), ) -> PublishType | None: """通过标签获取类型""" return utils.get_type_by_labels(labels) diff --git a/src/plugins/github/plugins/publish/utils.py b/src/plugins/github/plugins/publish/utils.py index 7e30697..5f01526 100644 --- a/src/plugins/github/plugins/publish/utils.py +++ b/src/plugins/github/plugins/publish/utils.py @@ -1,9 +1,8 @@ import asyncio import json import re -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any -from githubkit.typing import Missing from nonebot import logger from nonebot.adapters.github import Bot, GitHubBot @@ -13,13 +12,20 @@ ValidationDict, ) from src.plugins.github.depends import RepoInfo -from src.plugins.github.utils import dump_json, load_json, run_shell_command +from src.plugins.github.utils import ( + dump_json, + extract_author_info, + load_json, + run_shell_command, +) from src.plugins.github.utils import commit_message as _commit_message from src.plugins.github import plugin_config from src.plugins.github.constants import ISSUE_FIELD_PATTERN, ISSUE_FIELD_TEMPLATE +from src.plugins.github.typing import AuthorInfo, LabelsItems from .validation import validate_plugin_info_from_issue + from .constants import ( BRANCH_NAME_PREFIX, COMMIT_MESSAGE_PREFIX, @@ -37,25 +43,13 @@ from githubkit.rest import ( Issue, PullRequest, - PullRequestPropLabelsItems, PullRequestSimple, PullRequestSimplePropLabelsItems, - WebhookIssueCommentCreatedPropIssueAllof0PropLabelsItems, - WebhookIssuesEditedPropIssuePropLabelsItems, - WebhookIssuesOpenedPropIssuePropLabelsItems, - WebhookIssuesReopenedPropIssueMergedLabels, - WebhookPullRequestReviewSubmittedPropPullRequestPropLabelsItems, ) def get_type_by_labels( - labels: list["PullRequestPropLabelsItems"] - | list["PullRequestSimplePropLabelsItems"] - | list["WebhookPullRequestReviewSubmittedPropPullRequestPropLabelsItems"] - | Missing[list["WebhookIssuesOpenedPropIssuePropLabelsItems"]] - | Missing[list["WebhookIssuesReopenedPropIssueMergedLabels"]] - | Missing[list["WebhookIssuesEditedPropIssuePropLabelsItems"]] - | list["WebhookIssueCommentCreatedPropIssueAllof0PropLabelsItems"], + labels: LabelsItems | list["PullRequestSimplePropLabelsItems"], ) -> PublishType | None: """通过标签获取类型""" if not labels: @@ -137,6 +131,7 @@ async def resolve_conflict_pull_requests( continue publish_type = get_type_by_labels(pull.labels) + author_info = extract_author_info(await handler.get_issue(issue_number)) if publish_type: # 需要先获取远程分支,否则无法切换到对应分支 @@ -145,11 +140,12 @@ async def resolve_conflict_pull_requests( run_shell_command(["git", "checkout", pull.head.ref]) # 获取数据 - result = generate_validation_dict_from_file( - publish_type, + result = await generate_validation_dict_from_file( + publish_type=publish_type, # 提交时的 commit message 中包含插件名称 # 但因为仓库内的 plugins.json 中没有插件名称,所以需要从标题中提取 - extract_name_from_title(pull.title, publish_type) + author_info=author_info, + name=extract_name_from_title(pull.title, publish_type) if publish_type == PublishType.PLUGIN else None, ) @@ -170,27 +166,29 @@ async def resolve_conflict_pull_requests( logger.info("拉取请求更新完毕") -def generate_validation_dict_from_file( +async def generate_validation_dict_from_file( publish_type: PublishType, + author_info: AuthorInfo, name: str | None = None, ) -> ValidationDict: """从文件中获取发布所需数据""" + data: list[dict[str, Any]] match publish_type: case PublishType.ADAPTER: with plugin_config.input_config.adapter_path.open( "r", encoding="utf-8" ) as f: - data: list[dict[str, str]] = json.load(f) + data = json.load(f) raw_data = data[-1] case PublishType.BOT: with plugin_config.input_config.bot_path.open("r", encoding="utf-8") as f: - data: list[dict[str, str]] = json.load(f) + data = json.load(f) raw_data = data[-1] case PublishType.PLUGIN: with plugin_config.input_config.plugin_path.open( "r", encoding="utf-8" ) as f: - data: list[dict[str, str]] = json.load(f) + data = json.load(f) raw_data = data[-1] assert name, "插件名称不能为空" raw_data["name"] = name @@ -199,7 +197,8 @@ def generate_validation_dict_from_file( valid=True, type=publish_type, name=raw_data["name"], - author=raw_data["author"], + author_id=author_info["author_id"], + author=author_info["author"], data=raw_data, errors=[], ) @@ -208,6 +207,7 @@ def generate_validation_dict_from_file( def update_file(result: ValidationDict) -> None: """更新文件""" new_data = result.data + match result.type: case PublishType.ADAPTER: path = plugin_config.input_config.adapter_path @@ -219,7 +219,7 @@ def update_file(result: ValidationDict) -> None: new_data = { "module_name": new_data["module_name"], "project_link": new_data["project_link"], - "author": new_data["author"], + "author_id": new_data["author_id"], "tags": new_data["tags"], "is_official": new_data["is_official"], } diff --git a/src/plugins/github/plugins/publish/validation.py b/src/plugins/github/plugins/publish/validation.py index 590d203..7c852d0 100644 --- a/src/plugins/github/plugins/publish/validation.py +++ b/src/plugins/github/plugins/publish/validation.py @@ -9,7 +9,10 @@ ValidationDict, validate_info, ) -from src.plugins.github.utils import extract_publish_info_from_issue +from src.plugins.github.utils import ( + extract_author_info, + extract_publish_info_from_issue, +) from src.providers.constants import DOCKER_IMAGES from src.providers.docker_test import DockerPluginTest from src.providers.store_test.models import DockerTestResult, Metadata @@ -44,16 +47,6 @@ def strip_ansi(text: str | None) -> str: return ansi_escape.sub("", text) -def extract_author_info(issue: Issue) -> dict[str, Any]: - """ - 从议题中获取作者信息 - """ - return { - "author": issue.user.login if issue.user else "", - "author_id": issue.user.id if issue.user else None, - } - - async def validate_plugin_info_from_issue(issue: Issue) -> ValidationDict: """从议题中获取插件信息,并且运行插件测试加载且获取插件元信息后进行验证""" body = issue.body if issue.body else "" @@ -68,23 +61,21 @@ async def validate_plugin_info_from_issue(issue: Issue) -> ValidationDict: }, body, ) - raw_data.update(extract_author_info(issue)) + raw_data.update(extract_author_info(issue)) # 更新作者信息 test_config: str = raw_data["test_config"] module_name: str = raw_data["module_name"] project_link: str = raw_data["project_link"] + # 获取插件上次的数据 with plugin_config.input_config.plugin_path.open("r", encoding="utf-8") as f: previous_data: list[dict[str, str]] = json.load(f) - # 插件字段默认值 - raw_data["name"] = project_link + raw_data["name"] = project_link # 若插件没有元数据,则 name 默认为 project_link raw_data["metadata"] = None # 如果插件被跳过,则从议题获取插件信息 - plugin_test_output: str = "插件未进行测试" - if plugin_config.skip_plugin_test: plugin_info = extract_publish_info_from_issue( { @@ -117,10 +108,6 @@ async def validate_plugin_info_from_issue(issue: Issue) -> ValidationDict: if plugin_metadata: # 从插件测试结果中获得元数据 raw_data.update(plugin_metadata.model_dump()) - else: - # 插件缺少元数据 - # 可能为插件测试未通过,或者插件未按规范编写 - raw_data["name"] = project_link # 传入的验证插件信息的上下文 validation_context = { diff --git a/src/plugins/github/plugins/remove/__init__.py b/src/plugins/github/plugins/remove/__init__.py index 78ebba7..fb80ef6 100644 --- a/src/plugins/github/plugins/remove/__init__.py +++ b/src/plugins/github/plugins/remove/__init__.py @@ -10,7 +10,6 @@ ) from pydantic_core import PydanticCustomError - from src.plugins.github.constants import TITLE_MAX_LENGTH from src.plugins.github.models import IssueHandler from src.plugins.github.depends import ( @@ -40,7 +39,7 @@ async def pr_close_rule( related_issue_number: int | None = Depends(get_related_issue_number), ) -> bool: if not is_remove: - logger.info("拉取请求与发布无关,已跳过") + logger.info("拉取请求与删除无关,已跳过") return False if not related_issue_number: diff --git a/src/plugins/github/plugins/remove/depends.py b/src/plugins/github/plugins/remove/depends.py index be6acae..8878d85 100644 --- a/src/plugins/github/plugins/remove/depends.py +++ b/src/plugins/github/plugins/remove/depends.py @@ -1,30 +1,14 @@ -from githubkit.rest import ( - PullRequestPropLabelsItems, - WebhookIssueCommentCreatedPropIssueAllof0PropLabelsItems, - WebhookIssuesEditedPropIssuePropLabelsItems, - WebhookIssuesOpenedPropIssuePropLabelsItems, - WebhookIssuesReopenedPropIssueMergedLabels, - WebhookPullRequestReviewSubmittedPropPullRequestPropLabelsItems, -) -from githubkit.typing import Missing - from nonebot.params import Depends from src.plugins.github.depends import get_labels +from src.plugins.github.typing import LabelsItems def get_name_by_labels( - labels: list[PullRequestPropLabelsItems] - | list[WebhookPullRequestReviewSubmittedPropPullRequestPropLabelsItems] - | Missing[list[WebhookIssuesOpenedPropIssuePropLabelsItems]] - | Missing[list[WebhookIssuesReopenedPropIssueMergedLabels]] - | Missing[list[WebhookIssuesEditedPropIssuePropLabelsItems]] - | list[WebhookIssueCommentCreatedPropIssueAllof0PropLabelsItems] = Depends( - get_labels - ), + labels: LabelsItems = Depends(get_labels), ) -> list[str]: """通过标签获取名称""" - label_names = [] + label_names: list[str] = [] if not labels: return label_names @@ -34,7 +18,7 @@ def get_name_by_labels( return label_names -def check_labels(labels: list[str] | str): # -> Any: +def check_labels(labels: list[str] | str): """检查标签是否存在""" if isinstance(labels, str): labels = [labels] @@ -42,9 +26,6 @@ def check_labels(labels: list[str] | str): # -> Any: async def _check_labels( has_labels: list[str] = Depends(get_name_by_labels), ) -> bool: - for label in labels: - if label not in has_labels: - return False - return True + return all(label in has_labels for label in labels) return Depends(_check_labels) diff --git a/src/plugins/github/plugins/remove/validation.py b/src/plugins/github/plugins/remove/validation.py index 1dea9bb..d6aa72f 100644 --- a/src/plugins/github/plugins/remove/validation.py +++ b/src/plugins/github/plugins/remove/validation.py @@ -21,7 +21,7 @@ async def validate_author_info(issue: Issue) -> ValidationDict: issue.body or "", ).get("homepage") author = issue.user.login if issue.user else "" - author_id = issue.user.id if issue.user else None + author_id = issue.user.id if issue.user else 0 for type, path in PUBLISH_PATH.items(): if not path.exists(): @@ -44,6 +44,7 @@ async def validate_author_info(issue: Issue) -> ValidationDict: type=type, name=item.get("name") or item.get("module_name") or "", author=author, + author_id=author_id, errors=[], ) raise PydanticCustomError("author_info", "作者信息不匹配") diff --git a/src/plugins/github/typing.py b/src/plugins/github/typing.py index 2649c6a..413f87e 100644 --- a/src/plugins/github/typing.py +++ b/src/plugins/github/typing.py @@ -1,4 +1,4 @@ -from typing import TypeAlias +from typing import TypeAlias, TypedDict from nonebot.adapters.github import ( IssueCommentCreated, IssuesEdited, @@ -8,8 +8,32 @@ PullRequestReviewSubmitted, ) -# 暂不用 type 关键字,编辑器不支持 +from githubkit.rest import ( + PullRequestPropLabelsItems, + WebhookIssueCommentCreatedPropIssueAllof0PropLabelsItems, + WebhookIssuesEditedPropIssuePropLabelsItems, + WebhookIssuesOpenedPropIssuePropLabelsItems, + WebhookIssuesReopenedPropIssuePropLabelsItems, + WebhookPullRequestReviewSubmittedPropPullRequestPropLabelsItems, +) +from githubkit.typing import Missing + IssuesEvent: TypeAlias = ( IssuesOpened | IssuesReopened | IssuesEdited | IssueCommentCreated ) + PullRequestEvent: TypeAlias = PullRequestClosed | PullRequestReviewSubmitted + +LabelsItems: TypeAlias = ( + list[PullRequestPropLabelsItems] + | list[WebhookPullRequestReviewSubmittedPropPullRequestPropLabelsItems] + | Missing[list[WebhookIssuesOpenedPropIssuePropLabelsItems]] + | Missing[list[WebhookIssuesReopenedPropIssuePropLabelsItems]] + | Missing[list[WebhookIssuesEditedPropIssuePropLabelsItems]] + | list[WebhookIssueCommentCreatedPropIssueAllof0PropLabelsItems] +) + + +class AuthorInfo(TypedDict): + author: str + author_id: int diff --git a/src/plugins/github/utils.py b/src/plugins/github/utils.py index 125cb9b..5a50802 100644 --- a/src/plugins/github/utils.py +++ b/src/plugins/github/utils.py @@ -1,10 +1,15 @@ import json +import subprocess + from pathlib import Path from re import Pattern -import subprocess from typing import Any from nonebot import logger + from pydantic_core import to_jsonable_python +from githubkit.rest import Issue + +from src.plugins.github.typing import AuthorInfo def run_shell_command(command: list[str]): @@ -55,3 +60,13 @@ def extract_publish_info_from_issue( for key, match in matchers.items() } return data + + +def extract_author_info(issue: Issue) -> AuthorInfo: + """ + 从议题中获取作者信息 + """ + return { + "author": issue.user.login if issue.user else "", + "author_id": issue.user.id if issue.user else 0, + } diff --git a/src/providers/store_test/models.py b/src/providers/store_test/models.py index fa7d257..50857a4 100644 --- a/src/providers/store_test/models.py +++ b/src/providers/store_test/models.py @@ -49,7 +49,7 @@ class StorePlugin(TagModel): module_name: str project_link: str - author: str + author_id: int tags: list[Tag] is_official: bool @@ -79,7 +79,7 @@ class Plugin(TagModel): name: str desc: str author: str - author_id: int | None = None + author_id: int homepage: str tags: list[Tag] is_official: bool diff --git a/src/providers/store_test/store.py b/src/providers/store_test/store.py index 0038d4b..a86e3ce 100644 --- a/src/providers/store_test/store.py +++ b/src/providers/store_test/store.py @@ -162,8 +162,8 @@ async def worker(): click.echo(f"{i}/{limit} 正在测试插件 {key} ...") await worker() # TODO: 修改为并行 i += 1 - except Exception as e: - click.echo(e) + except Exception as err: + click.echo(err) continue return new_results, new_plugins @@ -225,6 +225,14 @@ async def run_single_plugin( if self.should_skip(key, force): return - new_result, new_plugin = await self.test_plugin(key, config, plugin_data) + new_plugin: Plugin | None = None + + try: + new_result, new_plugin = await self.test_plugin(key, config, plugin_data) + except Exception as err: + click.echo(err) + if new_plugin: await self.merge_data({key: new_result}, {key: new_plugin}) + else: + await self.merge_data({}, {}) diff --git a/src/providers/store_test/validation.py b/src/providers/store_test/validation.py index 64ecbfe..0dcef93 100644 --- a/src/providers/store_test/validation.py +++ b/src/providers/store_test/validation.py @@ -5,6 +5,7 @@ import click +from src.providers.validation.utils import get_author_name from src.providers.validation import PublishType, validate_info from src.providers.docker_test import DockerPluginTest from src.providers.constants import DOCKER_IMAGES @@ -33,15 +34,19 @@ async def validate_plugin( project_link = plugin.project_link module_name = plugin.module_name is_official = plugin.is_official + # 从 PyPI 获取信息 pypi_version = get_latest_version(project_link) pypi_time = get_upload_time(project_link) + # 如果传递了 data 参数 # 则直接使用 data 作为插件数据 # 并且将 skip_test 设置为 True if plugin_data: # 跳过测试时无法获取到测试的版本 test_version = None + # 跳过测试时无法获取到测试的环境 + test_env = "skip_test" # 因为跳过测试,测试结果无意义 plugin_test_load = True plugin_test_output = "已跳过测试" @@ -58,7 +63,7 @@ async def validate_plugin( metadata: Metadata | None = Metadata( name=new_plugin.name, - description=new_plugin.desc, + desc=new_plugin.desc, homepage=new_plugin.homepage, type=new_plugin.type, supported_adapters=new_plugin.supported_adapters, @@ -69,30 +74,53 @@ async def validate_plugin( ).run("3.10") # 获取测试结果 click.echo(f"测试结果:{test_result}") + plugin_test_output = test_result.outputs plugin_test_load = test_result.load test_version = test_result.version + test_env = test_result.test_env metadata = test_result.metadata + # 当跳过测试的插件首次通过加载测试,则不再标记为跳过测试 should_skip: bool = False if plugin_test_load else skip_test + # 通过 Github API 获取插件作者名称 + try: + author_name = get_author_name(plugin.author_id) + except Exception: + # 若无法请求,试图从上次的插件数据中获取 + author_name = previous_plugin.author if previous_plugin else "" + raw_data: dict[str, Any] = { "module_name": module_name, "project_link": project_link, - "author": plugin.author, + "author": author_name, + "author_id": plugin.author_id, "tags": plugin.tags, - "skip_plugin_test": should_skip, - "plugin_test_result": plugin_test_load, + "load": plugin_test_load, "plugin_test_output": "", - "plugin_test_metadata": metadata, + "metadata": metadata, + } + context = { "previous_data": [], + "skip_plugin_test": should_skip, } if metadata: raw_data.update(metadata.model_dump()) elif skip_test and previous_plugin: + # 将上次的插件数据作为新的插件数据 + raw_data.update(previous_plugin.model_dump()) raw_data.update(previous_plugin.metadata().model_dump()) - validation_data: ValidationDict = validate_info(PublishType.PLUGIN, raw_data) + + # 部分字段需要重置 + raw_data["metadata"] = previous_plugin.metadata() + raw_data["version"] = pypi_version + raw_data["time"] = pypi_time + + validation_data: ValidationDict = validate_info( + PublishType.PLUGIN, raw_data, context + ) new_data = { # 插件验证过程中无法获取是否是官方插件,因此需要从原始数据中获取 @@ -106,7 +134,7 @@ async def validate_plugin( if validation_data.valid: data = validation_data.data data.update(new_data) - new_plugin = Plugin(**validation_data.data) + new_plugin = Plugin(**data) elif previous_plugin: data = previous_plugin.model_dump() data.update(new_data) @@ -138,7 +166,7 @@ async def validate_plugin( "load": plugin_test_output, "metadata": metadata, }, - test_env={test_result.test_env: True}, + test_env={test_env: True}, ) return result, new_plugin diff --git a/src/providers/validation/__init__.py b/src/providers/validation/__init__.py index 21b86e6..c4be475 100644 --- a/src/providers/validation/__init__.py +++ b/src/providers/validation/__init__.py @@ -54,6 +54,7 @@ def validate_info( data: dict[str, Any] = validation_context["valid_data"] # 翻译错误 errors = translate_errors(errors) + return ValidationDict( valid=not errors, data=data, @@ -61,4 +62,5 @@ def validate_info( type=publish_type, name=data.get("name") or raw_data.get("name") or "", author=data.get("author", ""), + author_id=data.get("author_id", 0), ) diff --git a/src/providers/validation/models.py b/src/providers/validation/models.py index 9554720..168b77b 100644 --- a/src/providers/validation/models.py +++ b/src/providers/validation/models.py @@ -51,6 +51,7 @@ class ValidationDict(BaseModel): valid: bool type: PublishType name: str + author_id: int author: str data: dict[str, Any] = {} errors: list[ErrorDetails] = [] @@ -163,12 +164,14 @@ def homepage_validator(cls, v: str) -> str: @field_validator("tags", mode="before") @classmethod - def tags_validator(cls, v: str) -> list[dict[str, str]]: + def tags_validator(cls, v: str | list[Any]) -> list[dict[str, str]]: + if not isinstance(v, str): + return v + try: - tags: list[Any] | Any = json.loads(v) + return json.loads(v) except json.JSONDecodeError: raise PydanticCustomError("json_type", "JSON 格式不合法") - return tags class PluginPublishInfo(PublishInfo, PyPIMixin): @@ -259,7 +262,7 @@ def plugin_test_metadata_validator( # 如果没有传入插件元数据,尝试从上下文中获取 try: return Metadata(**context["valid_data"]) - except ValidationError as err: + except ValidationError: raise PydanticCustomError( "plugin.metadata", "插件无法获取到元数据", diff --git a/src/providers/validation/utils.py b/src/providers/validation/utils.py index 4b1f420..dbf028c 100644 --- a/src/providers/validation/utils.py +++ b/src/providers/validation/utils.py @@ -31,6 +31,14 @@ def check_url(url: str) -> tuple[int, str]: return -1, str(e) +@cache +def get_author_name(author_id: int) -> str: + """通过作者的ID获取作者名字""" + url = f"https://api.github.com/user/{author_id}" + resp = httpx.get(url) + return resp.json()["login"] + + def get_adapters() -> set[str]: """获取适配器列表""" resp = httpx.get(STORE_ADAPTERS_URL)