From 0540c8560beb6a2c4143739dbd4f0eb00ba76759 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Tue, 19 Mar 2024 16:53:06 -0700 Subject: [PATCH 001/101] chore: remove nox uninstall/reinstall from python build.sh template (#630) Source-Link: https://github.com/googleapis/synthtool/commit/26358881238150aa51939ccc82b78c0e33d3bc9c Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-python:latest@sha256:007e7e46ef05e5a32e652bd0062be02f6ff050347d91e0f357b28caab0a042c4 Co-authored-by: Owl Bot Co-authored-by: Victor Chudnovsky --- .github/.OwlBot.lock.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 5d9542b1c..44dcc967f 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -15,3 +15,4 @@ docker: image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest digest: sha256:3741fd1f5f5150378563c76afa06bcc12777b5fe54c5ee01115218f83872134f # created: 2024-03-15T16:26:15.743347415Z + From f52c0375be74d15604ad365335c79dda0fd01bf5 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Tue, 19 Mar 2024 17:05:25 -0700 Subject: [PATCH 002/101] chore(python): update dependencies in /.kokoro (#631) Source-Link: https://github.com/googleapis/synthtool/commit/db94845da69ccdfefd7ce55c84e6cfa74829747e Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-python:latest@sha256:a8a80fc6456e433df53fc2a0d72ca0345db0ddefb409f1b75b118dfd1babd952 Co-authored-by: Owl Bot Co-authored-by: Victor Chudnovsky --- .github/.OwlBot.lock.yaml | 1 - .kokoro/requirements.in | 3 +- .kokoro/requirements.txt | 114 +++++++++++++++++--------------------- 3 files changed, 54 insertions(+), 64 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 44dcc967f..5d9542b1c 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -15,4 +15,3 @@ docker: image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest digest: sha256:3741fd1f5f5150378563c76afa06bcc12777b5fe54c5ee01115218f83872134f # created: 2024-03-15T16:26:15.743347415Z - diff --git a/.kokoro/requirements.in b/.kokoro/requirements.in index ec867d9fd..fff4d9ce0 100644 --- a/.kokoro/requirements.in +++ b/.kokoro/requirements.in @@ -1,5 +1,5 @@ gcp-docuploader -gcp-releasetool>=1.10.5 # required for compatibility with cryptography>=39.x +gcp-releasetool>=2 # required for compatibility with cryptography>=42.x importlib-metadata typing-extensions twine @@ -8,3 +8,4 @@ setuptools nox>=2022.11.21 # required to remove dependency on py charset-normalizer<3 click<8.1.0 +cryptography>=42.0.5 diff --git a/.kokoro/requirements.txt b/.kokoro/requirements.txt index bda8e38c4..dd61f5f32 100644 --- a/.kokoro/requirements.txt +++ b/.kokoro/requirements.txt @@ -93,40 +93,41 @@ colorlog==6.7.0 \ # via # gcp-docuploader # nox -cryptography==42.0.4 \ - --hash=sha256:01911714117642a3f1792c7f376db572aadadbafcd8d75bb527166009c9f1d1b \ - --hash=sha256:0e89f7b84f421c56e7ff69f11c441ebda73b8a8e6488d322ef71746224c20fce \ - --hash=sha256:12d341bd42cdb7d4937b0cabbdf2a94f949413ac4504904d0cdbdce4a22cbf88 \ - --hash=sha256:15a1fb843c48b4a604663fa30af60818cd28f895572386e5f9b8a665874c26e7 \ - --hash=sha256:1cdcdbd117681c88d717437ada72bdd5be9de117f96e3f4d50dab3f59fd9ab20 \ - --hash=sha256:1df6fcbf60560d2113b5ed90f072dc0b108d64750d4cbd46a21ec882c7aefce9 \ - --hash=sha256:3c6048f217533d89f2f8f4f0fe3044bf0b2090453b7b73d0b77db47b80af8dff \ - --hash=sha256:3e970a2119507d0b104f0a8e281521ad28fc26f2820687b3436b8c9a5fcf20d1 \ - --hash=sha256:44a64043f743485925d3bcac548d05df0f9bb445c5fcca6681889c7c3ab12764 \ - --hash=sha256:4e36685cb634af55e0677d435d425043967ac2f3790ec652b2b88ad03b85c27b \ - --hash=sha256:5f8907fcf57392cd917892ae83708761c6ff3c37a8e835d7246ff0ad251d9298 \ - --hash=sha256:69b22ab6506a3fe483d67d1ed878e1602bdd5912a134e6202c1ec672233241c1 \ - --hash=sha256:6bfadd884e7280df24d26f2186e4e07556a05d37393b0f220a840b083dc6a824 \ - --hash=sha256:6d0fbe73728c44ca3a241eff9aefe6496ab2656d6e7a4ea2459865f2e8613257 \ - --hash=sha256:6ffb03d419edcab93b4b19c22ee80c007fb2d708429cecebf1dd3258956a563a \ - --hash=sha256:810bcf151caefc03e51a3d61e53335cd5c7316c0a105cc695f0959f2c638b129 \ - --hash=sha256:831a4b37accef30cccd34fcb916a5d7b5be3cbbe27268a02832c3e450aea39cb \ - --hash=sha256:887623fe0d70f48ab3f5e4dbf234986b1329a64c066d719432d0698522749929 \ - --hash=sha256:a0298bdc6e98ca21382afe914c642620370ce0470a01e1bef6dd9b5354c36854 \ - --hash=sha256:a1327f280c824ff7885bdeef8578f74690e9079267c1c8bd7dc5cc5aa065ae52 \ - --hash=sha256:c1f25b252d2c87088abc8bbc4f1ecbf7c919e05508a7e8628e6875c40bc70923 \ - --hash=sha256:c3a5cbc620e1e17009f30dd34cb0d85c987afd21c41a74352d1719be33380885 \ - --hash=sha256:ce8613beaffc7c14f091497346ef117c1798c202b01153a8cc7b8e2ebaaf41c0 \ - --hash=sha256:d2a27aca5597c8a71abbe10209184e1a8e91c1fd470b5070a2ea60cafec35bcd \ - --hash=sha256:dad9c385ba8ee025bb0d856714f71d7840020fe176ae0229de618f14dae7a6e2 \ - --hash=sha256:db4b65b02f59035037fde0998974d84244a64c3265bdef32a827ab9b63d61b18 \ - --hash=sha256:e09469a2cec88fb7b078e16d4adec594414397e8879a4341c6ace96013463d5b \ - --hash=sha256:e53dc41cda40b248ebc40b83b31516487f7db95ab8ceac1f042626bc43a2f992 \ - --hash=sha256:f1e85a178384bf19e36779d91ff35c7617c885da487d689b05c1366f9933ad74 \ - --hash=sha256:f47be41843200f7faec0683ad751e5ef11b9a56a220d57f300376cd8aba81660 \ - --hash=sha256:fb0cef872d8193e487fc6bdb08559c3aa41b659a7d9be48b2e10747f47863925 \ - --hash=sha256:ffc73996c4fca3d2b6c1c8c12bfd3ad00def8621da24f547626bf06441400449 +cryptography==42.0.5 \ + --hash=sha256:0270572b8bd2c833c3981724b8ee9747b3ec96f699a9665470018594301439ee \ + --hash=sha256:111a0d8553afcf8eb02a4fea6ca4f59d48ddb34497aa8706a6cf536f1a5ec576 \ + --hash=sha256:16a48c23a62a2f4a285699dba2e4ff2d1cff3115b9df052cdd976a18856d8e3d \ + --hash=sha256:1b95b98b0d2af784078fa69f637135e3c317091b615cd0905f8b8a087e86fa30 \ + --hash=sha256:1f71c10d1e88467126f0efd484bd44bca5e14c664ec2ede64c32f20875c0d413 \ + --hash=sha256:2424ff4c4ac7f6b8177b53c17ed5d8fa74ae5955656867f5a8affaca36a27abb \ + --hash=sha256:2bce03af1ce5a5567ab89bd90d11e7bbdff56b8af3acbbec1faded8f44cb06da \ + --hash=sha256:329906dcc7b20ff3cad13c069a78124ed8247adcac44b10bea1130e36caae0b4 \ + --hash=sha256:37dd623507659e08be98eec89323469e8c7b4c1407c85112634ae3dbdb926fdd \ + --hash=sha256:3eaafe47ec0d0ffcc9349e1708be2aaea4c6dd4978d76bf6eb0cb2c13636c6fc \ + --hash=sha256:5e6275c09d2badf57aea3afa80d975444f4be8d3bc58f7f80d2a484c6f9485c8 \ + --hash=sha256:6fe07eec95dfd477eb9530aef5bead34fec819b3aaf6c5bd6d20565da607bfe1 \ + --hash=sha256:7367d7b2eca6513681127ebad53b2582911d1736dc2ffc19f2c3ae49997496bc \ + --hash=sha256:7cde5f38e614f55e28d831754e8a3bacf9ace5d1566235e39d91b35502d6936e \ + --hash=sha256:9481ffe3cf013b71b2428b905c4f7a9a4f76ec03065b05ff499bb5682a8d9ad8 \ + --hash=sha256:98d8dc6d012b82287f2c3d26ce1d2dd130ec200c8679b6213b3c73c08b2b7940 \ + --hash=sha256:a011a644f6d7d03736214d38832e030d8268bcff4a41f728e6030325fea3e400 \ + --hash=sha256:a2913c5375154b6ef2e91c10b5720ea6e21007412f6437504ffea2109b5a33d7 \ + --hash=sha256:a30596bae9403a342c978fb47d9b0ee277699fa53bbafad14706af51fe543d16 \ + --hash=sha256:b03c2ae5d2f0fc05f9a2c0c997e1bc18c8229f392234e8a0194f202169ccd278 \ + --hash=sha256:b6cd2203306b63e41acdf39aa93b86fb566049aeb6dc489b70e34bcd07adca74 \ + --hash=sha256:b7ffe927ee6531c78f81aa17e684e2ff617daeba7f189f911065b2ea2d526dec \ + --hash=sha256:b8cac287fafc4ad485b8a9b67d0ee80c66bf3574f655d3b97ef2e1082360faf1 \ + --hash=sha256:ba334e6e4b1d92442b75ddacc615c5476d4ad55cc29b15d590cc6b86efa487e2 \ + --hash=sha256:ba3e4a42397c25b7ff88cdec6e2a16c2be18720f317506ee25210f6d31925f9c \ + --hash=sha256:c41fb5e6a5fe9ebcd58ca3abfeb51dffb5d83d6775405305bfa8715b76521922 \ + --hash=sha256:cd2030f6650c089aeb304cf093f3244d34745ce0cfcc39f20c6fbfe030102e2a \ + --hash=sha256:cd65d75953847815962c84a4654a84850b2bb4aed3f26fadcc1c13892e1e29f6 \ + --hash=sha256:e4985a790f921508f36f81831817cbc03b102d643b5fcb81cd33df3fa291a1a1 \ + --hash=sha256:e807b3188f9eb0eaa7bbb579b462c5ace579f1cedb28107ce8b48a9f7ad3679e \ + --hash=sha256:f12764b8fffc7a123f641d7d049d382b73f96a34117e0b637b80643169cec8ac \ + --hash=sha256:f8837fe1d6ac4a8052a9a8ddab256bc006242696f03368a4009be7ee3075cdb7 # via + # -r requirements.in # gcp-releasetool # secretstorage distlib==0.3.7 \ @@ -145,9 +146,9 @@ gcp-docuploader==0.6.5 \ --hash=sha256:30221d4ac3e5a2b9c69aa52fdbef68cc3f27d0e6d0d90e220fc024584b8d2318 \ --hash=sha256:b7458ef93f605b9d46a4bf3a8dc1755dad1f31d030c8679edf304e343b347eea # via -r requirements.in -gcp-releasetool==1.16.0 \ - --hash=sha256:27bf19d2e87aaa884096ff941aa3c592c482be3d6a2bfe6f06afafa6af2353e3 \ - --hash=sha256:a316b197a543fd036209d0caba7a8eb4d236d8e65381c80cbc6d7efaa7606d63 +gcp-releasetool==2.0.0 \ + --hash=sha256:3d73480b50ba243f22d7c7ec08b115a30e1c7817c4899781840c26f9c55b8277 \ + --hash=sha256:7aa9fd935ec61e581eb8458ad00823786d91756c25e492f372b2b30962f3c28f # via -r requirements.in google-api-core==2.12.0 \ --hash=sha256:c22e01b1e3c4dcd90998494879612c38d0a3411d1f7b679eb89e2abe3ce1f553 \ @@ -392,29 +393,18 @@ platformdirs==3.11.0 \ --hash=sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3 \ --hash=sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e # via virtualenv -protobuf==3.20.3 \ - --hash=sha256:03038ac1cfbc41aa21f6afcbcd357281d7521b4157926f30ebecc8d4ea59dcb7 \ - --hash=sha256:28545383d61f55b57cf4df63eebd9827754fd2dc25f80c5253f9184235db242c \ - --hash=sha256:2e3427429c9cffebf259491be0af70189607f365c2f41c7c3764af6f337105f2 \ - --hash=sha256:398a9e0c3eaceb34ec1aee71894ca3299605fa8e761544934378bbc6c97de23b \ - --hash=sha256:44246bab5dd4b7fbd3c0c80b6f16686808fab0e4aca819ade6e8d294a29c7050 \ - --hash=sha256:447d43819997825d4e71bf5769d869b968ce96848b6479397e29fc24c4a5dfe9 \ - --hash=sha256:67a3598f0a2dcbc58d02dd1928544e7d88f764b47d4a286202913f0b2801c2e7 \ - --hash=sha256:74480f79a023f90dc6e18febbf7b8bac7508420f2006fabd512013c0c238f454 \ - --hash=sha256:819559cafa1a373b7096a482b504ae8a857c89593cf3a25af743ac9ecbd23480 \ - --hash=sha256:899dc660cd599d7352d6f10d83c95df430a38b410c1b66b407a6b29265d66469 \ - --hash=sha256:8c0c984a1b8fef4086329ff8dd19ac77576b384079247c770f29cc8ce3afa06c \ - --hash=sha256:9aae4406ea63d825636cc11ffb34ad3379335803216ee3a856787bcf5ccc751e \ - --hash=sha256:a7ca6d488aa8ff7f329d4c545b2dbad8ac31464f1d8b1c87ad1346717731e4db \ - --hash=sha256:b6cc7ba72a8850621bfec987cb72623e703b7fe2b9127a161ce61e61558ad905 \ - --hash=sha256:bf01b5720be110540be4286e791db73f84a2b721072a3711efff6c324cdf074b \ - --hash=sha256:c02ce36ec760252242a33967d51c289fd0e1c0e6e5cc9397e2279177716add86 \ - --hash=sha256:d9e4432ff660d67d775c66ac42a67cf2453c27cb4d738fc22cb53b5d84c135d4 \ - --hash=sha256:daa564862dd0d39c00f8086f88700fdbe8bc717e993a21e90711acfed02f2402 \ - --hash=sha256:de78575669dddf6099a8a0f46a27e82a1783c557ccc38ee620ed8cc96d3be7d7 \ - --hash=sha256:e64857f395505ebf3d2569935506ae0dfc4a15cb80dc25261176c784662cdcc4 \ - --hash=sha256:f4bd856d702e5b0d96a00ec6b307b0f51c1982c2bf9c0052cf9019e9a544ba99 \ - --hash=sha256:f4c42102bc82a51108e449cbb32b19b180022941c727bac0cfd50170341f16ee +protobuf==4.25.3 \ + --hash=sha256:19b270aeaa0099f16d3ca02628546b8baefe2955bbe23224aaf856134eccf1e4 \ + --hash=sha256:209ba4cc916bab46f64e56b85b090607a676f66b473e6b762e6f1d9d591eb2e8 \ + --hash=sha256:25b5d0b42fd000320bd7830b349e3b696435f3b329810427a6bcce6a5492cc5c \ + --hash=sha256:7c8daa26095f82482307bc717364e7c13f4f1c99659be82890dcfc215194554d \ + --hash=sha256:c053062984e61144385022e53678fbded7aea14ebb3e0305ae3592fb219ccfa4 \ + --hash=sha256:d4198877797a83cbfe9bffa3803602bbe1625dc30d8a097365dbc762e5790faa \ + --hash=sha256:e3c97a1555fd6388f857770ff8b9703083de6bf1f9274a002a332d65fbb56c8c \ + --hash=sha256:e7cb0ae90dd83727f0c0718634ed56837bfeeee29a5f82a7514c03ee1364c019 \ + --hash=sha256:f0700d54bcf45424477e46a9f0944155b46fb0639d69728739c0e47bab83f2b9 \ + --hash=sha256:f1279ab38ecbfae7e456a108c5c0681e4956d5b1090027c1de0f934dfdb4b35c \ + --hash=sha256:f4f118245c4a087776e0a8408be33cf09f6c547442c00395fbfb116fac2f8ac2 # via # gcp-docuploader # gcp-releasetool @@ -518,7 +508,7 @@ zipp==3.17.0 \ # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: -setuptools==68.2.2 \ - --hash=sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87 \ - --hash=sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a +setuptools==69.2.0 \ + --hash=sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e \ + --hash=sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c # via -r requirements.in From 2100f7869d9dc4b75c594c95c51e4db8c392834f Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Thu, 21 Mar 2024 16:13:45 -0400 Subject: [PATCH 003/101] chore(main): release 2.18.0 (#615) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(main): release 2.18.0 * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> Co-authored-by: Owl Bot --- .kokoro/requirements.in | 3 +- .kokoro/requirements.txt | 114 ++++++++++++++++++++----------------- CHANGELOG.md | 13 +++++ google/api_core/version.py | 2 +- 4 files changed, 77 insertions(+), 55 deletions(-) diff --git a/.kokoro/requirements.in b/.kokoro/requirements.in index fff4d9ce0..ec867d9fd 100644 --- a/.kokoro/requirements.in +++ b/.kokoro/requirements.in @@ -1,5 +1,5 @@ gcp-docuploader -gcp-releasetool>=2 # required for compatibility with cryptography>=42.x +gcp-releasetool>=1.10.5 # required for compatibility with cryptography>=39.x importlib-metadata typing-extensions twine @@ -8,4 +8,3 @@ setuptools nox>=2022.11.21 # required to remove dependency on py charset-normalizer<3 click<8.1.0 -cryptography>=42.0.5 diff --git a/.kokoro/requirements.txt b/.kokoro/requirements.txt index dd61f5f32..bda8e38c4 100644 --- a/.kokoro/requirements.txt +++ b/.kokoro/requirements.txt @@ -93,41 +93,40 @@ colorlog==6.7.0 \ # via # gcp-docuploader # nox -cryptography==42.0.5 \ - --hash=sha256:0270572b8bd2c833c3981724b8ee9747b3ec96f699a9665470018594301439ee \ - --hash=sha256:111a0d8553afcf8eb02a4fea6ca4f59d48ddb34497aa8706a6cf536f1a5ec576 \ - --hash=sha256:16a48c23a62a2f4a285699dba2e4ff2d1cff3115b9df052cdd976a18856d8e3d \ - --hash=sha256:1b95b98b0d2af784078fa69f637135e3c317091b615cd0905f8b8a087e86fa30 \ - --hash=sha256:1f71c10d1e88467126f0efd484bd44bca5e14c664ec2ede64c32f20875c0d413 \ - --hash=sha256:2424ff4c4ac7f6b8177b53c17ed5d8fa74ae5955656867f5a8affaca36a27abb \ - --hash=sha256:2bce03af1ce5a5567ab89bd90d11e7bbdff56b8af3acbbec1faded8f44cb06da \ - --hash=sha256:329906dcc7b20ff3cad13c069a78124ed8247adcac44b10bea1130e36caae0b4 \ - --hash=sha256:37dd623507659e08be98eec89323469e8c7b4c1407c85112634ae3dbdb926fdd \ - --hash=sha256:3eaafe47ec0d0ffcc9349e1708be2aaea4c6dd4978d76bf6eb0cb2c13636c6fc \ - --hash=sha256:5e6275c09d2badf57aea3afa80d975444f4be8d3bc58f7f80d2a484c6f9485c8 \ - --hash=sha256:6fe07eec95dfd477eb9530aef5bead34fec819b3aaf6c5bd6d20565da607bfe1 \ - --hash=sha256:7367d7b2eca6513681127ebad53b2582911d1736dc2ffc19f2c3ae49997496bc \ - --hash=sha256:7cde5f38e614f55e28d831754e8a3bacf9ace5d1566235e39d91b35502d6936e \ - --hash=sha256:9481ffe3cf013b71b2428b905c4f7a9a4f76ec03065b05ff499bb5682a8d9ad8 \ - --hash=sha256:98d8dc6d012b82287f2c3d26ce1d2dd130ec200c8679b6213b3c73c08b2b7940 \ - --hash=sha256:a011a644f6d7d03736214d38832e030d8268bcff4a41f728e6030325fea3e400 \ - --hash=sha256:a2913c5375154b6ef2e91c10b5720ea6e21007412f6437504ffea2109b5a33d7 \ - --hash=sha256:a30596bae9403a342c978fb47d9b0ee277699fa53bbafad14706af51fe543d16 \ - --hash=sha256:b03c2ae5d2f0fc05f9a2c0c997e1bc18c8229f392234e8a0194f202169ccd278 \ - --hash=sha256:b6cd2203306b63e41acdf39aa93b86fb566049aeb6dc489b70e34bcd07adca74 \ - --hash=sha256:b7ffe927ee6531c78f81aa17e684e2ff617daeba7f189f911065b2ea2d526dec \ - --hash=sha256:b8cac287fafc4ad485b8a9b67d0ee80c66bf3574f655d3b97ef2e1082360faf1 \ - --hash=sha256:ba334e6e4b1d92442b75ddacc615c5476d4ad55cc29b15d590cc6b86efa487e2 \ - --hash=sha256:ba3e4a42397c25b7ff88cdec6e2a16c2be18720f317506ee25210f6d31925f9c \ - --hash=sha256:c41fb5e6a5fe9ebcd58ca3abfeb51dffb5d83d6775405305bfa8715b76521922 \ - --hash=sha256:cd2030f6650c089aeb304cf093f3244d34745ce0cfcc39f20c6fbfe030102e2a \ - --hash=sha256:cd65d75953847815962c84a4654a84850b2bb4aed3f26fadcc1c13892e1e29f6 \ - --hash=sha256:e4985a790f921508f36f81831817cbc03b102d643b5fcb81cd33df3fa291a1a1 \ - --hash=sha256:e807b3188f9eb0eaa7bbb579b462c5ace579f1cedb28107ce8b48a9f7ad3679e \ - --hash=sha256:f12764b8fffc7a123f641d7d049d382b73f96a34117e0b637b80643169cec8ac \ - --hash=sha256:f8837fe1d6ac4a8052a9a8ddab256bc006242696f03368a4009be7ee3075cdb7 +cryptography==42.0.4 \ + --hash=sha256:01911714117642a3f1792c7f376db572aadadbafcd8d75bb527166009c9f1d1b \ + --hash=sha256:0e89f7b84f421c56e7ff69f11c441ebda73b8a8e6488d322ef71746224c20fce \ + --hash=sha256:12d341bd42cdb7d4937b0cabbdf2a94f949413ac4504904d0cdbdce4a22cbf88 \ + --hash=sha256:15a1fb843c48b4a604663fa30af60818cd28f895572386e5f9b8a665874c26e7 \ + --hash=sha256:1cdcdbd117681c88d717437ada72bdd5be9de117f96e3f4d50dab3f59fd9ab20 \ + --hash=sha256:1df6fcbf60560d2113b5ed90f072dc0b108d64750d4cbd46a21ec882c7aefce9 \ + --hash=sha256:3c6048f217533d89f2f8f4f0fe3044bf0b2090453b7b73d0b77db47b80af8dff \ + --hash=sha256:3e970a2119507d0b104f0a8e281521ad28fc26f2820687b3436b8c9a5fcf20d1 \ + --hash=sha256:44a64043f743485925d3bcac548d05df0f9bb445c5fcca6681889c7c3ab12764 \ + --hash=sha256:4e36685cb634af55e0677d435d425043967ac2f3790ec652b2b88ad03b85c27b \ + --hash=sha256:5f8907fcf57392cd917892ae83708761c6ff3c37a8e835d7246ff0ad251d9298 \ + --hash=sha256:69b22ab6506a3fe483d67d1ed878e1602bdd5912a134e6202c1ec672233241c1 \ + --hash=sha256:6bfadd884e7280df24d26f2186e4e07556a05d37393b0f220a840b083dc6a824 \ + --hash=sha256:6d0fbe73728c44ca3a241eff9aefe6496ab2656d6e7a4ea2459865f2e8613257 \ + --hash=sha256:6ffb03d419edcab93b4b19c22ee80c007fb2d708429cecebf1dd3258956a563a \ + --hash=sha256:810bcf151caefc03e51a3d61e53335cd5c7316c0a105cc695f0959f2c638b129 \ + --hash=sha256:831a4b37accef30cccd34fcb916a5d7b5be3cbbe27268a02832c3e450aea39cb \ + --hash=sha256:887623fe0d70f48ab3f5e4dbf234986b1329a64c066d719432d0698522749929 \ + --hash=sha256:a0298bdc6e98ca21382afe914c642620370ce0470a01e1bef6dd9b5354c36854 \ + --hash=sha256:a1327f280c824ff7885bdeef8578f74690e9079267c1c8bd7dc5cc5aa065ae52 \ + --hash=sha256:c1f25b252d2c87088abc8bbc4f1ecbf7c919e05508a7e8628e6875c40bc70923 \ + --hash=sha256:c3a5cbc620e1e17009f30dd34cb0d85c987afd21c41a74352d1719be33380885 \ + --hash=sha256:ce8613beaffc7c14f091497346ef117c1798c202b01153a8cc7b8e2ebaaf41c0 \ + --hash=sha256:d2a27aca5597c8a71abbe10209184e1a8e91c1fd470b5070a2ea60cafec35bcd \ + --hash=sha256:dad9c385ba8ee025bb0d856714f71d7840020fe176ae0229de618f14dae7a6e2 \ + --hash=sha256:db4b65b02f59035037fde0998974d84244a64c3265bdef32a827ab9b63d61b18 \ + --hash=sha256:e09469a2cec88fb7b078e16d4adec594414397e8879a4341c6ace96013463d5b \ + --hash=sha256:e53dc41cda40b248ebc40b83b31516487f7db95ab8ceac1f042626bc43a2f992 \ + --hash=sha256:f1e85a178384bf19e36779d91ff35c7617c885da487d689b05c1366f9933ad74 \ + --hash=sha256:f47be41843200f7faec0683ad751e5ef11b9a56a220d57f300376cd8aba81660 \ + --hash=sha256:fb0cef872d8193e487fc6bdb08559c3aa41b659a7d9be48b2e10747f47863925 \ + --hash=sha256:ffc73996c4fca3d2b6c1c8c12bfd3ad00def8621da24f547626bf06441400449 # via - # -r requirements.in # gcp-releasetool # secretstorage distlib==0.3.7 \ @@ -146,9 +145,9 @@ gcp-docuploader==0.6.5 \ --hash=sha256:30221d4ac3e5a2b9c69aa52fdbef68cc3f27d0e6d0d90e220fc024584b8d2318 \ --hash=sha256:b7458ef93f605b9d46a4bf3a8dc1755dad1f31d030c8679edf304e343b347eea # via -r requirements.in -gcp-releasetool==2.0.0 \ - --hash=sha256:3d73480b50ba243f22d7c7ec08b115a30e1c7817c4899781840c26f9c55b8277 \ - --hash=sha256:7aa9fd935ec61e581eb8458ad00823786d91756c25e492f372b2b30962f3c28f +gcp-releasetool==1.16.0 \ + --hash=sha256:27bf19d2e87aaa884096ff941aa3c592c482be3d6a2bfe6f06afafa6af2353e3 \ + --hash=sha256:a316b197a543fd036209d0caba7a8eb4d236d8e65381c80cbc6d7efaa7606d63 # via -r requirements.in google-api-core==2.12.0 \ --hash=sha256:c22e01b1e3c4dcd90998494879612c38d0a3411d1f7b679eb89e2abe3ce1f553 \ @@ -393,18 +392,29 @@ platformdirs==3.11.0 \ --hash=sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3 \ --hash=sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e # via virtualenv -protobuf==4.25.3 \ - --hash=sha256:19b270aeaa0099f16d3ca02628546b8baefe2955bbe23224aaf856134eccf1e4 \ - --hash=sha256:209ba4cc916bab46f64e56b85b090607a676f66b473e6b762e6f1d9d591eb2e8 \ - --hash=sha256:25b5d0b42fd000320bd7830b349e3b696435f3b329810427a6bcce6a5492cc5c \ - --hash=sha256:7c8daa26095f82482307bc717364e7c13f4f1c99659be82890dcfc215194554d \ - --hash=sha256:c053062984e61144385022e53678fbded7aea14ebb3e0305ae3592fb219ccfa4 \ - --hash=sha256:d4198877797a83cbfe9bffa3803602bbe1625dc30d8a097365dbc762e5790faa \ - --hash=sha256:e3c97a1555fd6388f857770ff8b9703083de6bf1f9274a002a332d65fbb56c8c \ - --hash=sha256:e7cb0ae90dd83727f0c0718634ed56837bfeeee29a5f82a7514c03ee1364c019 \ - --hash=sha256:f0700d54bcf45424477e46a9f0944155b46fb0639d69728739c0e47bab83f2b9 \ - --hash=sha256:f1279ab38ecbfae7e456a108c5c0681e4956d5b1090027c1de0f934dfdb4b35c \ - --hash=sha256:f4f118245c4a087776e0a8408be33cf09f6c547442c00395fbfb116fac2f8ac2 +protobuf==3.20.3 \ + --hash=sha256:03038ac1cfbc41aa21f6afcbcd357281d7521b4157926f30ebecc8d4ea59dcb7 \ + --hash=sha256:28545383d61f55b57cf4df63eebd9827754fd2dc25f80c5253f9184235db242c \ + --hash=sha256:2e3427429c9cffebf259491be0af70189607f365c2f41c7c3764af6f337105f2 \ + --hash=sha256:398a9e0c3eaceb34ec1aee71894ca3299605fa8e761544934378bbc6c97de23b \ + --hash=sha256:44246bab5dd4b7fbd3c0c80b6f16686808fab0e4aca819ade6e8d294a29c7050 \ + --hash=sha256:447d43819997825d4e71bf5769d869b968ce96848b6479397e29fc24c4a5dfe9 \ + --hash=sha256:67a3598f0a2dcbc58d02dd1928544e7d88f764b47d4a286202913f0b2801c2e7 \ + --hash=sha256:74480f79a023f90dc6e18febbf7b8bac7508420f2006fabd512013c0c238f454 \ + --hash=sha256:819559cafa1a373b7096a482b504ae8a857c89593cf3a25af743ac9ecbd23480 \ + --hash=sha256:899dc660cd599d7352d6f10d83c95df430a38b410c1b66b407a6b29265d66469 \ + --hash=sha256:8c0c984a1b8fef4086329ff8dd19ac77576b384079247c770f29cc8ce3afa06c \ + --hash=sha256:9aae4406ea63d825636cc11ffb34ad3379335803216ee3a856787bcf5ccc751e \ + --hash=sha256:a7ca6d488aa8ff7f329d4c545b2dbad8ac31464f1d8b1c87ad1346717731e4db \ + --hash=sha256:b6cc7ba72a8850621bfec987cb72623e703b7fe2b9127a161ce61e61558ad905 \ + --hash=sha256:bf01b5720be110540be4286e791db73f84a2b721072a3711efff6c324cdf074b \ + --hash=sha256:c02ce36ec760252242a33967d51c289fd0e1c0e6e5cc9397e2279177716add86 \ + --hash=sha256:d9e4432ff660d67d775c66ac42a67cf2453c27cb4d738fc22cb53b5d84c135d4 \ + --hash=sha256:daa564862dd0d39c00f8086f88700fdbe8bc717e993a21e90711acfed02f2402 \ + --hash=sha256:de78575669dddf6099a8a0f46a27e82a1783c557ccc38ee620ed8cc96d3be7d7 \ + --hash=sha256:e64857f395505ebf3d2569935506ae0dfc4a15cb80dc25261176c784662cdcc4 \ + --hash=sha256:f4bd856d702e5b0d96a00ec6b307b0f51c1982c2bf9c0052cf9019e9a544ba99 \ + --hash=sha256:f4c42102bc82a51108e449cbb32b19b180022941c727bac0cfd50170341f16ee # via # gcp-docuploader # gcp-releasetool @@ -508,7 +518,7 @@ zipp==3.17.0 \ # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: -setuptools==69.2.0 \ - --hash=sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e \ - --hash=sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c +setuptools==68.2.2 \ + --hash=sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87 \ + --hash=sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a # via -r requirements.in diff --git a/CHANGELOG.md b/CHANGELOG.md index 28cc08a05..b1643772e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ [1]: https://pypi.org/project/google-api-core/#history +## [2.18.0](https://github.com/googleapis/python-api-core/compare/v2.17.1...v2.18.0) (2024-03-20) + + +### Features + +* Add common logic for supporting universe domain ([#621](https://github.com/googleapis/python-api-core/issues/621)) ([94f2ca3](https://github.com/googleapis/python-api-core/commit/94f2ca3b4d094e6e10154634d3463d07ebea2035)) + + +### Bug Fixes + +* Add _registered_method to grpc ChannelStub ([#614](https://github.com/googleapis/python-api-core/issues/614)) ([5eaaea8](https://github.com/googleapis/python-api-core/commit/5eaaea8a989f8bdbdb5fbc95a155a20837c87f42)) +* **deps:** Require proto-plus >= 1.22.3 ([#626](https://github.com/googleapis/python-api-core/issues/626)) ([4fed37c](https://github.com/googleapis/python-api-core/commit/4fed37cbc32122f156e38250b5fa8b2b08a787a1)) + ## [2.17.1](https://github.com/googleapis/python-api-core/compare/v2.17.0...v2.17.1) (2024-02-13) diff --git a/google/api_core/version.py b/google/api_core/version.py index 3fd9f0007..a613e5ea2 100644 --- a/google/api_core/version.py +++ b/google/api_core/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "2.17.1" +__version__ = "2.18.0" From c38dd1e5adad6ba0057e86bd93985989f9139683 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Sun, 7 Apr 2024 23:54:48 -0400 Subject: [PATCH 004/101] docs: fix docs session (#636) Source-Link: https://github.com/googleapis/synthtool/commit/223f39e29577145d4238a522633c2f3e5e6dc8dc Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-python:latest@sha256:8244c1253becbaa533f48724a6348e4b92a10df4b4dfb66d87e615e633059bdf Co-authored-by: Owl Bot --- .github/.OwlBot.lock.yaml | 4 +- .github/auto-label.yaml | 5 ++ .github/blunderbuss.yml | 17 ++++++ .kokoro/requirements.in | 3 +- .kokoro/requirements.txt | 114 +++++++++++++++++--------------------- 5 files changed, 78 insertions(+), 65 deletions(-) create mode 100644 .github/blunderbuss.yml diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 5d9542b1c..ee2c6d1f3 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest - digest: sha256:3741fd1f5f5150378563c76afa06bcc12777b5fe54c5ee01115218f83872134f -# created: 2024-03-15T16:26:15.743347415Z + digest: sha256:8244c1253becbaa533f48724a6348e4b92a10df4b4dfb66d87e615e633059bdf +# created: 2024-04-07T11:43:40.730565127Z diff --git a/.github/auto-label.yaml b/.github/auto-label.yaml index b2016d119..8b37ee897 100644 --- a/.github/auto-label.yaml +++ b/.github/auto-label.yaml @@ -13,3 +13,8 @@ # limitations under the License. requestsize: enabled: true + +path: + pullrequest: true + paths: + samples: "samples" diff --git a/.github/blunderbuss.yml b/.github/blunderbuss.yml new file mode 100644 index 000000000..1618464d1 --- /dev/null +++ b/.github/blunderbuss.yml @@ -0,0 +1,17 @@ +# Blunderbuss config +# +# This file controls who is assigned for pull requests and issues. +# Note: This file is autogenerated. To make changes to the assignee +# team, please update `codeowner_team` in `.repo-metadata.json`. +assign_issues: + - googleapis/actools-python + +assign_issues_by: + - labels: + - "samples" + to: + - googleapis/python-samples-reviewers + - googleapis/actools-python + +assign_prs: + - googleapis/actools-python diff --git a/.kokoro/requirements.in b/.kokoro/requirements.in index ec867d9fd..fff4d9ce0 100644 --- a/.kokoro/requirements.in +++ b/.kokoro/requirements.in @@ -1,5 +1,5 @@ gcp-docuploader -gcp-releasetool>=1.10.5 # required for compatibility with cryptography>=39.x +gcp-releasetool>=2 # required for compatibility with cryptography>=42.x importlib-metadata typing-extensions twine @@ -8,3 +8,4 @@ setuptools nox>=2022.11.21 # required to remove dependency on py charset-normalizer<3 click<8.1.0 +cryptography>=42.0.5 diff --git a/.kokoro/requirements.txt b/.kokoro/requirements.txt index bda8e38c4..dd61f5f32 100644 --- a/.kokoro/requirements.txt +++ b/.kokoro/requirements.txt @@ -93,40 +93,41 @@ colorlog==6.7.0 \ # via # gcp-docuploader # nox -cryptography==42.0.4 \ - --hash=sha256:01911714117642a3f1792c7f376db572aadadbafcd8d75bb527166009c9f1d1b \ - --hash=sha256:0e89f7b84f421c56e7ff69f11c441ebda73b8a8e6488d322ef71746224c20fce \ - --hash=sha256:12d341bd42cdb7d4937b0cabbdf2a94f949413ac4504904d0cdbdce4a22cbf88 \ - --hash=sha256:15a1fb843c48b4a604663fa30af60818cd28f895572386e5f9b8a665874c26e7 \ - --hash=sha256:1cdcdbd117681c88d717437ada72bdd5be9de117f96e3f4d50dab3f59fd9ab20 \ - --hash=sha256:1df6fcbf60560d2113b5ed90f072dc0b108d64750d4cbd46a21ec882c7aefce9 \ - --hash=sha256:3c6048f217533d89f2f8f4f0fe3044bf0b2090453b7b73d0b77db47b80af8dff \ - --hash=sha256:3e970a2119507d0b104f0a8e281521ad28fc26f2820687b3436b8c9a5fcf20d1 \ - --hash=sha256:44a64043f743485925d3bcac548d05df0f9bb445c5fcca6681889c7c3ab12764 \ - --hash=sha256:4e36685cb634af55e0677d435d425043967ac2f3790ec652b2b88ad03b85c27b \ - --hash=sha256:5f8907fcf57392cd917892ae83708761c6ff3c37a8e835d7246ff0ad251d9298 \ - --hash=sha256:69b22ab6506a3fe483d67d1ed878e1602bdd5912a134e6202c1ec672233241c1 \ - --hash=sha256:6bfadd884e7280df24d26f2186e4e07556a05d37393b0f220a840b083dc6a824 \ - --hash=sha256:6d0fbe73728c44ca3a241eff9aefe6496ab2656d6e7a4ea2459865f2e8613257 \ - --hash=sha256:6ffb03d419edcab93b4b19c22ee80c007fb2d708429cecebf1dd3258956a563a \ - --hash=sha256:810bcf151caefc03e51a3d61e53335cd5c7316c0a105cc695f0959f2c638b129 \ - --hash=sha256:831a4b37accef30cccd34fcb916a5d7b5be3cbbe27268a02832c3e450aea39cb \ - --hash=sha256:887623fe0d70f48ab3f5e4dbf234986b1329a64c066d719432d0698522749929 \ - --hash=sha256:a0298bdc6e98ca21382afe914c642620370ce0470a01e1bef6dd9b5354c36854 \ - --hash=sha256:a1327f280c824ff7885bdeef8578f74690e9079267c1c8bd7dc5cc5aa065ae52 \ - --hash=sha256:c1f25b252d2c87088abc8bbc4f1ecbf7c919e05508a7e8628e6875c40bc70923 \ - --hash=sha256:c3a5cbc620e1e17009f30dd34cb0d85c987afd21c41a74352d1719be33380885 \ - --hash=sha256:ce8613beaffc7c14f091497346ef117c1798c202b01153a8cc7b8e2ebaaf41c0 \ - --hash=sha256:d2a27aca5597c8a71abbe10209184e1a8e91c1fd470b5070a2ea60cafec35bcd \ - --hash=sha256:dad9c385ba8ee025bb0d856714f71d7840020fe176ae0229de618f14dae7a6e2 \ - --hash=sha256:db4b65b02f59035037fde0998974d84244a64c3265bdef32a827ab9b63d61b18 \ - --hash=sha256:e09469a2cec88fb7b078e16d4adec594414397e8879a4341c6ace96013463d5b \ - --hash=sha256:e53dc41cda40b248ebc40b83b31516487f7db95ab8ceac1f042626bc43a2f992 \ - --hash=sha256:f1e85a178384bf19e36779d91ff35c7617c885da487d689b05c1366f9933ad74 \ - --hash=sha256:f47be41843200f7faec0683ad751e5ef11b9a56a220d57f300376cd8aba81660 \ - --hash=sha256:fb0cef872d8193e487fc6bdb08559c3aa41b659a7d9be48b2e10747f47863925 \ - --hash=sha256:ffc73996c4fca3d2b6c1c8c12bfd3ad00def8621da24f547626bf06441400449 +cryptography==42.0.5 \ + --hash=sha256:0270572b8bd2c833c3981724b8ee9747b3ec96f699a9665470018594301439ee \ + --hash=sha256:111a0d8553afcf8eb02a4fea6ca4f59d48ddb34497aa8706a6cf536f1a5ec576 \ + --hash=sha256:16a48c23a62a2f4a285699dba2e4ff2d1cff3115b9df052cdd976a18856d8e3d \ + --hash=sha256:1b95b98b0d2af784078fa69f637135e3c317091b615cd0905f8b8a087e86fa30 \ + --hash=sha256:1f71c10d1e88467126f0efd484bd44bca5e14c664ec2ede64c32f20875c0d413 \ + --hash=sha256:2424ff4c4ac7f6b8177b53c17ed5d8fa74ae5955656867f5a8affaca36a27abb \ + --hash=sha256:2bce03af1ce5a5567ab89bd90d11e7bbdff56b8af3acbbec1faded8f44cb06da \ + --hash=sha256:329906dcc7b20ff3cad13c069a78124ed8247adcac44b10bea1130e36caae0b4 \ + --hash=sha256:37dd623507659e08be98eec89323469e8c7b4c1407c85112634ae3dbdb926fdd \ + --hash=sha256:3eaafe47ec0d0ffcc9349e1708be2aaea4c6dd4978d76bf6eb0cb2c13636c6fc \ + --hash=sha256:5e6275c09d2badf57aea3afa80d975444f4be8d3bc58f7f80d2a484c6f9485c8 \ + --hash=sha256:6fe07eec95dfd477eb9530aef5bead34fec819b3aaf6c5bd6d20565da607bfe1 \ + --hash=sha256:7367d7b2eca6513681127ebad53b2582911d1736dc2ffc19f2c3ae49997496bc \ + --hash=sha256:7cde5f38e614f55e28d831754e8a3bacf9ace5d1566235e39d91b35502d6936e \ + --hash=sha256:9481ffe3cf013b71b2428b905c4f7a9a4f76ec03065b05ff499bb5682a8d9ad8 \ + --hash=sha256:98d8dc6d012b82287f2c3d26ce1d2dd130ec200c8679b6213b3c73c08b2b7940 \ + --hash=sha256:a011a644f6d7d03736214d38832e030d8268bcff4a41f728e6030325fea3e400 \ + --hash=sha256:a2913c5375154b6ef2e91c10b5720ea6e21007412f6437504ffea2109b5a33d7 \ + --hash=sha256:a30596bae9403a342c978fb47d9b0ee277699fa53bbafad14706af51fe543d16 \ + --hash=sha256:b03c2ae5d2f0fc05f9a2c0c997e1bc18c8229f392234e8a0194f202169ccd278 \ + --hash=sha256:b6cd2203306b63e41acdf39aa93b86fb566049aeb6dc489b70e34bcd07adca74 \ + --hash=sha256:b7ffe927ee6531c78f81aa17e684e2ff617daeba7f189f911065b2ea2d526dec \ + --hash=sha256:b8cac287fafc4ad485b8a9b67d0ee80c66bf3574f655d3b97ef2e1082360faf1 \ + --hash=sha256:ba334e6e4b1d92442b75ddacc615c5476d4ad55cc29b15d590cc6b86efa487e2 \ + --hash=sha256:ba3e4a42397c25b7ff88cdec6e2a16c2be18720f317506ee25210f6d31925f9c \ + --hash=sha256:c41fb5e6a5fe9ebcd58ca3abfeb51dffb5d83d6775405305bfa8715b76521922 \ + --hash=sha256:cd2030f6650c089aeb304cf093f3244d34745ce0cfcc39f20c6fbfe030102e2a \ + --hash=sha256:cd65d75953847815962c84a4654a84850b2bb4aed3f26fadcc1c13892e1e29f6 \ + --hash=sha256:e4985a790f921508f36f81831817cbc03b102d643b5fcb81cd33df3fa291a1a1 \ + --hash=sha256:e807b3188f9eb0eaa7bbb579b462c5ace579f1cedb28107ce8b48a9f7ad3679e \ + --hash=sha256:f12764b8fffc7a123f641d7d049d382b73f96a34117e0b637b80643169cec8ac \ + --hash=sha256:f8837fe1d6ac4a8052a9a8ddab256bc006242696f03368a4009be7ee3075cdb7 # via + # -r requirements.in # gcp-releasetool # secretstorage distlib==0.3.7 \ @@ -145,9 +146,9 @@ gcp-docuploader==0.6.5 \ --hash=sha256:30221d4ac3e5a2b9c69aa52fdbef68cc3f27d0e6d0d90e220fc024584b8d2318 \ --hash=sha256:b7458ef93f605b9d46a4bf3a8dc1755dad1f31d030c8679edf304e343b347eea # via -r requirements.in -gcp-releasetool==1.16.0 \ - --hash=sha256:27bf19d2e87aaa884096ff941aa3c592c482be3d6a2bfe6f06afafa6af2353e3 \ - --hash=sha256:a316b197a543fd036209d0caba7a8eb4d236d8e65381c80cbc6d7efaa7606d63 +gcp-releasetool==2.0.0 \ + --hash=sha256:3d73480b50ba243f22d7c7ec08b115a30e1c7817c4899781840c26f9c55b8277 \ + --hash=sha256:7aa9fd935ec61e581eb8458ad00823786d91756c25e492f372b2b30962f3c28f # via -r requirements.in google-api-core==2.12.0 \ --hash=sha256:c22e01b1e3c4dcd90998494879612c38d0a3411d1f7b679eb89e2abe3ce1f553 \ @@ -392,29 +393,18 @@ platformdirs==3.11.0 \ --hash=sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3 \ --hash=sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e # via virtualenv -protobuf==3.20.3 \ - --hash=sha256:03038ac1cfbc41aa21f6afcbcd357281d7521b4157926f30ebecc8d4ea59dcb7 \ - --hash=sha256:28545383d61f55b57cf4df63eebd9827754fd2dc25f80c5253f9184235db242c \ - --hash=sha256:2e3427429c9cffebf259491be0af70189607f365c2f41c7c3764af6f337105f2 \ - --hash=sha256:398a9e0c3eaceb34ec1aee71894ca3299605fa8e761544934378bbc6c97de23b \ - --hash=sha256:44246bab5dd4b7fbd3c0c80b6f16686808fab0e4aca819ade6e8d294a29c7050 \ - --hash=sha256:447d43819997825d4e71bf5769d869b968ce96848b6479397e29fc24c4a5dfe9 \ - --hash=sha256:67a3598f0a2dcbc58d02dd1928544e7d88f764b47d4a286202913f0b2801c2e7 \ - --hash=sha256:74480f79a023f90dc6e18febbf7b8bac7508420f2006fabd512013c0c238f454 \ - --hash=sha256:819559cafa1a373b7096a482b504ae8a857c89593cf3a25af743ac9ecbd23480 \ - --hash=sha256:899dc660cd599d7352d6f10d83c95df430a38b410c1b66b407a6b29265d66469 \ - --hash=sha256:8c0c984a1b8fef4086329ff8dd19ac77576b384079247c770f29cc8ce3afa06c \ - --hash=sha256:9aae4406ea63d825636cc11ffb34ad3379335803216ee3a856787bcf5ccc751e \ - --hash=sha256:a7ca6d488aa8ff7f329d4c545b2dbad8ac31464f1d8b1c87ad1346717731e4db \ - --hash=sha256:b6cc7ba72a8850621bfec987cb72623e703b7fe2b9127a161ce61e61558ad905 \ - --hash=sha256:bf01b5720be110540be4286e791db73f84a2b721072a3711efff6c324cdf074b \ - --hash=sha256:c02ce36ec760252242a33967d51c289fd0e1c0e6e5cc9397e2279177716add86 \ - --hash=sha256:d9e4432ff660d67d775c66ac42a67cf2453c27cb4d738fc22cb53b5d84c135d4 \ - --hash=sha256:daa564862dd0d39c00f8086f88700fdbe8bc717e993a21e90711acfed02f2402 \ - --hash=sha256:de78575669dddf6099a8a0f46a27e82a1783c557ccc38ee620ed8cc96d3be7d7 \ - --hash=sha256:e64857f395505ebf3d2569935506ae0dfc4a15cb80dc25261176c784662cdcc4 \ - --hash=sha256:f4bd856d702e5b0d96a00ec6b307b0f51c1982c2bf9c0052cf9019e9a544ba99 \ - --hash=sha256:f4c42102bc82a51108e449cbb32b19b180022941c727bac0cfd50170341f16ee +protobuf==4.25.3 \ + --hash=sha256:19b270aeaa0099f16d3ca02628546b8baefe2955bbe23224aaf856134eccf1e4 \ + --hash=sha256:209ba4cc916bab46f64e56b85b090607a676f66b473e6b762e6f1d9d591eb2e8 \ + --hash=sha256:25b5d0b42fd000320bd7830b349e3b696435f3b329810427a6bcce6a5492cc5c \ + --hash=sha256:7c8daa26095f82482307bc717364e7c13f4f1c99659be82890dcfc215194554d \ + --hash=sha256:c053062984e61144385022e53678fbded7aea14ebb3e0305ae3592fb219ccfa4 \ + --hash=sha256:d4198877797a83cbfe9bffa3803602bbe1625dc30d8a097365dbc762e5790faa \ + --hash=sha256:e3c97a1555fd6388f857770ff8b9703083de6bf1f9274a002a332d65fbb56c8c \ + --hash=sha256:e7cb0ae90dd83727f0c0718634ed56837bfeeee29a5f82a7514c03ee1364c019 \ + --hash=sha256:f0700d54bcf45424477e46a9f0944155b46fb0639d69728739c0e47bab83f2b9 \ + --hash=sha256:f1279ab38ecbfae7e456a108c5c0681e4956d5b1090027c1de0f934dfdb4b35c \ + --hash=sha256:f4f118245c4a087776e0a8408be33cf09f6c547442c00395fbfb116fac2f8ac2 # via # gcp-docuploader # gcp-releasetool @@ -518,7 +508,7 @@ zipp==3.17.0 \ # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: -setuptools==68.2.2 \ - --hash=sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87 \ - --hash=sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a +setuptools==69.2.0 \ + --hash=sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e \ + --hash=sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c # via -r requirements.in From 828ffe1d7b2edd36be5e852524adb7f5d1241770 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Fri, 12 Apr 2024 16:39:32 -0400 Subject: [PATCH 005/101] chore(python): bump idna from 3.4 to 3.7 in .kokoro (#640) Source-Link: https://github.com/googleapis/synthtool/commit/d50980e704793a2d3310bfb3664f3a82f24b5796 Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-python:latest@sha256:5a4c19d17e597b92d786e569be101e636c9c2817731f80a5adec56b2aa8fe070 Co-authored-by: Owl Bot --- .github/.OwlBot.lock.yaml | 4 ++-- .kokoro/requirements.txt | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index ee2c6d1f3..81f87c569 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest - digest: sha256:8244c1253becbaa533f48724a6348e4b92a10df4b4dfb66d87e615e633059bdf -# created: 2024-04-07T11:43:40.730565127Z + digest: sha256:5a4c19d17e597b92d786e569be101e636c9c2817731f80a5adec56b2aa8fe070 +# created: 2024-04-12T11:35:58.922854369Z diff --git a/.kokoro/requirements.txt b/.kokoro/requirements.txt index dd61f5f32..51f92b8e1 100644 --- a/.kokoro/requirements.txt +++ b/.kokoro/requirements.txt @@ -252,9 +252,9 @@ googleapis-common-protos==1.61.0 \ --hash=sha256:22f1915393bb3245343f6efe87f6fe868532efc12aa26b391b15132e1279f1c0 \ --hash=sha256:8a64866a97f6304a7179873a465d6eee97b7a24ec6cfd78e0f575e96b821240b # via google-api-core -idna==3.4 \ - --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \ - --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2 +idna==3.7 \ + --hash=sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc \ + --hash=sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0 # via requests importlib-metadata==6.8.0 \ --hash=sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb \ From 07dfc93c6d5b2166a0273d49b20ad70f071bf2a8 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Mon, 29 Apr 2024 16:35:46 +0200 Subject: [PATCH 006/101] chore(deps): update all dependencies (#634) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(deps): update all dependencies * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * See https://github.com/googleapis/synthtool/pull/1910/files * fix artifact name * remove space * See https://github.com/googleapis/python-api-core/issues/642 --------- Co-authored-by: Owl Bot Co-authored-by: Anthonios Partheniou --- .github/workflows/unittest.yml | 10 +++++----- noxfile.py | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 2cfaada36..a8eef6142 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -43,9 +43,9 @@ jobs: run: | nox -s unit${{ matrix.option }}-${{ matrix.python }} - name: Upload coverage results - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: coverage-artifacts + name: coverage-artifact-${{ matrix.option }}-${{ matrix.python }} path: .coverage${{ matrix.option }}-${{ matrix.python }} report-coverage: @@ -65,11 +65,11 @@ jobs: python -m pip install --upgrade setuptools pip wheel python -m pip install coverage - name: Download coverage results - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: - name: coverage-artifacts path: .coverage-results/ - name: Report coverage results run: | - coverage combine .coverage-results/.coverage* + find .coverage-results -type f -name '*.zip' -exec unzip {} \; + coverage combine .coverage-results/**/.coverage* coverage report --show-missing --fail-under=100 diff --git a/noxfile.py b/noxfile.py index 2c7ec6c72..8fbcaec0a 100644 --- a/noxfile.py +++ b/noxfile.py @@ -175,12 +175,12 @@ def pytype(session): def mypy(session): """Run type-checking.""" session.install(".[grpc]", "mypy") - # Exclude types-protobuf==4.24.0.20240106 - # See https://github.com/python/typeshed/issues/11254 session.install( "types-setuptools", "types-requests", - "types-protobuf!=4.24.0.20240106", + # TODO(https://github.com/googleapis/python-api-core/issues/642): + # Use the latest version of types-protobuf. + "types-protobuf<5", "types-mock", "types-dataclasses", ) From a7b53e9e9a7deb88baf92a2827958429e3677069 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Mon, 29 Apr 2024 16:41:52 -0400 Subject: [PATCH 007/101] feat: add google.api_core.version_header (#638) * feat: add google.api_core.gapic_v1.version_header * fix mypy * clarify comment --- google/api_core/version_header.py | 29 +++++++++++++++++++++++++++++ tests/unit/test_version_header.py | 23 +++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 google/api_core/version_header.py create mode 100644 tests/unit/test_version_header.py diff --git a/google/api_core/version_header.py b/google/api_core/version_header.py new file mode 100644 index 000000000..cf1972aca --- /dev/null +++ b/google/api_core/version_header.py @@ -0,0 +1,29 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +API_VERSION_METADATA_KEY = "x-goog-api-version" + + +def to_api_version_header(version_identifier): + """Returns data for the API Version header for the given `version_identifier`. + + Args: + version_identifier (str): The version identifier to be used in the + tuple returned. + + Returns: + Tuple(str, str): A tuple containing the API Version metadata key and + value. + """ + return (API_VERSION_METADATA_KEY, version_identifier) diff --git a/tests/unit/test_version_header.py b/tests/unit/test_version_header.py new file mode 100644 index 000000000..ea7028e27 --- /dev/null +++ b/tests/unit/test_version_header.py @@ -0,0 +1,23 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +from google.api_core import version_header + + +@pytest.mark.parametrize("version_identifier", ["some_value", ""]) +def test_to_api_version_header(version_identifier): + value = version_header.to_api_version_header(version_identifier) + assert value == (version_header.API_VERSION_METADATA_KEY, version_identifier) From 7d874628a525afa3b7d7e21d0dcbe8f9906068b1 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 30 Apr 2024 09:59:12 -0700 Subject: [PATCH 008/101] chore(main): release 2.19.0 (#646) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ google/api_core/version.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1643772e..9e64d3570 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://pypi.org/project/google-api-core/#history +## [2.19.0](https://github.com/googleapis/python-api-core/compare/v2.18.0...v2.19.0) (2024-04-29) + + +### Features + +* Add google.api_core.version_header ([#638](https://github.com/googleapis/python-api-core/issues/638)) ([a7b53e9](https://github.com/googleapis/python-api-core/commit/a7b53e9e9a7deb88baf92a2827958429e3677069)) + ## [2.18.0](https://github.com/googleapis/python-api-core/compare/v2.17.1...v2.18.0) (2024-03-20) diff --git a/google/api_core/version.py b/google/api_core/version.py index a613e5ea2..2605c08a3 100644 --- a/google/api_core/version.py +++ b/google/api_core/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "2.18.0" +__version__ = "2.19.0" From ab22afdf311a2d87493c29833b35ef3b3ca8f246 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Fri, 3 May 2024 13:23:48 -0600 Subject: [PATCH 009/101] chore: avoid checking instance on each stream call (#529) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: avoid checking instance on each stream call * fixed indentation * added check for unary call * fixed type check * fixed tests * fixed coverage * added exception to test class * added comment to test * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: Owl Bot --- google/api_core/grpc_helpers_async.py | 24 +++----- tests/asyncio/test_grpc_helpers_async.py | 78 +++++++++++++++++++----- 2 files changed, 73 insertions(+), 29 deletions(-) diff --git a/google/api_core/grpc_helpers_async.py b/google/api_core/grpc_helpers_async.py index 9423d2b6c..718b5f059 100644 --- a/google/api_core/grpc_helpers_async.py +++ b/google/api_core/grpc_helpers_async.py @@ -159,7 +159,6 @@ class _WrappedStreamStreamCall( def _wrap_unary_errors(callable_): """Map errors for Unary-Unary async callables.""" - grpc_helpers._patch_callable_name(callable_) @functools.wraps(callable_) def error_remapped_callable(*args, **kwargs): @@ -169,23 +168,13 @@ def error_remapped_callable(*args, **kwargs): return error_remapped_callable -def _wrap_stream_errors(callable_): +def _wrap_stream_errors(callable_, wrapper_type): """Map errors for streaming RPC async callables.""" - grpc_helpers._patch_callable_name(callable_) @functools.wraps(callable_) async def error_remapped_callable(*args, **kwargs): call = callable_(*args, **kwargs) - - if isinstance(call, aio.UnaryStreamCall): - call = _WrappedUnaryStreamCall().with_call(call) - elif isinstance(call, aio.StreamUnaryCall): - call = _WrappedStreamUnaryCall().with_call(call) - elif isinstance(call, aio.StreamStreamCall): - call = _WrappedStreamStreamCall().with_call(call) - else: - raise TypeError("Unexpected type of call %s" % type(call)) - + call = wrapper_type().with_call(call) await call.wait_for_connection() return call @@ -207,10 +196,17 @@ def wrap_errors(callable_): Returns: Callable: The wrapped gRPC callable. """ + grpc_helpers._patch_callable_name(callable_) if isinstance(callable_, aio.UnaryUnaryMultiCallable): return _wrap_unary_errors(callable_) + elif isinstance(callable_, aio.UnaryStreamMultiCallable): + return _wrap_stream_errors(callable_, _WrappedUnaryStreamCall) + elif isinstance(callable_, aio.StreamUnaryMultiCallable): + return _wrap_stream_errors(callable_, _WrappedStreamUnaryCall) + elif isinstance(callable_, aio.StreamStreamMultiCallable): + return _wrap_stream_errors(callable_, _WrappedStreamStreamCall) else: - return _wrap_stream_errors(callable_) + raise TypeError("Unexpected type of callable: {}".format(type(callable_))) def create_channel( diff --git a/tests/asyncio/test_grpc_helpers_async.py b/tests/asyncio/test_grpc_helpers_async.py index 6bde59cae..6e08f10a6 100644 --- a/tests/asyncio/test_grpc_helpers_async.py +++ b/tests/asyncio/test_grpc_helpers_async.py @@ -97,12 +97,40 @@ async def test_common_methods_in_wrapped_call(): assert mock_call.wait_for_connection.call_count == 1 +@pytest.mark.asyncio +@pytest.mark.parametrize( + "callable_type,expected_wrapper_type", + [ + (grpc.aio.UnaryStreamMultiCallable, grpc_helpers_async._WrappedUnaryStreamCall), + (grpc.aio.StreamUnaryMultiCallable, grpc_helpers_async._WrappedStreamUnaryCall), + ( + grpc.aio.StreamStreamMultiCallable, + grpc_helpers_async._WrappedStreamStreamCall, + ), + ], +) +async def test_wrap_errors_w_stream_type(callable_type, expected_wrapper_type): + class ConcreteMulticallable(callable_type): + def __call__(self, *args, **kwargs): + raise NotImplementedError("Should not be called") + + with mock.patch.object( + grpc_helpers_async, "_wrap_stream_errors" + ) as wrap_stream_errors: + callable_ = ConcreteMulticallable() + grpc_helpers_async.wrap_errors(callable_) + assert wrap_stream_errors.call_count == 1 + wrap_stream_errors.assert_called_once_with(callable_, expected_wrapper_type) + + @pytest.mark.asyncio async def test_wrap_stream_errors_unary_stream(): mock_call = mock.Mock(aio.UnaryStreamCall, autospec=True) multicallable = mock.Mock(return_value=mock_call) - wrapped_callable = grpc_helpers_async._wrap_stream_errors(multicallable) + wrapped_callable = grpc_helpers_async._wrap_stream_errors( + multicallable, grpc_helpers_async._WrappedUnaryStreamCall + ) await wrapped_callable(1, 2, three="four") multicallable.assert_called_once_with(1, 2, three="four") @@ -114,7 +142,9 @@ async def test_wrap_stream_errors_stream_unary(): mock_call = mock.Mock(aio.StreamUnaryCall, autospec=True) multicallable = mock.Mock(return_value=mock_call) - wrapped_callable = grpc_helpers_async._wrap_stream_errors(multicallable) + wrapped_callable = grpc_helpers_async._wrap_stream_errors( + multicallable, grpc_helpers_async._WrappedStreamUnaryCall + ) await wrapped_callable(1, 2, three="four") multicallable.assert_called_once_with(1, 2, three="four") @@ -126,7 +156,9 @@ async def test_wrap_stream_errors_stream_stream(): mock_call = mock.Mock(aio.StreamStreamCall, autospec=True) multicallable = mock.Mock(return_value=mock_call) - wrapped_callable = grpc_helpers_async._wrap_stream_errors(multicallable) + wrapped_callable = grpc_helpers_async._wrap_stream_errors( + multicallable, grpc_helpers_async._WrappedStreamStreamCall + ) await wrapped_callable(1, 2, three="four") multicallable.assert_called_once_with(1, 2, three="four") @@ -134,14 +166,16 @@ async def test_wrap_stream_errors_stream_stream(): @pytest.mark.asyncio -async def test_wrap_stream_errors_type_error(): +async def test_wrap_errors_type_error(): + """ + If wrap_errors is called with an unexpected type, it should raise a TypeError. + """ mock_call = mock.Mock() multicallable = mock.Mock(return_value=mock_call) - wrapped_callable = grpc_helpers_async._wrap_stream_errors(multicallable) - - with pytest.raises(TypeError): - await wrapped_callable() + with pytest.raises(TypeError) as exc: + grpc_helpers_async.wrap_errors(multicallable) + assert "Unexpected type" in str(exc.value) @pytest.mark.asyncio @@ -151,7 +185,9 @@ async def test_wrap_stream_errors_raised(): mock_call.wait_for_connection = mock.AsyncMock(side_effect=[grpc_error]) multicallable = mock.Mock(return_value=mock_call) - wrapped_callable = grpc_helpers_async._wrap_stream_errors(multicallable) + wrapped_callable = grpc_helpers_async._wrap_stream_errors( + multicallable, grpc_helpers_async._WrappedStreamStreamCall + ) with pytest.raises(exceptions.InvalidArgument): await wrapped_callable() @@ -166,7 +202,9 @@ async def test_wrap_stream_errors_read(): mock_call.read = mock.AsyncMock(side_effect=grpc_error) multicallable = mock.Mock(return_value=mock_call) - wrapped_callable = grpc_helpers_async._wrap_stream_errors(multicallable) + wrapped_callable = grpc_helpers_async._wrap_stream_errors( + multicallable, grpc_helpers_async._WrappedStreamStreamCall + ) wrapped_call = await wrapped_callable(1, 2, three="four") multicallable.assert_called_once_with(1, 2, three="four") @@ -189,7 +227,9 @@ async def test_wrap_stream_errors_aiter(): mock_call.__aiter__ = mock.Mock(return_value=mocked_aiter) multicallable = mock.Mock(return_value=mock_call) - wrapped_callable = grpc_helpers_async._wrap_stream_errors(multicallable) + wrapped_callable = grpc_helpers_async._wrap_stream_errors( + multicallable, grpc_helpers_async._WrappedStreamStreamCall + ) wrapped_call = await wrapped_callable() with pytest.raises(exceptions.InvalidArgument) as exc_info: @@ -210,7 +250,9 @@ async def test_wrap_stream_errors_aiter_non_rpc_error(): mock_call.__aiter__ = mock.Mock(return_value=mocked_aiter) multicallable = mock.Mock(return_value=mock_call) - wrapped_callable = grpc_helpers_async._wrap_stream_errors(multicallable) + wrapped_callable = grpc_helpers_async._wrap_stream_errors( + multicallable, grpc_helpers_async._WrappedStreamStreamCall + ) wrapped_call = await wrapped_callable() with pytest.raises(TypeError) as exc_info: @@ -224,7 +266,9 @@ async def test_wrap_stream_errors_aiter_called_multiple_times(): mock_call = mock.Mock(aio.StreamStreamCall, autospec=True) multicallable = mock.Mock(return_value=mock_call) - wrapped_callable = grpc_helpers_async._wrap_stream_errors(multicallable) + wrapped_callable = grpc_helpers_async._wrap_stream_errors( + multicallable, grpc_helpers_async._WrappedStreamStreamCall + ) wrapped_call = await wrapped_callable() assert wrapped_call.__aiter__() == wrapped_call.__aiter__() @@ -239,7 +283,9 @@ async def test_wrap_stream_errors_write(): mock_call.done_writing = mock.AsyncMock(side_effect=[None, grpc_error]) multicallable = mock.Mock(return_value=mock_call) - wrapped_callable = grpc_helpers_async._wrap_stream_errors(multicallable) + wrapped_callable = grpc_helpers_async._wrap_stream_errors( + multicallable, grpc_helpers_async._WrappedStreamStreamCall + ) wrapped_call = await wrapped_callable() @@ -295,7 +341,9 @@ def test_wrap_errors_streaming(wrap_stream_errors): result = grpc_helpers_async.wrap_errors(callable_) assert result == wrap_stream_errors.return_value - wrap_stream_errors.assert_called_once_with(callable_) + wrap_stream_errors.assert_called_once_with( + callable_, grpc_helpers_async._WrappedUnaryStreamCall + ) @pytest.mark.parametrize( From d96eb5cdd8120bfec97d62b09512c6fecc325be8 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Fri, 3 May 2024 14:05:28 -0600 Subject: [PATCH 010/101] feat: add caching to GapicCallable (#527) * feat: optimize _GapicCallable * cleaned up metadata lines * chore: avoid type checks in error wrapper * Revert "chore: avoid type checks in error wrapper" This reverts commit c97a6365028f3f04d20f26aa1cc0e3131164f53e. * add default wrapped function * fixed decorator order * fixed spacing * fixed comment typo * fixed spacing * fixed spacing * removed unneeded helpers * use caching * improved metadata parsing * improved docstring * fixed logic * added benchmark test * update threshold * run benchmark in loop for testing * use verbose logs * Revert testing * used smaller value * changed threshold * removed link in comment --- google/api_core/gapic_v1/method.py | 73 +++++++++++++----------------- tests/unit/gapic/test_method.py | 21 +++++++++ 2 files changed, 53 insertions(+), 41 deletions(-) diff --git a/google/api_core/gapic_v1/method.py b/google/api_core/gapic_v1/method.py index 0f14ea9c3..206549ea3 100644 --- a/google/api_core/gapic_v1/method.py +++ b/google/api_core/gapic_v1/method.py @@ -42,24 +42,6 @@ class _MethodDefault(enum.Enum): so the default should be used.""" -def _is_not_none_or_false(value): - return value is not None and value is not False - - -def _apply_decorators(func, decorators): - """Apply a list of decorators to a given function. - - ``decorators`` may contain items that are ``None`` or ``False`` which will - be ignored. - """ - filtered_decorators = filter(_is_not_none_or_false, reversed(decorators)) - - for decorator in filtered_decorators: - func = decorator(func) - - return func - - class _GapicCallable(object): """Callable that applies retry, timeout, and metadata logic. @@ -91,6 +73,8 @@ def __init__( ): self._target = target self._retry = retry + if isinstance(timeout, (int, float)): + timeout = TimeToDeadlineTimeout(timeout=timeout) self._timeout = timeout self._compression = compression self._metadata = metadata @@ -100,35 +84,42 @@ def __call__( ): """Invoke the low-level RPC with retry, timeout, compression, and metadata.""" - if retry is DEFAULT: - retry = self._retry - - if timeout is DEFAULT: - timeout = self._timeout - if compression is DEFAULT: compression = self._compression - - if isinstance(timeout, (int, float)): - timeout = TimeToDeadlineTimeout(timeout=timeout) - - # Apply all applicable decorators. - wrapped_func = _apply_decorators(self._target, [retry, timeout]) + if compression is not None: + kwargs["compression"] = compression # Add the user agent metadata to the call. if self._metadata is not None: - metadata = kwargs.get("metadata", []) - # Due to the nature of invocation, None should be treated the same - # as not specified. - if metadata is None: - metadata = [] - metadata = list(metadata) - metadata.extend(self._metadata) - kwargs["metadata"] = metadata - if self._compression is not None: - kwargs["compression"] = compression + try: + # attempt to concatenate default metadata with user-provided metadata + kwargs["metadata"] = (*kwargs["metadata"], *self._metadata) + except (KeyError, TypeError): + # if metadata is not provided, use just the default metadata + kwargs["metadata"] = self._metadata + + call = self._build_wrapped_call(timeout, retry) + return call(*args, **kwargs) + + @functools.lru_cache(maxsize=4) + def _build_wrapped_call(self, timeout, retry): + """ + Build a wrapped callable that applies retry, timeout, and metadata logic. + """ + wrapped_func = self._target + if timeout is DEFAULT: + timeout = self._timeout + elif isinstance(timeout, (int, float)): + timeout = TimeToDeadlineTimeout(timeout=timeout) + if timeout is not None: + wrapped_func = timeout(wrapped_func) + + if retry is DEFAULT: + retry = self._retry + if retry is not None: + wrapped_func = retry(wrapped_func) - return wrapped_func(*args, **kwargs) + return wrapped_func def wrap_method( diff --git a/tests/unit/gapic/test_method.py b/tests/unit/gapic/test_method.py index d966f478f..370da501d 100644 --- a/tests/unit/gapic/test_method.py +++ b/tests/unit/gapic/test_method.py @@ -222,3 +222,24 @@ def test_wrap_method_with_call_not_supported(): with pytest.raises(ValueError) as exc_info: google.api_core.gapic_v1.method.wrap_method(method, with_call=True) assert "with_call=True is only supported for unary calls" in str(exc_info.value) + + +def test_benchmark_gapic_call(): + """ + Ensure the __call__ method performance does not regress from expectations + + __call__ builds a new wrapped function using passed-in timeout and retry, but + subsequent calls are cached + + Note: The threshold has been tuned for the CI workers. Test may flake on + slower hardware + """ + from google.api_core.gapic_v1.method import _GapicCallable + from google.api_core.retry import Retry + from timeit import timeit + + gapic_callable = _GapicCallable( + lambda *a, **k: 1, retry=Retry(), timeout=1010, compression=False + ) + avg_time = timeit(lambda: gapic_callable(), number=10_000) + assert avg_time < 0.4 From c9e1cbb3297fc1878b85eb4c38919b5e9bb165bd Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Tue, 4 Jun 2024 04:40:25 -0700 Subject: [PATCH 011/101] chore: fallback to unary_unary when wrapping async callables (#653) * fix: fallback to unary_unary when wrapping async callables We recently [made a change](https://github.com/googleapis/python-api-core/commit/ab22afdf311a2d87493c29833b35ef3b3ca8f246) to move some computation in the async rpc wrapper from call-time to wrap-time. This way, individual calls would execute faster, since they don't have to re-compute some data on each call A side-effect of this change is that now some [type validation](https://github.com/googleapis/python-api-core/blob/d96eb5cdd8120bfec97d62b09512c6fecc325be8/google/api_core/grpc_helpers_async.py#L209) happens earlier. This caused some downstream tests to fail when a mock grpc channel is used. The wrapper doesn't know how to handle the mock.Mock type, and raises an exception while constructing the client object This PR fixes the issue by falling back to the unary wrapper when the callable type is unknown, rather than raising an exception. This is in-line with how [the sync version handles it](https://github.com/googleapis/python-api-core/blob/d96eb5cdd8120bfec97d62b09512c6fecc325be8/google/api_core/grpc_helpers.py#L198) * fixed elif to if * removed outdated test --- google/api_core/grpc_helpers_async.py | 7 +++---- tests/asyncio/test_grpc_helpers_async.py | 13 ------------- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/google/api_core/grpc_helpers_async.py b/google/api_core/grpc_helpers_async.py index 718b5f059..6feb22299 100644 --- a/google/api_core/grpc_helpers_async.py +++ b/google/api_core/grpc_helpers_async.py @@ -197,16 +197,15 @@ def wrap_errors(callable_): Returns: Callable: The wrapped gRPC callable. """ grpc_helpers._patch_callable_name(callable_) - if isinstance(callable_, aio.UnaryUnaryMultiCallable): - return _wrap_unary_errors(callable_) - elif isinstance(callable_, aio.UnaryStreamMultiCallable): + + if isinstance(callable_, aio.UnaryStreamMultiCallable): return _wrap_stream_errors(callable_, _WrappedUnaryStreamCall) elif isinstance(callable_, aio.StreamUnaryMultiCallable): return _wrap_stream_errors(callable_, _WrappedStreamUnaryCall) elif isinstance(callable_, aio.StreamStreamMultiCallable): return _wrap_stream_errors(callable_, _WrappedStreamStreamCall) else: - raise TypeError("Unexpected type of callable: {}".format(type(callable_))) + return _wrap_unary_errors(callable_) def create_channel( diff --git a/tests/asyncio/test_grpc_helpers_async.py b/tests/asyncio/test_grpc_helpers_async.py index 6e08f10a6..1a408ccde 100644 --- a/tests/asyncio/test_grpc_helpers_async.py +++ b/tests/asyncio/test_grpc_helpers_async.py @@ -165,19 +165,6 @@ async def test_wrap_stream_errors_stream_stream(): assert mock_call.wait_for_connection.call_count == 1 -@pytest.mark.asyncio -async def test_wrap_errors_type_error(): - """ - If wrap_errors is called with an unexpected type, it should raise a TypeError. - """ - mock_call = mock.Mock() - multicallable = mock.Mock(return_value=mock_call) - - with pytest.raises(TypeError) as exc: - grpc_helpers_async.wrap_errors(multicallable) - assert "Unexpected type" in str(exc.value) - - @pytest.mark.asyncio async def test_wrap_stream_errors_raised(): grpc_error = RpcErrorImpl(grpc.StatusCode.INVALID_ARGUMENT) From 1203fb97d2685535f89113e944c4764c1deb595e Mon Sep 17 00:00:00 2001 From: Mark Daoust Date: Tue, 4 Jun 2024 04:44:01 -0700 Subject: [PATCH 012/101] fix: Ignore unknown fields in rest streaming. (#651) * Ignore unknown fields in rest streaming. If the api adds a new field to a stremaing response, it shouldn't break old clients. We found this in the google.ai.generativelanguage API. Colab forces our clients to use rest, so all our streaming examples broke when the API team rolled out a new field. * format --------- Co-authored-by: Anthonios Partheniou --- google/api_core/rest_streaming.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/google/api_core/rest_streaming.py b/google/api_core/rest_streaming.py index 3f5b6b033..88bcb31bd 100644 --- a/google/api_core/rest_streaming.py +++ b/google/api_core/rest_streaming.py @@ -118,7 +118,9 @@ def __next__(self): def _grab(self): # Add extra quotes to make json.loads happy. if issubclass(self._response_message_cls, proto.Message): - return self._response_message_cls.from_json(self._ready_objs.popleft()) + return self._response_message_cls.from_json( + self._ready_objs.popleft(), ignore_unknown_fields=True + ) elif issubclass(self._response_message_cls, google.protobuf.message.Message): return Parse(self._ready_objs.popleft(), self._response_message_cls()) else: From ac098a747a6ee09d70479ae5b73479172ad6cd78 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Tue, 4 Jun 2024 04:47:13 -0700 Subject: [PATCH 013/101] chore: move retry async check to wrap time (#649) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: move retry async check to wrap time * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * added no cover mark --------- Co-authored-by: Owl Bot Co-authored-by: Anthonios Partheniou --- google/api_core/retry/retry_unary.py | 7 +++---- tests/unit/retry/test_retry_unary.py | 16 +++++++++++----- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/google/api_core/retry/retry_unary.py b/google/api_core/retry/retry_unary.py index ab1b4030b..090431333 100644 --- a/google/api_core/retry/retry_unary.py +++ b/google/api_core/retry/retry_unary.py @@ -141,10 +141,7 @@ def retry_target( for sleep in sleep_generator: try: - result = target() - if inspect.isawaitable(result): - warnings.warn(_ASYNC_RETRY_WARNING) - return result + return target() # pylint: disable=broad-except # This function explicitly must deal with broad exceptions. @@ -280,6 +277,8 @@ def __call__( Callable: A callable that will invoke ``func`` with retry behavior. """ + if inspect.iscoroutinefunction(func): + warnings.warn(_ASYNC_RETRY_WARNING) if self._on_error is not None: on_error = self._on_error diff --git a/tests/unit/retry/test_retry_unary.py b/tests/unit/retry/test_retry_unary.py index 7dcd8dd6a..71f6e2463 100644 --- a/tests/unit/retry/test_retry_unary.py +++ b/tests/unit/retry/test_retry_unary.py @@ -101,14 +101,20 @@ def test_retry_target_non_retryable_error(utcnow, sleep): ) @pytest.mark.asyncio async def test_retry_target_warning_for_retry(utcnow, sleep): - predicate = retry.if_exception_type(ValueError) - target = mock.AsyncMock(spec=["__call__"]) + """ + retry.Retry should raise warning when wrapping an async function. + """ + + async def target(): + pass # pragma: NO COVER + + retry_obj = retry.Retry() with pytest.warns(Warning) as exc_info: - # Note: predicate is just a filler and doesn't affect the test - retry.retry_target(target, predicate, range(10), None) + # raise warning when wrapping an async function + retry_obj(target) - assert len(exc_info) == 2 + assert len(exc_info) == 1 assert str(exc_info[0].message) == retry.retry_unary._ASYNC_RETRY_WARNING sleep.assert_not_called() From e031eb5fb6af2237517160b021ad58e4f4e5577b Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Tue, 4 Jun 2024 10:28:29 -0400 Subject: [PATCH 014/101] chore(revert): Revert "feat: add caching to GapicCallable" (#665) This reverts commit d96eb5cdd8120bfec97d62b09512c6fecc325be8. --- google/api_core/gapic_v1/method.py | 73 +++++++++++++++++------------- tests/unit/gapic/test_method.py | 21 --------- 2 files changed, 41 insertions(+), 53 deletions(-) diff --git a/google/api_core/gapic_v1/method.py b/google/api_core/gapic_v1/method.py index 206549ea3..0f14ea9c3 100644 --- a/google/api_core/gapic_v1/method.py +++ b/google/api_core/gapic_v1/method.py @@ -42,6 +42,24 @@ class _MethodDefault(enum.Enum): so the default should be used.""" +def _is_not_none_or_false(value): + return value is not None and value is not False + + +def _apply_decorators(func, decorators): + """Apply a list of decorators to a given function. + + ``decorators`` may contain items that are ``None`` or ``False`` which will + be ignored. + """ + filtered_decorators = filter(_is_not_none_or_false, reversed(decorators)) + + for decorator in filtered_decorators: + func = decorator(func) + + return func + + class _GapicCallable(object): """Callable that applies retry, timeout, and metadata logic. @@ -73,8 +91,6 @@ def __init__( ): self._target = target self._retry = retry - if isinstance(timeout, (int, float)): - timeout = TimeToDeadlineTimeout(timeout=timeout) self._timeout = timeout self._compression = compression self._metadata = metadata @@ -84,42 +100,35 @@ def __call__( ): """Invoke the low-level RPC with retry, timeout, compression, and metadata.""" - if compression is DEFAULT: - compression = self._compression - if compression is not None: - kwargs["compression"] = compression + if retry is DEFAULT: + retry = self._retry - # Add the user agent metadata to the call. - if self._metadata is not None: - try: - # attempt to concatenate default metadata with user-provided metadata - kwargs["metadata"] = (*kwargs["metadata"], *self._metadata) - except (KeyError, TypeError): - # if metadata is not provided, use just the default metadata - kwargs["metadata"] = self._metadata - - call = self._build_wrapped_call(timeout, retry) - return call(*args, **kwargs) - - @functools.lru_cache(maxsize=4) - def _build_wrapped_call(self, timeout, retry): - """ - Build a wrapped callable that applies retry, timeout, and metadata logic. - """ - wrapped_func = self._target if timeout is DEFAULT: timeout = self._timeout - elif isinstance(timeout, (int, float)): + + if compression is DEFAULT: + compression = self._compression + + if isinstance(timeout, (int, float)): timeout = TimeToDeadlineTimeout(timeout=timeout) - if timeout is not None: - wrapped_func = timeout(wrapped_func) - if retry is DEFAULT: - retry = self._retry - if retry is not None: - wrapped_func = retry(wrapped_func) + # Apply all applicable decorators. + wrapped_func = _apply_decorators(self._target, [retry, timeout]) + + # Add the user agent metadata to the call. + if self._metadata is not None: + metadata = kwargs.get("metadata", []) + # Due to the nature of invocation, None should be treated the same + # as not specified. + if metadata is None: + metadata = [] + metadata = list(metadata) + metadata.extend(self._metadata) + kwargs["metadata"] = metadata + if self._compression is not None: + kwargs["compression"] = compression - return wrapped_func + return wrapped_func(*args, **kwargs) def wrap_method( diff --git a/tests/unit/gapic/test_method.py b/tests/unit/gapic/test_method.py index 370da501d..d966f478f 100644 --- a/tests/unit/gapic/test_method.py +++ b/tests/unit/gapic/test_method.py @@ -222,24 +222,3 @@ def test_wrap_method_with_call_not_supported(): with pytest.raises(ValueError) as exc_info: google.api_core.gapic_v1.method.wrap_method(method, with_call=True) assert "with_call=True is only supported for unary calls" in str(exc_info.value) - - -def test_benchmark_gapic_call(): - """ - Ensure the __call__ method performance does not regress from expectations - - __call__ builds a new wrapped function using passed-in timeout and retry, but - subsequent calls are cached - - Note: The threshold has been tuned for the CI workers. Test may flake on - slower hardware - """ - from google.api_core.gapic_v1.method import _GapicCallable - from google.api_core.retry import Retry - from timeit import timeit - - gapic_callable = _GapicCallable( - lambda *a, **k: 1, retry=Retry(), timeout=1010, compression=False - ) - avg_time = timeit(lambda: gapic_callable(), number=10_000) - assert avg_time < 0.4 From 126b5c7154223da5f0537c90462a49e7e0d3ad97 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Wed, 5 Jun 2024 12:15:45 -0400 Subject: [PATCH 015/101] chore(revert): Revert "chore: move retry async check to wrap time (#649)" (#667) This reverts commit ac098a747a6ee09d70479ae5b73479172ad6cd78. --- google/api_core/retry/retry_unary.py | 7 ++++--- tests/unit/retry/test_retry_unary.py | 16 +++++----------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/google/api_core/retry/retry_unary.py b/google/api_core/retry/retry_unary.py index 090431333..ab1b4030b 100644 --- a/google/api_core/retry/retry_unary.py +++ b/google/api_core/retry/retry_unary.py @@ -141,7 +141,10 @@ def retry_target( for sleep in sleep_generator: try: - return target() + result = target() + if inspect.isawaitable(result): + warnings.warn(_ASYNC_RETRY_WARNING) + return result # pylint: disable=broad-except # This function explicitly must deal with broad exceptions. @@ -277,8 +280,6 @@ def __call__( Callable: A callable that will invoke ``func`` with retry behavior. """ - if inspect.iscoroutinefunction(func): - warnings.warn(_ASYNC_RETRY_WARNING) if self._on_error is not None: on_error = self._on_error diff --git a/tests/unit/retry/test_retry_unary.py b/tests/unit/retry/test_retry_unary.py index 71f6e2463..7dcd8dd6a 100644 --- a/tests/unit/retry/test_retry_unary.py +++ b/tests/unit/retry/test_retry_unary.py @@ -101,20 +101,14 @@ def test_retry_target_non_retryable_error(utcnow, sleep): ) @pytest.mark.asyncio async def test_retry_target_warning_for_retry(utcnow, sleep): - """ - retry.Retry should raise warning when wrapping an async function. - """ - - async def target(): - pass # pragma: NO COVER - - retry_obj = retry.Retry() + predicate = retry.if_exception_type(ValueError) + target = mock.AsyncMock(spec=["__call__"]) with pytest.warns(Warning) as exc_info: - # raise warning when wrapping an async function - retry_obj(target) + # Note: predicate is just a filler and doesn't affect the test + retry.retry_target(target, predicate, range(10), None) - assert len(exc_info) == 1 + assert len(exc_info) == 2 assert str(exc_info[0].message) == retry.retry_unary._ASYNC_RETRY_WARNING sleep.assert_not_called() From fda0ca6f0664ac5044671591ed62618175a7393f Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Wed, 19 Jun 2024 11:22:24 -0400 Subject: [PATCH 016/101] fix: add support for protobuf 5.x (#644) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: add support for protobuf 5.x * remove pin for types-protobuf * remove pytest from noxfile * refactor common code * Refactor Co-authored-by: Victor Chudnovsky * run black * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * mypy * run pre-release test against all python versions * filter warning --------- Co-authored-by: Victor Chudnovsky Co-authored-by: Owl Bot --- .github/workflows/unittest.yml | 2 +- google/api_core/operations_v1/__init__.py | 6 +- .../api_core/operations_v1/transports/base.py | 2 +- .../api_core/operations_v1/transports/rest.py | 67 +++++++----- noxfile.py | 100 +++++++++++++++--- owlbot.py | 4 +- pytest.ini | 2 + setup.py | 2 +- tests/unit/test_rest_streaming.py | 8 +- 9 files changed, 141 insertions(+), 52 deletions(-) diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index a8eef6142..34d29b7c1 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - option: ["", "_grpc_gcp", "_wo_grpc"] + option: ["", "_grpc_gcp", "_wo_grpc", "_with_prerelease_deps"] python: - "3.7" - "3.8" diff --git a/google/api_core/operations_v1/__init__.py b/google/api_core/operations_v1/__init__.py index 61186451f..8b75426b9 100644 --- a/google/api_core/operations_v1/__init__.py +++ b/google/api_core/operations_v1/__init__.py @@ -14,7 +14,9 @@ """Package for interacting with the google.longrunning.operations meta-API.""" -from google.api_core.operations_v1.abstract_operations_client import AbstractOperationsClient +from google.api_core.operations_v1.abstract_operations_client import ( + AbstractOperationsClient, +) from google.api_core.operations_v1.operations_async_client import OperationsAsyncClient from google.api_core.operations_v1.operations_client import OperationsClient from google.api_core.operations_v1.transports.rest import OperationsRestTransport @@ -23,5 +25,5 @@ "AbstractOperationsClient", "OperationsAsyncClient", "OperationsClient", - "OperationsRestTransport" + "OperationsRestTransport", ] diff --git a/google/api_core/operations_v1/transports/base.py b/google/api_core/operations_v1/transports/base.py index 98cf7896a..fb1d4fc9e 100644 --- a/google/api_core/operations_v1/transports/base.py +++ b/google/api_core/operations_v1/transports/base.py @@ -45,7 +45,7 @@ def __init__( self, *, host: str = DEFAULT_HOST, - credentials: ga_credentials.Credentials = None, + credentials: Optional[ga_credentials.Credentials] = None, credentials_file: Optional[str] = None, scopes: Optional[Sequence[str]] = None, quota_project_id: Optional[str] = None, diff --git a/google/api_core/operations_v1/transports/rest.py b/google/api_core/operations_v1/transports/rest.py index 49f99d210..f37bb3441 100644 --- a/google/api_core/operations_v1/transports/rest.py +++ b/google/api_core/operations_v1/transports/rest.py @@ -29,9 +29,13 @@ from google.longrunning import operations_pb2 # type: ignore from google.protobuf import empty_pb2 # type: ignore from google.protobuf import json_format # type: ignore +import google.protobuf + import grpc from .base import DEFAULT_CLIENT_INFO as BASE_DEFAULT_CLIENT_INFO, OperationsTransport +PROTOBUF_VERSION = google.protobuf.__version__ + OptionalRetry = Union[retries.Retry, object] DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( @@ -66,7 +70,7 @@ def __init__( self, *, host: str = "longrunning.googleapis.com", - credentials: ga_credentials.Credentials = None, + credentials: Optional[ga_credentials.Credentials] = None, credentials_file: Optional[str] = None, scopes: Optional[Sequence[str]] = None, client_cert_source_for_mtls: Optional[Callable[[], Tuple[bytes, bytes]]] = None, @@ -184,11 +188,7 @@ def _list_operations( "google.longrunning.Operations.ListOperations" ] - request_kwargs = json_format.MessageToDict( - request, - preserving_proto_field_name=True, - including_default_value_fields=True, - ) + request_kwargs = self._convert_protobuf_message_to_dict(request) transcoded_request = path_template.transcode(http_options, **request_kwargs) uri = transcoded_request["uri"] @@ -199,7 +199,6 @@ def _list_operations( json_format.ParseDict(transcoded_request["query_params"], query_params_request) query_params = json_format.MessageToDict( query_params_request, - including_default_value_fields=False, preserving_proto_field_name=False, use_integers_for_enums=False, ) @@ -265,11 +264,7 @@ def _get_operation( "google.longrunning.Operations.GetOperation" ] - request_kwargs = json_format.MessageToDict( - request, - preserving_proto_field_name=True, - including_default_value_fields=True, - ) + request_kwargs = self._convert_protobuf_message_to_dict(request) transcoded_request = path_template.transcode(http_options, **request_kwargs) uri = transcoded_request["uri"] @@ -280,7 +275,6 @@ def _get_operation( json_format.ParseDict(transcoded_request["query_params"], query_params_request) query_params = json_format.MessageToDict( query_params_request, - including_default_value_fields=False, preserving_proto_field_name=False, use_integers_for_enums=False, ) @@ -339,11 +333,7 @@ def _delete_operation( "google.longrunning.Operations.DeleteOperation" ] - request_kwargs = json_format.MessageToDict( - request, - preserving_proto_field_name=True, - including_default_value_fields=True, - ) + request_kwargs = self._convert_protobuf_message_to_dict(request) transcoded_request = path_template.transcode(http_options, **request_kwargs) uri = transcoded_request["uri"] @@ -354,7 +344,6 @@ def _delete_operation( json_format.ParseDict(transcoded_request["query_params"], query_params_request) query_params = json_format.MessageToDict( query_params_request, - including_default_value_fields=False, preserving_proto_field_name=False, use_integers_for_enums=False, ) @@ -411,11 +400,7 @@ def _cancel_operation( "google.longrunning.Operations.CancelOperation" ] - request_kwargs = json_format.MessageToDict( - request, - preserving_proto_field_name=True, - including_default_value_fields=True, - ) + request_kwargs = self._convert_protobuf_message_to_dict(request) transcoded_request = path_template.transcode(http_options, **request_kwargs) # Jsonify the request body @@ -423,7 +408,6 @@ def _cancel_operation( json_format.ParseDict(transcoded_request["body"], body_request) body = json_format.MessageToDict( body_request, - including_default_value_fields=False, preserving_proto_field_name=False, use_integers_for_enums=False, ) @@ -435,7 +419,6 @@ def _cancel_operation( json_format.ParseDict(transcoded_request["query_params"], query_params_request) query_params = json_format.MessageToDict( query_params_request, - including_default_value_fields=False, preserving_proto_field_name=False, use_integers_for_enums=False, ) @@ -458,6 +441,38 @@ def _cancel_operation( return empty_pb2.Empty() + def _convert_protobuf_message_to_dict( + self, message: google.protobuf.message.Message + ): + r"""Converts protobuf message to a dictionary. + + When the dictionary is encoded to JSON, it conforms to proto3 JSON spec. + + Args: + message(google.protobuf.message.Message): The protocol buffers message + instance to serialize. + + Returns: + A dict representation of the protocol buffer message. + """ + # For backwards compatibility with protobuf 3.x 4.x + # Remove once support for protobuf 3.x and 4.x is dropped + # https://github.com/googleapis/python-api-core/issues/643 + if PROTOBUF_VERSION[0:2] in ["3.", "4."]: + result = json_format.MessageToDict( + message, + preserving_proto_field_name=True, + including_default_value_fields=True, # type: ignore # backward compatibility + ) + else: + result = json_format.MessageToDict( + message, + preserving_proto_field_name=True, + always_print_fields_with_no_presence=True, + ) + + return result + @property def list_operations( self, diff --git a/noxfile.py b/noxfile.py index 8fbcaec0a..593bfc856 100644 --- a/noxfile.py +++ b/noxfile.py @@ -15,7 +15,9 @@ from __future__ import absolute_import import os import pathlib +import re import shutil +import unittest # https://github.com/google/importlab/issues/25 import nox # pytype: disable=import-error @@ -26,6 +28,8 @@ # Black and flake8 clash on the syntax for ignoring flake8's F401 in this file. BLACK_EXCLUDES = ["--exclude", "^/google/api_core/operations_v1/__init__.py"] +PYTHON_VERSIONS = ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] + DEFAULT_PYTHON_VERSION = "3.10" CURRENT_DIRECTORY = pathlib.Path(__file__).parent.absolute() @@ -72,7 +76,37 @@ def blacken(session): session.run("black", *BLACK_EXCLUDES, *BLACK_PATHS) -def default(session, install_grpc=True): +def install_prerelease_dependencies(session, constraints_path): + with open(constraints_path, encoding="utf-8") as constraints_file: + constraints_text = constraints_file.read() + # Ignore leading whitespace and comment lines. + constraints_deps = [ + match.group(1) + for match in re.finditer( + r"^\s*(\S+)(?===\S+)", constraints_text, flags=re.MULTILINE + ) + ] + session.install(*constraints_deps) + prerel_deps = [ + "google-auth", + "googleapis-common-protos", + "grpcio", + "grpcio-status", + "proto-plus", + "protobuf", + ] + + for dep in prerel_deps: + session.install("--pre", "--no-deps", "--upgrade", dep) + + # Remaining dependencies + other_deps = [ + "requests", + ] + session.install(*other_deps) + + +def default(session, install_grpc=True, prerelease=False): """Default unit test session. This is intended to be run **without** an interpreter set, so @@ -80,9 +114,8 @@ def default(session, install_grpc=True): Python corresponding to the ``nox`` binary the ``PATH`` can run the tests. """ - constraints_path = str( - CURRENT_DIRECTORY / "testing" / f"constraints-{session.python}.txt" - ) + if prerelease and not install_grpc: + unittest.skip("The pre-release session cannot be run without grpc") session.install( "dataclasses", @@ -92,10 +125,36 @@ def default(session, install_grpc=True): "pytest-xdist", ) - if install_grpc: - session.install("-e", ".[grpc]", "-c", constraints_path) + constraints_dir = str(CURRENT_DIRECTORY / "testing") + + if prerelease: + install_prerelease_dependencies( + session, f"{constraints_dir}/constraints-{PYTHON_VERSIONS[0]}.txt" + ) + # This *must* be the last install command to get the package from source. + session.install("-e", ".", "--no-deps") else: - session.install("-e", ".", "-c", constraints_path) + session.install( + "-e", + ".[grpc]" if install_grpc else ".", + "-c", + f"{constraints_dir}/constraints-{session.python}.txt", + ) + + # Print out package versions of dependencies + session.run( + "python", "-c", "import google.protobuf; print(google.protobuf.__version__)" + ) + # Support for proto.version was added in v1.23.0 + # https://github.com/googleapis/proto-plus-python/releases/tag/v1.23.0 + session.run( + "python", + "-c", + """import proto; hasattr(proto, "version") and print(proto.version.__version__)""", + ) + if install_grpc: + session.run("python", "-c", "import grpc; print(grpc.__version__)") + session.run("python", "-c", "import google.auth; print(google.auth.__version__)") pytest_args = [ "python", @@ -130,15 +189,26 @@ def default(session, install_grpc=True): session.run(*pytest_args) -@nox.session(python=["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]) +@nox.session(python=PYTHON_VERSIONS) def unit(session): """Run the unit test suite.""" default(session) -@nox.session(python=["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]) +@nox.session(python=PYTHON_VERSIONS) +def unit_with_prerelease_deps(session): + """Run the unit test suite.""" + default(session, prerelease=True) + + +@nox.session(python=PYTHON_VERSIONS) def unit_grpc_gcp(session): - """Run the unit test suite with grpcio-gcp installed.""" + """ + Run the unit test suite with grpcio-gcp installed. + `grpcio-gcp` doesn't support protobuf 4+. + Remove extra `grpcgcp` when protobuf 3.x is dropped. + https://github.com/googleapis/python-api-core/issues/594 + """ constraints_path = str( CURRENT_DIRECTORY / "testing" / f"constraints-{session.python}.txt" ) @@ -150,7 +220,7 @@ def unit_grpc_gcp(session): default(session) -@nox.session(python=["3.8", "3.10", "3.11", "3.12"]) +@nox.session(python=PYTHON_VERSIONS) def unit_wo_grpc(session): """Run the unit test suite w/o grpcio installed""" default(session, install_grpc=False) @@ -164,10 +234,10 @@ def lint_setup_py(session): session.run("python", "setup.py", "check", "--restructuredtext", "--strict") -@nox.session(python="3.8") +@nox.session(python=DEFAULT_PYTHON_VERSION) def pytype(session): """Run type-checking.""" - session.install(".[grpc]", "pytype >= 2019.3.21") + session.install(".[grpc]", "pytype") session.run("pytype") @@ -178,9 +248,7 @@ def mypy(session): session.install( "types-setuptools", "types-requests", - # TODO(https://github.com/googleapis/python-api-core/issues/642): - # Use the latest version of types-protobuf. - "types-protobuf<5", + "types-protobuf", "types-mock", "types-dataclasses", ) diff --git a/owlbot.py b/owlbot.py index 5a83032e8..c8c76542d 100644 --- a/owlbot.py +++ b/owlbot.py @@ -29,8 +29,8 @@ ".flake8", # flake8-import-order, layout ".coveragerc", # layout "CONTRIBUTING.rst", # no systests - ".github/workflows/unittest.yml", # exclude unittest gh action - ".github/workflows/lint.yml", # exclude lint gh action + ".github/workflows/unittest.yml", # exclude unittest gh action + ".github/workflows/lint.yml", # exclude lint gh action "README.rst", ] templated_files = common.py_library(microgenerator=True, cov_level=100) diff --git a/pytest.ini b/pytest.ini index 13d5bf4d9..696548cfc 100644 --- a/pytest.ini +++ b/pytest.ini @@ -19,3 +19,5 @@ filterwarnings = ignore:.*pkg_resources is deprecated as an API:DeprecationWarning # Remove once https://github.com/grpc/grpc/issues/35086 is fixed (and version newer than 1.60.0 is published) ignore:There is no current event loop:DeprecationWarning + # Remove after support for Python 3.7 is dropped + ignore:After January 1, 2024, new releases of this library will drop support for Python 3.7:DeprecationWarning diff --git a/setup.py b/setup.py index 47e0b454c..a9e01f49c 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ release_status = "Development Status :: 5 - Production/Stable" dependencies = [ "googleapis-common-protos >= 1.56.2, < 2.0.dev0", - "protobuf>=3.19.5,<5.0.0.dev0,!=3.20.0,!=3.20.1,!=4.21.0,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5", + "protobuf>=3.19.5,<6.0.0.dev0,!=3.20.0,!=3.20.1,!=4.21.0,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5", "proto-plus >= 1.22.3, <2.0.0dev", "google-auth >= 2.14.1, < 3.0.dev0", "requests >= 2.18.0, < 3.0.0.dev0", diff --git a/tests/unit/test_rest_streaming.py b/tests/unit/test_rest_streaming.py index b532eb1de..0f2b3b32a 100644 --- a/tests/unit/test_rest_streaming.py +++ b/tests/unit/test_rest_streaming.py @@ -101,9 +101,11 @@ def _parse_responses(self, responses: List[proto.Message]) -> bytes: # json.dumps returns a string surrounded with quotes that need to be stripped # in order to be an actual JSON. json_responses = [ - self._response_message_cls.to_json(r).strip('"') - if issubclass(self._response_message_cls, proto.Message) - else MessageToJson(r).strip('"') + ( + self._response_message_cls.to_json(r).strip('"') + if issubclass(self._response_message_cls, proto.Message) + else MessageToJson(r).strip('"') + ) for r in responses ] logging.info(f"Sending JSON stream: {json_responses}") From fef936315f82a6b1db0bf23595e7bf71c7b503e4 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 24 Jun 2024 09:48:07 -0700 Subject: [PATCH 017/101] chore(main): release 2.19.1 (#650) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 8 ++++++++ google/api_core/version.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e64d3570..e48409f42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ [1]: https://pypi.org/project/google-api-core/#history +## [2.19.1](https://github.com/googleapis/python-api-core/compare/v2.19.0...v2.19.1) (2024-06-19) + + +### Bug Fixes + +* Add support for protobuf 5.x ([#644](https://github.com/googleapis/python-api-core/issues/644)) ([fda0ca6](https://github.com/googleapis/python-api-core/commit/fda0ca6f0664ac5044671591ed62618175a7393f)) +* Ignore unknown fields in rest streaming. ([#651](https://github.com/googleapis/python-api-core/issues/651)) ([1203fb9](https://github.com/googleapis/python-api-core/commit/1203fb97d2685535f89113e944c4764c1deb595e)) + ## [2.19.0](https://github.com/googleapis/python-api-core/compare/v2.18.0...v2.19.0) (2024-04-29) diff --git a/google/api_core/version.py b/google/api_core/version.py index 2605c08a3..25b6f2f82 100644 --- a/google/api_core/version.py +++ b/google/api_core/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "2.19.0" +__version__ = "2.19.1" From aff6f55b598a15e502ed13aceacd8fcef21378a5 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Wed, 3 Jul 2024 15:01:14 -0400 Subject: [PATCH 018/101] chore: update templated files (#674) Source-Link: https://github.com/googleapis/synthtool/commit/a37f74cd300d1f56d6f28c368d2931f72adee948 Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-python:latest@sha256:d3de8a02819f65001effcbd3ea76ce97e9bcff035c7a89457f40f892c87c5b32 Co-authored-by: Owl Bot --- .github/.OwlBot.lock.yaml | 4 +- .github/auto-label.yaml | 2 +- .kokoro/build.sh | 2 +- .kokoro/docker/docs/Dockerfile | 2 +- .kokoro/populate-secrets.sh | 2 +- .kokoro/publish-docs.sh | 2 +- .kokoro/release.sh | 2 +- .kokoro/requirements.txt | 509 ++++++++++++++------------- .kokoro/test-samples-against-head.sh | 2 +- .kokoro/test-samples-impl.sh | 2 +- .kokoro/test-samples.sh | 2 +- .kokoro/trampoline.sh | 2 +- .kokoro/trampoline_v2.sh | 2 +- .pre-commit-config.yaml | 2 +- .trampolinerc | 2 +- MANIFEST.in | 2 +- docs/conf.py | 2 +- scripts/decrypt-secrets.sh | 2 +- scripts/readme-gen/readme_gen.py | 2 +- 19 files changed, 285 insertions(+), 262 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 81f87c569..91d742b5b 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest - digest: sha256:5a4c19d17e597b92d786e569be101e636c9c2817731f80a5adec56b2aa8fe070 -# created: 2024-04-12T11:35:58.922854369Z + digest: sha256:d3de8a02819f65001effcbd3ea76ce97e9bcff035c7a89457f40f892c87c5b32 +# created: 2024-07-03T17:43:00.77142528Z diff --git a/.github/auto-label.yaml b/.github/auto-label.yaml index 8b37ee897..21786a4eb 100644 --- a/.github/auto-label.yaml +++ b/.github/auto-label.yaml @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/.kokoro/build.sh b/.kokoro/build.sh index b3c0ce5e0..f05e867ca 100755 --- a/.kokoro/build.sh +++ b/.kokoro/build.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2023 Google LLC +# Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/.kokoro/docker/docs/Dockerfile b/.kokoro/docker/docs/Dockerfile index bdaf39fe2..a26ce6193 100644 --- a/.kokoro/docker/docs/Dockerfile +++ b/.kokoro/docker/docs/Dockerfile @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/.kokoro/populate-secrets.sh b/.kokoro/populate-secrets.sh index 6f3972140..c435402f4 100755 --- a/.kokoro/populate-secrets.sh +++ b/.kokoro/populate-secrets.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2023 Google LLC. +# Copyright 2024 Google LLC. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/.kokoro/publish-docs.sh b/.kokoro/publish-docs.sh index 9eafe0be3..38f083f05 100755 --- a/.kokoro/publish-docs.sh +++ b/.kokoro/publish-docs.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2023 Google LLC +# Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/.kokoro/release.sh b/.kokoro/release.sh index b07000dfb..d2734b2cc 100755 --- a/.kokoro/release.sh +++ b/.kokoro/release.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2023 Google LLC +# Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/.kokoro/requirements.txt b/.kokoro/requirements.txt index 51f92b8e1..35ece0e4d 100644 --- a/.kokoro/requirements.txt +++ b/.kokoro/requirements.txt @@ -4,21 +4,25 @@ # # pip-compile --allow-unsafe --generate-hashes requirements.in # -argcomplete==3.1.4 \ - --hash=sha256:72558ba729e4c468572609817226fb0a6e7e9a0a7d477b882be168c0b4a62b94 \ - --hash=sha256:fbe56f8cda08aa9a04b307d8482ea703e96a6a801611acb4be9bf3942017989f +argcomplete==3.4.0 \ + --hash=sha256:69a79e083a716173e5532e0fa3bef45f793f4e61096cf52b5a42c0211c8b8aa5 \ + --hash=sha256:c2abcdfe1be8ace47ba777d4fce319eb13bf8ad9dace8d085dcad6eded88057f # via nox -attrs==23.1.0 \ - --hash=sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04 \ - --hash=sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015 +attrs==23.2.0 \ + --hash=sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30 \ + --hash=sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1 # via gcp-releasetool -cachetools==5.3.2 \ - --hash=sha256:086ee420196f7b2ab9ca2db2520aca326318b68fe5ba8bc4d49cca91add450f2 \ - --hash=sha256:861f35a13a451f94e301ce2bec7cac63e881232ccce7ed67fab9b5df4d3beaa1 +backports-tarfile==1.2.0 \ + --hash=sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34 \ + --hash=sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991 + # via jaraco-context +cachetools==5.3.3 \ + --hash=sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945 \ + --hash=sha256:ba29e2dfa0b8b556606f097407ed1aa62080ee108ab0dc5ec9d6a723a007d105 # via google-auth -certifi==2023.7.22 \ - --hash=sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082 \ - --hash=sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9 +certifi==2024.6.2 \ + --hash=sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516 \ + --hash=sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56 # via requests cffi==1.16.0 \ --hash=sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc \ @@ -87,90 +91,90 @@ click==8.0.4 \ # -r requirements.in # gcp-docuploader # gcp-releasetool -colorlog==6.7.0 \ - --hash=sha256:0d33ca236784a1ba3ff9c532d4964126d8a2c44f1f0cb1d2b0728196f512f662 \ - --hash=sha256:bd94bd21c1e13fac7bd3153f4bc3a7dc0eb0974b8bc2fdf1a989e474f6e582e5 +colorlog==6.8.2 \ + --hash=sha256:3e3e079a41feb5a1b64f978b5ea4f46040a94f11f0e8bbb8261e3dbbeca64d44 \ + --hash=sha256:4dcbb62368e2800cb3c5abd348da7e53f6c362dda502ec27c560b2e58a66bd33 # via # gcp-docuploader # nox -cryptography==42.0.5 \ - --hash=sha256:0270572b8bd2c833c3981724b8ee9747b3ec96f699a9665470018594301439ee \ - --hash=sha256:111a0d8553afcf8eb02a4fea6ca4f59d48ddb34497aa8706a6cf536f1a5ec576 \ - --hash=sha256:16a48c23a62a2f4a285699dba2e4ff2d1cff3115b9df052cdd976a18856d8e3d \ - --hash=sha256:1b95b98b0d2af784078fa69f637135e3c317091b615cd0905f8b8a087e86fa30 \ - --hash=sha256:1f71c10d1e88467126f0efd484bd44bca5e14c664ec2ede64c32f20875c0d413 \ - --hash=sha256:2424ff4c4ac7f6b8177b53c17ed5d8fa74ae5955656867f5a8affaca36a27abb \ - --hash=sha256:2bce03af1ce5a5567ab89bd90d11e7bbdff56b8af3acbbec1faded8f44cb06da \ - --hash=sha256:329906dcc7b20ff3cad13c069a78124ed8247adcac44b10bea1130e36caae0b4 \ - --hash=sha256:37dd623507659e08be98eec89323469e8c7b4c1407c85112634ae3dbdb926fdd \ - --hash=sha256:3eaafe47ec0d0ffcc9349e1708be2aaea4c6dd4978d76bf6eb0cb2c13636c6fc \ - --hash=sha256:5e6275c09d2badf57aea3afa80d975444f4be8d3bc58f7f80d2a484c6f9485c8 \ - --hash=sha256:6fe07eec95dfd477eb9530aef5bead34fec819b3aaf6c5bd6d20565da607bfe1 \ - --hash=sha256:7367d7b2eca6513681127ebad53b2582911d1736dc2ffc19f2c3ae49997496bc \ - --hash=sha256:7cde5f38e614f55e28d831754e8a3bacf9ace5d1566235e39d91b35502d6936e \ - --hash=sha256:9481ffe3cf013b71b2428b905c4f7a9a4f76ec03065b05ff499bb5682a8d9ad8 \ - --hash=sha256:98d8dc6d012b82287f2c3d26ce1d2dd130ec200c8679b6213b3c73c08b2b7940 \ - --hash=sha256:a011a644f6d7d03736214d38832e030d8268bcff4a41f728e6030325fea3e400 \ - --hash=sha256:a2913c5375154b6ef2e91c10b5720ea6e21007412f6437504ffea2109b5a33d7 \ - --hash=sha256:a30596bae9403a342c978fb47d9b0ee277699fa53bbafad14706af51fe543d16 \ - --hash=sha256:b03c2ae5d2f0fc05f9a2c0c997e1bc18c8229f392234e8a0194f202169ccd278 \ - --hash=sha256:b6cd2203306b63e41acdf39aa93b86fb566049aeb6dc489b70e34bcd07adca74 \ - --hash=sha256:b7ffe927ee6531c78f81aa17e684e2ff617daeba7f189f911065b2ea2d526dec \ - --hash=sha256:b8cac287fafc4ad485b8a9b67d0ee80c66bf3574f655d3b97ef2e1082360faf1 \ - --hash=sha256:ba334e6e4b1d92442b75ddacc615c5476d4ad55cc29b15d590cc6b86efa487e2 \ - --hash=sha256:ba3e4a42397c25b7ff88cdec6e2a16c2be18720f317506ee25210f6d31925f9c \ - --hash=sha256:c41fb5e6a5fe9ebcd58ca3abfeb51dffb5d83d6775405305bfa8715b76521922 \ - --hash=sha256:cd2030f6650c089aeb304cf093f3244d34745ce0cfcc39f20c6fbfe030102e2a \ - --hash=sha256:cd65d75953847815962c84a4654a84850b2bb4aed3f26fadcc1c13892e1e29f6 \ - --hash=sha256:e4985a790f921508f36f81831817cbc03b102d643b5fcb81cd33df3fa291a1a1 \ - --hash=sha256:e807b3188f9eb0eaa7bbb579b462c5ace579f1cedb28107ce8b48a9f7ad3679e \ - --hash=sha256:f12764b8fffc7a123f641d7d049d382b73f96a34117e0b637b80643169cec8ac \ - --hash=sha256:f8837fe1d6ac4a8052a9a8ddab256bc006242696f03368a4009be7ee3075cdb7 +cryptography==42.0.8 \ + --hash=sha256:013629ae70b40af70c9a7a5db40abe5d9054e6f4380e50ce769947b73bf3caad \ + --hash=sha256:2346b911eb349ab547076f47f2e035fc8ff2c02380a7cbbf8d87114fa0f1c583 \ + --hash=sha256:2f66d9cd9147ee495a8374a45ca445819f8929a3efcd2e3df6428e46c3cbb10b \ + --hash=sha256:2f88d197e66c65be5e42cd72e5c18afbfae3f741742070e3019ac8f4ac57262c \ + --hash=sha256:31f721658a29331f895a5a54e7e82075554ccfb8b163a18719d342f5ffe5ecb1 \ + --hash=sha256:343728aac38decfdeecf55ecab3264b015be68fc2816ca800db649607aeee648 \ + --hash=sha256:5226d5d21ab681f432a9c1cf8b658c0cb02533eece706b155e5fbd8a0cdd3949 \ + --hash=sha256:57080dee41209e556a9a4ce60d229244f7a66ef52750f813bfbe18959770cfba \ + --hash=sha256:5a94eccb2a81a309806027e1670a358b99b8fe8bfe9f8d329f27d72c094dde8c \ + --hash=sha256:6b7c4f03ce01afd3b76cf69a5455caa9cfa3de8c8f493e0d3ab7d20611c8dae9 \ + --hash=sha256:7016f837e15b0a1c119d27ecd89b3515f01f90a8615ed5e9427e30d9cdbfed3d \ + --hash=sha256:81884c4d096c272f00aeb1f11cf62ccd39763581645b0812e99a91505fa48e0c \ + --hash=sha256:81d8a521705787afe7a18d5bfb47ea9d9cc068206270aad0b96a725022e18d2e \ + --hash=sha256:8d09d05439ce7baa8e9e95b07ec5b6c886f548deb7e0f69ef25f64b3bce842f2 \ + --hash=sha256:961e61cefdcb06e0c6d7e3a1b22ebe8b996eb2bf50614e89384be54c48c6b63d \ + --hash=sha256:9c0c1716c8447ee7dbf08d6db2e5c41c688544c61074b54fc4564196f55c25a7 \ + --hash=sha256:a0608251135d0e03111152e41f0cc2392d1e74e35703960d4190b2e0f4ca9c70 \ + --hash=sha256:a0c5b2b0585b6af82d7e385f55a8bc568abff8923af147ee3c07bd8b42cda8b2 \ + --hash=sha256:ad803773e9df0b92e0a817d22fd8a3675493f690b96130a5e24f1b8fabbea9c7 \ + --hash=sha256:b297f90c5723d04bcc8265fc2a0f86d4ea2e0f7ab4b6994459548d3a6b992a14 \ + --hash=sha256:ba4f0a211697362e89ad822e667d8d340b4d8d55fae72cdd619389fb5912eefe \ + --hash=sha256:c4783183f7cb757b73b2ae9aed6599b96338eb957233c58ca8f49a49cc32fd5e \ + --hash=sha256:c9bb2ae11bfbab395bdd072985abde58ea9860ed84e59dbc0463a5d0159f5b71 \ + --hash=sha256:cafb92b2bc622cd1aa6a1dce4b93307792633f4c5fe1f46c6b97cf67073ec961 \ + --hash=sha256:d45b940883a03e19e944456a558b67a41160e367a719833c53de6911cabba2b7 \ + --hash=sha256:dc0fdf6787f37b1c6b08e6dfc892d9d068b5bdb671198c72072828b80bd5fe4c \ + --hash=sha256:dea567d1b0e8bc5764b9443858b673b734100c2871dc93163f58c46a97a83d28 \ + --hash=sha256:dec9b018df185f08483f294cae6ccac29e7a6e0678996587363dc352dc65c842 \ + --hash=sha256:e3ec3672626e1b9e55afd0df6d774ff0e953452886e06e0f1eb7eb0c832e8902 \ + --hash=sha256:e599b53fd95357d92304510fb7bda8523ed1f79ca98dce2f43c115950aa78801 \ + --hash=sha256:fa76fbb7596cc5839320000cdd5d0955313696d9511debab7ee7278fc8b5c84a \ + --hash=sha256:fff12c88a672ab9c9c1cf7b0c80e3ad9e2ebd9d828d955c126be4fd3e5578c9e # via # -r requirements.in # gcp-releasetool # secretstorage -distlib==0.3.7 \ - --hash=sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057 \ - --hash=sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8 +distlib==0.3.8 \ + --hash=sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784 \ + --hash=sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64 # via virtualenv -docutils==0.20.1 \ - --hash=sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6 \ - --hash=sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b +docutils==0.21.2 \ + --hash=sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f \ + --hash=sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2 # via readme-renderer -filelock==3.13.1 \ - --hash=sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e \ - --hash=sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c +filelock==3.15.4 \ + --hash=sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb \ + --hash=sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7 # via virtualenv gcp-docuploader==0.6.5 \ --hash=sha256:30221d4ac3e5a2b9c69aa52fdbef68cc3f27d0e6d0d90e220fc024584b8d2318 \ --hash=sha256:b7458ef93f605b9d46a4bf3a8dc1755dad1f31d030c8679edf304e343b347eea # via -r requirements.in -gcp-releasetool==2.0.0 \ - --hash=sha256:3d73480b50ba243f22d7c7ec08b115a30e1c7817c4899781840c26f9c55b8277 \ - --hash=sha256:7aa9fd935ec61e581eb8458ad00823786d91756c25e492f372b2b30962f3c28f +gcp-releasetool==2.0.1 \ + --hash=sha256:34314a910c08e8911d9c965bd44f8f2185c4f556e737d719c33a41f6a610de96 \ + --hash=sha256:b0d5863c6a070702b10883d37c4bdfd74bf930fe417f36c0c965d3b7c779ae62 # via -r requirements.in -google-api-core==2.12.0 \ - --hash=sha256:c22e01b1e3c4dcd90998494879612c38d0a3411d1f7b679eb89e2abe3ce1f553 \ - --hash=sha256:ec6054f7d64ad13b41e43d96f735acbd763b0f3b695dabaa2d579673f6a6e160 +google-api-core==2.19.1 \ + --hash=sha256:f12a9b8309b5e21d92483bbd47ce2c445861ec7d269ef6784ecc0ea8c1fa6125 \ + --hash=sha256:f4695f1e3650b316a795108a76a1c416e6afb036199d1c1f1f110916df479ffd # via # google-cloud-core # google-cloud-storage -google-auth==2.23.4 \ - --hash=sha256:79905d6b1652187def79d491d6e23d0cbb3a21d3c7ba0dbaa9c8a01906b13ff3 \ - --hash=sha256:d4bbc92fe4b8bfd2f3e8d88e5ba7085935da208ee38a134fc280e7ce682a05f2 +google-auth==2.31.0 \ + --hash=sha256:042c4702efa9f7d3c48d3a69341c209381b125faa6dbf3ebe56bc7e40ae05c23 \ + --hash=sha256:87805c36970047247c8afe614d4e3af8eceafc1ebba0c679fe75ddd1d575e871 # via # gcp-releasetool # google-api-core # google-cloud-core # google-cloud-storage -google-cloud-core==2.3.3 \ - --hash=sha256:37b80273c8d7eee1ae816b3a20ae43585ea50506cb0e60f3cf5be5f87f1373cb \ - --hash=sha256:fbd11cad3e98a7e5b0343dc07cb1039a5ffd7a5bb96e1f1e27cee4bda4a90863 +google-cloud-core==2.4.1 \ + --hash=sha256:9b7749272a812bde58fff28868d0c5e2f585b82f37e09a1f6ed2d4d10f134073 \ + --hash=sha256:a9e6a4422b9ac5c29f79a0ede9485473338e2ce78d91f2370c01e730eab22e61 # via google-cloud-storage -google-cloud-storage==2.13.0 \ - --hash=sha256:ab0bf2e1780a1b74cf17fccb13788070b729f50c252f0c94ada2aae0ca95437d \ - --hash=sha256:f62dc4c7b6cd4360d072e3deb28035fbdad491ac3d9b0b1815a12daea10f37c7 +google-cloud-storage==2.17.0 \ + --hash=sha256:49378abff54ef656b52dca5ef0f2eba9aa83dc2b2c72c78714b03a1a95fe9388 \ + --hash=sha256:5b393bc766b7a3bc6f5407b9e665b2450d36282614b7945e570b3480a456d1e1 # via gcp-docuploader google-crc32c==1.5.0 \ --hash=sha256:024894d9d3cfbc5943f8f230e23950cd4906b2fe004c72e29b209420a1e6b05a \ @@ -244,28 +248,36 @@ google-crc32c==1.5.0 \ # via # google-cloud-storage # google-resumable-media -google-resumable-media==2.6.0 \ - --hash=sha256:972852f6c65f933e15a4a210c2b96930763b47197cdf4aa5f5bea435efb626e7 \ - --hash=sha256:fc03d344381970f79eebb632a3c18bb1828593a2dc5572b5f90115ef7d11e81b +google-resumable-media==2.7.1 \ + --hash=sha256:103ebc4ba331ab1bfdac0250f8033627a2cd7cde09e7ccff9181e31ba4315b2c \ + --hash=sha256:eae451a7b2e2cdbaaa0fd2eb00cc8a1ee5e95e16b55597359cbc3d27d7d90e33 # via google-cloud-storage -googleapis-common-protos==1.61.0 \ - --hash=sha256:22f1915393bb3245343f6efe87f6fe868532efc12aa26b391b15132e1279f1c0 \ - --hash=sha256:8a64866a97f6304a7179873a465d6eee97b7a24ec6cfd78e0f575e96b821240b +googleapis-common-protos==1.63.2 \ + --hash=sha256:27a2499c7e8aff199665b22741997e485eccc8645aa9176c7c988e6fae507945 \ + --hash=sha256:27c5abdffc4911f28101e635de1533fb4cfd2c37fbaa9174587c799fac90aa87 # via google-api-core idna==3.7 \ --hash=sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc \ --hash=sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0 # via requests -importlib-metadata==6.8.0 \ - --hash=sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb \ - --hash=sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743 +importlib-metadata==8.0.0 \ + --hash=sha256:15584cf2b1bf449d98ff8a6ff1abef57bf20f3ac6454f431736cd3e660921b2f \ + --hash=sha256:188bd24e4c346d3f0a933f275c2fec67050326a856b9a359881d7c2a697e8812 # via # -r requirements.in # keyring # twine -jaraco-classes==3.3.0 \ - --hash=sha256:10afa92b6743f25c0cf5f37c6bb6e18e2c5bb84a16527ccfc0040ea377e7aaeb \ - --hash=sha256:c063dd08e89217cee02c8d5e5ec560f2c8ce6cdc2fcdc2e68f7b2e5547ed3621 +jaraco-classes==3.4.0 \ + --hash=sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd \ + --hash=sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790 + # via keyring +jaraco-context==5.3.0 \ + --hash=sha256:3e16388f7da43d384a1a7cd3452e72e14732ac9fe459678773a3608a812bf266 \ + --hash=sha256:c2f67165ce1f9be20f32f650f25d8edfc1646a8aeee48ae06fb35f90763576d2 + # via keyring +jaraco-functools==4.0.1 \ + --hash=sha256:3b24ccb921d6b593bdceb56ce14799204f473976e2a9d4b15b04d0f2c2326664 \ + --hash=sha256:d33fa765374c0611b52f8b3a795f8900869aa88c84769d4d1746cd68fb28c3e8 # via keyring jeepney==0.8.0 \ --hash=sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806 \ @@ -273,13 +285,13 @@ jeepney==0.8.0 \ # via # keyring # secretstorage -jinja2==3.1.3 \ - --hash=sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa \ - --hash=sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90 +jinja2==3.1.4 \ + --hash=sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369 \ + --hash=sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d # via gcp-releasetool -keyring==24.2.0 \ - --hash=sha256:4901caaf597bfd3bbd78c9a0c7c4c29fcd8310dab2cffefe749e916b6527acd6 \ - --hash=sha256:ca0746a19ec421219f4d713f848fa297a661a8a8c1504867e55bfb5e09091509 +keyring==25.2.1 \ + --hash=sha256:2458681cdefc0dbc0b7eb6cf75d0b98e59f9ad9b2d4edd319d18f68bdca95e50 \ + --hash=sha256:daaffd42dbda25ddafb1ad5fec4024e5bbcfe424597ca1ca452b299861e49f1b # via # gcp-releasetool # twine @@ -287,146 +299,153 @@ markdown-it-py==3.0.0 \ --hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \ --hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb # via rich -markupsafe==2.1.3 \ - --hash=sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e \ - --hash=sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e \ - --hash=sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431 \ - --hash=sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686 \ - --hash=sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c \ - --hash=sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559 \ - --hash=sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc \ - --hash=sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb \ - --hash=sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939 \ - --hash=sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c \ - --hash=sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0 \ - --hash=sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4 \ - --hash=sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9 \ - --hash=sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575 \ - --hash=sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba \ - --hash=sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d \ - --hash=sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd \ - --hash=sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3 \ - --hash=sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00 \ - --hash=sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155 \ - --hash=sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac \ - --hash=sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52 \ - --hash=sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f \ - --hash=sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8 \ - --hash=sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b \ - --hash=sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007 \ - --hash=sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24 \ - --hash=sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea \ - --hash=sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198 \ - --hash=sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0 \ - --hash=sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee \ - --hash=sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be \ - --hash=sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2 \ - --hash=sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1 \ - --hash=sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707 \ - --hash=sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6 \ - --hash=sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c \ - --hash=sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58 \ - --hash=sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823 \ - --hash=sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779 \ - --hash=sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636 \ - --hash=sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c \ - --hash=sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad \ - --hash=sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee \ - --hash=sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc \ - --hash=sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2 \ - --hash=sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48 \ - --hash=sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7 \ - --hash=sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e \ - --hash=sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b \ - --hash=sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa \ - --hash=sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5 \ - --hash=sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e \ - --hash=sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb \ - --hash=sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9 \ - --hash=sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57 \ - --hash=sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc \ - --hash=sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc \ - --hash=sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2 \ - --hash=sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11 +markupsafe==2.1.5 \ + --hash=sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf \ + --hash=sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff \ + --hash=sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f \ + --hash=sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3 \ + --hash=sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532 \ + --hash=sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f \ + --hash=sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617 \ + --hash=sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df \ + --hash=sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4 \ + --hash=sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906 \ + --hash=sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f \ + --hash=sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4 \ + --hash=sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8 \ + --hash=sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371 \ + --hash=sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2 \ + --hash=sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465 \ + --hash=sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52 \ + --hash=sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6 \ + --hash=sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169 \ + --hash=sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad \ + --hash=sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2 \ + --hash=sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0 \ + --hash=sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029 \ + --hash=sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f \ + --hash=sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a \ + --hash=sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced \ + --hash=sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5 \ + --hash=sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c \ + --hash=sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf \ + --hash=sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9 \ + --hash=sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb \ + --hash=sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad \ + --hash=sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3 \ + --hash=sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1 \ + --hash=sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46 \ + --hash=sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc \ + --hash=sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a \ + --hash=sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee \ + --hash=sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900 \ + --hash=sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5 \ + --hash=sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea \ + --hash=sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f \ + --hash=sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5 \ + --hash=sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e \ + --hash=sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a \ + --hash=sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f \ + --hash=sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50 \ + --hash=sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a \ + --hash=sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b \ + --hash=sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4 \ + --hash=sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff \ + --hash=sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2 \ + --hash=sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46 \ + --hash=sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b \ + --hash=sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf \ + --hash=sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5 \ + --hash=sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5 \ + --hash=sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab \ + --hash=sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd \ + --hash=sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68 # via jinja2 mdurl==0.1.2 \ --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba # via markdown-it-py -more-itertools==10.1.0 \ - --hash=sha256:626c369fa0eb37bac0291bce8259b332fd59ac792fa5497b59837309cd5b114a \ - --hash=sha256:64e0735fcfdc6f3464ea133afe8ea4483b1c5fe3a3d69852e6503b43a0b222e6 - # via jaraco-classes -nh3==0.2.14 \ - --hash=sha256:116c9515937f94f0057ef50ebcbcc10600860065953ba56f14473ff706371873 \ - --hash=sha256:18415df36db9b001f71a42a3a5395db79cf23d556996090d293764436e98e8ad \ - --hash=sha256:203cac86e313cf6486704d0ec620a992c8bc164c86d3a4fd3d761dd552d839b5 \ - --hash=sha256:2b0be5c792bd43d0abef8ca39dd8acb3c0611052ce466d0401d51ea0d9aa7525 \ - --hash=sha256:377aaf6a9e7c63962f367158d808c6a1344e2b4f83d071c43fbd631b75c4f0b2 \ - --hash=sha256:525846c56c2bcd376f5eaee76063ebf33cf1e620c1498b2a40107f60cfc6054e \ - --hash=sha256:5529a3bf99402c34056576d80ae5547123f1078da76aa99e8ed79e44fa67282d \ - --hash=sha256:7771d43222b639a4cd9e341f870cee336b9d886de1ad9bec8dddab22fe1de450 \ - --hash=sha256:88c753efbcdfc2644a5012938c6b9753f1c64a5723a67f0301ca43e7b85dcf0e \ - --hash=sha256:93a943cfd3e33bd03f77b97baa11990148687877b74193bf777956b67054dcc6 \ - --hash=sha256:9be2f68fb9a40d8440cbf34cbf40758aa7f6093160bfc7fb018cce8e424f0c3a \ - --hash=sha256:a0c509894fd4dccdff557068e5074999ae3b75f4c5a2d6fb5415e782e25679c4 \ - --hash=sha256:ac8056e937f264995a82bf0053ca898a1cb1c9efc7cd68fa07fe0060734df7e4 \ - --hash=sha256:aed56a86daa43966dd790ba86d4b810b219f75b4bb737461b6886ce2bde38fd6 \ - --hash=sha256:e8986f1dd3221d1e741fda0a12eaa4a273f1d80a35e31a1ffe579e7c621d069e \ - --hash=sha256:f99212a81c62b5f22f9e7c3e347aa00491114a5647e1f13bbebd79c3e5f08d75 +more-itertools==10.3.0 \ + --hash=sha256:e5d93ef411224fbcef366a6e8ddc4c5781bc6359d43412a65dd5964e46111463 \ + --hash=sha256:ea6a02e24a9161e51faad17a8782b92a0df82c12c1c8886fec7f0c3fa1a1b320 + # via + # jaraco-classes + # jaraco-functools +nh3==0.2.17 \ + --hash=sha256:0316c25b76289cf23be6b66c77d3608a4fdf537b35426280032f432f14291b9a \ + --hash=sha256:1a814dd7bba1cb0aba5bcb9bebcc88fd801b63e21e2450ae6c52d3b3336bc911 \ + --hash=sha256:1aa52a7def528297f256de0844e8dd680ee279e79583c76d6fa73a978186ddfb \ + --hash=sha256:22c26e20acbb253a5bdd33d432a326d18508a910e4dcf9a3316179860d53345a \ + --hash=sha256:40015514022af31975c0b3bca4014634fa13cb5dc4dbcbc00570acc781316dcc \ + --hash=sha256:40d0741a19c3d645e54efba71cb0d8c475b59135c1e3c580f879ad5514cbf028 \ + --hash=sha256:551672fd71d06cd828e282abdb810d1be24e1abb7ae2543a8fa36a71c1006fe9 \ + --hash=sha256:66f17d78826096291bd264f260213d2b3905e3c7fae6dfc5337d49429f1dc9f3 \ + --hash=sha256:85cdbcca8ef10733bd31f931956f7fbb85145a4d11ab9e6742bbf44d88b7e351 \ + --hash=sha256:a3f55fabe29164ba6026b5ad5c3151c314d136fd67415a17660b4aaddacf1b10 \ + --hash=sha256:b4427ef0d2dfdec10b641ed0bdaf17957eb625b2ec0ea9329b3d28806c153d71 \ + --hash=sha256:ba73a2f8d3a1b966e9cdba7b211779ad8a2561d2dba9674b8a19ed817923f65f \ + --hash=sha256:c21bac1a7245cbd88c0b0e4a420221b7bfa838a2814ee5bb924e9c2f10a1120b \ + --hash=sha256:c551eb2a3876e8ff2ac63dff1585236ed5dfec5ffd82216a7a174f7c5082a78a \ + --hash=sha256:c790769152308421283679a142dbdb3d1c46c79c823008ecea8e8141db1a2062 \ + --hash=sha256:d7a25fd8c86657f5d9d576268e3b3767c5cd4f42867c9383618be8517f0f022a # via readme-renderer -nox==2023.4.22 \ - --hash=sha256:0b1adc619c58ab4fa57d6ab2e7823fe47a32e70202f287d78474adcc7bda1891 \ - --hash=sha256:46c0560b0dc609d7d967dc99e22cb463d3c4caf54a5fda735d6c11b5177e3a9f +nox==2024.4.15 \ + --hash=sha256:6492236efa15a460ecb98e7b67562a28b70da006ab0be164e8821177577c0565 \ + --hash=sha256:ecf6700199cdfa9e5ea0a41ff5e6ef4641d09508eda6edb89d9987864115817f # via -r requirements.in -packaging==23.2 \ - --hash=sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5 \ - --hash=sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7 +packaging==24.1 \ + --hash=sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002 \ + --hash=sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124 # via # gcp-releasetool # nox -pkginfo==1.9.6 \ - --hash=sha256:4b7a555a6d5a22169fcc9cf7bfd78d296b0361adad412a346c1226849af5e546 \ - --hash=sha256:8fd5896e8718a4372f0ea9cc9d96f6417c9b986e23a4d116dda26b62cc29d046 +pkginfo==1.10.0 \ + --hash=sha256:5df73835398d10db79f8eecd5cd86b1f6d29317589ea70796994d49399af6297 \ + --hash=sha256:889a6da2ed7ffc58ab5b900d888ddce90bce912f2d2de1dc1c26f4cb9fe65097 # via twine -platformdirs==3.11.0 \ - --hash=sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3 \ - --hash=sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e +platformdirs==4.2.2 \ + --hash=sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee \ + --hash=sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3 # via virtualenv -protobuf==4.25.3 \ - --hash=sha256:19b270aeaa0099f16d3ca02628546b8baefe2955bbe23224aaf856134eccf1e4 \ - --hash=sha256:209ba4cc916bab46f64e56b85b090607a676f66b473e6b762e6f1d9d591eb2e8 \ - --hash=sha256:25b5d0b42fd000320bd7830b349e3b696435f3b329810427a6bcce6a5492cc5c \ - --hash=sha256:7c8daa26095f82482307bc717364e7c13f4f1c99659be82890dcfc215194554d \ - --hash=sha256:c053062984e61144385022e53678fbded7aea14ebb3e0305ae3592fb219ccfa4 \ - --hash=sha256:d4198877797a83cbfe9bffa3803602bbe1625dc30d8a097365dbc762e5790faa \ - --hash=sha256:e3c97a1555fd6388f857770ff8b9703083de6bf1f9274a002a332d65fbb56c8c \ - --hash=sha256:e7cb0ae90dd83727f0c0718634ed56837bfeeee29a5f82a7514c03ee1364c019 \ - --hash=sha256:f0700d54bcf45424477e46a9f0944155b46fb0639d69728739c0e47bab83f2b9 \ - --hash=sha256:f1279ab38ecbfae7e456a108c5c0681e4956d5b1090027c1de0f934dfdb4b35c \ - --hash=sha256:f4f118245c4a087776e0a8408be33cf09f6c547442c00395fbfb116fac2f8ac2 +proto-plus==1.24.0 \ + --hash=sha256:30b72a5ecafe4406b0d339db35b56c4059064e69227b8c3bda7462397f966445 \ + --hash=sha256:402576830425e5f6ce4c2a6702400ac79897dab0b4343821aa5188b0fab81a12 + # via google-api-core +protobuf==5.27.2 \ + --hash=sha256:0e341109c609749d501986b835f667c6e1e24531096cff9d34ae411595e26505 \ + --hash=sha256:176c12b1f1c880bf7a76d9f7c75822b6a2bc3db2d28baa4d300e8ce4cde7409b \ + --hash=sha256:354d84fac2b0d76062e9b3221f4abbbacdfd2a4d8af36bab0474f3a0bb30ab38 \ + --hash=sha256:4fadd8d83e1992eed0248bc50a4a6361dc31bcccc84388c54c86e530b7f58863 \ + --hash=sha256:54330f07e4949d09614707c48b06d1a22f8ffb5763c159efd5c0928326a91470 \ + --hash=sha256:610e700f02469c4a997e58e328cac6f305f649826853813177e6290416e846c6 \ + --hash=sha256:7fc3add9e6003e026da5fc9e59b131b8f22b428b991ccd53e2af8071687b4fce \ + --hash=sha256:9e8f199bf7f97bd7ecebffcae45ebf9527603549b2b562df0fbc6d4d688f14ca \ + --hash=sha256:a109916aaac42bff84702fb5187f3edadbc7c97fc2c99c5ff81dd15dcce0d1e5 \ + --hash=sha256:b848dbe1d57ed7c191dfc4ea64b8b004a3f9ece4bf4d0d80a367b76df20bf36e \ + --hash=sha256:f3ecdef226b9af856075f28227ff2c90ce3a594d092c39bee5513573f25e2714 # via # gcp-docuploader # gcp-releasetool # google-api-core # googleapis-common-protos -pyasn1==0.5.0 \ - --hash=sha256:87a2121042a1ac9358cabcaf1d07680ff97ee6404333bacca15f76aa8ad01a57 \ - --hash=sha256:97b7290ca68e62a832558ec3976f15cbf911bf5d7c7039d8b861c2a0ece69fde + # proto-plus +pyasn1==0.6.0 \ + --hash=sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c \ + --hash=sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473 # via # pyasn1-modules # rsa -pyasn1-modules==0.3.0 \ - --hash=sha256:5bd01446b736eb9d31512a30d46c1ac3395d676c6f3cafa4c03eb54b9925631c \ - --hash=sha256:d3ccd6ed470d9ffbc716be08bd90efbd44d0734bc9303818f7336070984a162d +pyasn1-modules==0.4.0 \ + --hash=sha256:831dbcea1b177b28c9baddf4c6d1013c24c3accd14a1873fffaa6a2e905f17b6 \ + --hash=sha256:be04f15b66c206eed667e0bb5ab27e2b1855ea54a842e5037738099e8ca4ae0b # via google-auth -pycparser==2.21 \ - --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \ - --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206 +pycparser==2.22 \ + --hash=sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6 \ + --hash=sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc # via cffi -pygments==2.16.1 \ - --hash=sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692 \ - --hash=sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29 +pygments==2.18.0 \ + --hash=sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199 \ + --hash=sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a # via # readme-renderer # rich @@ -434,20 +453,20 @@ pyjwt==2.8.0 \ --hash=sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de \ --hash=sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320 # via gcp-releasetool -pyperclip==1.8.2 \ - --hash=sha256:105254a8b04934f0bc84e9c24eb360a591aaf6535c9def5f29d92af107a9bf57 +pyperclip==1.9.0 \ + --hash=sha256:b7de0142ddc81bfc5c7507eea19da920b92252b548b96186caf94a5e2527d310 # via gcp-releasetool -python-dateutil==2.8.2 \ - --hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \ - --hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9 +python-dateutil==2.9.0.post0 \ + --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ + --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 # via gcp-releasetool -readme-renderer==42.0 \ - --hash=sha256:13d039515c1f24de668e2c93f2e877b9dbe6c6c32328b90a40a49d8b2b85f36d \ - --hash=sha256:2d55489f83be4992fe4454939d1a051c33edbab778e82761d060c9fc6b308cd1 +readme-renderer==43.0 \ + --hash=sha256:1818dd28140813509eeed8d62687f7cd4f7bad90d4db586001c5dc09d4fde311 \ + --hash=sha256:19db308d86ecd60e5affa3b2a98f017af384678c63c88e5d4556a380e674f3f9 # via twine -requests==2.31.0 \ - --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \ - --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1 +requests==2.32.3 \ + --hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \ + --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6 # via # gcp-releasetool # google-api-core @@ -462,9 +481,9 @@ rfc3986==2.0.0 \ --hash=sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd \ --hash=sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c # via twine -rich==13.6.0 \ - --hash=sha256:2b38e2fe9ca72c9a00170a1a2d20c63c790d0e10ef1fe35eba76e1e7b1d7d245 \ - --hash=sha256:5c14d22737e6d5084ef4771b62d5d4363165b403455a30a1c8ca39dc7b644bef +rich==13.7.1 \ + --hash=sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222 \ + --hash=sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432 # via twine rsa==4.9 \ --hash=sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7 \ @@ -480,35 +499,39 @@ six==1.16.0 \ # via # gcp-docuploader # python-dateutil -twine==4.0.2 \ - --hash=sha256:929bc3c280033347a00f847236564d1c52a3e61b1ac2516c97c48f3ceab756d8 \ - --hash=sha256:9e102ef5fdd5a20661eb88fad46338806c3bd32cf1db729603fe3697b1bc83c8 +tomli==2.0.1 \ + --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ + --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f + # via nox +twine==5.1.1 \ + --hash=sha256:215dbe7b4b94c2c50a7315c0275d2258399280fbb7d04182c7e55e24b5f93997 \ + --hash=sha256:9aa0825139c02b3434d913545c7b847a21c835e11597f5255842d457da2322db # via -r requirements.in -typing-extensions==4.8.0 \ - --hash=sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0 \ - --hash=sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef +typing-extensions==4.12.2 \ + --hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \ + --hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8 # via -r requirements.in -urllib3==2.0.7 \ - --hash=sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84 \ - --hash=sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e +urllib3==2.2.2 \ + --hash=sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472 \ + --hash=sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168 # via # requests # twine -virtualenv==20.24.6 \ - --hash=sha256:02ece4f56fbf939dbbc33c0715159951d6bf14aaf5457b092e4548e1382455af \ - --hash=sha256:520d056652454c5098a00c0f073611ccbea4c79089331f60bf9d7ba247bb7381 +virtualenv==20.26.3 \ + --hash=sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a \ + --hash=sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589 # via nox -wheel==0.41.3 \ - --hash=sha256:488609bc63a29322326e05560731bf7bfea8e48ad646e1f5e40d366607de0942 \ - --hash=sha256:4d4987ce51a49370ea65c0bfd2234e8ce80a12780820d9dc462597a6e60d0841 +wheel==0.43.0 \ + --hash=sha256:465ef92c69fa5c5da2d1cf8ac40559a8c940886afcef87dcf14b9470862f1d85 \ + --hash=sha256:55c570405f142630c6b9f72fe09d9b67cf1477fcf543ae5b8dcb1f5b7377da81 # via -r requirements.in -zipp==3.17.0 \ - --hash=sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31 \ - --hash=sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0 +zipp==3.19.2 \ + --hash=sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19 \ + --hash=sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: -setuptools==69.2.0 \ - --hash=sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e \ - --hash=sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c +setuptools==70.2.0 \ + --hash=sha256:b8b8060bb426838fbe942479c90296ce976249451118ef566a5a0b7d8b78fb05 \ + --hash=sha256:bd63e505105011b25c3c11f753f7e3b8465ea739efddaccef8f0efac2137bac1 # via -r requirements.in diff --git a/.kokoro/test-samples-against-head.sh b/.kokoro/test-samples-against-head.sh index 63ac41dfa..e9d8bd79a 100755 --- a/.kokoro/test-samples-against-head.sh +++ b/.kokoro/test-samples-against-head.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2023 Google LLC +# Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/.kokoro/test-samples-impl.sh b/.kokoro/test-samples-impl.sh index 5a0f5fab6..55910c8ba 100755 --- a/.kokoro/test-samples-impl.sh +++ b/.kokoro/test-samples-impl.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2023 Google LLC +# Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/.kokoro/test-samples.sh b/.kokoro/test-samples.sh index 50b35a48c..7933d8201 100755 --- a/.kokoro/test-samples.sh +++ b/.kokoro/test-samples.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2023 Google LLC +# Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/.kokoro/trampoline.sh b/.kokoro/trampoline.sh index d85b1f267..48f796997 100755 --- a/.kokoro/trampoline.sh +++ b/.kokoro/trampoline.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright 2023 Google LLC +# Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/.kokoro/trampoline_v2.sh b/.kokoro/trampoline_v2.sh index 59a7cf3a9..35fa52923 100755 --- a/.kokoro/trampoline_v2.sh +++ b/.kokoro/trampoline_v2.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Copyright 2023 Google LLC +# Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6a8e16950..1d74695f7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/.trampolinerc b/.trampolinerc index a7dfeb42c..008015237 100644 --- a/.trampolinerc +++ b/.trampolinerc @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/MANIFEST.in b/MANIFEST.in index e0a667053..d6814cd60 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright 2023 Google LLC +# Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docs/conf.py b/docs/conf.py index 588e983f9..ad4723c00 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2023 Google LLC +# Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/scripts/decrypt-secrets.sh b/scripts/decrypt-secrets.sh index 0018b421d..120b0ddc4 100755 --- a/scripts/decrypt-secrets.sh +++ b/scripts/decrypt-secrets.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Copyright 2023 Google LLC All rights reserved. +# Copyright 2024 Google LLC All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/scripts/readme-gen/readme_gen.py b/scripts/readme-gen/readme_gen.py index 1acc11983..8f5e248a0 100644 --- a/scripts/readme-gen/readme_gen.py +++ b/scripts/readme-gen/readme_gen.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -# Copyright 2023 Google LLC +# Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. From 3c5e034fdd6f8a98d2fd9d4a927c58c202823767 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Tue, 9 Jul 2024 15:58:30 -0400 Subject: [PATCH 019/101] chore(python): use python 3.10 for docs build (#678) * chore(python): use python 3.10 for docs build Source-Link: https://github.com/googleapis/synthtool/commit/9ae07858520bf035a3d5be569b5a65d960ee4392 Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-python:latest@sha256:52210e0e0559f5ea8c52be148b33504022e1faef4e95fbe4b32d68022af2fa7e * use python 3.10 for docs build --------- Co-authored-by: Owl Bot Co-authored-by: Anthonios Partheniou --- .github/.OwlBot.lock.yaml | 4 +-- .kokoro/docker/docs/Dockerfile | 21 +++++++------ .kokoro/docker/docs/requirements.txt | 40 +++++++++++++----------- .kokoro/requirements.txt | 46 ++++++++++++++-------------- noxfile.py | 5 ++- 5 files changed, 63 insertions(+), 53 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 91d742b5b..f30cb3775 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest - digest: sha256:d3de8a02819f65001effcbd3ea76ce97e9bcff035c7a89457f40f892c87c5b32 -# created: 2024-07-03T17:43:00.77142528Z + digest: sha256:52210e0e0559f5ea8c52be148b33504022e1faef4e95fbe4b32d68022af2fa7e +# created: 2024-07-08T19:25:35.862283192Z diff --git a/.kokoro/docker/docs/Dockerfile b/.kokoro/docker/docs/Dockerfile index a26ce6193..5205308b3 100644 --- a/.kokoro/docker/docs/Dockerfile +++ b/.kokoro/docker/docs/Dockerfile @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from ubuntu:22.04 +from ubuntu:24.04 ENV DEBIAN_FRONTEND noninteractive @@ -40,7 +40,6 @@ RUN apt-get update \ libssl-dev \ libsqlite3-dev \ portaudio19-dev \ - python3-distutils \ redis-server \ software-properties-common \ ssh \ @@ -60,18 +59,22 @@ RUN apt-get update \ && rm -rf /var/lib/apt/lists/* \ && rm -f /var/cache/apt/archives/*.deb -###################### Install python 3.9.13 -# Download python 3.9.13 -RUN wget https://www.python.org/ftp/python/3.9.13/Python-3.9.13.tgz +###################### Install python 3.10.14 for docs/docfx session + +# Download python 3.10.14 +RUN wget https://www.python.org/ftp/python/3.10.14/Python-3.10.14.tgz # Extract files -RUN tar -xvf Python-3.9.13.tgz +RUN tar -xvf Python-3.10.14.tgz -# Install python 3.9.13 -RUN ./Python-3.9.13/configure --enable-optimizations +# Install python 3.10.14 +RUN ./Python-3.10.14/configure --enable-optimizations RUN make altinstall +RUN python3.10 -m venv /venv +ENV PATH /venv/bin:$PATH + ###################### Install pip RUN wget -O /tmp/get-pip.py 'https://bootstrap.pypa.io/get-pip.py' \ && python3 /tmp/get-pip.py \ @@ -84,4 +87,4 @@ RUN python3 -m pip COPY requirements.txt /requirements.txt RUN python3 -m pip install --require-hashes -r requirements.txt -CMD ["python3.8"] +CMD ["python3.10"] diff --git a/.kokoro/docker/docs/requirements.txt b/.kokoro/docker/docs/requirements.txt index 0e5d70f20..7129c7715 100644 --- a/.kokoro/docker/docs/requirements.txt +++ b/.kokoro/docker/docs/requirements.txt @@ -4,9 +4,9 @@ # # pip-compile --allow-unsafe --generate-hashes requirements.in # -argcomplete==3.2.3 \ - --hash=sha256:bf7900329262e481be5a15f56f19736b376df6f82ed27576fa893652c5de6c23 \ - --hash=sha256:c12355e0494c76a2a7b73e3a59b09024ca0ba1e279fb9ed6c1b82d5b74b6a70c +argcomplete==3.4.0 \ + --hash=sha256:69a79e083a716173e5532e0fa3bef45f793f4e61096cf52b5a42c0211c8b8aa5 \ + --hash=sha256:c2abcdfe1be8ace47ba777d4fce319eb13bf8ad9dace8d085dcad6eded88057f # via nox colorlog==6.8.2 \ --hash=sha256:3e3e079a41feb5a1b64f978b5ea4f46040a94f11f0e8bbb8261e3dbbeca64d44 \ @@ -16,23 +16,27 @@ distlib==0.3.8 \ --hash=sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784 \ --hash=sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64 # via virtualenv -filelock==3.13.1 \ - --hash=sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e \ - --hash=sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c +filelock==3.15.4 \ + --hash=sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb \ + --hash=sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7 # via virtualenv -nox==2024.3.2 \ - --hash=sha256:e53514173ac0b98dd47585096a55572fe504fecede58ced708979184d05440be \ - --hash=sha256:f521ae08a15adbf5e11f16cb34e8d0e6ea521e0b92868f684e91677deb974553 +nox==2024.4.15 \ + --hash=sha256:6492236efa15a460ecb98e7b67562a28b70da006ab0be164e8821177577c0565 \ + --hash=sha256:ecf6700199cdfa9e5ea0a41ff5e6ef4641d09508eda6edb89d9987864115817f # via -r requirements.in -packaging==24.0 \ - --hash=sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5 \ - --hash=sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9 +packaging==24.1 \ + --hash=sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002 \ + --hash=sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124 # via nox -platformdirs==4.2.0 \ - --hash=sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068 \ - --hash=sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768 +platformdirs==4.2.2 \ + --hash=sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee \ + --hash=sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3 # via virtualenv -virtualenv==20.25.1 \ - --hash=sha256:961c026ac520bac5f69acb8ea063e8a4f071bcc9457b9c1f28f6b085c511583a \ - --hash=sha256:e08e13ecdca7a0bd53798f356d5831434afa5b07b93f0abdf0797b7a06ffe197 +tomli==2.0.1 \ + --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ + --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f + # via nox +virtualenv==20.26.3 \ + --hash=sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a \ + --hash=sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589 # via nox diff --git a/.kokoro/requirements.txt b/.kokoro/requirements.txt index 35ece0e4d..9622baf0b 100644 --- a/.kokoro/requirements.txt +++ b/.kokoro/requirements.txt @@ -20,9 +20,9 @@ cachetools==5.3.3 \ --hash=sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945 \ --hash=sha256:ba29e2dfa0b8b556606f097407ed1aa62080ee108ab0dc5ec9d6a723a007d105 # via google-auth -certifi==2024.6.2 \ - --hash=sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516 \ - --hash=sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56 +certifi==2024.7.4 \ + --hash=sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b \ + --hash=sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90 # via requests cffi==1.16.0 \ --hash=sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc \ @@ -371,23 +371,23 @@ more-itertools==10.3.0 \ # via # jaraco-classes # jaraco-functools -nh3==0.2.17 \ - --hash=sha256:0316c25b76289cf23be6b66c77d3608a4fdf537b35426280032f432f14291b9a \ - --hash=sha256:1a814dd7bba1cb0aba5bcb9bebcc88fd801b63e21e2450ae6c52d3b3336bc911 \ - --hash=sha256:1aa52a7def528297f256de0844e8dd680ee279e79583c76d6fa73a978186ddfb \ - --hash=sha256:22c26e20acbb253a5bdd33d432a326d18508a910e4dcf9a3316179860d53345a \ - --hash=sha256:40015514022af31975c0b3bca4014634fa13cb5dc4dbcbc00570acc781316dcc \ - --hash=sha256:40d0741a19c3d645e54efba71cb0d8c475b59135c1e3c580f879ad5514cbf028 \ - --hash=sha256:551672fd71d06cd828e282abdb810d1be24e1abb7ae2543a8fa36a71c1006fe9 \ - --hash=sha256:66f17d78826096291bd264f260213d2b3905e3c7fae6dfc5337d49429f1dc9f3 \ - --hash=sha256:85cdbcca8ef10733bd31f931956f7fbb85145a4d11ab9e6742bbf44d88b7e351 \ - --hash=sha256:a3f55fabe29164ba6026b5ad5c3151c314d136fd67415a17660b4aaddacf1b10 \ - --hash=sha256:b4427ef0d2dfdec10b641ed0bdaf17957eb625b2ec0ea9329b3d28806c153d71 \ - --hash=sha256:ba73a2f8d3a1b966e9cdba7b211779ad8a2561d2dba9674b8a19ed817923f65f \ - --hash=sha256:c21bac1a7245cbd88c0b0e4a420221b7bfa838a2814ee5bb924e9c2f10a1120b \ - --hash=sha256:c551eb2a3876e8ff2ac63dff1585236ed5dfec5ffd82216a7a174f7c5082a78a \ - --hash=sha256:c790769152308421283679a142dbdb3d1c46c79c823008ecea8e8141db1a2062 \ - --hash=sha256:d7a25fd8c86657f5d9d576268e3b3767c5cd4f42867c9383618be8517f0f022a +nh3==0.2.18 \ + --hash=sha256:0411beb0589eacb6734f28d5497ca2ed379eafab8ad8c84b31bb5c34072b7164 \ + --hash=sha256:14c5a72e9fe82aea5fe3072116ad4661af5cf8e8ff8fc5ad3450f123e4925e86 \ + --hash=sha256:19aaba96e0f795bd0a6c56291495ff59364f4300d4a39b29a0abc9cb3774a84b \ + --hash=sha256:34c03fa78e328c691f982b7c03d4423bdfd7da69cd707fe572f544cf74ac23ad \ + --hash=sha256:36c95d4b70530b320b365659bb5034341316e6a9b30f0b25fa9c9eff4c27a204 \ + --hash=sha256:3a157ab149e591bb638a55c8c6bcb8cdb559c8b12c13a8affaba6cedfe51713a \ + --hash=sha256:42c64511469005058cd17cc1537578eac40ae9f7200bedcfd1fc1a05f4f8c200 \ + --hash=sha256:5f36b271dae35c465ef5e9090e1fdaba4a60a56f0bb0ba03e0932a66f28b9189 \ + --hash=sha256:6955369e4d9f48f41e3f238a9e60f9410645db7e07435e62c6a9ea6135a4907f \ + --hash=sha256:7b7c2a3c9eb1a827d42539aa64091640bd275b81e097cd1d8d82ef91ffa2e811 \ + --hash=sha256:8ce0f819d2f1933953fca255db2471ad58184a60508f03e6285e5114b6254844 \ + --hash=sha256:94a166927e53972a9698af9542ace4e38b9de50c34352b962f4d9a7d4c927af4 \ + --hash=sha256:a7f1b5b2c15866f2db413a3649a8fe4fd7b428ae58be2c0f6bca5eefd53ca2be \ + --hash=sha256:c8b3a1cebcba9b3669ed1a84cc65bf005728d2f0bc1ed2a6594a992e817f3a50 \ + --hash=sha256:de3ceed6e661954871d6cd78b410213bdcb136f79aafe22aa7182e028b8c7307 \ + --hash=sha256:f0eca9ca8628dbb4e916ae2491d72957fdd35f7a5d326b7032a345f111ac07fe # via readme-renderer nox==2024.4.15 \ --hash=sha256:6492236efa15a460ecb98e7b67562a28b70da006ab0be164e8821177577c0565 \ @@ -460,9 +460,9 @@ python-dateutil==2.9.0.post0 \ --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 # via gcp-releasetool -readme-renderer==43.0 \ - --hash=sha256:1818dd28140813509eeed8d62687f7cd4f7bad90d4db586001c5dc09d4fde311 \ - --hash=sha256:19db308d86ecd60e5affa3b2a98f017af384678c63c88e5d4556a380e674f3f9 +readme-renderer==44.0 \ + --hash=sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151 \ + --hash=sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1 # via twine requests==2.32.3 \ --hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \ diff --git a/noxfile.py b/noxfile.py index 593bfc856..1e6a570a6 100644 --- a/noxfile.py +++ b/noxfile.py @@ -47,6 +47,9 @@ "docs", ] +# Error if a python version is missing +nox.options.error_on_missing_interpreters = True + @nox.session(python=DEFAULT_PYTHON_VERSION) def lint(session): @@ -267,7 +270,7 @@ def cover(session): session.run("coverage", "erase") -@nox.session(python="3.9") +@nox.session(python="3.10") def docs(session): """Build the docs for this library.""" From 6d1b96bf8cafc38c782d6ecd5f541318899db249 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Thu, 1 Aug 2024 10:27:05 -0400 Subject: [PATCH 020/101] build: use mypy<1.11.0 until #682 is fixed (#683) * build: use mypy<1.11.0 until #682 is fixed * add comment --- noxfile.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index 1e6a570a6..a15795ea1 100644 --- a/noxfile.py +++ b/noxfile.py @@ -247,7 +247,9 @@ def pytype(session): @nox.session(python=DEFAULT_PYTHON_VERSION) def mypy(session): """Run type-checking.""" - session.install(".[grpc]", "mypy") + # TODO(https://github.com/googleapis/python-api-core/issues/682): + # Use the latest version of mypy instead of mypy<1.11.0 + session.install(".[grpc]", "mypy<1.11.0") session.install( "types-setuptools", "types-requests", From 7ccbf5738fa236649f9a155055c71789362b5c4c Mon Sep 17 00:00:00 2001 From: NickGoog <66492516+NickGoog@users.noreply.github.com> Date: Wed, 7 Aug 2024 12:41:35 -0400 Subject: [PATCH 021/101] fix: Fail gracefully if could not import `rpc_status` module (#680) * fix: Fail gracefully if could not import rpc_status module * revert --------- Co-authored-by: Anthonios Partheniou --- google/api_core/exceptions.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/google/api_core/exceptions.py b/google/api_core/exceptions.py index d4cb99733..74f46ef39 100644 --- a/google/api_core/exceptions.py +++ b/google/api_core/exceptions.py @@ -28,16 +28,21 @@ from google.rpc import error_details_pb2 + +def _warn_could_not_import_grpcio_status(): + warnings.warn( + "Please install grpcio-status to obtain helpful grpc error messages.", + ImportWarning, + ) # pragma: NO COVER + + try: import grpc try: from grpc_status import rpc_status except ImportError: # pragma: NO COVER - warnings.warn( - "Please install grpcio-status to obtain helpful grpc error messages.", - ImportWarning, - ) + _warn_could_not_import_grpcio_status() rpc_status = None except ImportError: # pragma: NO COVER grpc = None @@ -560,6 +565,9 @@ def _is_informative_grpc_error(rpc_exc): def _parse_grpc_error_details(rpc_exc): + if not rpc_status: # pragma: NO COVER + _warn_could_not_import_grpcio_status() + return [], None try: status = rpc_status.from_call(rpc_exc) except NotImplementedError: # workaround From b5ddd6236560f70405c0ea8614d67b27230341c5 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Fri, 16 Aug 2024 15:45:22 -0700 Subject: [PATCH 022/101] chore(python): fix docs build (#681) Source-Link: https://github.com/googleapis/synthtool/commit/bef813d194de29ddf3576eda60148b6b3dcc93d9 Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-python:latest@sha256:94bb690db96e6242b2567a4860a94d48fa48696d092e51b0884a1a2c0a79a407 Co-authored-by: Owl Bot Co-authored-by: Anthonios Partheniou Co-authored-by: Victor Chudnovsky --- .github/.OwlBot.lock.yaml | 4 ++-- .kokoro/docker/docs/Dockerfile | 9 ++++----- .kokoro/publish-docs.sh | 20 ++++++++++---------- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index f30cb3775..6d064ddb9 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest - digest: sha256:52210e0e0559f5ea8c52be148b33504022e1faef4e95fbe4b32d68022af2fa7e -# created: 2024-07-08T19:25:35.862283192Z + digest: sha256:94bb690db96e6242b2567a4860a94d48fa48696d092e51b0884a1a2c0a79a407 +# created: 2024-07-31T14:52:44.926548819Z diff --git a/.kokoro/docker/docs/Dockerfile b/.kokoro/docker/docs/Dockerfile index 5205308b3..e5410e296 100644 --- a/.kokoro/docker/docs/Dockerfile +++ b/.kokoro/docker/docs/Dockerfile @@ -72,19 +72,18 @@ RUN tar -xvf Python-3.10.14.tgz RUN ./Python-3.10.14/configure --enable-optimizations RUN make altinstall -RUN python3.10 -m venv /venv -ENV PATH /venv/bin:$PATH +ENV PATH /usr/local/bin/python3.10:$PATH ###################### Install pip RUN wget -O /tmp/get-pip.py 'https://bootstrap.pypa.io/get-pip.py' \ - && python3 /tmp/get-pip.py \ + && python3.10 /tmp/get-pip.py \ && rm /tmp/get-pip.py # Test pip -RUN python3 -m pip +RUN python3.10 -m pip # Install build requirements COPY requirements.txt /requirements.txt -RUN python3 -m pip install --require-hashes -r requirements.txt +RUN python3.10 -m pip install --require-hashes -r requirements.txt CMD ["python3.10"] diff --git a/.kokoro/publish-docs.sh b/.kokoro/publish-docs.sh index 38f083f05..233205d58 100755 --- a/.kokoro/publish-docs.sh +++ b/.kokoro/publish-docs.sh @@ -21,18 +21,18 @@ export PYTHONUNBUFFERED=1 export PATH="${HOME}/.local/bin:${PATH}" # Install nox -python3 -m pip install --require-hashes -r .kokoro/requirements.txt -python3 -m nox --version +python3.10 -m pip install --require-hashes -r .kokoro/requirements.txt +python3.10 -m nox --version # build docs nox -s docs # create metadata -python3 -m docuploader create-metadata \ +python3.10 -m docuploader create-metadata \ --name=$(jq --raw-output '.name // empty' .repo-metadata.json) \ - --version=$(python3 setup.py --version) \ + --version=$(python3.10 setup.py --version) \ --language=$(jq --raw-output '.language // empty' .repo-metadata.json) \ - --distribution-name=$(python3 setup.py --name) \ + --distribution-name=$(python3.10 setup.py --name) \ --product-page=$(jq --raw-output '.product_documentation // empty' .repo-metadata.json) \ --github-repository=$(jq --raw-output '.repo // empty' .repo-metadata.json) \ --issue-tracker=$(jq --raw-output '.issue_tracker // empty' .repo-metadata.json) @@ -40,18 +40,18 @@ python3 -m docuploader create-metadata \ cat docs.metadata # upload docs -python3 -m docuploader upload docs/_build/html --metadata-file docs.metadata --staging-bucket "${STAGING_BUCKET}" +python3.10 -m docuploader upload docs/_build/html --metadata-file docs.metadata --staging-bucket "${STAGING_BUCKET}" # docfx yaml files nox -s docfx # create metadata. -python3 -m docuploader create-metadata \ +python3.10 -m docuploader create-metadata \ --name=$(jq --raw-output '.name // empty' .repo-metadata.json) \ - --version=$(python3 setup.py --version) \ + --version=$(python3.10 setup.py --version) \ --language=$(jq --raw-output '.language // empty' .repo-metadata.json) \ - --distribution-name=$(python3 setup.py --name) \ + --distribution-name=$(python3.10 setup.py --name) \ --product-page=$(jq --raw-output '.product_documentation // empty' .repo-metadata.json) \ --github-repository=$(jq --raw-output '.repo // empty' .repo-metadata.json) \ --issue-tracker=$(jq --raw-output '.issue_tracker // empty' .repo-metadata.json) @@ -59,4 +59,4 @@ python3 -m docuploader create-metadata \ cat docs.metadata # upload docs -python3 -m docuploader upload docs/_build/html/docfx_yaml --metadata-file docs.metadata --destination-prefix docfx --staging-bucket "${V2_STAGING_BUCKET}" +python3.10 -m docuploader upload docs/_build/html/docfx_yaml --metadata-file docs.metadata --destination-prefix docfx --staging-bucket "${V2_STAGING_BUCKET}" From ce8d45d9664ab5550a356c768a8cd285e0910152 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 27 Aug 2024 16:31:21 -0400 Subject: [PATCH 023/101] chore(main): release 2.19.2 (#685) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ google/api_core/version.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e48409f42..a25a39c87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://pypi.org/project/google-api-core/#history +## [2.19.2](https://github.com/googleapis/python-api-core/compare/v2.19.1...v2.19.2) (2024-08-16) + + +### Bug Fixes + +* Fail gracefully if could not import `rpc_status` module ([#680](https://github.com/googleapis/python-api-core/issues/680)) ([7ccbf57](https://github.com/googleapis/python-api-core/commit/7ccbf5738fa236649f9a155055c71789362b5c4c)) + ## [2.19.1](https://github.com/googleapis/python-api-core/compare/v2.19.0...v2.19.1) (2024-06-19) diff --git a/google/api_core/version.py b/google/api_core/version.py index 25b6f2f82..4b0e3c81d 100644 --- a/google/api_core/version.py +++ b/google/api_core/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "2.19.1" +__version__ = "2.19.2" From 082bce2305c2b6feeaaf2bd3da57012e1357a139 Mon Sep 17 00:00:00 2001 From: ohmayr Date: Mon, 9 Sep 2024 21:15:39 -0400 Subject: [PATCH 024/101] chore: update unittest workflow template (#692) --- .github/workflows/unittest.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 34d29b7c1..8adc535e0 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -47,6 +47,7 @@ jobs: with: name: coverage-artifact-${{ matrix.option }}-${{ matrix.python }} path: .coverage${{ matrix.option }}-${{ matrix.python }} + include-hidden-files: true report-coverage: name: cover From 1c4b0d079f2103a7b5562371a7bd1ada92528de3 Mon Sep 17 00:00:00 2001 From: ohmayr Date: Tue, 10 Sep 2024 14:22:03 -0400 Subject: [PATCH 025/101] feat: Add support for creating exceptions from an asynchronous response (#688) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add suport for mapping exceptions to rest callables * avoid wrapping errors for rest callable * fix lint issues * add test coverage * address PR comments * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * fix lint issues * fix for none type method --------- Co-authored-by: Owl Bot --- google/api_core/exceptions.py | 60 ++++++++++++++++++------ google/api_core/gapic_v1/method_async.py | 6 ++- tests/asyncio/gapic/test_method_async.py | 11 +++++ 3 files changed, 61 insertions(+), 16 deletions(-) diff --git a/google/api_core/exceptions.py b/google/api_core/exceptions.py index 74f46ef39..8dc8c08f0 100644 --- a/google/api_core/exceptions.py +++ b/google/api_core/exceptions.py @@ -22,7 +22,7 @@ from __future__ import unicode_literals import http.client -from typing import Dict +from typing import Optional, Dict from typing import Union import warnings @@ -476,22 +476,37 @@ def from_http_status(status_code, message, **kwargs): return error -def from_http_response(response): - """Create a :class:`GoogleAPICallError` from a :class:`requests.Response`. +def _format_rest_error_message(error, method, url): + method = method.upper() if method else None + message = "{method} {url}: {error}".format( + method=method, + url=url, + error=error, + ) + return message + + +# NOTE: We're moving away from `from_http_status` because it expects an aiohttp response compared +# to `format_http_response_error` which expects a more abstract response from google.auth and is +# compatible with both sync and async response types. +# TODO(https://github.com/googleapis/python-api-core/issues/691): Add type hint for response. +def format_http_response_error( + response, method: str, url: str, payload: Optional[Dict] = None +): + """Create a :class:`GoogleAPICallError` from a google auth rest response. Args: - response (requests.Response): The HTTP response. + response Union[google.auth.transport.Response, google.auth.aio.transport.Response]: The HTTP response. + method Optional(str): The HTTP request method. + url Optional(str): The HTTP request url. + payload Optional(dict): The HTTP response payload. If not passed in, it is read from response for a response type of google.auth.transport.Response. Returns: GoogleAPICallError: An instance of the appropriate subclass of :class:`GoogleAPICallError`, with the message and errors populated from the response. """ - try: - payload = response.json() - except ValueError: - payload = {"error": {"message": response.text or "unknown error"}} - + payload = {} if not payload else payload error_message = payload.get("error", {}).get("message", "unknown error") errors = payload.get("error", {}).get("errors", ()) # In JSON, details are already formatted in developer-friendly way. @@ -504,12 +519,7 @@ def from_http_response(response): ) ) error_info = error_info[0] if error_info else None - - message = "{method} {url}: {error}".format( - method=response.request.method, - url=response.request.url, - error=error_message, - ) + message = _format_rest_error_message(error_message, method, url) exception = from_http_status( response.status_code, @@ -522,6 +532,26 @@ def from_http_response(response): return exception +def from_http_response(response): + """Create a :class:`GoogleAPICallError` from a :class:`requests.Response`. + + Args: + response (requests.Response): The HTTP response. + + Returns: + GoogleAPICallError: An instance of the appropriate subclass of + :class:`GoogleAPICallError`, with the message and errors populated + from the response. + """ + try: + payload = response.json() + except ValueError: + payload = {"error": {"message": response.text or "unknown error"}} + return format_http_response_error( + response, response.request.method, response.request.url, payload + ) + + def exception_class_for_grpc_status(status_code): """Return the exception class for a specific :class:`grpc.StatusCode`. diff --git a/google/api_core/gapic_v1/method_async.py b/google/api_core/gapic_v1/method_async.py index 24880756d..c0f38c0e8 100644 --- a/google/api_core/gapic_v1/method_async.py +++ b/google/api_core/gapic_v1/method_async.py @@ -25,6 +25,8 @@ from google.api_core.gapic_v1.method import DEFAULT # noqa: F401 from google.api_core.gapic_v1.method import USE_DEFAULT_METADATA # noqa: F401 +_DEFAULT_ASYNC_TRANSPORT_KIND = "grpc_asyncio" + def wrap_method( func, @@ -32,6 +34,7 @@ def wrap_method( default_timeout=None, default_compression=None, client_info=client_info.DEFAULT_CLIENT_INFO, + kind=_DEFAULT_ASYNC_TRANSPORT_KIND, ): """Wrap an async RPC method with common behavior. @@ -40,7 +43,8 @@ def wrap_method( and ``compression`` arguments and applies the common error mapping, retry, timeout, metadata, and compression behavior to the low-level RPC method. """ - func = grpc_helpers_async.wrap_errors(func) + if kind == _DEFAULT_ASYNC_TRANSPORT_KIND: + func = grpc_helpers_async.wrap_errors(func) metadata = [client_info.to_grpc_metadata()] if client_info is not None else None diff --git a/tests/asyncio/gapic/test_method_async.py b/tests/asyncio/gapic/test_method_async.py index ee2069796..f64157b44 100644 --- a/tests/asyncio/gapic/test_method_async.py +++ b/tests/asyncio/gapic/test_method_async.py @@ -252,3 +252,14 @@ async def test_wrap_method_with_overriding_timeout_as_a_number(): assert result == 42 method.assert_called_once_with(timeout=22, metadata=mock.ANY) + + +@pytest.mark.asyncio +async def test_wrap_method_without_wrap_errors(): + fake_call = mock.AsyncMock() + + wrapped_method = gapic_v1.method_async.wrap_method(fake_call, kind="rest") + with mock.patch("google.api_core.grpc_helpers_async.wrap_errors") as method: + await wrapped_method() + + method.assert_not_called() From 8c137feb6e880fdd93d1248d9b6c10002dc3c096 Mon Sep 17 00:00:00 2001 From: ohmayr Date: Tue, 10 Sep 2024 16:50:29 -0400 Subject: [PATCH 026/101] feat: add async unsupported paramater exception (#694) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add async unsupported paramater exception * Update google/api_core/exceptions.py Co-authored-by: Victor Chudnovsky * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: Victor Chudnovsky Co-authored-by: Owl Bot --- google/api_core/exceptions.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/google/api_core/exceptions.py b/google/api_core/exceptions.py index 8dc8c08f0..5b25d1240 100644 --- a/google/api_core/exceptions.py +++ b/google/api_core/exceptions.py @@ -442,6 +442,12 @@ class DeadlineExceeded(GatewayTimeout): grpc_status_code = grpc.StatusCode.DEADLINE_EXCEEDED if grpc is not None else None +class AsyncRestUnsupportedParameterError(NotImplementedError): + """Raised when an unsupported parameter is configured against async rest transport.""" + + pass + + def exception_class_for_http_status(status_code): """Return the exception class for a specific HTTP status code. From e5421244cbf7b11c4104235b0391cbb1d4fa68b8 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Tue, 17 Sep 2024 11:06:42 -0400 Subject: [PATCH 027/101] build(python): release script update (#699) Source-Link: https://github.com/googleapis/synthtool/commit/71a72973dddbc66ea64073b53eda49f0d22e0942 Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-python:latest@sha256:e8dcfd7cbfd8beac3a3ff8d3f3185287ea0625d859168cc80faccfc9a7a00455 Co-authored-by: Owl Bot --- .github/.OwlBot.lock.yaml | 4 ++-- .kokoro/release.sh | 2 +- .kokoro/release/common.cfg | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 6d064ddb9..597e0c326 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest - digest: sha256:94bb690db96e6242b2567a4860a94d48fa48696d092e51b0884a1a2c0a79a407 -# created: 2024-07-31T14:52:44.926548819Z + digest: sha256:e8dcfd7cbfd8beac3a3ff8d3f3185287ea0625d859168cc80faccfc9a7a00455 +# created: 2024-09-16T21:04:09.091105552Z diff --git a/.kokoro/release.sh b/.kokoro/release.sh index d2734b2cc..dd1331c65 100755 --- a/.kokoro/release.sh +++ b/.kokoro/release.sh @@ -23,7 +23,7 @@ python3 -m releasetool publish-reporter-script > /tmp/publisher-script; source / export PYTHONUNBUFFERED=1 # Move into the package, build the distribution and upload. -TWINE_PASSWORD=$(cat "${KOKORO_KEYSTORE_DIR}/73713_google-cloud-pypi-token-keystore-1") +TWINE_PASSWORD=$(cat "${KOKORO_KEYSTORE_DIR}/73713_google-cloud-pypi-token-keystore-2") cd github/python-api-core python3 setup.py sdist bdist_wheel twine upload --username __token__ --password "${TWINE_PASSWORD}" dist/* diff --git a/.kokoro/release/common.cfg b/.kokoro/release/common.cfg index 5890a7fe3..bb8198fbc 100644 --- a/.kokoro/release/common.cfg +++ b/.kokoro/release/common.cfg @@ -28,7 +28,7 @@ before_action { fetch_keystore { keystore_resource { keystore_config_id: 73713 - keyname: "google-cloud-pypi-token-keystore-1" + keyname: "google-cloud-pypi-token-keystore-2" } } } From 1b7bb6d1b721e4ee1561e8e4a347846d7fdd7c27 Mon Sep 17 00:00:00 2001 From: ohmayr Date: Wed, 18 Sep 2024 11:36:01 -0400 Subject: [PATCH 028/101] feat: add support for asynchronous rest streaming (#686) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * duplicating file to base * restore original file * duplicate file to async * restore original file * duplicate test file for async * restore test file * feat: add support for asynchronous rest streaming * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * fix naming issue * fix import module name * pull auth feature branch * revert setup file * address PR comments * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * run black * address PR comments * update nox coverage * address PR comments * fix nox session name in workflow * use https for remote repo * add context manager methods * address PR comments * update auth error versions * update import error --------- Co-authored-by: Owl Bot --- .github/workflows/unittest.yml | 2 +- google/api_core/_rest_streaming_base.py | 118 +++++++ google/api_core/rest_streaming.py | 82 +---- google/api_core/rest_streaming_async.py | 83 +++++ noxfile.py | 14 +- tests/asyncio/test_rest_streaming_async.py | 378 +++++++++++++++++++++ tests/helpers.py | 71 ++++ tests/unit/test_rest_streaming.py | 59 +--- 8 files changed, 679 insertions(+), 128 deletions(-) create mode 100644 google/api_core/_rest_streaming_base.py create mode 100644 google/api_core/rest_streaming_async.py create mode 100644 tests/asyncio/test_rest_streaming_async.py create mode 100644 tests/helpers.py diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 8adc535e0..5980f825b 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - option: ["", "_grpc_gcp", "_wo_grpc", "_with_prerelease_deps"] + option: ["", "_grpc_gcp", "_wo_grpc", "_with_prerelease_deps", "_with_auth_aio"] python: - "3.7" - "3.8" diff --git a/google/api_core/_rest_streaming_base.py b/google/api_core/_rest_streaming_base.py new file mode 100644 index 000000000..3bc87a963 --- /dev/null +++ b/google/api_core/_rest_streaming_base.py @@ -0,0 +1,118 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Helpers for server-side streaming in REST.""" + +from collections import deque +import string +from typing import Deque, Union +import types + +import proto +import google.protobuf.message +from google.protobuf.json_format import Parse + + +class BaseResponseIterator: + """Base Iterator over REST API responses. This class should not be used directly. + + Args: + response_message_cls (Union[proto.Message, google.protobuf.message.Message]): A response + class expected to be returned from an API. + + Raises: + ValueError: If `response_message_cls` is not a subclass of `proto.Message` or `google.protobuf.message.Message`. + """ + + def __init__( + self, + response_message_cls: Union[proto.Message, google.protobuf.message.Message], + ): + self._response_message_cls = response_message_cls + # Contains a list of JSON responses ready to be sent to user. + self._ready_objs: Deque[str] = deque() + # Current JSON response being built. + self._obj = "" + # Keeps track of the nesting level within a JSON object. + self._level = 0 + # Keeps track whether HTTP response is currently sending values + # inside of a string value. + self._in_string = False + # Whether an escape symbol "\" was encountered. + self._escape_next = False + + self._grab = types.MethodType(self._create_grab(), self) + + def _process_chunk(self, chunk: str): + if self._level == 0: + if chunk[0] != "[": + raise ValueError( + "Can only parse array of JSON objects, instead got %s" % chunk + ) + for char in chunk: + if char == "{": + if self._level == 1: + # Level 1 corresponds to the outermost JSON object + # (i.e. the one we care about). + self._obj = "" + if not self._in_string: + self._level += 1 + self._obj += char + elif char == "}": + self._obj += char + if not self._in_string: + self._level -= 1 + if not self._in_string and self._level == 1: + self._ready_objs.append(self._obj) + elif char == '"': + # Helps to deal with an escaped quotes inside of a string. + if not self._escape_next: + self._in_string = not self._in_string + self._obj += char + elif char in string.whitespace: + if self._in_string: + self._obj += char + elif char == "[": + if self._level == 0: + self._level += 1 + else: + self._obj += char + elif char == "]": + if self._level == 1: + self._level -= 1 + else: + self._obj += char + else: + self._obj += char + self._escape_next = not self._escape_next if char == "\\" else False + + def _create_grab(self): + if issubclass(self._response_message_cls, proto.Message): + + def grab(this): + return this._response_message_cls.from_json( + this._ready_objs.popleft(), ignore_unknown_fields=True + ) + + return grab + elif issubclass(self._response_message_cls, google.protobuf.message.Message): + + def grab(this): + return Parse(this._ready_objs.popleft(), this._response_message_cls()) + + return grab + else: + raise ValueError( + "Response message class must be a subclass of proto.Message or google.protobuf.message.Message." + ) diff --git a/google/api_core/rest_streaming.py b/google/api_core/rest_streaming.py index 88bcb31bd..84aa270c0 100644 --- a/google/api_core/rest_streaming.py +++ b/google/api_core/rest_streaming.py @@ -14,17 +14,15 @@ """Helpers for server-side streaming in REST.""" -from collections import deque -import string -from typing import Deque, Union +from typing import Union import proto import requests import google.protobuf.message -from google.protobuf.json_format import Parse +from google.api_core._rest_streaming_base import BaseResponseIterator -class ResponseIterator: +class ResponseIterator(BaseResponseIterator): """Iterator over REST API responses. Args: @@ -33,7 +31,8 @@ class ResponseIterator: class expected to be returned from an API. Raises: - ValueError: If `response_message_cls` is not a subclass of `proto.Message` or `google.protobuf.message.Message`. + ValueError: + - If `response_message_cls` is not a subclass of `proto.Message` or `google.protobuf.message.Message`. """ def __init__( @@ -42,68 +41,16 @@ def __init__( response_message_cls: Union[proto.Message, google.protobuf.message.Message], ): self._response = response - self._response_message_cls = response_message_cls # Inner iterator over HTTP response's content. self._response_itr = self._response.iter_content(decode_unicode=True) - # Contains a list of JSON responses ready to be sent to user. - self._ready_objs: Deque[str] = deque() - # Current JSON response being built. - self._obj = "" - # Keeps track of the nesting level within a JSON object. - self._level = 0 - # Keeps track whether HTTP response is currently sending values - # inside of a string value. - self._in_string = False - # Whether an escape symbol "\" was encountered. - self._escape_next = False + super(ResponseIterator, self).__init__( + response_message_cls=response_message_cls + ) def cancel(self): """Cancel existing streaming operation.""" self._response.close() - def _process_chunk(self, chunk: str): - if self._level == 0: - if chunk[0] != "[": - raise ValueError( - "Can only parse array of JSON objects, instead got %s" % chunk - ) - for char in chunk: - if char == "{": - if self._level == 1: - # Level 1 corresponds to the outermost JSON object - # (i.e. the one we care about). - self._obj = "" - if not self._in_string: - self._level += 1 - self._obj += char - elif char == "}": - self._obj += char - if not self._in_string: - self._level -= 1 - if not self._in_string and self._level == 1: - self._ready_objs.append(self._obj) - elif char == '"': - # Helps to deal with an escaped quotes inside of a string. - if not self._escape_next: - self._in_string = not self._in_string - self._obj += char - elif char in string.whitespace: - if self._in_string: - self._obj += char - elif char == "[": - if self._level == 0: - self._level += 1 - else: - self._obj += char - elif char == "]": - if self._level == 1: - self._level -= 1 - else: - self._obj += char - else: - self._obj += char - self._escape_next = not self._escape_next if char == "\\" else False - def __next__(self): while not self._ready_objs: try: @@ -115,18 +62,5 @@ def __next__(self): raise e return self._grab() - def _grab(self): - # Add extra quotes to make json.loads happy. - if issubclass(self._response_message_cls, proto.Message): - return self._response_message_cls.from_json( - self._ready_objs.popleft(), ignore_unknown_fields=True - ) - elif issubclass(self._response_message_cls, google.protobuf.message.Message): - return Parse(self._ready_objs.popleft(), self._response_message_cls()) - else: - raise ValueError( - "Response message class must be a subclass of proto.Message or google.protobuf.message.Message." - ) - def __iter__(self): return self diff --git a/google/api_core/rest_streaming_async.py b/google/api_core/rest_streaming_async.py new file mode 100644 index 000000000..d1f996f61 --- /dev/null +++ b/google/api_core/rest_streaming_async.py @@ -0,0 +1,83 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Helpers for asynchronous server-side streaming in REST.""" + +from typing import Union + +import proto + +try: + import google.auth.aio.transport +except ImportError as e: # pragma: NO COVER + raise ImportError( + "google-auth>=2.35.0 is required to use asynchronous rest streaming." + ) from e + +import google.protobuf.message +from google.api_core._rest_streaming_base import BaseResponseIterator + + +class AsyncResponseIterator(BaseResponseIterator): + """Asynchronous Iterator over REST API responses. + + Args: + response (google.auth.aio.transport.Response): An API response object. + response_message_cls (Union[proto.Message, google.protobuf.message.Message]): A response + class expected to be returned from an API. + + Raises: + ValueError: + - If `response_message_cls` is not a subclass of `proto.Message` or `google.protobuf.message.Message`. + """ + + def __init__( + self, + response: google.auth.aio.transport.Response, + response_message_cls: Union[proto.Message, google.protobuf.message.Message], + ): + self._response = response + self._chunk_size = 1024 + self._response_itr = self._response.content().__aiter__() + super(AsyncResponseIterator, self).__init__( + response_message_cls=response_message_cls + ) + + async def __aenter__(self): + return self + + async def cancel(self): + """Cancel existing streaming operation.""" + await self._response.close() + + async def __anext__(self): + while not self._ready_objs: + try: + chunk = await self._response_itr.__anext__() + chunk = chunk.decode("utf-8") + self._process_chunk(chunk) + except StopAsyncIteration as e: + if self._level > 0: + raise ValueError("i Unfinished stream: %s" % self._obj) + raise e + except ValueError as e: + raise e + return self._grab() + + def __aiter__(self): + return self + + async def __aexit__(self, exc_type, exc, tb): + """Cancel existing async streaming operation.""" + await self._response.close() diff --git a/noxfile.py b/noxfile.py index a15795ea1..3fc4a722c 100644 --- a/noxfile.py +++ b/noxfile.py @@ -38,6 +38,7 @@ "unit", "unit_grpc_gcp", "unit_wo_grpc", + "unit_with_auth_aio", "cover", "pytype", "mypy", @@ -109,7 +110,7 @@ def install_prerelease_dependencies(session, constraints_path): session.install(*other_deps) -def default(session, install_grpc=True, prerelease=False): +def default(session, install_grpc=True, prerelease=False, install_auth_aio=False): """Default unit test session. This is intended to be run **without** an interpreter set, so @@ -144,6 +145,11 @@ def default(session, install_grpc=True, prerelease=False): f"{constraints_dir}/constraints-{session.python}.txt", ) + if install_auth_aio: + session.install( + "google-auth @ git+https://git@github.com/googleapis/google-auth-library-python@8833ad6f92c3300d6645355994c7db2356bd30ad" + ) + # Print out package versions of dependencies session.run( "python", "-c", "import google.protobuf; print(google.protobuf.__version__)" @@ -229,6 +235,12 @@ def unit_wo_grpc(session): default(session, install_grpc=False) +@nox.session(python=PYTHON_VERSIONS) +def unit_with_auth_aio(session): + """Run the unit test suite with google.auth.aio installed""" + default(session, install_auth_aio=True) + + @nox.session(python=DEFAULT_PYTHON_VERSION) def lint_setup_py(session): """Verify that setup.py is valid (including RST check).""" diff --git a/tests/asyncio/test_rest_streaming_async.py b/tests/asyncio/test_rest_streaming_async.py new file mode 100644 index 000000000..35820de66 --- /dev/null +++ b/tests/asyncio/test_rest_streaming_async.py @@ -0,0 +1,378 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# TODO: set random.seed explicitly in each test function. +# See related issue: https://github.com/googleapis/python-api-core/issues/689. + +import pytest # noqa: I202 +import mock + +import datetime +import logging +import random +import time +from typing import List, AsyncIterator + +import proto + +try: + from google.auth.aio.transport import Response + + AUTH_AIO_INSTALLED = True +except ImportError: + AUTH_AIO_INSTALLED = False + +if not AUTH_AIO_INSTALLED: # pragma: NO COVER + pytest.skip( + "google-auth>=2.35.0 is required to use asynchronous rest streaming.", + allow_module_level=True, + ) + +from google.api_core import rest_streaming_async +from google.api import http_pb2 +from google.api import httpbody_pb2 + + +from ..helpers import Composer, Song, EchoResponse, parse_responses + + +__protobuf__ = proto.module(package=__name__) +SEED = int(time.time()) +logging.info(f"Starting async rest streaming tests with random seed: {SEED}") +random.seed(SEED) + + +async def mock_async_gen(data, chunk_size=1): + for i in range(0, len(data)): # pragma: NO COVER + chunk = data[i : i + chunk_size] + yield chunk.encode("utf-8") + + +class ResponseMock(Response): + class _ResponseItr(AsyncIterator[bytes]): + def __init__(self, _response_bytes: bytes, random_split=False): + self._responses_bytes = _response_bytes + self._idx = 0 + self._random_split = random_split + + def __aiter__(self): + return self + + async def __anext__(self): + if self._idx >= len(self._responses_bytes): + raise StopAsyncIteration + if self._random_split: + n = random.randint(1, len(self._responses_bytes[self._idx :])) + else: + n = 1 + x = self._responses_bytes[self._idx : self._idx + n] + self._idx += n + return x + + def __init__( + self, + responses: List[proto.Message], + response_cls, + random_split=False, + ): + self._responses = responses + self._random_split = random_split + self._response_message_cls = response_cls + + def _parse_responses(self): + return parse_responses(self._response_message_cls, self._responses) + + @property + async def headers(self): + raise NotImplementedError() + + @property + async def status_code(self): + raise NotImplementedError() + + async def close(self): + raise NotImplementedError() + + async def content(self, chunk_size=None): + itr = self._ResponseItr( + self._parse_responses(), random_split=self._random_split + ) + async for chunk in itr: + yield chunk + + async def read(self): + raise NotImplementedError() + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "random_split,resp_message_is_proto_plus", + [(False, True), (False, False)], +) +async def test_next_simple(random_split, resp_message_is_proto_plus): + if resp_message_is_proto_plus: + response_type = EchoResponse + responses = [EchoResponse(content="hello world"), EchoResponse(content="yes")] + else: + response_type = httpbody_pb2.HttpBody + responses = [ + httpbody_pb2.HttpBody(content_type="hello world"), + httpbody_pb2.HttpBody(content_type="yes"), + ] + + resp = ResponseMock( + responses=responses, random_split=random_split, response_cls=response_type + ) + itr = rest_streaming_async.AsyncResponseIterator(resp, response_type) + idx = 0 + async for response in itr: + assert response == responses[idx] + idx += 1 + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "random_split,resp_message_is_proto_plus", + [ + (True, True), + (False, True), + (True, False), + (False, False), + ], +) +async def test_next_nested(random_split, resp_message_is_proto_plus): + if resp_message_is_proto_plus: + response_type = Song + responses = [ + Song(title="some song", composer=Composer(given_name="some name")), + Song(title="another song", date_added=datetime.datetime(2021, 12, 17)), + ] + else: + # Although `http_pb2.HttpRule`` is used in the response, any response message + # can be used which meets this criteria for the test of having a nested field. + response_type = http_pb2.HttpRule + responses = [ + http_pb2.HttpRule( + selector="some selector", + custom=http_pb2.CustomHttpPattern(kind="some kind"), + ), + http_pb2.HttpRule( + selector="another selector", + custom=http_pb2.CustomHttpPattern(path="some path"), + ), + ] + resp = ResponseMock( + responses=responses, random_split=random_split, response_cls=response_type + ) + itr = rest_streaming_async.AsyncResponseIterator(resp, response_type) + idx = 0 + async for response in itr: + assert response == responses[idx] + idx += 1 + assert idx == len(responses) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "random_split,resp_message_is_proto_plus", + [ + (True, True), + (False, True), + (True, False), + (False, False), + ], +) +async def test_next_stress(random_split, resp_message_is_proto_plus): + n = 50 + if resp_message_is_proto_plus: + response_type = Song + responses = [ + Song(title="title_%d" % i, composer=Composer(given_name="name_%d" % i)) + for i in range(n) + ] + else: + response_type = http_pb2.HttpRule + responses = [ + http_pb2.HttpRule( + selector="selector_%d" % i, + custom=http_pb2.CustomHttpPattern(path="path_%d" % i), + ) + for i in range(n) + ] + resp = ResponseMock( + responses=responses, random_split=random_split, response_cls=response_type + ) + itr = rest_streaming_async.AsyncResponseIterator(resp, response_type) + idx = 0 + async for response in itr: + assert response == responses[idx] + idx += 1 + assert idx == n + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "random_split,resp_message_is_proto_plus", + [ + (True, True), + (False, True), + (True, False), + (False, False), + ], +) +async def test_next_escaped_characters_in_string( + random_split, resp_message_is_proto_plus +): + if resp_message_is_proto_plus: + response_type = Song + composer_with_relateds = Composer() + relateds = ["Artist A", "Artist B"] + composer_with_relateds.relateds = relateds + + responses = [ + Song( + title='ti"tle\nfoo\tbar{}', composer=Composer(given_name="name\n\n\n") + ), + Song( + title='{"this is weird": "totally"}', + composer=Composer(given_name="\\{}\\"), + ), + Song(title='\\{"key": ["value",]}\\', composer=composer_with_relateds), + ] + else: + response_type = http_pb2.Http + responses = [ + http_pb2.Http( + rules=[ + http_pb2.HttpRule( + selector='ti"tle\nfoo\tbar{}', + custom=http_pb2.CustomHttpPattern(kind="name\n\n\n"), + ) + ] + ), + http_pb2.Http( + rules=[ + http_pb2.HttpRule( + selector='{"this is weird": "totally"}', + custom=http_pb2.CustomHttpPattern(kind="\\{}\\"), + ) + ] + ), + http_pb2.Http( + rules=[ + http_pb2.HttpRule( + selector='\\{"key": ["value",]}\\', + custom=http_pb2.CustomHttpPattern(kind="\\{}\\"), + ) + ] + ), + ] + resp = ResponseMock( + responses=responses, random_split=random_split, response_cls=response_type + ) + itr = rest_streaming_async.AsyncResponseIterator(resp, response_type) + idx = 0 + async for response in itr: + assert response == responses[idx] + idx += 1 + assert idx == len(responses) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("response_type", [EchoResponse, httpbody_pb2.HttpBody]) +async def test_next_not_array(response_type): + + data = '{"hello": 0}' + with mock.patch.object( + ResponseMock, "content", return_value=mock_async_gen(data) + ) as mock_method: + resp = ResponseMock(responses=[], response_cls=response_type) + itr = rest_streaming_async.AsyncResponseIterator(resp, response_type) + with pytest.raises(ValueError): + await itr.__anext__() + mock_method.assert_called_once() + + +@pytest.mark.asyncio +@pytest.mark.parametrize("response_type", [EchoResponse, httpbody_pb2.HttpBody]) +async def test_cancel(response_type): + with mock.patch.object( + ResponseMock, "close", new_callable=mock.AsyncMock + ) as mock_method: + resp = ResponseMock(responses=[], response_cls=response_type) + itr = rest_streaming_async.AsyncResponseIterator(resp, response_type) + await itr.cancel() + mock_method.assert_called_once() + + +@pytest.mark.asyncio +@pytest.mark.parametrize("response_type", [EchoResponse, httpbody_pb2.HttpBody]) +async def test_iterator_as_context_manager(response_type): + with mock.patch.object( + ResponseMock, "close", new_callable=mock.AsyncMock + ) as mock_method: + resp = ResponseMock(responses=[], response_cls=response_type) + async with rest_streaming_async.AsyncResponseIterator(resp, response_type): + pass + mock_method.assert_called_once() + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "response_type,return_value", + [ + (EchoResponse, bytes('[{"content": "hello"}, {', "utf-8")), + (httpbody_pb2.HttpBody, bytes('[{"content_type": "hello"}, {', "utf-8")), + ], +) +async def test_check_buffer(response_type, return_value): + with mock.patch.object( + ResponseMock, + "_parse_responses", + return_value=return_value, + ): + resp = ResponseMock(responses=[], response_cls=response_type) + itr = rest_streaming_async.AsyncResponseIterator(resp, response_type) + with pytest.raises(ValueError): + await itr.__anext__() + await itr.__anext__() + + +@pytest.mark.asyncio +@pytest.mark.parametrize("response_type", [EchoResponse, httpbody_pb2.HttpBody]) +async def test_next_html(response_type): + + data = "" + with mock.patch.object( + ResponseMock, "content", return_value=mock_async_gen(data) + ) as mock_method: + resp = ResponseMock(responses=[], response_cls=response_type) + + itr = rest_streaming_async.AsyncResponseIterator(resp, response_type) + with pytest.raises(ValueError): + await itr.__anext__() + mock_method.assert_called_once() + + +@pytest.mark.asyncio +async def test_invalid_response_class(): + class SomeClass: + pass + + resp = ResponseMock(responses=[], response_cls=SomeClass) + with pytest.raises( + ValueError, + match="Response message class must be a subclass of proto.Message or google.protobuf.message.Message", + ): + rest_streaming_async.AsyncResponseIterator(resp, SomeClass) diff --git a/tests/helpers.py b/tests/helpers.py new file mode 100644 index 000000000..3429d511e --- /dev/null +++ b/tests/helpers.py @@ -0,0 +1,71 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Helpers for tests""" + +import logging +from typing import List + +import proto + +from google.protobuf import duration_pb2 +from google.protobuf import timestamp_pb2 +from google.protobuf.json_format import MessageToJson + + +class Genre(proto.Enum): + GENRE_UNSPECIFIED = 0 + CLASSICAL = 1 + JAZZ = 2 + ROCK = 3 + + +class Composer(proto.Message): + given_name = proto.Field(proto.STRING, number=1) + family_name = proto.Field(proto.STRING, number=2) + relateds = proto.RepeatedField(proto.STRING, number=3) + indices = proto.MapField(proto.STRING, proto.STRING, number=4) + + +class Song(proto.Message): + composer = proto.Field(Composer, number=1) + title = proto.Field(proto.STRING, number=2) + lyrics = proto.Field(proto.STRING, number=3) + year = proto.Field(proto.INT32, number=4) + genre = proto.Field(Genre, number=5) + is_five_mins_longer = proto.Field(proto.BOOL, number=6) + score = proto.Field(proto.DOUBLE, number=7) + likes = proto.Field(proto.INT64, number=8) + duration = proto.Field(duration_pb2.Duration, number=9) + date_added = proto.Field(timestamp_pb2.Timestamp, number=10) + + +class EchoResponse(proto.Message): + content = proto.Field(proto.STRING, number=1) + + +def parse_responses(response_message_cls, all_responses: List[proto.Message]) -> bytes: + # json.dumps returns a string surrounded with quotes that need to be stripped + # in order to be an actual JSON. + json_responses = [ + ( + response_message_cls.to_json(response).strip('"') + if issubclass(response_message_cls, proto.Message) + else MessageToJson(response).strip('"') + ) + for response in all_responses + ] + logging.info(f"Sending JSON stream: {json_responses}") + ret_val = "[{}]".format(",".join(json_responses)) + return bytes(ret_val, "utf-8") diff --git a/tests/unit/test_rest_streaming.py b/tests/unit/test_rest_streaming.py index 0f2b3b32a..0f998dfe7 100644 --- a/tests/unit/test_rest_streaming.py +++ b/tests/unit/test_rest_streaming.py @@ -26,48 +26,16 @@ from google.api_core import rest_streaming from google.api import http_pb2 from google.api import httpbody_pb2 -from google.protobuf import duration_pb2 -from google.protobuf import timestamp_pb2 -from google.protobuf.json_format import MessageToJson + +from ..helpers import Composer, Song, EchoResponse, parse_responses __protobuf__ = proto.module(package=__name__) SEED = int(time.time()) -logging.info(f"Starting rest streaming tests with random seed: {SEED}") +logging.info(f"Starting sync rest streaming tests with random seed: {SEED}") random.seed(SEED) -class Genre(proto.Enum): - GENRE_UNSPECIFIED = 0 - CLASSICAL = 1 - JAZZ = 2 - ROCK = 3 - - -class Composer(proto.Message): - given_name = proto.Field(proto.STRING, number=1) - family_name = proto.Field(proto.STRING, number=2) - relateds = proto.RepeatedField(proto.STRING, number=3) - indices = proto.MapField(proto.STRING, proto.STRING, number=4) - - -class Song(proto.Message): - composer = proto.Field(Composer, number=1) - title = proto.Field(proto.STRING, number=2) - lyrics = proto.Field(proto.STRING, number=3) - year = proto.Field(proto.INT32, number=4) - genre = proto.Field(Genre, number=5) - is_five_mins_longer = proto.Field(proto.BOOL, number=6) - score = proto.Field(proto.DOUBLE, number=7) - likes = proto.Field(proto.INT64, number=8) - duration = proto.Field(duration_pb2.Duration, number=9) - date_added = proto.Field(timestamp_pb2.Timestamp, number=10) - - -class EchoResponse(proto.Message): - content = proto.Field(proto.STRING, number=1) - - class ResponseMock(requests.Response): class _ResponseItr: def __init__(self, _response_bytes: bytes, random_split=False): @@ -97,27 +65,15 @@ def __init__( self._random_split = random_split self._response_message_cls = response_cls - def _parse_responses(self, responses: List[proto.Message]) -> bytes: - # json.dumps returns a string surrounded with quotes that need to be stripped - # in order to be an actual JSON. - json_responses = [ - ( - self._response_message_cls.to_json(r).strip('"') - if issubclass(self._response_message_cls, proto.Message) - else MessageToJson(r).strip('"') - ) - for r in responses - ] - logging.info(f"Sending JSON stream: {json_responses}") - ret_val = "[{}]".format(",".join(json_responses)) - return bytes(ret_val, "utf-8") + def _parse_responses(self): + return parse_responses(self._response_message_cls, self._responses) def close(self): raise NotImplementedError() def iter_content(self, *args, **kwargs): return self._ResponseItr( - self._parse_responses(self._responses), + self._parse_responses(), random_split=self._random_split, ) @@ -333,9 +289,8 @@ class SomeClass: pass resp = ResponseMock(responses=[], response_cls=SomeClass) - response_iterator = rest_streaming.ResponseIterator(resp, SomeClass) with pytest.raises( ValueError, match="Response message class must be a subclass of proto.Message or google.protobuf.message.Message", ): - response_iterator._grab() + rest_streaming.ResponseIterator(resp, SomeClass) From 58516ef1e4ffe5495b9f235c226d89e4ad6e0874 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Thu, 19 Sep 2024 14:19:15 -0400 Subject: [PATCH 029/101] chore(main): release 2.20.0 (#693) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 9 +++++++++ google/api_core/version.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a25a39c87..a1552b532 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ [1]: https://pypi.org/project/google-api-core/#history +## [2.20.0](https://github.com/googleapis/python-api-core/compare/v2.19.2...v2.20.0) (2024-09-18) + + +### Features + +* Add async unsupported paramater exception ([#694](https://github.com/googleapis/python-api-core/issues/694)) ([8c137fe](https://github.com/googleapis/python-api-core/commit/8c137feb6e880fdd93d1248d9b6c10002dc3c096)) +* Add support for asynchronous rest streaming ([#686](https://github.com/googleapis/python-api-core/issues/686)) ([1b7bb6d](https://github.com/googleapis/python-api-core/commit/1b7bb6d1b721e4ee1561e8e4a347846d7fdd7c27)) +* Add support for creating exceptions from an asynchronous response ([#688](https://github.com/googleapis/python-api-core/issues/688)) ([1c4b0d0](https://github.com/googleapis/python-api-core/commit/1c4b0d079f2103a7b5562371a7bd1ada92528de3)) + ## [2.19.2](https://github.com/googleapis/python-api-core/compare/v2.19.1...v2.19.2) (2024-08-16) diff --git a/google/api_core/version.py b/google/api_core/version.py index 4b0e3c81d..9fea4fece 100644 --- a/google/api_core/version.py +++ b/google/api_core/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "2.19.2" +__version__ = "2.20.0" From 45b8a6db5a5c75acdd8be896d0152f11608c7e51 Mon Sep 17 00:00:00 2001 From: ohmayr Date: Fri, 20 Sep 2024 16:01:37 -0400 Subject: [PATCH 030/101] fix: set chunk size for async stream content (#702) * fix: set chunk size for async stream content * add TODO comment --- google/api_core/rest_streaming_async.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/google/api_core/rest_streaming_async.py b/google/api_core/rest_streaming_async.py index d1f996f61..812854c54 100644 --- a/google/api_core/rest_streaming_async.py +++ b/google/api_core/rest_streaming_async.py @@ -49,7 +49,11 @@ def __init__( ): self._response = response self._chunk_size = 1024 - self._response_itr = self._response.content().__aiter__() + # TODO(https://github.com/googleapis/python-api-core/issues/703): mypy does not recognize the abstract content + # method as an async generator as it looks for the `yield` keyword in the implementation. + # Given that the abstract method is not implemented, mypy fails to recognize it as an async generator. + # mypy warnings are silenced until the linked issue is resolved. + self._response_itr = self._response.content(self._chunk_size).__aiter__() # type: ignore super(AsyncResponseIterator, self).__init__( response_message_cls=response_message_cls ) From 84bf63717df67f8a78e6385c0de6c7826f0cdaa0 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Mon, 30 Sep 2024 16:58:06 -0400 Subject: [PATCH 031/101] chore: add `async_rest` extra for async rest dependencies (#701) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: add extra for aiohttp * improve error message * with -> w; apply same change to prerelease_deps * move error to try block * address feedback * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * update noxfile --------- Co-authored-by: ohmayr Co-authored-by: Owl Bot --- .github/workflows/unittest.yml | 2 +- google/api_core/rest_streaming_async.py | 4 +- noxfile.py | 46 ++++++++++++++-------- setup.py | 3 ++ testing/constraints-3.7.txt | 1 - testing/constraints-async-rest-3.7.txt | 17 ++++++++ tests/asyncio/test_rest_streaming_async.py | 7 +--- 7 files changed, 55 insertions(+), 25 deletions(-) create mode 100644 testing/constraints-async-rest-3.7.txt diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 5980f825b..2d1193d69 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - option: ["", "_grpc_gcp", "_wo_grpc", "_with_prerelease_deps", "_with_auth_aio"] + option: ["", "_grpc_gcp", "_wo_grpc", "_w_prerelease_deps", "_w_async_rest_extra"] python: - "3.7" - "3.8" diff --git a/google/api_core/rest_streaming_async.py b/google/api_core/rest_streaming_async.py index 812854c54..370c2b53f 100644 --- a/google/api_core/rest_streaming_async.py +++ b/google/api_core/rest_streaming_async.py @@ -22,7 +22,9 @@ import google.auth.aio.transport except ImportError as e: # pragma: NO COVER raise ImportError( - "google-auth>=2.35.0 is required to use asynchronous rest streaming." + "`google-api-core[async_rest]` is required to use asynchronous rest streaming. " + "Install the `async_rest` extra of `google-api-core` using " + "`pip install google-api-core[async_rest]`." ) from e import google.protobuf.message diff --git a/noxfile.py b/noxfile.py index 3fc4a722c..144e3e210 100644 --- a/noxfile.py +++ b/noxfile.py @@ -38,7 +38,8 @@ "unit", "unit_grpc_gcp", "unit_wo_grpc", - "unit_with_auth_aio", + "unit_w_prerelease_deps", + "unit_w_async_rest_extra", "cover", "pytype", "mypy", @@ -110,7 +111,7 @@ def install_prerelease_dependencies(session, constraints_path): session.install(*other_deps) -def default(session, install_grpc=True, prerelease=False, install_auth_aio=False): +def default(session, install_grpc=True, prerelease=False, install_async_rest=False): """Default unit test session. This is intended to be run **without** an interpreter set, so @@ -129,25 +130,38 @@ def default(session, install_grpc=True, prerelease=False, install_auth_aio=False "pytest-xdist", ) + install_extras = [] + if install_grpc: + install_extras.append("grpc") + constraints_dir = str(CURRENT_DIRECTORY / "testing") + if install_async_rest: + install_extras.append("async_rest") + constraints_type = "async-rest-" + else: + constraints_type = "" + lib_with_extras = f".[{','.join(install_extras)}]" if len(install_extras) else "." if prerelease: install_prerelease_dependencies( - session, f"{constraints_dir}/constraints-{PYTHON_VERSIONS[0]}.txt" + session, + f"{constraints_dir}/constraints-{constraints_type}{PYTHON_VERSIONS[0]}.txt", ) # This *must* be the last install command to get the package from source. - session.install("-e", ".", "--no-deps") + session.install("-e", lib_with_extras, "--no-deps") else: - session.install( - "-e", - ".[grpc]" if install_grpc else ".", - "-c", - f"{constraints_dir}/constraints-{session.python}.txt", + constraints_file = ( + f"{constraints_dir}/constraints-{constraints_type}{session.python}.txt" ) + # fall back to standard constraints file + if not pathlib.Path(constraints_file).exists(): + constraints_file = f"{constraints_dir}/constraints-{session.python}.txt" - if install_auth_aio: session.install( - "google-auth @ git+https://git@github.com/googleapis/google-auth-library-python@8833ad6f92c3300d6645355994c7db2356bd30ad" + "-e", + lib_with_extras, + "-c", + constraints_file, ) # Print out package versions of dependencies @@ -205,7 +219,7 @@ def unit(session): @nox.session(python=PYTHON_VERSIONS) -def unit_with_prerelease_deps(session): +def unit_w_prerelease_deps(session): """Run the unit test suite.""" default(session, prerelease=True) @@ -236,9 +250,9 @@ def unit_wo_grpc(session): @nox.session(python=PYTHON_VERSIONS) -def unit_with_auth_aio(session): - """Run the unit test suite with google.auth.aio installed""" - default(session, install_auth_aio=True) +def unit_w_async_rest_extra(session): + """Run the unit test suite with the `async_rest` extra""" + default(session, install_async_rest=True) @nox.session(python=DEFAULT_PYTHON_VERSION) @@ -261,7 +275,7 @@ def mypy(session): """Run type-checking.""" # TODO(https://github.com/googleapis/python-api-core/issues/682): # Use the latest version of mypy instead of mypy<1.11.0 - session.install(".[grpc]", "mypy<1.11.0") + session.install(".[grpc,async_rest]", "mypy<1.11.0") session.install( "types-setuptools", "types-requests", diff --git a/setup.py b/setup.py index a9e01f49c..d3c2a2b42 100644 --- a/setup.py +++ b/setup.py @@ -36,6 +36,9 @@ "requests >= 2.18.0, < 3.0.0.dev0", ] extras = { + "async_rest": [ + "google-auth[aiohttp] >= 2.35.0, < 3.0.dev0", + ], "grpc": [ "grpcio >= 1.33.2, < 2.0dev", "grpcio >= 1.49.1, < 2.0dev; python_version>='3.11'", diff --git a/testing/constraints-3.7.txt b/testing/constraints-3.7.txt index fcc9831fd..4ce1c8999 100644 --- a/testing/constraints-3.7.txt +++ b/testing/constraints-3.7.txt @@ -9,7 +9,6 @@ googleapis-common-protos==1.56.2 protobuf==3.19.5 google-auth==2.14.1 requests==2.18.0 -packaging==14.3 grpcio==1.33.2 grpcio-status==1.33.2 grpcio-gcp==0.2.2 diff --git a/testing/constraints-async-rest-3.7.txt b/testing/constraints-async-rest-3.7.txt new file mode 100644 index 000000000..7aedeb1ca --- /dev/null +++ b/testing/constraints-async-rest-3.7.txt @@ -0,0 +1,17 @@ +# This constraints file is used to check that lower bounds +# are correct in setup.py +# List *all* library dependencies and extras in this file. +# Pin the version to the lower bound. +# +# e.g., if setup.py has "foo >= 1.14.0, < 2.0.0dev", +# Then this file should have foo==1.14.0 +googleapis-common-protos==1.56.2 +protobuf==3.19.5 +google-auth==2.35.0 +# from google-auth[aiohttp] +aiohttp==3.6.2 +requests==2.20.0 +grpcio==1.33.2 +grpcio-status==1.33.2 +grpcio-gcp==0.2.2 +proto-plus==1.22.3 diff --git a/tests/asyncio/test_rest_streaming_async.py b/tests/asyncio/test_rest_streaming_async.py index 35820de66..da5b1c8d6 100644 --- a/tests/asyncio/test_rest_streaming_async.py +++ b/tests/asyncio/test_rest_streaming_async.py @@ -28,14 +28,9 @@ try: from google.auth.aio.transport import Response - - AUTH_AIO_INSTALLED = True except ImportError: - AUTH_AIO_INSTALLED = False - -if not AUTH_AIO_INSTALLED: # pragma: NO COVER pytest.skip( - "google-auth>=2.35.0 is required to use asynchronous rest streaming.", + "google-api-core[async_rest] is required to test asynchronous rest streaming.", allow_module_level=True, ) From aaed69b6f1d694cd7e561e2aa03fdd8d6cfb369a Mon Sep 17 00:00:00 2001 From: ohmayr Date: Mon, 7 Oct 2024 18:49:40 -0400 Subject: [PATCH 032/101] feat: add support for asynchronous long running operations (#724) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: implement `OperationsRestAsyncTransport` to support long running operations (#700) * feat: Add OperationsRestAsyncTransport to support long running operations * update TODO comment * update TODO comment * address feedback * address feedback * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * fix mypy and lint issues * minor fix * add no cover * fix no cover tag * link coverage issue * silence coverage issue * fix statement name error * address PR feedback * address PR feedback * address PR comments --------- Co-authored-by: ohmayr Co-authored-by: Owl Bot * feat: implement async client for LROs (#707) * feat: implement `AbstractOperationsAsyncClient` to support long running operations * remove coverage guards * address presubmit failures * fix coverage for cancel operation * tests cleanup * fix incorrect tests * file bugs * add auth import * address PR comments * address PR comments * fix unit tests and address more comments * disable retry parameter * add retry parameter * address PR comments --------- Co-authored-by: ohmayr Co-authored-by: ohmayr * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: Anthonios Partheniou Co-authored-by: Owl Bot --- google/api_core/client_info.py | 3 +- google/api_core/gapic_v1/client_info.py | 2 + google/api_core/operations_v1/__init__.py | 19 +- .../abstract_operations_base_client.py | 370 ++++++++ .../abstract_operations_client.py | 322 +------ .../operations_rest_client_async.py | 345 ++++++++ google/api_core/operations_v1/pagers.py | 29 +- google/api_core/operations_v1/pagers_async.py | 71 ++ google/api_core/operations_v1/pagers_base.py | 73 ++ .../operations_v1/transports/__init__.py | 24 +- .../api_core/operations_v1/transports/base.py | 54 +- .../api_core/operations_v1/transports/rest.py | 58 +- .../operations_v1/transports/rest_asyncio.py | 560 ++++++++++++ .../test_operations_rest_client.py | 829 ++++++++++++++---- 14 files changed, 2208 insertions(+), 551 deletions(-) create mode 100644 google/api_core/operations_v1/abstract_operations_base_client.py create mode 100644 google/api_core/operations_v1/operations_rest_client_async.py create mode 100644 google/api_core/operations_v1/pagers_async.py create mode 100644 google/api_core/operations_v1/pagers_base.py create mode 100644 google/api_core/operations_v1/transports/rest_asyncio.py diff --git a/google/api_core/client_info.py b/google/api_core/client_info.py index 483267997..90926beb8 100644 --- a/google/api_core/client_info.py +++ b/google/api_core/client_info.py @@ -57,7 +57,8 @@ class ClientInfo(object): user_agent (Optional[str]): Prefix to the user agent header. This is used to supply information such as application name or partner tool. Recommended format: ``application-or-tool-ID/major.minor.version``. - rest_version (Optional[str]): The requests library version. + rest_version (Optional[str]): A string with labeled versions of the + dependencies used for REST transport. """ def __init__( diff --git a/google/api_core/gapic_v1/client_info.py b/google/api_core/gapic_v1/client_info.py index 2de1be7f2..4516f339b 100644 --- a/google/api_core/gapic_v1/client_info.py +++ b/google/api_core/gapic_v1/client_info.py @@ -45,6 +45,8 @@ class ClientInfo(client_info.ClientInfo): user_agent (Optional[str]): Prefix to the user agent header. This is used to supply information such as application name or partner tool. Recommended format: ``application-or-tool-ID/major.minor.version``. + rest_version (Optional[str]): A string with labeled versions of the + dependencies used for REST transport. """ def to_grpc_metadata(self): diff --git a/google/api_core/operations_v1/__init__.py b/google/api_core/operations_v1/__init__.py index 8b75426b9..4db32a4cb 100644 --- a/google/api_core/operations_v1/__init__.py +++ b/google/api_core/operations_v1/__init__.py @@ -14,9 +14,7 @@ """Package for interacting with the google.longrunning.operations meta-API.""" -from google.api_core.operations_v1.abstract_operations_client import ( - AbstractOperationsClient, -) +from google.api_core.operations_v1.abstract_operations_client import AbstractOperationsClient from google.api_core.operations_v1.operations_async_client import OperationsAsyncClient from google.api_core.operations_v1.operations_client import OperationsClient from google.api_core.operations_v1.transports.rest import OperationsRestTransport @@ -25,5 +23,18 @@ "AbstractOperationsClient", "OperationsAsyncClient", "OperationsClient", - "OperationsRestTransport", + "OperationsRestTransport" ] + +try: + from google.api_core.operations_v1.transports.rest_asyncio import ( + AsyncOperationsRestTransport, + ) + from google.api_core.operations_v1.operations_rest_client_async import AsyncOperationsRestClient + + __all__ += ["AsyncOperationsRestClient", "AsyncOperationsRestTransport"] +except ImportError: + # This import requires the `async_rest` extra. + # Don't raise an exception if `AsyncOperationsRestTransport` cannot be imported + # as other transports are still available. + pass diff --git a/google/api_core/operations_v1/abstract_operations_base_client.py b/google/api_core/operations_v1/abstract_operations_base_client.py new file mode 100644 index 000000000..160c2a88f --- /dev/null +++ b/google/api_core/operations_v1/abstract_operations_base_client.py @@ -0,0 +1,370 @@ +# -*- coding: utf-8 -*- +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from collections import OrderedDict +import os +import re +from typing import Dict, Optional, Type, Union + +from google.api_core import client_options as client_options_lib # type: ignore +from google.api_core import gapic_v1 # type: ignore +from google.api_core.operations_v1.transports.base import ( + DEFAULT_CLIENT_INFO, + OperationsTransport, +) +from google.api_core.operations_v1.transports.rest import OperationsRestTransport + +try: + from google.api_core.operations_v1.transports.rest_asyncio import ( + AsyncOperationsRestTransport, + ) + + HAS_ASYNC_REST_DEPENDENCIES = True +except ImportError as e: + HAS_ASYNC_REST_DEPENDENCIES = False + ASYNC_REST_EXCEPTION = e + +from google.auth import credentials as ga_credentials # type: ignore +from google.auth.exceptions import MutualTLSChannelError # type: ignore +from google.auth.transport import mtls # type: ignore + + +class AbstractOperationsBaseClientMeta(type): + """Metaclass for the Operations Base client. + + This provides base class-level methods for building and retrieving + support objects (e.g. transport) without polluting the client instance + objects. + """ + + _transport_registry = OrderedDict() # type: Dict[str, Type[OperationsTransport]] + _transport_registry["rest"] = OperationsRestTransport + if HAS_ASYNC_REST_DEPENDENCIES: + _transport_registry["rest_asyncio"] = AsyncOperationsRestTransport + + def get_transport_class( + cls, + label: Optional[str] = None, + ) -> Type[OperationsTransport]: + """Returns an appropriate transport class. + + Args: + label: The name of the desired transport. If none is + provided, then the first transport in the registry is used. + + Returns: + The transport class to use. + """ + # If a specific transport is requested, return that one. + if ( + label == "rest_asyncio" and not HAS_ASYNC_REST_DEPENDENCIES + ): # pragma: NO COVER + raise ASYNC_REST_EXCEPTION + + if label: + return cls._transport_registry[label] + + # No transport is requested; return the default (that is, the first one + # in the dictionary). + return next(iter(cls._transport_registry.values())) + + +class AbstractOperationsBaseClient(metaclass=AbstractOperationsBaseClientMeta): + """Manages long-running operations with an API service. + + When an API method normally takes long time to complete, it can be + designed to return [Operation][google.api_core.operations_v1.Operation] to the + client, and the client can use this interface to receive the real + response asynchronously by polling the operation resource, or pass + the operation resource to another API (such as Google Cloud Pub/Sub + API) to receive the response. Any API service that returns + long-running operations should implement the ``Operations`` + interface so developers can have a consistent client experience. + """ + + @staticmethod + def _get_default_mtls_endpoint(api_endpoint): + """Converts api endpoint to mTLS endpoint. + + Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to + "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. + Args: + api_endpoint (Optional[str]): the api endpoint to convert. + Returns: + str: converted mTLS api endpoint. + """ + if not api_endpoint: + return api_endpoint + + mtls_endpoint_re = re.compile( + r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" + ) + + m = mtls_endpoint_re.match(api_endpoint) + name, mtls, sandbox, googledomain = m.groups() + if mtls or not googledomain: + return api_endpoint + + if sandbox: + return api_endpoint.replace( + "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" + ) + + return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") + + DEFAULT_ENDPOINT = "longrunning.googleapis.com" + DEFAULT_MTLS_ENDPOINT = _get_default_mtls_endpoint.__func__( # type: ignore + DEFAULT_ENDPOINT + ) + + @classmethod + def from_service_account_info(cls, info: dict, *args, **kwargs): + """ + This class method should be overridden by the subclasses. + + Args: + info (dict): The service account private key info. + args: Additional arguments to pass to the constructor. + kwargs: Additional arguments to pass to the constructor. + + Raises: + NotImplementedError: If the method is called on the base class. + """ + raise NotImplementedError("`from_service_account_info` is not implemented.") + + @classmethod + def from_service_account_file(cls, filename: str, *args, **kwargs): + """ + This class method should be overridden by the subclasses. + + Args: + filename (str): The path to the service account private key json + file. + args: Additional arguments to pass to the constructor. + kwargs: Additional arguments to pass to the constructor. + + Raises: + NotImplementedError: If the method is called on the base class. + """ + raise NotImplementedError("`from_service_account_file` is not implemented.") + + from_service_account_json = from_service_account_file + + @property + def transport(self) -> OperationsTransport: + """Returns the transport used by the client instance. + + Returns: + OperationsTransport: The transport used by the client + instance. + """ + return self._transport + + @staticmethod + def common_billing_account_path( + billing_account: str, + ) -> str: + """Returns a fully-qualified billing_account string.""" + return "billingAccounts/{billing_account}".format( + billing_account=billing_account, + ) + + @staticmethod + def parse_common_billing_account_path(path: str) -> Dict[str, str]: + """Parse a billing_account path into its component segments.""" + m = re.match(r"^billingAccounts/(?P.+?)$", path) + return m.groupdict() if m else {} + + @staticmethod + def common_folder_path( + folder: str, + ) -> str: + """Returns a fully-qualified folder string.""" + return "folders/{folder}".format( + folder=folder, + ) + + @staticmethod + def parse_common_folder_path(path: str) -> Dict[str, str]: + """Parse a folder path into its component segments.""" + m = re.match(r"^folders/(?P.+?)$", path) + return m.groupdict() if m else {} + + @staticmethod + def common_organization_path( + organization: str, + ) -> str: + """Returns a fully-qualified organization string.""" + return "organizations/{organization}".format( + organization=organization, + ) + + @staticmethod + def parse_common_organization_path(path: str) -> Dict[str, str]: + """Parse a organization path into its component segments.""" + m = re.match(r"^organizations/(?P.+?)$", path) + return m.groupdict() if m else {} + + @staticmethod + def common_project_path( + project: str, + ) -> str: + """Returns a fully-qualified project string.""" + return "projects/{project}".format( + project=project, + ) + + @staticmethod + def parse_common_project_path(path: str) -> Dict[str, str]: + """Parse a project path into its component segments.""" + m = re.match(r"^projects/(?P.+?)$", path) + return m.groupdict() if m else {} + + @staticmethod + def common_location_path( + project: str, + location: str, + ) -> str: + """Returns a fully-qualified location string.""" + return "projects/{project}/locations/{location}".format( + project=project, + location=location, + ) + + @staticmethod + def parse_common_location_path(path: str) -> Dict[str, str]: + """Parse a location path into its component segments.""" + m = re.match(r"^projects/(?P.+?)/locations/(?P.+?)$", path) + return m.groupdict() if m else {} + + def __init__( + self, + *, + credentials: Optional[ga_credentials.Credentials] = None, + transport: Union[str, OperationsTransport, None] = None, + client_options: Optional[client_options_lib.ClientOptions] = None, + client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, + ) -> None: + """Instantiates the operations client. + + Args: + credentials (Optional[google.auth.credentials.Credentials]): The + authorization credentials to attach to requests. These + credentials identify the application to the service; if none + are specified, the client will attempt to ascertain the + credentials from the environment. + transport (Union[str, OperationsTransport]): The + transport to use. If set to None, a transport is chosen + automatically. + client_options (google.api_core.client_options.ClientOptions): Custom options for the + client. It won't take effect if a ``transport`` instance is provided. + (1) The ``api_endpoint`` property can be used to override the + default endpoint provided by the client. GOOGLE_API_USE_MTLS_ENDPOINT + environment variable can also be used to override the endpoint: + "always" (always use the default mTLS endpoint), "never" (always + use the default regular endpoint) and "auto" (auto switch to the + default mTLS endpoint if client certificate is present, this is + the default value). However, the ``api_endpoint`` property takes + precedence if provided. + (2) If GOOGLE_API_USE_CLIENT_CERTIFICATE environment variable + is "true", then the ``client_cert_source`` property can be used + to provide client certificate for mutual TLS transport. If + not provided, the default SSL client certificate will be used if + present. If GOOGLE_API_USE_CLIENT_CERTIFICATE is "false" or not + set, no client certificate will be used. + client_info (google.api_core.gapic_v1.client_info.ClientInfo): + The client info used to send a user-agent string along with + API requests. If ``None``, then default info will be used. + Generally, you only need to set this if you're developing + your own client library. + + Raises: + google.auth.exceptions.MutualTLSChannelError: If mutual TLS transport + creation failed for any reason. + """ + if isinstance(client_options, dict): + client_options = client_options_lib.from_dict(client_options) + if client_options is None: + client_options = client_options_lib.ClientOptions() + + # Create SSL credentials for mutual TLS if needed. + use_client_cert = os.getenv( + "GOOGLE_API_USE_CLIENT_CERTIFICATE", "false" + ).lower() + if use_client_cert not in ("true", "false"): + raise ValueError( + "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be either `true` or `false`" + ) + client_cert_source_func = None + is_mtls = False + if use_client_cert == "true": + if client_options.client_cert_source: + is_mtls = True + client_cert_source_func = client_options.client_cert_source + else: + is_mtls = mtls.has_default_client_cert_source() + if is_mtls: + client_cert_source_func = mtls.default_client_cert_source() + else: + client_cert_source_func = None + + # Figure out which api endpoint to use. + if client_options.api_endpoint is not None: + api_endpoint = client_options.api_endpoint + else: + use_mtls_env = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto") + if use_mtls_env == "never": + api_endpoint = self.DEFAULT_ENDPOINT + elif use_mtls_env == "always": + api_endpoint = self.DEFAULT_MTLS_ENDPOINT + elif use_mtls_env == "auto": + if is_mtls: + api_endpoint = self.DEFAULT_MTLS_ENDPOINT + else: + api_endpoint = self.DEFAULT_ENDPOINT + else: + raise MutualTLSChannelError( + "Unsupported GOOGLE_API_USE_MTLS_ENDPOINT value. Accepted " + "values: never, auto, always" + ) + + # Save or instantiate the transport. + # Ordinarily, we provide the transport, but allowing a custom transport + # instance provides an extensibility point for unusual situations. + if isinstance(transport, OperationsTransport): + # transport is a OperationsTransport instance. + if credentials or client_options.credentials_file: + raise ValueError( + "When providing a transport instance, " + "provide its credentials directly." + ) + if client_options.scopes: + raise ValueError( + "When providing a transport instance, provide its scopes " + "directly." + ) + self._transport = transport + else: + Transport = type(self).get_transport_class(transport) + self._transport = Transport( + credentials=credentials, + credentials_file=client_options.credentials_file, + host=api_endpoint, + scopes=client_options.scopes, + client_cert_source_for_mtls=client_cert_source_func, + quota_project_id=client_options.quota_project_id, + client_info=client_info, + always_use_jwt_access=True, + ) diff --git a/google/api_core/operations_v1/abstract_operations_client.py b/google/api_core/operations_v1/abstract_operations_client.py index 38f532af5..fc4453625 100644 --- a/google/api_core/operations_v1/abstract_operations_client.py +++ b/google/api_core/operations_v1/abstract_operations_client.py @@ -13,10 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # -from collections import OrderedDict -import os -import re -from typing import Dict, Optional, Sequence, Tuple, Type, Union +from typing import Optional, Sequence, Tuple, Union from google.api_core import client_options as client_options_lib # type: ignore from google.api_core import gapic_v1 # type: ignore @@ -26,10 +23,10 @@ DEFAULT_CLIENT_INFO, OperationsTransport, ) -from google.api_core.operations_v1.transports.rest import OperationsRestTransport +from google.api_core.operations_v1.abstract_operations_base_client import ( + AbstractOperationsBaseClient, +) from google.auth import credentials as ga_credentials # type: ignore -from google.auth.exceptions import MutualTLSChannelError # type: ignore -from google.auth.transport import mtls # type: ignore from google.longrunning import operations_pb2 from google.oauth2 import service_account # type: ignore import grpc @@ -37,40 +34,7 @@ OptionalRetry = Union[retries.Retry, object] -class AbstractOperationsClientMeta(type): - """Metaclass for the Operations client. - - This provides class-level methods for building and retrieving - support objects (e.g. transport) without polluting the client instance - objects. - """ - - _transport_registry = OrderedDict() # type: Dict[str, Type[OperationsTransport]] - _transport_registry["rest"] = OperationsRestTransport - - def get_transport_class( - cls, - label: Optional[str] = None, - ) -> Type[OperationsTransport]: - """Returns an appropriate transport class. - - Args: - label: The name of the desired transport. If none is - provided, then the first transport in the registry is used. - - Returns: - The transport class to use. - """ - # If a specific transport is requested, return that one. - if label: - return cls._transport_registry[label] - - # No transport is requested; return the default (that is, the first one - # in the dictionary). - return next(iter(cls._transport_registry.values())) - - -class AbstractOperationsClient(metaclass=AbstractOperationsClientMeta): +class AbstractOperationsClient(AbstractOperationsBaseClient): """Manages long-running operations with an API service. When an API method normally takes long time to complete, it can be @@ -83,165 +47,6 @@ class AbstractOperationsClient(metaclass=AbstractOperationsClientMeta): interface so developers can have a consistent client experience. """ - @staticmethod - def _get_default_mtls_endpoint(api_endpoint): - """Converts api endpoint to mTLS endpoint. - - Convert "*.sandbox.googleapis.com" and "*.googleapis.com" to - "*.mtls.sandbox.googleapis.com" and "*.mtls.googleapis.com" respectively. - Args: - api_endpoint (Optional[str]): the api endpoint to convert. - Returns: - str: converted mTLS api endpoint. - """ - if not api_endpoint: - return api_endpoint - - mtls_endpoint_re = re.compile( - r"(?P[^.]+)(?P\.mtls)?(?P\.sandbox)?(?P\.googleapis\.com)?" - ) - - m = mtls_endpoint_re.match(api_endpoint) - name, mtls, sandbox, googledomain = m.groups() - if mtls or not googledomain: - return api_endpoint - - if sandbox: - return api_endpoint.replace( - "sandbox.googleapis.com", "mtls.sandbox.googleapis.com" - ) - - return api_endpoint.replace(".googleapis.com", ".mtls.googleapis.com") - - DEFAULT_ENDPOINT = "longrunning.googleapis.com" - DEFAULT_MTLS_ENDPOINT = _get_default_mtls_endpoint.__func__( # type: ignore - DEFAULT_ENDPOINT - ) - - @classmethod - def from_service_account_info(cls, info: dict, *args, **kwargs): - """Creates an instance of this client using the provided credentials - info. - - Args: - info (dict): The service account private key info. - args: Additional arguments to pass to the constructor. - kwargs: Additional arguments to pass to the constructor. - - Returns: - AbstractOperationsClient: The constructed client. - """ - credentials = service_account.Credentials.from_service_account_info(info) - kwargs["credentials"] = credentials - return cls(*args, **kwargs) - - @classmethod - def from_service_account_file(cls, filename: str, *args, **kwargs): - """Creates an instance of this client using the provided credentials - file. - - Args: - filename (str): The path to the service account private key json - file. - args: Additional arguments to pass to the constructor. - kwargs: Additional arguments to pass to the constructor. - - Returns: - AbstractOperationsClient: The constructed client. - """ - credentials = service_account.Credentials.from_service_account_file(filename) - kwargs["credentials"] = credentials - return cls(*args, **kwargs) - - from_service_account_json = from_service_account_file - - @property - def transport(self) -> OperationsTransport: - """Returns the transport used by the client instance. - - Returns: - OperationsTransport: The transport used by the client - instance. - """ - return self._transport - - @staticmethod - def common_billing_account_path( - billing_account: str, - ) -> str: - """Returns a fully-qualified billing_account string.""" - return "billingAccounts/{billing_account}".format( - billing_account=billing_account, - ) - - @staticmethod - def parse_common_billing_account_path(path: str) -> Dict[str, str]: - """Parse a billing_account path into its component segments.""" - m = re.match(r"^billingAccounts/(?P.+?)$", path) - return m.groupdict() if m else {} - - @staticmethod - def common_folder_path( - folder: str, - ) -> str: - """Returns a fully-qualified folder string.""" - return "folders/{folder}".format( - folder=folder, - ) - - @staticmethod - def parse_common_folder_path(path: str) -> Dict[str, str]: - """Parse a folder path into its component segments.""" - m = re.match(r"^folders/(?P.+?)$", path) - return m.groupdict() if m else {} - - @staticmethod - def common_organization_path( - organization: str, - ) -> str: - """Returns a fully-qualified organization string.""" - return "organizations/{organization}".format( - organization=organization, - ) - - @staticmethod - def parse_common_organization_path(path: str) -> Dict[str, str]: - """Parse a organization path into its component segments.""" - m = re.match(r"^organizations/(?P.+?)$", path) - return m.groupdict() if m else {} - - @staticmethod - def common_project_path( - project: str, - ) -> str: - """Returns a fully-qualified project string.""" - return "projects/{project}".format( - project=project, - ) - - @staticmethod - def parse_common_project_path(path: str) -> Dict[str, str]: - """Parse a project path into its component segments.""" - m = re.match(r"^projects/(?P.+?)$", path) - return m.groupdict() if m else {} - - @staticmethod - def common_location_path( - project: str, - location: str, - ) -> str: - """Returns a fully-qualified location string.""" - return "projects/{project}/locations/{location}".format( - project=project, - location=location, - ) - - @staticmethod - def parse_common_location_path(path: str) -> Dict[str, str]: - """Parse a location path into its component segments.""" - m = re.match(r"^projects/(?P.+?)/locations/(?P.+?)$", path) - return m.groupdict() if m else {} - def __init__( self, *, @@ -287,80 +92,49 @@ def __init__( google.auth.exceptions.MutualTLSChannelError: If mutual TLS transport creation failed for any reason. """ - if isinstance(client_options, dict): - client_options = client_options_lib.from_dict(client_options) - if client_options is None: - client_options = client_options_lib.ClientOptions() - - # Create SSL credentials for mutual TLS if needed. - use_client_cert = os.getenv( - "GOOGLE_API_USE_CLIENT_CERTIFICATE", "false" - ).lower() - if use_client_cert not in ("true", "false"): - raise ValueError( - "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be either `true` or `false`" - ) - client_cert_source_func = None - is_mtls = False - if use_client_cert == "true": - if client_options.client_cert_source: - is_mtls = True - client_cert_source_func = client_options.client_cert_source - else: - is_mtls = mtls.has_default_client_cert_source() - if is_mtls: - client_cert_source_func = mtls.default_client_cert_source() - else: - client_cert_source_func = None - - # Figure out which api endpoint to use. - if client_options.api_endpoint is not None: - api_endpoint = client_options.api_endpoint - else: - use_mtls_env = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto") - if use_mtls_env == "never": - api_endpoint = self.DEFAULT_ENDPOINT - elif use_mtls_env == "always": - api_endpoint = self.DEFAULT_MTLS_ENDPOINT - elif use_mtls_env == "auto": - if is_mtls: - api_endpoint = self.DEFAULT_MTLS_ENDPOINT - else: - api_endpoint = self.DEFAULT_ENDPOINT - else: - raise MutualTLSChannelError( - "Unsupported GOOGLE_API_USE_MTLS_ENDPOINT value. Accepted " - "values: never, auto, always" - ) - - # Save or instantiate the transport. - # Ordinarily, we provide the transport, but allowing a custom transport - # instance provides an extensibility point for unusual situations. - if isinstance(transport, OperationsTransport): - # transport is a OperationsTransport instance. - if credentials or client_options.credentials_file: - raise ValueError( - "When providing a transport instance, " - "provide its credentials directly." - ) - if client_options.scopes: - raise ValueError( - "When providing a transport instance, provide its scopes " - "directly." - ) - self._transport = transport - else: - Transport = type(self).get_transport_class(transport) - self._transport = Transport( - credentials=credentials, - credentials_file=client_options.credentials_file, - host=api_endpoint, - scopes=client_options.scopes, - client_cert_source_for_mtls=client_cert_source_func, - quota_project_id=client_options.quota_project_id, - client_info=client_info, - always_use_jwt_access=True, - ) + super().__init__( + credentials=credentials, + transport=transport, + client_options=client_options, + client_info=client_info, + ) + + @classmethod + def from_service_account_info(cls, info: dict, *args, **kwargs): + """Creates an instance of this client using the provided credentials + info. + + Args: + info (dict): The service account private key info. + args: Additional arguments to pass to the constructor. + kwargs: Additional arguments to pass to the constructor. + + Returns: + AbstractOperationsClient: The constructed client. + """ + credentials = service_account.Credentials.from_service_account_info(info) + kwargs["credentials"] = credentials + return cls(*args, **kwargs) + + @classmethod + def from_service_account_file(cls, filename: str, *args, **kwargs): + """Creates an instance of this client using the provided credentials + file. + + Args: + filename (str): The path to the service account private key json + file. + args: Additional arguments to pass to the constructor. + kwargs: Additional arguments to pass to the constructor. + + Returns: + AbstractOperationsClient: The constructed client. + """ + credentials = service_account.Credentials.from_service_account_file(filename) + kwargs["credentials"] = credentials + return cls(*args, **kwargs) + + from_service_account_json = from_service_account_file def list_operations( self, diff --git a/google/api_core/operations_v1/operations_rest_client_async.py b/google/api_core/operations_v1/operations_rest_client_async.py new file mode 100644 index 000000000..7ab0cd36f --- /dev/null +++ b/google/api_core/operations_v1/operations_rest_client_async.py @@ -0,0 +1,345 @@ +# -*- coding: utf-8 -*- +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from typing import Optional, Sequence, Tuple, Union + +from google.api_core import client_options as client_options_lib # type: ignore +from google.api_core import gapic_v1 # type: ignore +from google.api_core.operations_v1 import pagers_async as pagers +from google.api_core.operations_v1.transports.base import ( + DEFAULT_CLIENT_INFO, + OperationsTransport, +) +from google.api_core.operations_v1.abstract_operations_base_client import ( + AbstractOperationsBaseClient, +) +from google.longrunning import operations_pb2 + +try: + from google.auth.aio import credentials as ga_credentials # type: ignore +except ImportError as e: # pragma: NO COVER + raise ImportError( + "The `async_rest` extra of `google-api-core` is required to use long-running operations. Install it by running " + "`pip install google-api-core[async_rest]`." + ) from e + + +class AsyncOperationsRestClient(AbstractOperationsBaseClient): + """Manages long-running operations with a REST API service for the asynchronous client. + + When an API method normally takes long time to complete, it can be + designed to return [Operation][google.api_core.operations_v1.Operation] to the + client, and the client can use this interface to receive the real + response asynchronously by polling the operation resource, or pass + the operation resource to another API (such as Google Cloud Pub/Sub + API) to receive the response. Any API service that returns + long-running operations should implement the ``Operations`` + interface so developers can have a consistent client experience. + """ + + def __init__( + self, + *, + credentials: Optional[ga_credentials.Credentials] = None, + transport: Union[str, OperationsTransport, None] = None, + client_options: Optional[client_options_lib.ClientOptions] = None, + client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, + ) -> None: + """Instantiates the operations client. + + Args: + credentials (Optional[google.auth.aio.credentials.Credentials]): The + authorization credentials to attach to requests. These + credentials identify the application to the service; if none + are specified, the client will attempt to ascertain the + credentials from the environment. + transport (Union[str, OperationsTransport]): The + transport to use. If set to None, this defaults to 'rest_asyncio'. + client_options (google.api_core.client_options.ClientOptions): Custom options for the + client. It won't take effect if a ``transport`` instance is provided. + (1) The ``api_endpoint`` property can be used to override the + default endpoint provided by the client. GOOGLE_API_USE_MTLS_ENDPOINT + environment variable can also be used to override the endpoint: + "always" (always use the default mTLS endpoint), "never" (always + use the default regular endpoint) and "auto" (auto switch to the + default mTLS endpoint if client certificate is present, this is + the default value). However, the ``api_endpoint`` property takes + precedence if provided. + (2) If GOOGLE_API_USE_CLIENT_CERTIFICATE environment variable + is "true", then the ``client_cert_source`` property can be used + to provide client certificate for mutual TLS transport. If + not provided, the default SSL client certificate will be used if + present. If GOOGLE_API_USE_CLIENT_CERTIFICATE is "false" or not + set, no client certificate will be used. + client_info (google.api_core.gapic_v1.client_info.ClientInfo): + The client info used to send a user-agent string along with + API requests. If ``None``, then default info will be used. + Generally, you only need to set this if you're developing + your own client library. + + Raises: + google.auth.exceptions.MutualTLSChannelError: If mutual TLS transport + creation failed for any reason. + """ + super().__init__( + credentials=credentials, # type: ignore + # NOTE: If a transport is not provided, we force the client to use the async + # REST transport. + transport=transport or "rest_asyncio", + client_options=client_options, + client_info=client_info, + ) + + async def get_operation( + self, + name: str, + *, + # TODO(https://github.com/googleapis/python-api-core/issues/722): Leverage `retry` + # to allow configuring retryable error codes. + retry=gapic_v1.method_async.DEFAULT, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> operations_pb2.Operation: + r"""Gets the latest state of a long-running operation. + Clients can use this method to poll the operation result + at intervals as recommended by the API service. + + Args: + name (str): + The name of the operation resource. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + + Returns: + google.longrunning.operations_pb2.Operation: + This resource represents a long- + running operation that is the result of a + network API call. + + """ + + request = operations_pb2.GetOperationRequest(name=name) + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = self._transport._wrapped_methods[self._transport.get_operation] + + # Certain fields should be provided within the metadata header; + # add these here. + metadata = tuple(metadata or ()) + ( + gapic_v1.routing_header.to_grpc_metadata((("name", request.name),)), + ) + + # Send the request. + response = await rpc( + request, + retry=retry, + timeout=timeout, + metadata=metadata, + ) + + # Done; return the response. + return response + + async def list_operations( + self, + name: str, + filter_: Optional[str] = None, + *, + page_size: Optional[int] = None, + page_token: Optional[str] = None, + # TODO(https://github.com/googleapis/python-api-core/issues/722): Leverage `retry` + # to allow configuring retryable error codes. + retry=gapic_v1.method_async.DEFAULT, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> pagers.ListOperationsAsyncPager: + r"""Lists operations that match the specified filter in the request. + If the server doesn't support this method, it returns + ``UNIMPLEMENTED``. + + NOTE: the ``name`` binding allows API services to override the + binding to use different resource name schemes, such as + ``users/*/operations``. To override the binding, API services + can add a binding such as ``"/v1/{name=users/*}/operations"`` to + their service configuration. For backwards compatibility, the + default name includes the operations collection id, however + overriding users must ensure the name binding is the parent + resource, without the operations collection id. + + Args: + name (str): + The name of the operation's parent + resource. + filter_ (str): + The standard list filter. + This corresponds to the ``filter`` field + on the ``request`` instance; if ``request`` is provided, this + should not be set. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + + Returns: + google.api_core.operations_v1.pagers.ListOperationsPager: + The response message for + [Operations.ListOperations][google.api_core.operations_v1.Operations.ListOperations]. + + Iterating over this object will yield results and + resolve additional pages automatically. + + """ + # Create a protobuf request object. + request = operations_pb2.ListOperationsRequest(name=name, filter=filter_) + if page_size is not None: + request.page_size = page_size + if page_token is not None: + request.page_token = page_token + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = self._transport._wrapped_methods[self._transport.list_operations] + + # Certain fields should be provided within the metadata header; + # add these here. + metadata = tuple(metadata or ()) + ( + gapic_v1.routing_header.to_grpc_metadata((("name", request.name),)), + ) + + # Send the request. + response = await rpc( + request, + retry=retry, + timeout=timeout, + metadata=metadata, + ) + + # This method is paged; wrap the response in a pager, which provides + # an `__iter__` convenience method. + response = pagers.ListOperationsAsyncPager( + method=rpc, + request=request, + response=response, + metadata=metadata, + ) + + # Done; return the response. + return response + + async def delete_operation( + self, + name: str, + *, + # TODO(https://github.com/googleapis/python-api-core/issues/722): Leverage `retry` + # to allow configuring retryable error codes. + retry=gapic_v1.method_async.DEFAULT, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> None: + r"""Deletes a long-running operation. This method indicates that the + client is no longer interested in the operation result. It does + not cancel the operation. If the server doesn't support this + method, it returns ``google.rpc.Code.UNIMPLEMENTED``. + + Args: + name (str): + The name of the operation resource to + be deleted. + + This corresponds to the ``name`` field + on the ``request`` instance; if ``request`` is provided, this + should not be set. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + """ + # Create the request object. + request = operations_pb2.DeleteOperationRequest(name=name) + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = self._transport._wrapped_methods[self._transport.delete_operation] + + # Certain fields should be provided within the metadata header; + # add these here. + metadata = tuple(metadata or ()) + ( + gapic_v1.routing_header.to_grpc_metadata((("name", request.name),)), + ) + + # Send the request. + await rpc( + request, + retry=retry, + timeout=timeout, + metadata=metadata, + ) + + async def cancel_operation( + self, + name: Optional[str] = None, + *, + # TODO(https://github.com/googleapis/python-api-core/issues/722): Leverage `retry` + # to allow configuring retryable error codes. + retry=gapic_v1.method_async.DEFAULT, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> None: + r"""Starts asynchronous cancellation on a long-running operation. + The server makes a best effort to cancel the operation, but + success is not guaranteed. If the server doesn't support this + method, it returns ``google.rpc.Code.UNIMPLEMENTED``. Clients + can use + [Operations.GetOperation][google.api_core.operations_v1.Operations.GetOperation] + or other methods to check whether the cancellation succeeded or + whether the operation completed despite cancellation. On + successful cancellation, the operation is not deleted; instead, + it becomes an operation with an + [Operation.error][google.api_core.operations_v1.Operation.error] value with + a [google.rpc.Status.code][google.rpc.Status.code] of 1, + corresponding to ``Code.CANCELLED``. + + Args: + name (str): + The name of the operation resource to + be cancelled. + + This corresponds to the ``name`` field + on the ``request`` instance; if ``request`` is provided, this + should not be set. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + """ + # Create the request object. + request = operations_pb2.CancelOperationRequest(name=name) + + # Wrap the RPC method; this adds retry and timeout information, + # and friendly error handling. + rpc = self._transport._wrapped_methods[self._transport.cancel_operation] + + # Certain fields should be provided within the metadata header; + # add these here. + metadata = tuple(metadata or ()) + ( + gapic_v1.routing_header.to_grpc_metadata((("name", request.name),)), + ) + + # Send the request. + await rpc( + request, + retry=retry, + timeout=timeout, + metadata=metadata, + ) diff --git a/google/api_core/operations_v1/pagers.py b/google/api_core/operations_v1/pagers.py index b8a47757b..132f1c664 100644 --- a/google/api_core/operations_v1/pagers.py +++ b/google/api_core/operations_v1/pagers.py @@ -14,7 +14,6 @@ # limitations under the License. # from typing import ( - Any, Callable, Iterator, Sequence, @@ -22,9 +21,10 @@ ) from google.longrunning import operations_pb2 +from google.api_core.operations_v1.pagers_base import ListOperationsPagerBase -class ListOperationsPager: +class ListOperationsPager(ListOperationsPagerBase): """A pager for iterating through ``list_operations`` requests. This class thinly wraps an initial @@ -50,25 +50,9 @@ def __init__( *, metadata: Sequence[Tuple[str, str]] = () ): - """Instantiate the pager. - - Args: - method (Callable): The method that was originally called, and - which instantiated this pager. - request (google.longrunning.operations_pb2.ListOperationsRequest): - The initial request object. - response (google.longrunning.operations_pb2.ListOperationsResponse): - The initial response object. - metadata (Sequence[Tuple[str, str]]): Strings which should be - sent along with the request as metadata. - """ - self._method = method - self._request = request - self._response = response - self._metadata = metadata - - def __getattr__(self, name: str) -> Any: - return getattr(self._response, name) + super().__init__( + method=method, request=request, response=response, metadata=metadata + ) @property def pages(self) -> Iterator[operations_pb2.ListOperationsResponse]: @@ -81,6 +65,3 @@ def pages(self) -> Iterator[operations_pb2.ListOperationsResponse]: def __iter__(self) -> Iterator[operations_pb2.Operation]: for page in self.pages: yield from page.operations - - def __repr__(self) -> str: - return "{0}<{1!r}>".format(self.__class__.__name__, self._response) diff --git a/google/api_core/operations_v1/pagers_async.py b/google/api_core/operations_v1/pagers_async.py new file mode 100644 index 000000000..e2909dd50 --- /dev/null +++ b/google/api_core/operations_v1/pagers_async.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from typing import ( + Callable, + AsyncIterator, + Sequence, + Tuple, +) + +from google.longrunning import operations_pb2 +from google.api_core.operations_v1.pagers_base import ListOperationsPagerBase + + +class ListOperationsAsyncPager(ListOperationsPagerBase): + """A pager for iterating through ``list_operations`` requests. + + This class thinly wraps an initial + :class:`google.longrunning.operations_pb2.ListOperationsResponse` object, and + provides an ``__iter__`` method to iterate through its + ``operations`` field. + + If there are more pages, the ``__iter__`` method will make additional + ``ListOperations`` requests and continue to iterate + through the ``operations`` field on the + corresponding responses. + + All the usual :class:`google.longrunning.operations_pb2.ListOperationsResponse` + attributes are available on the pager. If multiple requests are made, only + the most recent response is retained, and thus used for attribute lookup. + """ + + def __init__( + self, + method: Callable[..., operations_pb2.ListOperationsResponse], + request: operations_pb2.ListOperationsRequest, + response: operations_pb2.ListOperationsResponse, + *, + metadata: Sequence[Tuple[str, str]] = () + ): + super().__init__( + method=method, request=request, response=response, metadata=metadata + ) + + @property + async def pages(self) -> AsyncIterator[operations_pb2.ListOperationsResponse]: + yield self._response + while self._response.next_page_token: + self._request.page_token = self._response.next_page_token + self._response = await self._method(self._request, metadata=self._metadata) + yield self._response + + def __aiter__(self) -> AsyncIterator[operations_pb2.Operation]: + async def async_generator(): + async for page in self.pages: + for operation in page.operations: + yield operation + + return async_generator() diff --git a/google/api_core/operations_v1/pagers_base.py b/google/api_core/operations_v1/pagers_base.py new file mode 100644 index 000000000..24caf74f2 --- /dev/null +++ b/google/api_core/operations_v1/pagers_base.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from typing import ( + Any, + Callable, + Sequence, + Tuple, +) + +from google.longrunning import operations_pb2 + + +class ListOperationsPagerBase: + """A pager for iterating through ``list_operations`` requests. + + This class thinly wraps an initial + :class:`google.longrunning.operations_pb2.ListOperationsResponse` object, and + provides an ``__iter__`` method to iterate through its + ``operations`` field. + + If there are more pages, the ``__iter__`` method will make additional + ``ListOperations`` requests and continue to iterate + through the ``operations`` field on the + corresponding responses. + + All the usual :class:`google.longrunning.operations_pb2.ListOperationsResponse` + attributes are available on the pager. If multiple requests are made, only + the most recent response is retained, and thus used for attribute lookup. + """ + + def __init__( + self, + method: Callable[..., operations_pb2.ListOperationsResponse], + request: operations_pb2.ListOperationsRequest, + response: operations_pb2.ListOperationsResponse, + *, + metadata: Sequence[Tuple[str, str]] = () + ): + """Instantiate the pager. + + Args: + method (Callable): The method that was originally called, and + which instantiated this pager. + request (google.longrunning.operations_pb2.ListOperationsRequest): + The initial request object. + response (google.longrunning.operations_pb2.ListOperationsResponse): + The initial response object. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + """ + self._method = method + self._request = request + self._response = response + self._metadata = metadata + + def __getattr__(self, name: str) -> Any: + return getattr(self._response, name) + + def __repr__(self) -> str: + return "{0}<{1!r}>".format(self.__class__.__name__, self._response) diff --git a/google/api_core/operations_v1/transports/__init__.py b/google/api_core/operations_v1/transports/__init__.py index df53e15e4..8c24ce6ef 100644 --- a/google/api_core/operations_v1/transports/__init__.py +++ b/google/api_core/operations_v1/transports/__init__.py @@ -14,16 +14,26 @@ # limitations under the License. # from collections import OrderedDict +from typing import cast, Dict, Tuple from .base import OperationsTransport from .rest import OperationsRestTransport - # Compile a registry of transports. -_transport_registry = OrderedDict() -_transport_registry["rest"] = OperationsRestTransport +_transport_registry: Dict[str, OperationsTransport] = OrderedDict() +_transport_registry["rest"] = cast(OperationsTransport, OperationsRestTransport) + +__all__: Tuple[str, ...] = ("OperationsTransport", "OperationsRestTransport") + +try: + from .rest_asyncio import AsyncOperationsRestTransport -__all__ = ( - "OperationsTransport", - "OperationsRestTransport", -) + __all__ += ("AsyncOperationsRestTransport",) + _transport_registry["rest_asyncio"] = cast( + OperationsTransport, AsyncOperationsRestTransport + ) +except ImportError: + # This import requires the `async_rest` extra. + # Don't raise an exception if `AsyncOperationsRestTransport` cannot be imported + # as other transports are still available. + pass diff --git a/google/api_core/operations_v1/transports/base.py b/google/api_core/operations_v1/transports/base.py index fb1d4fc9e..50e137612 100644 --- a/google/api_core/operations_v1/transports/base.py +++ b/google/api_core/operations_v1/transports/base.py @@ -14,6 +14,7 @@ # limitations under the License. # import abc +import re from typing import Awaitable, Callable, Optional, Sequence, Union import google.api_core # type: ignore @@ -25,10 +26,13 @@ from google.auth import credentials as ga_credentials # type: ignore from google.longrunning import operations_pb2 from google.oauth2 import service_account # type: ignore -from google.protobuf import empty_pb2 # type: ignore +import google.protobuf +from google.protobuf import empty_pb2, json_format # type: ignore from grpc import Compression +PROTOBUF_VERSION = google.protobuf.__version__ + DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( gapic_version=version.__version__, ) @@ -45,12 +49,14 @@ def __init__( self, *, host: str = DEFAULT_HOST, + # TODO(https://github.com/googleapis/python-api-core/issues/709): update type hint for credentials to include `google.auth.aio.Credentials`. credentials: Optional[ga_credentials.Credentials] = None, credentials_file: Optional[str] = None, scopes: Optional[Sequence[str]] = None, quota_project_id: Optional[str] = None, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, + url_scheme="https", **kwargs, ) -> None: """Instantiate the transport. @@ -76,10 +82,23 @@ def __init__( your own client library. always_use_jwt_access (Optional[bool]): Whether self signed JWT should be used for service account credentials. + url_scheme: the protocol scheme for the API endpoint. Normally + "https", but for testing or local servers, + "http" can be specified. """ + maybe_url_match = re.match("^(?Phttp(?:s)?://)?(?P.*)$", host) + if maybe_url_match is None: + raise ValueError( + f"Unexpected hostname structure: {host}" + ) # pragma: NO COVER + + url_match_items = maybe_url_match.groupdict() + + host = f"{url_scheme}://{host}" if not url_match_items["scheme"] else host + # Save the hostname. Default to port 443 (HTTPS) if none is specified. if ":" not in host: - host += ":443" + host += ":443" # pragma: NO COVER self._host = host scopes_kwargs = {"scopes": scopes, "default_scopes": self.AUTH_SCOPES} @@ -189,6 +208,37 @@ def close(self): """ raise NotImplementedError() + def _convert_protobuf_message_to_dict( + self, message: google.protobuf.message.Message + ): + r"""Converts protobuf message to a dictionary. + + When the dictionary is encoded to JSON, it conforms to proto3 JSON spec. + + Args: + message(google.protobuf.message.Message): The protocol buffers message + instance to serialize. + + Returns: + A dict representation of the protocol buffer message. + """ + # TODO(https://github.com/googleapis/python-api-core/issues/643): For backwards compatibility + # with protobuf 3.x 4.x, Remove once support for protobuf 3.x and 4.x is dropped. + if PROTOBUF_VERSION[0:2] in ["3.", "4."]: + result = json_format.MessageToDict( + message, + preserving_proto_field_name=True, + including_default_value_fields=True, # type: ignore # backward compatibility + ) + else: + result = json_format.MessageToDict( + message, + preserving_proto_field_name=True, + always_print_fields_with_no_presence=True, + ) + + return result + @property def list_operations( self, diff --git a/google/api_core/operations_v1/transports/rest.py b/google/api_core/operations_v1/transports/rest.py index f37bb3441..766a6685b 100644 --- a/google/api_core/operations_v1/transports/rest.py +++ b/google/api_core/operations_v1/transports/rest.py @@ -14,7 +14,6 @@ # limitations under the License. # -import re from typing import Callable, Dict, Optional, Sequence, Tuple, Union from requests import __version__ as requests_version @@ -41,7 +40,7 @@ DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( gapic_version=BASE_DEFAULT_CLIENT_INFO.gapic_version, grpc_version=None, - rest_version=requests_version, + rest_version=f"requests@{requests_version}", ) @@ -123,16 +122,6 @@ def __init__( # TODO(yon-mg): resolve other ctor params i.e. scopes, quota, etc. # TODO: When custom host (api_endpoint) is set, `scopes` must *also* be set on the # credentials object - maybe_url_match = re.match("^(?Phttp(?:s)?://)?(?P.*)$", host) - if maybe_url_match is None: - raise ValueError( - f"Unexpected hostname structure: {host}" - ) # pragma: NO COVER - - url_match_items = maybe_url_match.groupdict() - - host = f"{url_scheme}://{host}" if not url_match_items["scheme"] else host - super().__init__( host=host, credentials=credentials, @@ -144,6 +133,7 @@ def __init__( ) if client_cert_source_for_mtls: self._session.configure_mtls_channel(client_cert_source_for_mtls) + # TODO(https://github.com/googleapis/python-api-core/issues/720): Add wrap logic directly to the property methods for callables. self._prep_wrapped_messages(client_info) self._http_options = http_options or {} self._path_prefix = path_prefix @@ -152,6 +142,8 @@ def _list_operations( self, request: operations_pb2.ListOperationsRequest, *, + # TODO(https://github.com/googleapis/python-api-core/issues/723): Leverage `retry` + # to allow configuring retryable error codes. retry: OptionalRetry = gapic_v1.method.DEFAULT, timeout: Optional[float] = None, compression: Optional[grpc.Compression] = gapic_v1.method.DEFAULT, @@ -206,6 +198,7 @@ def _list_operations( # Send the request headers = dict(metadata) headers["Content-Type"] = "application/json" + # TODO(https://github.com/googleapis/python-api-core/issues/721): Update incorrect use of `uri`` variable name. response = getattr(self._session, method)( "{host}{uri}".format(host=self._host, uri=uri), timeout=timeout, @@ -227,6 +220,8 @@ def _get_operation( self, request: operations_pb2.GetOperationRequest, *, + # TODO(https://github.com/googleapis/python-api-core/issues/723): Leverage `retry` + # to allow configuring retryable error codes. retry: OptionalRetry = gapic_v1.method.DEFAULT, timeout: Optional[float] = None, compression: Optional[grpc.Compression] = gapic_v1.method.DEFAULT, @@ -282,6 +277,7 @@ def _get_operation( # Send the request headers = dict(metadata) headers["Content-Type"] = "application/json" + # TODO(https://github.com/googleapis/python-api-core/issues/721): Update incorrect use of `uri`` variable name. response = getattr(self._session, method)( "{host}{uri}".format(host=self._host, uri=uri), timeout=timeout, @@ -303,6 +299,8 @@ def _delete_operation( self, request: operations_pb2.DeleteOperationRequest, *, + # TODO(https://github.com/googleapis/python-api-core/issues/723): Leverage `retry` + # to allow configuring retryable error codes. retry: OptionalRetry = gapic_v1.method.DEFAULT, timeout: Optional[float] = None, compression: Optional[grpc.Compression] = gapic_v1.method.DEFAULT, @@ -351,6 +349,7 @@ def _delete_operation( # Send the request headers = dict(metadata) headers["Content-Type"] = "application/json" + # TODO(https://github.com/googleapis/python-api-core/issues/721): Update incorrect use of `uri`` variable name. response = getattr(self._session, method)( "{host}{uri}".format(host=self._host, uri=uri), timeout=timeout, @@ -369,6 +368,8 @@ def _cancel_operation( self, request: operations_pb2.CancelOperationRequest, *, + # TODO(https://github.com/googleapis/python-api-core/issues/723): Leverage `retry` + # to allow configuring retryable error codes. retry: OptionalRetry = gapic_v1.method.DEFAULT, timeout: Optional[float] = None, compression: Optional[grpc.Compression] = gapic_v1.method.DEFAULT, @@ -426,6 +427,7 @@ def _cancel_operation( # Send the request headers = dict(metadata) headers["Content-Type"] = "application/json" + # TODO(https://github.com/googleapis/python-api-core/issues/721): Update incorrect use of `uri`` variable name. response = getattr(self._session, method)( "{host}{uri}".format(host=self._host, uri=uri), timeout=timeout, @@ -441,38 +443,6 @@ def _cancel_operation( return empty_pb2.Empty() - def _convert_protobuf_message_to_dict( - self, message: google.protobuf.message.Message - ): - r"""Converts protobuf message to a dictionary. - - When the dictionary is encoded to JSON, it conforms to proto3 JSON spec. - - Args: - message(google.protobuf.message.Message): The protocol buffers message - instance to serialize. - - Returns: - A dict representation of the protocol buffer message. - """ - # For backwards compatibility with protobuf 3.x 4.x - # Remove once support for protobuf 3.x and 4.x is dropped - # https://github.com/googleapis/python-api-core/issues/643 - if PROTOBUF_VERSION[0:2] in ["3.", "4."]: - result = json_format.MessageToDict( - message, - preserving_proto_field_name=True, - including_default_value_fields=True, # type: ignore # backward compatibility - ) - else: - result = json_format.MessageToDict( - message, - preserving_proto_field_name=True, - always_print_fields_with_no_presence=True, - ) - - return result - @property def list_operations( self, diff --git a/google/api_core/operations_v1/transports/rest_asyncio.py b/google/api_core/operations_v1/transports/rest_asyncio.py new file mode 100644 index 000000000..71c20eb8a --- /dev/null +++ b/google/api_core/operations_v1/transports/rest_asyncio.py @@ -0,0 +1,560 @@ +# -*- coding: utf-8 -*- +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import json +from typing import Any, Callable, Coroutine, Dict, Optional, Sequence, Tuple + +from google.auth import __version__ as auth_version + +try: + from google.auth.aio.transport.sessions import AsyncAuthorizedSession # type: ignore +except ImportError as e: # pragma: NO COVER + raise ImportError( + "The `async_rest` extra of `google-api-core` is required to use long-running operations. Install it by running " + "`pip install google-api-core[async_rest]`." + ) from e + +from google.api_core import exceptions as core_exceptions # type: ignore +from google.api_core import gapic_v1 # type: ignore +from google.api_core import path_template # type: ignore +from google.api_core import rest_helpers # type: ignore +from google.api_core import retry_async as retries_async # type: ignore +from google.auth.aio import credentials as ga_credentials_async # type: ignore +from google.longrunning import operations_pb2 # type: ignore +from google.protobuf import empty_pb2 # type: ignore +from google.protobuf import json_format # type: ignore + +from .base import DEFAULT_CLIENT_INFO as BASE_DEFAULT_CLIENT_INFO, OperationsTransport + +DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( + gapic_version=BASE_DEFAULT_CLIENT_INFO.gapic_version, + grpc_version=None, + rest_version=f"google-auth@{auth_version}", +) + + +class AsyncOperationsRestTransport(OperationsTransport): + """Asynchronous REST backend transport for Operations. + + Manages async long-running operations with an API service. + + When an API method normally takes long time to complete, it can be + designed to return [Operation][google.api_core.operations_v1.Operation] to the + client, and the client can use this interface to receive the real + response asynchronously by polling the operation resource, or pass + the operation resource to another API (such as Google Cloud Pub/Sub + API) to receive the response. Any API service that returns + long-running operations should implement the ``Operations`` + interface so developers can have a consistent client experience. + + This class defines the same methods as the primary client, so the + primary client can load the underlying transport implementation + and call it. + + It sends JSON representations of protocol buffers over HTTP/1.1 + """ + + def __init__( + self, + *, + host: str = "longrunning.googleapis.com", + credentials: Optional[ga_credentials_async.Credentials] = None, + credentials_file: Optional[str] = None, + scopes: Optional[Sequence[str]] = None, + client_cert_source_for_mtls: Optional[Callable[[], Tuple[bytes, bytes]]] = None, + quota_project_id: Optional[str] = None, + client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, + always_use_jwt_access: Optional[bool] = False, + url_scheme: str = "https", + http_options: Optional[Dict] = None, + path_prefix: str = "v1", + # TODO(https://github.com/googleapis/python-api-core/issues/715): Add docstring for `credentials_file` to async REST transport. + # TODO(https://github.com/googleapis/python-api-core/issues/716): Add docstring for `scopes` to async REST transport. + # TODO(https://github.com/googleapis/python-api-core/issues/717): Add docstring for `quota_project_id` to async REST transport. + # TODO(https://github.com/googleapis/python-api-core/issues/718): Add docstring for `client_cert_source` to async REST transport. + ) -> None: + """Instantiate the transport. + + Args: + host (Optional[str]): + The hostname to connect to. + credentials (Optional[google.auth.aio.credentials.Credentials]): The + authorization credentials to attach to requests. These + credentials identify the application to the service; if none + are specified, the client will attempt to ascertain the + credentials from the environment. + client_info (google.api_core.gapic_v1.client_info.ClientInfo): + The client info used to send a user-agent string along with + API requests. If ``None``, then default info will be used. + Generally, you only need to set this if you're developing + your own client library. + always_use_jwt_access (Optional[bool]): Whether self signed JWT should + be used for service account credentials. + url_scheme: the protocol scheme for the API endpoint. Normally + "https", but for testing or local servers, + "http" can be specified. + http_options: a dictionary of http_options for transcoding, to override + the defaults from operations.proto. Each method has an entry + with the corresponding http rules as value. + path_prefix: path prefix (usually represents API version). Set to + "v1" by default. + + """ + unsupported_params = { + # TODO(https://github.com/googleapis/python-api-core/issues/715): Add support for `credentials_file` to async REST transport. + "google.api_core.client_options.ClientOptions.credentials_file": credentials_file, + # TODO(https://github.com/googleapis/python-api-core/issues/716): Add support for `scopes` to async REST transport. + "google.api_core.client_options.ClientOptions.scopes": scopes, + # TODO(https://github.com/googleapis/python-api-core/issues/717): Add support for `quota_project_id` to async REST transport. + "google.api_core.client_options.ClientOptions.quota_project_id": quota_project_id, + # TODO(https://github.com/googleapis/python-api-core/issues/718): Add support for `client_cert_source` to async REST transport. + "google.api_core.client_options.ClientOptions.client_cert_source": client_cert_source_for_mtls, + # TODO(https://github.com/googleapis/python-api-core/issues/718): Add support for `client_cert_source` to async REST transport. + "google.api_core.client_options.ClientOptions.client_cert_source": client_cert_source_for_mtls, + } + provided_unsupported_params = [ + name for name, value in unsupported_params.items() if value is not None + ] + if provided_unsupported_params: + raise core_exceptions.AsyncRestUnsupportedParameterError( + f"The following provided parameters are not supported for `transport=rest_asyncio`: {', '.join(provided_unsupported_params)}" + ) + + super().__init__( + host=host, + # TODO(https://github.com/googleapis/python-api-core/issues/709): Remove `type: ignore` when the linked issue is resolved. + credentials=credentials, # type: ignore + client_info=client_info, + # TODO(https://github.com/googleapis/python-api-core/issues/725): Set always_use_jwt_access token when supported. + always_use_jwt_access=False, + ) + # TODO(https://github.com/googleapis/python-api-core/issues/708): add support for + # `default_host` in AsyncAuthorizedSession for feature parity with the synchronous + # code. + # TODO(https://github.com/googleapis/python-api-core/issues/709): Remove `type: ignore` when the linked issue is resolved. + self._session = AsyncAuthorizedSession(self._credentials) # type: ignore + # TODO(https://github.com/googleapis/python-api-core/issues/720): Add wrap logic directly to the property methods for callables. + self._prep_wrapped_messages(client_info) + self._http_options = http_options or {} + self._path_prefix = path_prefix + + def _prep_wrapped_messages(self, client_info): + # Precompute the wrapped methods. + self._wrapped_methods = { + self.list_operations: gapic_v1.method_async.wrap_method( + self.list_operations, + default_retry=retries_async.AsyncRetry( + initial=0.5, + maximum=10.0, + multiplier=2.0, + predicate=retries_async.if_exception_type( + core_exceptions.ServiceUnavailable, + ), + deadline=10.0, + ), + default_timeout=10.0, + client_info=client_info, + kind="rest_asyncio", + ), + self.get_operation: gapic_v1.method_async.wrap_method( + self.get_operation, + default_retry=retries_async.AsyncRetry( + initial=0.5, + maximum=10.0, + multiplier=2.0, + predicate=retries_async.if_exception_type( + core_exceptions.ServiceUnavailable, + ), + deadline=10.0, + ), + default_timeout=10.0, + client_info=client_info, + kind="rest_asyncio", + ), + self.delete_operation: gapic_v1.method_async.wrap_method( + self.delete_operation, + default_retry=retries_async.AsyncRetry( + initial=0.5, + maximum=10.0, + multiplier=2.0, + predicate=retries_async.if_exception_type( + core_exceptions.ServiceUnavailable, + ), + deadline=10.0, + ), + default_timeout=10.0, + client_info=client_info, + kind="rest_asyncio", + ), + self.cancel_operation: gapic_v1.method_async.wrap_method( + self.cancel_operation, + default_retry=retries_async.AsyncRetry( + initial=0.5, + maximum=10.0, + multiplier=2.0, + predicate=retries_async.if_exception_type( + core_exceptions.ServiceUnavailable, + ), + deadline=10.0, + ), + default_timeout=10.0, + client_info=client_info, + kind="rest_asyncio", + ), + } + + async def _list_operations( + self, + request: operations_pb2.ListOperationsRequest, + *, + # TODO(https://github.com/googleapis/python-api-core/issues/722): Leverage `retry` + # to allow configuring retryable error codes. + retry=gapic_v1.method_async.DEFAULT, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> operations_pb2.ListOperationsResponse: + r"""Asynchronously call the list operations method over HTTP. + + Args: + request (~.operations_pb2.ListOperationsRequest): + The request object. The request message for + [Operations.ListOperations][google.api_core.operations_v1.Operations.ListOperations]. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + + Returns: + ~.operations_pb2.ListOperationsResponse: + The response message for + [Operations.ListOperations][google.api_core.operations_v1.Operations.ListOperations]. + + """ + + http_options = [ + { + "method": "get", + "uri": "/{}/{{name=**}}/operations".format(self._path_prefix), + }, + ] + if "google.longrunning.Operations.ListOperations" in self._http_options: + http_options = self._http_options[ + "google.longrunning.Operations.ListOperations" + ] + + request_kwargs = self._convert_protobuf_message_to_dict(request) + transcoded_request = path_template.transcode(http_options, **request_kwargs) + + uri = transcoded_request["uri"] + method = transcoded_request["method"] + + # Jsonify the query params + query_params_request = operations_pb2.ListOperationsRequest() + json_format.ParseDict(transcoded_request["query_params"], query_params_request) + query_params = json_format.MessageToDict( + query_params_request, + preserving_proto_field_name=False, + use_integers_for_enums=False, + ) + + # Send the request + headers = dict(metadata) + headers["Content-Type"] = "application/json" + # TODO(https://github.com/googleapis/python-api-core/issues/721): Update incorrect use of `uri`` variable name. + response = await getattr(self._session, method)( + "{host}{uri}".format(host=self._host, uri=uri), + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params), + ) + content = await response.read() + + # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception + # subclass. + if response.status_code >= 400: + payload = json.loads(content.decode("utf-8")) + request_url = "{host}{uri}".format(host=self._host, uri=uri) + raise core_exceptions.format_http_response_error(response, method, request_url, payload) # type: ignore + + # Return the response + api_response = operations_pb2.ListOperationsResponse() + json_format.Parse(content, api_response, ignore_unknown_fields=False) + return api_response + + async def _get_operation( + self, + request: operations_pb2.GetOperationRequest, + *, + # TODO(https://github.com/googleapis/python-api-core/issues/722): Leverage `retry` + # to allow configuring retryable error codes. + retry=gapic_v1.method_async.DEFAULT, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> operations_pb2.Operation: + r"""Asynchronously call the get operation method over HTTP. + + Args: + request (~.operations_pb2.GetOperationRequest): + The request object. The request message for + [Operations.GetOperation][google.api_core.operations_v1.Operations.GetOperation]. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + + Returns: + ~.operations_pb2.Operation: + This resource represents a long- + running operation that is the result of a + network API call. + + """ + + http_options = [ + { + "method": "get", + "uri": "/{}/{{name=**/operations/*}}".format(self._path_prefix), + }, + ] + if "google.longrunning.Operations.GetOperation" in self._http_options: + http_options = self._http_options[ + "google.longrunning.Operations.GetOperation" + ] + + request_kwargs = self._convert_protobuf_message_to_dict(request) + transcoded_request = path_template.transcode(http_options, **request_kwargs) + + uri = transcoded_request["uri"] + method = transcoded_request["method"] + + # Jsonify the query params + query_params_request = operations_pb2.GetOperationRequest() + json_format.ParseDict(transcoded_request["query_params"], query_params_request) + query_params = json_format.MessageToDict( + query_params_request, + preserving_proto_field_name=False, + use_integers_for_enums=False, + ) + + # Send the request + headers = dict(metadata) + headers["Content-Type"] = "application/json" + # TODO(https://github.com/googleapis/python-api-core/issues/721): Update incorrect use of `uri`` variable name. + response = await getattr(self._session, method)( + "{host}{uri}".format(host=self._host, uri=uri), + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params), + ) + content = await response.read() + + # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception + # subclass. + if response.status_code >= 400: + payload = json.loads(content.decode("utf-8")) + request_url = "{host}{uri}".format(host=self._host, uri=uri) + raise core_exceptions.format_http_response_error(response, method, request_url, payload) # type: ignore + + # Return the response + api_response = operations_pb2.Operation() + json_format.Parse(content, api_response, ignore_unknown_fields=False) + return api_response + + async def _delete_operation( + self, + request: operations_pb2.DeleteOperationRequest, + *, + # TODO(https://github.com/googleapis/python-api-core/issues/722): Leverage `retry` + # to allow configuring retryable error codes. + retry=gapic_v1.method_async.DEFAULT, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> empty_pb2.Empty: + r"""Asynchronously call the delete operation method over HTTP. + + Args: + request (~.operations_pb2.DeleteOperationRequest): + The request object. The request message for + [Operations.DeleteOperation][google.api_core.operations_v1.Operations.DeleteOperation]. + + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + """ + + http_options = [ + { + "method": "delete", + "uri": "/{}/{{name=**/operations/*}}".format(self._path_prefix), + }, + ] + if "google.longrunning.Operations.DeleteOperation" in self._http_options: + http_options = self._http_options[ + "google.longrunning.Operations.DeleteOperation" + ] + + request_kwargs = self._convert_protobuf_message_to_dict(request) + transcoded_request = path_template.transcode(http_options, **request_kwargs) + + uri = transcoded_request["uri"] + method = transcoded_request["method"] + + # Jsonify the query params + query_params_request = operations_pb2.DeleteOperationRequest() + json_format.ParseDict(transcoded_request["query_params"], query_params_request) + query_params = json_format.MessageToDict( + query_params_request, + preserving_proto_field_name=False, + use_integers_for_enums=False, + ) + + # Send the request + headers = dict(metadata) + headers["Content-Type"] = "application/json" + # TODO(https://github.com/googleapis/python-api-core/issues/721): Update incorrect use of `uri`` variable name. + response = await getattr(self._session, method)( + "{host}{uri}".format(host=self._host, uri=uri), + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params), + ) + + # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception + # subclass. + if response.status_code >= 400: + content = await response.read() + payload = json.loads(content.decode("utf-8")) + request_url = "{host}{uri}".format(host=self._host, uri=uri) + raise core_exceptions.format_http_response_error(response, method, request_url, payload) # type: ignore + + return empty_pb2.Empty() + + async def _cancel_operation( + self, + request: operations_pb2.CancelOperationRequest, + *, + # TODO(https://github.com/googleapis/python-api-core/issues/722): Leverage `retry` + # to allow configuring retryable error codes. + retry=gapic_v1.method_async.DEFAULT, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, str]] = (), + # TODO(https://github.com/googleapis/python-api-core/issues/722): Add `retry` parameter + # to allow configuring retryable error codes. + ) -> empty_pb2.Empty: + r"""Asynchronously call the cancel operation method over HTTP. + + Args: + request (~.operations_pb2.CancelOperationRequest): + The request object. The request message for + [Operations.CancelOperation][google.api_core.operations_v1.Operations.CancelOperation]. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + """ + + http_options = [ + { + "method": "post", + "uri": "/{}/{{name=**/operations/*}}:cancel".format(self._path_prefix), + "body": "*", + }, + ] + if "google.longrunning.Operations.CancelOperation" in self._http_options: + http_options = self._http_options[ + "google.longrunning.Operations.CancelOperation" + ] + + request_kwargs = self._convert_protobuf_message_to_dict(request) + transcoded_request = path_template.transcode(http_options, **request_kwargs) + + # Jsonify the request body + body_request = operations_pb2.CancelOperationRequest() + json_format.ParseDict(transcoded_request["body"], body_request) + body = json_format.MessageToDict( + body_request, + preserving_proto_field_name=False, + use_integers_for_enums=False, + ) + uri = transcoded_request["uri"] + method = transcoded_request["method"] + + # Jsonify the query params + query_params_request = operations_pb2.CancelOperationRequest() + json_format.ParseDict(transcoded_request["query_params"], query_params_request) + query_params = json_format.MessageToDict( + query_params_request, + preserving_proto_field_name=False, + use_integers_for_enums=False, + ) + + # Send the request + headers = dict(metadata) + headers["Content-Type"] = "application/json" + # TODO(https://github.com/googleapis/python-api-core/issues/721): Update incorrect use of `uri`` variable name. + response = await getattr(self._session, method)( + "{host}{uri}".format(host=self._host, uri=uri), + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params), + data=body, + ) + + # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception + # subclass. + if response.status_code >= 400: + content = await response.read() + payload = json.loads(content.decode("utf-8")) + request_url = "{host}{uri}".format(host=self._host, uri=uri) + raise core_exceptions.format_http_response_error(response, method, request_url, payload) # type: ignore + + return empty_pb2.Empty() + + @property + def list_operations( + self, + ) -> Callable[ + [operations_pb2.ListOperationsRequest], + Coroutine[Any, Any, operations_pb2.ListOperationsResponse], + ]: + return self._list_operations + + @property + def get_operation( + self, + ) -> Callable[ + [operations_pb2.GetOperationRequest], + Coroutine[Any, Any, operations_pb2.Operation], + ]: + return self._get_operation + + @property + def delete_operation( + self, + ) -> Callable[ + [operations_pb2.DeleteOperationRequest], Coroutine[Any, Any, empty_pb2.Empty] + ]: + return self._delete_operation + + @property + def cancel_operation( + self, + ) -> Callable[ + [operations_pb2.CancelOperationRequest], Coroutine[Any, Any, empty_pb2.Empty] + ]: + return self._cancel_operation + + +__all__ = ("AsyncOperationsRestTransport",) diff --git a/tests/unit/operations_v1/test_operations_rest_client.py b/tests/unit/operations_v1/test_operations_rest_client.py index 4ab4f1f75..644cf2663 100644 --- a/tests/unit/operations_v1/test_operations_rest_client.py +++ b/tests/unit/operations_v1/test_operations_rest_client.py @@ -17,21 +17,24 @@ import mock import pytest +from typing import Any, List try: import grpc # noqa: F401 except ImportError: # pragma: NO COVER pytest.skip("No GRPC", allow_module_level=True) from requests import Response # noqa I201 -from requests.sessions import Session +from google.auth.transport.requests import AuthorizedSession from google.api_core import client_options from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 from google.api_core.operations_v1 import AbstractOperationsClient + +import google.auth from google.api_core.operations_v1 import pagers +from google.api_core.operations_v1 import pagers_async from google.api_core.operations_v1 import transports -import google.auth from google.auth import credentials as ga_credentials from google.auth.exceptions import MutualTLSChannelError from google.longrunning import operations_pb2 @@ -39,6 +42,16 @@ from google.protobuf import json_format # type: ignore from google.rpc import status_pb2 # type: ignore +try: + import aiohttp # noqa: F401 + import google.auth.aio.transport + from google.auth.aio.transport.sessions import AsyncAuthorizedSession + from google.api_core.operations_v1 import AsyncOperationsRestClient + from google.auth.aio import credentials as ga_credentials_async + + GOOGLE_AUTH_AIO_INSTALLED = True +except ImportError: + GOOGLE_AUTH_AIO_INSTALLED = False HTTP_OPTIONS = { "google.longrunning.Operations.CancelOperation": [ @@ -55,17 +68,62 @@ ], } +PYPARAM_CLIENT: List[Any] = [ + AbstractOperationsClient, +] +PYPARAM_CLIENT_TRANSPORT_NAME = [ + [AbstractOperationsClient, transports.OperationsRestTransport, "rest"], +] +PYPARAM_CLIENT_TRANSPORT_CREDENTIALS = [ + [ + AbstractOperationsClient, + transports.OperationsRestTransport, + ga_credentials.AnonymousCredentials(), + ], +] + +if GOOGLE_AUTH_AIO_INSTALLED: + PYPARAM_CLIENT.append(AsyncOperationsRestClient) + PYPARAM_CLIENT_TRANSPORT_NAME.append( + [ + AsyncOperationsRestClient, + transports.AsyncOperationsRestTransport, + "rest_asyncio", + ] + ) + PYPARAM_CLIENT_TRANSPORT_CREDENTIALS.append( + [ + AsyncOperationsRestClient, + transports.AsyncOperationsRestTransport, + ga_credentials_async.AnonymousCredentials(), + ] + ) + def client_cert_source_callback(): return b"cert bytes", b"key bytes" -def _get_operations_client(http_options=HTTP_OPTIONS): - transport = transports.rest.OperationsRestTransport( - credentials=ga_credentials.AnonymousCredentials(), http_options=http_options +def _get_session_type(is_async: bool): + return ( + AsyncAuthorizedSession + if is_async and GOOGLE_AUTH_AIO_INSTALLED + else AuthorizedSession ) - return AbstractOperationsClient(transport=transport) + +def _get_operations_client(is_async: bool, http_options=HTTP_OPTIONS): + if is_async and GOOGLE_AUTH_AIO_INSTALLED: + async_transport = transports.rest_asyncio.AsyncOperationsRestTransport( + credentials=ga_credentials_async.AnonymousCredentials(), + http_options=http_options, + ) + return AsyncOperationsRestClient(transport=async_transport) + else: + sync_transport = transports.rest.OperationsRestTransport( + credentials=ga_credentials.AnonymousCredentials(), http_options=http_options + ) + return AbstractOperationsClient(transport=sync_transport) # If default endpoint is localhost, then default mtls endpoint will be the same. @@ -79,57 +137,69 @@ def modify_default_endpoint(client): ) -def test__get_default_mtls_endpoint(): +# TODO: Add support for mtls in async rest +@pytest.mark.parametrize( + "client_class", + [ + AbstractOperationsClient, + ], +) +def test__get_default_mtls_endpoint(client_class): api_endpoint = "example.googleapis.com" api_mtls_endpoint = "example.mtls.googleapis.com" sandbox_endpoint = "example.sandbox.googleapis.com" sandbox_mtls_endpoint = "example.mtls.sandbox.googleapis.com" non_googleapi = "api.example.com" - assert AbstractOperationsClient._get_default_mtls_endpoint(None) is None - assert ( - AbstractOperationsClient._get_default_mtls_endpoint(api_endpoint) - == api_mtls_endpoint - ) + assert client_class._get_default_mtls_endpoint(None) is None + assert client_class._get_default_mtls_endpoint(api_endpoint) == api_mtls_endpoint assert ( - AbstractOperationsClient._get_default_mtls_endpoint(api_mtls_endpoint) - == api_mtls_endpoint + client_class._get_default_mtls_endpoint(api_mtls_endpoint) == api_mtls_endpoint ) assert ( - AbstractOperationsClient._get_default_mtls_endpoint(sandbox_endpoint) + client_class._get_default_mtls_endpoint(sandbox_endpoint) == sandbox_mtls_endpoint ) assert ( - AbstractOperationsClient._get_default_mtls_endpoint(sandbox_mtls_endpoint) + client_class._get_default_mtls_endpoint(sandbox_mtls_endpoint) == sandbox_mtls_endpoint ) - assert ( - AbstractOperationsClient._get_default_mtls_endpoint(non_googleapi) - == non_googleapi - ) + assert client_class._get_default_mtls_endpoint(non_googleapi) == non_googleapi -@pytest.mark.parametrize("client_class", [AbstractOperationsClient]) +@pytest.mark.parametrize( + "client_class", + PYPARAM_CLIENT, +) def test_operations_client_from_service_account_info(client_class): creds = ga_credentials.AnonymousCredentials() - with mock.patch.object( - service_account.Credentials, "from_service_account_info" - ) as factory: - factory.return_value = creds - info = {"valid": True} - client = client_class.from_service_account_info(info) - assert client.transport._credentials == creds - assert isinstance(client, client_class) + if "async" in str(client_class): + # TODO(): Add support for service account info to async REST transport. + with pytest.raises(NotImplementedError): + info = {"valid": True} + client_class.from_service_account_info(info) + else: + with mock.patch.object( + service_account.Credentials, "from_service_account_info" + ) as factory: + factory.return_value = creds + info = {"valid": True} + client = client_class.from_service_account_info(info) + assert client.transport._credentials == creds + assert isinstance(client, client_class) - assert client.transport._host == "https://longrunning.googleapis.com" + assert client.transport._host == "https://longrunning.googleapis.com" @pytest.mark.parametrize( - "transport_class,transport_name", [(transports.OperationsRestTransport, "rest")] + "transport_class", + [ + transports.OperationsRestTransport, + # TODO(https://github.com/googleapis/python-api-core/issues/706): Add support for + # service account credentials in transports.AsyncOperationsRestTransport + ], ) -def test_operations_client_service_account_always_use_jwt( - transport_class, transport_name -): +def test_operations_client_service_account_always_use_jwt(transport_class): with mock.patch.object( service_account.Credentials, "with_always_use_jwt_access", create=True ) as use_jwt: @@ -145,35 +215,53 @@ def test_operations_client_service_account_always_use_jwt( use_jwt.assert_not_called() -@pytest.mark.parametrize("client_class", [AbstractOperationsClient]) +@pytest.mark.parametrize( + "client_class", + PYPARAM_CLIENT, +) def test_operations_client_from_service_account_file(client_class): - creds = ga_credentials.AnonymousCredentials() - with mock.patch.object( - service_account.Credentials, "from_service_account_file" - ) as factory: - factory.return_value = creds - client = client_class.from_service_account_file("dummy/file/path.json") - assert client.transport._credentials == creds - assert isinstance(client, client_class) - client = client_class.from_service_account_json("dummy/file/path.json") - assert client.transport._credentials == creds - assert isinstance(client, client_class) + if "async" in str(client_class): + # TODO(): Add support for service account creds to async REST transport. + with pytest.raises(NotImplementedError): + client_class.from_service_account_file("dummy/file/path.json") + else: + creds = ga_credentials.AnonymousCredentials() + with mock.patch.object( + service_account.Credentials, "from_service_account_file" + ) as factory: + factory.return_value = creds + client = client_class.from_service_account_file("dummy/file/path.json") + assert client.transport._credentials == creds + assert isinstance(client, client_class) + + client = client_class.from_service_account_json("dummy/file/path.json") + assert client.transport._credentials == creds + assert isinstance(client, client_class) - assert client.transport._host == "https://longrunning.googleapis.com" + assert client.transport._host == "https://longrunning.googleapis.com" -def test_operations_client_get_transport_class(): - transport = AbstractOperationsClient.get_transport_class() +@pytest.mark.parametrize( + "client_class,transport_class,transport_name", + PYPARAM_CLIENT_TRANSPORT_NAME, +) +def test_operations_client_get_transport_class( + client_class, transport_class, transport_name +): + transport = client_class.get_transport_class() available_transports = [ transports.OperationsRestTransport, ] + if GOOGLE_AUTH_AIO_INSTALLED: + available_transports.append(transports.AsyncOperationsRestTransport) assert transport in available_transports - transport = AbstractOperationsClient.get_transport_class("rest") - assert transport == transports.OperationsRestTransport + transport = client_class.get_transport_class(transport_name) + assert transport == transport_class +# TODO(): Update this test case to include async REST once we have support for MTLS. @pytest.mark.parametrize( "client_class,transport_class,transport_name", [(AbstractOperationsClient, transports.OperationsRestTransport, "rest")], @@ -186,22 +274,21 @@ def test_operations_client_get_transport_class(): def test_operations_client_client_options( client_class, transport_class, transport_name ): - # Check that if channel is provided we won't create a new one. - with mock.patch.object(AbstractOperationsClient, "get_transport_class") as gtc: - transport = transport_class(credentials=ga_credentials.AnonymousCredentials()) - client = client_class(transport=transport) - gtc.assert_not_called() + # # Check that if channel is provided we won't create a new one. + # with mock.patch.object(AbstractOperationsBaseClient, "get_transport_class") as gtc: + # client = client_class(transport=transport_class()) + # gtc.assert_not_called() - # Check that if channel is provided via str we will create a new one. - with mock.patch.object(AbstractOperationsClient, "get_transport_class") as gtc: - client = client_class(transport=transport_name) - gtc.assert_called() + # # Check that if channel is provided via str we will create a new one. + # with mock.patch.object(AbstractOperationsBaseClient, "get_transport_class") as gtc: + # client = client_class(transport=transport_name) + # gtc.assert_called() # Check the case api_endpoint is provided. options = client_options.ClientOptions(api_endpoint="squid.clam.whelk") with mock.patch.object(transport_class, "__init__") as patched: patched.return_value = None - client = client_class(client_options=options) + client = client_class(client_options=options, transport=transport_name) patched.assert_called_once_with( credentials=None, credentials_file=None, @@ -218,7 +305,7 @@ def test_operations_client_client_options( with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "never"}): with mock.patch.object(transport_class, "__init__") as patched: patched.return_value = None - client = client_class() + client = client_class(transport=transport_name) patched.assert_called_once_with( credentials=None, credentials_file=None, @@ -235,7 +322,7 @@ def test_operations_client_client_options( with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "always"}): with mock.patch.object(transport_class, "__init__") as patched: patched.return_value = None - client = client_class() + client = client_class(transport=transport_name) patched.assert_called_once_with( credentials=None, credentials_file=None, @@ -264,7 +351,7 @@ def test_operations_client_client_options( options = client_options.ClientOptions(quota_project_id="octopus") with mock.patch.object(transport_class, "__init__") as patched: patched.return_value = None - client = client_class(client_options=options) + client = client_class(client_options=options, transport=transport_name) patched.assert_called_once_with( credentials=None, credentials_file=None, @@ -277,6 +364,7 @@ def test_operations_client_client_options( ) +# TODO: Add support for mtls in async REST @pytest.mark.parametrize( "client_class,transport_class,transport_name,use_client_cert_env", [ @@ -393,7 +481,7 @@ def fake_init(client_cert_source_for_mtls=None, **kwargs): @pytest.mark.parametrize( "client_class,transport_class,transport_name", - [(AbstractOperationsClient, transports.OperationsRestTransport, "rest")], + PYPARAM_CLIENT_TRANSPORT_NAME, ) def test_operations_client_client_options_scopes( client_class, transport_class, transport_name @@ -402,52 +490,59 @@ def test_operations_client_client_options_scopes( options = client_options.ClientOptions( scopes=["1", "2"], ) - with mock.patch.object(transport_class, "__init__") as patched: - patched.return_value = None - client = client_class(client_options=options) - patched.assert_called_once_with( - credentials=None, - credentials_file=None, - host=client.DEFAULT_ENDPOINT, - scopes=["1", "2"], - client_cert_source_for_mtls=None, - quota_project_id=None, - client_info=transports.base.DEFAULT_CLIENT_INFO, - always_use_jwt_access=True, - ) + if "async" in str(client_class): + # TODO(): Add support for scopes to async REST transport. + with pytest.raises(core_exceptions.AsyncRestUnsupportedParameterError): + client_class(client_options=options, transport=transport_name) + else: + with mock.patch.object(transport_class, "__init__") as patched: + patched.return_value = None + client = client_class(client_options=options, transport=transport_name) + patched.assert_called_once_with( + credentials=None, + credentials_file=None, + host=client.DEFAULT_ENDPOINT, + scopes=["1", "2"], + client_cert_source_for_mtls=None, + quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, + always_use_jwt_access=True, + ) @pytest.mark.parametrize( "client_class,transport_class,transport_name", - [(AbstractOperationsClient, transports.OperationsRestTransport, "rest")], + PYPARAM_CLIENT_TRANSPORT_NAME, ) def test_operations_client_client_options_credentials_file( client_class, transport_class, transport_name ): # Check the case credentials file is provided. options = client_options.ClientOptions(credentials_file="credentials.json") - with mock.patch.object(transport_class, "__init__") as patched: - patched.return_value = None - client = client_class(client_options=options) - patched.assert_called_once_with( - credentials=None, - credentials_file="credentials.json", - host=client.DEFAULT_ENDPOINT, - scopes=None, - client_cert_source_for_mtls=None, - quota_project_id=None, - client_info=transports.base.DEFAULT_CLIENT_INFO, - always_use_jwt_access=True, - ) - + if "async" in str(client_class): + # TODO(): Add support for credentials file to async REST transport. + with pytest.raises(core_exceptions.AsyncRestUnsupportedParameterError): + client_class(client_options=options, transport=transport_name) + else: + with mock.patch.object(transport_class, "__init__") as patched: + patched.return_value = None + client = client_class(client_options=options, transport=transport_name) + patched.assert_called_once_with( + credentials=None, + credentials_file="credentials.json", + host=client.DEFAULT_ENDPOINT, + scopes=None, + client_cert_source_for_mtls=None, + quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, + always_use_jwt_access=True, + ) -def test_list_operations_rest( - transport: str = "rest", request_type=operations_pb2.ListOperationsRequest -): - client = _get_operations_client() +def test_list_operations_rest(): + client = _get_operations_client(is_async=False) # Mock the http request call within the method and fake a response. - with mock.patch.object(Session, "request") as req: + with mock.patch.object(_get_session_type(is_async=False), "request") as req: # Designate an appropriate value for the returned response. return_value = operations_pb2.ListOperationsResponse( next_page_token="next_page_token_value", @@ -477,10 +572,49 @@ def test_list_operations_rest( assert response.next_page_token == "next_page_token_value" +@pytest.mark.asyncio +async def test_list_operations_rest_async(): + if not GOOGLE_AUTH_AIO_INSTALLED: + pytest.skip("Skipped because google-api-core[async_rest] is not installed") + + client = _get_operations_client(is_async=True) + # Mock the http request call within the method and fake a response. + with mock.patch.object(_get_session_type(is_async=True), "request") as req: + # Designate an appropriate value for the returned response. + return_value = operations_pb2.ListOperationsResponse( + next_page_token="next_page_token_value", + ) + + # Wrap the value into a proper Response obj + response_value = mock.Mock() + response_value.status_code = 200 + json_return_value = json_format.MessageToJson(return_value) + response_value.read = mock.AsyncMock( + return_value=json_return_value.encode("UTF-8") + ) + req.return_value = response_value + response = await client.list_operations( + name="operations", filter_="my_filter", page_size=10, page_token="abc" + ) + + actual_args = req.call_args + assert actual_args.args[0] == "GET" + assert actual_args.args[1] == "https://longrunning.googleapis.com/v3/operations" + assert actual_args.kwargs["params"] == [ + ("filter", "my_filter"), + ("pageSize", 10), + ("pageToken", "abc"), + ] + + # Establish that the response is the type that we expect. + assert isinstance(response, pagers_async.ListOperationsAsyncPager) + assert response.next_page_token == "next_page_token_value" + + def test_list_operations_rest_failure(): - client = _get_operations_client(http_options=None) + client = _get_operations_client(is_async=False, http_options=None) - with mock.patch.object(Session, "request") as req: + with mock.patch.object(_get_session_type(is_async=False), "request") as req: response_value = Response() response_value.status_code = 400 mock_request = mock.MagicMock() @@ -492,13 +626,31 @@ def test_list_operations_rest_failure(): client.list_operations(name="operations") +@pytest.mark.asyncio +async def test_list_operations_rest_failure_async(): + if not GOOGLE_AUTH_AIO_INSTALLED: + pytest.skip("Skipped because google-api-core[async_rest] is not installed") + + client = _get_operations_client(is_async=True, http_options=None) + + with mock.patch.object(_get_session_type(is_async=True), "request") as req: + response_value = mock.Mock() + response_value.status_code = 400 + response_value.read = mock.AsyncMock(return_value=b"{}") + mock_request = mock.MagicMock() + mock_request.method = "GET" + mock_request.url = "https://longrunning.googleapis.com:443/v1/operations" + response_value.request = mock_request + req.return_value = response_value + with pytest.raises(core_exceptions.GoogleAPIError): + await client.list_operations(name="operations") + + def test_list_operations_rest_pager(): - client = AbstractOperationsClient( - credentials=ga_credentials.AnonymousCredentials(), - ) + client = _get_operations_client(is_async=False, http_options=None) # Mock the http request call within the method and fake a response. - with mock.patch.object(Session, "request") as req: + with mock.patch.object(_get_session_type(is_async=False), "request") as req: # TODO(kbandes): remove this mock unless there's a good reason for it. # with mock.patch.object(path_template, 'transcode') as transcode: # Set the response as a series of pages @@ -545,13 +697,80 @@ def test_list_operations_rest_pager(): assert page_.next_page_token == token -def test_get_operation_rest( - transport: str = "rest", request_type=operations_pb2.GetOperationRequest -): - client = _get_operations_client() +@pytest.mark.asyncio +async def test_list_operations_rest_pager_async(): + if not GOOGLE_AUTH_AIO_INSTALLED: + pytest.skip("Skipped because google-api-core[async_rest] is not installed") + client = _get_operations_client(is_async=True, http_options=None) + + # Mock the http request call within the method and fake a response. + with mock.patch.object(_get_session_type(is_async=True), "request") as req: + # TODO(kbandes): remove this mock unless there's a good reason for it. + # with mock.patch.object(path_template, 'transcode') as transcode: + # Set the response as a series of pages + response = ( + operations_pb2.ListOperationsResponse( + operations=[ + operations_pb2.Operation(), + operations_pb2.Operation(), + operations_pb2.Operation(), + ], + next_page_token="abc", + ), + operations_pb2.ListOperationsResponse( + operations=[], + next_page_token="def", + ), + operations_pb2.ListOperationsResponse( + operations=[operations_pb2.Operation()], + next_page_token="ghi", + ), + operations_pb2.ListOperationsResponse( + operations=[operations_pb2.Operation(), operations_pb2.Operation()], + ), + ) + # Two responses for two calls + response = response + response + + # Wrap the values into proper Response objs + response = tuple(json_format.MessageToJson(x) for x in response) + return_values = tuple(mock.Mock() for i in response) + for return_val, response_val in zip(return_values, response): + return_val.read = mock.AsyncMock(return_value=response_val.encode("UTF-8")) + return_val.status_code = 200 + req.side_effect = return_values + + pager = await client.list_operations(name="operations") + + responses = [] + async for response in pager: + responses.append(response) + + results = list(responses) + assert len(results) == 6 + assert all(isinstance(i, operations_pb2.Operation) for i in results) + pager = await client.list_operations(name="operations") + + responses = [] + async for response in pager: + responses.append(response) + + assert len(responses) == 6 + assert all(isinstance(i, operations_pb2.Operation) for i in results) + + pages = [] + + async for page in pager.pages: + pages.append(page) + for page_, token in zip(pages, ["", "", "", "abc", "def", "ghi", ""]): + assert page_.next_page_token == token + + +def test_get_operation_rest(): + client = _get_operations_client(is_async=False) # Mock the http request call within the method and fake a response. - with mock.patch.object(Session, "request") as req: + with mock.patch.object(_get_session_type(is_async=False), "request") as req: # Designate an appropriate value for the returned response. return_value = operations_pb2.Operation( name="operations/sample1", @@ -580,10 +799,46 @@ def test_get_operation_rest( assert response.done is True +@pytest.mark.asyncio +async def test_get_operation_rest_async(): + if not GOOGLE_AUTH_AIO_INSTALLED: + pytest.skip("Skipped because google-api-core[async_rest] is not installed") + client = _get_operations_client(is_async=True) + + # Mock the http request call within the method and fake a response. + with mock.patch.object(_get_session_type(is_async=True), "request") as req: + # Designate an appropriate value for the returned response. + return_value = operations_pb2.Operation( + name="operations/sample1", + done=True, + error=status_pb2.Status(code=411), + ) + + # Wrap the value into a proper Response obj + response_value = mock.Mock() + response_value.status_code = 200 + json_return_value = json_format.MessageToJson(return_value) + response_value.read = mock.AsyncMock(return_value=json_return_value) + req.return_value = response_value + response = await client.get_operation("operations/sample1") + + actual_args = req.call_args + assert actual_args.args[0] == "GET" + assert ( + actual_args.args[1] + == "https://longrunning.googleapis.com/v3/operations/sample1" + ) + + # Establish that the response is the type that we expect. + assert isinstance(response, operations_pb2.Operation) + assert response.name == "operations/sample1" + assert response.done is True + + def test_get_operation_rest_failure(): - client = _get_operations_client(http_options=None) + client = _get_operations_client(is_async=False, http_options=None) - with mock.patch.object(Session, "request") as req: + with mock.patch.object(_get_session_type(is_async=False), "request") as req: response_value = Response() response_value.status_code = 400 mock_request = mock.MagicMock() @@ -595,13 +850,30 @@ def test_get_operation_rest_failure(): client.get_operation("sample0/operations/sample1") -def test_delete_operation_rest( - transport: str = "rest", request_type=operations_pb2.DeleteOperationRequest -): - client = _get_operations_client() +@pytest.mark.asyncio +async def test_get_operation_rest_failure_async(): + if not GOOGLE_AUTH_AIO_INSTALLED: + pytest.skip("Skipped because google-api-core[async_rest] is not installed") + client = _get_operations_client(is_async=True, http_options=None) + + with mock.patch.object(_get_session_type(is_async=True), "request") as req: + response_value = mock.Mock() + response_value.status_code = 400 + response_value.read = mock.AsyncMock(return_value=b"{}") + mock_request = mock.MagicMock() + mock_request.method = "GET" + mock_request.url = "https://longrunning.googleapis.com/v1/operations/sample1" + response_value.request = mock_request + req.return_value = response_value + with pytest.raises(core_exceptions.GoogleAPIError): + await client.get_operation("sample0/operations/sample1") + + +def test_delete_operation_rest(): + client = _get_operations_client(is_async=False) # Mock the http request call within the method and fake a response. - with mock.patch.object(Session, "request") as req: + with mock.patch.object(_get_session_type(is_async=False), "request") as req: # Wrap the value into a proper Response obj response_value = Response() response_value.status_code = 200 @@ -618,10 +890,36 @@ def test_delete_operation_rest( ) +@pytest.mark.asyncio +async def test_delete_operation_rest_async(): + if not GOOGLE_AUTH_AIO_INSTALLED: + pytest.skip("Skipped because google-api-core[async_rest] is not installed") + client = _get_operations_client(is_async=True) + + # Mock the http request call within the method and fake a response. + with mock.patch.object(_get_session_type(is_async=True), "request") as req: + # Wrap the value into a proper Response obj + response_value = mock.Mock() + response_value.status_code = 200 + json_return_value = "" + response_value.read = mock.AsyncMock( + return_value=json_return_value.encode("UTF-8") + ) + req.return_value = response_value + await client.delete_operation(name="operations/sample1") + assert req.call_count == 1 + actual_args = req.call_args + assert actual_args.args[0] == "DELETE" + assert ( + actual_args.args[1] + == "https://longrunning.googleapis.com/v3/operations/sample1" + ) + + def test_delete_operation_rest_failure(): - client = _get_operations_client(http_options=None) + client = _get_operations_client(is_async=False, http_options=None) - with mock.patch.object(Session, "request") as req: + with mock.patch.object(_get_session_type(is_async=False), "request") as req: response_value = Response() response_value.status_code = 400 mock_request = mock.MagicMock() @@ -633,11 +931,30 @@ def test_delete_operation_rest_failure(): client.delete_operation(name="sample0/operations/sample1") -def test_cancel_operation_rest(transport: str = "rest"): - client = _get_operations_client() +@pytest.mark.asyncio +async def test_delete_operation_rest_failure_async(): + if not GOOGLE_AUTH_AIO_INSTALLED: + pytest.skip("Skipped because google-api-core[async_rest] is not installed") + client = _get_operations_client(is_async=True, http_options=None) + + with mock.patch.object(_get_session_type(is_async=True), "request") as req: + response_value = mock.Mock() + response_value.status_code = 400 + response_value.read = mock.AsyncMock(return_value=b"{}") + mock_request = mock.MagicMock() + mock_request.method = "DELETE" + mock_request.url = "https://longrunning.googleapis.com/v1/operations/sample1" + response_value.request = mock_request + req.return_value = response_value + with pytest.raises(core_exceptions.GoogleAPIError): + await client.delete_operation(name="sample0/operations/sample1") + + +def test_cancel_operation_rest(): + client = _get_operations_client(is_async=False) # Mock the http request call within the method and fake a response. - with mock.patch.object(Session, "request") as req: + with mock.patch.object(_get_session_type(is_async=False), "request") as req: # Wrap the value into a proper Response obj response_value = Response() response_value.status_code = 200 @@ -654,10 +971,36 @@ def test_cancel_operation_rest(transport: str = "rest"): ) +@pytest.mark.asyncio +async def test_cancel_operation_rest_async(): + if not GOOGLE_AUTH_AIO_INSTALLED: + pytest.skip("Skipped because google-api-core[async_rest] is not installed") + client = _get_operations_client(is_async=True) + + # Mock the http request call within the method and fake a response. + with mock.patch.object(_get_session_type(is_async=True), "request") as req: + # Wrap the value into a proper Response obj + response_value = mock.Mock() + response_value.status_code = 200 + json_return_value = "" + response_value.read = mock.AsyncMock( + return_value=json_return_value.encode("UTF-8") + ) + req.return_value = response_value + await client.cancel_operation(name="operations/sample1") + assert req.call_count == 1 + actual_args = req.call_args + assert actual_args.args[0] == "POST" + assert ( + actual_args.args[1] + == "https://longrunning.googleapis.com/v3/operations/sample1:cancel" + ) + + def test_cancel_operation_rest_failure(): - client = _get_operations_client(http_options=None) + client = _get_operations_client(is_async=False, http_options=None) - with mock.patch.object(Session, "request") as req: + with mock.patch.object(_get_session_type(is_async=False), "request") as req: response_value = Response() response_value.status_code = 400 mock_request = mock.MagicMock() @@ -671,52 +1014,79 @@ def test_cancel_operation_rest_failure(): client.cancel_operation(name="sample0/operations/sample1") -def test_credentials_transport_error(): +@pytest.mark.asyncio +async def test_cancel_operation_rest_failure_async(): + if not GOOGLE_AUTH_AIO_INSTALLED: + pytest.skip("Skipped because google-api-core[async_rest] is not installed") + client = _get_operations_client(is_async=True, http_options=None) + + with mock.patch.object(_get_session_type(is_async=True), "request") as req: + response_value = mock.Mock() + response_value.status_code = 400 + response_value.read = mock.AsyncMock(return_value=b"{}") + mock_request = mock.MagicMock() + mock_request.method = "POST" + mock_request.url = ( + "https://longrunning.googleapis.com/v1/operations/sample1:cancel" + ) + response_value.request = mock_request + req.return_value = response_value + with pytest.raises(core_exceptions.GoogleAPIError): + await client.cancel_operation(name="sample0/operations/sample1") + + +@pytest.mark.parametrize( + "client_class,transport_class,credentials", + PYPARAM_CLIENT_TRANSPORT_CREDENTIALS, +) +def test_credentials_transport_error(client_class, transport_class, credentials): + # It is an error to provide credentials and a transport instance. - transport = transports.OperationsRestTransport( - credentials=ga_credentials.AnonymousCredentials(), - ) + transport = transport_class(credentials=credentials) with pytest.raises(ValueError): - AbstractOperationsClient( + client_class( credentials=ga_credentials.AnonymousCredentials(), transport=transport, ) # It is an error to provide a credentials file and a transport instance. - transport = transports.OperationsRestTransport( - credentials=ga_credentials.AnonymousCredentials(), - ) + transport = transport_class(credentials=credentials) with pytest.raises(ValueError): - AbstractOperationsClient( + client_class( client_options={"credentials_file": "credentials.json"}, transport=transport, ) # It is an error to provide scopes and a transport instance. - transport = transports.OperationsRestTransport( - credentials=ga_credentials.AnonymousCredentials(), - ) + transport = transport_class(credentials=credentials) with pytest.raises(ValueError): - AbstractOperationsClient( + client_class( client_options={"scopes": ["1", "2"]}, transport=transport, ) -def test_transport_instance(): +@pytest.mark.parametrize( + "client_class,transport_class,credentials", + PYPARAM_CLIENT_TRANSPORT_CREDENTIALS, +) +def test_transport_instance(client_class, transport_class, credentials): # A client may be instantiated with a custom transport instance. - transport = transports.OperationsRestTransport( - credentials=ga_credentials.AnonymousCredentials(), + transport = transport_class( + credentials=credentials, ) - client = AbstractOperationsClient(transport=transport) + client = client_class(transport=transport) assert client.transport is transport -@pytest.mark.parametrize("transport_class", [transports.OperationsRestTransport]) -def test_transport_adc(transport_class): +@pytest.mark.parametrize( + "client_class,transport_class,credentials", + PYPARAM_CLIENT_TRANSPORT_CREDENTIALS, +) +def test_transport_adc(client_class, transport_class, credentials): # Test default credentials are used if not provided. with mock.patch.object(google.auth, "default") as adc: - adc.return_value = (ga_credentials.AnonymousCredentials(), None) + adc.return_value = (credentials, None) transport_class() adc.assert_called_once() @@ -788,32 +1158,59 @@ def test_operations_base_transport_with_adc(): adc.assert_called_once() -def test_operations_auth_adc(): +@pytest.mark.parametrize( + "client_class", + PYPARAM_CLIENT, +) +def test_operations_auth_adc(client_class): # If no credentials are provided, we should use ADC credentials. with mock.patch.object(google.auth, "default", autospec=True) as adc: adc.return_value = (ga_credentials.AnonymousCredentials(), None) - AbstractOperationsClient() - adc.assert_called_once_with( - scopes=None, - default_scopes=(), - quota_project_id=None, - ) + if "async" in str(client_class).lower(): + # TODO(): Add support for adc to async REST transport. + # NOTE: Ideally, the logic for adc shouldn't be called if transport + # is set to async REST. If the user does not configure credentials + # of type `google.auth.aio.credentials.Credentials`, + # we should raise an exception to avoid the adc workflow. + with pytest.raises(google.auth.exceptions.InvalidType): + client_class() + else: + client_class() + adc.assert_called_once_with( + scopes=None, + default_scopes=(), + quota_project_id=None, + ) -def test_operations_http_transport_client_cert_source_for_mtls(): + +# TODO(https://github.com/googleapis/python-api-core/issues/705): Add +# testing for `transports.AsyncOperationsRestTransport` once MTLS is supported +# in `google.auth.aio.transport`. +@pytest.mark.parametrize( + "transport_class", + [ + transports.OperationsRestTransport, + ], +) +def test_operations_http_transport_client_cert_source_for_mtls(transport_class): cred = ga_credentials.AnonymousCredentials() with mock.patch( "google.auth.transport.requests.AuthorizedSession.configure_mtls_channel" ) as mock_configure_mtls_channel: - transports.OperationsRestTransport( + transport_class( credentials=cred, client_cert_source_for_mtls=client_cert_source_callback ) mock_configure_mtls_channel.assert_called_once_with(client_cert_source_callback) -def test_operations_host_no_port(): - client = AbstractOperationsClient( - credentials=ga_credentials.AnonymousCredentials(), +@pytest.mark.parametrize( + "client_class,transport_class,credentials", + PYPARAM_CLIENT_TRANSPORT_CREDENTIALS, +) +def test_operations_host_no_port(client_class, transport_class, credentials): + client = client_class( + credentials=credentials, client_options=client_options.ClientOptions( api_endpoint="longrunning.googleapis.com" ), @@ -821,9 +1218,13 @@ def test_operations_host_no_port(): assert client.transport._host == "https://longrunning.googleapis.com" -def test_operations_host_with_port(): - client = AbstractOperationsClient( - credentials=ga_credentials.AnonymousCredentials(), +@pytest.mark.parametrize( + "client_class,transport_class,credentials", + PYPARAM_CLIENT_TRANSPORT_CREDENTIALS, +) +def test_operations_host_with_port(client_class, transport_class, credentials): + client = client_class( + credentials=credentials, client_options=client_options.ClientOptions( api_endpoint="longrunning.googleapis.com:8000" ), @@ -831,127 +1232,165 @@ def test_operations_host_with_port(): assert client.transport._host == "https://longrunning.googleapis.com:8000" -def test_common_billing_account_path(): +@pytest.mark.parametrize( + "client_class", + PYPARAM_CLIENT, +) +def test_common_billing_account_path(client_class): billing_account = "squid" expected = "billingAccounts/{billing_account}".format( billing_account=billing_account, ) - actual = AbstractOperationsClient.common_billing_account_path(billing_account) + actual = client_class.common_billing_account_path(billing_account) assert expected == actual -def test_parse_common_billing_account_path(): +@pytest.mark.parametrize( + "client_class", + PYPARAM_CLIENT, +) +def test_parse_common_billing_account_path(client_class): expected = { "billing_account": "clam", } - path = AbstractOperationsClient.common_billing_account_path(**expected) + path = client_class.common_billing_account_path(**expected) # Check that the path construction is reversible. - actual = AbstractOperationsClient.parse_common_billing_account_path(path) + actual = client_class.parse_common_billing_account_path(path) assert expected == actual -def test_common_folder_path(): +@pytest.mark.parametrize( + "client_class", + PYPARAM_CLIENT, +) +def test_common_folder_path(client_class): folder = "whelk" expected = "folders/{folder}".format( folder=folder, ) - actual = AbstractOperationsClient.common_folder_path(folder) + actual = client_class.common_folder_path(folder) assert expected == actual -def test_parse_common_folder_path(): +@pytest.mark.parametrize( + "client_class", + PYPARAM_CLIENT, +) +def test_parse_common_folder_path(client_class): expected = { "folder": "octopus", } - path = AbstractOperationsClient.common_folder_path(**expected) + path = client_class.common_folder_path(**expected) # Check that the path construction is reversible. - actual = AbstractOperationsClient.parse_common_folder_path(path) + actual = client_class.parse_common_folder_path(path) assert expected == actual -def test_common_organization_path(): +@pytest.mark.parametrize( + "client_class", + PYPARAM_CLIENT, +) +def test_common_organization_path(client_class): organization = "oyster" expected = "organizations/{organization}".format( organization=organization, ) - actual = AbstractOperationsClient.common_organization_path(organization) + actual = client_class.common_organization_path(organization) assert expected == actual -def test_parse_common_organization_path(): +@pytest.mark.parametrize( + "client_class", + PYPARAM_CLIENT, +) +def test_parse_common_organization_path(client_class): expected = { "organization": "nudibranch", } - path = AbstractOperationsClient.common_organization_path(**expected) + path = client_class.common_organization_path(**expected) # Check that the path construction is reversible. - actual = AbstractOperationsClient.parse_common_organization_path(path) + actual = client_class.parse_common_organization_path(path) assert expected == actual -def test_common_project_path(): +@pytest.mark.parametrize( + "client_class", + PYPARAM_CLIENT, +) +def test_common_project_path(client_class): project = "cuttlefish" expected = "projects/{project}".format( project=project, ) - actual = AbstractOperationsClient.common_project_path(project) + actual = client_class.common_project_path(project) assert expected == actual -def test_parse_common_project_path(): +@pytest.mark.parametrize( + "client_class", + PYPARAM_CLIENT, +) +def test_parse_common_project_path(client_class): expected = { "project": "mussel", } - path = AbstractOperationsClient.common_project_path(**expected) + path = client_class.common_project_path(**expected) # Check that the path construction is reversible. - actual = AbstractOperationsClient.parse_common_project_path(path) + actual = client_class.parse_common_project_path(path) assert expected == actual -def test_common_location_path(): +@pytest.mark.parametrize( + "client_class", + PYPARAM_CLIENT, +) +def test_common_location_path(client_class): project = "winkle" location = "nautilus" expected = "projects/{project}/locations/{location}".format( project=project, location=location, ) - actual = AbstractOperationsClient.common_location_path(project, location) + actual = client_class.common_location_path(project, location) assert expected == actual -def test_parse_common_location_path(): +@pytest.mark.parametrize( + "client_class", + PYPARAM_CLIENT, +) +def test_parse_common_location_path(client_class): expected = { "project": "scallop", "location": "abalone", } - path = AbstractOperationsClient.common_location_path(**expected) + path = client_class.common_location_path(**expected) # Check that the path construction is reversible. - actual = AbstractOperationsClient.parse_common_location_path(path) + actual = client_class.parse_common_location_path(path) assert expected == actual -def test_client_withDEFAULT_CLIENT_INFO(): +@pytest.mark.parametrize( + "client_class,transport_class,credentials", + PYPARAM_CLIENT_TRANSPORT_CREDENTIALS, +) +def test_client_withDEFAULT_CLIENT_INFO(client_class, transport_class, credentials): client_info = gapic_v1.client_info.ClientInfo() - - with mock.patch.object( - transports.OperationsTransport, "_prep_wrapped_messages" - ) as prep: - AbstractOperationsClient( - credentials=ga_credentials.AnonymousCredentials(), + with mock.patch.object(transport_class, "_prep_wrapped_messages") as prep: + client_class( + credentials=credentials, client_info=client_info, ) prep.assert_called_once_with(client_info) - with mock.patch.object( - transports.OperationsTransport, "_prep_wrapped_messages" - ) as prep: - transport_class = AbstractOperationsClient.get_transport_class() + with mock.patch.object(transport_class, "_prep_wrapped_messages") as prep: transport_class( - credentials=ga_credentials.AnonymousCredentials(), + credentials=credentials, client_info=client_info, ) prep.assert_called_once_with(client_info) From b2baf4712595cea4bc9a9bcd57f3ffda6534c8d1 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 9 Oct 2024 09:40:10 -0400 Subject: [PATCH 033/101] chore(main): release 2.21.0 (#704) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 12 ++++++++++++ google/api_core/version.py | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1552b532..06ad44d22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ [1]: https://pypi.org/project/google-api-core/#history +## [2.21.0](https://github.com/googleapis/python-api-core/compare/v2.20.0...v2.21.0) (2024-10-07) + + +### Features + +* Add support for asynchronous long running operations ([#724](https://github.com/googleapis/python-api-core/issues/724)) ([aaed69b](https://github.com/googleapis/python-api-core/commit/aaed69b6f1d694cd7e561e2aa03fdd8d6cfb369a)) + + +### Bug Fixes + +* Set chunk size for async stream content ([#702](https://github.com/googleapis/python-api-core/issues/702)) ([45b8a6d](https://github.com/googleapis/python-api-core/commit/45b8a6db5a5c75acdd8be896d0152f11608c7e51)) + ## [2.20.0](https://github.com/googleapis/python-api-core/compare/v2.19.2...v2.20.0) (2024-09-18) diff --git a/google/api_core/version.py b/google/api_core/version.py index 9fea4fece..563b0e160 100644 --- a/google/api_core/version.py +++ b/google/api_core/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "2.20.0" +__version__ = "2.21.0" From 8c533819b7e212aa2f1d695a7ce08629f4fb2daf Mon Sep 17 00:00:00 2001 From: Steve Kowalik Date: Thu, 10 Oct 2024 02:56:00 +1100 Subject: [PATCH 034/101] fix: Switch to unittest.mock from mock (#713) * test: Switch to unittest.mock from mock Now that the minimum supported version of Python is 3.7, we can stop using the external mock requirement, and import it from unittest. I have also attempted to keep imports ordered. Fixes #377 * test: Fallback to external mock for AsyncMock AsyncMock is not included in unittest.mock under Python 3.7, so we must fallback to the external mock requirement for that Python version. Only install it for that version. Keep this as a separate commit so it can be reverted when 3.7 isn't supported anymore. * lint * clean up to satisfy mypy * lint * fix build --------- Co-authored-by: Anthonios Partheniou --- noxfile.py | 4 ++-- tests/asyncio/future/test_async_future.py | 2 +- tests/asyncio/gapic/test_method_async.py | 6 +++++- .../operations_v1/test_operations_async_client.py | 3 ++- tests/asyncio/retry/test_retry_streaming_async.py | 9 +++++++-- tests/asyncio/retry/test_retry_unary_async.py | 6 +++++- tests/asyncio/test_grpc_helpers_async.py | 6 +++++- tests/asyncio/test_operation_async.py | 7 ++++++- tests/asyncio/test_page_iterator_async.py | 6 +++++- tests/asyncio/test_rest_streaming_async.py | 11 ++++++++--- tests/unit/future/test__helpers.py | 2 +- tests/unit/future/test_polling.py | 2 +- tests/unit/gapic/test_method.py | 2 +- .../unit/operations_v1/test_operations_rest_client.py | 7 ++++++- tests/unit/retry/test_retry_base.py | 2 +- tests/unit/retry/test_retry_streaming.py | 7 ++++++- tests/unit/retry/test_retry_unary.py | 8 ++++++-- tests/unit/test_bidi.py | 7 ++++++- tests/unit/test_exceptions.py | 2 +- tests/unit/test_extended_operation.py | 2 +- tests/unit/test_grpc_helpers.py | 3 ++- tests/unit/test_operation.py | 3 ++- tests/unit/test_page_iterator.py | 2 +- tests/unit/test_path_template.py | 2 +- tests/unit/test_timeout.py | 3 +-- 25 files changed, 83 insertions(+), 31 deletions(-) diff --git a/noxfile.py b/noxfile.py index 144e3e210..3dbd27df0 100644 --- a/noxfile.py +++ b/noxfile.py @@ -124,7 +124,7 @@ def default(session, install_grpc=True, prerelease=False, install_async_rest=Fal session.install( "dataclasses", - "mock", + "mock; python_version=='3.7'", "pytest", "pytest-cov", "pytest-xdist", @@ -280,8 +280,8 @@ def mypy(session): "types-setuptools", "types-requests", "types-protobuf", - "types-mock", "types-dataclasses", + "types-mock; python_version=='3.7'", ) session.run("mypy", "google", "tests") diff --git a/tests/asyncio/future/test_async_future.py b/tests/asyncio/future/test_async_future.py index 0cfe67734..659f41cf9 100644 --- a/tests/asyncio/future/test_async_future.py +++ b/tests/asyncio/future/test_async_future.py @@ -13,8 +13,8 @@ # limitations under the License. import asyncio +from unittest import mock -import mock import pytest from google.api_core import exceptions diff --git a/tests/asyncio/gapic/test_method_async.py b/tests/asyncio/gapic/test_method_async.py index f64157b44..73f67b8db 100644 --- a/tests/asyncio/gapic/test_method_async.py +++ b/tests/asyncio/gapic/test_method_async.py @@ -14,7 +14,11 @@ import datetime -import mock +try: + from unittest import mock + from unittest.mock import AsyncMock # pragma: NO COVER # noqa: F401 +except ImportError: # pragma: NO COVER + import mock # type: ignore import pytest try: diff --git a/tests/asyncio/operations_v1/test_operations_async_client.py b/tests/asyncio/operations_v1/test_operations_async_client.py index 19ac9b566..e5b20dcd0 100644 --- a/tests/asyncio/operations_v1/test_operations_async_client.py +++ b/tests/asyncio/operations_v1/test_operations_async_client.py @@ -12,7 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -import mock +from unittest import mock + import pytest try: diff --git a/tests/asyncio/retry/test_retry_streaming_async.py b/tests/asyncio/retry/test_retry_streaming_async.py index 28ae6ff17..ffdf314a1 100644 --- a/tests/asyncio/retry/test_retry_streaming_async.py +++ b/tests/asyncio/retry/test_retry_streaming_async.py @@ -12,11 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. +import asyncio import datetime import re -import asyncio -import mock +try: + from unittest import mock + from unittest.mock import AsyncMock # pragma: NO COVER # noqa: F401 +except ImportError: # pragma: NO COVER + import mock # type: ignore + import pytest from google.api_core import exceptions diff --git a/tests/asyncio/retry/test_retry_unary_async.py b/tests/asyncio/retry/test_retry_unary_async.py index fc2f572b4..dc64299f9 100644 --- a/tests/asyncio/retry/test_retry_unary_async.py +++ b/tests/asyncio/retry/test_retry_unary_async.py @@ -15,7 +15,11 @@ import datetime import re -import mock +try: + from unittest import mock + from unittest.mock import AsyncMock # pragma: NO COVER # noqa: F401 +except ImportError: # pragma: NO COVER + import mock # type: ignore import pytest from google.api_core import exceptions diff --git a/tests/asyncio/test_grpc_helpers_async.py b/tests/asyncio/test_grpc_helpers_async.py index 1a408ccde..d8f20ae44 100644 --- a/tests/asyncio/test_grpc_helpers_async.py +++ b/tests/asyncio/test_grpc_helpers_async.py @@ -12,7 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -import mock +try: + from unittest import mock + from unittest.mock import AsyncMock # pragma: NO COVER # noqa: F401 +except ImportError: # pragma: NO COVER + import mock # type: ignore import pytest # noqa: I202 try: diff --git a/tests/asyncio/test_operation_async.py b/tests/asyncio/test_operation_async.py index 127ba6343..939be0947 100644 --- a/tests/asyncio/test_operation_async.py +++ b/tests/asyncio/test_operation_async.py @@ -13,9 +13,14 @@ # limitations under the License. -import mock import pytest +try: + from unittest import mock + from unittest.mock import AsyncMock # pragma: NO COVER # noqa: F401 +except ImportError: # pragma: NO COVER + import mock # type: ignore + try: import grpc # noqa: F401 except ImportError: # pragma: NO COVER diff --git a/tests/asyncio/test_page_iterator_async.py b/tests/asyncio/test_page_iterator_async.py index 75f9e1cf4..d8bba9345 100644 --- a/tests/asyncio/test_page_iterator_async.py +++ b/tests/asyncio/test_page_iterator_async.py @@ -14,7 +14,11 @@ import inspect -import mock +try: + from unittest import mock + from unittest.mock import AsyncMock # pragma: NO COVER # noqa: F401 +except ImportError: # pragma: NO COVER + import mock # type: ignore import pytest from google.api_core import page_iterator_async diff --git a/tests/asyncio/test_rest_streaming_async.py b/tests/asyncio/test_rest_streaming_async.py index da5b1c8d6..c9caa2b15 100644 --- a/tests/asyncio/test_rest_streaming_async.py +++ b/tests/asyncio/test_rest_streaming_async.py @@ -15,15 +15,20 @@ # TODO: set random.seed explicitly in each test function. # See related issue: https://github.com/googleapis/python-api-core/issues/689. -import pytest # noqa: I202 -import mock - import datetime import logging import random import time from typing import List, AsyncIterator +try: + from unittest import mock + from unittest.mock import AsyncMock # pragma: NO COVER # noqa: F401 +except ImportError: # pragma: NO COVER + import mock # type: ignore + +import pytest # noqa: I202 + import proto try: diff --git a/tests/unit/future/test__helpers.py b/tests/unit/future/test__helpers.py index 98afc599f..a37efdd40 100644 --- a/tests/unit/future/test__helpers.py +++ b/tests/unit/future/test__helpers.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -import mock +from unittest import mock from google.api_core.future import _helpers diff --git a/tests/unit/future/test_polling.py b/tests/unit/future/test_polling.py index f5d9b4f10..2f66f2304 100644 --- a/tests/unit/future/test_polling.py +++ b/tests/unit/future/test_polling.py @@ -15,8 +15,8 @@ import concurrent.futures import threading import time +from unittest import mock -import mock import pytest from google.api_core import exceptions, retry diff --git a/tests/unit/gapic/test_method.py b/tests/unit/gapic/test_method.py index d966f478f..87aa63905 100644 --- a/tests/unit/gapic/test_method.py +++ b/tests/unit/gapic/test_method.py @@ -13,8 +13,8 @@ # limitations under the License. import datetime +from unittest import mock -import mock import pytest try: diff --git a/tests/unit/operations_v1/test_operations_rest_client.py b/tests/unit/operations_v1/test_operations_rest_client.py index 644cf2663..d1f6e0eba 100644 --- a/tests/unit/operations_v1/test_operations_rest_client.py +++ b/tests/unit/operations_v1/test_operations_rest_client.py @@ -15,7 +15,12 @@ # import os -import mock +try: + from unittest import mock + from unittest.mock import AsyncMock # pragma: NO COVER # noqa: F401 +except ImportError: # pragma: NO COVER + import mock # type: ignore + import pytest from typing import Any, List diff --git a/tests/unit/retry/test_retry_base.py b/tests/unit/retry/test_retry_base.py index a0c6776bf..212c4293b 100644 --- a/tests/unit/retry/test_retry_base.py +++ b/tests/unit/retry/test_retry_base.py @@ -14,8 +14,8 @@ import itertools import re +from unittest import mock -import mock import pytest import requests.exceptions diff --git a/tests/unit/retry/test_retry_streaming.py b/tests/unit/retry/test_retry_streaming.py index 01f35327b..0bc85d926 100644 --- a/tests/unit/retry/test_retry_streaming.py +++ b/tests/unit/retry/test_retry_streaming.py @@ -14,7 +14,12 @@ import re -import mock +try: + from unittest import mock + from unittest.mock import AsyncMock # pragma: NO COVER # noqa: F401 +except ImportError: # pragma: NO COVER + import mock # type: ignore + import pytest from google.api_core import exceptions diff --git a/tests/unit/retry/test_retry_unary.py b/tests/unit/retry/test_retry_unary.py index 7dcd8dd6a..6851fbe48 100644 --- a/tests/unit/retry/test_retry_unary.py +++ b/tests/unit/retry/test_retry_unary.py @@ -13,10 +13,14 @@ # limitations under the License. import datetime +import pytest import re -import mock -import pytest +try: + from unittest import mock + from unittest.mock import AsyncMock # pragma: NO COVER # noqa: F401 +except ImportError: # pragma: NO COVER + import mock # type: ignore from google.api_core import exceptions from google.api_core import retry diff --git a/tests/unit/test_bidi.py b/tests/unit/test_bidi.py index 84ac9dc52..c196a6821 100644 --- a/tests/unit/test_bidi.py +++ b/tests/unit/test_bidi.py @@ -17,7 +17,12 @@ import queue import threading -import mock +try: + from unittest import mock + from unittest.mock import AsyncMock # pragma: NO COVER # noqa: F401 +except ImportError: # pragma: NO COVER + import mock # type: ignore + import pytest try: diff --git a/tests/unit/test_exceptions.py b/tests/unit/test_exceptions.py index 07a368175..e3f8f909a 100644 --- a/tests/unit/test_exceptions.py +++ b/tests/unit/test_exceptions.py @@ -14,8 +14,8 @@ import http.client import json +from unittest import mock -import mock import pytest import requests diff --git a/tests/unit/test_extended_operation.py b/tests/unit/test_extended_operation.py index 53af52047..ab5506624 100644 --- a/tests/unit/test_extended_operation.py +++ b/tests/unit/test_extended_operation.py @@ -15,8 +15,8 @@ import dataclasses import enum import typing +from unittest import mock -import mock import pytest from google.api_core import exceptions diff --git a/tests/unit/test_grpc_helpers.py b/tests/unit/test_grpc_helpers.py index 59442d437..8de9d8c0b 100644 --- a/tests/unit/test_grpc_helpers.py +++ b/tests/unit/test_grpc_helpers.py @@ -12,7 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -import mock +from unittest import mock + import pytest try: diff --git a/tests/unit/test_operation.py b/tests/unit/test_operation.py index f029866c6..806807201 100644 --- a/tests/unit/test_operation.py +++ b/tests/unit/test_operation.py @@ -13,7 +13,8 @@ # limitations under the License. -import mock +from unittest import mock + import pytest try: diff --git a/tests/unit/test_page_iterator.py b/tests/unit/test_page_iterator.py index cf43aedfb..560722c5d 100644 --- a/tests/unit/test_page_iterator.py +++ b/tests/unit/test_page_iterator.py @@ -14,8 +14,8 @@ import math import types +from unittest import mock -import mock import pytest from google.api_core import page_iterator diff --git a/tests/unit/test_path_template.py b/tests/unit/test_path_template.py index 808b36f34..c34dd0f32 100644 --- a/tests/unit/test_path_template.py +++ b/tests/unit/test_path_template.py @@ -13,8 +13,8 @@ # limitations under the License. from __future__ import unicode_literals +from unittest import mock -import mock import pytest from google.api import auth_pb2 diff --git a/tests/unit/test_timeout.py b/tests/unit/test_timeout.py index 0bcf07f07..60a2e65d3 100644 --- a/tests/unit/test_timeout.py +++ b/tests/unit/test_timeout.py @@ -14,8 +14,7 @@ import datetime import itertools - -import mock +from unittest import mock from google.api_core import timeout as timeouts From 46b3d3abaa1bae28e9d788d7c3006224cd6f74d5 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Tue, 15 Oct 2024 12:49:55 -0400 Subject: [PATCH 035/101] feat: add support for python 3.13 (#696) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add support for python 3.13 * Add constraints file * Avoid Python3.13 warning coroutine method 'aclose' was never awaited * remove allow-prereleases: true * exclude grpcio 1.67.0rc1 * add comment * remove empty line * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * Update comment Co-authored-by: ohmayr --------- Co-authored-by: Owl Bot Co-authored-by: ohmayr --- .github/workflows/unittest.yml | 1 + CONTRIBUTING.rst | 6 ++++-- noxfile.py | 6 ++++-- setup.py | 1 + testing/constraints-3.13.txt | 0 tests/asyncio/retry/test_retry_streaming_async.py | 5 +++++ tests/asyncio/test_page_iterator_async.py | 2 ++ 7 files changed, 17 insertions(+), 4 deletions(-) create mode 100644 testing/constraints-3.13.txt diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 2d1193d69..a82859e16 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -19,6 +19,7 @@ jobs: - "3.10" - "3.11" - "3.12" + - "3.13" exclude: - option: "_wo_grpc" python: 3.7 diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 8d1475cee..1a1f608b6 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -21,7 +21,7 @@ In order to add a feature: documentation. - The feature must work fully on the following CPython versions: - 3.7, 3.8, 3.9, 3.10, 3.11 and 3.12 on both UNIX and Windows. + 3.7, 3.8, 3.9, 3.10, 3.11, 3.12 and 3.13 on both UNIX and Windows. - The feature must not add unnecessary dependencies (where "unnecessary" is of course subjective, but new dependencies should @@ -71,7 +71,7 @@ We use `nox `__ to instrument our tests. - To run a single unit test:: - $ nox -s unit-3.12 -- -k + $ nox -s unit-3.13 -- -k .. note:: @@ -203,6 +203,7 @@ We support: - `Python 3.10`_ - `Python 3.11`_ - `Python 3.12`_ +- `Python 3.13`_ .. _Python 3.7: https://docs.python.org/3.7/ .. _Python 3.8: https://docs.python.org/3.8/ @@ -210,6 +211,7 @@ We support: .. _Python 3.10: https://docs.python.org/3.10/ .. _Python 3.11: https://docs.python.org/3.11/ .. _Python 3.12: https://docs.python.org/3.12/ +.. _Python 3.13: https://docs.python.org/3.13/ Supported versions can be found in our ``noxfile.py`` `config`_. diff --git a/noxfile.py b/noxfile.py index 3dbd27df0..ada6f330f 100644 --- a/noxfile.py +++ b/noxfile.py @@ -28,7 +28,7 @@ # Black and flake8 clash on the syntax for ignoring flake8's F401 in this file. BLACK_EXCLUDES = ["--exclude", "^/google/api_core/operations_v1/__init__.py"] -PYTHON_VERSIONS = ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] +PYTHON_VERSIONS = ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] DEFAULT_PYTHON_VERSION = "3.10" CURRENT_DIRECTORY = pathlib.Path(__file__).parent.absolute() @@ -95,7 +95,8 @@ def install_prerelease_dependencies(session, constraints_path): prerel_deps = [ "google-auth", "googleapis-common-protos", - "grpcio", + # Exclude grpcio!=1.67.0rc1 which does not support python 3.13 + "grpcio!=1.67.0rc1", "grpcio-status", "proto-plus", "protobuf", @@ -132,6 +133,7 @@ def default(session, install_grpc=True, prerelease=False, install_async_rest=Fal install_extras = [] if install_grpc: + # Note: The extra is called `grpc` and not `grpcio`. install_extras.append("grpc") constraints_dir = str(CURRENT_DIRECTORY / "testing") diff --git a/setup.py b/setup.py index d3c2a2b42..a15df5609 100644 --- a/setup.py +++ b/setup.py @@ -93,6 +93,7 @@ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Operating System :: OS Independent", "Topic :: Internet", ], diff --git a/testing/constraints-3.13.txt b/testing/constraints-3.13.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/asyncio/retry/test_retry_streaming_async.py b/tests/asyncio/retry/test_retry_streaming_async.py index ffdf314a1..d0fd799c8 100644 --- a/tests/asyncio/retry/test_retry_streaming_async.py +++ b/tests/asyncio/retry/test_retry_streaming_async.py @@ -139,6 +139,7 @@ async def test___call___generator_retry(self, sleep): unpacked = [await generator.__anext__() for i in range(10)] assert unpacked == [0, 1, 2, 0, 1, 2, 0, 1, 2, 0] assert on_error.call_count == 3 + await generator.aclose() @mock.patch("random.uniform", autospec=True, side_effect=lambda m, n: n) @mock.patch("asyncio.sleep", autospec=True) @@ -246,6 +247,7 @@ async def _mock_send_gen(): recv = await generator.asend(msg) out_messages.append(recv) assert in_messages == out_messages + await generator.aclose() @mock.patch("asyncio.sleep", autospec=True) @pytest.mark.asyncio @@ -263,6 +265,7 @@ async def test___call___generator_send_retry(self, sleep): with pytest.raises(TypeError) as exc_info: await generator.asend("cannot send to fresh generator") assert exc_info.match("can't send non-None value") + await generator.aclose() # error thrown on 3 # generator should contain 0, 1, 2 looping @@ -271,6 +274,7 @@ async def test___call___generator_send_retry(self, sleep): unpacked = [await generator.asend(i) for i in range(10)] assert unpacked == [1, 2, 0, 1, 2, 0, 1, 2, 0, 1] assert on_error.call_count == 3 + await generator.aclose() @mock.patch("asyncio.sleep", autospec=True) @pytest.mark.asyncio @@ -382,6 +386,7 @@ async def wrapper(): assert await retryable.asend("test") == 1 assert await retryable.asend("test2") == 2 assert await retryable.asend("test3") == 3 + await retryable.aclose() @pytest.mark.parametrize("awaitable_wrapped", [True, False]) @mock.patch("asyncio.sleep", autospec=True) diff --git a/tests/asyncio/test_page_iterator_async.py b/tests/asyncio/test_page_iterator_async.py index d8bba9345..63e26d02e 100644 --- a/tests/asyncio/test_page_iterator_async.py +++ b/tests/asyncio/test_page_iterator_async.py @@ -110,6 +110,7 @@ async def test__page_aiter_increment(self): await page_aiter.__anext__() assert iterator.num_results == 1 + await page_aiter.aclose() @pytest.mark.asyncio async def test__page_aiter_no_increment(self): @@ -122,6 +123,7 @@ async def test__page_aiter_no_increment(self): # results should still be 0 after fetching a page. assert iterator.num_results == 0 + await page_aiter.aclose() @pytest.mark.asyncio async def test__items_aiter(self): From b91ed19210148dfa49ec790c4dd5f4a7bff80954 Mon Sep 17 00:00:00 2001 From: Rin Arakaki Date: Sun, 20 Oct 2024 02:59:31 +0900 Subject: [PATCH 036/101] fix: add type hints to ClientOptions (#735) * Update * Update client_options.py * Update * fix build --------- Co-authored-by: Anthonios Partheniou --- google/api_core/client_options.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/google/api_core/client_options.py b/google/api_core/client_options.py index e93f95863..e3bddfeb2 100644 --- a/google/api_core/client_options.py +++ b/google/api_core/client_options.py @@ -48,6 +48,8 @@ def get_client_cert(): """ +from typing import Callable, Mapping, Optional, Sequence, Tuple + class ClientOptions(object): """Client Options used to set options on clients. @@ -55,11 +57,11 @@ class ClientOptions(object): Args: api_endpoint (Optional[str]): The desired API endpoint, e.g., compute.googleapis.com - client_cert_source (Optional[Callable[[], (bytes, bytes)]]): A callback + client_cert_source (Optional[Callable[[], Tuple[bytes, bytes]]]): A callback which returns client certificate bytes and private key bytes both in PEM format. ``client_cert_source`` and ``client_encrypted_cert_source`` are mutually exclusive. - client_encrypted_cert_source (Optional[Callable[[], (str, str, bytes)]]): + client_encrypted_cert_source (Optional[Callable[[], Tuple[str, str, bytes]]]): A callback which returns client certificate file path, encrypted private key file path, and the passphrase bytes.``client_cert_source`` and ``client_encrypted_cert_source`` are mutually exclusive. @@ -88,15 +90,17 @@ class ClientOptions(object): def __init__( self, - api_endpoint=None, - client_cert_source=None, - client_encrypted_cert_source=None, - quota_project_id=None, - credentials_file=None, - scopes=None, - api_key=None, - api_audience=None, - universe_domain=None, + api_endpoint: Optional[str] = None, + client_cert_source: Optional[Callable[[], Tuple[bytes, bytes]]] = None, + client_encrypted_cert_source: Optional[ + Callable[[], Tuple[str, str, bytes]] + ] = None, + quota_project_id: Optional[str] = None, + credentials_file: Optional[str] = None, + scopes: Optional[Sequence[str]] = None, + api_key: Optional[str] = None, + api_audience: Optional[str] = None, + universe_domain: Optional[str] = None, ): if client_cert_source and client_encrypted_cert_source: raise ValueError( @@ -114,11 +118,11 @@ def __init__( self.api_audience = api_audience self.universe_domain = universe_domain - def __repr__(self): + def __repr__(self) -> str: return "ClientOptions: " + repr(self.__dict__) -def from_dict(options): +def from_dict(options: Mapping[str, object]) -> ClientOptions: """Construct a client options object from a mapping object. Args: From 3c698679e544df571ed5e181db29b2d55eafbe2b Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Mon, 21 Oct 2024 10:46:30 -0400 Subject: [PATCH 037/101] chore: move retry async check to wrap time (#668) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: move retry async check to wrap time * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * added no cover mark --------- Co-authored-by: Daniel Sanche Co-authored-by: Owl Bot Co-authored-by: Daniel Sanche --- google/api_core/retry/retry_unary.py | 7 +++---- tests/unit/retry/test_retry_unary.py | 16 +++++++++++----- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/google/api_core/retry/retry_unary.py b/google/api_core/retry/retry_unary.py index ab1b4030b..090431333 100644 --- a/google/api_core/retry/retry_unary.py +++ b/google/api_core/retry/retry_unary.py @@ -141,10 +141,7 @@ def retry_target( for sleep in sleep_generator: try: - result = target() - if inspect.isawaitable(result): - warnings.warn(_ASYNC_RETRY_WARNING) - return result + return target() # pylint: disable=broad-except # This function explicitly must deal with broad exceptions. @@ -280,6 +277,8 @@ def __call__( Callable: A callable that will invoke ``func`` with retry behavior. """ + if inspect.iscoroutinefunction(func): + warnings.warn(_ASYNC_RETRY_WARNING) if self._on_error is not None: on_error = self._on_error diff --git a/tests/unit/retry/test_retry_unary.py b/tests/unit/retry/test_retry_unary.py index 6851fbe48..d3359b15f 100644 --- a/tests/unit/retry/test_retry_unary.py +++ b/tests/unit/retry/test_retry_unary.py @@ -105,14 +105,20 @@ def test_retry_target_non_retryable_error(utcnow, sleep): ) @pytest.mark.asyncio async def test_retry_target_warning_for_retry(utcnow, sleep): - predicate = retry.if_exception_type(ValueError) - target = mock.AsyncMock(spec=["__call__"]) + """ + retry.Retry should raise warning when wrapping an async function. + """ + + async def target(): + pass # pragma: NO COVER + + retry_obj = retry.Retry() with pytest.warns(Warning) as exc_info: - # Note: predicate is just a filler and doesn't affect the test - retry.retry_target(target, predicate, range(10), None) + # raise warning when wrapping an async function + retry_obj(target) - assert len(exc_info) == 2 + assert len(exc_info) == 1 assert str(exc_info[0].message) == retry.retry_unary._ASYNC_RETRY_WARNING sleep.assert_not_called() From 169511959497bd4509305c5424532e817c6a9cb2 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Mon, 21 Oct 2024 11:11:13 -0400 Subject: [PATCH 038/101] feat: add caching to GapicCallable (#666) * feat: optimize _GapicCallable * cleaned up metadata lines * chore: avoid type checks in error wrapper * Revert "chore: avoid type checks in error wrapper" This reverts commit c97a6365028f3f04d20f26aa1cc0e3131164f53e. * add default wrapped function * fixed decorator order * fixed spacing * fixed comment typo * fixed spacing * fixed spacing * removed unneeded helpers * use caching * improved metadata parsing * improved docstring * fixed logic * added benchmark test * update threshold * run benchmark in loop for testing * use verbose logs * Revert testing * used smaller value * changed threshold * removed link in comment * use list type for metadata * add types to docstring * fixed lint * convert to list at init time --------- Co-authored-by: Daniel Sanche Co-authored-by: Daniel Sanche --- google/api_core/gapic_v1/method.py | 80 ++++++++++++++---------------- tests/unit/gapic/test_method.py | 21 ++++++++ 2 files changed, 57 insertions(+), 44 deletions(-) diff --git a/google/api_core/gapic_v1/method.py b/google/api_core/gapic_v1/method.py index 0f14ea9c3..c26342e4e 100644 --- a/google/api_core/gapic_v1/method.py +++ b/google/api_core/gapic_v1/method.py @@ -42,24 +42,6 @@ class _MethodDefault(enum.Enum): so the default should be used.""" -def _is_not_none_or_false(value): - return value is not None and value is not False - - -def _apply_decorators(func, decorators): - """Apply a list of decorators to a given function. - - ``decorators`` may contain items that are ``None`` or ``False`` which will - be ignored. - """ - filtered_decorators = filter(_is_not_none_or_false, reversed(decorators)) - - for decorator in filtered_decorators: - func = decorator(func) - - return func - - class _GapicCallable(object): """Callable that applies retry, timeout, and metadata logic. @@ -91,44 +73,53 @@ def __init__( ): self._target = target self._retry = retry + if isinstance(timeout, (int, float)): + timeout = TimeToDeadlineTimeout(timeout=timeout) self._timeout = timeout self._compression = compression - self._metadata = metadata + self._metadata = list(metadata) if metadata is not None else None def __call__( self, *args, timeout=DEFAULT, retry=DEFAULT, compression=DEFAULT, **kwargs ): """Invoke the low-level RPC with retry, timeout, compression, and metadata.""" - if retry is DEFAULT: - retry = self._retry - - if timeout is DEFAULT: - timeout = self._timeout - if compression is DEFAULT: compression = self._compression - - if isinstance(timeout, (int, float)): - timeout = TimeToDeadlineTimeout(timeout=timeout) - - # Apply all applicable decorators. - wrapped_func = _apply_decorators(self._target, [retry, timeout]) + if compression is not None: + kwargs["compression"] = compression # Add the user agent metadata to the call. if self._metadata is not None: - metadata = kwargs.get("metadata", []) - # Due to the nature of invocation, None should be treated the same - # as not specified. - if metadata is None: - metadata = [] - metadata = list(metadata) - metadata.extend(self._metadata) - kwargs["metadata"] = metadata - if self._compression is not None: - kwargs["compression"] = compression + try: + # attempt to concatenate default metadata with user-provided metadata + kwargs["metadata"] = [*kwargs["metadata"], *self._metadata] + except (KeyError, TypeError): + # if metadata is not provided, use just the default metadata + kwargs["metadata"] = self._metadata + + call = self._build_wrapped_call(timeout, retry) + return call(*args, **kwargs) + + @functools.lru_cache(maxsize=4) + def _build_wrapped_call(self, timeout, retry): + """ + Build a wrapped callable that applies retry, timeout, and metadata logic. + """ + wrapped_func = self._target + if timeout is DEFAULT: + timeout = self._timeout + elif isinstance(timeout, (int, float)): + timeout = TimeToDeadlineTimeout(timeout=timeout) + if timeout is not None: + wrapped_func = timeout(wrapped_func) + + if retry is DEFAULT: + retry = self._retry + if retry is not None: + wrapped_func = retry(wrapped_func) - return wrapped_func(*args, **kwargs) + return wrapped_func def wrap_method( @@ -202,8 +193,9 @@ def get_topic(name, timeout=None): Args: func (Callable[Any]): The function to wrap. It should accept an - optional ``timeout`` argument. If ``metadata`` is not ``None``, it - should accept a ``metadata`` argument. + optional ``timeout`` (google.api_core.timeout.Timeout) argument. + If ``metadata`` is not ``None``, it should accept a ``metadata`` + (Sequence[Tuple[str, str]]) argument. default_retry (Optional[google.api_core.Retry]): The default retry strategy. If ``None``, the method will not retry by default. default_timeout (Optional[google.api_core.Timeout]): The default diff --git a/tests/unit/gapic/test_method.py b/tests/unit/gapic/test_method.py index 87aa63905..8ada092bb 100644 --- a/tests/unit/gapic/test_method.py +++ b/tests/unit/gapic/test_method.py @@ -222,3 +222,24 @@ def test_wrap_method_with_call_not_supported(): with pytest.raises(ValueError) as exc_info: google.api_core.gapic_v1.method.wrap_method(method, with_call=True) assert "with_call=True is only supported for unary calls" in str(exc_info.value) + + +def test_benchmark_gapic_call(): + """ + Ensure the __call__ method performance does not regress from expectations + + __call__ builds a new wrapped function using passed-in timeout and retry, but + subsequent calls are cached + + Note: The threshold has been tuned for the CI workers. Test may flake on + slower hardware + """ + from google.api_core.gapic_v1.method import _GapicCallable + from google.api_core.retry import Retry + from timeit import timeit + + gapic_callable = _GapicCallable( + lambda *a, **k: 1, retry=Retry(), timeout=1010, compression=False + ) + avg_time = timeit(lambda: gapic_callable(), number=10_000) + assert avg_time < 0.4 From 4dbb405602295cc39f75b0b8568e4078042db9e4 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Mon, 21 Oct 2024 13:47:06 -0400 Subject: [PATCH 039/101] chore: update templated files (#738) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: update templated files * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: Owl Bot Co-authored-by: ohmayr --- .github/.OwlBot.lock.yaml | 4 +- .kokoro/docker/docs/requirements.txt | 42 +- .kokoro/release.sh | 2 +- .kokoro/release/common.cfg | 2 +- .kokoro/requirements.txt | 610 +++++++++++++-------------- 5 files changed, 316 insertions(+), 344 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 597e0c326..95461aca7 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest - digest: sha256:e8dcfd7cbfd8beac3a3ff8d3f3185287ea0625d859168cc80faccfc9a7a00455 -# created: 2024-09-16T21:04:09.091105552Z + digest: sha256:56cd3192d58a72bcd10b031ff2917d75b6b3c2a39a2571c080f1d6c519d40330 +# created: 2024-10-19 diff --git a/.kokoro/docker/docs/requirements.txt b/.kokoro/docker/docs/requirements.txt index 7129c7715..66eacc82f 100644 --- a/.kokoro/docker/docs/requirements.txt +++ b/.kokoro/docker/docs/requirements.txt @@ -4,39 +4,39 @@ # # pip-compile --allow-unsafe --generate-hashes requirements.in # -argcomplete==3.4.0 \ - --hash=sha256:69a79e083a716173e5532e0fa3bef45f793f4e61096cf52b5a42c0211c8b8aa5 \ - --hash=sha256:c2abcdfe1be8ace47ba777d4fce319eb13bf8ad9dace8d085dcad6eded88057f +argcomplete==3.5.1 \ + --hash=sha256:1a1d148bdaa3e3b93454900163403df41448a248af01b6e849edc5ac08e6c363 \ + --hash=sha256:eb1ee355aa2557bd3d0145de7b06b2a45b0ce461e1e7813f5d066039ab4177b4 # via nox colorlog==6.8.2 \ --hash=sha256:3e3e079a41feb5a1b64f978b5ea4f46040a94f11f0e8bbb8261e3dbbeca64d44 \ --hash=sha256:4dcbb62368e2800cb3c5abd348da7e53f6c362dda502ec27c560b2e58a66bd33 # via nox -distlib==0.3.8 \ - --hash=sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784 \ - --hash=sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64 +distlib==0.3.9 \ + --hash=sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87 \ + --hash=sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403 # via virtualenv -filelock==3.15.4 \ - --hash=sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb \ - --hash=sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7 +filelock==3.16.1 \ + --hash=sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0 \ + --hash=sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435 # via virtualenv -nox==2024.4.15 \ - --hash=sha256:6492236efa15a460ecb98e7b67562a28b70da006ab0be164e8821177577c0565 \ - --hash=sha256:ecf6700199cdfa9e5ea0a41ff5e6ef4641d09508eda6edb89d9987864115817f +nox==2024.10.9 \ + --hash=sha256:1d36f309a0a2a853e9bccb76bbef6bb118ba92fa92674d15604ca99adeb29eab \ + --hash=sha256:7aa9dc8d1c27e9f45ab046ffd1c3b2c4f7c91755304769df231308849ebded95 # via -r requirements.in packaging==24.1 \ --hash=sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002 \ --hash=sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124 # via nox -platformdirs==4.2.2 \ - --hash=sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee \ - --hash=sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3 +platformdirs==4.3.6 \ + --hash=sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907 \ + --hash=sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb # via virtualenv -tomli==2.0.1 \ - --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ - --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f +tomli==2.0.2 \ + --hash=sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38 \ + --hash=sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed # via nox -virtualenv==20.26.3 \ - --hash=sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a \ - --hash=sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589 +virtualenv==20.26.6 \ + --hash=sha256:280aede09a2a5c317e409a00102e7077c6432c5a38f0ef938e643805a7ad2c48 \ + --hash=sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2 # via nox diff --git a/.kokoro/release.sh b/.kokoro/release.sh index dd1331c65..62dc059bb 100755 --- a/.kokoro/release.sh +++ b/.kokoro/release.sh @@ -23,7 +23,7 @@ python3 -m releasetool publish-reporter-script > /tmp/publisher-script; source / export PYTHONUNBUFFERED=1 # Move into the package, build the distribution and upload. -TWINE_PASSWORD=$(cat "${KOKORO_KEYSTORE_DIR}/73713_google-cloud-pypi-token-keystore-2") +TWINE_PASSWORD=$(cat "${KOKORO_KEYSTORE_DIR}/73713_google-cloud-pypi-token-keystore-3") cd github/python-api-core python3 setup.py sdist bdist_wheel twine upload --username __token__ --password "${TWINE_PASSWORD}" dist/* diff --git a/.kokoro/release/common.cfg b/.kokoro/release/common.cfg index bb8198fbc..e2d6aab84 100644 --- a/.kokoro/release/common.cfg +++ b/.kokoro/release/common.cfg @@ -28,7 +28,7 @@ before_action { fetch_keystore { keystore_resource { keystore_config_id: 73713 - keyname: "google-cloud-pypi-token-keystore-2" + keyname: "google-cloud-pypi-token-keystore-3" } } } diff --git a/.kokoro/requirements.txt b/.kokoro/requirements.txt index 9622baf0b..006d8ef93 100644 --- a/.kokoro/requirements.txt +++ b/.kokoro/requirements.txt @@ -4,79 +4,94 @@ # # pip-compile --allow-unsafe --generate-hashes requirements.in # -argcomplete==3.4.0 \ - --hash=sha256:69a79e083a716173e5532e0fa3bef45f793f4e61096cf52b5a42c0211c8b8aa5 \ - --hash=sha256:c2abcdfe1be8ace47ba777d4fce319eb13bf8ad9dace8d085dcad6eded88057f +argcomplete==3.5.1 \ + --hash=sha256:1a1d148bdaa3e3b93454900163403df41448a248af01b6e849edc5ac08e6c363 \ + --hash=sha256:eb1ee355aa2557bd3d0145de7b06b2a45b0ce461e1e7813f5d066039ab4177b4 # via nox -attrs==23.2.0 \ - --hash=sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30 \ - --hash=sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1 +attrs==24.2.0 \ + --hash=sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346 \ + --hash=sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2 # via gcp-releasetool backports-tarfile==1.2.0 \ --hash=sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34 \ --hash=sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991 # via jaraco-context -cachetools==5.3.3 \ - --hash=sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945 \ - --hash=sha256:ba29e2dfa0b8b556606f097407ed1aa62080ee108ab0dc5ec9d6a723a007d105 +cachetools==5.5.0 \ + --hash=sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292 \ + --hash=sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a # via google-auth -certifi==2024.7.4 \ - --hash=sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b \ - --hash=sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90 +certifi==2024.8.30 \ + --hash=sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8 \ + --hash=sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9 # via requests -cffi==1.16.0 \ - --hash=sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc \ - --hash=sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a \ - --hash=sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417 \ - --hash=sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab \ - --hash=sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520 \ - --hash=sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36 \ - --hash=sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743 \ - --hash=sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8 \ - --hash=sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed \ - --hash=sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684 \ - --hash=sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56 \ - --hash=sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324 \ - --hash=sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d \ - --hash=sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235 \ - --hash=sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e \ - --hash=sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088 \ - --hash=sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000 \ - --hash=sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7 \ - --hash=sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e \ - --hash=sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673 \ - --hash=sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c \ - --hash=sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe \ - --hash=sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2 \ - --hash=sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098 \ - --hash=sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8 \ - --hash=sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a \ - --hash=sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0 \ - --hash=sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b \ - --hash=sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896 \ - --hash=sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e \ - --hash=sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9 \ - --hash=sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2 \ - --hash=sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b \ - --hash=sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6 \ - --hash=sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404 \ - --hash=sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f \ - --hash=sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0 \ - --hash=sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4 \ - --hash=sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc \ - --hash=sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936 \ - --hash=sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba \ - --hash=sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872 \ - --hash=sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb \ - --hash=sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614 \ - --hash=sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1 \ - --hash=sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d \ - --hash=sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969 \ - --hash=sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b \ - --hash=sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4 \ - --hash=sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627 \ - --hash=sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956 \ - --hash=sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357 +cffi==1.17.1 \ + --hash=sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8 \ + --hash=sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2 \ + --hash=sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1 \ + --hash=sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15 \ + --hash=sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36 \ + --hash=sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824 \ + --hash=sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8 \ + --hash=sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36 \ + --hash=sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17 \ + --hash=sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf \ + --hash=sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc \ + --hash=sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3 \ + --hash=sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed \ + --hash=sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702 \ + --hash=sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1 \ + --hash=sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8 \ + --hash=sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903 \ + --hash=sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6 \ + --hash=sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d \ + --hash=sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b \ + --hash=sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e \ + --hash=sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be \ + --hash=sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c \ + --hash=sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683 \ + --hash=sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9 \ + --hash=sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c \ + --hash=sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8 \ + --hash=sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1 \ + --hash=sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4 \ + --hash=sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655 \ + --hash=sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67 \ + --hash=sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595 \ + --hash=sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0 \ + --hash=sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65 \ + --hash=sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41 \ + --hash=sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6 \ + --hash=sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401 \ + --hash=sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6 \ + --hash=sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3 \ + --hash=sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16 \ + --hash=sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93 \ + --hash=sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e \ + --hash=sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4 \ + --hash=sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964 \ + --hash=sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c \ + --hash=sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576 \ + --hash=sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0 \ + --hash=sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3 \ + --hash=sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662 \ + --hash=sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3 \ + --hash=sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff \ + --hash=sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5 \ + --hash=sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd \ + --hash=sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f \ + --hash=sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5 \ + --hash=sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14 \ + --hash=sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d \ + --hash=sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9 \ + --hash=sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7 \ + --hash=sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382 \ + --hash=sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a \ + --hash=sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e \ + --hash=sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a \ + --hash=sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4 \ + --hash=sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99 \ + --hash=sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87 \ + --hash=sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b # via cryptography charset-normalizer==2.1.1 \ --hash=sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845 \ @@ -97,72 +112,67 @@ colorlog==6.8.2 \ # via # gcp-docuploader # nox -cryptography==42.0.8 \ - --hash=sha256:013629ae70b40af70c9a7a5db40abe5d9054e6f4380e50ce769947b73bf3caad \ - --hash=sha256:2346b911eb349ab547076f47f2e035fc8ff2c02380a7cbbf8d87114fa0f1c583 \ - --hash=sha256:2f66d9cd9147ee495a8374a45ca445819f8929a3efcd2e3df6428e46c3cbb10b \ - --hash=sha256:2f88d197e66c65be5e42cd72e5c18afbfae3f741742070e3019ac8f4ac57262c \ - --hash=sha256:31f721658a29331f895a5a54e7e82075554ccfb8b163a18719d342f5ffe5ecb1 \ - --hash=sha256:343728aac38decfdeecf55ecab3264b015be68fc2816ca800db649607aeee648 \ - --hash=sha256:5226d5d21ab681f432a9c1cf8b658c0cb02533eece706b155e5fbd8a0cdd3949 \ - --hash=sha256:57080dee41209e556a9a4ce60d229244f7a66ef52750f813bfbe18959770cfba \ - --hash=sha256:5a94eccb2a81a309806027e1670a358b99b8fe8bfe9f8d329f27d72c094dde8c \ - --hash=sha256:6b7c4f03ce01afd3b76cf69a5455caa9cfa3de8c8f493e0d3ab7d20611c8dae9 \ - --hash=sha256:7016f837e15b0a1c119d27ecd89b3515f01f90a8615ed5e9427e30d9cdbfed3d \ - --hash=sha256:81884c4d096c272f00aeb1f11cf62ccd39763581645b0812e99a91505fa48e0c \ - --hash=sha256:81d8a521705787afe7a18d5bfb47ea9d9cc068206270aad0b96a725022e18d2e \ - --hash=sha256:8d09d05439ce7baa8e9e95b07ec5b6c886f548deb7e0f69ef25f64b3bce842f2 \ - --hash=sha256:961e61cefdcb06e0c6d7e3a1b22ebe8b996eb2bf50614e89384be54c48c6b63d \ - --hash=sha256:9c0c1716c8447ee7dbf08d6db2e5c41c688544c61074b54fc4564196f55c25a7 \ - --hash=sha256:a0608251135d0e03111152e41f0cc2392d1e74e35703960d4190b2e0f4ca9c70 \ - --hash=sha256:a0c5b2b0585b6af82d7e385f55a8bc568abff8923af147ee3c07bd8b42cda8b2 \ - --hash=sha256:ad803773e9df0b92e0a817d22fd8a3675493f690b96130a5e24f1b8fabbea9c7 \ - --hash=sha256:b297f90c5723d04bcc8265fc2a0f86d4ea2e0f7ab4b6994459548d3a6b992a14 \ - --hash=sha256:ba4f0a211697362e89ad822e667d8d340b4d8d55fae72cdd619389fb5912eefe \ - --hash=sha256:c4783183f7cb757b73b2ae9aed6599b96338eb957233c58ca8f49a49cc32fd5e \ - --hash=sha256:c9bb2ae11bfbab395bdd072985abde58ea9860ed84e59dbc0463a5d0159f5b71 \ - --hash=sha256:cafb92b2bc622cd1aa6a1dce4b93307792633f4c5fe1f46c6b97cf67073ec961 \ - --hash=sha256:d45b940883a03e19e944456a558b67a41160e367a719833c53de6911cabba2b7 \ - --hash=sha256:dc0fdf6787f37b1c6b08e6dfc892d9d068b5bdb671198c72072828b80bd5fe4c \ - --hash=sha256:dea567d1b0e8bc5764b9443858b673b734100c2871dc93163f58c46a97a83d28 \ - --hash=sha256:dec9b018df185f08483f294cae6ccac29e7a6e0678996587363dc352dc65c842 \ - --hash=sha256:e3ec3672626e1b9e55afd0df6d774ff0e953452886e06e0f1eb7eb0c832e8902 \ - --hash=sha256:e599b53fd95357d92304510fb7bda8523ed1f79ca98dce2f43c115950aa78801 \ - --hash=sha256:fa76fbb7596cc5839320000cdd5d0955313696d9511debab7ee7278fc8b5c84a \ - --hash=sha256:fff12c88a672ab9c9c1cf7b0c80e3ad9e2ebd9d828d955c126be4fd3e5578c9e +cryptography==43.0.1 \ + --hash=sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494 \ + --hash=sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806 \ + --hash=sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d \ + --hash=sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062 \ + --hash=sha256:2bd51274dcd59f09dd952afb696bf9c61a7a49dfc764c04dd33ef7a6b502a1e2 \ + --hash=sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4 \ + --hash=sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1 \ + --hash=sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85 \ + --hash=sha256:5b43d1ea6b378b54a1dc99dd8a2b5be47658fe9a7ce0a58ff0b55f4b43ef2b84 \ + --hash=sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042 \ + --hash=sha256:666ae11966643886c2987b3b721899d250855718d6d9ce41b521252a17985f4d \ + --hash=sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962 \ + --hash=sha256:7c05650fe8023c5ed0d46793d4b7d7e6cd9c04e68eabe5b0aeea836e37bdcec2 \ + --hash=sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa \ + --hash=sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d \ + --hash=sha256:88cce104c36870d70c49c7c8fd22885875d950d9ee6ab54df2745f83ba0dc365 \ + --hash=sha256:9d3cdb25fa98afdd3d0892d132b8d7139e2c087da1712041f6b762e4f807cc96 \ + --hash=sha256:a575913fb06e05e6b4b814d7f7468c2c660e8bb16d8d5a1faf9b33ccc569dd47 \ + --hash=sha256:ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d \ + --hash=sha256:c1332724be35d23a854994ff0b66530119500b6053d0bd3363265f7e5e77288d \ + --hash=sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c \ + --hash=sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb \ + --hash=sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277 \ + --hash=sha256:e710bf40870f4db63c3d7d929aa9e09e4e7ee219e703f949ec4073b4294f6172 \ + --hash=sha256:ea25acb556320250756e53f9e20a4177515f012c9eaea17eb7587a8c4d8ae034 \ + --hash=sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a \ + --hash=sha256:fba1007b3ef89946dbbb515aeeb41e30203b004f0b4b00e5e16078b518563289 # via # -r requirements.in # gcp-releasetool # secretstorage -distlib==0.3.8 \ - --hash=sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784 \ - --hash=sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64 +distlib==0.3.9 \ + --hash=sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87 \ + --hash=sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403 # via virtualenv docutils==0.21.2 \ --hash=sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f \ --hash=sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2 # via readme-renderer -filelock==3.15.4 \ - --hash=sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb \ - --hash=sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7 +filelock==3.16.1 \ + --hash=sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0 \ + --hash=sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435 # via virtualenv gcp-docuploader==0.6.5 \ --hash=sha256:30221d4ac3e5a2b9c69aa52fdbef68cc3f27d0e6d0d90e220fc024584b8d2318 \ --hash=sha256:b7458ef93f605b9d46a4bf3a8dc1755dad1f31d030c8679edf304e343b347eea # via -r requirements.in -gcp-releasetool==2.0.1 \ - --hash=sha256:34314a910c08e8911d9c965bd44f8f2185c4f556e737d719c33a41f6a610de96 \ - --hash=sha256:b0d5863c6a070702b10883d37c4bdfd74bf930fe417f36c0c965d3b7c779ae62 +gcp-releasetool==2.1.1 \ + --hash=sha256:25639269f4eae510094f9dbed9894977e1966933211eb155a451deebc3fc0b30 \ + --hash=sha256:845f4ded3d9bfe8cc7fdaad789e83f4ea014affa77785259a7ddac4b243e099e # via -r requirements.in -google-api-core==2.19.1 \ - --hash=sha256:f12a9b8309b5e21d92483bbd47ce2c445861ec7d269ef6784ecc0ea8c1fa6125 \ - --hash=sha256:f4695f1e3650b316a795108a76a1c416e6afb036199d1c1f1f110916df479ffd +google-api-core==2.21.0 \ + --hash=sha256:4a152fd11a9f774ea606388d423b68aa7e6d6a0ffe4c8266f74979613ec09f81 \ + --hash=sha256:6869eacb2a37720380ba5898312af79a4d30b8bca1548fb4093e0697dc4bdf5d # via # google-cloud-core # google-cloud-storage -google-auth==2.31.0 \ - --hash=sha256:042c4702efa9f7d3c48d3a69341c209381b125faa6dbf3ebe56bc7e40ae05c23 \ - --hash=sha256:87805c36970047247c8afe614d4e3af8eceafc1ebba0c679fe75ddd1d575e871 +google-auth==2.35.0 \ + --hash=sha256:25df55f327ef021de8be50bad0dfd4a916ad0de96da86cd05661c9297723ad3f \ + --hash=sha256:f4c64ed4e01e8e8b646ef34c018f8bf3338df0c8e37d8b3bba40e7f574a3278a # via # gcp-releasetool # google-api-core @@ -172,97 +182,56 @@ google-cloud-core==2.4.1 \ --hash=sha256:9b7749272a812bde58fff28868d0c5e2f585b82f37e09a1f6ed2d4d10f134073 \ --hash=sha256:a9e6a4422b9ac5c29f79a0ede9485473338e2ce78d91f2370c01e730eab22e61 # via google-cloud-storage -google-cloud-storage==2.17.0 \ - --hash=sha256:49378abff54ef656b52dca5ef0f2eba9aa83dc2b2c72c78714b03a1a95fe9388 \ - --hash=sha256:5b393bc766b7a3bc6f5407b9e665b2450d36282614b7945e570b3480a456d1e1 +google-cloud-storage==2.18.2 \ + --hash=sha256:97a4d45c368b7d401ed48c4fdfe86e1e1cb96401c9e199e419d289e2c0370166 \ + --hash=sha256:aaf7acd70cdad9f274d29332673fcab98708d0e1f4dceb5a5356aaef06af4d99 # via gcp-docuploader -google-crc32c==1.5.0 \ - --hash=sha256:024894d9d3cfbc5943f8f230e23950cd4906b2fe004c72e29b209420a1e6b05a \ - --hash=sha256:02c65b9817512edc6a4ae7c7e987fea799d2e0ee40c53ec573a692bee24de876 \ - --hash=sha256:02ebb8bf46c13e36998aeaad1de9b48f4caf545e91d14041270d9dca767b780c \ - --hash=sha256:07eb3c611ce363c51a933bf6bd7f8e3878a51d124acfc89452a75120bc436289 \ - --hash=sha256:1034d91442ead5a95b5aaef90dbfaca8633b0247d1e41621d1e9f9db88c36298 \ - --hash=sha256:116a7c3c616dd14a3de8c64a965828b197e5f2d121fedd2f8c5585c547e87b02 \ - --hash=sha256:19e0a019d2c4dcc5e598cd4a4bc7b008546b0358bd322537c74ad47a5386884f \ - --hash=sha256:1c7abdac90433b09bad6c43a43af253e688c9cfc1c86d332aed13f9a7c7f65e2 \ - --hash=sha256:1e986b206dae4476f41bcec1faa057851f3889503a70e1bdb2378d406223994a \ - --hash=sha256:272d3892a1e1a2dbc39cc5cde96834c236d5327e2122d3aaa19f6614531bb6eb \ - --hash=sha256:278d2ed7c16cfc075c91378c4f47924c0625f5fc84b2d50d921b18b7975bd210 \ - --hash=sha256:2ad40e31093a4af319dadf503b2467ccdc8f67c72e4bcba97f8c10cb078207b5 \ - --hash=sha256:2e920d506ec85eb4ba50cd4228c2bec05642894d4c73c59b3a2fe20346bd00ee \ - --hash=sha256:3359fc442a743e870f4588fcf5dcbc1bf929df1fad8fb9905cd94e5edb02e84c \ - --hash=sha256:37933ec6e693e51a5b07505bd05de57eee12f3e8c32b07da7e73669398e6630a \ - --hash=sha256:398af5e3ba9cf768787eef45c803ff9614cc3e22a5b2f7d7ae116df8b11e3314 \ - --hash=sha256:3b747a674c20a67343cb61d43fdd9207ce5da6a99f629c6e2541aa0e89215bcd \ - --hash=sha256:461665ff58895f508e2866824a47bdee72497b091c730071f2b7575d5762ab65 \ - --hash=sha256:4c6fdd4fccbec90cc8a01fc00773fcd5fa28db683c116ee3cb35cd5da9ef6c37 \ - --hash=sha256:5829b792bf5822fd0a6f6eb34c5f81dd074f01d570ed7f36aa101d6fc7a0a6e4 \ - --hash=sha256:596d1f98fc70232fcb6590c439f43b350cb762fb5d61ce7b0e9db4539654cc13 \ - --hash=sha256:5ae44e10a8e3407dbe138984f21e536583f2bba1be9491239f942c2464ac0894 \ - --hash=sha256:635f5d4dd18758a1fbd1049a8e8d2fee4ffed124462d837d1a02a0e009c3ab31 \ - --hash=sha256:64e52e2b3970bd891309c113b54cf0e4384762c934d5ae56e283f9a0afcd953e \ - --hash=sha256:66741ef4ee08ea0b2cc3c86916ab66b6aef03768525627fd6a1b34968b4e3709 \ - --hash=sha256:67b741654b851abafb7bc625b6d1cdd520a379074e64b6a128e3b688c3c04740 \ - --hash=sha256:6ac08d24c1f16bd2bf5eca8eaf8304812f44af5cfe5062006ec676e7e1d50afc \ - --hash=sha256:6f998db4e71b645350b9ac28a2167e6632c239963ca9da411523bb439c5c514d \ - --hash=sha256:72218785ce41b9cfd2fc1d6a017dc1ff7acfc4c17d01053265c41a2c0cc39b8c \ - --hash=sha256:74dea7751d98034887dbd821b7aae3e1d36eda111d6ca36c206c44478035709c \ - --hash=sha256:759ce4851a4bb15ecabae28f4d2e18983c244eddd767f560165563bf9aefbc8d \ - --hash=sha256:77e2fd3057c9d78e225fa0a2160f96b64a824de17840351b26825b0848022906 \ - --hash=sha256:7c074fece789b5034b9b1404a1f8208fc2d4c6ce9decdd16e8220c5a793e6f61 \ - --hash=sha256:7c42c70cd1d362284289c6273adda4c6af8039a8ae12dc451dcd61cdabb8ab57 \ - --hash=sha256:7f57f14606cd1dd0f0de396e1e53824c371e9544a822648cd76c034d209b559c \ - --hash=sha256:83c681c526a3439b5cf94f7420471705bbf96262f49a6fe546a6db5f687a3d4a \ - --hash=sha256:8485b340a6a9e76c62a7dce3c98e5f102c9219f4cfbf896a00cf48caf078d438 \ - --hash=sha256:84e6e8cd997930fc66d5bb4fde61e2b62ba19d62b7abd7a69920406f9ecca946 \ - --hash=sha256:89284716bc6a5a415d4eaa11b1726d2d60a0cd12aadf5439828353662ede9dd7 \ - --hash=sha256:8b87e1a59c38f275c0e3676fc2ab6d59eccecfd460be267ac360cc31f7bcde96 \ - --hash=sha256:8f24ed114432de109aa9fd317278518a5af2d31ac2ea6b952b2f7782b43da091 \ - --hash=sha256:98cb4d057f285bd80d8778ebc4fde6b4d509ac3f331758fb1528b733215443ae \ - --hash=sha256:998679bf62b7fb599d2878aa3ed06b9ce688b8974893e7223c60db155f26bd8d \ - --hash=sha256:9ba053c5f50430a3fcfd36f75aff9caeba0440b2d076afdb79a318d6ca245f88 \ - --hash=sha256:9c99616c853bb585301df6de07ca2cadad344fd1ada6d62bb30aec05219c45d2 \ - --hash=sha256:a1fd716e7a01f8e717490fbe2e431d2905ab8aa598b9b12f8d10abebb36b04dd \ - --hash=sha256:a2355cba1f4ad8b6988a4ca3feed5bff33f6af2d7f134852cf279c2aebfde541 \ - --hash=sha256:b1f8133c9a275df5613a451e73f36c2aea4fe13c5c8997e22cf355ebd7bd0728 \ - --hash=sha256:b8667b48e7a7ef66afba2c81e1094ef526388d35b873966d8a9a447974ed9178 \ - --hash=sha256:ba1eb1843304b1e5537e1fca632fa894d6f6deca8d6389636ee5b4797affb968 \ - --hash=sha256:be82c3c8cfb15b30f36768797a640e800513793d6ae1724aaaafe5bf86f8f346 \ - --hash=sha256:c02ec1c5856179f171e032a31d6f8bf84e5a75c45c33b2e20a3de353b266ebd8 \ - --hash=sha256:c672d99a345849301784604bfeaeba4db0c7aae50b95be04dd651fd2a7310b93 \ - --hash=sha256:c6c777a480337ac14f38564ac88ae82d4cd238bf293f0a22295b66eb89ffced7 \ - --hash=sha256:cae0274952c079886567f3f4f685bcaf5708f0a23a5f5216fdab71f81a6c0273 \ - --hash=sha256:cd67cf24a553339d5062eff51013780a00d6f97a39ca062781d06b3a73b15462 \ - --hash=sha256:d3515f198eaa2f0ed49f8819d5732d70698c3fa37384146079b3799b97667a94 \ - --hash=sha256:d5280312b9af0976231f9e317c20e4a61cd2f9629b7bfea6a693d1878a264ebd \ - --hash=sha256:de06adc872bcd8c2a4e0dc51250e9e65ef2ca91be023b9d13ebd67c2ba552e1e \ - --hash=sha256:e1674e4307fa3024fc897ca774e9c7562c957af85df55efe2988ed9056dc4e57 \ - --hash=sha256:e2096eddb4e7c7bdae4bd69ad364e55e07b8316653234a56552d9c988bd2d61b \ - --hash=sha256:e560628513ed34759456a416bf86b54b2476c59144a9138165c9a1575801d0d9 \ - --hash=sha256:edfedb64740750e1a3b16152620220f51d58ff1b4abceb339ca92e934775c27a \ - --hash=sha256:f13cae8cc389a440def0c8c52057f37359014ccbc9dc1f0827936bcd367c6100 \ - --hash=sha256:f314013e7dcd5cf45ab1945d92e713eec788166262ae8deb2cfacd53def27325 \ - --hash=sha256:f583edb943cf2e09c60441b910d6a20b4d9d626c75a36c8fcac01a6c96c01183 \ - --hash=sha256:fd8536e902db7e365f49e7d9029283403974ccf29b13fc7028b97e2295b33556 \ - --hash=sha256:fe70e325aa68fa4b5edf7d1a4b6f691eb04bbccac0ace68e34820d283b5f80d4 +google-crc32c==1.6.0 \ + --hash=sha256:05e2d8c9a2f853ff116db9706b4a27350587f341eda835f46db3c0a8c8ce2f24 \ + --hash=sha256:18e311c64008f1f1379158158bb3f0c8d72635b9eb4f9545f8cf990c5668e59d \ + --hash=sha256:236c87a46cdf06384f614e9092b82c05f81bd34b80248021f729396a78e55d7e \ + --hash=sha256:35834855408429cecf495cac67ccbab802de269e948e27478b1e47dfb6465e57 \ + --hash=sha256:386122eeaaa76951a8196310432c5b0ef3b53590ef4c317ec7588ec554fec5d2 \ + --hash=sha256:40b05ab32a5067525670880eb5d169529089a26fe35dce8891127aeddc1950e8 \ + --hash=sha256:48abd62ca76a2cbe034542ed1b6aee851b6f28aaca4e6551b5599b6f3ef175cc \ + --hash=sha256:50cf2a96da226dcbff8671233ecf37bf6e95de98b2a2ebadbfdf455e6d05df42 \ + --hash=sha256:51c4f54dd8c6dfeb58d1df5e4f7f97df8abf17a36626a217f169893d1d7f3e9f \ + --hash=sha256:5bcc90b34df28a4b38653c36bb5ada35671ad105c99cfe915fb5bed7ad6924aa \ + --hash=sha256:62f6d4a29fea082ac4a3c9be5e415218255cf11684ac6ef5488eea0c9132689b \ + --hash=sha256:6eceb6ad197656a1ff49ebfbbfa870678c75be4344feb35ac1edf694309413dc \ + --hash=sha256:7aec8e88a3583515f9e0957fe4f5f6d8d4997e36d0f61624e70469771584c760 \ + --hash=sha256:91ca8145b060679ec9176e6de4f89b07363d6805bd4760631ef254905503598d \ + --hash=sha256:a184243544811e4a50d345838a883733461e67578959ac59964e43cca2c791e7 \ + --hash=sha256:a9e4b426c3702f3cd23b933436487eb34e01e00327fac20c9aebb68ccf34117d \ + --hash=sha256:bb0966e1c50d0ef5bc743312cc730b533491d60585a9a08f897274e57c3f70e0 \ + --hash=sha256:bb8b3c75bd157010459b15222c3fd30577042a7060e29d42dabce449c087f2b3 \ + --hash=sha256:bd5e7d2445d1a958c266bfa5d04c39932dc54093fa391736dbfdb0f1929c1fb3 \ + --hash=sha256:c87d98c7c4a69066fd31701c4e10d178a648c2cac3452e62c6b24dc51f9fcc00 \ + --hash=sha256:d2952396dc604544ea7476b33fe87faedc24d666fb0c2d5ac971a2b9576ab871 \ + --hash=sha256:d8797406499f28b5ef791f339594b0b5fdedf54e203b5066675c406ba69d705c \ + --hash=sha256:d9e9913f7bd69e093b81da4535ce27af842e7bf371cde42d1ae9e9bd382dc0e9 \ + --hash=sha256:e2806553238cd076f0a55bddab37a532b53580e699ed8e5606d0de1f856b5205 \ + --hash=sha256:ebab974b1687509e5c973b5c4b8b146683e101e102e17a86bd196ecaa4d099fc \ + --hash=sha256:ed767bf4ba90104c1216b68111613f0d5926fb3780660ea1198fc469af410e9d \ + --hash=sha256:f7a1fc29803712f80879b0806cb83ab24ce62fc8daf0569f2204a0cfd7f68ed4 # via # google-cloud-storage # google-resumable-media -google-resumable-media==2.7.1 \ - --hash=sha256:103ebc4ba331ab1bfdac0250f8033627a2cd7cde09e7ccff9181e31ba4315b2c \ - --hash=sha256:eae451a7b2e2cdbaaa0fd2eb00cc8a1ee5e95e16b55597359cbc3d27d7d90e33 +google-resumable-media==2.7.2 \ + --hash=sha256:3ce7551e9fe6d99e9a126101d2536612bb73486721951e9562fee0f90c6ababa \ + --hash=sha256:5280aed4629f2b60b847b0d42f9857fd4935c11af266744df33d8074cae92fe0 # via google-cloud-storage -googleapis-common-protos==1.63.2 \ - --hash=sha256:27a2499c7e8aff199665b22741997e485eccc8645aa9176c7c988e6fae507945 \ - --hash=sha256:27c5abdffc4911f28101e635de1533fb4cfd2c37fbaa9174587c799fac90aa87 +googleapis-common-protos==1.65.0 \ + --hash=sha256:2972e6c496f435b92590fd54045060867f3fe9be2c82ab148fc8885035479a63 \ + --hash=sha256:334a29d07cddc3aa01dee4988f9afd9b2916ee2ff49d6b757155dc0d197852c0 # via google-api-core -idna==3.7 \ - --hash=sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc \ - --hash=sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0 +idna==3.10 \ + --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \ + --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3 # via requests -importlib-metadata==8.0.0 \ - --hash=sha256:15584cf2b1bf449d98ff8a6ff1abef57bf20f3ac6454f431736cd3e660921b2f \ - --hash=sha256:188bd24e4c346d3f0a933f275c2fec67050326a856b9a359881d7c2a697e8812 +importlib-metadata==8.5.0 \ + --hash=sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b \ + --hash=sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7 # via # -r requirements.in # keyring @@ -271,13 +240,13 @@ jaraco-classes==3.4.0 \ --hash=sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd \ --hash=sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790 # via keyring -jaraco-context==5.3.0 \ - --hash=sha256:3e16388f7da43d384a1a7cd3452e72e14732ac9fe459678773a3608a812bf266 \ - --hash=sha256:c2f67165ce1f9be20f32f650f25d8edfc1646a8aeee48ae06fb35f90763576d2 +jaraco-context==6.0.1 \ + --hash=sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3 \ + --hash=sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4 # via keyring -jaraco-functools==4.0.1 \ - --hash=sha256:3b24ccb921d6b593bdceb56ce14799204f473976e2a9d4b15b04d0f2c2326664 \ - --hash=sha256:d33fa765374c0611b52f8b3a795f8900869aa88c84769d4d1746cd68fb28c3e8 +jaraco-functools==4.1.0 \ + --hash=sha256:70f7e0e2ae076498e212562325e805204fc092d7b4c17e0e86c959e249701a9d \ + --hash=sha256:ad159f13428bc4acbf5541ad6dec511f91573b90fba04df61dafa2a1231cf649 # via keyring jeepney==0.8.0 \ --hash=sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806 \ @@ -289,9 +258,9 @@ jinja2==3.1.4 \ --hash=sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369 \ --hash=sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d # via gcp-releasetool -keyring==25.2.1 \ - --hash=sha256:2458681cdefc0dbc0b7eb6cf75d0b98e59f9ad9b2d4edd319d18f68bdca95e50 \ - --hash=sha256:daaffd42dbda25ddafb1ad5fec4024e5bbcfe424597ca1ca452b299861e49f1b +keyring==25.4.1 \ + --hash=sha256:5426f817cf7f6f007ba5ec722b1bcad95a75b27d780343772ad76b17cb47b0bf \ + --hash=sha256:b07ebc55f3e8ed86ac81dd31ef14e81ace9dd9c3d4b5d77a6e9a2016d0d71a1b # via # gcp-releasetool # twine @@ -299,75 +268,76 @@ markdown-it-py==3.0.0 \ --hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \ --hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb # via rich -markupsafe==2.1.5 \ - --hash=sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf \ - --hash=sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff \ - --hash=sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f \ - --hash=sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3 \ - --hash=sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532 \ - --hash=sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f \ - --hash=sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617 \ - --hash=sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df \ - --hash=sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4 \ - --hash=sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906 \ - --hash=sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f \ - --hash=sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4 \ - --hash=sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8 \ - --hash=sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371 \ - --hash=sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2 \ - --hash=sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465 \ - --hash=sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52 \ - --hash=sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6 \ - --hash=sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169 \ - --hash=sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad \ - --hash=sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2 \ - --hash=sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0 \ - --hash=sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029 \ - --hash=sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f \ - --hash=sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a \ - --hash=sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced \ - --hash=sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5 \ - --hash=sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c \ - --hash=sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf \ - --hash=sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9 \ - --hash=sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb \ - --hash=sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad \ - --hash=sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3 \ - --hash=sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1 \ - --hash=sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46 \ - --hash=sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc \ - --hash=sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a \ - --hash=sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee \ - --hash=sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900 \ - --hash=sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5 \ - --hash=sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea \ - --hash=sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f \ - --hash=sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5 \ - --hash=sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e \ - --hash=sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a \ - --hash=sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f \ - --hash=sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50 \ - --hash=sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a \ - --hash=sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b \ - --hash=sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4 \ - --hash=sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff \ - --hash=sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2 \ - --hash=sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46 \ - --hash=sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b \ - --hash=sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf \ - --hash=sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5 \ - --hash=sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5 \ - --hash=sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab \ - --hash=sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd \ - --hash=sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68 +markupsafe==3.0.1 \ + --hash=sha256:0778de17cff1acaeccc3ff30cd99a3fd5c50fc58ad3d6c0e0c4c58092b859396 \ + --hash=sha256:0f84af7e813784feb4d5e4ff7db633aba6c8ca64a833f61d8e4eade234ef0c38 \ + --hash=sha256:17b2aea42a7280db02ac644db1d634ad47dcc96faf38ab304fe26ba2680d359a \ + --hash=sha256:242d6860f1fd9191aef5fae22b51c5c19767f93fb9ead4d21924e0bcb17619d8 \ + --hash=sha256:244dbe463d5fb6d7ce161301a03a6fe744dac9072328ba9fc82289238582697b \ + --hash=sha256:26627785a54a947f6d7336ce5963569b5d75614619e75193bdb4e06e21d447ad \ + --hash=sha256:2a4b34a8d14649315c4bc26bbfa352663eb51d146e35eef231dd739d54a5430a \ + --hash=sha256:2ae99f31f47d849758a687102afdd05bd3d3ff7dbab0a8f1587981b58a76152a \ + --hash=sha256:312387403cd40699ab91d50735ea7a507b788091c416dd007eac54434aee51da \ + --hash=sha256:3341c043c37d78cc5ae6e3e305e988532b072329639007fd408a476642a89fd6 \ + --hash=sha256:33d1c36b90e570ba7785dacd1faaf091203d9942bc036118fab8110a401eb1a8 \ + --hash=sha256:3e683ee4f5d0fa2dde4db77ed8dd8a876686e3fc417655c2ece9a90576905344 \ + --hash=sha256:3ffb4a8e7d46ed96ae48805746755fadd0909fea2306f93d5d8233ba23dda12a \ + --hash=sha256:40621d60d0e58aa573b68ac5e2d6b20d44392878e0bfc159012a5787c4e35bc8 \ + --hash=sha256:40f1e10d51c92859765522cbd79c5c8989f40f0419614bcdc5015e7b6bf97fc5 \ + --hash=sha256:45d42d132cff577c92bfba536aefcfea7e26efb975bd455db4e6602f5c9f45e7 \ + --hash=sha256:48488d999ed50ba8d38c581d67e496f955821dc183883550a6fbc7f1aefdc170 \ + --hash=sha256:4935dd7883f1d50e2ffecca0aa33dc1946a94c8f3fdafb8df5c330e48f71b132 \ + --hash=sha256:4c2d64fdba74ad16138300815cfdc6ab2f4647e23ced81f59e940d7d4a1469d9 \ + --hash=sha256:4c8817557d0de9349109acb38b9dd570b03cc5014e8aabf1cbddc6e81005becd \ + --hash=sha256:4ffaaac913c3f7345579db4f33b0020db693f302ca5137f106060316761beea9 \ + --hash=sha256:5a4cb365cb49b750bdb60b846b0c0bc49ed62e59a76635095a179d440540c346 \ + --hash=sha256:62fada2c942702ef8952754abfc1a9f7658a4d5460fabe95ac7ec2cbe0d02abc \ + --hash=sha256:67c519635a4f64e495c50e3107d9b4075aec33634272b5db1cde839e07367589 \ + --hash=sha256:6a54c43d3ec4cf2a39f4387ad044221c66a376e58c0d0e971d47c475ba79c6b5 \ + --hash=sha256:7044312a928a66a4c2a22644147bc61a199c1709712069a344a3fb5cfcf16915 \ + --hash=sha256:730d86af59e0e43ce277bb83970530dd223bf7f2a838e086b50affa6ec5f9295 \ + --hash=sha256:800100d45176652ded796134277ecb13640c1a537cad3b8b53da45aa96330453 \ + --hash=sha256:80fcbf3add8790caddfab6764bde258b5d09aefbe9169c183f88a7410f0f6dea \ + --hash=sha256:82b5dba6eb1bcc29cc305a18a3c5365d2af06ee71b123216416f7e20d2a84e5b \ + --hash=sha256:852dc840f6d7c985603e60b5deaae1d89c56cb038b577f6b5b8c808c97580f1d \ + --hash=sha256:8ad4ad1429cd4f315f32ef263c1342166695fad76c100c5d979c45d5570ed58b \ + --hash=sha256:8ae369e84466aa70f3154ee23c1451fda10a8ee1b63923ce76667e3077f2b0c4 \ + --hash=sha256:93e8248d650e7e9d49e8251f883eed60ecbc0e8ffd6349e18550925e31bd029b \ + --hash=sha256:973a371a55ce9ed333a3a0f8e0bcfae9e0d637711534bcb11e130af2ab9334e7 \ + --hash=sha256:9ba25a71ebf05b9bb0e2ae99f8bc08a07ee8e98c612175087112656ca0f5c8bf \ + --hash=sha256:a10860e00ded1dd0a65b83e717af28845bb7bd16d8ace40fe5531491de76b79f \ + --hash=sha256:a4792d3b3a6dfafefdf8e937f14906a51bd27025a36f4b188728a73382231d91 \ + --hash=sha256:a7420ceda262dbb4b8d839a4ec63d61c261e4e77677ed7c66c99f4e7cb5030dd \ + --hash=sha256:ad91738f14eb8da0ff82f2acd0098b6257621410dcbd4df20aaa5b4233d75a50 \ + --hash=sha256:b6a387d61fe41cdf7ea95b38e9af11cfb1a63499af2759444b99185c4ab33f5b \ + --hash=sha256:b954093679d5750495725ea6f88409946d69cfb25ea7b4c846eef5044194f583 \ + --hash=sha256:bbde71a705f8e9e4c3e9e33db69341d040c827c7afa6789b14c6e16776074f5a \ + --hash=sha256:beeebf760a9c1f4c07ef6a53465e8cfa776ea6a2021eda0d0417ec41043fe984 \ + --hash=sha256:c91b394f7601438ff79a4b93d16be92f216adb57d813a78be4446fe0f6bc2d8c \ + --hash=sha256:c97ff7fedf56d86bae92fa0a646ce1a0ec7509a7578e1ed238731ba13aabcd1c \ + --hash=sha256:cb53e2a99df28eee3b5f4fea166020d3ef9116fdc5764bc5117486e6d1211b25 \ + --hash=sha256:cbf445eb5628981a80f54087f9acdbf84f9b7d862756110d172993b9a5ae81aa \ + --hash=sha256:d06b24c686a34c86c8c1fba923181eae6b10565e4d80bdd7bc1c8e2f11247aa4 \ + --hash=sha256:d98e66a24497637dd31ccab090b34392dddb1f2f811c4b4cd80c230205c074a3 \ + --hash=sha256:db15ce28e1e127a0013dfb8ac243a8e392db8c61eae113337536edb28bdc1f97 \ + --hash=sha256:db842712984e91707437461930e6011e60b39136c7331e971952bb30465bc1a1 \ + --hash=sha256:e24bfe89c6ac4c31792793ad9f861b8f6dc4546ac6dc8f1c9083c7c4f2b335cd \ + --hash=sha256:e81c52638315ff4ac1b533d427f50bc0afc746deb949210bc85f05d4f15fd772 \ + --hash=sha256:e9393357f19954248b00bed7c56f29a25c930593a77630c719653d51e7669c2a \ + --hash=sha256:ee3941769bd2522fe39222206f6dd97ae83c442a94c90f2b7a25d847d40f4729 \ + --hash=sha256:f31ae06f1328595d762c9a2bf29dafd8621c7d3adc130cbb46278079758779ca \ + --hash=sha256:f94190df587738280d544971500b9cafc9b950d32efcb1fba9ac10d84e6aa4e6 \ + --hash=sha256:fa7d686ed9883f3d664d39d5a8e74d3c5f63e603c2e3ff0abcba23eac6542635 \ + --hash=sha256:fb532dd9900381d2e8f48172ddc5a59db4c445a11b9fab40b3b786da40d3b56b \ + --hash=sha256:fe32482b37b4b00c7a52a07211b479653b7fe4f22b2e481b9a9b099d8a430f2f # via jinja2 mdurl==0.1.2 \ --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba # via markdown-it-py -more-itertools==10.3.0 \ - --hash=sha256:e5d93ef411224fbcef366a6e8ddc4c5781bc6359d43412a65dd5964e46111463 \ - --hash=sha256:ea6a02e24a9161e51faad17a8782b92a0df82c12c1c8886fec7f0c3fa1a1b320 +more-itertools==10.5.0 \ + --hash=sha256:037b0d3203ce90cca8ab1defbbdac29d5f993fc20131f3664dc8d6acfa872aef \ + --hash=sha256:5482bfef7849c25dc3c6dd53a6173ae4795da2a41a80faea6700d9f5846c5da6 # via # jaraco-classes # jaraco-functools @@ -389,9 +359,9 @@ nh3==0.2.18 \ --hash=sha256:de3ceed6e661954871d6cd78b410213bdcb136f79aafe22aa7182e028b8c7307 \ --hash=sha256:f0eca9ca8628dbb4e916ae2491d72957fdd35f7a5d326b7032a345f111ac07fe # via readme-renderer -nox==2024.4.15 \ - --hash=sha256:6492236efa15a460ecb98e7b67562a28b70da006ab0be164e8821177577c0565 \ - --hash=sha256:ecf6700199cdfa9e5ea0a41ff5e6ef4641d09508eda6edb89d9987864115817f +nox==2024.10.9 \ + --hash=sha256:1d36f309a0a2a853e9bccb76bbef6bb118ba92fa92674d15604ca99adeb29eab \ + --hash=sha256:7aa9dc8d1c27e9f45ab046ffd1c3b2c4f7c91755304769df231308849ebded95 # via -r requirements.in packaging==24.1 \ --hash=sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002 \ @@ -403,41 +373,41 @@ pkginfo==1.10.0 \ --hash=sha256:5df73835398d10db79f8eecd5cd86b1f6d29317589ea70796994d49399af6297 \ --hash=sha256:889a6da2ed7ffc58ab5b900d888ddce90bce912f2d2de1dc1c26f4cb9fe65097 # via twine -platformdirs==4.2.2 \ - --hash=sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee \ - --hash=sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3 +platformdirs==4.3.6 \ + --hash=sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907 \ + --hash=sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb # via virtualenv proto-plus==1.24.0 \ --hash=sha256:30b72a5ecafe4406b0d339db35b56c4059064e69227b8c3bda7462397f966445 \ --hash=sha256:402576830425e5f6ce4c2a6702400ac79897dab0b4343821aa5188b0fab81a12 # via google-api-core -protobuf==5.27.2 \ - --hash=sha256:0e341109c609749d501986b835f667c6e1e24531096cff9d34ae411595e26505 \ - --hash=sha256:176c12b1f1c880bf7a76d9f7c75822b6a2bc3db2d28baa4d300e8ce4cde7409b \ - --hash=sha256:354d84fac2b0d76062e9b3221f4abbbacdfd2a4d8af36bab0474f3a0bb30ab38 \ - --hash=sha256:4fadd8d83e1992eed0248bc50a4a6361dc31bcccc84388c54c86e530b7f58863 \ - --hash=sha256:54330f07e4949d09614707c48b06d1a22f8ffb5763c159efd5c0928326a91470 \ - --hash=sha256:610e700f02469c4a997e58e328cac6f305f649826853813177e6290416e846c6 \ - --hash=sha256:7fc3add9e6003e026da5fc9e59b131b8f22b428b991ccd53e2af8071687b4fce \ - --hash=sha256:9e8f199bf7f97bd7ecebffcae45ebf9527603549b2b562df0fbc6d4d688f14ca \ - --hash=sha256:a109916aaac42bff84702fb5187f3edadbc7c97fc2c99c5ff81dd15dcce0d1e5 \ - --hash=sha256:b848dbe1d57ed7c191dfc4ea64b8b004a3f9ece4bf4d0d80a367b76df20bf36e \ - --hash=sha256:f3ecdef226b9af856075f28227ff2c90ce3a594d092c39bee5513573f25e2714 +protobuf==5.28.2 \ + --hash=sha256:2c69461a7fcc8e24be697624c09a839976d82ae75062b11a0972e41fd2cd9132 \ + --hash=sha256:35cfcb15f213449af7ff6198d6eb5f739c37d7e4f1c09b5d0641babf2cc0c68f \ + --hash=sha256:52235802093bd8a2811abbe8bf0ab9c5f54cca0a751fdd3f6ac2a21438bffece \ + --hash=sha256:59379674ff119717404f7454647913787034f03fe7049cbef1d74a97bb4593f0 \ + --hash=sha256:5e8a95246d581eef20471b5d5ba010d55f66740942b95ba9b872d918c459452f \ + --hash=sha256:87317e9bcda04a32f2ee82089a204d3a2f0d3c8aeed16568c7daf4756e4f1fe0 \ + --hash=sha256:8ddc60bf374785fb7cb12510b267f59067fa10087325b8e1855b898a0d81d276 \ + --hash=sha256:a8b9403fc70764b08d2f593ce44f1d2920c5077bf7d311fefec999f8c40f78b7 \ + --hash=sha256:c0ea0123dac3399a2eeb1a1443d82b7afc9ff40241433296769f7da42d142ec3 \ + --hash=sha256:ca53faf29896c526863366a52a8f4d88e69cd04ec9571ed6082fa117fac3ab36 \ + --hash=sha256:eeea10f3dc0ac7e6b4933d32db20662902b4ab81bf28df12218aa389e9c2102d # via # gcp-docuploader # gcp-releasetool # google-api-core # googleapis-common-protos # proto-plus -pyasn1==0.6.0 \ - --hash=sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c \ - --hash=sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473 +pyasn1==0.6.1 \ + --hash=sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629 \ + --hash=sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034 # via # pyasn1-modules # rsa -pyasn1-modules==0.4.0 \ - --hash=sha256:831dbcea1b177b28c9baddf4c6d1013c24c3accd14a1873fffaa6a2e905f17b6 \ - --hash=sha256:be04f15b66c206eed667e0bb5ab27e2b1855ea54a842e5037738099e8ca4ae0b +pyasn1-modules==0.4.1 \ + --hash=sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd \ + --hash=sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c # via google-auth pycparser==2.22 \ --hash=sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6 \ @@ -449,9 +419,9 @@ pygments==2.18.0 \ # via # readme-renderer # rich -pyjwt==2.8.0 \ - --hash=sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de \ - --hash=sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320 +pyjwt==2.9.0 \ + --hash=sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850 \ + --hash=sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c # via gcp-releasetool pyperclip==1.9.0 \ --hash=sha256:b7de0142ddc81bfc5c7507eea19da920b92252b548b96186caf94a5e2527d310 @@ -481,9 +451,9 @@ rfc3986==2.0.0 \ --hash=sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd \ --hash=sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c # via twine -rich==13.7.1 \ - --hash=sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222 \ - --hash=sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432 +rich==13.9.2 \ + --hash=sha256:51a2c62057461aaf7152b4d611168f93a9fc73068f8ded2790f29fe2b5366d0c \ + --hash=sha256:8c82a3d3f8dcfe9e734771313e606b39d8247bb6b826e196f4914b333b743cf1 # via twine rsa==4.9 \ --hash=sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7 \ @@ -499,9 +469,9 @@ six==1.16.0 \ # via # gcp-docuploader # python-dateutil -tomli==2.0.1 \ - --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ - --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f +tomli==2.0.2 \ + --hash=sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38 \ + --hash=sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed # via nox twine==5.1.1 \ --hash=sha256:215dbe7b4b94c2c50a7315c0275d2258399280fbb7d04182c7e55e24b5f93997 \ @@ -510,28 +480,30 @@ twine==5.1.1 \ typing-extensions==4.12.2 \ --hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \ --hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8 - # via -r requirements.in -urllib3==2.2.2 \ - --hash=sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472 \ - --hash=sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168 + # via + # -r requirements.in + # rich +urllib3==2.2.3 \ + --hash=sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac \ + --hash=sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9 # via # requests # twine -virtualenv==20.26.3 \ - --hash=sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a \ - --hash=sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589 +virtualenv==20.26.6 \ + --hash=sha256:280aede09a2a5c317e409a00102e7077c6432c5a38f0ef938e643805a7ad2c48 \ + --hash=sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2 # via nox -wheel==0.43.0 \ - --hash=sha256:465ef92c69fa5c5da2d1cf8ac40559a8c940886afcef87dcf14b9470862f1d85 \ - --hash=sha256:55c570405f142630c6b9f72fe09d9b67cf1477fcf543ae5b8dcb1f5b7377da81 +wheel==0.44.0 \ + --hash=sha256:2376a90c98cc337d18623527a97c31797bd02bad0033d41547043a1cbfbe448f \ + --hash=sha256:a29c3f2817e95ab89aa4660681ad547c0e9547f20e75b0562fe7723c9a2a9d49 # via -r requirements.in -zipp==3.19.2 \ - --hash=sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19 \ - --hash=sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c +zipp==3.20.2 \ + --hash=sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350 \ + --hash=sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29 # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: -setuptools==70.2.0 \ - --hash=sha256:b8b8060bb426838fbe942479c90296ce976249451118ef566a5a0b7d8b78fb05 \ - --hash=sha256:bd63e505105011b25c3c11f753f7e3b8465ea739efddaccef8f0efac2137bac1 +setuptools==75.1.0 \ + --hash=sha256:35ab7fd3bcd95e6b7fd704e4a1539513edad446c097797f2985e0e4b960772f2 \ + --hash=sha256:d59a21b17a275fb872a9c3dae73963160ae079f1049ed956880cd7c09b120538 # via -r requirements.in From 0d5ed37c96f9b40bccae98e228163a88abeb1763 Mon Sep 17 00:00:00 2001 From: Mark Daoust Date: Tue, 22 Oct 2024 07:24:59 -0700 Subject: [PATCH 040/101] fix: Improve `Any` decode error (#712) * Improve Any decoding-error. Was: TypeError: Could not convert Any to PredictLongRunningResponse Now: TypeError: Could not convert Any[google.ai.generativelanguage.v1main.PredictLongRunningResponse] to google.ai.generativelanguage.v1beta.PredictLongRunningResponse * update test * update error message * address review feedback --------- Co-authored-by: Anthonios Partheniou --- google/api_core/protobuf_helpers.py | 4 +--- tests/unit/test_protobuf_helpers.py | 8 +++++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/google/api_core/protobuf_helpers.py b/google/api_core/protobuf_helpers.py index d777c5f89..30cd7c859 100644 --- a/google/api_core/protobuf_helpers.py +++ b/google/api_core/protobuf_helpers.py @@ -63,9 +63,7 @@ def from_any_pb(pb_type, any_pb): # Unpack the Any object and populate the protobuf message instance. if not any_pb.Unpack(msg_pb): raise TypeError( - "Could not convert {} to {}".format( - any_pb.__class__.__name__, pb_type.__name__ - ) + f"Could not convert `{any_pb.TypeName()}` with underlying type `google.protobuf.any_pb2.Any` to `{msg_pb.DESCRIPTOR.full_name}`" ) # Done; return the message. diff --git a/tests/unit/test_protobuf_helpers.py b/tests/unit/test_protobuf_helpers.py index 5b2c6dfdb..5678d3bc5 100644 --- a/tests/unit/test_protobuf_helpers.py +++ b/tests/unit/test_protobuf_helpers.py @@ -13,6 +13,7 @@ # limitations under the License. import pytest +import re from google.api import http_pb2 from google.api_core import protobuf_helpers @@ -65,7 +66,12 @@ def test_from_any_pb_failure(): in_message = any_pb2.Any() in_message.Pack(date_pb2.Date(year=1990)) - with pytest.raises(TypeError): + with pytest.raises( + TypeError, + match=re.escape( + "Could not convert `google.type.Date` with underlying type `google.protobuf.any_pb2.Any` to `google.type.TimeOfDay`" + ), + ): protobuf_helpers.from_any_pb(timeofday_pb2.TimeOfDay, in_message) From 77274058cec1aba8b06819458bd79488e7ca3972 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Tue, 22 Oct 2024 13:29:03 -0400 Subject: [PATCH 041/101] chore: Revert "chore: move retry async check to wrap time (#668)" (#743) This reverts commit 3c698679e544df571ed5e181db29b2d55eafbe2b. --- google/api_core/retry/retry_unary.py | 7 ++++--- tests/unit/retry/test_retry_unary.py | 16 +++++----------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/google/api_core/retry/retry_unary.py b/google/api_core/retry/retry_unary.py index 090431333..ab1b4030b 100644 --- a/google/api_core/retry/retry_unary.py +++ b/google/api_core/retry/retry_unary.py @@ -141,7 +141,10 @@ def retry_target( for sleep in sleep_generator: try: - return target() + result = target() + if inspect.isawaitable(result): + warnings.warn(_ASYNC_RETRY_WARNING) + return result # pylint: disable=broad-except # This function explicitly must deal with broad exceptions. @@ -277,8 +280,6 @@ def __call__( Callable: A callable that will invoke ``func`` with retry behavior. """ - if inspect.iscoroutinefunction(func): - warnings.warn(_ASYNC_RETRY_WARNING) if self._on_error is not None: on_error = self._on_error diff --git a/tests/unit/retry/test_retry_unary.py b/tests/unit/retry/test_retry_unary.py index d3359b15f..6851fbe48 100644 --- a/tests/unit/retry/test_retry_unary.py +++ b/tests/unit/retry/test_retry_unary.py @@ -105,20 +105,14 @@ def test_retry_target_non_retryable_error(utcnow, sleep): ) @pytest.mark.asyncio async def test_retry_target_warning_for_retry(utcnow, sleep): - """ - retry.Retry should raise warning when wrapping an async function. - """ - - async def target(): - pass # pragma: NO COVER - - retry_obj = retry.Retry() + predicate = retry.if_exception_type(ValueError) + target = mock.AsyncMock(spec=["__call__"]) with pytest.warns(Warning) as exc_info: - # raise warning when wrapping an async function - retry_obj(target) + # Note: predicate is just a filler and doesn't affect the test + retry.retry_target(target, predicate, range(10), None) - assert len(exc_info) == 1 + assert len(exc_info) == 2 assert str(exc_info[0].message) == retry.retry_unary._ASYNC_RETRY_WARNING sleep.assert_not_called() From a26313e1cb12e44aa498f12622edccc0c83ba0c3 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Wed, 23 Oct 2024 16:20:23 -0400 Subject: [PATCH 042/101] fix: require proto-plus >= 1.25.0 for Python 3.13 (#740) --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a15df5609..2a9b3b321 100644 --- a/setup.py +++ b/setup.py @@ -31,7 +31,8 @@ dependencies = [ "googleapis-common-protos >= 1.56.2, < 2.0.dev0", "protobuf>=3.19.5,<6.0.0.dev0,!=3.20.0,!=3.20.1,!=4.21.0,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5", - "proto-plus >= 1.22.3, <2.0.0dev", + "proto-plus >= 1.22.3, < 2.0.0dev", + "proto-plus >= 1.25.0, < 2.0.0dev; python_version >= '3.13'", "google-auth >= 2.14.1, < 3.0.dev0", "requests >= 2.18.0, < 3.0.0.dev0", ] From 7858ab4e389d926bb8c38b649d70c6648a713893 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Wed, 23 Oct 2024 17:06:53 -0400 Subject: [PATCH 043/101] chore: Revert "feat: add caching to GapicCallable (#666)" (#744) This reverts commit 169511959497bd4509305c5424532e817c6a9cb2. --- google/api_core/gapic_v1/method.py | 80 ++++++++++++++++-------------- tests/unit/gapic/test_method.py | 21 -------- 2 files changed, 44 insertions(+), 57 deletions(-) diff --git a/google/api_core/gapic_v1/method.py b/google/api_core/gapic_v1/method.py index c26342e4e..0f14ea9c3 100644 --- a/google/api_core/gapic_v1/method.py +++ b/google/api_core/gapic_v1/method.py @@ -42,6 +42,24 @@ class _MethodDefault(enum.Enum): so the default should be used.""" +def _is_not_none_or_false(value): + return value is not None and value is not False + + +def _apply_decorators(func, decorators): + """Apply a list of decorators to a given function. + + ``decorators`` may contain items that are ``None`` or ``False`` which will + be ignored. + """ + filtered_decorators = filter(_is_not_none_or_false, reversed(decorators)) + + for decorator in filtered_decorators: + func = decorator(func) + + return func + + class _GapicCallable(object): """Callable that applies retry, timeout, and metadata logic. @@ -73,53 +91,44 @@ def __init__( ): self._target = target self._retry = retry - if isinstance(timeout, (int, float)): - timeout = TimeToDeadlineTimeout(timeout=timeout) self._timeout = timeout self._compression = compression - self._metadata = list(metadata) if metadata is not None else None + self._metadata = metadata def __call__( self, *args, timeout=DEFAULT, retry=DEFAULT, compression=DEFAULT, **kwargs ): """Invoke the low-level RPC with retry, timeout, compression, and metadata.""" - if compression is DEFAULT: - compression = self._compression - if compression is not None: - kwargs["compression"] = compression + if retry is DEFAULT: + retry = self._retry - # Add the user agent metadata to the call. - if self._metadata is not None: - try: - # attempt to concatenate default metadata with user-provided metadata - kwargs["metadata"] = [*kwargs["metadata"], *self._metadata] - except (KeyError, TypeError): - # if metadata is not provided, use just the default metadata - kwargs["metadata"] = self._metadata - - call = self._build_wrapped_call(timeout, retry) - return call(*args, **kwargs) - - @functools.lru_cache(maxsize=4) - def _build_wrapped_call(self, timeout, retry): - """ - Build a wrapped callable that applies retry, timeout, and metadata logic. - """ - wrapped_func = self._target if timeout is DEFAULT: timeout = self._timeout - elif isinstance(timeout, (int, float)): + + if compression is DEFAULT: + compression = self._compression + + if isinstance(timeout, (int, float)): timeout = TimeToDeadlineTimeout(timeout=timeout) - if timeout is not None: - wrapped_func = timeout(wrapped_func) - if retry is DEFAULT: - retry = self._retry - if retry is not None: - wrapped_func = retry(wrapped_func) + # Apply all applicable decorators. + wrapped_func = _apply_decorators(self._target, [retry, timeout]) + + # Add the user agent metadata to the call. + if self._metadata is not None: + metadata = kwargs.get("metadata", []) + # Due to the nature of invocation, None should be treated the same + # as not specified. + if metadata is None: + metadata = [] + metadata = list(metadata) + metadata.extend(self._metadata) + kwargs["metadata"] = metadata + if self._compression is not None: + kwargs["compression"] = compression - return wrapped_func + return wrapped_func(*args, **kwargs) def wrap_method( @@ -193,9 +202,8 @@ def get_topic(name, timeout=None): Args: func (Callable[Any]): The function to wrap. It should accept an - optional ``timeout`` (google.api_core.timeout.Timeout) argument. - If ``metadata`` is not ``None``, it should accept a ``metadata`` - (Sequence[Tuple[str, str]]) argument. + optional ``timeout`` argument. If ``metadata`` is not ``None``, it + should accept a ``metadata`` argument. default_retry (Optional[google.api_core.Retry]): The default retry strategy. If ``None``, the method will not retry by default. default_timeout (Optional[google.api_core.Timeout]): The default diff --git a/tests/unit/gapic/test_method.py b/tests/unit/gapic/test_method.py index 8ada092bb..87aa63905 100644 --- a/tests/unit/gapic/test_method.py +++ b/tests/unit/gapic/test_method.py @@ -222,24 +222,3 @@ def test_wrap_method_with_call_not_supported(): with pytest.raises(ValueError) as exc_info: google.api_core.gapic_v1.method.wrap_method(method, with_call=True) assert "with_call=True is only supported for unary calls" in str(exc_info.value) - - -def test_benchmark_gapic_call(): - """ - Ensure the __call__ method performance does not regress from expectations - - __call__ builds a new wrapped function using passed-in timeout and retry, but - subsequent calls are cached - - Note: The threshold has been tuned for the CI workers. Test may flake on - slower hardware - """ - from google.api_core.gapic_v1.method import _GapicCallable - from google.api_core.retry import Retry - from timeit import timeit - - gapic_callable = _GapicCallable( - lambda *a, **k: 1, retry=Retry(), timeout=1010, compression=False - ) - avg_time = timeit(lambda: gapic_callable(), number=10_000) - assert avg_time < 0.4 From c87c1eaef71c259d890a6ff81c40afb6b0e5215f Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Fri, 25 Oct 2024 15:30:20 -0400 Subject: [PATCH 044/101] chore: update templated files (#746) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: update templated files * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: Owl Bot --- .github/.OwlBot.lock.yaml | 4 +- .kokoro/docs/common.cfg | 6 +-- .kokoro/release/common.cfg | 6 --- .kokoro/samples/python3.13/common.cfg | 40 ++++++++++++++++++++ .kokoro/samples/python3.13/continuous.cfg | 6 +++ .kokoro/samples/python3.13/periodic-head.cfg | 11 ++++++ .kokoro/samples/python3.13/periodic.cfg | 6 +++ .kokoro/samples/python3.13/presubmit.cfg | 6 +++ 8 files changed, 74 insertions(+), 11 deletions(-) create mode 100644 .kokoro/samples/python3.13/common.cfg create mode 100644 .kokoro/samples/python3.13/continuous.cfg create mode 100644 .kokoro/samples/python3.13/periodic-head.cfg create mode 100644 .kokoro/samples/python3.13/periodic.cfg create mode 100644 .kokoro/samples/python3.13/presubmit.cfg diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 95461aca7..13fc69ce9 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest - digest: sha256:56cd3192d58a72bcd10b031ff2917d75b6b3c2a39a2571c080f1d6c519d40330 -# created: 2024-10-19 + digest: sha256:5efdf8d38e5a22c1ec9e5541cbdfde56399bdffcb6f531183f84ac66052a8024 +# created: 2024-10-25 diff --git a/.kokoro/docs/common.cfg b/.kokoro/docs/common.cfg index 48e89855d..722f447aa 100644 --- a/.kokoro/docs/common.cfg +++ b/.kokoro/docs/common.cfg @@ -30,9 +30,9 @@ env_vars: { env_vars: { key: "V2_STAGING_BUCKET" - # Push non-cloud library docs to `docs-staging-v2-staging` instead of the + # Push non-cloud library docs to `docs-staging-v2-dev` instead of the # Cloud RAD bucket `docs-staging-v2` - value: "docs-staging-v2-staging" + value: "docs-staging-v2-dev" } # It will upload the docker image after successful builds. @@ -64,4 +64,4 @@ before_action { keyname: "docuploader_service_account" } } -} \ No newline at end of file +} diff --git a/.kokoro/release/common.cfg b/.kokoro/release/common.cfg index e2d6aab84..c856f396c 100644 --- a/.kokoro/release/common.cfg +++ b/.kokoro/release/common.cfg @@ -33,12 +33,6 @@ before_action { } } -# Tokens needed to report release status back to GitHub -env_vars: { - key: "SECRET_MANAGER_KEYS" - value: "releasetool-publish-reporter-app,releasetool-publish-reporter-googleapis-installation,releasetool-publish-reporter-pem" -} - # Store the packages we uploaded to PyPI. That way, we have a record of exactly # what we published, which we can use to generate SBOMs and attestations. action { diff --git a/.kokoro/samples/python3.13/common.cfg b/.kokoro/samples/python3.13/common.cfg new file mode 100644 index 000000000..2a4199f40 --- /dev/null +++ b/.kokoro/samples/python3.13/common.cfg @@ -0,0 +1,40 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Build logs will be here +action { + define_artifacts { + regex: "**/*sponge_log.xml" + } +} + +# Specify which tests to run +env_vars: { + key: "RUN_TESTS_SESSION" + value: "py-3.13" +} + +# Declare build specific Cloud project. +env_vars: { + key: "BUILD_SPECIFIC_GCLOUD_PROJECT" + value: "python-docs-samples-tests-313" +} + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/python-api-core/.kokoro/test-samples.sh" +} + +# Configure the docker image for kokoro-trampoline. +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/python-samples-testing-docker" +} + +# Download secrets for samples +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/python-docs-samples" + +# Download trampoline resources. +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" + +# Use the trampoline script to run in docker. +build_file: "python-api-core/.kokoro/trampoline_v2.sh" diff --git a/.kokoro/samples/python3.13/continuous.cfg b/.kokoro/samples/python3.13/continuous.cfg new file mode 100644 index 000000000..a1c8d9759 --- /dev/null +++ b/.kokoro/samples/python3.13/continuous.cfg @@ -0,0 +1,6 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "True" +} \ No newline at end of file diff --git a/.kokoro/samples/python3.13/periodic-head.cfg b/.kokoro/samples/python3.13/periodic-head.cfg new file mode 100644 index 000000000..a18c0cfc6 --- /dev/null +++ b/.kokoro/samples/python3.13/periodic-head.cfg @@ -0,0 +1,11 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "True" +} + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/python-api-core/.kokoro/test-samples-against-head.sh" +} diff --git a/.kokoro/samples/python3.13/periodic.cfg b/.kokoro/samples/python3.13/periodic.cfg new file mode 100644 index 000000000..71cd1e597 --- /dev/null +++ b/.kokoro/samples/python3.13/periodic.cfg @@ -0,0 +1,6 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "False" +} diff --git a/.kokoro/samples/python3.13/presubmit.cfg b/.kokoro/samples/python3.13/presubmit.cfg new file mode 100644 index 000000000..a1c8d9759 --- /dev/null +++ b/.kokoro/samples/python3.13/presubmit.cfg @@ -0,0 +1,6 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "True" +} \ No newline at end of file From e1b5a110157388a8f7ed2ecfb380afbef9a6cd57 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 28 Oct 2024 12:09:45 -0400 Subject: [PATCH 045/101] chore(main): release 2.22.0 (#729) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 15 +++++++++++++++ google/api_core/version.py | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06ad44d22..ea7f9e97d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,21 @@ [1]: https://pypi.org/project/google-api-core/#history +## [2.22.0](https://github.com/googleapis/python-api-core/compare/v2.21.0...v2.22.0) (2024-10-25) + + +### Features + +* Add support for python 3.13 ([#696](https://github.com/googleapis/python-api-core/issues/696)) ([46b3d3a](https://github.com/googleapis/python-api-core/commit/46b3d3abaa1bae28e9d788d7c3006224cd6f74d5)) + + +### Bug Fixes + +* Add type hints to ClientOptions ([#735](https://github.com/googleapis/python-api-core/issues/735)) ([b91ed19](https://github.com/googleapis/python-api-core/commit/b91ed19210148dfa49ec790c4dd5f4a7bff80954)) +* Improve `Any` decode error ([#712](https://github.com/googleapis/python-api-core/issues/712)) ([0d5ed37](https://github.com/googleapis/python-api-core/commit/0d5ed37c96f9b40bccae98e228163a88abeb1763)) +* Require proto-plus >= 1.25.0 for Python 3.13 ([#740](https://github.com/googleapis/python-api-core/issues/740)) ([a26313e](https://github.com/googleapis/python-api-core/commit/a26313e1cb12e44aa498f12622edccc0c83ba0c3)) +* Switch to unittest.mock from mock ([#713](https://github.com/googleapis/python-api-core/issues/713)) ([8c53381](https://github.com/googleapis/python-api-core/commit/8c533819b7e212aa2f1d695a7ce08629f4fb2daf)) + ## [2.21.0](https://github.com/googleapis/python-api-core/compare/v2.20.0...v2.21.0) (2024-10-07) diff --git a/google/api_core/version.py b/google/api_core/version.py index 563b0e160..2db0ca518 100644 --- a/google/api_core/version.py +++ b/google/api_core/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "2.21.0" +__version__ = "2.22.0" From 159e9a49525937f18a55c38136aae32575424d55 Mon Sep 17 00:00:00 2001 From: Rin Arakaki Date: Tue, 29 Oct 2024 01:12:51 +0900 Subject: [PATCH 046/101] feat: migrate to pyproject.toml (#736) * migrate to pyproject.toml * remove version * remove readme_filename * remove long_description * dynamic version * move description * move urls * move packages * zip_safe is deprecated * include-package-data = true by default * platforms are ignored * Add copyright notice * proto-plus when python_version >= '3.13' * remove mypy.ini * remove pytest.ini --------- Co-authored-by: Anthonios Partheniou --- mypy.ini | 4 -- pyproject.toml | 107 +++++++++++++++++++++++++++++++++++++++++++++++++ pytest.ini | 23 ----------- setup.py | 92 +----------------------------------------- 4 files changed, 108 insertions(+), 118 deletions(-) delete mode 100644 mypy.ini create mode 100644 pyproject.toml delete mode 100644 pytest.ini diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index ce33582a6..000000000 --- a/mypy.ini +++ /dev/null @@ -1,4 +0,0 @@ -[mypy] -python_version = 3.7 -namespace_packages = True -ignore_missing_imports = True diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..fda8f01bf --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,107 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project] +name = "google-api-core" +authors = [{ name = "Google LLC", email = "googleapis-packages@google.com" }] +license = { text = "Apache 2.0" } +requires-python = ">=3.7" +readme = "README.rst" +description = "Google API client core library" +classifiers = [ + # Should be one of: + # "Development Status :: 3 - Alpha" + # "Development Status :: 4 - Beta" + # "Development Status :: 5 - Production/Stable" + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Operating System :: OS Independent", + "Topic :: Internet", +] +dependencies = [ + "googleapis-common-protos >= 1.56.2, < 2.0.dev0", + "protobuf >= 3.19.5, < 6.0.0.dev0, != 3.20.0, != 3.20.1, != 4.21.0, != 4.21.1, != 4.21.2, != 4.21.3, != 4.21.4, != 4.21.5", + "proto-plus >= 1.22.3, < 2.0.0dev", + "proto-plus >= 1.25.0, < 2.0.0dev; python_version >= '3.13'", + "google-auth >= 2.14.1, < 3.0.dev0", + "requests >= 2.18.0, < 3.0.0.dev0", +] +dynamic = ["version"] + +[project.urls] +Documentation = "https://googleapis.dev/python/google-api-core/latest/" +Repository = "https://github.com/googleapis/python-api-core" + +[project.optional-dependencies] +async_rest = ["google-auth[aiohttp] >= 2.35.0, < 3.0.dev0"] +grpc = [ + "grpcio >= 1.33.2, < 2.0dev", + "grpcio >= 1.49.1, < 2.0dev; python_version >= '3.11'", + "grpcio-status >= 1.33.2, < 2.0.dev0", + "grpcio-status >= 1.49.1, < 2.0.dev0; python_version >= '3.11'", +] +grpcgcp = ["grpcio-gcp >= 0.2.2, < 1.0.dev0"] +grpcio-gcp = ["grpcio-gcp >= 0.2.2, < 1.0.dev0"] + +[tool.setuptools.dynamic] +version = { attr = "google.api_core.version.__version__" } + +[tool.setuptools.packages.find] +# Only include packages under the 'google' namespace. Do not include tests, +# benchmarks, etc. +include = ["google*"] + +[tool.mypy] +python_version = "3.7" +namespace_packages = true +ignore_missing_imports = true + +[tool.pytest] +filterwarnings = [ + # treat all warnings as errors + "error", + # Remove once https://github.com/pytest-dev/pytest-cov/issues/621 is fixed + "ignore:.*The --rsyncdir command line argument and rsyncdirs config variable are deprecated:DeprecationWarning", + # Remove once https://github.com/protocolbuffers/protobuf/issues/12186 is fixed + "ignore:.*custom tp_new.*in Python 3.14:DeprecationWarning", + # Remove once support for python 3.7 is dropped + # This warning only appears when using python 3.7 + "ignore:.*Using or importing the ABCs from.*collections:DeprecationWarning", + # Remove once support for grpcio-gcp is deprecated + # See https://github.com/googleapis/python-api-core/blob/42e8b6e6f426cab749b34906529e8aaf3f133d75/google/api_core/grpc_helpers.py#L39-L45 + "ignore:.*Support for grpcio-gcp is deprecated:DeprecationWarning", + "ignore: The `compression` argument is ignored for grpc_gcp.secure_channel creation:DeprecationWarning", + "ignore:The `attempt_direct_path` argument is ignored for grpc_gcp.secure_channel creation:DeprecationWarning", + # Remove once the minimum supported version of googleapis-common-protos is 1.62.0 + "ignore:.*pkg_resources.declare_namespace:DeprecationWarning", + "ignore:.*pkg_resources is deprecated as an API:DeprecationWarning", + # Remove once https://github.com/grpc/grpc/issues/35086 is fixed (and version newer than 1.60.0 is published) + "ignore:There is no current event loop:DeprecationWarning", + # Remove after support for Python 3.7 is dropped + "ignore:After January 1, 2024, new releases of this library will drop support for Python 3.7:DeprecationWarning", +] diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index 696548cfc..000000000 --- a/pytest.ini +++ /dev/null @@ -1,23 +0,0 @@ -[pytest] -filterwarnings = - # treat all warnings as errors - error - # Remove once https://github.com/pytest-dev/pytest-cov/issues/621 is fixed - ignore:.*The --rsyncdir command line argument and rsyncdirs config variable are deprecated:DeprecationWarning - # Remove once https://github.com/protocolbuffers/protobuf/issues/12186 is fixed - ignore:.*custom tp_new.*in Python 3.14:DeprecationWarning - # Remove once support for python 3.7 is dropped - # This warning only appears when using python 3.7 - ignore:.*Using or importing the ABCs from.*collections:DeprecationWarning - # Remove once support for grpcio-gcp is deprecated - # See https://github.com/googleapis/python-api-core/blob/42e8b6e6f426cab749b34906529e8aaf3f133d75/google/api_core/grpc_helpers.py#L39-L45 - ignore:.*Support for grpcio-gcp is deprecated:DeprecationWarning - ignore: The `compression` argument is ignored for grpc_gcp.secure_channel creation:DeprecationWarning - ignore:The `attempt_direct_path` argument is ignored for grpc_gcp.secure_channel creation:DeprecationWarning - # Remove once the minimum supported version of googleapis-common-protos is 1.62.0 - ignore:.*pkg_resources.declare_namespace:DeprecationWarning - ignore:.*pkg_resources is deprecated as an API:DeprecationWarning - # Remove once https://github.com/grpc/grpc/issues/35086 is fixed (and version newer than 1.60.0 is published) - ignore:There is no current event loop:DeprecationWarning - # Remove after support for Python 3.7 is dropped - ignore:After January 1, 2024, new releases of this library will drop support for Python 3.7:DeprecationWarning diff --git a/setup.py b/setup.py index 2a9b3b321..168877fa5 100644 --- a/setup.py +++ b/setup.py @@ -12,97 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -import io -import os - import setuptools -# Package metadata. - -name = "google-api-core" -description = "Google API client core library" - -# Should be one of: -# 'Development Status :: 3 - Alpha' -# 'Development Status :: 4 - Beta' -# 'Development Status :: 5 - Production/Stable' -release_status = "Development Status :: 5 - Production/Stable" -dependencies = [ - "googleapis-common-protos >= 1.56.2, < 2.0.dev0", - "protobuf>=3.19.5,<6.0.0.dev0,!=3.20.0,!=3.20.1,!=4.21.0,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5", - "proto-plus >= 1.22.3, < 2.0.0dev", - "proto-plus >= 1.25.0, < 2.0.0dev; python_version >= '3.13'", - "google-auth >= 2.14.1, < 3.0.dev0", - "requests >= 2.18.0, < 3.0.0.dev0", -] -extras = { - "async_rest": [ - "google-auth[aiohttp] >= 2.35.0, < 3.0.dev0", - ], - "grpc": [ - "grpcio >= 1.33.2, < 2.0dev", - "grpcio >= 1.49.1, < 2.0dev; python_version>='3.11'", - "grpcio-status >= 1.33.2, < 2.0.dev0", - "grpcio-status >= 1.49.1, < 2.0.dev0; python_version>='3.11'", - ], - "grpcgcp": "grpcio-gcp >= 0.2.2, < 1.0.dev0", - "grpcio-gcp": "grpcio-gcp >= 0.2.2, < 1.0.dev0", -} - - -# Setup boilerplate below this line. - -package_root = os.path.abspath(os.path.dirname(__file__)) - - -version = {} -with open(os.path.join(package_root, "google/api_core/version.py")) as fp: - exec(fp.read(), version) -version = version["__version__"] - -readme_filename = os.path.join(package_root, "README.rst") -with io.open(readme_filename, encoding="utf-8") as readme_file: - readme = readme_file.read() - -# Only include packages under the 'google' namespace. Do not include tests, -# benchmarks, etc. -packages = [ - package - for package in setuptools.find_namespace_packages() - if package.startswith("google") -] - -setuptools.setup( - name=name, - version=version, - description=description, - long_description=readme, - author="Google LLC", - author_email="googleapis-packages@google.com", - license="Apache 2.0", - url="https://github.com/googleapis/python-api-core", - classifiers=[ - release_status, - "Intended Audience :: Developers", - "License :: OSI Approved :: Apache Software License", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Operating System :: OS Independent", - "Topic :: Internet", - ], - platforms="Posix; MacOS X; Windows", - packages=packages, - install_requires=dependencies, - extras_require=extras, - python_requires=">=3.7", - include_package_data=True, - zip_safe=False, -) +setuptools.setup() From 9370c7aa9da66cd9e83209e6d73bcc48046893ab Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Mon, 11 Nov 2024 12:40:09 -0500 Subject: [PATCH 047/101] chore(python): remove obsolete release scripts and config files (#755) * chore(python): remove obsolete release scripts and config files Source-Link: https://github.com/googleapis/synthtool/commit/635751753776b1a7cabd4dcaa48013a96274372d Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-python:latest@sha256:91d0075c6f2fd6a073a06168feee19fa2a8507692f2519a1dc7de3366d157e99 * See https://github.com/googleapis/synthtool/pull/2035 --------- Co-authored-by: Owl Bot Co-authored-by: Anthonios Partheniou --- .github/.OwlBot.lock.yaml | 4 +- .github/release-trigger.yml | 1 + .kokoro/release.sh | 29 -- .kokoro/release/common.cfg | 43 --- .kokoro/release/release.cfg | 1 - .kokoro/requirements.in | 11 - .kokoro/requirements.txt | 509 ----------------------------------- .kokoro/test-samples-impl.sh | 3 +- 8 files changed, 5 insertions(+), 596 deletions(-) delete mode 100755 .kokoro/release.sh delete mode 100644 .kokoro/release/common.cfg delete mode 100644 .kokoro/release/release.cfg delete mode 100644 .kokoro/requirements.in delete mode 100644 .kokoro/requirements.txt diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 13fc69ce9..b2770d4e0 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest - digest: sha256:5efdf8d38e5a22c1ec9e5541cbdfde56399bdffcb6f531183f84ac66052a8024 -# created: 2024-10-25 + digest: sha256:91d0075c6f2fd6a073a06168feee19fa2a8507692f2519a1dc7de3366d157e99 +# created: 2024-11-11T16:13:09.302418532Z diff --git a/.github/release-trigger.yml b/.github/release-trigger.yml index d4ca94189..50e8bd300 100644 --- a/.github/release-trigger.yml +++ b/.github/release-trigger.yml @@ -1 +1,2 @@ enabled: true +multiScmName: python-api-core diff --git a/.kokoro/release.sh b/.kokoro/release.sh deleted file mode 100755 index 62dc059bb..000000000 --- a/.kokoro/release.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/bash -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -set -eo pipefail - -# Start the releasetool reporter -python3 -m pip install --require-hashes -r github/python-api-core/.kokoro/requirements.txt -python3 -m releasetool publish-reporter-script > /tmp/publisher-script; source /tmp/publisher-script - -# Disable buffering, so that the logs stream through. -export PYTHONUNBUFFERED=1 - -# Move into the package, build the distribution and upload. -TWINE_PASSWORD=$(cat "${KOKORO_KEYSTORE_DIR}/73713_google-cloud-pypi-token-keystore-3") -cd github/python-api-core -python3 setup.py sdist bdist_wheel -twine upload --username __token__ --password "${TWINE_PASSWORD}" dist/* diff --git a/.kokoro/release/common.cfg b/.kokoro/release/common.cfg deleted file mode 100644 index c856f396c..000000000 --- a/.kokoro/release/common.cfg +++ /dev/null @@ -1,43 +0,0 @@ -# Format: //devtools/kokoro/config/proto/build.proto - -# Build logs will be here -action { - define_artifacts { - regex: "**/*sponge_log.xml" - } -} - -# Download trampoline resources. -gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" - -# Use the trampoline script to run in docker. -build_file: "python-api-core/.kokoro/trampoline.sh" - -# Configure the docker image for kokoro-trampoline. -env_vars: { - key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/python-multi" -} -env_vars: { - key: "TRAMPOLINE_BUILD_FILE" - value: "github/python-api-core/.kokoro/release.sh" -} - -# Fetch PyPI password -before_action { - fetch_keystore { - keystore_resource { - keystore_config_id: 73713 - keyname: "google-cloud-pypi-token-keystore-3" - } - } -} - -# Store the packages we uploaded to PyPI. That way, we have a record of exactly -# what we published, which we can use to generate SBOMs and attestations. -action { - define_artifacts { - regex: "github/python-api-core/**/*.tar.gz" - strip_prefix: "github/python-api-core" - } -} diff --git a/.kokoro/release/release.cfg b/.kokoro/release/release.cfg deleted file mode 100644 index 8f43917d9..000000000 --- a/.kokoro/release/release.cfg +++ /dev/null @@ -1 +0,0 @@ -# Format: //devtools/kokoro/config/proto/build.proto \ No newline at end of file diff --git a/.kokoro/requirements.in b/.kokoro/requirements.in deleted file mode 100644 index fff4d9ce0..000000000 --- a/.kokoro/requirements.in +++ /dev/null @@ -1,11 +0,0 @@ -gcp-docuploader -gcp-releasetool>=2 # required for compatibility with cryptography>=42.x -importlib-metadata -typing-extensions -twine -wheel -setuptools -nox>=2022.11.21 # required to remove dependency on py -charset-normalizer<3 -click<8.1.0 -cryptography>=42.0.5 diff --git a/.kokoro/requirements.txt b/.kokoro/requirements.txt deleted file mode 100644 index 006d8ef93..000000000 --- a/.kokoro/requirements.txt +++ /dev/null @@ -1,509 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.9 -# by the following command: -# -# pip-compile --allow-unsafe --generate-hashes requirements.in -# -argcomplete==3.5.1 \ - --hash=sha256:1a1d148bdaa3e3b93454900163403df41448a248af01b6e849edc5ac08e6c363 \ - --hash=sha256:eb1ee355aa2557bd3d0145de7b06b2a45b0ce461e1e7813f5d066039ab4177b4 - # via nox -attrs==24.2.0 \ - --hash=sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346 \ - --hash=sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2 - # via gcp-releasetool -backports-tarfile==1.2.0 \ - --hash=sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34 \ - --hash=sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991 - # via jaraco-context -cachetools==5.5.0 \ - --hash=sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292 \ - --hash=sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a - # via google-auth -certifi==2024.8.30 \ - --hash=sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8 \ - --hash=sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9 - # via requests -cffi==1.17.1 \ - --hash=sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8 \ - --hash=sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2 \ - --hash=sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1 \ - --hash=sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15 \ - --hash=sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36 \ - --hash=sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824 \ - --hash=sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8 \ - --hash=sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36 \ - --hash=sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17 \ - --hash=sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf \ - --hash=sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc \ - --hash=sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3 \ - --hash=sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed \ - --hash=sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702 \ - --hash=sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1 \ - --hash=sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8 \ - --hash=sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903 \ - --hash=sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6 \ - --hash=sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d \ - --hash=sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b \ - --hash=sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e \ - --hash=sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be \ - --hash=sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c \ - --hash=sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683 \ - --hash=sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9 \ - --hash=sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c \ - --hash=sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8 \ - --hash=sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1 \ - --hash=sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4 \ - --hash=sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655 \ - --hash=sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67 \ - --hash=sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595 \ - --hash=sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0 \ - --hash=sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65 \ - --hash=sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41 \ - --hash=sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6 \ - --hash=sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401 \ - --hash=sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6 \ - --hash=sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3 \ - --hash=sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16 \ - --hash=sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93 \ - --hash=sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e \ - --hash=sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4 \ - --hash=sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964 \ - --hash=sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c \ - --hash=sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576 \ - --hash=sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0 \ - --hash=sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3 \ - --hash=sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662 \ - --hash=sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3 \ - --hash=sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff \ - --hash=sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5 \ - --hash=sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd \ - --hash=sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f \ - --hash=sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5 \ - --hash=sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14 \ - --hash=sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d \ - --hash=sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9 \ - --hash=sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7 \ - --hash=sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382 \ - --hash=sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a \ - --hash=sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e \ - --hash=sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a \ - --hash=sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4 \ - --hash=sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99 \ - --hash=sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87 \ - --hash=sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b - # via cryptography -charset-normalizer==2.1.1 \ - --hash=sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845 \ - --hash=sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f - # via - # -r requirements.in - # requests -click==8.0.4 \ - --hash=sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1 \ - --hash=sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb - # via - # -r requirements.in - # gcp-docuploader - # gcp-releasetool -colorlog==6.8.2 \ - --hash=sha256:3e3e079a41feb5a1b64f978b5ea4f46040a94f11f0e8bbb8261e3dbbeca64d44 \ - --hash=sha256:4dcbb62368e2800cb3c5abd348da7e53f6c362dda502ec27c560b2e58a66bd33 - # via - # gcp-docuploader - # nox -cryptography==43.0.1 \ - --hash=sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494 \ - --hash=sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806 \ - --hash=sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d \ - --hash=sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062 \ - --hash=sha256:2bd51274dcd59f09dd952afb696bf9c61a7a49dfc764c04dd33ef7a6b502a1e2 \ - --hash=sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4 \ - --hash=sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1 \ - --hash=sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85 \ - --hash=sha256:5b43d1ea6b378b54a1dc99dd8a2b5be47658fe9a7ce0a58ff0b55f4b43ef2b84 \ - --hash=sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042 \ - --hash=sha256:666ae11966643886c2987b3b721899d250855718d6d9ce41b521252a17985f4d \ - --hash=sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962 \ - --hash=sha256:7c05650fe8023c5ed0d46793d4b7d7e6cd9c04e68eabe5b0aeea836e37bdcec2 \ - --hash=sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa \ - --hash=sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d \ - --hash=sha256:88cce104c36870d70c49c7c8fd22885875d950d9ee6ab54df2745f83ba0dc365 \ - --hash=sha256:9d3cdb25fa98afdd3d0892d132b8d7139e2c087da1712041f6b762e4f807cc96 \ - --hash=sha256:a575913fb06e05e6b4b814d7f7468c2c660e8bb16d8d5a1faf9b33ccc569dd47 \ - --hash=sha256:ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d \ - --hash=sha256:c1332724be35d23a854994ff0b66530119500b6053d0bd3363265f7e5e77288d \ - --hash=sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c \ - --hash=sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb \ - --hash=sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277 \ - --hash=sha256:e710bf40870f4db63c3d7d929aa9e09e4e7ee219e703f949ec4073b4294f6172 \ - --hash=sha256:ea25acb556320250756e53f9e20a4177515f012c9eaea17eb7587a8c4d8ae034 \ - --hash=sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a \ - --hash=sha256:fba1007b3ef89946dbbb515aeeb41e30203b004f0b4b00e5e16078b518563289 - # via - # -r requirements.in - # gcp-releasetool - # secretstorage -distlib==0.3.9 \ - --hash=sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87 \ - --hash=sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403 - # via virtualenv -docutils==0.21.2 \ - --hash=sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f \ - --hash=sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2 - # via readme-renderer -filelock==3.16.1 \ - --hash=sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0 \ - --hash=sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435 - # via virtualenv -gcp-docuploader==0.6.5 \ - --hash=sha256:30221d4ac3e5a2b9c69aa52fdbef68cc3f27d0e6d0d90e220fc024584b8d2318 \ - --hash=sha256:b7458ef93f605b9d46a4bf3a8dc1755dad1f31d030c8679edf304e343b347eea - # via -r requirements.in -gcp-releasetool==2.1.1 \ - --hash=sha256:25639269f4eae510094f9dbed9894977e1966933211eb155a451deebc3fc0b30 \ - --hash=sha256:845f4ded3d9bfe8cc7fdaad789e83f4ea014affa77785259a7ddac4b243e099e - # via -r requirements.in -google-api-core==2.21.0 \ - --hash=sha256:4a152fd11a9f774ea606388d423b68aa7e6d6a0ffe4c8266f74979613ec09f81 \ - --hash=sha256:6869eacb2a37720380ba5898312af79a4d30b8bca1548fb4093e0697dc4bdf5d - # via - # google-cloud-core - # google-cloud-storage -google-auth==2.35.0 \ - --hash=sha256:25df55f327ef021de8be50bad0dfd4a916ad0de96da86cd05661c9297723ad3f \ - --hash=sha256:f4c64ed4e01e8e8b646ef34c018f8bf3338df0c8e37d8b3bba40e7f574a3278a - # via - # gcp-releasetool - # google-api-core - # google-cloud-core - # google-cloud-storage -google-cloud-core==2.4.1 \ - --hash=sha256:9b7749272a812bde58fff28868d0c5e2f585b82f37e09a1f6ed2d4d10f134073 \ - --hash=sha256:a9e6a4422b9ac5c29f79a0ede9485473338e2ce78d91f2370c01e730eab22e61 - # via google-cloud-storage -google-cloud-storage==2.18.2 \ - --hash=sha256:97a4d45c368b7d401ed48c4fdfe86e1e1cb96401c9e199e419d289e2c0370166 \ - --hash=sha256:aaf7acd70cdad9f274d29332673fcab98708d0e1f4dceb5a5356aaef06af4d99 - # via gcp-docuploader -google-crc32c==1.6.0 \ - --hash=sha256:05e2d8c9a2f853ff116db9706b4a27350587f341eda835f46db3c0a8c8ce2f24 \ - --hash=sha256:18e311c64008f1f1379158158bb3f0c8d72635b9eb4f9545f8cf990c5668e59d \ - --hash=sha256:236c87a46cdf06384f614e9092b82c05f81bd34b80248021f729396a78e55d7e \ - --hash=sha256:35834855408429cecf495cac67ccbab802de269e948e27478b1e47dfb6465e57 \ - --hash=sha256:386122eeaaa76951a8196310432c5b0ef3b53590ef4c317ec7588ec554fec5d2 \ - --hash=sha256:40b05ab32a5067525670880eb5d169529089a26fe35dce8891127aeddc1950e8 \ - --hash=sha256:48abd62ca76a2cbe034542ed1b6aee851b6f28aaca4e6551b5599b6f3ef175cc \ - --hash=sha256:50cf2a96da226dcbff8671233ecf37bf6e95de98b2a2ebadbfdf455e6d05df42 \ - --hash=sha256:51c4f54dd8c6dfeb58d1df5e4f7f97df8abf17a36626a217f169893d1d7f3e9f \ - --hash=sha256:5bcc90b34df28a4b38653c36bb5ada35671ad105c99cfe915fb5bed7ad6924aa \ - --hash=sha256:62f6d4a29fea082ac4a3c9be5e415218255cf11684ac6ef5488eea0c9132689b \ - --hash=sha256:6eceb6ad197656a1ff49ebfbbfa870678c75be4344feb35ac1edf694309413dc \ - --hash=sha256:7aec8e88a3583515f9e0957fe4f5f6d8d4997e36d0f61624e70469771584c760 \ - --hash=sha256:91ca8145b060679ec9176e6de4f89b07363d6805bd4760631ef254905503598d \ - --hash=sha256:a184243544811e4a50d345838a883733461e67578959ac59964e43cca2c791e7 \ - --hash=sha256:a9e4b426c3702f3cd23b933436487eb34e01e00327fac20c9aebb68ccf34117d \ - --hash=sha256:bb0966e1c50d0ef5bc743312cc730b533491d60585a9a08f897274e57c3f70e0 \ - --hash=sha256:bb8b3c75bd157010459b15222c3fd30577042a7060e29d42dabce449c087f2b3 \ - --hash=sha256:bd5e7d2445d1a958c266bfa5d04c39932dc54093fa391736dbfdb0f1929c1fb3 \ - --hash=sha256:c87d98c7c4a69066fd31701c4e10d178a648c2cac3452e62c6b24dc51f9fcc00 \ - --hash=sha256:d2952396dc604544ea7476b33fe87faedc24d666fb0c2d5ac971a2b9576ab871 \ - --hash=sha256:d8797406499f28b5ef791f339594b0b5fdedf54e203b5066675c406ba69d705c \ - --hash=sha256:d9e9913f7bd69e093b81da4535ce27af842e7bf371cde42d1ae9e9bd382dc0e9 \ - --hash=sha256:e2806553238cd076f0a55bddab37a532b53580e699ed8e5606d0de1f856b5205 \ - --hash=sha256:ebab974b1687509e5c973b5c4b8b146683e101e102e17a86bd196ecaa4d099fc \ - --hash=sha256:ed767bf4ba90104c1216b68111613f0d5926fb3780660ea1198fc469af410e9d \ - --hash=sha256:f7a1fc29803712f80879b0806cb83ab24ce62fc8daf0569f2204a0cfd7f68ed4 - # via - # google-cloud-storage - # google-resumable-media -google-resumable-media==2.7.2 \ - --hash=sha256:3ce7551e9fe6d99e9a126101d2536612bb73486721951e9562fee0f90c6ababa \ - --hash=sha256:5280aed4629f2b60b847b0d42f9857fd4935c11af266744df33d8074cae92fe0 - # via google-cloud-storage -googleapis-common-protos==1.65.0 \ - --hash=sha256:2972e6c496f435b92590fd54045060867f3fe9be2c82ab148fc8885035479a63 \ - --hash=sha256:334a29d07cddc3aa01dee4988f9afd9b2916ee2ff49d6b757155dc0d197852c0 - # via google-api-core -idna==3.10 \ - --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \ - --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3 - # via requests -importlib-metadata==8.5.0 \ - --hash=sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b \ - --hash=sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7 - # via - # -r requirements.in - # keyring - # twine -jaraco-classes==3.4.0 \ - --hash=sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd \ - --hash=sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790 - # via keyring -jaraco-context==6.0.1 \ - --hash=sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3 \ - --hash=sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4 - # via keyring -jaraco-functools==4.1.0 \ - --hash=sha256:70f7e0e2ae076498e212562325e805204fc092d7b4c17e0e86c959e249701a9d \ - --hash=sha256:ad159f13428bc4acbf5541ad6dec511f91573b90fba04df61dafa2a1231cf649 - # via keyring -jeepney==0.8.0 \ - --hash=sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806 \ - --hash=sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755 - # via - # keyring - # secretstorage -jinja2==3.1.4 \ - --hash=sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369 \ - --hash=sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d - # via gcp-releasetool -keyring==25.4.1 \ - --hash=sha256:5426f817cf7f6f007ba5ec722b1bcad95a75b27d780343772ad76b17cb47b0bf \ - --hash=sha256:b07ebc55f3e8ed86ac81dd31ef14e81ace9dd9c3d4b5d77a6e9a2016d0d71a1b - # via - # gcp-releasetool - # twine -markdown-it-py==3.0.0 \ - --hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \ - --hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb - # via rich -markupsafe==3.0.1 \ - --hash=sha256:0778de17cff1acaeccc3ff30cd99a3fd5c50fc58ad3d6c0e0c4c58092b859396 \ - --hash=sha256:0f84af7e813784feb4d5e4ff7db633aba6c8ca64a833f61d8e4eade234ef0c38 \ - --hash=sha256:17b2aea42a7280db02ac644db1d634ad47dcc96faf38ab304fe26ba2680d359a \ - --hash=sha256:242d6860f1fd9191aef5fae22b51c5c19767f93fb9ead4d21924e0bcb17619d8 \ - --hash=sha256:244dbe463d5fb6d7ce161301a03a6fe744dac9072328ba9fc82289238582697b \ - --hash=sha256:26627785a54a947f6d7336ce5963569b5d75614619e75193bdb4e06e21d447ad \ - --hash=sha256:2a4b34a8d14649315c4bc26bbfa352663eb51d146e35eef231dd739d54a5430a \ - --hash=sha256:2ae99f31f47d849758a687102afdd05bd3d3ff7dbab0a8f1587981b58a76152a \ - --hash=sha256:312387403cd40699ab91d50735ea7a507b788091c416dd007eac54434aee51da \ - --hash=sha256:3341c043c37d78cc5ae6e3e305e988532b072329639007fd408a476642a89fd6 \ - --hash=sha256:33d1c36b90e570ba7785dacd1faaf091203d9942bc036118fab8110a401eb1a8 \ - --hash=sha256:3e683ee4f5d0fa2dde4db77ed8dd8a876686e3fc417655c2ece9a90576905344 \ - --hash=sha256:3ffb4a8e7d46ed96ae48805746755fadd0909fea2306f93d5d8233ba23dda12a \ - --hash=sha256:40621d60d0e58aa573b68ac5e2d6b20d44392878e0bfc159012a5787c4e35bc8 \ - --hash=sha256:40f1e10d51c92859765522cbd79c5c8989f40f0419614bcdc5015e7b6bf97fc5 \ - --hash=sha256:45d42d132cff577c92bfba536aefcfea7e26efb975bd455db4e6602f5c9f45e7 \ - --hash=sha256:48488d999ed50ba8d38c581d67e496f955821dc183883550a6fbc7f1aefdc170 \ - --hash=sha256:4935dd7883f1d50e2ffecca0aa33dc1946a94c8f3fdafb8df5c330e48f71b132 \ - --hash=sha256:4c2d64fdba74ad16138300815cfdc6ab2f4647e23ced81f59e940d7d4a1469d9 \ - --hash=sha256:4c8817557d0de9349109acb38b9dd570b03cc5014e8aabf1cbddc6e81005becd \ - --hash=sha256:4ffaaac913c3f7345579db4f33b0020db693f302ca5137f106060316761beea9 \ - --hash=sha256:5a4cb365cb49b750bdb60b846b0c0bc49ed62e59a76635095a179d440540c346 \ - --hash=sha256:62fada2c942702ef8952754abfc1a9f7658a4d5460fabe95ac7ec2cbe0d02abc \ - --hash=sha256:67c519635a4f64e495c50e3107d9b4075aec33634272b5db1cde839e07367589 \ - --hash=sha256:6a54c43d3ec4cf2a39f4387ad044221c66a376e58c0d0e971d47c475ba79c6b5 \ - --hash=sha256:7044312a928a66a4c2a22644147bc61a199c1709712069a344a3fb5cfcf16915 \ - --hash=sha256:730d86af59e0e43ce277bb83970530dd223bf7f2a838e086b50affa6ec5f9295 \ - --hash=sha256:800100d45176652ded796134277ecb13640c1a537cad3b8b53da45aa96330453 \ - --hash=sha256:80fcbf3add8790caddfab6764bde258b5d09aefbe9169c183f88a7410f0f6dea \ - --hash=sha256:82b5dba6eb1bcc29cc305a18a3c5365d2af06ee71b123216416f7e20d2a84e5b \ - --hash=sha256:852dc840f6d7c985603e60b5deaae1d89c56cb038b577f6b5b8c808c97580f1d \ - --hash=sha256:8ad4ad1429cd4f315f32ef263c1342166695fad76c100c5d979c45d5570ed58b \ - --hash=sha256:8ae369e84466aa70f3154ee23c1451fda10a8ee1b63923ce76667e3077f2b0c4 \ - --hash=sha256:93e8248d650e7e9d49e8251f883eed60ecbc0e8ffd6349e18550925e31bd029b \ - --hash=sha256:973a371a55ce9ed333a3a0f8e0bcfae9e0d637711534bcb11e130af2ab9334e7 \ - --hash=sha256:9ba25a71ebf05b9bb0e2ae99f8bc08a07ee8e98c612175087112656ca0f5c8bf \ - --hash=sha256:a10860e00ded1dd0a65b83e717af28845bb7bd16d8ace40fe5531491de76b79f \ - --hash=sha256:a4792d3b3a6dfafefdf8e937f14906a51bd27025a36f4b188728a73382231d91 \ - --hash=sha256:a7420ceda262dbb4b8d839a4ec63d61c261e4e77677ed7c66c99f4e7cb5030dd \ - --hash=sha256:ad91738f14eb8da0ff82f2acd0098b6257621410dcbd4df20aaa5b4233d75a50 \ - --hash=sha256:b6a387d61fe41cdf7ea95b38e9af11cfb1a63499af2759444b99185c4ab33f5b \ - --hash=sha256:b954093679d5750495725ea6f88409946d69cfb25ea7b4c846eef5044194f583 \ - --hash=sha256:bbde71a705f8e9e4c3e9e33db69341d040c827c7afa6789b14c6e16776074f5a \ - --hash=sha256:beeebf760a9c1f4c07ef6a53465e8cfa776ea6a2021eda0d0417ec41043fe984 \ - --hash=sha256:c91b394f7601438ff79a4b93d16be92f216adb57d813a78be4446fe0f6bc2d8c \ - --hash=sha256:c97ff7fedf56d86bae92fa0a646ce1a0ec7509a7578e1ed238731ba13aabcd1c \ - --hash=sha256:cb53e2a99df28eee3b5f4fea166020d3ef9116fdc5764bc5117486e6d1211b25 \ - --hash=sha256:cbf445eb5628981a80f54087f9acdbf84f9b7d862756110d172993b9a5ae81aa \ - --hash=sha256:d06b24c686a34c86c8c1fba923181eae6b10565e4d80bdd7bc1c8e2f11247aa4 \ - --hash=sha256:d98e66a24497637dd31ccab090b34392dddb1f2f811c4b4cd80c230205c074a3 \ - --hash=sha256:db15ce28e1e127a0013dfb8ac243a8e392db8c61eae113337536edb28bdc1f97 \ - --hash=sha256:db842712984e91707437461930e6011e60b39136c7331e971952bb30465bc1a1 \ - --hash=sha256:e24bfe89c6ac4c31792793ad9f861b8f6dc4546ac6dc8f1c9083c7c4f2b335cd \ - --hash=sha256:e81c52638315ff4ac1b533d427f50bc0afc746deb949210bc85f05d4f15fd772 \ - --hash=sha256:e9393357f19954248b00bed7c56f29a25c930593a77630c719653d51e7669c2a \ - --hash=sha256:ee3941769bd2522fe39222206f6dd97ae83c442a94c90f2b7a25d847d40f4729 \ - --hash=sha256:f31ae06f1328595d762c9a2bf29dafd8621c7d3adc130cbb46278079758779ca \ - --hash=sha256:f94190df587738280d544971500b9cafc9b950d32efcb1fba9ac10d84e6aa4e6 \ - --hash=sha256:fa7d686ed9883f3d664d39d5a8e74d3c5f63e603c2e3ff0abcba23eac6542635 \ - --hash=sha256:fb532dd9900381d2e8f48172ddc5a59db4c445a11b9fab40b3b786da40d3b56b \ - --hash=sha256:fe32482b37b4b00c7a52a07211b479653b7fe4f22b2e481b9a9b099d8a430f2f - # via jinja2 -mdurl==0.1.2 \ - --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ - --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba - # via markdown-it-py -more-itertools==10.5.0 \ - --hash=sha256:037b0d3203ce90cca8ab1defbbdac29d5f993fc20131f3664dc8d6acfa872aef \ - --hash=sha256:5482bfef7849c25dc3c6dd53a6173ae4795da2a41a80faea6700d9f5846c5da6 - # via - # jaraco-classes - # jaraco-functools -nh3==0.2.18 \ - --hash=sha256:0411beb0589eacb6734f28d5497ca2ed379eafab8ad8c84b31bb5c34072b7164 \ - --hash=sha256:14c5a72e9fe82aea5fe3072116ad4661af5cf8e8ff8fc5ad3450f123e4925e86 \ - --hash=sha256:19aaba96e0f795bd0a6c56291495ff59364f4300d4a39b29a0abc9cb3774a84b \ - --hash=sha256:34c03fa78e328c691f982b7c03d4423bdfd7da69cd707fe572f544cf74ac23ad \ - --hash=sha256:36c95d4b70530b320b365659bb5034341316e6a9b30f0b25fa9c9eff4c27a204 \ - --hash=sha256:3a157ab149e591bb638a55c8c6bcb8cdb559c8b12c13a8affaba6cedfe51713a \ - --hash=sha256:42c64511469005058cd17cc1537578eac40ae9f7200bedcfd1fc1a05f4f8c200 \ - --hash=sha256:5f36b271dae35c465ef5e9090e1fdaba4a60a56f0bb0ba03e0932a66f28b9189 \ - --hash=sha256:6955369e4d9f48f41e3f238a9e60f9410645db7e07435e62c6a9ea6135a4907f \ - --hash=sha256:7b7c2a3c9eb1a827d42539aa64091640bd275b81e097cd1d8d82ef91ffa2e811 \ - --hash=sha256:8ce0f819d2f1933953fca255db2471ad58184a60508f03e6285e5114b6254844 \ - --hash=sha256:94a166927e53972a9698af9542ace4e38b9de50c34352b962f4d9a7d4c927af4 \ - --hash=sha256:a7f1b5b2c15866f2db413a3649a8fe4fd7b428ae58be2c0f6bca5eefd53ca2be \ - --hash=sha256:c8b3a1cebcba9b3669ed1a84cc65bf005728d2f0bc1ed2a6594a992e817f3a50 \ - --hash=sha256:de3ceed6e661954871d6cd78b410213bdcb136f79aafe22aa7182e028b8c7307 \ - --hash=sha256:f0eca9ca8628dbb4e916ae2491d72957fdd35f7a5d326b7032a345f111ac07fe - # via readme-renderer -nox==2024.10.9 \ - --hash=sha256:1d36f309a0a2a853e9bccb76bbef6bb118ba92fa92674d15604ca99adeb29eab \ - --hash=sha256:7aa9dc8d1c27e9f45ab046ffd1c3b2c4f7c91755304769df231308849ebded95 - # via -r requirements.in -packaging==24.1 \ - --hash=sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002 \ - --hash=sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124 - # via - # gcp-releasetool - # nox -pkginfo==1.10.0 \ - --hash=sha256:5df73835398d10db79f8eecd5cd86b1f6d29317589ea70796994d49399af6297 \ - --hash=sha256:889a6da2ed7ffc58ab5b900d888ddce90bce912f2d2de1dc1c26f4cb9fe65097 - # via twine -platformdirs==4.3.6 \ - --hash=sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907 \ - --hash=sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb - # via virtualenv -proto-plus==1.24.0 \ - --hash=sha256:30b72a5ecafe4406b0d339db35b56c4059064e69227b8c3bda7462397f966445 \ - --hash=sha256:402576830425e5f6ce4c2a6702400ac79897dab0b4343821aa5188b0fab81a12 - # via google-api-core -protobuf==5.28.2 \ - --hash=sha256:2c69461a7fcc8e24be697624c09a839976d82ae75062b11a0972e41fd2cd9132 \ - --hash=sha256:35cfcb15f213449af7ff6198d6eb5f739c37d7e4f1c09b5d0641babf2cc0c68f \ - --hash=sha256:52235802093bd8a2811abbe8bf0ab9c5f54cca0a751fdd3f6ac2a21438bffece \ - --hash=sha256:59379674ff119717404f7454647913787034f03fe7049cbef1d74a97bb4593f0 \ - --hash=sha256:5e8a95246d581eef20471b5d5ba010d55f66740942b95ba9b872d918c459452f \ - --hash=sha256:87317e9bcda04a32f2ee82089a204d3a2f0d3c8aeed16568c7daf4756e4f1fe0 \ - --hash=sha256:8ddc60bf374785fb7cb12510b267f59067fa10087325b8e1855b898a0d81d276 \ - --hash=sha256:a8b9403fc70764b08d2f593ce44f1d2920c5077bf7d311fefec999f8c40f78b7 \ - --hash=sha256:c0ea0123dac3399a2eeb1a1443d82b7afc9ff40241433296769f7da42d142ec3 \ - --hash=sha256:ca53faf29896c526863366a52a8f4d88e69cd04ec9571ed6082fa117fac3ab36 \ - --hash=sha256:eeea10f3dc0ac7e6b4933d32db20662902b4ab81bf28df12218aa389e9c2102d - # via - # gcp-docuploader - # gcp-releasetool - # google-api-core - # googleapis-common-protos - # proto-plus -pyasn1==0.6.1 \ - --hash=sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629 \ - --hash=sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034 - # via - # pyasn1-modules - # rsa -pyasn1-modules==0.4.1 \ - --hash=sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd \ - --hash=sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c - # via google-auth -pycparser==2.22 \ - --hash=sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6 \ - --hash=sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc - # via cffi -pygments==2.18.0 \ - --hash=sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199 \ - --hash=sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a - # via - # readme-renderer - # rich -pyjwt==2.9.0 \ - --hash=sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850 \ - --hash=sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c - # via gcp-releasetool -pyperclip==1.9.0 \ - --hash=sha256:b7de0142ddc81bfc5c7507eea19da920b92252b548b96186caf94a5e2527d310 - # via gcp-releasetool -python-dateutil==2.9.0.post0 \ - --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ - --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 - # via gcp-releasetool -readme-renderer==44.0 \ - --hash=sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151 \ - --hash=sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1 - # via twine -requests==2.32.3 \ - --hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \ - --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6 - # via - # gcp-releasetool - # google-api-core - # google-cloud-storage - # requests-toolbelt - # twine -requests-toolbelt==1.0.0 \ - --hash=sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6 \ - --hash=sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06 - # via twine -rfc3986==2.0.0 \ - --hash=sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd \ - --hash=sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c - # via twine -rich==13.9.2 \ - --hash=sha256:51a2c62057461aaf7152b4d611168f93a9fc73068f8ded2790f29fe2b5366d0c \ - --hash=sha256:8c82a3d3f8dcfe9e734771313e606b39d8247bb6b826e196f4914b333b743cf1 - # via twine -rsa==4.9 \ - --hash=sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7 \ - --hash=sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21 - # via google-auth -secretstorage==3.3.3 \ - --hash=sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77 \ - --hash=sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99 - # via keyring -six==1.16.0 \ - --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ - --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 - # via - # gcp-docuploader - # python-dateutil -tomli==2.0.2 \ - --hash=sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38 \ - --hash=sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed - # via nox -twine==5.1.1 \ - --hash=sha256:215dbe7b4b94c2c50a7315c0275d2258399280fbb7d04182c7e55e24b5f93997 \ - --hash=sha256:9aa0825139c02b3434d913545c7b847a21c835e11597f5255842d457da2322db - # via -r requirements.in -typing-extensions==4.12.2 \ - --hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \ - --hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8 - # via - # -r requirements.in - # rich -urllib3==2.2.3 \ - --hash=sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac \ - --hash=sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9 - # via - # requests - # twine -virtualenv==20.26.6 \ - --hash=sha256:280aede09a2a5c317e409a00102e7077c6432c5a38f0ef938e643805a7ad2c48 \ - --hash=sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2 - # via nox -wheel==0.44.0 \ - --hash=sha256:2376a90c98cc337d18623527a97c31797bd02bad0033d41547043a1cbfbe448f \ - --hash=sha256:a29c3f2817e95ab89aa4660681ad547c0e9547f20e75b0562fe7723c9a2a9d49 - # via -r requirements.in -zipp==3.20.2 \ - --hash=sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350 \ - --hash=sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29 - # via importlib-metadata - -# The following packages are considered to be unsafe in a requirements file: -setuptools==75.1.0 \ - --hash=sha256:35ab7fd3bcd95e6b7fd704e4a1539513edad446c097797f2985e0e4b960772f2 \ - --hash=sha256:d59a21b17a275fb872a9c3dae73963160ae079f1049ed956880cd7c09b120538 - # via -r requirements.in diff --git a/.kokoro/test-samples-impl.sh b/.kokoro/test-samples-impl.sh index 55910c8ba..53e365bc4 100755 --- a/.kokoro/test-samples-impl.sh +++ b/.kokoro/test-samples-impl.sh @@ -33,7 +33,8 @@ export PYTHONUNBUFFERED=1 env | grep KOKORO # Install nox -python3.9 -m pip install --upgrade --quiet nox +# `virtualenv==20.26.6` is added for Python 3.7 compatibility +python3.9 -m pip install --upgrade --quiet nox virtualenv==20.26.6 # Use secrets acessor service account to get secrets if [[ -f "${KOKORO_GFILE_DIR}/secrets_viewer_service_account.json" ]]; then From 2844cf09ea8b2b78d53c7f0fe4d5e5bd1b926329 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 11 Nov 2024 12:42:46 -0500 Subject: [PATCH 048/101] chore(main): release 2.23.0 (#747) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ google/api_core/version.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea7f9e97d..060a0a865 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://pypi.org/project/google-api-core/#history +## [2.23.0](https://github.com/googleapis/python-api-core/compare/v2.22.0...v2.23.0) (2024-11-11) + + +### Features + +* Migrate to pyproject.toml ([#736](https://github.com/googleapis/python-api-core/issues/736)) ([159e9a4](https://github.com/googleapis/python-api-core/commit/159e9a49525937f18a55c38136aae32575424d55)) + ## [2.22.0](https://github.com/googleapis/python-api-core/compare/v2.21.0...v2.22.0) (2024-10-25) diff --git a/google/api_core/version.py b/google/api_core/version.py index 2db0ca518..416bf20ed 100644 --- a/google/api_core/version.py +++ b/google/api_core/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "2.22.0" +__version__ = "2.23.0" From e298ebdfbbb839d64c2da570b454c6c0f6c4e00c Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Tue, 12 Nov 2024 07:33:43 -0500 Subject: [PATCH 049/101] chore(python): update dependencies in .kokoro/docker/docs (#757) Source-Link: https://github.com/googleapis/synthtool/commit/59171c8f83f3522ce186e4d110d27e772da4ba7a Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-python:latest@sha256:2ed982f884312e4883e01b5ab8af8b6935f0216a5a2d82928d273081fc3be562 Co-authored-by: Owl Bot --- .github/.OwlBot.lock.yaml | 4 ++-- .kokoro/docker/docs/requirements.txt | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index b2770d4e0..6301519a9 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest - digest: sha256:91d0075c6f2fd6a073a06168feee19fa2a8507692f2519a1dc7de3366d157e99 -# created: 2024-11-11T16:13:09.302418532Z + digest: sha256:2ed982f884312e4883e01b5ab8af8b6935f0216a5a2d82928d273081fc3be562 +# created: 2024-11-12T12:09:45.821174897Z diff --git a/.kokoro/docker/docs/requirements.txt b/.kokoro/docker/docs/requirements.txt index 66eacc82f..8bb076459 100644 --- a/.kokoro/docker/docs/requirements.txt +++ b/.kokoro/docker/docs/requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.9 +# This file is autogenerated by pip-compile with Python 3.10 # by the following command: # # pip-compile --allow-unsafe --generate-hashes requirements.in @@ -8,9 +8,9 @@ argcomplete==3.5.1 \ --hash=sha256:1a1d148bdaa3e3b93454900163403df41448a248af01b6e849edc5ac08e6c363 \ --hash=sha256:eb1ee355aa2557bd3d0145de7b06b2a45b0ce461e1e7813f5d066039ab4177b4 # via nox -colorlog==6.8.2 \ - --hash=sha256:3e3e079a41feb5a1b64f978b5ea4f46040a94f11f0e8bbb8261e3dbbeca64d44 \ - --hash=sha256:4dcbb62368e2800cb3c5abd348da7e53f6c362dda502ec27c560b2e58a66bd33 +colorlog==6.9.0 \ + --hash=sha256:5906e71acd67cb07a71e779c47c4bcb45fb8c2993eebe9e5adcd6a6f1b283eff \ + --hash=sha256:bfba54a1b93b94f54e1f4fe48395725a3d92fd2a4af702f6bd70946bdc0c6ac2 # via nox distlib==0.3.9 \ --hash=sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87 \ @@ -24,9 +24,9 @@ nox==2024.10.9 \ --hash=sha256:1d36f309a0a2a853e9bccb76bbef6bb118ba92fa92674d15604ca99adeb29eab \ --hash=sha256:7aa9dc8d1c27e9f45ab046ffd1c3b2c4f7c91755304769df231308849ebded95 # via -r requirements.in -packaging==24.1 \ - --hash=sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002 \ - --hash=sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124 +packaging==24.2 \ + --hash=sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759 \ + --hash=sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f # via nox platformdirs==4.3.6 \ --hash=sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907 \ @@ -36,7 +36,7 @@ tomli==2.0.2 \ --hash=sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38 \ --hash=sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed # via nox -virtualenv==20.26.6 \ - --hash=sha256:280aede09a2a5c317e409a00102e7077c6432c5a38f0ef938e643805a7ad2c48 \ - --hash=sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2 +virtualenv==20.27.1 \ + --hash=sha256:142c6be10212543b32c6c45d3d3893dff89112cc588b7d0879ae5a1ec03a47ba \ + --hash=sha256:f11f1b8a29525562925f745563bfd48b189450f61fb34c4f9cc79dd5aa32a1f4 # via nox From d18d9b5131162b44eebcc0859a7aca1198a2ac06 Mon Sep 17 00:00:00 2001 From: ohmayr Date: Mon, 2 Dec 2024 16:12:40 +0500 Subject: [PATCH 050/101] feat: add automatic logging config to support debug logging (#754) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add base logger to enable debug logging * address PR feedback * address PR feedback * add tests * address PR feedback * address PR feedback * address PR feedback * address PR feedback * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * address PR feedback * address PR feedback * address PR feedback * add coverage for already configured scope * Add partial documentation; clarify test base logger * address PR feedback * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: Owl Bot Co-authored-by: Victor Chudnovsky Co-authored-by: Anthonios Partheniou --- google/api_core/client_logging.py | 143 ++++++++++++++++++++++++++++++ tests/unit/test_client_logging.py | 140 +++++++++++++++++++++++++++++ 2 files changed, 283 insertions(+) create mode 100644 google/api_core/client_logging.py create mode 100644 tests/unit/test_client_logging.py diff --git a/google/api_core/client_logging.py b/google/api_core/client_logging.py new file mode 100644 index 000000000..02d3bf60c --- /dev/null +++ b/google/api_core/client_logging.py @@ -0,0 +1,143 @@ +import logging +import json +import os + +from typing import List, Optional + +_LOGGING_INITIALIZED = False +_BASE_LOGGER_NAME = "google" + +# Fields to be included in the StructuredLogFormatter. +# +# TODO(https://github.com/googleapis/python-api-core/issues/761): Update this list to support additional logging fields. +_recognized_logging_fields = [ + "httpRequest", + "rpcName", + "serviceName", + "credentialsType", + "credentialInfo", + "universeDomain", + "request", + "response", + "metadata", + "retryAttempt", +] # Additional fields to be Logged. + + +def logger_configured(logger) -> bool: + """Determines whether `logger` has non-default configuration + + Args: + logger: The logger to check. + + Returns: + bool: Whether the logger has any non-default configuration. + """ + return ( + logger.handlers != [] or logger.level != logging.NOTSET or not logger.propagate + ) + + +def initialize_logging(): + """Initializes "google" loggers, partly based on the environment variable + + Initializes the "google" logger and any loggers (at the "google" + level or lower) specified by the environment variable + GOOGLE_SDK_PYTHON_LOGGING_SCOPE, as long as none of these loggers + were previously configured. If any such loggers (including the + "google" logger) are initialized, they are set to NOT propagate + log events up to their parent loggers. + + This initialization is executed only once, and hence the + environment variable is only processed the first time this + function is called. + """ + global _LOGGING_INITIALIZED + if _LOGGING_INITIALIZED: + return + scopes = os.getenv("GOOGLE_SDK_PYTHON_LOGGING_SCOPE", "") + setup_logging(scopes) + _LOGGING_INITIALIZED = True + + +def parse_logging_scopes(scopes: Optional[str] = None) -> List[str]: + """Returns a list of logger names. + + Splits the single string of comma-separated logger names into a list of individual logger name strings. + + Args: + scopes: The name of a single logger. (In the future, this will be a comma-separated list of multiple loggers.) + + Returns: + A list of all the logger names in scopes. + """ + if not scopes: + return [] + # TODO(https://github.com/googleapis/python-api-core/issues/759): check if the namespace is a valid namespace. + # TODO(b/380481951): Support logging multiple scopes. + # TODO(b/380483756): Raise or log a warning for an invalid scope. + namespaces = [scopes] + return namespaces + + +def configure_defaults(logger): + """Configures `logger` to emit structured info to stdout.""" + if not logger_configured(logger): + console_handler = logging.StreamHandler() + logger.setLevel("DEBUG") + logger.propagate = False + formatter = StructuredLogFormatter() + console_handler.setFormatter(formatter) + logger.addHandler(console_handler) + + +def setup_logging(scopes: str = ""): + """Sets up logging for the specified `scopes`. + + If the loggers specified in `scopes` have not been previously + configured, this will configure them to emit structured log + entries to stdout, and to not propagate their log events to their + parent loggers. Additionally, if the "google" logger (whether it + was specified in `scopes` or not) was not previously configured, + it will also configure it to not propagate log events to the root + logger. + + Args: + scopes: The name of a single logger. (In the future, this will be a comma-separated list of multiple loggers.) + + """ + + # only returns valid logger scopes (namespaces) + # this list has at most one element. + logger_names = parse_logging_scopes(scopes) + + for namespace in logger_names: + # This will either create a module level logger or get the reference of the base logger instantiated above. + logger = logging.getLogger(namespace) + + # Configure default settings. + configure_defaults(logger) + + # disable log propagation at base logger level to the root logger only if a base logger is not already configured via code changes. + base_logger = logging.getLogger(_BASE_LOGGER_NAME) + if not logger_configured(base_logger): + base_logger.propagate = False + + +# TODO(https://github.com/googleapis/python-api-core/issues/763): Expand documentation. +class StructuredLogFormatter(logging.Formatter): + # TODO(https://github.com/googleapis/python-api-core/issues/761): ensure that additional fields such as + # function name, file name, and line no. appear in a log output. + def format(self, record: logging.LogRecord): + log_obj = { + "timestamp": self.formatTime(record), + "severity": record.levelname, + "name": record.name, + "message": record.getMessage(), + } + + for field_name in _recognized_logging_fields: + value = getattr(record, field_name, None) + if value is not None: + log_obj[field_name] = value + return json.dumps(log_obj) diff --git a/tests/unit/test_client_logging.py b/tests/unit/test_client_logging.py new file mode 100644 index 000000000..b3b0b5c8e --- /dev/null +++ b/tests/unit/test_client_logging.py @@ -0,0 +1,140 @@ +import json +import logging +from unittest import mock + +from google.api_core.client_logging import ( + setup_logging, + initialize_logging, + StructuredLogFormatter, +) + + +def reset_logger(scope): + logger = logging.getLogger(scope) + logger.handlers = [] + logger.setLevel(logging.NOTSET) + logger.propagate = True + + +def test_setup_logging_w_no_scopes(): + with mock.patch("google.api_core.client_logging._BASE_LOGGER_NAME", "foogle"): + setup_logging() + base_logger = logging.getLogger("foogle") + assert base_logger.handlers == [] + assert not base_logger.propagate + assert base_logger.level == logging.NOTSET + + reset_logger("foogle") + + +def test_setup_logging_w_base_scope(): + with mock.patch("google.api_core.client_logging._BASE_LOGGER_NAME", "foogle"): + setup_logging("foogle") + base_logger = logging.getLogger("foogle") + assert isinstance(base_logger.handlers[0], logging.StreamHandler) + assert not base_logger.propagate + assert base_logger.level == logging.DEBUG + + reset_logger("foogle") + + +def test_setup_logging_w_configured_scope(): + with mock.patch("google.api_core.client_logging._BASE_LOGGER_NAME", "foogle"): + base_logger = logging.getLogger("foogle") + base_logger.propagate = False + setup_logging("foogle") + assert base_logger.handlers == [] + assert not base_logger.propagate + assert base_logger.level == logging.NOTSET + + reset_logger("foogle") + + +def test_setup_logging_w_module_scope(): + with mock.patch("google.api_core.client_logging._BASE_LOGGER_NAME", "foogle"): + setup_logging("foogle.bar") + + base_logger = logging.getLogger("foogle") + assert base_logger.handlers == [] + assert not base_logger.propagate + assert base_logger.level == logging.NOTSET + + module_logger = logging.getLogger("foogle.bar") + assert isinstance(module_logger.handlers[0], logging.StreamHandler) + assert not module_logger.propagate + assert module_logger.level == logging.DEBUG + + reset_logger("foogle") + reset_logger("foogle.bar") + + +def test_setup_logging_w_incorrect_scope(): + with mock.patch("google.api_core.client_logging._BASE_LOGGER_NAME", "foogle"): + setup_logging("abc") + + base_logger = logging.getLogger("foogle") + assert base_logger.handlers == [] + assert not base_logger.propagate + assert base_logger.level == logging.NOTSET + + # TODO(https://github.com/googleapis/python-api-core/issues/759): update test once we add logic to ignore an incorrect scope. + logger = logging.getLogger("abc") + assert isinstance(logger.handlers[0], logging.StreamHandler) + assert not logger.propagate + assert logger.level == logging.DEBUG + + reset_logger("foogle") + reset_logger("abc") + + +def test_initialize_logging(): + + with mock.patch("os.getenv", return_value="foogle.bar"): + with mock.patch("google.api_core.client_logging._BASE_LOGGER_NAME", "foogle"): + initialize_logging() + + base_logger = logging.getLogger("foogle") + assert base_logger.handlers == [] + assert not base_logger.propagate + assert base_logger.level == logging.NOTSET + + module_logger = logging.getLogger("foogle.bar") + assert isinstance(module_logger.handlers[0], logging.StreamHandler) + assert not module_logger.propagate + assert module_logger.level == logging.DEBUG + + # Check that `initialize_logging()` is a no-op after the first time by verifying that user-set configs are not modified: + base_logger.propagate = True + module_logger.propagate = True + + initialize_logging() + + assert base_logger.propagate + assert module_logger.propagate + + reset_logger("foogle") + reset_logger("foogle.bar") + + +def test_structured_log_formatter(): + # TODO(https://github.com/googleapis/python-api-core/issues/761): Test additional fields when implemented. + record = logging.LogRecord( + name="Appelation", + level=logging.DEBUG, + msg="This is a test message.", + pathname="some/path", + lineno=25, + args=None, + exc_info=None, + ) + + # Extra fields: + record.rpcName = "bar" + + formatted_msg = StructuredLogFormatter().format(record) + parsed_msg = json.loads(formatted_msg) + + assert parsed_msg["name"] == "Appelation" + assert parsed_msg["severity"] == "DEBUG" + assert parsed_msg["message"] == "This is a test message." + assert parsed_msg["rpcName"] == "bar" From 5f80f778bc25d878b3187c6138077ad8c6bcd35f Mon Sep 17 00:00:00 2001 From: ohmayr Date: Sat, 7 Dec 2024 00:23:03 +0500 Subject: [PATCH 051/101] feat: update recognized logging fields (#766) --- google/api_core/client_logging.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/google/api_core/client_logging.py b/google/api_core/client_logging.py index 02d3bf60c..837e3e0c4 100644 --- a/google/api_core/client_logging.py +++ b/google/api_core/client_logging.py @@ -15,12 +15,13 @@ "rpcName", "serviceName", "credentialsType", - "credentialInfo", + "credentialsInfo", "universeDomain", "request", "response", "metadata", "retryAttempt", + "httpResponse", ] # Additional fields to be Logged. From 65576194d6a8236ce97a4f4a336c2d90e03de199 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 01:11:44 +0500 Subject: [PATCH 052/101] chore(main): release 2.24.0 (#764) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 8 ++++++++ google/api_core/version.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 060a0a865..c7eacaf0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ [1]: https://pypi.org/project/google-api-core/#history +## [2.24.0](https://github.com/googleapis/python-api-core/compare/v2.23.0...v2.24.0) (2024-12-06) + + +### Features + +* Add automatic logging config to support debug logging ([#754](https://github.com/googleapis/python-api-core/issues/754)) ([d18d9b5](https://github.com/googleapis/python-api-core/commit/d18d9b5131162b44eebcc0859a7aca1198a2ac06)) +* Update recognized logging fields ([#766](https://github.com/googleapis/python-api-core/issues/766)) ([5f80f77](https://github.com/googleapis/python-api-core/commit/5f80f778bc25d878b3187c6138077ad8c6bcd35f)) + ## [2.23.0](https://github.com/googleapis/python-api-core/compare/v2.22.0...v2.23.0) (2024-11-11) diff --git a/google/api_core/version.py b/google/api_core/version.py index 416bf20ed..84f6b4643 100644 --- a/google/api_core/version.py +++ b/google/api_core/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "2.23.0" +__version__ = "2.24.0" From 91829160815cd97219bca1d88cc19a72c9a6e935 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Fri, 3 Jan 2025 14:29:31 -0500 Subject: [PATCH 053/101] chore(python): Update the python version in docs presubmit to use 3.10 (#773) * chore(python): Update the python version in docs presubmit to use 3.10 Source-Link: https://github.com/googleapis/synthtool/commit/de3def663b75d8b9ae1e5d548364c960ff13af8f Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-python:latest@sha256:a1c5112b81d645f5bbc4d4bbc99d7dcb5089a52216c0e3fb1203a0eeabadd7d5 * See https://github.com/googleapis/synthtool/pull/2047 --------- Co-authored-by: Owl Bot Co-authored-by: Anthonios Partheniou --- .github/.OwlBot.lock.yaml | 6 ++-- .github/workflows/docs.yml | 2 +- .github/workflows/unittest.yml | 5 ++- .kokoro/docker/docs/requirements.txt | 52 ++++++++++++++++++++++------ 4 files changed, 49 insertions(+), 16 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 6301519a9..1d0fd7e78 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -1,4 +1,4 @@ -# Copyright 2024 Google LLC +# Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest - digest: sha256:2ed982f884312e4883e01b5ab8af8b6935f0216a5a2d82928d273081fc3be562 -# created: 2024-11-12T12:09:45.821174897Z + digest: sha256:a1c5112b81d645f5bbc4d4bbc99d7dcb5089a52216c0e3fb1203a0eeabadd7d5 +# created: 2025-01-02T23:09:36.975468657Z diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 698fbc5c9..2833fe98f 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -12,7 +12,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v5 with: - python-version: "3.9" + python-version: "3.10" - name: Install nox run: | python -m pip install --upgrade setuptools pip wheel diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index a82859e16..4e4fa25de 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -8,7 +8,10 @@ on: jobs: run-unittests: name: unit${{ matrix.option }}-${{ matrix.python }} - runs-on: ubuntu-latest + # TODO(https://github.com/googleapis/gapic-generator-python/issues/2303): use `ubuntu-latest` once this bug is fixed. + # Use ubuntu-22.04 until Python 3.7 is removed from the test matrix + # https://docs.github.com/en/actions/using-github-hosted-runners/using-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories + runs-on: ubuntu-22.04 strategy: matrix: option: ["", "_grpc_gcp", "_wo_grpc", "_w_prerelease_deps", "_w_async_rest_extra"] diff --git a/.kokoro/docker/docs/requirements.txt b/.kokoro/docker/docs/requirements.txt index 8bb076459..f99a5c4aa 100644 --- a/.kokoro/docker/docs/requirements.txt +++ b/.kokoro/docker/docs/requirements.txt @@ -2,11 +2,11 @@ # This file is autogenerated by pip-compile with Python 3.10 # by the following command: # -# pip-compile --allow-unsafe --generate-hashes requirements.in +# pip-compile --allow-unsafe --generate-hashes synthtool/gcp/templates/python_library/.kokoro/docker/docs/requirements.in # -argcomplete==3.5.1 \ - --hash=sha256:1a1d148bdaa3e3b93454900163403df41448a248af01b6e849edc5ac08e6c363 \ - --hash=sha256:eb1ee355aa2557bd3d0145de7b06b2a45b0ce461e1e7813f5d066039ab4177b4 +argcomplete==3.5.2 \ + --hash=sha256:036d020d79048a5d525bc63880d7a4b8d1668566b8a76daf1144c0bbe0f63472 \ + --hash=sha256:23146ed7ac4403b70bd6026402468942ceba34a6732255b9edf5b7354f68a6bb # via nox colorlog==6.9.0 \ --hash=sha256:5906e71acd67cb07a71e779c47c4bcb45fb8c2993eebe9e5adcd6a6f1b283eff \ @@ -23,7 +23,7 @@ filelock==3.16.1 \ nox==2024.10.9 \ --hash=sha256:1d36f309a0a2a853e9bccb76bbef6bb118ba92fa92674d15604ca99adeb29eab \ --hash=sha256:7aa9dc8d1c27e9f45ab046ffd1c3b2c4f7c91755304769df231308849ebded95 - # via -r requirements.in + # via -r synthtool/gcp/templates/python_library/.kokoro/docker/docs/requirements.in packaging==24.2 \ --hash=sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759 \ --hash=sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f @@ -32,11 +32,41 @@ platformdirs==4.3.6 \ --hash=sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907 \ --hash=sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb # via virtualenv -tomli==2.0.2 \ - --hash=sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38 \ - --hash=sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed +tomli==2.2.1 \ + --hash=sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6 \ + --hash=sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd \ + --hash=sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c \ + --hash=sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b \ + --hash=sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8 \ + --hash=sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6 \ + --hash=sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77 \ + --hash=sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff \ + --hash=sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea \ + --hash=sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192 \ + --hash=sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249 \ + --hash=sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee \ + --hash=sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4 \ + --hash=sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98 \ + --hash=sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8 \ + --hash=sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4 \ + --hash=sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281 \ + --hash=sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744 \ + --hash=sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69 \ + --hash=sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13 \ + --hash=sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140 \ + --hash=sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e \ + --hash=sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e \ + --hash=sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc \ + --hash=sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff \ + --hash=sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec \ + --hash=sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2 \ + --hash=sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222 \ + --hash=sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106 \ + --hash=sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272 \ + --hash=sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a \ + --hash=sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7 # via nox -virtualenv==20.27.1 \ - --hash=sha256:142c6be10212543b32c6c45d3d3893dff89112cc588b7d0879ae5a1ec03a47ba \ - --hash=sha256:f11f1b8a29525562925f745563bfd48b189450f61fb34c4f9cc79dd5aa32a1f4 +virtualenv==20.28.0 \ + --hash=sha256:23eae1b4516ecd610481eda647f3a7c09aea295055337331bb4e6892ecce47b0 \ + --hash=sha256:2c9c3262bb8e7b87ea801d715fae4495e6032450c71d2309be9550e7364049aa # via nox From b1fae31c8c71ed65ad22a415dab2b54720c3d4ba Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Fri, 10 Jan 2025 12:35:15 -0800 Subject: [PATCH 054/101] chore(python): exclude .github/workflows/unittest.yml in renovate config (#774) Source-Link: https://github.com/googleapis/synthtool/commit/106d292bd234e5d9977231dcfbc4831e34eba13a Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-python:latest@sha256:8ff1efe878e18bd82a0fb7b70bb86f77e7ab6901fed394440b6135db0ba8d84a Co-authored-by: Owl Bot --- .github/.OwlBot.lock.yaml | 4 ++-- renovate.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 1d0fd7e78..10cf433a8 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest - digest: sha256:a1c5112b81d645f5bbc4d4bbc99d7dcb5089a52216c0e3fb1203a0eeabadd7d5 -# created: 2025-01-02T23:09:36.975468657Z + digest: sha256:8ff1efe878e18bd82a0fb7b70bb86f77e7ab6901fed394440b6135db0ba8d84a +# created: 2025-01-09T12:01:16.422459506Z diff --git a/renovate.json b/renovate.json index 39b2a0ec9..c7875c469 100644 --- a/renovate.json +++ b/renovate.json @@ -5,7 +5,7 @@ ":preserveSemverRanges", ":disableDependencyDashboard" ], - "ignorePaths": [".pre-commit-config.yaml", ".kokoro/requirements.txt", "setup.py"], + "ignorePaths": [".pre-commit-config.yaml", ".kokoro/requirements.txt", "setup.py", ".github/workflows/unittest.yml"], "pip_requirements": { "fileMatch": ["requirements-test.txt", "samples/[\\S/]*constraints.txt", "samples/[\\S/]*constraints-test.txt"] } From a5604a55070c6d92618d078191bf99f4c168d5f6 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Thu, 16 Jan 2025 18:50:36 -0500 Subject: [PATCH 055/101] fix: resolve the issue where rpc timeout of 0 is used when timeout expires (#776) * fix: resolve the issue where rpc timeout of 0 is used when timeout expires * address offline feedback * formatting * update comment --- google/api_core/timeout.py | 18 ++++++++++++++---- tests/unit/test_timeout.py | 4 ++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/google/api_core/timeout.py b/google/api_core/timeout.py index 868e3e9f4..55b195e90 100644 --- a/google/api_core/timeout.py +++ b/google/api_core/timeout.py @@ -102,8 +102,7 @@ def __call__(self, func): def func_with_timeout(*args, **kwargs): """Wrapped function that adds timeout.""" - remaining_timeout = self._timeout - if remaining_timeout is not None: + if self._timeout is not None: # All calculations are in seconds now_timestamp = self._clock().timestamp() @@ -114,8 +113,19 @@ def func_with_timeout(*args, **kwargs): now_timestamp = first_attempt_timestamp time_since_first_attempt = now_timestamp - first_attempt_timestamp - # Avoid setting negative timeout - kwargs["timeout"] = max(0, self._timeout - time_since_first_attempt) + remaining_timeout = self._timeout - time_since_first_attempt + + # Although the `deadline` parameter in `google.api_core.retry.Retry` + # is deprecated, and should be treated the same as the `timeout`, + # it is still possible for the `deadline` argument in + # `google.api_core.retry.Retry` to be larger than the `timeout`. + # See https://github.com/googleapis/python-api-core/issues/654 + # Only positive non-zero timeouts are supported. + # Revert back to the initial timeout for negative or 0 timeout values. + if remaining_timeout < 1: + remaining_timeout = self._timeout + + kwargs["timeout"] = remaining_timeout return func(*args, **kwargs) diff --git a/tests/unit/test_timeout.py b/tests/unit/test_timeout.py index 60a2e65d3..2c20202bd 100644 --- a/tests/unit/test_timeout.py +++ b/tests/unit/test_timeout.py @@ -84,9 +84,9 @@ def _clock(): wrapped() target.assert_called_with(timeout=3.0) wrapped() - target.assert_called_with(timeout=0.0) + target.assert_called_with(timeout=42.0) wrapped() - target.assert_called_with(timeout=0.0) + target.assert_called_with(timeout=42.0) def test_apply_no_timeout(self): target = mock.Mock(spec=["__call__", "__name__"], __name__="target") From d8ca706c55f3f1900daaef6b35464f9135163097 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Wed, 22 Jan 2025 16:24:54 -0500 Subject: [PATCH 056/101] chore(python): fix docs publish build (#777) Source-Link: https://github.com/googleapis/synthtool/commit/bd9ede2fea1b640b7e90d5a1d110e6b300a2b43f Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-python:latest@sha256:04c35dc5f49f0f503a306397d6d043685f8d2bb822ab515818c4208d7fb2db3a Co-authored-by: Owl Bot Co-authored-by: Anthonios Partheniou --- .github/.OwlBot.lock.yaml | 4 +- .kokoro/docker/docs/requirements.in | 1 + .kokoro/docker/docs/requirements.txt | 243 ++++++++++++++++++++++++++- .kokoro/publish-docs.sh | 4 - 4 files changed, 237 insertions(+), 15 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 10cf433a8..4c0027ff1 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest - digest: sha256:8ff1efe878e18bd82a0fb7b70bb86f77e7ab6901fed394440b6135db0ba8d84a -# created: 2025-01-09T12:01:16.422459506Z + digest: sha256:04c35dc5f49f0f503a306397d6d043685f8d2bb822ab515818c4208d7fb2db3a +# created: 2025-01-16T15:24:11.364245182Z diff --git a/.kokoro/docker/docs/requirements.in b/.kokoro/docker/docs/requirements.in index 816817c67..586bd0703 100644 --- a/.kokoro/docker/docs/requirements.in +++ b/.kokoro/docker/docs/requirements.in @@ -1 +1,2 @@ nox +gcp-docuploader diff --git a/.kokoro/docker/docs/requirements.txt b/.kokoro/docker/docs/requirements.txt index f99a5c4aa..a9360a25b 100644 --- a/.kokoro/docker/docs/requirements.txt +++ b/.kokoro/docker/docs/requirements.txt @@ -2,16 +2,124 @@ # This file is autogenerated by pip-compile with Python 3.10 # by the following command: # -# pip-compile --allow-unsafe --generate-hashes synthtool/gcp/templates/python_library/.kokoro/docker/docs/requirements.in +# pip-compile --allow-unsafe --generate-hashes requirements.in # -argcomplete==3.5.2 \ - --hash=sha256:036d020d79048a5d525bc63880d7a4b8d1668566b8a76daf1144c0bbe0f63472 \ - --hash=sha256:23146ed7ac4403b70bd6026402468942ceba34a6732255b9edf5b7354f68a6bb +argcomplete==3.5.3 \ + --hash=sha256:2ab2c4a215c59fd6caaff41a869480a23e8f6a5f910b266c1808037f4e375b61 \ + --hash=sha256:c12bf50eded8aebb298c7b7da7a5ff3ee24dffd9f5281867dfe1424b58c55392 # via nox +cachetools==5.5.0 \ + --hash=sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292 \ + --hash=sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a + # via google-auth +certifi==2024.12.14 \ + --hash=sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56 \ + --hash=sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db + # via requests +charset-normalizer==3.4.1 \ + --hash=sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537 \ + --hash=sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa \ + --hash=sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a \ + --hash=sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294 \ + --hash=sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b \ + --hash=sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd \ + --hash=sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601 \ + --hash=sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd \ + --hash=sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4 \ + --hash=sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d \ + --hash=sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2 \ + --hash=sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313 \ + --hash=sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd \ + --hash=sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa \ + --hash=sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8 \ + --hash=sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1 \ + --hash=sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2 \ + --hash=sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496 \ + --hash=sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d \ + --hash=sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b \ + --hash=sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e \ + --hash=sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a \ + --hash=sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4 \ + --hash=sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca \ + --hash=sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78 \ + --hash=sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408 \ + --hash=sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5 \ + --hash=sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3 \ + --hash=sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f \ + --hash=sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a \ + --hash=sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765 \ + --hash=sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6 \ + --hash=sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146 \ + --hash=sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6 \ + --hash=sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9 \ + --hash=sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd \ + --hash=sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c \ + --hash=sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f \ + --hash=sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545 \ + --hash=sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176 \ + --hash=sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770 \ + --hash=sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824 \ + --hash=sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f \ + --hash=sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf \ + --hash=sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487 \ + --hash=sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d \ + --hash=sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd \ + --hash=sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b \ + --hash=sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534 \ + --hash=sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f \ + --hash=sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b \ + --hash=sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9 \ + --hash=sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd \ + --hash=sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125 \ + --hash=sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9 \ + --hash=sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de \ + --hash=sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11 \ + --hash=sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d \ + --hash=sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35 \ + --hash=sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f \ + --hash=sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda \ + --hash=sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7 \ + --hash=sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a \ + --hash=sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971 \ + --hash=sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8 \ + --hash=sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41 \ + --hash=sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d \ + --hash=sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f \ + --hash=sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757 \ + --hash=sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a \ + --hash=sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886 \ + --hash=sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77 \ + --hash=sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76 \ + --hash=sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247 \ + --hash=sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85 \ + --hash=sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb \ + --hash=sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7 \ + --hash=sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e \ + --hash=sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6 \ + --hash=sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037 \ + --hash=sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1 \ + --hash=sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e \ + --hash=sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807 \ + --hash=sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407 \ + --hash=sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c \ + --hash=sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12 \ + --hash=sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3 \ + --hash=sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089 \ + --hash=sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd \ + --hash=sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e \ + --hash=sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00 \ + --hash=sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616 + # via requests +click==8.1.8 \ + --hash=sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2 \ + --hash=sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a + # via gcp-docuploader colorlog==6.9.0 \ --hash=sha256:5906e71acd67cb07a71e779c47c4bcb45fb8c2993eebe9e5adcd6a6f1b283eff \ --hash=sha256:bfba54a1b93b94f54e1f4fe48395725a3d92fd2a4af702f6bd70946bdc0c6ac2 - # via nox + # via + # gcp-docuploader + # nox distlib==0.3.9 \ --hash=sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87 \ --hash=sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403 @@ -20,10 +128,78 @@ filelock==3.16.1 \ --hash=sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0 \ --hash=sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435 # via virtualenv +gcp-docuploader==0.6.5 \ + --hash=sha256:30221d4ac3e5a2b9c69aa52fdbef68cc3f27d0e6d0d90e220fc024584b8d2318 \ + --hash=sha256:b7458ef93f605b9d46a4bf3a8dc1755dad1f31d030c8679edf304e343b347eea + # via -r requirements.in +google-api-core==2.24.0 \ + --hash=sha256:10d82ac0fca69c82a25b3efdeefccf6f28e02ebb97925a8cce8edbfe379929d9 \ + --hash=sha256:e255640547a597a4da010876d333208ddac417d60add22b6851a0c66a831fcaf + # via + # google-cloud-core + # google-cloud-storage +google-auth==2.37.0 \ + --hash=sha256:0054623abf1f9c83492c63d3f47e77f0a544caa3d40b2d98e099a611c2dd5d00 \ + --hash=sha256:42664f18290a6be591be5329a96fe30184be1a1badb7292a7f686a9659de9ca0 + # via + # google-api-core + # google-cloud-core + # google-cloud-storage +google-cloud-core==2.4.1 \ + --hash=sha256:9b7749272a812bde58fff28868d0c5e2f585b82f37e09a1f6ed2d4d10f134073 \ + --hash=sha256:a9e6a4422b9ac5c29f79a0ede9485473338e2ce78d91f2370c01e730eab22e61 + # via google-cloud-storage +google-cloud-storage==2.19.0 \ + --hash=sha256:aeb971b5c29cf8ab98445082cbfe7b161a1f48ed275822f59ed3f1524ea54fba \ + --hash=sha256:cd05e9e7191ba6cb68934d8eb76054d9be4562aa89dbc4236feee4d7d51342b2 + # via gcp-docuploader +google-crc32c==1.6.0 \ + --hash=sha256:05e2d8c9a2f853ff116db9706b4a27350587f341eda835f46db3c0a8c8ce2f24 \ + --hash=sha256:18e311c64008f1f1379158158bb3f0c8d72635b9eb4f9545f8cf990c5668e59d \ + --hash=sha256:236c87a46cdf06384f614e9092b82c05f81bd34b80248021f729396a78e55d7e \ + --hash=sha256:35834855408429cecf495cac67ccbab802de269e948e27478b1e47dfb6465e57 \ + --hash=sha256:386122eeaaa76951a8196310432c5b0ef3b53590ef4c317ec7588ec554fec5d2 \ + --hash=sha256:40b05ab32a5067525670880eb5d169529089a26fe35dce8891127aeddc1950e8 \ + --hash=sha256:48abd62ca76a2cbe034542ed1b6aee851b6f28aaca4e6551b5599b6f3ef175cc \ + --hash=sha256:50cf2a96da226dcbff8671233ecf37bf6e95de98b2a2ebadbfdf455e6d05df42 \ + --hash=sha256:51c4f54dd8c6dfeb58d1df5e4f7f97df8abf17a36626a217f169893d1d7f3e9f \ + --hash=sha256:5bcc90b34df28a4b38653c36bb5ada35671ad105c99cfe915fb5bed7ad6924aa \ + --hash=sha256:62f6d4a29fea082ac4a3c9be5e415218255cf11684ac6ef5488eea0c9132689b \ + --hash=sha256:6eceb6ad197656a1ff49ebfbbfa870678c75be4344feb35ac1edf694309413dc \ + --hash=sha256:7aec8e88a3583515f9e0957fe4f5f6d8d4997e36d0f61624e70469771584c760 \ + --hash=sha256:91ca8145b060679ec9176e6de4f89b07363d6805bd4760631ef254905503598d \ + --hash=sha256:a184243544811e4a50d345838a883733461e67578959ac59964e43cca2c791e7 \ + --hash=sha256:a9e4b426c3702f3cd23b933436487eb34e01e00327fac20c9aebb68ccf34117d \ + --hash=sha256:bb0966e1c50d0ef5bc743312cc730b533491d60585a9a08f897274e57c3f70e0 \ + --hash=sha256:bb8b3c75bd157010459b15222c3fd30577042a7060e29d42dabce449c087f2b3 \ + --hash=sha256:bd5e7d2445d1a958c266bfa5d04c39932dc54093fa391736dbfdb0f1929c1fb3 \ + --hash=sha256:c87d98c7c4a69066fd31701c4e10d178a648c2cac3452e62c6b24dc51f9fcc00 \ + --hash=sha256:d2952396dc604544ea7476b33fe87faedc24d666fb0c2d5ac971a2b9576ab871 \ + --hash=sha256:d8797406499f28b5ef791f339594b0b5fdedf54e203b5066675c406ba69d705c \ + --hash=sha256:d9e9913f7bd69e093b81da4535ce27af842e7bf371cde42d1ae9e9bd382dc0e9 \ + --hash=sha256:e2806553238cd076f0a55bddab37a532b53580e699ed8e5606d0de1f856b5205 \ + --hash=sha256:ebab974b1687509e5c973b5c4b8b146683e101e102e17a86bd196ecaa4d099fc \ + --hash=sha256:ed767bf4ba90104c1216b68111613f0d5926fb3780660ea1198fc469af410e9d \ + --hash=sha256:f7a1fc29803712f80879b0806cb83ab24ce62fc8daf0569f2204a0cfd7f68ed4 + # via + # google-cloud-storage + # google-resumable-media +google-resumable-media==2.7.2 \ + --hash=sha256:3ce7551e9fe6d99e9a126101d2536612bb73486721951e9562fee0f90c6ababa \ + --hash=sha256:5280aed4629f2b60b847b0d42f9857fd4935c11af266744df33d8074cae92fe0 + # via google-cloud-storage +googleapis-common-protos==1.66.0 \ + --hash=sha256:c3e7b33d15fdca5374cc0a7346dd92ffa847425cc4ea941d970f13680052ec8c \ + --hash=sha256:d7abcd75fabb2e0ec9f74466401f6c119a0b498e27370e9be4c94cb7e382b8ed + # via google-api-core +idna==3.10 \ + --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \ + --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3 + # via requests nox==2024.10.9 \ --hash=sha256:1d36f309a0a2a853e9bccb76bbef6bb118ba92fa92674d15604ca99adeb29eab \ --hash=sha256:7aa9dc8d1c27e9f45ab046ffd1c3b2c4f7c91755304769df231308849ebded95 - # via -r synthtool/gcp/templates/python_library/.kokoro/docker/docs/requirements.in + # via -r requirements.in packaging==24.2 \ --hash=sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759 \ --hash=sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f @@ -32,6 +208,51 @@ platformdirs==4.3.6 \ --hash=sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907 \ --hash=sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb # via virtualenv +proto-plus==1.25.0 \ + --hash=sha256:c91fc4a65074ade8e458e95ef8bac34d4008daa7cce4a12d6707066fca648961 \ + --hash=sha256:fbb17f57f7bd05a68b7707e745e26528b0b3c34e378db91eef93912c54982d91 + # via google-api-core +protobuf==5.29.3 \ + --hash=sha256:0a18ed4a24198528f2333802eb075e59dea9d679ab7a6c5efb017a59004d849f \ + --hash=sha256:0eb32bfa5219fc8d4111803e9a690658aa2e6366384fd0851064b963b6d1f2a7 \ + --hash=sha256:3ea51771449e1035f26069c4c7fd51fba990d07bc55ba80701c78f886bf9c888 \ + --hash=sha256:5da0f41edaf117bde316404bad1a486cb4ededf8e4a54891296f648e8e076620 \ + --hash=sha256:6ce8cc3389a20693bfde6c6562e03474c40851b44975c9b2bf6df7d8c4f864da \ + --hash=sha256:84a57163a0ccef3f96e4b6a20516cedcf5bb3a95a657131c5c3ac62200d23252 \ + --hash=sha256:a4fa6f80816a9a0678429e84973f2f98cbc218cca434abe8db2ad0bffc98503a \ + --hash=sha256:a8434404bbf139aa9e1300dbf989667a83d42ddda9153d8ab76e0d5dcaca484e \ + --hash=sha256:b89c115d877892a512f79a8114564fb435943b59067615894c3b13cd3e1fa107 \ + --hash=sha256:c027e08a08be10b67c06bf2370b99c811c466398c357e615ca88c91c07f0910f \ + --hash=sha256:daaf63f70f25e8689c072cfad4334ca0ac1d1e05a92fc15c54eb9cf23c3efd84 + # via + # gcp-docuploader + # google-api-core + # googleapis-common-protos + # proto-plus +pyasn1==0.6.1 \ + --hash=sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629 \ + --hash=sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034 + # via + # pyasn1-modules + # rsa +pyasn1-modules==0.4.1 \ + --hash=sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd \ + --hash=sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c + # via google-auth +requests==2.32.3 \ + --hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \ + --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6 + # via + # google-api-core + # google-cloud-storage +rsa==4.9 \ + --hash=sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7 \ + --hash=sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21 + # via google-auth +six==1.17.0 \ + --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \ + --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81 + # via gcp-docuploader tomli==2.2.1 \ --hash=sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6 \ --hash=sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd \ @@ -66,7 +287,11 @@ tomli==2.2.1 \ --hash=sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a \ --hash=sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7 # via nox -virtualenv==20.28.0 \ - --hash=sha256:23eae1b4516ecd610481eda647f3a7c09aea295055337331bb4e6892ecce47b0 \ - --hash=sha256:2c9c3262bb8e7b87ea801d715fae4495e6032450c71d2309be9550e7364049aa +urllib3==2.3.0 \ + --hash=sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df \ + --hash=sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d + # via requests +virtualenv==20.28.1 \ + --hash=sha256:412773c85d4dab0409b83ec36f7a6499e72eaf08c80e81e9576bca61831c71cb \ + --hash=sha256:5d34ab240fdb5d21549b76f9e8ff3af28252f5499fb6d6f031adac4e5a8c5329 # via nox diff --git a/.kokoro/publish-docs.sh b/.kokoro/publish-docs.sh index 233205d58..4ed4aaf13 100755 --- a/.kokoro/publish-docs.sh +++ b/.kokoro/publish-docs.sh @@ -20,10 +20,6 @@ export PYTHONUNBUFFERED=1 export PATH="${HOME}/.local/bin:${PATH}" -# Install nox -python3.10 -m pip install --require-hashes -r .kokoro/requirements.txt -python3.10 -m nox --version - # build docs nox -s docs From fb1c3a9d8e3a79eaea1f6bf981eb9580feeea27d Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Wed, 22 Jan 2025 13:27:41 -0800 Subject: [PATCH 057/101] chore(types): update mypy version (#768) * use latest mypy for check * use separate variable for error_info list vs dict * make wrapped call generic * fix typing for nullable retry functions * include async_rest in mypy installation * removed generic application --------- Co-authored-by: Anthonios Partheniou --- google/api_core/exceptions.py | 4 ++-- google/api_core/grpc_helpers_async.py | 4 ++-- google/api_core/retry/retry_unary.py | 2 +- google/api_core/retry/retry_unary_async.py | 2 +- noxfile.py | 4 +--- tests/asyncio/test_grpc_helpers_async.py | 2 +- 6 files changed, 8 insertions(+), 10 deletions(-) diff --git a/google/api_core/exceptions.py b/google/api_core/exceptions.py index 5b25d1240..e3eb696c7 100644 --- a/google/api_core/exceptions.py +++ b/google/api_core/exceptions.py @@ -517,14 +517,14 @@ def format_http_response_error( errors = payload.get("error", {}).get("errors", ()) # In JSON, details are already formatted in developer-friendly way. details = payload.get("error", {}).get("details", ()) - error_info = list( + error_info_list = list( filter( lambda detail: detail.get("@type", "") == "type.googleapis.com/google.rpc.ErrorInfo", details, ) ) - error_info = error_info[0] if error_info else None + error_info = error_info_list[0] if error_info_list else None message = _format_rest_error_message(error_message, method, url) exception = from_http_status( diff --git a/google/api_core/grpc_helpers_async.py b/google/api_core/grpc_helpers_async.py index 6feb22299..269604568 100644 --- a/google/api_core/grpc_helpers_async.py +++ b/google/api_core/grpc_helpers_async.py @@ -152,9 +152,9 @@ class _WrappedStreamStreamCall( # public type alias denoting the return type of async streaming gapic calls -GrpcAsyncStream = _WrappedStreamResponseMixin[P] +GrpcAsyncStream = _WrappedStreamResponseMixin # public type alias denoting the return type of unary gapic calls -AwaitableGrpcCall = _WrappedUnaryResponseMixin[P] +AwaitableGrpcCall = _WrappedUnaryResponseMixin def _wrap_unary_errors(callable_): diff --git a/google/api_core/retry/retry_unary.py b/google/api_core/retry/retry_unary.py index ab1b4030b..d5dff663f 100644 --- a/google/api_core/retry/retry_unary.py +++ b/google/api_core/retry/retry_unary.py @@ -83,7 +83,7 @@ def check_if_exists(): def retry_target( - target: Callable[_P, _R], + target: Callable[[], _R], predicate: Callable[[Exception], bool], sleep_generator: Iterable[float], timeout: float | None = None, diff --git a/google/api_core/retry/retry_unary_async.py b/google/api_core/retry/retry_unary_async.py index 3bdf6c719..e76a37bb0 100644 --- a/google/api_core/retry/retry_unary_async.py +++ b/google/api_core/retry/retry_unary_async.py @@ -94,7 +94,7 @@ async def check_if_exists(): async def retry_target( - target: Callable[_P, Awaitable[_R]], + target: Callable[[], Awaitable[_R]], predicate: Callable[[Exception], bool], sleep_generator: Iterable[float], timeout: float | None = None, diff --git a/noxfile.py b/noxfile.py index ada6f330f..cffef972d 100644 --- a/noxfile.py +++ b/noxfile.py @@ -275,9 +275,7 @@ def pytype(session): @nox.session(python=DEFAULT_PYTHON_VERSION) def mypy(session): """Run type-checking.""" - # TODO(https://github.com/googleapis/python-api-core/issues/682): - # Use the latest version of mypy instead of mypy<1.11.0 - session.install(".[grpc,async_rest]", "mypy<1.11.0") + session.install(".[grpc,async_rest]", "mypy") session.install( "types-setuptools", "types-requests", diff --git a/tests/asyncio/test_grpc_helpers_async.py b/tests/asyncio/test_grpc_helpers_async.py index d8f20ae44..aa8d5d10b 100644 --- a/tests/asyncio/test_grpc_helpers_async.py +++ b/tests/asyncio/test_grpc_helpers_async.py @@ -319,7 +319,7 @@ def test_awaitable_grpc_call(): """ AwaitableGrpcCall type should be an Awaitable and a grpc.aio.Call. """ - instance = grpc_helpers_async.AwaitableGrpcCall[int]() + instance = grpc_helpers_async.AwaitableGrpcCall() assert isinstance(instance, grpc.aio.Call) # should implement __await__ assert hasattr(instance, "__await__") From c1b8afa4e2abe256e70651defccdc285f104ed19 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Wed, 22 Jan 2025 13:46:51 -0800 Subject: [PATCH 058/101] fix: memory leak in bidi classes (#770) * clean unneeded fields after close * added assertions to tests --------- Co-authored-by: Anthonios Partheniou --- google/api_core/bidi.py | 3 +++ tests/unit/test_bidi.py | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/google/api_core/bidi.py b/google/api_core/bidi.py index 78d98b98b..4e800c88e 100644 --- a/google/api_core/bidi.py +++ b/google/api_core/bidi.py @@ -306,6 +306,8 @@ def close(self): self._request_queue.put(None) self.call.cancel() self._request_generator = None + self._initial_request = None + self._callbacks = [] # Don't set self.call to None. Keep it around so that send/recv can # raise the error. @@ -717,6 +719,7 @@ def stop(self): _LOGGER.warning("Background thread did not exit.") self._thread = None + self._on_response = None @property def is_active(self): diff --git a/tests/unit/test_bidi.py b/tests/unit/test_bidi.py index c196a6821..cca9a2143 100644 --- a/tests/unit/test_bidi.py +++ b/tests/unit/test_bidi.py @@ -296,6 +296,9 @@ def test_close(self): # ensure the request queue was signaled to stop. assert bidi_rpc.pending_requests == 1 assert bidi_rpc._request_queue.get() is None + # ensure request and callbacks are cleaned up + assert bidi_rpc._initial_request is None + assert not bidi_rpc._callbacks def test_close_no_rpc(self): bidi_rpc = bidi.BidiRpc(None) @@ -623,6 +626,8 @@ def cancel_side_effect(): assert bidi_rpc.pending_requests == 1 assert bidi_rpc._request_queue.get() is None assert bidi_rpc._finalized + assert bidi_rpc._initial_request is None + assert not bidi_rpc._callbacks def test_reopen_failure_on_rpc_restart(self): error1 = ValueError("1") @@ -777,6 +782,7 @@ def on_response(response): consumer.stop() assert consumer.is_active is False + assert consumer._on_response is None def test_wake_on_error(self): should_continue = threading.Event() @@ -884,6 +890,7 @@ def close_side_effect(): consumer.stop() assert consumer.is_active is False + assert consumer._on_response is None # calling stop twice should not result in an error. consumer.stop() From d15131317f443dde08f1130dfef72c30faaa8259 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Wed, 22 Jan 2025 23:27:59 +0100 Subject: [PATCH 059/101] chore(deps): update all dependencies (#780) Co-authored-by: Anthonios Partheniou --- .kokoro/docker/docs/requirements.txt | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.kokoro/docker/docs/requirements.txt b/.kokoro/docker/docs/requirements.txt index a9360a25b..e91a2eef5 100644 --- a/.kokoro/docker/docs/requirements.txt +++ b/.kokoro/docker/docs/requirements.txt @@ -8,9 +8,9 @@ argcomplete==3.5.3 \ --hash=sha256:2ab2c4a215c59fd6caaff41a869480a23e8f6a5f910b266c1808037f4e375b61 \ --hash=sha256:c12bf50eded8aebb298c7b7da7a5ff3ee24dffd9f5281867dfe1424b58c55392 # via nox -cachetools==5.5.0 \ - --hash=sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292 \ - --hash=sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a +cachetools==5.5.1 \ + --hash=sha256:70f238fbba50383ef62e55c6aff6d9673175fe59f7c6782c7a0b9e38f4a9df95 \ + --hash=sha256:b76651fdc3b24ead3c648bbdeeb940c1b04d365b38b4af66788f9ec4a81d42bb # via google-auth certifi==2024.12.14 \ --hash=sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56 \ @@ -124,9 +124,9 @@ distlib==0.3.9 \ --hash=sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87 \ --hash=sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403 # via virtualenv -filelock==3.16.1 \ - --hash=sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0 \ - --hash=sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435 +filelock==3.17.0 \ + --hash=sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338 \ + --hash=sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e # via virtualenv gcp-docuploader==0.6.5 \ --hash=sha256:30221d4ac3e5a2b9c69aa52fdbef68cc3f27d0e6d0d90e220fc024584b8d2318 \ @@ -291,7 +291,7 @@ urllib3==2.3.0 \ --hash=sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df \ --hash=sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d # via requests -virtualenv==20.28.1 \ - --hash=sha256:412773c85d4dab0409b83ec36f7a6499e72eaf08c80e81e9576bca61831c71cb \ - --hash=sha256:5d34ab240fdb5d21549b76f9e8ff3af28252f5499fb6d6f031adac4e5a8c5329 +virtualenv==20.29.1 \ + --hash=sha256:4e4cb403c0b0da39e13b46b1b2476e505cb0046b25f242bee80f62bf990b2779 \ + --hash=sha256:b8b8970138d32fb606192cb97f6cd4bb644fa486be9308fb9b63f81091b5dc35 # via nox From 0ec18254b90721684679a98bcacef4615467a227 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Fri, 24 Jan 2025 13:07:08 -0500 Subject: [PATCH 060/101] docs: Add warnings regarding consuming externally sourced credentials (#783) --- google/api_core/client_options.py | 12 ++++++++++ google/api_core/grpc_helpers.py | 24 +++++++++++++++++++ google/api_core/grpc_helpers_async.py | 12 ++++++++++ .../api_core/operations_v1/transports/base.py | 12 ++++++++++ .../api_core/operations_v1/transports/rest.py | 12 ++++++++++ 5 files changed, 72 insertions(+) diff --git a/google/api_core/client_options.py b/google/api_core/client_options.py index e3bddfeb2..d11665d22 100644 --- a/google/api_core/client_options.py +++ b/google/api_core/client_options.py @@ -69,6 +69,18 @@ class ClientOptions(object): quota belongs to. credentials_file (Optional[str]): A path to a file storing credentials. ``credentials_file` and ``api_key`` are mutually exclusive. + + .. warning:: + Important: If you accept a credential configuration (credential JSON/File/Stream) + from an external source for authentication to Google Cloud Platform, you must + validate it before providing it to any Google API or client library. Providing an + unvalidated credential configuration to Google APIs or libraries can compromise + the security of your systems and data. For more information, refer to + `Validate credential configurations from external sources`_. + + .. _Validate credential configurations from external sources: + + https://cloud.google.com/docs/authentication/external/externally-sourced-credentials scopes (Optional[Sequence[str]]): OAuth access token override scopes. api_key (Optional[str]): Google API key. ``credentials_file`` and ``api_key`` are mutually exclusive. diff --git a/google/api_core/grpc_helpers.py b/google/api_core/grpc_helpers.py index 1dcbb8b92..079630248 100644 --- a/google/api_core/grpc_helpers.py +++ b/google/api_core/grpc_helpers.py @@ -216,6 +216,18 @@ def _create_composite_credentials( credentials_file (str): A file with credentials that can be loaded with :func:`google.auth.load_credentials_from_file`. This argument is mutually exclusive with credentials. + + .. warning:: + Important: If you accept a credential configuration (credential JSON/File/Stream) + from an external source for authentication to Google Cloud Platform, you must + validate it before providing it to any Google API or client library. Providing an + unvalidated credential configuration to Google APIs or libraries can compromise + the security of your systems and data. For more information, refer to + `Validate credential configurations from external sources`_. + + .. _Validate credential configurations from external sources: + + https://cloud.google.com/docs/authentication/external/externally-sourced-credentials default_scopes (Sequence[str]): A optional list of scopes needed for this service. These are only used when credentials are not specified and are passed to :func:`google.auth.default`. @@ -316,6 +328,18 @@ def create_channel( credentials_file (str): A file with credentials that can be loaded with :func:`google.auth.load_credentials_from_file`. This argument is mutually exclusive with credentials. + + .. warning:: + Important: If you accept a credential configuration (credential JSON/File/Stream) + from an external source for authentication to Google Cloud Platform, you must + validate it before providing it to any Google API or client library. Providing an + unvalidated credential configuration to Google APIs or libraries can compromise + the security of your systems and data. For more information, refer to + `Validate credential configurations from external sources`_. + + .. _Validate credential configurations from external sources: + + https://cloud.google.com/docs/authentication/external/externally-sourced-credentials quota_project_id (str): An optional project to use for billing and quota. default_scopes (Sequence[str]): Default scopes passed by a Google client library. Use 'scopes' for user-defined scopes. diff --git a/google/api_core/grpc_helpers_async.py b/google/api_core/grpc_helpers_async.py index 269604568..af6614302 100644 --- a/google/api_core/grpc_helpers_async.py +++ b/google/api_core/grpc_helpers_async.py @@ -236,6 +236,18 @@ def create_channel( credentials_file (str): A file with credentials that can be loaded with :func:`google.auth.load_credentials_from_file`. This argument is mutually exclusive with credentials. + + .. warning:: + Important: If you accept a credential configuration (credential JSON/File/Stream) + from an external source for authentication to Google Cloud Platform, you must + validate it before providing it to any Google API or client library. Providing an + unvalidated credential configuration to Google APIs or libraries can compromise + the security of your systems and data. For more information, refer to + `Validate credential configurations from external sources`_. + + .. _Validate credential configurations from external sources: + + https://cloud.google.com/docs/authentication/external/externally-sourced-credentials quota_project_id (str): An optional project to use for billing and quota. default_scopes (Sequence[str]): Default scopes passed by a Google client library. Use 'scopes' for user-defined scopes. diff --git a/google/api_core/operations_v1/transports/base.py b/google/api_core/operations_v1/transports/base.py index 50e137612..71764c1e9 100644 --- a/google/api_core/operations_v1/transports/base.py +++ b/google/api_core/operations_v1/transports/base.py @@ -72,6 +72,18 @@ def __init__( credentials_file (Optional[str]): A file with credentials that can be loaded with :func:`google.auth.load_credentials_from_file`. This argument is mutually exclusive with credentials. + + .. warning:: + Important: If you accept a credential configuration (credential JSON/File/Stream) + from an external source for authentication to Google Cloud Platform, you must + validate it before providing it to any Google API or client library. Providing an + unvalidated credential configuration to Google APIs or libraries can compromise + the security of your systems and data. For more information, refer to + `Validate credential configurations from external sources`_. + + .. _Validate credential configurations from external sources: + + https://cloud.google.com/docs/authentication/external/externally-sourced-credentials scopes (Optional[Sequence[str]]): A list of scopes. quota_project_id (Optional[str]): An optional project to use for billing and quota. diff --git a/google/api_core/operations_v1/transports/rest.py b/google/api_core/operations_v1/transports/rest.py index 766a6685b..0705c518a 100644 --- a/google/api_core/operations_v1/transports/rest.py +++ b/google/api_core/operations_v1/transports/rest.py @@ -94,6 +94,18 @@ def __init__( credentials_file (Optional[str]): A file with credentials that can be loaded with :func:`google.auth.load_credentials_from_file`. This argument is ignored if ``channel`` is provided. + + .. warning:: + Important: If you accept a credential configuration (credential JSON/File/Stream) + from an external source for authentication to Google Cloud Platform, you must + validate it before providing it to any Google API or client library. Providing an + unvalidated credential configuration to Google APIs or libraries can compromise + the security of your systems and data. For more information, refer to + `Validate credential configurations from external sources`_. + + .. _Validate credential configurations from external sources: + + https://cloud.google.com/docs/authentication/external/externally-sourced-credentials scopes (Optional(Sequence[str])): A list of scopes. This argument is ignored if ``channel`` is provided. client_cert_source_for_mtls (Callable[[], Tuple[bytes, bytes]]): Client From b51ca57409cd787d6f11d7f844b39a432784f8b3 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 27 Jan 2025 15:39:42 -0500 Subject: [PATCH 061/101] chore(main): release 2.24.1 (#778) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(main): release 2.24.1 * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> Co-authored-by: Owl Bot --- .kokoro/docker/docs/requirements.txt | 18 +++++++++--------- CHANGELOG.md | 13 +++++++++++++ google/api_core/version.py | 2 +- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/.kokoro/docker/docs/requirements.txt b/.kokoro/docker/docs/requirements.txt index e91a2eef5..a9360a25b 100644 --- a/.kokoro/docker/docs/requirements.txt +++ b/.kokoro/docker/docs/requirements.txt @@ -8,9 +8,9 @@ argcomplete==3.5.3 \ --hash=sha256:2ab2c4a215c59fd6caaff41a869480a23e8f6a5f910b266c1808037f4e375b61 \ --hash=sha256:c12bf50eded8aebb298c7b7da7a5ff3ee24dffd9f5281867dfe1424b58c55392 # via nox -cachetools==5.5.1 \ - --hash=sha256:70f238fbba50383ef62e55c6aff6d9673175fe59f7c6782c7a0b9e38f4a9df95 \ - --hash=sha256:b76651fdc3b24ead3c648bbdeeb940c1b04d365b38b4af66788f9ec4a81d42bb +cachetools==5.5.0 \ + --hash=sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292 \ + --hash=sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a # via google-auth certifi==2024.12.14 \ --hash=sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56 \ @@ -124,9 +124,9 @@ distlib==0.3.9 \ --hash=sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87 \ --hash=sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403 # via virtualenv -filelock==3.17.0 \ - --hash=sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338 \ - --hash=sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e +filelock==3.16.1 \ + --hash=sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0 \ + --hash=sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435 # via virtualenv gcp-docuploader==0.6.5 \ --hash=sha256:30221d4ac3e5a2b9c69aa52fdbef68cc3f27d0e6d0d90e220fc024584b8d2318 \ @@ -291,7 +291,7 @@ urllib3==2.3.0 \ --hash=sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df \ --hash=sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d # via requests -virtualenv==20.29.1 \ - --hash=sha256:4e4cb403c0b0da39e13b46b1b2476e505cb0046b25f242bee80f62bf990b2779 \ - --hash=sha256:b8b8970138d32fb606192cb97f6cd4bb644fa486be9308fb9b63f81091b5dc35 +virtualenv==20.28.1 \ + --hash=sha256:412773c85d4dab0409b83ec36f7a6499e72eaf08c80e81e9576bca61831c71cb \ + --hash=sha256:5d34ab240fdb5d21549b76f9e8ff3af28252f5499fb6d6f031adac4e5a8c5329 # via nox diff --git a/CHANGELOG.md b/CHANGELOG.md index c7eacaf0b..5717d6e60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ [1]: https://pypi.org/project/google-api-core/#history +## [2.24.1](https://github.com/googleapis/python-api-core/compare/v2.24.0...v2.24.1) (2025-01-24) + + +### Bug Fixes + +* Memory leak in bidi classes ([#770](https://github.com/googleapis/python-api-core/issues/770)) ([c1b8afa](https://github.com/googleapis/python-api-core/commit/c1b8afa4e2abe256e70651defccdc285f104ed19)) +* Resolve the issue where rpc timeout of 0 is used when timeout expires ([#776](https://github.com/googleapis/python-api-core/issues/776)) ([a5604a5](https://github.com/googleapis/python-api-core/commit/a5604a55070c6d92618d078191bf99f4c168d5f6)) + + +### Documentation + +* Add warnings regarding consuming externally sourced credentials ([#783](https://github.com/googleapis/python-api-core/issues/783)) ([0ec1825](https://github.com/googleapis/python-api-core/commit/0ec18254b90721684679a98bcacef4615467a227)) + ## [2.24.0](https://github.com/googleapis/python-api-core/compare/v2.23.0...v2.24.0) (2024-12-06) diff --git a/google/api_core/version.py b/google/api_core/version.py index 84f6b4643..96f84438a 100644 --- a/google/api_core/version.py +++ b/google/api_core/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "2.24.0" +__version__ = "2.24.1" From 7fbd5fda207d856b5835d0e0166df52e4819522a Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Thu, 30 Jan 2025 23:46:29 +0100 Subject: [PATCH 062/101] chore(deps): update all dependencies (#784) --- .kokoro/docker/docs/requirements.txt | 42 ++++++++++++++-------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/.kokoro/docker/docs/requirements.txt b/.kokoro/docker/docs/requirements.txt index a9360a25b..d6e938947 100644 --- a/.kokoro/docker/docs/requirements.txt +++ b/.kokoro/docker/docs/requirements.txt @@ -8,9 +8,9 @@ argcomplete==3.5.3 \ --hash=sha256:2ab2c4a215c59fd6caaff41a869480a23e8f6a5f910b266c1808037f4e375b61 \ --hash=sha256:c12bf50eded8aebb298c7b7da7a5ff3ee24dffd9f5281867dfe1424b58c55392 # via nox -cachetools==5.5.0 \ - --hash=sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292 \ - --hash=sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a +cachetools==5.5.1 \ + --hash=sha256:70f238fbba50383ef62e55c6aff6d9673175fe59f7c6782c7a0b9e38f4a9df95 \ + --hash=sha256:b76651fdc3b24ead3c648bbdeeb940c1b04d365b38b4af66788f9ec4a81d42bb # via google-auth certifi==2024.12.14 \ --hash=sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56 \ @@ -124,23 +124,23 @@ distlib==0.3.9 \ --hash=sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87 \ --hash=sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403 # via virtualenv -filelock==3.16.1 \ - --hash=sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0 \ - --hash=sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435 +filelock==3.17.0 \ + --hash=sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338 \ + --hash=sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e # via virtualenv gcp-docuploader==0.6.5 \ --hash=sha256:30221d4ac3e5a2b9c69aa52fdbef68cc3f27d0e6d0d90e220fc024584b8d2318 \ --hash=sha256:b7458ef93f605b9d46a4bf3a8dc1755dad1f31d030c8679edf304e343b347eea # via -r requirements.in -google-api-core==2.24.0 \ - --hash=sha256:10d82ac0fca69c82a25b3efdeefccf6f28e02ebb97925a8cce8edbfe379929d9 \ - --hash=sha256:e255640547a597a4da010876d333208ddac417d60add22b6851a0c66a831fcaf +google-api-core==2.24.1 \ + --hash=sha256:bc78d608f5a5bf853b80bd70a795f703294de656c096c0968320830a4bc280f1 \ + --hash=sha256:f8b36f5456ab0dd99a1b693a40a31d1e7757beea380ad1b38faaf8941eae9d8a # via # google-cloud-core # google-cloud-storage -google-auth==2.37.0 \ - --hash=sha256:0054623abf1f9c83492c63d3f47e77f0a544caa3d40b2d98e099a611c2dd5d00 \ - --hash=sha256:42664f18290a6be591be5329a96fe30184be1a1badb7292a7f686a9659de9ca0 +google-auth==2.38.0 \ + --hash=sha256:8285113607d3b80a3f1543b75962447ba8a09fe85783432a784fdeef6ac094c4 \ + --hash=sha256:e7dae6694313f434a2727bf2906f27ad259bae090d7aa896590d86feec3d9d4a # via # google-api-core # google-cloud-core @@ -149,9 +149,9 @@ google-cloud-core==2.4.1 \ --hash=sha256:9b7749272a812bde58fff28868d0c5e2f585b82f37e09a1f6ed2d4d10f134073 \ --hash=sha256:a9e6a4422b9ac5c29f79a0ede9485473338e2ce78d91f2370c01e730eab22e61 # via google-cloud-storage -google-cloud-storage==2.19.0 \ - --hash=sha256:aeb971b5c29cf8ab98445082cbfe7b161a1f48ed275822f59ed3f1524ea54fba \ - --hash=sha256:cd05e9e7191ba6cb68934d8eb76054d9be4562aa89dbc4236feee4d7d51342b2 +google-cloud-storage==3.0.0 \ + --hash=sha256:2accb3e828e584888beff1165e5f3ac61aa9088965eb0165794a82d8c7f95297 \ + --hash=sha256:f85fd059650d2dbb0ac158a9a6b304b66143b35ed2419afec2905ca522eb2c6a # via gcp-docuploader google-crc32c==1.6.0 \ --hash=sha256:05e2d8c9a2f853ff116db9706b4a27350587f341eda835f46db3c0a8c8ce2f24 \ @@ -208,9 +208,9 @@ platformdirs==4.3.6 \ --hash=sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907 \ --hash=sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb # via virtualenv -proto-plus==1.25.0 \ - --hash=sha256:c91fc4a65074ade8e458e95ef8bac34d4008daa7cce4a12d6707066fca648961 \ - --hash=sha256:fbb17f57f7bd05a68b7707e745e26528b0b3c34e378db91eef93912c54982d91 +proto-plus==1.26.0 \ + --hash=sha256:6e93d5f5ca267b54300880fff156b6a3386b3fa3f43b1da62e680fc0c586ef22 \ + --hash=sha256:bf2dfaa3da281fc3187d12d224c707cb57214fb2c22ba854eb0c105a3fb2d4d7 # via google-api-core protobuf==5.29.3 \ --hash=sha256:0a18ed4a24198528f2333802eb075e59dea9d679ab7a6c5efb017a59004d849f \ @@ -291,7 +291,7 @@ urllib3==2.3.0 \ --hash=sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df \ --hash=sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d # via requests -virtualenv==20.28.1 \ - --hash=sha256:412773c85d4dab0409b83ec36f7a6499e72eaf08c80e81e9576bca61831c71cb \ - --hash=sha256:5d34ab240fdb5d21549b76f9e8ff3af28252f5499fb6d6f031adac4e5a8c5329 +virtualenv==20.29.1 \ + --hash=sha256:4e4cb403c0b0da39e13b46b1b2476e505cb0046b25f242bee80f62bf990b2779 \ + --hash=sha256:b8b8970138d32fb606192cb97f6cd4bb644fa486be9308fb9b63f81091b5dc35 # via nox From 6b15116c885deff1e432b7d70d0d5d8e795fce1e Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Tue, 25 Feb 2025 11:52:16 -0500 Subject: [PATCH 063/101] build: use mypy<1.15.0 until #799 is fixed (#800) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * build: use mypy<1.15.0 until #799 is fixed * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: Owl Bot --- .kokoro/docker/docs/requirements.txt | 42 ++++++++++++++-------------- noxfile.py | 4 ++- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/.kokoro/docker/docs/requirements.txt b/.kokoro/docker/docs/requirements.txt index d6e938947..a9360a25b 100644 --- a/.kokoro/docker/docs/requirements.txt +++ b/.kokoro/docker/docs/requirements.txt @@ -8,9 +8,9 @@ argcomplete==3.5.3 \ --hash=sha256:2ab2c4a215c59fd6caaff41a869480a23e8f6a5f910b266c1808037f4e375b61 \ --hash=sha256:c12bf50eded8aebb298c7b7da7a5ff3ee24dffd9f5281867dfe1424b58c55392 # via nox -cachetools==5.5.1 \ - --hash=sha256:70f238fbba50383ef62e55c6aff6d9673175fe59f7c6782c7a0b9e38f4a9df95 \ - --hash=sha256:b76651fdc3b24ead3c648bbdeeb940c1b04d365b38b4af66788f9ec4a81d42bb +cachetools==5.5.0 \ + --hash=sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292 \ + --hash=sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a # via google-auth certifi==2024.12.14 \ --hash=sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56 \ @@ -124,23 +124,23 @@ distlib==0.3.9 \ --hash=sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87 \ --hash=sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403 # via virtualenv -filelock==3.17.0 \ - --hash=sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338 \ - --hash=sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e +filelock==3.16.1 \ + --hash=sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0 \ + --hash=sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435 # via virtualenv gcp-docuploader==0.6.5 \ --hash=sha256:30221d4ac3e5a2b9c69aa52fdbef68cc3f27d0e6d0d90e220fc024584b8d2318 \ --hash=sha256:b7458ef93f605b9d46a4bf3a8dc1755dad1f31d030c8679edf304e343b347eea # via -r requirements.in -google-api-core==2.24.1 \ - --hash=sha256:bc78d608f5a5bf853b80bd70a795f703294de656c096c0968320830a4bc280f1 \ - --hash=sha256:f8b36f5456ab0dd99a1b693a40a31d1e7757beea380ad1b38faaf8941eae9d8a +google-api-core==2.24.0 \ + --hash=sha256:10d82ac0fca69c82a25b3efdeefccf6f28e02ebb97925a8cce8edbfe379929d9 \ + --hash=sha256:e255640547a597a4da010876d333208ddac417d60add22b6851a0c66a831fcaf # via # google-cloud-core # google-cloud-storage -google-auth==2.38.0 \ - --hash=sha256:8285113607d3b80a3f1543b75962447ba8a09fe85783432a784fdeef6ac094c4 \ - --hash=sha256:e7dae6694313f434a2727bf2906f27ad259bae090d7aa896590d86feec3d9d4a +google-auth==2.37.0 \ + --hash=sha256:0054623abf1f9c83492c63d3f47e77f0a544caa3d40b2d98e099a611c2dd5d00 \ + --hash=sha256:42664f18290a6be591be5329a96fe30184be1a1badb7292a7f686a9659de9ca0 # via # google-api-core # google-cloud-core @@ -149,9 +149,9 @@ google-cloud-core==2.4.1 \ --hash=sha256:9b7749272a812bde58fff28868d0c5e2f585b82f37e09a1f6ed2d4d10f134073 \ --hash=sha256:a9e6a4422b9ac5c29f79a0ede9485473338e2ce78d91f2370c01e730eab22e61 # via google-cloud-storage -google-cloud-storage==3.0.0 \ - --hash=sha256:2accb3e828e584888beff1165e5f3ac61aa9088965eb0165794a82d8c7f95297 \ - --hash=sha256:f85fd059650d2dbb0ac158a9a6b304b66143b35ed2419afec2905ca522eb2c6a +google-cloud-storage==2.19.0 \ + --hash=sha256:aeb971b5c29cf8ab98445082cbfe7b161a1f48ed275822f59ed3f1524ea54fba \ + --hash=sha256:cd05e9e7191ba6cb68934d8eb76054d9be4562aa89dbc4236feee4d7d51342b2 # via gcp-docuploader google-crc32c==1.6.0 \ --hash=sha256:05e2d8c9a2f853ff116db9706b4a27350587f341eda835f46db3c0a8c8ce2f24 \ @@ -208,9 +208,9 @@ platformdirs==4.3.6 \ --hash=sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907 \ --hash=sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb # via virtualenv -proto-plus==1.26.0 \ - --hash=sha256:6e93d5f5ca267b54300880fff156b6a3386b3fa3f43b1da62e680fc0c586ef22 \ - --hash=sha256:bf2dfaa3da281fc3187d12d224c707cb57214fb2c22ba854eb0c105a3fb2d4d7 +proto-plus==1.25.0 \ + --hash=sha256:c91fc4a65074ade8e458e95ef8bac34d4008daa7cce4a12d6707066fca648961 \ + --hash=sha256:fbb17f57f7bd05a68b7707e745e26528b0b3c34e378db91eef93912c54982d91 # via google-api-core protobuf==5.29.3 \ --hash=sha256:0a18ed4a24198528f2333802eb075e59dea9d679ab7a6c5efb017a59004d849f \ @@ -291,7 +291,7 @@ urllib3==2.3.0 \ --hash=sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df \ --hash=sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d # via requests -virtualenv==20.29.1 \ - --hash=sha256:4e4cb403c0b0da39e13b46b1b2476e505cb0046b25f242bee80f62bf990b2779 \ - --hash=sha256:b8b8970138d32fb606192cb97f6cd4bb644fa486be9308fb9b63f81091b5dc35 +virtualenv==20.28.1 \ + --hash=sha256:412773c85d4dab0409b83ec36f7a6499e72eaf08c80e81e9576bca61831c71cb \ + --hash=sha256:5d34ab240fdb5d21549b76f9e8ff3af28252f5499fb6d6f031adac4e5a8c5329 # via nox diff --git a/noxfile.py b/noxfile.py index cffef972d..2bbd011a9 100644 --- a/noxfile.py +++ b/noxfile.py @@ -275,7 +275,9 @@ def pytype(session): @nox.session(python=DEFAULT_PYTHON_VERSION) def mypy(session): """Run type-checking.""" - session.install(".[grpc,async_rest]", "mypy") + # TODO(https://github.com/googleapis/python-api-core/issues/799): + # Remove mypy constraint. We should use the latest version of mypy. + session.install(".[grpc,async_rest]", "mypy<1.15.0") session.install( "types-setuptools", "types-requests", From 971dc0398a3d7e61a583394371f19694f6eb2fd9 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Tue, 25 Feb 2025 12:26:52 -0500 Subject: [PATCH 064/101] build: Add presubmits for Python 3.14 pre-release (#786) * build: Add presubmits for Python 3.14 pre-release * build: use mypy<1.15.0 until #799 is fixed * update required checks --- .github/sync-repo-settings.yaml | 22 ++++++++++++++++++++++ .github/workflows/unittest.yml | 2 ++ noxfile.py | 5 ++--- testing/constraints-3.14.txt | 0 tests/asyncio/test_operation_async.py | 2 ++ 5 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 testing/constraints-3.14.txt diff --git a/.github/sync-repo-settings.yaml b/.github/sync-repo-settings.yaml index a19b27a7c..b724bada2 100644 --- a/.github/sync-repo-settings.yaml +++ b/.github/sync-repo-settings.yaml @@ -17,15 +17,37 @@ branchProtectionRules: - 'unit_grpc_gcp-3.10' - 'unit_grpc_gcp-3.11' - 'unit_grpc_gcp-3.12' + - 'unit_grpc_gcp-3.13' + - 'unit_grpc_gcp-3.14' - 'unit-3.7' - 'unit-3.8' - 'unit-3.9' - 'unit-3.10' - 'unit-3.11' - 'unit-3.12' + - 'unit-3.13' + - 'unit-3.14' - 'unit_wo_grpc-3.10' - 'unit_wo_grpc-3.11' - 'unit_wo_grpc-3.12' + - 'unit_wo_grpc-3.13' + - 'unit_wo_grpc-3.14' + - 'unit_w_prerelease_deps-3.7' + - 'unit_w_prerelease_deps-3.8' + - 'unit_w_prerelease_deps-3.9' + - 'unit_w_prerelease_deps-3.10' + - 'unit_w_prerelease_deps-3.11' + - 'unit_w_prerelease_deps-3.12' + - 'unit_w_prerelease_deps-3.13' + - 'unit_w_prerelease_deps-3.14' + - 'unit_w_async_rest_extra-3.7' + - 'unit_w_async_rest_extra-3.8' + - 'unit_w_async_rest_extra-3.9' + - 'unit_w_async_rest_extra-3.10' + - 'unit_w_async_rest_extra-3.11' + - 'unit_w_async_rest_extra-3.12' + - 'unit_w_async_rest_extra-3.13' + - 'unit_w_async_rest_extra-3.14' - 'cover' - 'docs' - 'docfx' diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 4e4fa25de..f260a6a55 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -23,6 +23,7 @@ jobs: - "3.11" - "3.12" - "3.13" + - "3.14" exclude: - option: "_wo_grpc" python: 3.7 @@ -37,6 +38,7 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} + allow-prereleases: true - name: Install nox run: | python -m pip install --upgrade setuptools pip wheel diff --git a/noxfile.py b/noxfile.py index 2bbd011a9..c318c49f8 100644 --- a/noxfile.py +++ b/noxfile.py @@ -28,7 +28,7 @@ # Black and flake8 clash on the syntax for ignoring flake8's F401 in this file. BLACK_EXCLUDES = ["--exclude", "^/google/api_core/operations_v1/__init__.py"] -PYTHON_VERSIONS = ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] +PYTHON_VERSIONS = ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] DEFAULT_PYTHON_VERSION = "3.10" CURRENT_DIRECTORY = pathlib.Path(__file__).parent.absolute() @@ -95,8 +95,7 @@ def install_prerelease_dependencies(session, constraints_path): prerel_deps = [ "google-auth", "googleapis-common-protos", - # Exclude grpcio!=1.67.0rc1 which does not support python 3.13 - "grpcio!=1.67.0rc1", + "grpcio", "grpcio-status", "proto-plus", "protobuf", diff --git a/testing/constraints-3.14.txt b/testing/constraints-3.14.txt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/asyncio/test_operation_async.py b/tests/asyncio/test_operation_async.py index 939be0947..9d9fb5d25 100644 --- a/tests/asyncio/test_operation_async.py +++ b/tests/asyncio/test_operation_async.py @@ -84,6 +84,7 @@ async def test_constructor(): assert await future.running() +@pytest.mark.asyncio def test_metadata(): expected_metadata = struct_pb2.Struct() future, _, _ = make_operation_future( @@ -176,6 +177,7 @@ async def test_unexpected_result(unused_sleep): assert "Unexpected state" in "{!r}".format(exception) +@pytest.mark.asyncio def test_from_gapic(): operation_proto = make_operation_proto(done=True) operations_client = mock.create_autospec( From 86359b21a1bd0cf760889a0a20c75bf002d6cdcb Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Mon, 3 Mar 2025 11:19:03 -0500 Subject: [PATCH 065/101] build: fix mypy issues (#801) * build: fix mypy issues * remove mypy constraint --------- Co-authored-by: Daniel Sanche --- google/api_core/retry/retry_streaming.py | 4 ++-- google/api_core/retry/retry_streaming_async.py | 4 ++-- noxfile.py | 4 +--- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/google/api_core/retry/retry_streaming.py b/google/api_core/retry/retry_streaming.py index e113323be..00666841c 100644 --- a/google/api_core/retry/retry_streaming.py +++ b/google/api_core/retry/retry_streaming.py @@ -59,8 +59,8 @@ def retry_target_stream( [List[Exception], RetryFailureReason, Optional[float]], Tuple[Exception, Optional[Exception]], ] = build_retry_error, - init_args: _P.args = (), - init_kwargs: _P.kwargs = {}, + init_args: tuple = (), + init_kwargs: dict = {}, **kwargs, ) -> Generator[_Y, Any, None]: """Create a generator wrapper that retries the wrapped stream if it fails. diff --git a/google/api_core/retry/retry_streaming_async.py b/google/api_core/retry/retry_streaming_async.py index 2924ba141..942abf5fe 100644 --- a/google/api_core/retry/retry_streaming_async.py +++ b/google/api_core/retry/retry_streaming_async.py @@ -62,8 +62,8 @@ async def retry_target_stream( [list[Exception], RetryFailureReason, float | None], tuple[Exception, Exception | None], ] = build_retry_error, - init_args: _P.args = (), - init_kwargs: _P.kwargs = {}, + init_args: tuple = (), + init_kwargs: dict = {}, **kwargs, ) -> AsyncGenerator[_Y, None]: """Create a generator wrapper that retries the wrapped stream if it fails. diff --git a/noxfile.py b/noxfile.py index c318c49f8..ac21330ef 100644 --- a/noxfile.py +++ b/noxfile.py @@ -274,9 +274,7 @@ def pytype(session): @nox.session(python=DEFAULT_PYTHON_VERSION) def mypy(session): """Run type-checking.""" - # TODO(https://github.com/googleapis/python-api-core/issues/799): - # Remove mypy constraint. We should use the latest version of mypy. - session.install(".[grpc,async_rest]", "mypy<1.15.0") + session.install(".[grpc,async_rest]", "mypy") session.install( "types-setuptools", "types-requests", From 314e34886d8b5543476eb080f7776890401cbd01 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Mon, 3 Mar 2025 16:43:45 +0000 Subject: [PATCH 066/101] chore(python): conditionally load credentials in .kokoro/build.sh (#798) Source-Link: https://github.com/googleapis/synthtool/commit/aa69fb74717c8f4c58c60f8cc101d3f4b2c07b09 Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-python:latest@sha256:f016446d6e520e5fb552c45b110cba3f217bffdd3d06bdddd076e9e6d13266cf Co-authored-by: Owl Bot Co-authored-by: Anthonios Partheniou --- .github/.OwlBot.lock.yaml | 4 ++-- .kokoro/build.sh | 20 ++++++++++++++------ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 4c0027ff1..3f7634f25 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest - digest: sha256:04c35dc5f49f0f503a306397d6d043685f8d2bb822ab515818c4208d7fb2db3a -# created: 2025-01-16T15:24:11.364245182Z + digest: sha256:f016446d6e520e5fb552c45b110cba3f217bffdd3d06bdddd076e9e6d13266cf +# created: 2025-02-21T19:32:52.01306189Z diff --git a/.kokoro/build.sh b/.kokoro/build.sh index f05e867ca..d41b45aa1 100755 --- a/.kokoro/build.sh +++ b/.kokoro/build.sh @@ -15,11 +15,13 @@ set -eo pipefail +CURRENT_DIR=$(dirname "${BASH_SOURCE[0]}") + if [[ -z "${PROJECT_ROOT:-}" ]]; then - PROJECT_ROOT="github/python-api-core" + PROJECT_ROOT=$(realpath "${CURRENT_DIR}/..") fi -cd "${PROJECT_ROOT}" +pushd "${PROJECT_ROOT}" # Disable buffering, so that the logs stream through. export PYTHONUNBUFFERED=1 @@ -28,10 +30,16 @@ export PYTHONUNBUFFERED=1 env | grep KOKORO # Setup service account credentials. -export GOOGLE_APPLICATION_CREDENTIALS=${KOKORO_GFILE_DIR}/service-account.json +if [[ -f "${KOKORO_GFILE_DIR}/service-account.json" ]] +then + export GOOGLE_APPLICATION_CREDENTIALS=${KOKORO_GFILE_DIR}/service-account.json +fi # Setup project id. -export PROJECT_ID=$(cat "${KOKORO_GFILE_DIR}/project-id.json") +if [[ -f "${KOKORO_GFILE_DIR}/project-id.json" ]] +then + export PROJECT_ID=$(cat "${KOKORO_GFILE_DIR}/project-id.json") +fi # If this is a continuous build, send the test log to the FlakyBot. # See https://github.com/googleapis/repo-automation-bots/tree/main/packages/flakybot. @@ -46,7 +54,7 @@ fi # If NOX_SESSION is set, it only runs the specified session, # otherwise run all the sessions. if [[ -n "${NOX_SESSION:-}" ]]; then - python3 -m nox -s ${NOX_SESSION:-} + python3 -m nox -s ${NOX_SESSION:-} else - python3 -m nox + python3 -m nox fi From 687be7cbf629a61feb43ef37d3d920fa32b2d636 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Wed, 5 Mar 2025 16:41:40 -0500 Subject: [PATCH 067/101] fix(deps): allow protobuf 6.x (#804) --- pyproject.toml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index fda8f01bf..ea3a52268 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,12 +44,12 @@ classifiers = [ "Topic :: Internet", ] dependencies = [ - "googleapis-common-protos >= 1.56.2, < 2.0.dev0", - "protobuf >= 3.19.5, < 6.0.0.dev0, != 3.20.0, != 3.20.1, != 4.21.0, != 4.21.1, != 4.21.2, != 4.21.3, != 4.21.4, != 4.21.5", - "proto-plus >= 1.22.3, < 2.0.0dev", - "proto-plus >= 1.25.0, < 2.0.0dev; python_version >= '3.13'", - "google-auth >= 2.14.1, < 3.0.dev0", - "requests >= 2.18.0, < 3.0.0.dev0", + "googleapis-common-protos >= 1.56.2, < 2.0.0", + "protobuf >= 3.19.5, < 7.0.0, != 3.20.0, != 3.20.1, != 4.21.0, != 4.21.1, != 4.21.2, != 4.21.3, != 4.21.4, != 4.21.5", + "proto-plus >= 1.22.3, < 2.0.0", + "proto-plus >= 1.25.0, < 2.0.0; python_version >= '3.13'", + "google-auth >= 2.14.1, < 3.0.0", + "requests >= 2.18.0, < 3.0.0", ] dynamic = ["version"] From 7f0a3a83b2487bec7b6dd9d8d61d181d2ae471a3 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Thu, 6 Mar 2025 10:37:03 -0500 Subject: [PATCH 068/101] build: remove unused docs scripts (#803) * build: remove unused docs scripts * update owlbot lock file --- .github/.OwlBot.lock.yaml | 4 +- .kokoro/docker/docs/Dockerfile | 89 -------- .kokoro/docker/docs/requirements.in | 2 - .kokoro/docker/docs/requirements.txt | 297 --------------------------- .kokoro/docs/common.cfg | 67 ------ .kokoro/docs/docs-presubmit.cfg | 28 --- .kokoro/docs/docs.cfg | 1 - .kokoro/publish-docs.sh | 58 ------ 8 files changed, 2 insertions(+), 544 deletions(-) delete mode 100644 .kokoro/docker/docs/Dockerfile delete mode 100644 .kokoro/docker/docs/requirements.in delete mode 100644 .kokoro/docker/docs/requirements.txt delete mode 100644 .kokoro/docs/common.cfg delete mode 100644 .kokoro/docs/docs-presubmit.cfg delete mode 100644 .kokoro/docs/docs.cfg delete mode 100755 .kokoro/publish-docs.sh diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 3f7634f25..a6d11c019 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest - digest: sha256:f016446d6e520e5fb552c45b110cba3f217bffdd3d06bdddd076e9e6d13266cf -# created: 2025-02-21T19:32:52.01306189Z + digest: sha256:5581906b957284864632cde4e9c51d1cc66b0094990b27e689132fe5cd036046 +# created: 2025-03-04 diff --git a/.kokoro/docker/docs/Dockerfile b/.kokoro/docker/docs/Dockerfile deleted file mode 100644 index e5410e296..000000000 --- a/.kokoro/docker/docs/Dockerfile +++ /dev/null @@ -1,89 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from ubuntu:24.04 - -ENV DEBIAN_FRONTEND noninteractive - -# Ensure local Python is preferred over distribution Python. -ENV PATH /usr/local/bin:$PATH - -# Install dependencies. -RUN apt-get update \ - && apt-get install -y --no-install-recommends \ - apt-transport-https \ - build-essential \ - ca-certificates \ - curl \ - dirmngr \ - git \ - gpg-agent \ - graphviz \ - libbz2-dev \ - libdb5.3-dev \ - libexpat1-dev \ - libffi-dev \ - liblzma-dev \ - libreadline-dev \ - libsnappy-dev \ - libssl-dev \ - libsqlite3-dev \ - portaudio19-dev \ - redis-server \ - software-properties-common \ - ssh \ - sudo \ - tcl \ - tcl-dev \ - tk \ - tk-dev \ - uuid-dev \ - wget \ - zlib1g-dev \ - && add-apt-repository universe \ - && apt-get update \ - && apt-get -y install jq \ - && apt-get clean autoclean \ - && apt-get autoremove -y \ - && rm -rf /var/lib/apt/lists/* \ - && rm -f /var/cache/apt/archives/*.deb - - -###################### Install python 3.10.14 for docs/docfx session - -# Download python 3.10.14 -RUN wget https://www.python.org/ftp/python/3.10.14/Python-3.10.14.tgz - -# Extract files -RUN tar -xvf Python-3.10.14.tgz - -# Install python 3.10.14 -RUN ./Python-3.10.14/configure --enable-optimizations -RUN make altinstall - -ENV PATH /usr/local/bin/python3.10:$PATH - -###################### Install pip -RUN wget -O /tmp/get-pip.py 'https://bootstrap.pypa.io/get-pip.py' \ - && python3.10 /tmp/get-pip.py \ - && rm /tmp/get-pip.py - -# Test pip -RUN python3.10 -m pip - -# Install build requirements -COPY requirements.txt /requirements.txt -RUN python3.10 -m pip install --require-hashes -r requirements.txt - -CMD ["python3.10"] diff --git a/.kokoro/docker/docs/requirements.in b/.kokoro/docker/docs/requirements.in deleted file mode 100644 index 586bd0703..000000000 --- a/.kokoro/docker/docs/requirements.in +++ /dev/null @@ -1,2 +0,0 @@ -nox -gcp-docuploader diff --git a/.kokoro/docker/docs/requirements.txt b/.kokoro/docker/docs/requirements.txt deleted file mode 100644 index a9360a25b..000000000 --- a/.kokoro/docker/docs/requirements.txt +++ /dev/null @@ -1,297 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.10 -# by the following command: -# -# pip-compile --allow-unsafe --generate-hashes requirements.in -# -argcomplete==3.5.3 \ - --hash=sha256:2ab2c4a215c59fd6caaff41a869480a23e8f6a5f910b266c1808037f4e375b61 \ - --hash=sha256:c12bf50eded8aebb298c7b7da7a5ff3ee24dffd9f5281867dfe1424b58c55392 - # via nox -cachetools==5.5.0 \ - --hash=sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292 \ - --hash=sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a - # via google-auth -certifi==2024.12.14 \ - --hash=sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56 \ - --hash=sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db - # via requests -charset-normalizer==3.4.1 \ - --hash=sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537 \ - --hash=sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa \ - --hash=sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a \ - --hash=sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294 \ - --hash=sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b \ - --hash=sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd \ - --hash=sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601 \ - --hash=sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd \ - --hash=sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4 \ - --hash=sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d \ - --hash=sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2 \ - --hash=sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313 \ - --hash=sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd \ - --hash=sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa \ - --hash=sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8 \ - --hash=sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1 \ - --hash=sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2 \ - --hash=sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496 \ - --hash=sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d \ - --hash=sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b \ - --hash=sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e \ - --hash=sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a \ - --hash=sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4 \ - --hash=sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca \ - --hash=sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78 \ - --hash=sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408 \ - --hash=sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5 \ - --hash=sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3 \ - --hash=sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f \ - --hash=sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a \ - --hash=sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765 \ - --hash=sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6 \ - --hash=sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146 \ - --hash=sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6 \ - --hash=sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9 \ - --hash=sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd \ - --hash=sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c \ - --hash=sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f \ - --hash=sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545 \ - --hash=sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176 \ - --hash=sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770 \ - --hash=sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824 \ - --hash=sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f \ - --hash=sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf \ - --hash=sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487 \ - --hash=sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d \ - --hash=sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd \ - --hash=sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b \ - --hash=sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534 \ - --hash=sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f \ - --hash=sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b \ - --hash=sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9 \ - --hash=sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd \ - --hash=sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125 \ - --hash=sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9 \ - --hash=sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de \ - --hash=sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11 \ - --hash=sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d \ - --hash=sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35 \ - --hash=sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f \ - --hash=sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda \ - --hash=sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7 \ - --hash=sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a \ - --hash=sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971 \ - --hash=sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8 \ - --hash=sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41 \ - --hash=sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d \ - --hash=sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f \ - --hash=sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757 \ - --hash=sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a \ - --hash=sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886 \ - --hash=sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77 \ - --hash=sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76 \ - --hash=sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247 \ - --hash=sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85 \ - --hash=sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb \ - --hash=sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7 \ - --hash=sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e \ - --hash=sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6 \ - --hash=sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037 \ - --hash=sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1 \ - --hash=sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e \ - --hash=sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807 \ - --hash=sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407 \ - --hash=sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c \ - --hash=sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12 \ - --hash=sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3 \ - --hash=sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089 \ - --hash=sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd \ - --hash=sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e \ - --hash=sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00 \ - --hash=sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616 - # via requests -click==8.1.8 \ - --hash=sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2 \ - --hash=sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a - # via gcp-docuploader -colorlog==6.9.0 \ - --hash=sha256:5906e71acd67cb07a71e779c47c4bcb45fb8c2993eebe9e5adcd6a6f1b283eff \ - --hash=sha256:bfba54a1b93b94f54e1f4fe48395725a3d92fd2a4af702f6bd70946bdc0c6ac2 - # via - # gcp-docuploader - # nox -distlib==0.3.9 \ - --hash=sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87 \ - --hash=sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403 - # via virtualenv -filelock==3.16.1 \ - --hash=sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0 \ - --hash=sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435 - # via virtualenv -gcp-docuploader==0.6.5 \ - --hash=sha256:30221d4ac3e5a2b9c69aa52fdbef68cc3f27d0e6d0d90e220fc024584b8d2318 \ - --hash=sha256:b7458ef93f605b9d46a4bf3a8dc1755dad1f31d030c8679edf304e343b347eea - # via -r requirements.in -google-api-core==2.24.0 \ - --hash=sha256:10d82ac0fca69c82a25b3efdeefccf6f28e02ebb97925a8cce8edbfe379929d9 \ - --hash=sha256:e255640547a597a4da010876d333208ddac417d60add22b6851a0c66a831fcaf - # via - # google-cloud-core - # google-cloud-storage -google-auth==2.37.0 \ - --hash=sha256:0054623abf1f9c83492c63d3f47e77f0a544caa3d40b2d98e099a611c2dd5d00 \ - --hash=sha256:42664f18290a6be591be5329a96fe30184be1a1badb7292a7f686a9659de9ca0 - # via - # google-api-core - # google-cloud-core - # google-cloud-storage -google-cloud-core==2.4.1 \ - --hash=sha256:9b7749272a812bde58fff28868d0c5e2f585b82f37e09a1f6ed2d4d10f134073 \ - --hash=sha256:a9e6a4422b9ac5c29f79a0ede9485473338e2ce78d91f2370c01e730eab22e61 - # via google-cloud-storage -google-cloud-storage==2.19.0 \ - --hash=sha256:aeb971b5c29cf8ab98445082cbfe7b161a1f48ed275822f59ed3f1524ea54fba \ - --hash=sha256:cd05e9e7191ba6cb68934d8eb76054d9be4562aa89dbc4236feee4d7d51342b2 - # via gcp-docuploader -google-crc32c==1.6.0 \ - --hash=sha256:05e2d8c9a2f853ff116db9706b4a27350587f341eda835f46db3c0a8c8ce2f24 \ - --hash=sha256:18e311c64008f1f1379158158bb3f0c8d72635b9eb4f9545f8cf990c5668e59d \ - --hash=sha256:236c87a46cdf06384f614e9092b82c05f81bd34b80248021f729396a78e55d7e \ - --hash=sha256:35834855408429cecf495cac67ccbab802de269e948e27478b1e47dfb6465e57 \ - --hash=sha256:386122eeaaa76951a8196310432c5b0ef3b53590ef4c317ec7588ec554fec5d2 \ - --hash=sha256:40b05ab32a5067525670880eb5d169529089a26fe35dce8891127aeddc1950e8 \ - --hash=sha256:48abd62ca76a2cbe034542ed1b6aee851b6f28aaca4e6551b5599b6f3ef175cc \ - --hash=sha256:50cf2a96da226dcbff8671233ecf37bf6e95de98b2a2ebadbfdf455e6d05df42 \ - --hash=sha256:51c4f54dd8c6dfeb58d1df5e4f7f97df8abf17a36626a217f169893d1d7f3e9f \ - --hash=sha256:5bcc90b34df28a4b38653c36bb5ada35671ad105c99cfe915fb5bed7ad6924aa \ - --hash=sha256:62f6d4a29fea082ac4a3c9be5e415218255cf11684ac6ef5488eea0c9132689b \ - --hash=sha256:6eceb6ad197656a1ff49ebfbbfa870678c75be4344feb35ac1edf694309413dc \ - --hash=sha256:7aec8e88a3583515f9e0957fe4f5f6d8d4997e36d0f61624e70469771584c760 \ - --hash=sha256:91ca8145b060679ec9176e6de4f89b07363d6805bd4760631ef254905503598d \ - --hash=sha256:a184243544811e4a50d345838a883733461e67578959ac59964e43cca2c791e7 \ - --hash=sha256:a9e4b426c3702f3cd23b933436487eb34e01e00327fac20c9aebb68ccf34117d \ - --hash=sha256:bb0966e1c50d0ef5bc743312cc730b533491d60585a9a08f897274e57c3f70e0 \ - --hash=sha256:bb8b3c75bd157010459b15222c3fd30577042a7060e29d42dabce449c087f2b3 \ - --hash=sha256:bd5e7d2445d1a958c266bfa5d04c39932dc54093fa391736dbfdb0f1929c1fb3 \ - --hash=sha256:c87d98c7c4a69066fd31701c4e10d178a648c2cac3452e62c6b24dc51f9fcc00 \ - --hash=sha256:d2952396dc604544ea7476b33fe87faedc24d666fb0c2d5ac971a2b9576ab871 \ - --hash=sha256:d8797406499f28b5ef791f339594b0b5fdedf54e203b5066675c406ba69d705c \ - --hash=sha256:d9e9913f7bd69e093b81da4535ce27af842e7bf371cde42d1ae9e9bd382dc0e9 \ - --hash=sha256:e2806553238cd076f0a55bddab37a532b53580e699ed8e5606d0de1f856b5205 \ - --hash=sha256:ebab974b1687509e5c973b5c4b8b146683e101e102e17a86bd196ecaa4d099fc \ - --hash=sha256:ed767bf4ba90104c1216b68111613f0d5926fb3780660ea1198fc469af410e9d \ - --hash=sha256:f7a1fc29803712f80879b0806cb83ab24ce62fc8daf0569f2204a0cfd7f68ed4 - # via - # google-cloud-storage - # google-resumable-media -google-resumable-media==2.7.2 \ - --hash=sha256:3ce7551e9fe6d99e9a126101d2536612bb73486721951e9562fee0f90c6ababa \ - --hash=sha256:5280aed4629f2b60b847b0d42f9857fd4935c11af266744df33d8074cae92fe0 - # via google-cloud-storage -googleapis-common-protos==1.66.0 \ - --hash=sha256:c3e7b33d15fdca5374cc0a7346dd92ffa847425cc4ea941d970f13680052ec8c \ - --hash=sha256:d7abcd75fabb2e0ec9f74466401f6c119a0b498e27370e9be4c94cb7e382b8ed - # via google-api-core -idna==3.10 \ - --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \ - --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3 - # via requests -nox==2024.10.9 \ - --hash=sha256:1d36f309a0a2a853e9bccb76bbef6bb118ba92fa92674d15604ca99adeb29eab \ - --hash=sha256:7aa9dc8d1c27e9f45ab046ffd1c3b2c4f7c91755304769df231308849ebded95 - # via -r requirements.in -packaging==24.2 \ - --hash=sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759 \ - --hash=sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f - # via nox -platformdirs==4.3.6 \ - --hash=sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907 \ - --hash=sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb - # via virtualenv -proto-plus==1.25.0 \ - --hash=sha256:c91fc4a65074ade8e458e95ef8bac34d4008daa7cce4a12d6707066fca648961 \ - --hash=sha256:fbb17f57f7bd05a68b7707e745e26528b0b3c34e378db91eef93912c54982d91 - # via google-api-core -protobuf==5.29.3 \ - --hash=sha256:0a18ed4a24198528f2333802eb075e59dea9d679ab7a6c5efb017a59004d849f \ - --hash=sha256:0eb32bfa5219fc8d4111803e9a690658aa2e6366384fd0851064b963b6d1f2a7 \ - --hash=sha256:3ea51771449e1035f26069c4c7fd51fba990d07bc55ba80701c78f886bf9c888 \ - --hash=sha256:5da0f41edaf117bde316404bad1a486cb4ededf8e4a54891296f648e8e076620 \ - --hash=sha256:6ce8cc3389a20693bfde6c6562e03474c40851b44975c9b2bf6df7d8c4f864da \ - --hash=sha256:84a57163a0ccef3f96e4b6a20516cedcf5bb3a95a657131c5c3ac62200d23252 \ - --hash=sha256:a4fa6f80816a9a0678429e84973f2f98cbc218cca434abe8db2ad0bffc98503a \ - --hash=sha256:a8434404bbf139aa9e1300dbf989667a83d42ddda9153d8ab76e0d5dcaca484e \ - --hash=sha256:b89c115d877892a512f79a8114564fb435943b59067615894c3b13cd3e1fa107 \ - --hash=sha256:c027e08a08be10b67c06bf2370b99c811c466398c357e615ca88c91c07f0910f \ - --hash=sha256:daaf63f70f25e8689c072cfad4334ca0ac1d1e05a92fc15c54eb9cf23c3efd84 - # via - # gcp-docuploader - # google-api-core - # googleapis-common-protos - # proto-plus -pyasn1==0.6.1 \ - --hash=sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629 \ - --hash=sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034 - # via - # pyasn1-modules - # rsa -pyasn1-modules==0.4.1 \ - --hash=sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd \ - --hash=sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c - # via google-auth -requests==2.32.3 \ - --hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \ - --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6 - # via - # google-api-core - # google-cloud-storage -rsa==4.9 \ - --hash=sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7 \ - --hash=sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21 - # via google-auth -six==1.17.0 \ - --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \ - --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81 - # via gcp-docuploader -tomli==2.2.1 \ - --hash=sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6 \ - --hash=sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd \ - --hash=sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c \ - --hash=sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b \ - --hash=sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8 \ - --hash=sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6 \ - --hash=sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77 \ - --hash=sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff \ - --hash=sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea \ - --hash=sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192 \ - --hash=sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249 \ - --hash=sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee \ - --hash=sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4 \ - --hash=sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98 \ - --hash=sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8 \ - --hash=sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4 \ - --hash=sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281 \ - --hash=sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744 \ - --hash=sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69 \ - --hash=sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13 \ - --hash=sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140 \ - --hash=sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e \ - --hash=sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e \ - --hash=sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc \ - --hash=sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff \ - --hash=sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec \ - --hash=sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2 \ - --hash=sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222 \ - --hash=sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106 \ - --hash=sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272 \ - --hash=sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a \ - --hash=sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7 - # via nox -urllib3==2.3.0 \ - --hash=sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df \ - --hash=sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d - # via requests -virtualenv==20.28.1 \ - --hash=sha256:412773c85d4dab0409b83ec36f7a6499e72eaf08c80e81e9576bca61831c71cb \ - --hash=sha256:5d34ab240fdb5d21549b76f9e8ff3af28252f5499fb6d6f031adac4e5a8c5329 - # via nox diff --git a/.kokoro/docs/common.cfg b/.kokoro/docs/common.cfg deleted file mode 100644 index 722f447aa..000000000 --- a/.kokoro/docs/common.cfg +++ /dev/null @@ -1,67 +0,0 @@ -# Format: //devtools/kokoro/config/proto/build.proto - -# Build logs will be here -action { - define_artifacts { - regex: "**/*sponge_log.xml" - } -} - -# Download trampoline resources. -gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" - -# Use the trampoline script to run in docker. -build_file: "python-api-core/.kokoro/trampoline_v2.sh" - -# Configure the docker image for kokoro-trampoline. -env_vars: { - key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-kokoro-resources/python-lib-docs" -} -env_vars: { - key: "TRAMPOLINE_BUILD_FILE" - value: "github/python-api-core/.kokoro/publish-docs.sh" -} - -env_vars: { - key: "STAGING_BUCKET" - value: "docs-staging" -} - -env_vars: { - key: "V2_STAGING_BUCKET" - # Push non-cloud library docs to `docs-staging-v2-dev` instead of the - # Cloud RAD bucket `docs-staging-v2` - value: "docs-staging-v2-dev" -} - -# It will upload the docker image after successful builds. -env_vars: { - key: "TRAMPOLINE_IMAGE_UPLOAD" - value: "true" -} - -# It will always build the docker image. -env_vars: { - key: "TRAMPOLINE_DOCKERFILE" - value: ".kokoro/docker/docs/Dockerfile" -} - -# Fetch the token needed for reporting release status to GitHub -before_action { - fetch_keystore { - keystore_resource { - keystore_config_id: 73713 - keyname: "yoshi-automation-github-key" - } - } -} - -before_action { - fetch_keystore { - keystore_resource { - keystore_config_id: 73713 - keyname: "docuploader_service_account" - } - } -} diff --git a/.kokoro/docs/docs-presubmit.cfg b/.kokoro/docs/docs-presubmit.cfg deleted file mode 100644 index d1ed51ebb..000000000 --- a/.kokoro/docs/docs-presubmit.cfg +++ /dev/null @@ -1,28 +0,0 @@ -# Format: //devtools/kokoro/config/proto/build.proto - -env_vars: { - key: "STAGING_BUCKET" - value: "gcloud-python-test" -} - -env_vars: { - key: "V2_STAGING_BUCKET" - value: "gcloud-python-test" -} - -# We only upload the image in the main `docs` build. -env_vars: { - key: "TRAMPOLINE_IMAGE_UPLOAD" - value: "false" -} - -env_vars: { - key: "TRAMPOLINE_BUILD_FILE" - value: "github/python-api-core/.kokoro/build.sh" -} - -# Only run this nox session. -env_vars: { - key: "NOX_SESSION" - value: "docs docfx" -} diff --git a/.kokoro/docs/docs.cfg b/.kokoro/docs/docs.cfg deleted file mode 100644 index 8f43917d9..000000000 --- a/.kokoro/docs/docs.cfg +++ /dev/null @@ -1 +0,0 @@ -# Format: //devtools/kokoro/config/proto/build.proto \ No newline at end of file diff --git a/.kokoro/publish-docs.sh b/.kokoro/publish-docs.sh deleted file mode 100755 index 4ed4aaf13..000000000 --- a/.kokoro/publish-docs.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/bin/bash -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -set -eo pipefail - -# Disable buffering, so that the logs stream through. -export PYTHONUNBUFFERED=1 - -export PATH="${HOME}/.local/bin:${PATH}" - -# build docs -nox -s docs - -# create metadata -python3.10 -m docuploader create-metadata \ - --name=$(jq --raw-output '.name // empty' .repo-metadata.json) \ - --version=$(python3.10 setup.py --version) \ - --language=$(jq --raw-output '.language // empty' .repo-metadata.json) \ - --distribution-name=$(python3.10 setup.py --name) \ - --product-page=$(jq --raw-output '.product_documentation // empty' .repo-metadata.json) \ - --github-repository=$(jq --raw-output '.repo // empty' .repo-metadata.json) \ - --issue-tracker=$(jq --raw-output '.issue_tracker // empty' .repo-metadata.json) - -cat docs.metadata - -# upload docs -python3.10 -m docuploader upload docs/_build/html --metadata-file docs.metadata --staging-bucket "${STAGING_BUCKET}" - - -# docfx yaml files -nox -s docfx - -# create metadata. -python3.10 -m docuploader create-metadata \ - --name=$(jq --raw-output '.name // empty' .repo-metadata.json) \ - --version=$(python3.10 setup.py --version) \ - --language=$(jq --raw-output '.language // empty' .repo-metadata.json) \ - --distribution-name=$(python3.10 setup.py --name) \ - --product-page=$(jq --raw-output '.product_documentation // empty' .repo-metadata.json) \ - --github-repository=$(jq --raw-output '.repo // empty' .repo-metadata.json) \ - --issue-tracker=$(jq --raw-output '.issue_tracker // empty' .repo-metadata.json) - -cat docs.metadata - -# upload docs -python3.10 -m docuploader upload docs/_build/html/docfx_yaml --metadata-file docs.metadata --destination-prefix docfx --staging-bucket "${V2_STAGING_BUCKET}" From a5648faf31c30158d59a3127ea37f7c9911ff765 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 10 Mar 2025 20:48:44 +0500 Subject: [PATCH 069/101] chore(main): release 2.24.2 (#805) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ google/api_core/version.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5717d6e60..e118fe462 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://pypi.org/project/google-api-core/#history +## [2.24.2](https://github.com/googleapis/python-api-core/compare/v2.24.1...v2.24.2) (2025-03-06) + + +### Bug Fixes + +* **deps:** Allow protobuf 6.x ([#804](https://github.com/googleapis/python-api-core/issues/804)) ([687be7c](https://github.com/googleapis/python-api-core/commit/687be7cbf629a61feb43ef37d3d920fa32b2d636)) + ## [2.24.1](https://github.com/googleapis/python-api-core/compare/v2.24.0...v2.24.1) (2025-01-24) diff --git a/google/api_core/version.py b/google/api_core/version.py index 96f84438a..04ffe6a5a 100644 --- a/google/api_core/version.py +++ b/google/api_core/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "2.24.1" +__version__ = "2.24.2" From 70697a3e39c389768e724fddacb3c9b97d609384 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Wed, 19 Mar 2025 08:09:57 -0700 Subject: [PATCH 070/101] feat: support dynamic retry backoff values (#793) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: retry generates backoff value after completing on_error callbacks * added comment * use single sleep iterator for retries * fix tests * update variable name * adjusted docstring * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * fixed mypy issues * added comment * added unit tests for dynamic backoff --------- Co-authored-by: Owl Bot Co-authored-by: Anthonios Partheniou --- google/api_core/retry/retry_base.py | 17 ++++++--- google/api_core/retry/retry_streaming.py | 13 +++---- .../api_core/retry/retry_streaming_async.py | 13 ++++--- google/api_core/retry/retry_unary.py | 13 +++---- google/api_core/retry/retry_unary_async.py | 13 +++---- .../retry/test_retry_streaming_async.py | 35 +++++++++++++++++-- tests/asyncio/retry/test_retry_unary_async.py | 27 +++++++++++++- tests/unit/retry/test_retry_streaming.py | 35 +++++++++++++++++-- tests/unit/retry/test_retry_unary.py | 28 ++++++++++++++- 9 files changed, 159 insertions(+), 35 deletions(-) diff --git a/google/api_core/retry/retry_base.py b/google/api_core/retry/retry_base.py index 1606e0fe2..263b4ccf3 100644 --- a/google/api_core/retry/retry_base.py +++ b/google/api_core/retry/retry_base.py @@ -25,7 +25,7 @@ import time from enum import Enum -from typing import Any, Callable, Optional, TYPE_CHECKING +from typing import Any, Callable, Optional, Iterator, TYPE_CHECKING import requests.exceptions @@ -174,7 +174,7 @@ def build_retry_error( def _retry_error_helper( exc: Exception, deadline: float | None, - next_sleep: float, + sleep_iterator: Iterator[float], error_list: list[Exception], predicate_fn: Callable[[Exception], bool], on_error_fn: Callable[[Exception], None] | None, @@ -183,7 +183,7 @@ def _retry_error_helper( tuple[Exception, Exception | None], ], original_timeout: float | None, -): +) -> float: """ Shared logic for handling an error for all retry implementations @@ -194,13 +194,15 @@ def _retry_error_helper( Args: - exc: the exception that was raised - deadline: the deadline for the retry, calculated as a diff from time.monotonic() - - next_sleep: the next sleep interval + - sleep_iterator: iterator to draw the next backoff value from - error_list: the list of exceptions that have been raised so far - predicate_fn: takes `exc` and returns true if the operation should be retried - on_error_fn: callback to execute when a retryable error occurs - exc_factory_fn: callback used to build the exception to be raised on terminal failure - original_timeout_val: the original timeout value for the retry (in seconds), to be passed to the exception factory for building an error message + Returns: + - the sleep value chosen before the next attempt """ error_list.append(exc) if not predicate_fn(exc): @@ -212,6 +214,12 @@ def _retry_error_helper( raise final_exc from source_exc if on_error_fn is not None: on_error_fn(exc) + # next_sleep is fetched after the on_error callback, to allow clients + # to update sleep_iterator values dynamically in response to errors + try: + next_sleep = next(sleep_iterator) + except StopIteration: + raise ValueError("Sleep generator stopped yielding sleep values.") from exc if deadline is not None and time.monotonic() + next_sleep > deadline: final_exc, source_exc = exc_factory_fn( error_list, @@ -222,6 +230,7 @@ def _retry_error_helper( _LOGGER.debug( "Retrying due to {}, sleeping {:.1f}s ...".format(error_list[-1], next_sleep) ) + return next_sleep class _BaseRetry(object): diff --git a/google/api_core/retry/retry_streaming.py b/google/api_core/retry/retry_streaming.py index 00666841c..e4474c8af 100644 --- a/google/api_core/retry/retry_streaming.py +++ b/google/api_core/retry/retry_streaming.py @@ -107,8 +107,11 @@ def retry_target_stream( time.monotonic() + timeout if timeout is not None else None ) error_list: list[Exception] = [] + sleep_iter = iter(sleep_generator) - for sleep in sleep_generator: + # continue trying until an attempt completes, or a terminal exception is raised in _retry_error_helper + # TODO: support max_attempts argument: https://github.com/googleapis/python-api-core/issues/535 + while True: # Start a new retry loop try: # Note: in the future, we can add a ResumptionStrategy object @@ -121,10 +124,10 @@ def retry_target_stream( # This function explicitly must deal with broad exceptions. except Exception as exc: # defer to shared logic for handling errors - _retry_error_helper( + next_sleep = _retry_error_helper( exc, deadline, - sleep, + sleep_iter, error_list, predicate, on_error, @@ -132,9 +135,7 @@ def retry_target_stream( timeout, ) # if exception not raised, sleep before next attempt - time.sleep(sleep) - - raise ValueError("Sleep generator stopped yielding sleep values.") + time.sleep(next_sleep) class StreamingRetry(_BaseRetry): diff --git a/google/api_core/retry/retry_streaming_async.py b/google/api_core/retry/retry_streaming_async.py index 942abf5fe..5e5fa2402 100644 --- a/google/api_core/retry/retry_streaming_async.py +++ b/google/api_core/retry/retry_streaming_async.py @@ -109,9 +109,12 @@ async def retry_target_stream( deadline = time.monotonic() + timeout if timeout else None # keep track of retryable exceptions we encounter to pass in to exception_factory error_list: list[Exception] = [] + sleep_iter = iter(sleep_generator) target_is_generator: bool | None = None - for sleep in sleep_generator: + # continue trying until an attempt completes, or a terminal exception is raised in _retry_error_helper + # TODO: support max_attempts argument: https://github.com/googleapis/python-api-core/issues/535 + while True: # Start a new retry loop try: # Note: in the future, we can add a ResumptionStrategy object @@ -174,10 +177,10 @@ async def retry_target_stream( # This function explicitly must deal with broad exceptions. except Exception as exc: # defer to shared logic for handling errors - _retry_error_helper( + next_sleep = _retry_error_helper( exc, deadline, - sleep, + sleep_iter, error_list, predicate, on_error, @@ -185,11 +188,11 @@ async def retry_target_stream( timeout, ) # if exception not raised, sleep before next attempt - await asyncio.sleep(sleep) + await asyncio.sleep(next_sleep) + finally: if target_is_generator and target_iterator is not None: await cast(AsyncGenerator["_Y", None], target_iterator).aclose() - raise ValueError("Sleep generator stopped yielding sleep values.") class AsyncStreamingRetry(_BaseRetry): diff --git a/google/api_core/retry/retry_unary.py b/google/api_core/retry/retry_unary.py index d5dff663f..6d36bc7d1 100644 --- a/google/api_core/retry/retry_unary.py +++ b/google/api_core/retry/retry_unary.py @@ -138,8 +138,11 @@ def retry_target( deadline = time.monotonic() + timeout if timeout is not None else None error_list: list[Exception] = [] + sleep_iter = iter(sleep_generator) - for sleep in sleep_generator: + # continue trying until an attempt completes, or a terminal exception is raised in _retry_error_helper + # TODO: support max_attempts argument: https://github.com/googleapis/python-api-core/issues/535 + while True: try: result = target() if inspect.isawaitable(result): @@ -150,10 +153,10 @@ def retry_target( # This function explicitly must deal with broad exceptions. except Exception as exc: # defer to shared logic for handling errors - _retry_error_helper( + next_sleep = _retry_error_helper( exc, deadline, - sleep, + sleep_iter, error_list, predicate, on_error, @@ -161,9 +164,7 @@ def retry_target( timeout, ) # if exception not raised, sleep before next attempt - time.sleep(sleep) - - raise ValueError("Sleep generator stopped yielding sleep values.") + time.sleep(next_sleep) class Retry(_BaseRetry): diff --git a/google/api_core/retry/retry_unary_async.py b/google/api_core/retry/retry_unary_async.py index e76a37bb0..1f72476af 100644 --- a/google/api_core/retry/retry_unary_async.py +++ b/google/api_core/retry/retry_unary_async.py @@ -149,18 +149,21 @@ async def retry_target( deadline = time.monotonic() + timeout if timeout is not None else None error_list: list[Exception] = [] + sleep_iter = iter(sleep_generator) - for sleep in sleep_generator: + # continue trying until an attempt completes, or a terminal exception is raised in _retry_error_helper + # TODO: support max_attempts argument: https://github.com/googleapis/python-api-core/issues/535 + while True: try: return await target() # pylint: disable=broad-except # This function explicitly must deal with broad exceptions. except Exception as exc: # defer to shared logic for handling errors - _retry_error_helper( + next_sleep = _retry_error_helper( exc, deadline, - sleep, + sleep_iter, error_list, predicate, on_error, @@ -168,9 +171,7 @@ async def retry_target( timeout, ) # if exception not raised, sleep before next attempt - await asyncio.sleep(sleep) - - raise ValueError("Sleep generator stopped yielding sleep values.") + await asyncio.sleep(next_sleep) class AsyncRetry(_BaseRetry): diff --git a/tests/asyncio/retry/test_retry_streaming_async.py b/tests/asyncio/retry/test_retry_streaming_async.py index d0fd799c8..e44f5361e 100644 --- a/tests/asyncio/retry/test_retry_streaming_async.py +++ b/tests/asyncio/retry/test_retry_streaming_async.py @@ -36,7 +36,36 @@ async def test_retry_streaming_target_bad_sleep_generator(): from google.api_core.retry.retry_streaming_async import retry_target_stream with pytest.raises(ValueError, match="Sleep generator"): - await retry_target_stream(None, None, [], None).__anext__() + await retry_target_stream(None, lambda x: True, [], None).__anext__() + + +@mock.patch("asyncio.sleep", autospec=True) +@pytest.mark.asyncio +async def test_retry_streaming_target_dynamic_backoff(sleep): + """ + sleep_generator should be iterated after on_error, to support dynamic backoff + """ + from functools import partial + from google.api_core.retry.retry_streaming_async import retry_target_stream + + sleep.side_effect = RuntimeError("stop after sleep") + # start with empty sleep generator; values are added after exception in push_sleep_value + sleep_values = [] + error_target = partial(TestAsyncStreamingRetry._generator_mock, error_on=0) + inserted_sleep = 99 + + def push_sleep_value(err): + sleep_values.append(inserted_sleep) + + with pytest.raises(RuntimeError): + await retry_target_stream( + error_target, + predicate=lambda x: True, + sleep_generator=sleep_values, + on_error=push_sleep_value, + ).__anext__() + assert sleep.call_count == 1 + sleep.assert_called_once_with(inserted_sleep) class TestAsyncStreamingRetry(Test_BaseRetry): @@ -66,8 +95,8 @@ def if_exception_type(exc): str(retry_), ) + @staticmethod async def _generator_mock( - self, num=5, error_on=None, exceptions_seen=None, @@ -87,7 +116,7 @@ async def _generator_mock( for i in range(num): if sleep_time: await asyncio.sleep(sleep_time) - if error_on and i == error_on: + if error_on is not None and i == error_on: raise ValueError("generator mock error") yield i except (Exception, BaseException, GeneratorExit) as e: diff --git a/tests/asyncio/retry/test_retry_unary_async.py b/tests/asyncio/retry/test_retry_unary_async.py index dc64299f9..e7fdc963f 100644 --- a/tests/asyncio/retry/test_retry_unary_async.py +++ b/tests/asyncio/retry/test_retry_unary_async.py @@ -136,9 +136,34 @@ async def test_retry_target_timeout_exceeded(monotonic, sleep, use_deadline_arg) @pytest.mark.asyncio async def test_retry_target_bad_sleep_generator(): with pytest.raises(ValueError, match="Sleep generator"): + await retry_async.retry_target(mock.sentinel.target, lambda x: True, [], None) + + +@mock.patch("asyncio.sleep", autospec=True) +@pytest.mark.asyncio +async def test_retry_target_dynamic_backoff(sleep): + """ + sleep_generator should be iterated after on_error, to support dynamic backoff + """ + sleep.side_effect = RuntimeError("stop after sleep") + # start with empty sleep generator; values are added after exception in push_sleep_value + sleep_values = [] + exception = ValueError("trigger retry") + error_target = mock.Mock(side_effect=exception) + inserted_sleep = 99 + + def push_sleep_value(err): + sleep_values.append(inserted_sleep) + + with pytest.raises(RuntimeError): await retry_async.retry_target( - mock.sentinel.target, mock.sentinel.predicate, [], None + error_target, + predicate=lambda x: True, + sleep_generator=sleep_values, + on_error=push_sleep_value, ) + assert sleep.call_count == 1 + sleep.assert_called_once_with(inserted_sleep) class TestAsyncRetry(Test_BaseRetry): diff --git a/tests/unit/retry/test_retry_streaming.py b/tests/unit/retry/test_retry_streaming.py index 0bc85d926..2499b2ae9 100644 --- a/tests/unit/retry/test_retry_streaming.py +++ b/tests/unit/retry/test_retry_streaming.py @@ -33,7 +33,36 @@ def test_retry_streaming_target_bad_sleep_generator(): with pytest.raises( ValueError, match="Sleep generator stopped yielding sleep values" ): - next(retry_streaming.retry_target_stream(None, None, [], None)) + next(retry_streaming.retry_target_stream(None, lambda x: True, [], None)) + + +@mock.patch("time.sleep", autospec=True) +def test_retry_streaming_target_dynamic_backoff(sleep): + """ + sleep_generator should be iterated after on_error, to support dynamic backoff + """ + from functools import partial + + sleep.side_effect = RuntimeError("stop after sleep") + # start with empty sleep generator; values are added after exception in push_sleep_value + sleep_values = [] + error_target = partial(TestStreamingRetry._generator_mock, error_on=0) + inserted_sleep = 99 + + def push_sleep_value(err): + sleep_values.append(inserted_sleep) + + with pytest.raises(RuntimeError): + next( + retry_streaming.retry_target_stream( + error_target, + predicate=lambda x: True, + sleep_generator=sleep_values, + on_error=push_sleep_value, + ) + ) + assert sleep.call_count == 1 + sleep.assert_called_once_with(inserted_sleep) class TestStreamingRetry(Test_BaseRetry): @@ -63,8 +92,8 @@ def if_exception_type(exc): str(retry_), ) + @staticmethod def _generator_mock( - self, num=5, error_on=None, return_val=None, @@ -82,7 +111,7 @@ def _generator_mock( """ try: for i in range(num): - if error_on and i == error_on: + if error_on is not None and i == error_on: raise ValueError("generator mock error") yield i return return_val diff --git a/tests/unit/retry/test_retry_unary.py b/tests/unit/retry/test_retry_unary.py index 6851fbe48..f5bbcff72 100644 --- a/tests/unit/retry/test_retry_unary.py +++ b/tests/unit/retry/test_retry_unary.py @@ -146,7 +146,33 @@ def test_retry_target_timeout_exceeded(monotonic, sleep, use_deadline_arg): def test_retry_target_bad_sleep_generator(): with pytest.raises(ValueError, match="Sleep generator"): - retry.retry_target(mock.sentinel.target, mock.sentinel.predicate, [], None) + retry.retry_target(mock.sentinel.target, lambda x: True, [], None) + + +@mock.patch("time.sleep", autospec=True) +def test_retry_target_dynamic_backoff(sleep): + """ + sleep_generator should be iterated after on_error, to support dynamic backoff + """ + sleep.side_effect = RuntimeError("stop after sleep") + # start with empty sleep generator; values are added after exception in push_sleep_value + sleep_values = [] + exception = ValueError("trigger retry") + error_target = mock.Mock(side_effect=exception) + inserted_sleep = 99 + + def push_sleep_value(err): + sleep_values.append(inserted_sleep) + + with pytest.raises(RuntimeError): + retry.retry_target( + error_target, + predicate=lambda x: True, + sleep_generator=sleep_values, + on_error=push_sleep_value, + ) + assert sleep.call_count == 1 + sleep.assert_called_once_with(inserted_sleep) class TestRetry(Test_BaseRetry): From 1ca7973a395099403be1a99c7c4583a8f22d5d8e Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Wed, 19 Mar 2025 11:40:57 -0400 Subject: [PATCH 071/101] fix: resolve issue where pre-release versions of dependencies are installed (#808) Co-authored-by: ohmayr --- pyproject.toml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ea3a52268..da404ab3b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,15 +58,15 @@ Documentation = "https://googleapis.dev/python/google-api-core/latest/" Repository = "https://github.com/googleapis/python-api-core" [project.optional-dependencies] -async_rest = ["google-auth[aiohttp] >= 2.35.0, < 3.0.dev0"] +async_rest = ["google-auth[aiohttp] >= 2.35.0, < 3.0.0"] grpc = [ - "grpcio >= 1.33.2, < 2.0dev", - "grpcio >= 1.49.1, < 2.0dev; python_version >= '3.11'", - "grpcio-status >= 1.33.2, < 2.0.dev0", - "grpcio-status >= 1.49.1, < 2.0.dev0; python_version >= '3.11'", + "grpcio >= 1.33.2, < 2.0.0", + "grpcio >= 1.49.1, < 2.0.0; python_version >= '3.11'", + "grpcio-status >= 1.33.2, < 2.0.0", + "grpcio-status >= 1.49.1, < 2.0.0; python_version >= '3.11'", ] -grpcgcp = ["grpcio-gcp >= 0.2.2, < 1.0.dev0"] -grpcio-gcp = ["grpcio-gcp >= 0.2.2, < 1.0.dev0"] +grpcgcp = ["grpcio-gcp >= 0.2.2, < 1.0.0"] +grpcio-gcp = ["grpcio-gcp >= 0.2.2, < 1.0.0"] [tool.setuptools.dynamic] version = { attr = "google.api_core.version.__version__" } From 8da1fa9b160b7c9c159fecc698eede498df91f78 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Thu, 10 Apr 2025 17:36:27 +0000 Subject: [PATCH 072/101] chore(python): remove .gitignore from templates (#815) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(python): remove .gitignore from templates Source-Link: https://github.com/googleapis/synthtool/commit/419d94cdddd0d859ac6743ffebd177693c8a027f Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-python:latest@sha256:a7aef70df5f13313ddc027409fc8f3151422ec2a57ac8730fce8fa75c060d5bb * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * Remove replacement in owlbot.py --------- Co-authored-by: Owl Bot Co-authored-by: Anthonios Partheniou --- .github/.OwlBot.lock.yaml | 4 ++-- owlbot.py | 13 ------------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index a6d11c019..51b21a62b 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest - digest: sha256:5581906b957284864632cde4e9c51d1cc66b0094990b27e689132fe5cd036046 -# created: 2025-03-04 + digest: sha256:a7aef70df5f13313ddc027409fc8f3151422ec2a57ac8730fce8fa75c060d5bb +# created: 2025-04-10T17:00:10.042601326Z diff --git a/owlbot.py b/owlbot.py index c8c76542d..58bc75170 100644 --- a/owlbot.py +++ b/owlbot.py @@ -26,7 +26,6 @@ excludes = [ "noxfile.py", # pytype "setup.cfg", # pytype - ".flake8", # flake8-import-order, layout ".coveragerc", # layout "CONTRIBUTING.rst", # no systests ".github/workflows/unittest.yml", # exclude unittest gh action @@ -36,18 +35,6 @@ templated_files = common.py_library(microgenerator=True, cov_level=100) s.move(templated_files, excludes=excludes) -# Add pytype support -s.replace( - ".gitignore", - """\ -.pytest_cache -""", - """\ -.pytest_cache -.pytype -""", -) - python.configure_previous_major_version_branches() s.shell.run(["nox", "-s", "blacken"], hide_output=False) From 66d84a3a9ef0ab0c2124a1bf12cc3c45c2393e74 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Thu, 10 Apr 2025 16:31:57 -0700 Subject: [PATCH 073/101] test: fix BackgroundConsumer error log (#814) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fixed null pointer * added test * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * removed unneeded file --------- Co-authored-by: Owl Bot --- google/api_core/bidi.py | 3 ++- tests/unit/test_bidi.py | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/google/api_core/bidi.py b/google/api_core/bidi.py index 4e800c88e..b002b409b 100644 --- a/google/api_core/bidi.py +++ b/google/api_core/bidi.py @@ -664,7 +664,8 @@ def _thread_main(self, ready): _LOGGER.debug("waiting for recv.") response = self._bidi_rpc.recv() _LOGGER.debug("recved response.") - self._on_response(response) + if self._on_response is not None: + self._on_response(response) except exceptions.GoogleAPICallError as exc: _LOGGER.debug( diff --git a/tests/unit/test_bidi.py b/tests/unit/test_bidi.py index cca9a2143..0e7b018ce 100644 --- a/tests/unit/test_bidi.py +++ b/tests/unit/test_bidi.py @@ -16,6 +16,7 @@ import logging import queue import threading +import time try: from unittest import mock @@ -894,3 +895,26 @@ def close_side_effect(): # calling stop twice should not result in an error. consumer.stop() + + def test_stop_error_logs(self, caplog): + """ + Closing the client should result in no internal error logs + + https://github.com/googleapis/python-api-core/issues/788 + """ + caplog.set_level(logging.DEBUG) + bidi_rpc = mock.create_autospec(bidi.BidiRpc, instance=True) + bidi_rpc.is_active = True + on_response = mock.Mock(spec=["__call__"]) + + consumer = bidi.BackgroundConsumer(bidi_rpc, on_response) + + consumer.start() + consumer.stop() + # let the background thread run for a while before exiting + time.sleep(0.1) + bidi_rpc.is_active = False + # running thread should not result in error logs + error_logs = [r.message for r in caplog.records if r.levelname == "ERROR"] + assert not error_logs, f"Found unexpected ERROR logs: {error_logs}" + bidi_rpc.is_active = False From 118bd96f3907234351972409834ab5309cdfcee4 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Tue, 6 May 2025 18:34:49 -0400 Subject: [PATCH 074/101] feat: Add protobuf runtime version to `x-goog-api-client` header (#812) * for testing purposes * lint --- google/api_core/client_info.py | 6 ++++++ google/api_core/gapic_v1/client_info.py | 1 + tests/asyncio/gapic/test_method_async.py | 1 + tests/unit/gapic/test_method.py | 1 + tests/unit/test_client_info.py | 10 ++++++++-- 5 files changed, 17 insertions(+), 2 deletions(-) diff --git a/google/api_core/client_info.py b/google/api_core/client_info.py index 90926beb8..f0678d24b 100644 --- a/google/api_core/client_info.py +++ b/google/api_core/client_info.py @@ -59,6 +59,7 @@ class ClientInfo(object): Recommended format: ``application-or-tool-ID/major.minor.version``. rest_version (Optional[str]): A string with labeled versions of the dependencies used for REST transport. + protobuf_runtime_version (Optional[str]): The protobuf runtime version. """ def __init__( @@ -70,6 +71,7 @@ def __init__( client_library_version=None, user_agent=None, rest_version=None, + protobuf_runtime_version=None, ): self.python_version = python_version self.grpc_version = grpc_version @@ -78,6 +80,7 @@ def __init__( self.client_library_version = client_library_version self.user_agent = user_agent self.rest_version = rest_version + self.protobuf_runtime_version = protobuf_runtime_version def to_user_agent(self): """Returns the user-agent string for this client info.""" @@ -105,4 +108,7 @@ def to_user_agent(self): if self.client_library_version is not None: ua += "gccl/{client_library_version} " + if self.protobuf_runtime_version is not None: + ua += "pb/{protobuf_runtime_version} " + return ua.format(**self.__dict__).strip() diff --git a/google/api_core/gapic_v1/client_info.py b/google/api_core/gapic_v1/client_info.py index 4516f339b..4b3b56493 100644 --- a/google/api_core/gapic_v1/client_info.py +++ b/google/api_core/gapic_v1/client_info.py @@ -47,6 +47,7 @@ class ClientInfo(client_info.ClientInfo): Recommended format: ``application-or-tool-ID/major.minor.version``. rest_version (Optional[str]): A string with labeled versions of the dependencies used for REST transport. + protobuf_runtime_version (Optional[str]): The protobuf runtime version. """ def to_grpc_metadata(self): diff --git a/tests/asyncio/gapic/test_method_async.py b/tests/asyncio/gapic/test_method_async.py index 73f67b8db..cc4e7de83 100644 --- a/tests/asyncio/gapic/test_method_async.py +++ b/tests/asyncio/gapic/test_method_async.py @@ -81,6 +81,7 @@ async def test_wrap_method_with_custom_client_info(): api_core_version=3, gapic_version=4, client_library_version=5, + protobuf_runtime_version=6, ) fake_call = grpc_helpers_async.FakeUnaryUnaryCall() method = mock.Mock(spec=aio.UnaryUnaryMultiCallable, return_value=fake_call) diff --git a/tests/unit/gapic/test_method.py b/tests/unit/gapic/test_method.py index 87aa63905..8896429cd 100644 --- a/tests/unit/gapic/test_method.py +++ b/tests/unit/gapic/test_method.py @@ -76,6 +76,7 @@ def test_wrap_method_with_custom_client_info(): api_core_version=3, gapic_version=4, client_library_version=5, + protobuf_runtime_version=6, ) method = mock.Mock(spec=["__call__"]) diff --git a/tests/unit/test_client_info.py b/tests/unit/test_client_info.py index 3361fef6e..3eacabcaf 100644 --- a/tests/unit/test_client_info.py +++ b/tests/unit/test_client_info.py @@ -46,6 +46,7 @@ def test_constructor_options(): client_library_version="5", user_agent="6", rest_version="7", + protobuf_runtime_version="8", ) assert info.python_version == "1" @@ -55,11 +56,15 @@ def test_constructor_options(): assert info.client_library_version == "5" assert info.user_agent == "6" assert info.rest_version == "7" + assert info.protobuf_runtime_version == "8" def test_to_user_agent_minimal(): info = client_info.ClientInfo( - python_version="1", api_core_version="2", grpc_version=None + python_version="1", + api_core_version="2", + grpc_version=None, + protobuf_runtime_version=None, ) user_agent = info.to_user_agent() @@ -75,11 +80,12 @@ def test_to_user_agent_full(): gapic_version="4", client_library_version="5", user_agent="app-name/1.0", + protobuf_runtime_version="6", ) user_agent = info.to_user_agent() - assert user_agent == "app-name/1.0 gl-python/1 grpc/2 gax/3 gapic/4 gccl/5" + assert user_agent == "app-name/1.0 gl-python/1 grpc/2 gax/3 gapic/4 gccl/5 pb/6" def test_to_user_agent_rest(): From 225bf75020ed3bda5f68b2dbd99a91499dd4acc9 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 12 May 2025 22:09:23 +0500 Subject: [PATCH 075/101] chore(main): release 2.25.0 (#811) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 13 +++++++++++++ google/api_core/version.py | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e118fe462..98a6b8d7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ [1]: https://pypi.org/project/google-api-core/#history +## [2.25.0](https://github.com/googleapis/python-api-core/compare/v2.24.2...v2.25.0) (2025-05-06) + + +### Features + +* Add protobuf runtime version to `x-goog-api-client` header ([#812](https://github.com/googleapis/python-api-core/issues/812)) ([118bd96](https://github.com/googleapis/python-api-core/commit/118bd96f3907234351972409834ab5309cdfcee4)) +* Support dynamic retry backoff values ([#793](https://github.com/googleapis/python-api-core/issues/793)) ([70697a3](https://github.com/googleapis/python-api-core/commit/70697a3e39c389768e724fddacb3c9b97d609384)) + + +### Bug Fixes + +* Resolve issue where pre-release versions of dependencies are installed ([#808](https://github.com/googleapis/python-api-core/issues/808)) ([1ca7973](https://github.com/googleapis/python-api-core/commit/1ca7973a395099403be1a99c7c4583a8f22d5d8e)) + ## [2.24.2](https://github.com/googleapis/python-api-core/compare/v2.24.1...v2.24.2) (2025-03-06) diff --git a/google/api_core/version.py b/google/api_core/version.py index 04ffe6a5a..f882cac3a 100644 --- a/google/api_core/version.py +++ b/google/api_core/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "2.24.2" +__version__ = "2.25.0" From 3206c0170dda80a613bf257ebcf3b78c1a20465f Mon Sep 17 00:00:00 2001 From: Andrew Browne <81702808+abbrowne126@users.noreply.github.com> Date: Fri, 30 May 2025 15:36:36 -0400 Subject: [PATCH 076/101] fix: Allow BackgroundConsumer To Inform Caller of Fatal Exceptions with Optional Callback (#821) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: add stop() call to BackgroundConsumer failures due to exceptions * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * move stop call to within the exception clauses * Update test_bidi.py * fix: allow exceptions to be surfaced to the caller of BackgroundConsumer * Update bidi.py * fix: allow BackgroundConsumer caller to pass `on_fatal_exception` to be informed of fatal processing errors * Update tests/unit/test_bidi.py Co-authored-by: Anthonios Partheniou * Update bidi.py --------- Co-authored-by: Owl Bot Co-authored-by: Anthonios Partheniou --- google/api_core/bidi.py | 18 +++++++++++++++--- tests/unit/test_bidi.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/google/api_core/bidi.py b/google/api_core/bidi.py index b002b409b..bed4c70e9 100644 --- a/google/api_core/bidi.py +++ b/google/api_core/bidi.py @@ -624,12 +624,15 @@ def on_response(response): ``open()``ed yet. on_response (Callable[[protobuf.Message], None]): The callback to be called for every response on the stream. + on_fatal_exception (Callable[[Exception], None]): The callback to + be called on fatal errors during consumption. Default None. """ - def __init__(self, bidi_rpc, on_response): + def __init__(self, bidi_rpc, on_response, on_fatal_exception=None): self._bidi_rpc = bidi_rpc self._on_response = on_response self._paused = False + self._on_fatal_exception = on_fatal_exception self._wake = threading.Condition() self._thread = None self._operational_lock = threading.Lock() @@ -676,6 +679,8 @@ def _thread_main(self, ready): exc, exc_info=True, ) + if self._on_fatal_exception is not None: + self._on_fatal_exception(exc) except Exception as exc: _LOGGER.exception( @@ -683,6 +688,8 @@ def _thread_main(self, ready): _BIDIRECTIONAL_CONSUMER_NAME, exc, ) + if self._on_fatal_exception is not None: + self._on_fatal_exception(exc) _LOGGER.info("%s exiting", _BIDIRECTIONAL_CONSUMER_NAME) @@ -694,8 +701,8 @@ def start(self): name=_BIDIRECTIONAL_CONSUMER_NAME, target=self._thread_main, args=(ready,), + daemon=True, ) - thread.daemon = True thread.start() # Other parts of the code rely on `thread.is_alive` which # isn't sufficient to know if a thread is active, just that it may @@ -706,7 +713,11 @@ def start(self): _LOGGER.debug("Started helper thread %s", thread.name) def stop(self): - """Stop consuming the stream and shutdown the background thread.""" + """Stop consuming the stream and shutdown the background thread. + + NOTE: Cannot be called within `_thread_main`, since it is not + possible to join a thread to itself. + """ with self._operational_lock: self._bidi_rpc.close() @@ -721,6 +732,7 @@ def stop(self): self._thread = None self._on_response = None + self._on_fatal_exception = None @property def is_active(self): diff --git a/tests/unit/test_bidi.py b/tests/unit/test_bidi.py index 0e7b018ce..7640367ce 100644 --- a/tests/unit/test_bidi.py +++ b/tests/unit/test_bidi.py @@ -918,3 +918,33 @@ def test_stop_error_logs(self, caplog): error_logs = [r.message for r in caplog.records if r.levelname == "ERROR"] assert not error_logs, f"Found unexpected ERROR logs: {error_logs}" bidi_rpc.is_active = False + + def test_fatal_exceptions_can_inform_consumer(self, caplog): + """ + https://github.com/googleapis/python-api-core/issues/820 + Exceptions thrown in the BackgroundConsumer not caught by `should_recover` / `should_terminate` + on the RPC should be bubbled back to the caller through `on_fatal_exception`, if passed. + """ + caplog.set_level(logging.DEBUG) + + for fatal_exception in ( + ValueError("some non-api error"), + exceptions.PermissionDenied("some api error"), + ): + bidi_rpc = mock.create_autospec(bidi.ResumableBidiRpc, instance=True) + bidi_rpc.is_active = True + on_response = mock.Mock(spec=["__call__"]) + + on_fatal_exception = mock.Mock(spec=["__call__"]) + + bidi_rpc.open.side_effect = fatal_exception + + consumer = bidi.BackgroundConsumer( + bidi_rpc, on_response, on_fatal_exception + ) + + consumer.start() + # let the background thread run for a while before exiting + time.sleep(0.1) + + on_fatal_exception.assert_called_once_with(fatal_exception) From 2c983853e930d1ad4a3ece6b6b55a6dc72206a17 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Thu, 12 Jun 2025 15:09:30 -0400 Subject: [PATCH 077/101] chore(main): release 2.25.1 (#825) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ google/api_core/version.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98a6b8d7b..aab5d53fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://pypi.org/project/google-api-core/#history +## [2.25.1](https://github.com/googleapis/python-api-core/compare/v2.25.0...v2.25.1) (2025-06-02) + + +### Bug Fixes + +* Allow BackgroundConsumer To Inform Caller of Fatal Exceptions with Optional Callback ([3206c01](https://github.com/googleapis/python-api-core/commit/3206c0170dda80a613bf257ebcf3b78c1a20465f)) + ## [2.25.0](https://github.com/googleapis/python-api-core/compare/v2.24.2...v2.25.0) (2025-05-06) diff --git a/google/api_core/version.py b/google/api_core/version.py index f882cac3a..21cbec9fe 100644 --- a/google/api_core/version.py +++ b/google/api_core/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "2.25.0" +__version__ = "2.25.1" From 14a59789e144905bd6c82180ad07a52bf1f84f02 Mon Sep 17 00:00:00 2001 From: Victor Chudnovsky Date: Thu, 4 Sep 2025 16:56:26 -0700 Subject: [PATCH 078/101] fix: fix async tests and round-off error in test expectations (#837) * fix: properly designate async test methods * fix: fix async tests * fix: made additional test be an async method --- tests/asyncio/gapic/test_method_async.py | 6 +++++- tests/asyncio/test_operation_async.py | 4 ++-- tests/unit/gapic/test_method.py | 6 +++++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/asyncio/gapic/test_method_async.py b/tests/asyncio/gapic/test_method_async.py index cc4e7de83..3edf8b6d4 100644 --- a/tests/asyncio/gapic/test_method_async.py +++ b/tests/asyncio/gapic/test_method_async.py @@ -256,7 +256,11 @@ async def test_wrap_method_with_overriding_timeout_as_a_number(): result = await wrapped_method(timeout=22) assert result == 42 - method.assert_called_once_with(timeout=22, metadata=mock.ANY) + + actual_timeout = method.call_args[1]["timeout"] + metadata = method.call_args[1]["metadata"] + assert metadata == mock.ANY + assert actual_timeout == pytest.approx(22, abs=0.01) @pytest.mark.asyncio diff --git a/tests/asyncio/test_operation_async.py b/tests/asyncio/test_operation_async.py index 9d9fb5d25..5b2f012bf 100644 --- a/tests/asyncio/test_operation_async.py +++ b/tests/asyncio/test_operation_async.py @@ -85,7 +85,7 @@ async def test_constructor(): @pytest.mark.asyncio -def test_metadata(): +async def test_metadata(): expected_metadata = struct_pb2.Struct() future, _, _ = make_operation_future( [make_operation_proto(metadata=expected_metadata)] @@ -178,7 +178,7 @@ async def test_unexpected_result(unused_sleep): @pytest.mark.asyncio -def test_from_gapic(): +async def test_from_gapic(): operation_proto = make_operation_proto(done=True) operations_client = mock.create_autospec( operations_v1.OperationsClient, instance=True diff --git a/tests/unit/gapic/test_method.py b/tests/unit/gapic/test_method.py index 8896429cd..c27de64ea 100644 --- a/tests/unit/gapic/test_method.py +++ b/tests/unit/gapic/test_method.py @@ -201,7 +201,11 @@ def test_wrap_method_with_overriding_timeout_as_a_number(): result = wrapped_method(timeout=22) assert result == 42 - method.assert_called_once_with(timeout=22, metadata=mock.ANY) + + actual_timeout = method.call_args[1]["timeout"] + metadata = method.call_args[1]["metadata"] + assert metadata == mock.ANY + assert actual_timeout == pytest.approx(22, abs=0.01) def test_wrap_method_with_call(): From 324eb7464d6ade9a8c2e413d4695bc7d7adfcb3d Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Wed, 1 Oct 2025 05:44:18 -0400 Subject: [PATCH 079/101] fix: deprecate credentials_file argument (#841) * fix: deprecate credentials_file argument * lint * cover * fix build --- google/api_core/client_options.py | 11 ++++-- google/api_core/general_helpers.py | 36 +++++++++++++++++++ google/api_core/grpc_helpers.py | 17 +++++---- google/api_core/grpc_helpers_async.py | 11 ++++-- .../api_core/operations_v1/transports/base.py | 20 +++++++---- .../api_core/operations_v1/transports/rest.py | 31 +++++++++------- .../operations_v1/transports/rest_asyncio.py | 21 +++++++++++ .../test_operations_rest_client.py | 35 ++++++++++++++++-- 8 files changed, 149 insertions(+), 33 deletions(-) diff --git a/google/api_core/client_options.py b/google/api_core/client_options.py index d11665d22..30bff4824 100644 --- a/google/api_core/client_options.py +++ b/google/api_core/client_options.py @@ -49,6 +49,9 @@ def get_client_cert(): """ from typing import Callable, Mapping, Optional, Sequence, Tuple +import warnings + +from google.api_core import general_helpers class ClientOptions(object): @@ -67,8 +70,9 @@ class ClientOptions(object): and ``client_encrypted_cert_source`` are mutually exclusive. quota_project_id (Optional[str]): A project name that a client's quota belongs to. - credentials_file (Optional[str]): A path to a file storing credentials. - ``credentials_file` and ``api_key`` are mutually exclusive. + credentials_file (Optional[str]): Deprecated. A path to a file storing credentials. + ``credentials_file` and ``api_key`` are mutually exclusive. This argument will be + removed in the next major version of `google-api-core`. .. warning:: Important: If you accept a credential configuration (credential JSON/File/Stream) @@ -114,6 +118,9 @@ def __init__( api_audience: Optional[str] = None, universe_domain: Optional[str] = None, ): + if credentials_file is not None: + warnings.warn(general_helpers._CREDENTIALS_FILE_WARNING, DeprecationWarning) + if client_cert_source and client_encrypted_cert_source: raise ValueError( "client_cert_source and client_encrypted_cert_source are mutually exclusive" diff --git a/google/api_core/general_helpers.py b/google/api_core/general_helpers.py index a6af45b7a..06282299d 100644 --- a/google/api_core/general_helpers.py +++ b/google/api_core/general_helpers.py @@ -14,3 +14,39 @@ # This import for backward compatibility only. from functools import wraps # noqa: F401 pragma: NO COVER + +_CREDENTIALS_FILE_WARNING = """\ +The `credentials_file` argument is deprecated because of a potential security risk. + +The `google.auth.load_credentials_from_file` method does not validate the credential +configuration. The security risk occurs when a credential configuration is accepted +from a source that is not under your control and used without validation on your side. + +If you know that you will be loading credential configurations of a +specific type, it is recommended to use a credential-type-specific +load method. + +This will ensure that an unexpected credential type with potential for +malicious intent is not loaded unintentionally. You might still have to do +validation for certain credential types. Please follow the recommendations +for that method. For example, if you want to load only service accounts, +you can create the service account credentials explicitly: + +``` +from google.cloud.vision_v1 import ImageAnnotatorClient +from google.oauth2 import service_account + +credentials = service_account.Credentials.from_service_account_file(filename) +client = ImageAnnotatorClient(credentials=credentials) +``` + +If you are loading your credential configuration from an untrusted source and have +not mitigated the risks (e.g. by validating the configuration yourself), make +these changes as soon as possible to prevent security risks to your environment. + +Regardless of the method used, it is always your responsibility to validate +configurations received from external sources. + +Refer to https://cloud.google.com/docs/authentication/external/externally-sourced-credentials +for more details. +""" diff --git a/google/api_core/grpc_helpers.py b/google/api_core/grpc_helpers.py index 079630248..430b8ce48 100644 --- a/google/api_core/grpc_helpers.py +++ b/google/api_core/grpc_helpers.py @@ -13,20 +13,19 @@ # limitations under the License. """Helpers for :mod:`grpc`.""" -from typing import Generic, Iterator, Optional, TypeVar - import collections import functools +from typing import Generic, Iterator, Optional, TypeVar import warnings -import grpc - -from google.api_core import exceptions import google.auth import google.auth.credentials import google.auth.transport.grpc import google.auth.transport.requests import google.protobuf +import grpc + +from google.api_core import exceptions, general_helpers PROTOBUF_VERSION = google.protobuf.__version__ @@ -213,9 +212,10 @@ def _create_composite_credentials( credentials (google.auth.credentials.Credentials): The credentials. If not specified, then this function will attempt to ascertain the credentials from the environment using :func:`google.auth.default`. - credentials_file (str): A file with credentials that can be loaded with + credentials_file (str): Deprecated. A file with credentials that can be loaded with :func:`google.auth.load_credentials_from_file`. This argument is - mutually exclusive with credentials. + mutually exclusive with credentials. This argument will be + removed in the next major version of `google-api-core`. .. warning:: Important: If you accept a credential configuration (credential JSON/File/Stream) @@ -245,6 +245,9 @@ def _create_composite_credentials( Raises: google.api_core.DuplicateCredentialArgs: If both a credentials object and credentials_file are passed. """ + if credentials_file is not None: + warnings.warn(general_helpers._CREDENTIALS_FILE_WARNING, DeprecationWarning) + if credentials and credentials_file: raise exceptions.DuplicateCredentialArgs( "'credentials' and 'credentials_file' are mutually exclusive." diff --git a/google/api_core/grpc_helpers_async.py b/google/api_core/grpc_helpers_async.py index af6614302..312d4df80 100644 --- a/google/api_core/grpc_helpers_async.py +++ b/google/api_core/grpc_helpers_async.py @@ -20,13 +20,14 @@ import asyncio import functools +import warnings from typing import AsyncGenerator, Generic, Iterator, Optional, TypeVar import grpc from grpc import aio -from google.api_core import exceptions, grpc_helpers +from google.api_core import exceptions, general_helpers, grpc_helpers # denotes the proto response type for grpc calls P = TypeVar("P") @@ -233,9 +234,10 @@ def create_channel( are passed to :func:`google.auth.default`. ssl_credentials (grpc.ChannelCredentials): Optional SSL channel credentials. This can be used to specify different certificates. - credentials_file (str): A file with credentials that can be loaded with + credentials_file (str): Deprecated. A file with credentials that can be loaded with :func:`google.auth.load_credentials_from_file`. This argument is - mutually exclusive with credentials. + mutually exclusive with credentials. This argument will be + removed in the next major version of `google-api-core`. .. warning:: Important: If you accept a credential configuration (credential JSON/File/Stream) @@ -280,6 +282,9 @@ def create_channel( ValueError: If `ssl_credentials` is set and `attempt_direct_path` is set to `True`. """ + if credentials_file is not None: + warnings.warn(general_helpers._CREDENTIALS_FILE_WARNING, DeprecationWarning) + # If `ssl_credentials` is set and `attempt_direct_path` is set to `True`, # raise ValueError as this is not yet supported. # See https://github.com/googleapis/python-api-core/issues/590 diff --git a/google/api_core/operations_v1/transports/base.py b/google/api_core/operations_v1/transports/base.py index 71764c1e9..46c2f5d16 100644 --- a/google/api_core/operations_v1/transports/base.py +++ b/google/api_core/operations_v1/transports/base.py @@ -16,12 +16,8 @@ import abc import re from typing import Awaitable, Callable, Optional, Sequence, Union +import warnings -import google.api_core # type: ignore -from google.api_core import exceptions as core_exceptions # type: ignore -from google.api_core import gapic_v1 # type: ignore -from google.api_core import retry as retries # type: ignore -from google.api_core import version import google.auth # type: ignore from google.auth import credentials as ga_credentials # type: ignore from google.longrunning import operations_pb2 @@ -30,6 +26,12 @@ from google.protobuf import empty_pb2, json_format # type: ignore from grpc import Compression +import google.api_core # type: ignore +from google.api_core import exceptions as core_exceptions # type: ignore +from google.api_core import gapic_v1 # type: ignore +from google.api_core import general_helpers +from google.api_core import retry as retries # type: ignore +from google.api_core import version PROTOBUF_VERSION = google.protobuf.__version__ @@ -69,9 +71,10 @@ def __init__( credentials identify the application to the service; if none are specified, the client will attempt to ascertain the credentials from the environment. - credentials_file (Optional[str]): A file with credentials that can + credentials_file (Optional[str]): Deprecated. A file with credentials that can be loaded with :func:`google.auth.load_credentials_from_file`. - This argument is mutually exclusive with credentials. + This argument is mutually exclusive with credentials. This argument will be + removed in the next major version of `google-api-core`. .. warning:: Important: If you accept a credential configuration (credential JSON/File/Stream) @@ -98,6 +101,9 @@ def __init__( "https", but for testing or local servers, "http" can be specified. """ + if credentials_file is not None: + warnings.warn(general_helpers._CREDENTIALS_FILE_WARNING, DeprecationWarning) + maybe_url_match = re.match("^(?Phttp(?:s)?://)?(?P.*)$", host) if maybe_url_match is None: raise ValueError( diff --git a/google/api_core/operations_v1/transports/rest.py b/google/api_core/operations_v1/transports/rest.py index 0705c518a..62f34d69f 100644 --- a/google/api_core/operations_v1/transports/rest.py +++ b/google/api_core/operations_v1/transports/rest.py @@ -15,23 +15,26 @@ # from typing import Callable, Dict, Optional, Sequence, Tuple, Union +import warnings +from google.auth import credentials as ga_credentials # type: ignore +from google.auth.transport.requests import AuthorizedSession # type: ignore +from google.longrunning import operations_pb2 # type: ignore +import google.protobuf +from google.protobuf import empty_pb2 # type: ignore +from google.protobuf import json_format # type: ignore +import grpc from requests import __version__ as requests_version from google.api_core import exceptions as core_exceptions # type: ignore from google.api_core import gapic_v1 # type: ignore +from google.api_core import general_helpers from google.api_core import path_template # type: ignore from google.api_core import rest_helpers # type: ignore from google.api_core import retry as retries # type: ignore -from google.auth import credentials as ga_credentials # type: ignore -from google.auth.transport.requests import AuthorizedSession # type: ignore -from google.longrunning import operations_pb2 # type: ignore -from google.protobuf import empty_pb2 # type: ignore -from google.protobuf import json_format # type: ignore -import google.protobuf -import grpc -from .base import DEFAULT_CLIENT_INFO as BASE_DEFAULT_CLIENT_INFO, OperationsTransport +from .base import DEFAULT_CLIENT_INFO as BASE_DEFAULT_CLIENT_INFO +from .base import OperationsTransport PROTOBUF_VERSION = google.protobuf.__version__ @@ -91,9 +94,10 @@ def __init__( are specified, the client will attempt to ascertain the credentials from the environment. - credentials_file (Optional[str]): A file with credentials that can + credentials_file (Optional[str]): Deprecated. A file with credentials that can be loaded with :func:`google.auth.load_credentials_from_file`. - This argument is ignored if ``channel`` is provided. + This argument is ignored if ``channel`` is provided. This argument will be + removed in the next major version of `google-api-core`. .. warning:: Important: If you accept a credential configuration (credential JSON/File/Stream) @@ -101,9 +105,9 @@ def __init__( validate it before providing it to any Google API or client library. Providing an unvalidated credential configuration to Google APIs or libraries can compromise the security of your systems and data. For more information, refer to - `Validate credential configurations from external sources`_. + `Validate credential configuration from external sources`_. - .. _Validate credential configurations from external sources: + .. _Validate credential configuration from external sources: https://cloud.google.com/docs/authentication/external/externally-sourced-credentials scopes (Optional(Sequence[str])): A list of scopes. This argument is @@ -130,6 +134,9 @@ def __init__( "v1" by default. """ + if credentials_file is not None: + warnings.warn(general_helpers._CREDENTIALS_FILE_WARNING, DeprecationWarning) + # Run the base constructor # TODO(yon-mg): resolve other ctor params i.e. scopes, quota, etc. # TODO: When custom host (api_endpoint) is set, `scopes` must *also* be set on the diff --git a/google/api_core/operations_v1/transports/rest_asyncio.py b/google/api_core/operations_v1/transports/rest_asyncio.py index 71c20eb8a..6fa9f56aa 100644 --- a/google/api_core/operations_v1/transports/rest_asyncio.py +++ b/google/api_core/operations_v1/transports/rest_asyncio.py @@ -16,6 +16,7 @@ import json from typing import Any, Callable, Coroutine, Dict, Optional, Sequence, Tuple +import warnings from google.auth import __version__ as auth_version @@ -29,6 +30,7 @@ from google.api_core import exceptions as core_exceptions # type: ignore from google.api_core import gapic_v1 # type: ignore +from google.api_core import general_helpers from google.api_core import path_template # type: ignore from google.api_core import rest_helpers # type: ignore from google.api_core import retry_async as retries_async # type: ignore @@ -96,6 +98,22 @@ def __init__( credentials identify the application to the service; if none are specified, the client will attempt to ascertain the credentials from the environment. + credentials_file (Optional[str]): Deprecated. A file with credentials that can + be loaded with :func:`google.auth.load_credentials_from_file`. + This argument is ignored if ``channel`` is provided. This argument will be + removed in the next major version of `google-api-core`. + + .. warning:: + Important: If you accept a credential configuration (credential JSON/File/Stream) + from an external source for authentication to Google Cloud Platform, you must + validate it before providing it to any Google API or client library. Providing an + unvalidated credential configuration to Google APIs or libraries can compromise + the security of your systems and data. For more information, refer to + `Validate credential configurations from external sources`_. + + .. _Validate credential configurations from external sources: + + https://cloud.google.com/docs/authentication/external/externally-sourced-credentials client_info (google.api_core.gapic_v1.client_info.ClientInfo): The client info used to send a user-agent string along with API requests. If ``None``, then default info will be used. @@ -113,6 +131,9 @@ def __init__( "v1" by default. """ + if credentials_file is not None: + warnings.warn(general_helpers._CREDENTIALS_FILE_WARNING, DeprecationWarning) + unsupported_params = { # TODO(https://github.com/googleapis/python-api-core/issues/715): Add support for `credentials_file` to async REST transport. "google.api_core.client_options.ClientOptions.credentials_file": credentials_file, diff --git a/tests/unit/operations_v1/test_operations_rest_client.py b/tests/unit/operations_v1/test_operations_rest_client.py index d1f6e0eba..4e8ef4073 100644 --- a/tests/unit/operations_v1/test_operations_rest_client.py +++ b/tests/unit/operations_v1/test_operations_rest_client.py @@ -368,6 +368,22 @@ def test_operations_client_client_options( always_use_jwt_access=True, ) + # Check the case credentials_file is provided + options = client_options.ClientOptions(credentials_file="credentials.json") + with mock.patch.object(transport_class, "__init__") as patched: + patched.return_value = None + client = client_class(client_options=options, transport=transport_name) + patched.assert_called_once_with( + credentials=None, + credentials_file="credentials.json", + host=client.DEFAULT_ENDPOINT, + scopes=None, + client_cert_source_for_mtls=None, + quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, + always_use_jwt_access=True, + ) + # TODO: Add support for mtls in async REST @pytest.mark.parametrize( @@ -544,8 +560,23 @@ def test_operations_client_client_options_credentials_file( ) -def test_list_operations_rest(): - client = _get_operations_client(is_async=False) +@pytest.mark.parametrize( + "credentials_file", + [None, "credentials.json"], +) +@mock.patch( + "google.auth.default", + autospec=True, + return_value=(mock.sentinel.credentials, mock.sentinel.project), +) +def test_list_operations_rest(google_auth_default, credentials_file): + sync_transport = transports.rest.OperationsRestTransport( + credentials_file=credentials_file, + http_options=HTTP_OPTIONS, + ) + + client = AbstractOperationsClient(transport=sync_transport) + # Mock the http request call within the method and fake a response. with mock.patch.object(_get_session_type(is_async=False), "request") as req: # Designate an appropriate value for the returned response. From 4587c0bac93bc693bfb8b5b51b294f8177ee8217 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Thu, 2 Oct 2025 16:56:41 -0700 Subject: [PATCH 080/101] chore(main): release 2.25.2 (#838) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 8 ++++++++ google/api_core/version.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aab5d53fe..669ff00e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ [1]: https://pypi.org/project/google-api-core/#history +## [2.25.2](https://github.com/googleapis/python-api-core/compare/v2.25.1...v2.25.2) (2025-10-01) + + +### Bug Fixes + +* Deprecate credentials_file argument ([#841](https://github.com/googleapis/python-api-core/issues/841)) ([324eb74](https://github.com/googleapis/python-api-core/commit/324eb7464d6ade9a8c2e413d4695bc7d7adfcb3d)) +* Fix async tests and round-off error in test expectations ([#837](https://github.com/googleapis/python-api-core/issues/837)) ([14a5978](https://github.com/googleapis/python-api-core/commit/14a59789e144905bd6c82180ad07a52bf1f84f02)) + ## [2.25.1](https://github.com/googleapis/python-api-core/compare/v2.25.0...v2.25.1) (2025-06-02) diff --git a/google/api_core/version.py b/google/api_core/version.py index 21cbec9fe..e8672849f 100644 --- a/google/api_core/version.py +++ b/google/api_core/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "2.25.1" +__version__ = "2.25.2" From 43690de33a23321d52ab856e2bf253590e1a9357 Mon Sep 17 00:00:00 2001 From: Lingqing Gan Date: Wed, 8 Oct 2025 13:33:54 -0700 Subject: [PATCH 081/101] feat: add trove classifier for Python 3.14 (#842) * chore: add trove classifier for Python 3.14 * pin grpcio and grpcio-status versions * revert pinning grpcio for python 3.12 and 3.13 --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index da404ab3b..71ce72245 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Operating System :: OS Independent", "Topic :: Internet", ] @@ -62,8 +63,10 @@ async_rest = ["google-auth[aiohttp] >= 2.35.0, < 3.0.0"] grpc = [ "grpcio >= 1.33.2, < 2.0.0", "grpcio >= 1.49.1, < 2.0.0; python_version >= '3.11'", + "grpcio >= 1.75.1, < 2.0.0; python_version >= '3.14'", "grpcio-status >= 1.33.2, < 2.0.0", "grpcio-status >= 1.49.1, < 2.0.0; python_version >= '3.11'", + "grpcio-status >= 1.75.1, < 2.0.0; python_version >= '3.14'", ] grpcgcp = ["grpcio-gcp >= 0.2.2, < 1.0.0"] grpcio-gcp = ["grpcio-gcp >= 0.2.2, < 1.0.0"] From 8168988306b5fdd55be4de26d4bcd0c8f953c74f Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 8 Oct 2025 14:28:33 -0700 Subject: [PATCH 082/101] chore(main): release 2.26.0 (#843) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ google/api_core/version.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 669ff00e4..c86f6ab6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://pypi.org/project/google-api-core/#history +## [2.26.0](https://github.com/googleapis/python-api-core/compare/v2.25.2...v2.26.0) (2025-10-08) + + +### Features + +* Add trove classifier for Python 3.14 ([#842](https://github.com/googleapis/python-api-core/issues/842)) ([43690de](https://github.com/googleapis/python-api-core/commit/43690de33a23321d52ab856e2bf253590e1a9357)) + ## [2.25.2](https://github.com/googleapis/python-api-core/compare/v2.25.1...v2.25.2) (2025-10-01) diff --git a/google/api_core/version.py b/google/api_core/version.py index e8672849f..1f7d79ab9 100644 --- a/google/api_core/version.py +++ b/google/api_core/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "2.25.2" +__version__ = "2.26.0" From 95305480d234b6dd0903960db020e55125a997e0 Mon Sep 17 00:00:00 2001 From: Chandra Shekhar Sirimala Date: Tue, 14 Oct 2025 22:05:54 +0530 Subject: [PATCH 083/101] feat: support for async bidi streaming apis (#836) --- google/api_core/bidi.py | 102 ++++------- google/api_core/bidi_async.py | 244 +++++++++++++++++++++++++ google/api_core/bidi_base.py | 88 +++++++++ tests/asyncio/test_bidi_async.py | 305 +++++++++++++++++++++++++++++++ 4 files changed, 676 insertions(+), 63 deletions(-) create mode 100644 google/api_core/bidi_async.py create mode 100644 google/api_core/bidi_base.py create mode 100644 tests/asyncio/test_bidi_async.py diff --git a/google/api_core/bidi.py b/google/api_core/bidi.py index bed4c70e9..270ad0915 100644 --- a/google/api_core/bidi.py +++ b/google/api_core/bidi.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Bi-directional streaming RPC helpers.""" +"""Helpers for synchronous bidirectional streaming RPCs.""" import collections import datetime @@ -22,6 +22,7 @@ import time from google.api_core import exceptions +from google.api_core.bidi_base import BidiRpcBase _LOGGER = logging.getLogger(__name__) _BIDIRECTIONAL_CONSUMER_NAME = "Thread-ConsumeBidirectionalStream" @@ -36,21 +37,6 @@ class _RequestQueueGenerator(object): otherwise open-ended set of requests to send through a request-streaming (or bidirectional) RPC. - The reason this is necessary is because gRPC takes an iterator as the - request for request-streaming RPCs. gRPC consumes this iterator in another - thread to allow it to block while generating requests for the stream. - However, if the generator blocks indefinitely gRPC will not be able to - clean up the thread as it'll be blocked on `next(iterator)` and not be able - to check the channel status to stop iterating. This helper mitigates that - by waiting on the queue with a timeout and checking the RPC state before - yielding. - - Finally, it allows for retrying without swapping queues because if it does - pull an item off the queue when the RPC is inactive, it'll immediately put - it back and then exit. This is necessary because yielding the item in this - case will cause gRPC to discard it. In practice, this means that the order - of messages is not guaranteed. If such a thing is necessary it would be - easy to use a priority queue. Example:: @@ -62,12 +48,6 @@ class _RequestQueueGenerator(object): print(response) q.put(...) - Note that it is possible to accomplish this behavior without "spinning" - (using a queue timeout). One possible way would be to use more threads to - multiplex the grpc end event with the queue, another possible way is to - use selectors and a custom event/queue object. Both of these approaches - are significant from an engineering perspective for small benefit - the - CPU consumed by spinning is pretty minuscule. Args: queue (queue_module.Queue): The request queue. @@ -96,6 +76,31 @@ def _is_active(self): return self.call is None or self.call.is_active() def __iter__(self): + # The reason this is necessary is because gRPC takes an iterator as the + # request for request-streaming RPCs. gRPC consumes this iterator in + # another thread to allow it to block while generating requests for + # the stream. However, if the generator blocks indefinitely gRPC will + # not be able to clean up the thread as it'll be blocked on + # `next(iterator)` and not be able to check the channel status to stop + # iterating. This helper mitigates that by waiting on the queue with + # a timeout and checking the RPC state before yielding. + # + # Finally, it allows for retrying without swapping queues because if + # it does pull an item off the queue when the RPC is inactive, it'll + # immediately put it back and then exit. This is necessary because + # yielding the item in this case will cause gRPC to discard it. In + # practice, this means that the order of messages is not guaranteed. + # If such a thing is necessary it would be easy to use a priority + # queue. + # + # Note that it is possible to accomplish this behavior without + # "spinning" (using a queue timeout). One possible way would be to use + # more threads to multiplex the grpc end event with the queue, another + # possible way is to use selectors and a custom event/queue object. + # Both of these approaches are significant from an engineering + # perspective for small benefit - the CPU consumed by spinning is + # pretty minuscule. + if self._initial_request is not None: if callable(self._initial_request): yield self._initial_request() @@ -201,7 +206,7 @@ def __repr__(self): ) -class BidiRpc(object): +class BidiRpc(BidiRpcBase): """A helper for consuming a bi-directional streaming RPC. This maps gRPC's built-in interface which uses a request iterator and a @@ -227,6 +232,8 @@ class BidiRpc(object): rpc.send(example_pb2.StreamingRpcRequest( data='example')) + rpc.close() + This does *not* retry the stream on errors. See :class:`ResumableBidiRpc`. Args: @@ -240,40 +247,14 @@ class BidiRpc(object): the request. """ - def __init__(self, start_rpc, initial_request=None, metadata=None): - self._start_rpc = start_rpc - self._initial_request = initial_request - self._rpc_metadata = metadata - self._request_queue = queue_module.Queue() - self._request_generator = None - self._is_active = False - self._callbacks = [] - self.call = None - - def add_done_callback(self, callback): - """Adds a callback that will be called when the RPC terminates. - - This occurs when the RPC errors or is successfully terminated. - - Args: - callback (Callable[[grpc.Future], None]): The callback to execute. - It will be provided with the same gRPC future as the underlying - stream which will also be a :class:`grpc.Call`. - """ - self._callbacks.append(callback) - - def _on_call_done(self, future): - # This occurs when the RPC errors or is successfully terminated. - # Note that grpc's "future" here can also be a grpc.RpcError. - # See note in https://github.com/grpc/grpc/issues/10885#issuecomment-302651331 - # that `grpc.RpcError` is also `grpc.call`. - for callback in self._callbacks: - callback(future) + def _create_queue(self): + """Create a queue for requests.""" + return queue_module.Queue() def open(self): """Opens the stream.""" if self.is_active: - raise ValueError("Can not open an already open stream.") + raise ValueError("Cannot open an already open stream.") request_generator = _RequestQueueGenerator( self._request_queue, initial_request=self._initial_request @@ -322,7 +303,7 @@ def send(self, request): request (protobuf.Message): The request to send. """ if self.call is None: - raise ValueError("Can not send() on an RPC that has never been open()ed.") + raise ValueError("Cannot send on an RPC stream that has never been opened.") # Don't use self.is_active(), as ResumableBidiRpc will overload it # to mean something semantically different. @@ -343,20 +324,15 @@ def recv(self): protobuf.Message: The received message. """ if self.call is None: - raise ValueError("Can not recv() on an RPC that has never been open()ed.") + raise ValueError("Cannot recv on an RPC stream that has never been opened.") return next(self.call) @property def is_active(self): - """bool: True if this stream is currently open and active.""" + """True if this stream is currently open and active.""" return self.call is not None and self.call.is_active() - @property - def pending_requests(self): - """int: Returns an estimate of the number of queued requests.""" - return self._request_queue.qsize() - def _never_terminate(future_or_error): """By default, no errors cause BiDi termination.""" @@ -544,7 +520,7 @@ def _send(self, request): call = self.call if call is None: - raise ValueError("Can not send() on an RPC that has never been open()ed.") + raise ValueError("Cannot send on an RPC that has never been opened.") # Don't use self.is_active(), as ResumableBidiRpc will overload it # to mean something semantically different. @@ -563,7 +539,7 @@ def _recv(self): call = self.call if call is None: - raise ValueError("Can not recv() on an RPC that has never been open()ed.") + raise ValueError("Cannot recv on an RPC that has never been opened.") return next(call) diff --git a/google/api_core/bidi_async.py b/google/api_core/bidi_async.py new file mode 100644 index 000000000..d73b4c98d --- /dev/null +++ b/google/api_core/bidi_async.py @@ -0,0 +1,244 @@ +# Copyright 2025, Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Asynchronous bi-directional streaming RPC helpers.""" + +import asyncio +import logging +from typing import Callable, Optional, Union + +from grpc import aio + +from google.api_core import exceptions +from google.api_core.bidi_base import BidiRpcBase + +from google.protobuf.message import Message as ProtobufMessage + + +_LOGGER = logging.getLogger(__name__) + + +class _AsyncRequestQueueGenerator: + """_AsyncRequestQueueGenerator is a helper class for sending asynchronous + requests to a gRPC stream from a Queue. + + This generator takes asynchronous requests off a given `asyncio.Queue` and + yields them to gRPC. + + It's useful when you have an indeterminate, indefinite, or otherwise + open-ended set of requests to send through a request-streaming (or + bidirectional) RPC. + + Example:: + + requests = _AsyncRequestQueueGenerator(q) + call = await stub.StreamingRequest(requests) + requests.call = call + + async for response in call: + print(response) + await q.put(...) + + Args: + queue (asyncio.Queue): The request queue. + initial_request (Union[ProtobufMessage, + Callable[[], ProtobufMessage]]): The initial request to + yield. This is done independently of the request queue to allow for + easily restarting streams that require some initial configuration + request. + """ + + def __init__( + self, + queue: asyncio.Queue, + initial_request: Optional[ + Union[ProtobufMessage, Callable[[], ProtobufMessage]] + ] = None, + ) -> None: + self._queue = queue + self._initial_request = initial_request + self.call: Optional[aio.Call] = None + + def _is_active(self) -> bool: + """Returns true if the call is not set or not completed.""" + # Note: there is a possibility that this starts *before* the call + # property is set. So we have to check if self.call is set before + # seeing if it's active. We need to return True if self.call is None. + # See https://github.com/googleapis/python-api-core/issues/560. + return self.call is None or not self.call.done() + + async def __aiter__(self): + # The reason this is necessary is because it lets the user have + # control on when they would want to send requests proto messages + # instead of sending all of them initially. + # + # This is achieved via asynchronous queue (asyncio.Queue), + # gRPC awaits until there's a message in the queue. + # + # Finally, it allows for retrying without swapping queues because if + # it does pull an item off the queue when the RPC is inactive, it'll + # immediately put it back and then exit. This is necessary because + # yielding the item in this case will cause gRPC to discard it. In + # practice, this means that the order of messages is not guaranteed. + # If preserving order is necessary it would be easy to use a priority + # queue. + if self._initial_request is not None: + if callable(self._initial_request): + yield self._initial_request() + else: + yield self._initial_request + + while True: + item = await self._queue.get() + + # The consumer explicitly sent "None", indicating that the request + # should end. + if item is None: + _LOGGER.debug("Cleanly exiting request generator.") + return + + if not self._is_active(): + # We have an item, but the call is closed. We should put the + # item back on the queue so that the next call can consume it. + await self._queue.put(item) + _LOGGER.debug( + "Inactive call, replacing item on queue and exiting " + "request generator." + ) + return + + yield item + + +class AsyncBidiRpc(BidiRpcBase): + """A helper for consuming a async bi-directional streaming RPC. + + This maps gRPC's built-in interface which uses a request iterator and a + response iterator into a socket-like :func:`send` and :func:`recv`. This + is a more useful pattern for long-running or asymmetric streams (streams + where there is not a direct correlation between the requests and + responses). + + Example:: + + initial_request = example_pb2.StreamingRpcRequest( + setting='example') + rpc = AsyncBidiRpc( + stub.StreamingRpc, + initial_request=initial_request, + metadata=[('name', 'value')] + ) + + await rpc.open() + + while rpc.is_active: + print(await rpc.recv()) + await rpc.send(example_pb2.StreamingRpcRequest( + data='example')) + + await rpc.close() + + This does *not* retry the stream on errors. + + Args: + start_rpc (grpc.aio.StreamStreamMultiCallable): The gRPC method used to + start the RPC. + initial_request (Union[ProtobufMessage, + Callable[[], ProtobufMessage]]): The initial request to + yield. This is useful if an initial request is needed to start the + stream. + metadata (Sequence[Tuple(str, str)]): RPC metadata to include in + the request. + """ + + def _create_queue(self) -> asyncio.Queue: + """Create a queue for requests.""" + return asyncio.Queue() + + async def open(self) -> None: + """Opens the stream.""" + if self.is_active: + raise ValueError("Cannot open an already open stream.") + + request_generator = _AsyncRequestQueueGenerator( + self._request_queue, initial_request=self._initial_request + ) + try: + call = await self._start_rpc(request_generator, metadata=self._rpc_metadata) + except exceptions.GoogleAPICallError as exc: + # The original `grpc.aio.AioRpcError` (which is usually also a + # `grpc.aio.Call`) is available from the ``response`` property on + # the mapped exception. + self._on_call_done(exc.response) + raise + + request_generator.call = call + + # TODO: api_core should expose the future interface for wrapped + # callables as well. + if hasattr(call, "_wrapped"): # pragma: NO COVER + call._wrapped.add_done_callback(self._on_call_done) + else: + call.add_done_callback(self._on_call_done) + + self._request_generator = request_generator + self.call = call + + async def close(self) -> None: + """Closes the stream.""" + if self.call is None: + return + + await self._request_queue.put(None) + self.call.cancel() + self._request_generator = None + self._initial_request = None + self._callbacks = [] + # Don't set self.call to None. Keep it around so that send/recv can + # raise the error. + + async def send(self, request: ProtobufMessage) -> None: + """Queue a message to be sent on the stream. + + If the underlying RPC has been closed, this will raise. + + Args: + request (ProtobufMessage): The request to send. + """ + if self.call is None: + raise ValueError("Cannot send on an RPC stream that has never been opened.") + + if not self.call.done(): + await self._request_queue.put(request) + else: + # calling read should cause the call to raise. + await self.call.read() + + async def recv(self) -> ProtobufMessage: + """Wait for a message to be returned from the stream. + + If the underlying RPC has been closed, this will raise. + + Returns: + ProtobufMessage: The received message. + """ + if self.call is None: + raise ValueError("Cannot recv on an RPC stream that has never been opened.") + + return await self.call.read() + + @property + def is_active(self) -> bool: + """Whether the stream is currently open and active.""" + return self.call is not None and not self.call.done() diff --git a/google/api_core/bidi_base.py b/google/api_core/bidi_base.py new file mode 100644 index 000000000..9288fda41 --- /dev/null +++ b/google/api_core/bidi_base.py @@ -0,0 +1,88 @@ +# Copyright 2025, Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# You may obtain a copy of the License at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Base class for bi-directional streaming RPC helpers.""" + + +class BidiRpcBase: + """A base class for consuming a bi-directional streaming RPC. + + This maps gRPC's built-in interface which uses a request iterator and a + response iterator into a socket-like :func:`send` and :func:`recv`. This + is a more useful pattern for long-running or asymmetric streams (streams + where there is not a direct correlation between the requests and + responses). + + This does *not* retry the stream on errors. + + Args: + start_rpc (Union[grpc.StreamStreamMultiCallable, + grpc.aio.StreamStreamMultiCallable]): The gRPC method used + to start the RPC. + initial_request (Union[protobuf.Message, + Callable[[], protobuf.Message]]): The initial request to + yield. This is useful if an initial request is needed to start the + stream. + metadata (Sequence[Tuple(str, str)]): RPC metadata to include in + the request. + """ + + def __init__(self, start_rpc, initial_request=None, metadata=None): + self._start_rpc = start_rpc + self._initial_request = initial_request + self._rpc_metadata = metadata + self._request_queue = self._create_queue() + self._request_generator = None + self._callbacks = [] + self.call = None + + def _create_queue(self): + """Create a queue for requests.""" + raise NotImplementedError("`_create_queue` is not implemented.") + + def add_done_callback(self, callback): + """Adds a callback that will be called when the RPC terminates. + + This occurs when the RPC errors or is successfully terminated. + + Args: + callback (Union[Callable[[grpc.Future], None], Callable[[Any], None]]): + The callback to execute after gRPC call completed (success or + failure). + + For sync streaming gRPC: Callable[[grpc.Future], None] + + For async streaming gRPC: Callable[[Any], None] + """ + self._callbacks.append(callback) + + def _on_call_done(self, future): + # This occurs when the RPC errors or is successfully terminated. + # Note that grpc's "future" here can also be a grpc.RpcError. + # See note in https://github.com/grpc/grpc/issues/10885#issuecomment-302651331 + # that `grpc.RpcError` is also `grpc.Call`. + # for asynchronous gRPC call it would be `grpc.aio.AioRpcError` + + # Note: sync callbacks can be limiting for async code, because you can't + # await anything in a sync callback. + for callback in self._callbacks: + callback(future) + + @property + def is_active(self): + """True if the gRPC call is not done yet.""" + raise NotImplementedError("`is_active` is not implemented.") + + @property + def pending_requests(self): + """Estimate of the number of queued requests.""" + return self._request_queue.qsize() diff --git a/tests/asyncio/test_bidi_async.py b/tests/asyncio/test_bidi_async.py new file mode 100644 index 000000000..696113dbf --- /dev/null +++ b/tests/asyncio/test_bidi_async.py @@ -0,0 +1,305 @@ +# Copyright 2025, Google LLC All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys +import asyncio + +from unittest import mock + +try: + from unittest.mock import AsyncMock +except ImportError: # pragma: NO COVER + from mock import AsyncMock # type: ignore + + +import pytest + +try: + from grpc import aio +except ImportError: # pragma: NO COVER + pytest.skip("No GRPC", allow_module_level=True) + +from google.api_core import bidi_async +from google.api_core import exceptions + +# TODO: remove this when droppping support for "Python 3.10" and below. +if sys.version_info < (3, 10): # type: ignore[operator] + + def aiter(obj): + return obj.__aiter__() + + async def anext(obj): + return await obj.__anext__() + + +@pytest.mark.asyncio +class Test_AsyncRequestQueueGenerator: + async def test_bounded_consume(self): + call = mock.create_autospec(aio.Call, instance=True) + call.done.return_value = False + + q = asyncio.Queue() + await q.put(mock.sentinel.A) + await q.put(mock.sentinel.B) + + generator = bidi_async._AsyncRequestQueueGenerator(q) + generator.call = call + + items = [] + gen_aiter = aiter(generator) + + items.append(await anext(gen_aiter)) + items.append(await anext(gen_aiter)) + + # At this point, the queue is empty. The next call to anext will sleep. + # We make the call inactive. + call.done.return_value = True + + with pytest.raises(asyncio.TimeoutError): + await asyncio.wait_for(anext(gen_aiter), timeout=0.01) + + assert items == [mock.sentinel.A, mock.sentinel.B] + + async def test_yield_initial_and_exit(self): + q = asyncio.Queue() + call = mock.create_autospec(aio.Call, instance=True) + call.done.return_value = True + + generator = bidi_async._AsyncRequestQueueGenerator( + q, initial_request=mock.sentinel.A + ) + generator.call = call + + assert await anext(aiter(generator)) == mock.sentinel.A + + async def test_yield_initial_callable_and_exit(self): + q = asyncio.Queue() + call = mock.create_autospec(aio.Call, instance=True) + call.done.return_value = True + + generator = bidi_async._AsyncRequestQueueGenerator( + q, initial_request=lambda: mock.sentinel.A + ) + generator.call = call + + assert await anext(aiter(generator)) == mock.sentinel.A + + async def test_exit_when_inactive_with_item(self): + q = asyncio.Queue() + await q.put(mock.sentinel.A) + + call = mock.create_autospec(aio.Call, instance=True) + call.done.return_value = True + + generator = bidi_async._AsyncRequestQueueGenerator(q) + generator.call = call + + with pytest.raises( + StopAsyncIteration, + ): + assert await anext(aiter(generator)) + + # Make sure it put the item back. + assert not q.empty() + assert await q.get() == mock.sentinel.A + + async def test_exit_when_inactive_empty(self): + q = asyncio.Queue() + call = mock.create_autospec(aio.Call, instance=True) + call.done.return_value = True + + generator = bidi_async._AsyncRequestQueueGenerator(q) + generator.call = call + + with pytest.raises(asyncio.TimeoutError): + await asyncio.wait_for(anext(aiter(generator)), timeout=0.01) + + async def test_exit_with_stop(self): + q = asyncio.Queue() + await q.put(None) + call = mock.create_autospec(aio.Call, instance=True) + call.done.return_value = False + + generator = bidi_async._AsyncRequestQueueGenerator(q) + generator.call = call + + with pytest.raises(StopAsyncIteration): + assert await anext(aiter(generator)) + + +def make_async_rpc(): + """Makes a mock async RPC used to test Bidi classes.""" + call = mock.create_autospec(aio.StreamStreamCall, instance=True) + rpc = AsyncMock() + + def rpc_side_effect(request, metadata=None): + call.done.return_value = False + return call + + rpc.side_effect = rpc_side_effect + + def cancel_side_effect(): + call.done.return_value = True + return True + + call.cancel.side_effect = cancel_side_effect + call.read = AsyncMock() + + return rpc, call + + +class AsyncClosedCall: + def __init__(self, exception): + self.exception = exception + + def done(self): + return True + + async def read(self): + raise self.exception + + +class TestAsyncBidiRpc: + def test_initial_state(self): + bidi_rpc = bidi_async.AsyncBidiRpc(None) + assert bidi_rpc.is_active is False + + def test_done_callbacks(self): + bidi_rpc = bidi_async.AsyncBidiRpc(None) + callback = mock.Mock(spec=["__call__"]) + + bidi_rpc.add_done_callback(callback) + bidi_rpc._on_call_done(mock.sentinel.future) + + callback.assert_called_once_with(mock.sentinel.future) + + @pytest.mark.asyncio + @pytest.mark.skipif( + sys.version_info < (3, 8), # type: ignore[operator] + reason="Versions of Python below 3.8 don't provide support for assert_awaited_once", + ) + async def test_metadata(self): + rpc, call = make_async_rpc() + bidi_rpc = bidi_async.AsyncBidiRpc(rpc, metadata=mock.sentinel.A) + assert bidi_rpc._rpc_metadata == mock.sentinel.A + + await bidi_rpc.open() + assert bidi_rpc.call == call + rpc.assert_awaited_once() + assert rpc.call_args.kwargs["metadata"] == mock.sentinel.A + + @pytest.mark.asyncio + async def test_open(self): + rpc, call = make_async_rpc() + bidi_rpc = bidi_async.AsyncBidiRpc(rpc) + + await bidi_rpc.open() + + assert bidi_rpc.call == call + assert bidi_rpc.is_active + call.add_done_callback.assert_called_once_with(bidi_rpc._on_call_done) + + @pytest.mark.asyncio + async def test_open_error_already_open(self): + rpc, _ = make_async_rpc() + bidi_rpc = bidi_async.AsyncBidiRpc(rpc) + + await bidi_rpc.open() + + with pytest.raises(ValueError): + await bidi_rpc.open() + + @pytest.mark.asyncio + async def test_open_error_call_error(self): + rpc, _ = make_async_rpc() + expected_exception = exceptions.GoogleAPICallError( + "test", response=mock.sentinel.response + ) + rpc.side_effect = expected_exception + bidi_rpc = bidi_async.AsyncBidiRpc(rpc) + callback = mock.Mock(spec=["__call__"]) + bidi_rpc.add_done_callback(callback) + + with pytest.raises(exceptions.GoogleAPICallError) as exc_info: + await bidi_rpc.open() + + assert exc_info.value == expected_exception + callback.assert_called_once_with(mock.sentinel.response) + + @pytest.mark.asyncio + async def test_close(self): + rpc, call = make_async_rpc() + bidi_rpc = bidi_async.AsyncBidiRpc(rpc) + await bidi_rpc.open() + + await bidi_rpc.close() + + call.cancel.assert_called_once() + assert bidi_rpc.call is call + assert bidi_rpc.is_active is False + # ensure the request queue was signaled to stop. + assert bidi_rpc.pending_requests == 1 + assert await bidi_rpc._request_queue.get() is None + # ensure request and callbacks are cleaned up + assert bidi_rpc._initial_request is None + assert not bidi_rpc._callbacks + + @pytest.mark.asyncio + async def test_close_no_rpc(self): + bidi_rpc = bidi_async.AsyncBidiRpc(None) + await bidi_rpc.close() + + @pytest.mark.asyncio + async def test_send(self): + rpc, call = make_async_rpc() + bidi_rpc = bidi_async.AsyncBidiRpc(rpc) + await bidi_rpc.open() + + await bidi_rpc.send(mock.sentinel.request) + + assert bidi_rpc.pending_requests == 1 + assert await bidi_rpc._request_queue.get() is mock.sentinel.request + + @pytest.mark.asyncio + async def test_send_not_open(self): + bidi_rpc = bidi_async.AsyncBidiRpc(None) + + with pytest.raises(ValueError): + await bidi_rpc.send(mock.sentinel.request) + + @pytest.mark.asyncio + async def test_send_dead_rpc(self): + error = ValueError() + bidi_rpc = bidi_async.AsyncBidiRpc(None) + bidi_rpc.call = AsyncClosedCall(error) + + with pytest.raises(ValueError): + await bidi_rpc.send(mock.sentinel.request) + + @pytest.mark.asyncio + async def test_recv(self): + bidi_rpc = bidi_async.AsyncBidiRpc(None) + bidi_rpc.call = mock.create_autospec(aio.Call, instance=True) + bidi_rpc.call.read = AsyncMock(return_value=mock.sentinel.response) + + response = await bidi_rpc.recv() + + assert response == mock.sentinel.response + + @pytest.mark.asyncio + async def test_recv_not_open(self): + bidi_rpc = bidi_async.AsyncBidiRpc(None) + + with pytest.raises(ValueError): + await bidi_rpc.recv() From 1df063f8891518b32ebeefb157ee7bf112d49230 Mon Sep 17 00:00:00 2001 From: "gcf-owl-bot[bot]" <78513119+gcf-owl-bot[bot]@users.noreply.github.com> Date: Wed, 22 Oct 2025 14:41:59 -0700 Subject: [PATCH 084/101] chore(python): Add Python 3.14 to python post processor image (#844) Source-Link: https://github.com/googleapis/synthtool/commit/16790a32126759493ba20781e04edd165825ff82 Post-Processor: gcr.io/cloud-devrel-public-resources/owlbot-python:latest@sha256:543e209e7c1c1ffe720eb4db1a3f045a75099304fb19aa11a47dc717b8aae2a9 Co-authored-by: Owl Bot Co-authored-by: Victor Chudnovsky --- .github/.OwlBot.lock.yaml | 4 +- .kokoro/samples/python3.14/common.cfg | 40 ++++++++++++++++++++ .kokoro/samples/python3.14/continuous.cfg | 6 +++ .kokoro/samples/python3.14/periodic-head.cfg | 11 ++++++ .kokoro/samples/python3.14/periodic.cfg | 6 +++ .kokoro/samples/python3.14/presubmit.cfg | 6 +++ 6 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 .kokoro/samples/python3.14/common.cfg create mode 100644 .kokoro/samples/python3.14/continuous.cfg create mode 100644 .kokoro/samples/python3.14/periodic-head.cfg create mode 100644 .kokoro/samples/python3.14/periodic.cfg create mode 100644 .kokoro/samples/python3.14/presubmit.cfg diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 51b21a62b..4a311db02 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,5 +13,5 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest - digest: sha256:a7aef70df5f13313ddc027409fc8f3151422ec2a57ac8730fce8fa75c060d5bb -# created: 2025-04-10T17:00:10.042601326Z + digest: sha256:543e209e7c1c1ffe720eb4db1a3f045a75099304fb19aa11a47dc717b8aae2a9 +# created: 2025-10-09T14:48:42.914384887Z diff --git a/.kokoro/samples/python3.14/common.cfg b/.kokoro/samples/python3.14/common.cfg new file mode 100644 index 000000000..a083385d7 --- /dev/null +++ b/.kokoro/samples/python3.14/common.cfg @@ -0,0 +1,40 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Build logs will be here +action { + define_artifacts { + regex: "**/*sponge_log.xml" + } +} + +# Specify which tests to run +env_vars: { + key: "RUN_TESTS_SESSION" + value: "py-3.14" +} + +# Declare build specific Cloud project. +env_vars: { + key: "BUILD_SPECIFIC_GCLOUD_PROJECT" + value: "python-docs-samples-tests-314" +} + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/python-api-core/.kokoro/test-samples.sh" +} + +# Configure the docker image for kokoro-trampoline. +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/python-samples-testing-docker" +} + +# Download secrets for samples +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/python-docs-samples" + +# Download trampoline resources. +gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" + +# Use the trampoline script to run in docker. +build_file: "python-api-core/.kokoro/trampoline_v2.sh" diff --git a/.kokoro/samples/python3.14/continuous.cfg b/.kokoro/samples/python3.14/continuous.cfg new file mode 100644 index 000000000..a1c8d9759 --- /dev/null +++ b/.kokoro/samples/python3.14/continuous.cfg @@ -0,0 +1,6 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "True" +} \ No newline at end of file diff --git a/.kokoro/samples/python3.14/periodic-head.cfg b/.kokoro/samples/python3.14/periodic-head.cfg new file mode 100644 index 000000000..a18c0cfc6 --- /dev/null +++ b/.kokoro/samples/python3.14/periodic-head.cfg @@ -0,0 +1,11 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "True" +} + +env_vars: { + key: "TRAMPOLINE_BUILD_FILE" + value: "github/python-api-core/.kokoro/test-samples-against-head.sh" +} diff --git a/.kokoro/samples/python3.14/periodic.cfg b/.kokoro/samples/python3.14/periodic.cfg new file mode 100644 index 000000000..71cd1e597 --- /dev/null +++ b/.kokoro/samples/python3.14/periodic.cfg @@ -0,0 +1,6 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "False" +} diff --git a/.kokoro/samples/python3.14/presubmit.cfg b/.kokoro/samples/python3.14/presubmit.cfg new file mode 100644 index 000000000..a1c8d9759 --- /dev/null +++ b/.kokoro/samples/python3.14/presubmit.cfg @@ -0,0 +1,6 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "INSTALL_LIBRARY_FROM_SOURCE" + value: "True" +} \ No newline at end of file From 6c16e96e3e1ec983571f5e35d1e11b2cf125a11b Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Wed, 22 Oct 2025 16:38:21 -0700 Subject: [PATCH 085/101] chore(main): release 2.27.0 (#845) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ google/api_core/version.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c86f6ab6a..69e124dc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://pypi.org/project/google-api-core/#history +## [2.27.0](https://github.com/googleapis/python-api-core/compare/v2.26.0...v2.27.0) (2025-10-22) + + +### Features + +* Support for async bidi streaming apis ([#836](https://github.com/googleapis/python-api-core/issues/836)) ([9530548](https://github.com/googleapis/python-api-core/commit/95305480d234b6dd0903960db020e55125a997e0)) + ## [2.26.0](https://github.com/googleapis/python-api-core/compare/v2.25.2...v2.26.0) (2025-10-08) diff --git a/google/api_core/version.py b/google/api_core/version.py index 1f7d79ab9..4f038c462 100644 --- a/google/api_core/version.py +++ b/google/api_core/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "2.26.0" +__version__ = "2.27.0" From d36e896f98a2371c4d58ce1a7a3bc1a77a081836 Mon Sep 17 00:00:00 2001 From: Victor Chudnovsky Date: Fri, 24 Oct 2025 09:51:31 -0700 Subject: [PATCH 086/101] feat: provide and use Python version support check (#832) --- google/api_core/__init__.py | 18 ++ google/api_core/_python_package_support.py | 209 ++++++++++++++++ google/api_core/_python_version_support.py | 269 +++++++++++++++++++++ tests/unit/gapic/test_method.py | 4 +- tests/unit/test_bidi.py | 6 + tests/unit/test_python_package_support.py | 147 +++++++++++ tests/unit/test_python_version_support.py | 253 +++++++++++++++++++ tests/unit/test_timeout.py | 11 +- 8 files changed, 915 insertions(+), 2 deletions(-) create mode 100644 google/api_core/_python_package_support.py create mode 100644 google/api_core/_python_version_support.py create mode 100644 tests/unit/test_python_package_support.py create mode 100644 tests/unit/test_python_version_support.py diff --git a/google/api_core/__init__.py b/google/api_core/__init__.py index b80ea3726..85811a157 100644 --- a/google/api_core/__init__.py +++ b/google/api_core/__init__.py @@ -17,6 +17,24 @@ This package contains common code and utilities used by Google client libraries. """ +from google.api_core import _python_package_support +from google.api_core import _python_version_support from google.api_core import version as api_core_version __version__ = api_core_version.__version__ + +# NOTE: Until dependent artifacts require this version of +# google.api_core, the functionality below must be made available +# manually in those artifacts. + +# expose dependency checks for external callers +check_python_version = _python_version_support.check_python_version +check_dependency_versions = _python_package_support.check_dependency_versions +warn_deprecation_for_versions_less_than = ( + _python_package_support.warn_deprecation_for_versions_less_than +) +DependencyConstraint = _python_package_support.DependencyConstraint + +# perform version checks against api_core, and emit warnings if needed +check_python_version(package="google.api_core") +check_dependency_versions("google.api_core") diff --git a/google/api_core/_python_package_support.py b/google/api_core/_python_package_support.py new file mode 100644 index 000000000..cc805b8d7 --- /dev/null +++ b/google/api_core/_python_package_support.py @@ -0,0 +1,209 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Code to check versions of dependencies used by Google Cloud Client Libraries.""" + +import warnings +import sys +from typing import Optional + +from collections import namedtuple + +from ._python_version_support import ( + _flatten_message, + _get_distribution_and_import_packages, +) + +from packaging.version import parse as parse_version + +# Here we list all the packages for which we want to issue warnings +# about deprecated and unsupported versions. +DependencyConstraint = namedtuple( + "DependencyConstraint", + ["package_name", "minimum_fully_supported_version", "recommended_version"], +) +_PACKAGE_DEPENDENCY_WARNINGS = [ + DependencyConstraint( + "google.protobuf", + minimum_fully_supported_version="4.25.8", + recommended_version="6.x", + ) +] + + +DependencyVersion = namedtuple("DependencyVersion", ["version", "version_string"]) +# Version string we provide in a DependencyVersion when we can't determine the version of a +# package. +UNKNOWN_VERSION_STRING = "--" + + +def get_dependency_version( + dependency_name: str, +) -> DependencyVersion: + """Get the parsed version of an installed package dependency. + + This function checks for an installed package and returns its version + as a `packaging.version.Version` object for safe comparison. It handles + both modern (Python 3.8+) and legacy (Python 3.7) environments. + + Args: + dependency_name: The distribution name of the package (e.g., 'requests'). + + Returns: + A DependencyVersion namedtuple with `version` and + `version_string` attributes, or `DependencyVersion(None, + UNKNOWN_VERSION_STRING)` if the package is not found or + another error occurs during version discovery. + + """ + try: + if sys.version_info >= (3, 8): + from importlib import metadata + + version_string = metadata.version(dependency_name) + return DependencyVersion(parse_version(version_string), version_string) + + # TODO(https://github.com/googleapis/python-api-core/issues/835): Remove + # this code path once we drop support for Python 3.7 + else: # pragma: NO COVER + # Use pkg_resources, which is part of setuptools. + import pkg_resources + + version_string = pkg_resources.get_distribution(dependency_name).version + return DependencyVersion(parse_version(version_string), version_string) + + except Exception: + return DependencyVersion(None, UNKNOWN_VERSION_STRING) + + +def warn_deprecation_for_versions_less_than( + consumer_import_package: str, + dependency_import_package: str, + minimum_fully_supported_version: str, + recommended_version: Optional[str] = None, + message_template: Optional[str] = None, +): + """Issue any needed deprecation warnings for `dependency_import_package`. + + If `dependency_import_package` is installed at a version less than + `minimum_fully_supported_version`, this issues a warning using either a + default `message_template` or one provided by the user. The + default `message_template` informs the user that they will not receive + future updates for `consumer_import_package` if + `dependency_import_package` is somehow pinned to a version lower + than `minimum_fully_supported_version`. + + Args: + consumer_import_package: The import name of the package that + needs `dependency_import_package`. + dependency_import_package: The import name of the dependency to check. + minimum_fully_supported_version: The dependency_import_package version number + below which a deprecation warning will be logged. + recommended_version: If provided, the recommended next version, which + could be higher than `minimum_fully_supported_version`. + message_template: A custom default message template to replace + the default. This `message_template` is treated as an + f-string, where the following variables are defined: + `dependency_import_package`, `consumer_import_package` and + `dependency_distribution_package` and + `consumer_distribution_package` and `dependency_package`, + `consumer_package` , which contain the import packages, the + distribution packages, and pretty string with both the + distribution and import packages for the dependency and the + consumer, respectively; and `minimum_fully_supported_version`, + `version_used`, and `version_used_string`, which refer to supported + and currently-used versions of the dependency. + + """ + if ( + not consumer_import_package + or not dependency_import_package + or not minimum_fully_supported_version + ): # pragma: NO COVER + return + dependency_version = get_dependency_version(dependency_import_package) + if not dependency_version.version: + return + if dependency_version.version < parse_version(minimum_fully_supported_version): + ( + dependency_package, + dependency_distribution_package, + ) = _get_distribution_and_import_packages(dependency_import_package) + ( + consumer_package, + consumer_distribution_package, + ) = _get_distribution_and_import_packages(consumer_import_package) + + recommendation = ( + " (we recommend {recommended_version})" if recommended_version else "" + ) + message_template = message_template or _flatten_message( + """ + DEPRECATION: Package {consumer_package} depends on + {dependency_package}, currently installed at version + {version_used_string}. Future updates to + {consumer_package} will require {dependency_package} at + version {minimum_fully_supported_version} or + higher{recommendation}. Please ensure that either (a) your + Python environment doesn't pin the version of + {dependency_package}, so that updates to + {consumer_package} can require the higher version, or (b) + you manually update your Python environment to use at + least version {minimum_fully_supported_version} of + {dependency_package}. + """ + ) + warnings.warn( + message_template.format( + consumer_import_package=consumer_import_package, + dependency_import_package=dependency_import_package, + consumer_distribution_package=consumer_distribution_package, + dependency_distribution_package=dependency_distribution_package, + dependency_package=dependency_package, + consumer_package=consumer_package, + minimum_fully_supported_version=minimum_fully_supported_version, + recommendation=recommendation, + version_used=dependency_version.version, + version_used_string=dependency_version.version_string, + ), + FutureWarning, + ) + + +def check_dependency_versions( + consumer_import_package: str, *package_dependency_warnings: DependencyConstraint +): + """Bundle checks for all package dependencies. + + This function can be called by all consumers of google.api_core, + to emit needed deprecation warnings for any of their + dependencies. The dependencies to check can be passed as arguments, or if + none are provided, it will default to the list in + `_PACKAGE_DEPENDENCY_WARNINGS`. + + Args: + consumer_import_package: The distribution name of the calling package, whose + dependencies we're checking. + *package_dependency_warnings: A variable number of DependencyConstraint + objects, each specifying a dependency to check. + """ + if not package_dependency_warnings: + package_dependency_warnings = tuple(_PACKAGE_DEPENDENCY_WARNINGS) + for package_info in package_dependency_warnings: + warn_deprecation_for_versions_less_than( + consumer_import_package, + package_info.package_name, + package_info.minimum_fully_supported_version, + recommended_version=package_info.recommended_version, + ) diff --git a/google/api_core/_python_version_support.py b/google/api_core/_python_version_support.py new file mode 100644 index 000000000..9fb92af6f --- /dev/null +++ b/google/api_core/_python_version_support.py @@ -0,0 +1,269 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Code to check Python versions supported by Google Cloud Client Libraries.""" + +import datetime +import enum +import warnings +import sys +import textwrap +from typing import Any, List, NamedTuple, Optional, Dict, Tuple + + +class PythonVersionStatus(enum.Enum): + """Support status of a Python version in this client library artifact release. + + "Support", in this context, means that this release of a client library + artifact is configured to run on the currently configured version of + Python. + """ + + PYTHON_VERSION_STATUS_UNSPECIFIED = "PYTHON_VERSION_STATUS_UNSPECIFIED" + + PYTHON_VERSION_SUPPORTED = "PYTHON_VERSION_SUPPORTED" + """This Python version is fully supported, so the artifact running on this + version will have all features and bug fixes.""" + + PYTHON_VERSION_DEPRECATED = "PYTHON_VERSION_DEPRECATED" + """This Python version is still supported, but support will end within a + year. At that time, there will be no more releases for this artifact + running under this Python version.""" + + PYTHON_VERSION_EOL = "PYTHON_VERSION_EOL" + """This Python version has reached its end of life in the Python community + (see https://devguide.python.org/versions/), and this artifact will cease + supporting this Python version within the next few releases.""" + + PYTHON_VERSION_UNSUPPORTED = "PYTHON_VERSION_UNSUPPORTED" + """This release of the client library artifact may not be the latest, since + current releases no longer support this Python version.""" + + +class VersionInfo(NamedTuple): + """Hold release and support date information for a Python version.""" + + version: str + python_beta: Optional[datetime.date] + python_start: datetime.date + python_eol: datetime.date + gapic_start: Optional[datetime.date] = None # unused + gapic_deprecation: Optional[datetime.date] = None + gapic_end: Optional[datetime.date] = None + dep_unpatchable_cve: Optional[datetime.date] = None # unused + + +PYTHON_VERSIONS: List[VersionInfo] = [ + # Refer to https://devguide.python.org/versions/ and the PEPs linked therefrom. + VersionInfo( + version="3.7", + python_beta=None, + python_start=datetime.date(2018, 6, 27), + python_eol=datetime.date(2023, 6, 27), + ), + VersionInfo( + version="3.8", + python_beta=None, + python_start=datetime.date(2019, 10, 14), + python_eol=datetime.date(2024, 10, 7), + ), + VersionInfo( + version="3.9", + python_beta=datetime.date(2020, 5, 18), + python_start=datetime.date(2020, 10, 5), + python_eol=datetime.date(2025, 10, 5), + gapic_end=datetime.date(2025, 10, 5) + datetime.timedelta(days=90), + ), + VersionInfo( + version="3.10", + python_beta=datetime.date(2021, 5, 3), + python_start=datetime.date(2021, 10, 4), + python_eol=datetime.date(2026, 10, 4), # TODO: specify day when announced + ), + VersionInfo( + version="3.11", + python_beta=datetime.date(2022, 5, 8), + python_start=datetime.date(2022, 10, 24), + python_eol=datetime.date(2027, 10, 24), # TODO: specify day when announced + ), + VersionInfo( + version="3.12", + python_beta=datetime.date(2023, 5, 22), + python_start=datetime.date(2023, 10, 2), + python_eol=datetime.date(2028, 10, 2), # TODO: specify day when announced + ), + VersionInfo( + version="3.13", + python_beta=datetime.date(2024, 5, 8), + python_start=datetime.date(2024, 10, 7), + python_eol=datetime.date(2029, 10, 7), # TODO: specify day when announced + ), + VersionInfo( + version="3.14", + python_beta=datetime.date(2025, 5, 7), + python_start=datetime.date(2025, 10, 7), + python_eol=datetime.date(2030, 10, 7), # TODO: specify day when announced + ), +] + +PYTHON_VERSION_INFO: Dict[Tuple[int, int], VersionInfo] = {} +for info in PYTHON_VERSIONS: + major, minor = map(int, info.version.split(".")) + PYTHON_VERSION_INFO[(major, minor)] = info + + +LOWEST_TRACKED_VERSION = min(PYTHON_VERSION_INFO.keys()) +_FAKE_PAST_DATE = datetime.date.min + datetime.timedelta(days=900) +_FAKE_PAST_VERSION = VersionInfo( + version="0.0", + python_beta=_FAKE_PAST_DATE, + python_start=_FAKE_PAST_DATE, + python_eol=_FAKE_PAST_DATE, +) +_FAKE_FUTURE_DATE = datetime.date.max - datetime.timedelta(days=900) +_FAKE_FUTURE_VERSION = VersionInfo( + version="999.0", + python_beta=_FAKE_FUTURE_DATE, + python_start=_FAKE_FUTURE_DATE, + python_eol=_FAKE_FUTURE_DATE, +) +DEPRECATION_WARNING_PERIOD = datetime.timedelta(days=365) +EOL_GRACE_PERIOD = datetime.timedelta(weeks=1) + + +def _flatten_message(text: str) -> str: + """Dedent a multi-line string and flatten it into a single line.""" + return " ".join(textwrap.dedent(text).strip().split()) + + +# TODO(https://github.com/googleapis/python-api-core/issues/835): Remove once we +# no longer support Python 3.7 +if sys.version_info < (3, 8): + + def _get_pypi_package_name(module_name): # pragma: NO COVER + """Determine the PyPI package name for a given module name.""" + return None + +else: + from importlib import metadata + + def _get_pypi_package_name(module_name): + """Determine the PyPI package name for a given module name.""" + try: + # Get the mapping of modules to distributions + module_to_distributions = metadata.packages_distributions() + + # Check if the module is found in the mapping + if module_name in module_to_distributions: # pragma: NO COVER + # The value is a list of distribution names, take the first one + return module_to_distributions[module_name][0] + else: + return None # Module not found in the mapping + except Exception as e: + print(f"An error occurred: {e}") + return None + + +def _get_distribution_and_import_packages(import_package: str) -> Tuple[str, Any]: + """Return a pretty string with distribution & import package names.""" + distribution_package = _get_pypi_package_name(import_package) + dependency_distribution_and_import_packages = ( + f"package {distribution_package} ({import_package})" + if distribution_package + else import_package + ) + return dependency_distribution_and_import_packages, distribution_package + + +def check_python_version( + package: str = "this package", today: Optional[datetime.date] = None +) -> PythonVersionStatus: + """Check the running Python version and issue a support warning if needed. + + Args: + today: The date to check against. Defaults to the current date. + + Returns: + The support status of the current Python version. + """ + today = today or datetime.date.today() + package_label, _ = _get_distribution_and_import_packages(package) + + python_version = sys.version_info + version_tuple = (python_version.major, python_version.minor) + py_version_str = sys.version.split()[0] + + version_info = PYTHON_VERSION_INFO.get(version_tuple) + + if not version_info: + if version_tuple < LOWEST_TRACKED_VERSION: + version_info = _FAKE_PAST_VERSION + else: + version_info = _FAKE_FUTURE_VERSION + + gapic_deprecation = version_info.gapic_deprecation or ( + version_info.python_eol - DEPRECATION_WARNING_PERIOD + ) + gapic_end = version_info.gapic_end or (version_info.python_eol + EOL_GRACE_PERIOD) + + def min_python(date: datetime.date) -> str: + """Find the minimum supported Python version for a given date.""" + for version, info in sorted(PYTHON_VERSION_INFO.items()): + if info.python_start <= date < info.python_eol: + return f"{version[0]}.{version[1]}" + return "at a currently supported version [https://devguide.python.org/versions]" + + if gapic_end < today: + message = _flatten_message( + f""" + You are using a non-supported Python version ({py_version_str}). + Google will not post any further updates to {package_label} + supporting this Python version. Please upgrade to the latest Python + version, or at least Python {min_python(today)}, and then update + {package_label}. + """ + ) + warnings.warn(message, FutureWarning) + return PythonVersionStatus.PYTHON_VERSION_UNSUPPORTED + + eol_date = version_info.python_eol + EOL_GRACE_PERIOD + if eol_date <= today <= gapic_end: + message = _flatten_message( + f""" + You are using a Python version ({py_version_str}) + past its end of life. Google will update {package_label} + with critical bug fixes on a best-effort basis, but not + with any other fixes or features. Please upgrade + to the latest Python version, or at least Python + {min_python(today)}, and then update {package_label}. + """ + ) + warnings.warn(message, FutureWarning) + return PythonVersionStatus.PYTHON_VERSION_EOL + + if gapic_deprecation <= today <= gapic_end: + message = _flatten_message( + f""" + You are using a Python version ({py_version_str}) which Google will + stop supporting in new releases of {package_label} once it reaches + its end of life ({version_info.python_eol}). Please upgrade to the + latest Python version, or at least Python + {min_python(version_info.python_eol)}, to continue receiving updates + for {package_label} past that date. + """ + ) + warnings.warn(message, FutureWarning) + return PythonVersionStatus.PYTHON_VERSION_DEPRECATED + + return PythonVersionStatus.PYTHON_VERSION_SUPPORTED diff --git a/tests/unit/gapic/test_method.py b/tests/unit/gapic/test_method.py index c27de64ea..927902bf0 100644 --- a/tests/unit/gapic/test_method.py +++ b/tests/unit/gapic/test_method.py @@ -186,7 +186,9 @@ def test_wrap_method_with_overriding_retry_timeout_compression(unused_sleep): assert result == 42 assert method.call_count == 2 method.assert_called_with( - timeout=22, compression=grpc.Compression.Deflate, metadata=mock.ANY + timeout=22, + compression=grpc.Compression.Deflate, + metadata=mock.ANY, ) diff --git a/tests/unit/test_bidi.py b/tests/unit/test_bidi.py index 7640367ce..b51db249a 100644 --- a/tests/unit/test_bidi.py +++ b/tests/unit/test_bidi.py @@ -828,7 +828,13 @@ def test_rpc_callback_fires_when_consumer_start_fails(self): bidi_rpc._start_rpc.side_effect = expected_exception consumer = bidi.BackgroundConsumer(bidi_rpc, on_response=None) + consumer.start() + + # Wait for the consumer's thread to exit. + while consumer.is_active: + pass # pragma: NO COVER + assert callback.call_args.args[0] == grpc.StatusCode.INVALID_ARGUMENT def test_consumer_expected_error(self, caplog): diff --git a/tests/unit/test_python_package_support.py b/tests/unit/test_python_package_support.py new file mode 100644 index 000000000..569903658 --- /dev/null +++ b/tests/unit/test_python_package_support.py @@ -0,0 +1,147 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys +import warnings +from unittest.mock import patch, MagicMock + +import pytest +from packaging.version import parse as parse_version + +from google.api_core._python_package_support import ( + get_dependency_version, + warn_deprecation_for_versions_less_than, + check_dependency_versions, + DependencyConstraint, + DependencyVersion, +) + + +# TODO(https://github.com/googleapis/python-api-core/issues/835): Remove +# this mark once we drop support for Python 3.7 +@pytest.mark.skipif(sys.version_info < (3, 8), reason="requires python3.8 or higher") +@patch("importlib.metadata.version") +def test_get_dependency_version_py38_plus(mock_version): + """Test get_dependency_version on Python 3.8+.""" + mock_version.return_value = "1.2.3" + expected = DependencyVersion(parse_version("1.2.3"), "1.2.3") + assert get_dependency_version("some-package") == expected + mock_version.assert_called_once_with("some-package") + + # Test package not found + mock_version.side_effect = ImportError + assert get_dependency_version("not-a-package") == DependencyVersion(None, "--") + + +# TODO(https://github.com/googleapis/python-api-core/issues/835): Remove +# this test function once we drop support for Python 3.7 +@pytest.mark.skipif(sys.version_info >= (3, 8), reason="requires python3.7") +@patch("pkg_resources.get_distribution") +def test_get_dependency_version_py37(mock_get_distribution): + """Test get_dependency_version on Python 3.7.""" + mock_dist = MagicMock() + mock_dist.version = "4.5.6" + mock_get_distribution.return_value = mock_dist + expected = DependencyVersion(parse_version("4.5.6"), "4.5.6") + assert get_dependency_version("another-package") == expected + mock_get_distribution.assert_called_once_with("another-package") + + # Test package not found + mock_get_distribution.side_effect = ( + Exception # pkg_resources has its own exception types + ) + assert get_dependency_version("not-a-package") == DependencyVersion(None, "--") + + +@patch("google.api_core._python_package_support._get_distribution_and_import_packages") +@patch("google.api_core._python_package_support.get_dependency_version") +def test_warn_deprecation_for_versions_less_than(mock_get_version, mock_get_packages): + """Test the deprecation warning logic.""" + # Mock the helper function to return predictable package strings + mock_get_packages.side_effect = [ + ("dep-package (dep.package)", "dep-package"), + ("my-package (my.package)", "my-package"), + ] + + mock_get_version.return_value = DependencyVersion(parse_version("1.0.0"), "1.0.0") + with pytest.warns(FutureWarning) as record: + warn_deprecation_for_versions_less_than("my.package", "dep.package", "2.0.0") + assert len(record) == 1 + assert ( + "DEPRECATION: Package my-package (my.package) depends on dep-package (dep.package)" + in str(record[0].message) + ) + + # Cases where no warning should be issued + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") # Capture all warnings + + # Case 2: Installed version is equal to required, should not warn. + mock_get_packages.reset_mock() + mock_get_version.return_value = DependencyVersion( + parse_version("2.0.0"), "2.0.0" + ) + warn_deprecation_for_versions_less_than("my.package", "dep.package", "2.0.0") + + # Case 3: Installed version is greater than required, should not warn. + mock_get_packages.reset_mock() + mock_get_version.return_value = DependencyVersion( + parse_version("3.0.0"), "3.0.0" + ) + warn_deprecation_for_versions_less_than("my.package", "dep.package", "2.0.0") + + # Case 4: Dependency not found, should not warn. + mock_get_packages.reset_mock() + mock_get_version.return_value = DependencyVersion(None, "--") + warn_deprecation_for_versions_less_than("my.package", "dep.package", "2.0.0") + + # Assert that no warnings were recorded + assert len(w) == 0 + + # Case 5: Custom message template. + mock_get_packages.reset_mock() + mock_get_packages.side_effect = [ + ("dep-package (dep.package)", "dep-package"), + ("my-package (my.package)", "my-package"), + ] + mock_get_version.return_value = DependencyVersion(parse_version("1.0.0"), "1.0.0") + template = "Custom warning for {dependency_package} used by {consumer_package}." + with pytest.warns(FutureWarning) as record: + warn_deprecation_for_versions_less_than( + "my.package", "dep.package", "2.0.0", message_template=template + ) + assert len(record) == 1 + assert ( + "Custom warning for dep-package (dep.package) used by my-package (my.package)." + in str(record[0].message) + ) + + +@patch( + "google.api_core._python_package_support.warn_deprecation_for_versions_less_than" +) +def test_check_dependency_versions_with_custom_warnings(mock_warn): + """Test check_dependency_versions with custom warning parameters.""" + custom_warning1 = DependencyConstraint("pkg1", "1.0.0", "2.0.0") + custom_warning2 = DependencyConstraint("pkg2", "2.0.0", "3.0.0") + + check_dependency_versions("my-consumer", custom_warning1, custom_warning2) + + assert mock_warn.call_count == 2 + mock_warn.assert_any_call( + "my-consumer", "pkg1", "1.0.0", recommended_version="2.0.0" + ) + mock_warn.assert_any_call( + "my-consumer", "pkg2", "2.0.0", recommended_version="3.0.0" + ) diff --git a/tests/unit/test_python_version_support.py b/tests/unit/test_python_version_support.py new file mode 100644 index 000000000..c38160b78 --- /dev/null +++ b/tests/unit/test_python_version_support.py @@ -0,0 +1,253 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest +import datetime +import textwrap +import warnings +from collections import namedtuple + +from unittest.mock import patch + +# Code to be tested +from google.api_core._python_version_support import ( + _flatten_message, + check_python_version, + PythonVersionStatus, + PYTHON_VERSION_INFO, +) + +# Helper object for mocking sys.version_info +VersionInfoMock = namedtuple("VersionInfoMock", ["major", "minor"]) + + +def test_flatten_message(): + """Test that _flatten_message correctly dedents and flattens a string.""" + input_text = """ + This is a multi-line + string with some + indentation. + """ + expected_output = "This is a multi-line string with some indentation." + assert _flatten_message(input_text) == expected_output + + +def _create_failure_message( + expected, result, py_version, date, gapic_dep, py_eol, eol_warn, gapic_end +): + """Create a detailed failure message for a test.""" + return textwrap.dedent( # pragma: NO COVER + f""" + --- Test Failed --- + Expected status: {expected.name} + Received status: {result.name} + --------------------- + Context: + - Mocked Python Version: {py_version} + - Mocked Today's Date: {date} + Calculated Dates: + - gapic_deprecation: {gapic_dep} + - python_eol: {py_eol} + - eol_warning_starts: {eol_warn} + - gapic_end: {gapic_end} + """ + ) + + +def generate_tracked_version_test_cases(): + """ + Yields test parameters for all tracked versions and boundary conditions. + """ + for version_tuple, version_info in PYTHON_VERSION_INFO.items(): + py_version_str = f"{version_tuple[0]}.{version_tuple[1]}" + gapic_dep = version_info.gapic_deprecation or ( + version_info.python_eol - datetime.timedelta(days=365) + ) + gapic_end = version_info.gapic_end or ( + version_info.python_eol + datetime.timedelta(weeks=1) + ) + eol_warning_starts = version_info.python_eol + datetime.timedelta(weeks=1) + + test_cases = { + "supported_before_deprecation_date": { + "date": gapic_dep - datetime.timedelta(days=1), + "expected": PythonVersionStatus.PYTHON_VERSION_SUPPORTED, + }, + "deprecated_on_deprecation_date": { + "date": gapic_dep, + "expected": PythonVersionStatus.PYTHON_VERSION_DEPRECATED, + }, + "deprecated_on_eol_date": { + "date": version_info.python_eol, + "expected": PythonVersionStatus.PYTHON_VERSION_DEPRECATED, + }, + "deprecated_before_eol_warning_starts": { + "date": eol_warning_starts - datetime.timedelta(days=1), + "expected": PythonVersionStatus.PYTHON_VERSION_DEPRECATED, + }, + "eol_on_eol_warning_date": { + "date": eol_warning_starts, + "expected": PythonVersionStatus.PYTHON_VERSION_EOL, + }, + "eol_on_gapic_end_date": { + "date": gapic_end, + "expected": PythonVersionStatus.PYTHON_VERSION_EOL, + }, + "unsupported_after_gapic_end_date": { + "date": gapic_end + datetime.timedelta(days=1), + "expected": PythonVersionStatus.PYTHON_VERSION_UNSUPPORTED, + }, + } + + for name, params in test_cases.items(): + yield pytest.param( + version_tuple, + params["date"], + params["expected"], + gapic_dep, + gapic_end, + eol_warning_starts, + id=f"{py_version_str}-{name}", + ) + + +@pytest.mark.parametrize( + "version_tuple, mock_date, expected_status, gapic_dep, gapic_end, eol_warning_starts", + generate_tracked_version_test_cases(), +) +def test_all_tracked_versions_and_date_scenarios( + version_tuple, mock_date, expected_status, gapic_dep, gapic_end, eol_warning_starts +): + """Test all outcomes for each tracked version using parametrization.""" + mock_py_v = VersionInfoMock(major=version_tuple[0], minor=version_tuple[1]) + + with patch("google.api_core._python_version_support.sys.version_info", mock_py_v): + # Supported versions should not issue warnings + if expected_status == PythonVersionStatus.PYTHON_VERSION_SUPPORTED: + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + result = check_python_version(today=mock_date) + assert len(w) == 0 + # All other statuses should issue a warning + else: + with pytest.warns(FutureWarning) as record: + result = check_python_version(today=mock_date) + assert len(record) == 1 + + if result != expected_status: # pragma: NO COVER + py_version_str = f"{version_tuple[0]}.{version_tuple[1]}" + version_info = PYTHON_VERSION_INFO[version_tuple] + + fail_msg = _create_failure_message( + expected_status, + result, + py_version_str, + mock_date, + gapic_dep, + version_info.python_eol, + eol_warning_starts, + gapic_end, + ) + pytest.fail(fail_msg, pytrace=False) + + +def test_override_gapic_end_only(): + """Test behavior when only gapic_end is manually overridden.""" + version_tuple = (3, 9) + original_info = PYTHON_VERSION_INFO[version_tuple] + mock_py_version = VersionInfoMock(major=version_tuple[0], minor=version_tuple[1]) + + custom_gapic_end = original_info.python_eol + datetime.timedelta(days=212) + overridden_info = original_info._replace(gapic_end=custom_gapic_end) + + with patch( + "google.api_core._python_version_support.sys.version_info", mock_py_version + ): + with patch.dict( + "google.api_core._python_version_support.PYTHON_VERSION_INFO", + {version_tuple: overridden_info}, + ): + result_before_boundary = check_python_version( + today=custom_gapic_end + datetime.timedelta(days=-1) + ) + assert result_before_boundary == PythonVersionStatus.PYTHON_VERSION_EOL + + result_at_boundary = check_python_version(today=custom_gapic_end) + assert result_at_boundary == PythonVersionStatus.PYTHON_VERSION_EOL + + result_after_boundary = check_python_version( + today=custom_gapic_end + datetime.timedelta(days=1) + ) + assert ( + result_after_boundary == PythonVersionStatus.PYTHON_VERSION_UNSUPPORTED + ) + + +def test_override_gapic_deprecation_only(): + """Test behavior when only gapic_deprecation is manually overridden.""" + version_tuple = (3, 9) + original_info = PYTHON_VERSION_INFO[version_tuple] + mock_py_version = VersionInfoMock(major=version_tuple[0], minor=version_tuple[1]) + + custom_gapic_dep = original_info.python_eol - datetime.timedelta(days=120) + overridden_info = original_info._replace(gapic_deprecation=custom_gapic_dep) + + with patch( + "google.api_core._python_version_support.sys.version_info", mock_py_version + ): + with patch.dict( + "google.api_core._python_version_support.PYTHON_VERSION_INFO", + {version_tuple: overridden_info}, + ): + result_before_boundary = check_python_version( + today=custom_gapic_dep - datetime.timedelta(days=1) + ) + assert ( + result_before_boundary == PythonVersionStatus.PYTHON_VERSION_SUPPORTED + ) + + result_at_boundary = check_python_version(today=custom_gapic_dep) + assert result_at_boundary == PythonVersionStatus.PYTHON_VERSION_DEPRECATED + + +def test_untracked_older_version_is_unsupported(): + """Test that an old, untracked version is unsupported and logs.""" + mock_py_version = VersionInfoMock(major=3, minor=6) + + with patch( + "google.api_core._python_version_support.sys.version_info", mock_py_version + ): + with pytest.warns(FutureWarning) as record: + mock_date = datetime.date(2025, 1, 15) + result = check_python_version(today=mock_date) + + assert result == PythonVersionStatus.PYTHON_VERSION_UNSUPPORTED + assert len(record) == 1 + assert "non-supported" in str(record[0].message) + + +def test_untracked_newer_version_is_supported(): + """Test that a new, untracked version is supported and does not log.""" + mock_py_version = VersionInfoMock(major=40, minor=0) + + with patch( + "google.api_core._python_version_support.sys.version_info", mock_py_version + ): + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + mock_date = datetime.date(2025, 1, 15) + result = check_python_version(today=mock_date) + + assert result == PythonVersionStatus.PYTHON_VERSION_SUPPORTED + assert len(w) == 0 diff --git a/tests/unit/test_timeout.py b/tests/unit/test_timeout.py index 2c20202bd..8ce143f35 100644 --- a/tests/unit/test_timeout.py +++ b/tests/unit/test_timeout.py @@ -14,6 +14,7 @@ import datetime import itertools +import pytest from unittest import mock from google.api_core import timeout as timeouts @@ -121,7 +122,15 @@ def test_apply_passthrough(self): wrapped(1, 2, meep="moop") - target.assert_called_once_with(1, 2, meep="moop", timeout=42.0) + actual_arg_0 = target.call_args[0][0] + actual_arg_1 = target.call_args[0][1] + actual_arg_meep = target.call_args[1]["meep"] + actual_arg_timeuut = target.call_args[1]["timeout"] + + assert actual_arg_0 == 1 + assert actual_arg_1 == 2 + assert actual_arg_meep == "moop" + assert actual_arg_timeuut == pytest.approx(42.0, abs=0.01) class TestConstantTimeout(object): From 6206ce08e019fff0b8599d51c45b9abd2c7a754c Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 15:35:37 -0700 Subject: [PATCH 087/101] chore(main): release 2.28.0 (#847) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ google/api_core/version.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69e124dc6..08fdc19f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://pypi.org/project/google-api-core/#history +## [2.28.0](https://github.com/googleapis/python-api-core/compare/v2.27.0...v2.28.0) (2025-10-24) + + +### Features + +* Provide and use Python version support check ([#832](https://github.com/googleapis/python-api-core/issues/832)) ([d36e896](https://github.com/googleapis/python-api-core/commit/d36e896f98a2371c4d58ce1a7a3bc1a77a081836)) + ## [2.27.0](https://github.com/googleapis/python-api-core/compare/v2.26.0...v2.27.0) (2025-10-22) diff --git a/google/api_core/version.py b/google/api_core/version.py index 4f038c462..10e7fb4f5 100644 --- a/google/api_core/version.py +++ b/google/api_core/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "2.27.0" +__version__ = "2.28.0" From ca59a863b08a79c2bf0607f9085de1417422820b Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Tue, 28 Oct 2025 16:57:38 -0400 Subject: [PATCH 088/101] fix: remove dependency on packaging and pkg_resources (#852) * fix: remove dependency on packaging and pkg_resources * add test case * lint * update docstring * add constraint for importlib_metadata * address feedback * address feedback * remove comment * address feedback * address feedback --- google/api_core/_python_package_support.py | 65 +++++++++++++++------- noxfile.py | 1 + pyproject.toml | 3 + testing/constraints-3.7.txt | 1 + tests/unit/test_python_package_support.py | 63 ++++++++++----------- 5 files changed, 78 insertions(+), 55 deletions(-) diff --git a/google/api_core/_python_package_support.py b/google/api_core/_python_package_support.py index cc805b8d7..06da2bb00 100644 --- a/google/api_core/_python_package_support.py +++ b/google/api_core/_python_package_support.py @@ -16,7 +16,7 @@ import warnings import sys -from typing import Optional +from typing import Optional, Tuple from collections import namedtuple @@ -25,7 +25,14 @@ _get_distribution_and_import_packages, ) -from packaging.version import parse as parse_version +if sys.version_info >= (3, 8): + from importlib import metadata +else: + # TODO(https://github.com/googleapis/python-api-core/issues/835): Remove + # this code path once we drop support for Python 3.7 + import importlib_metadata as metadata + +ParsedVersion = Tuple[int, ...] # Here we list all the packages for which we want to issue warnings # about deprecated and unsupported versions. @@ -48,42 +55,56 @@ UNKNOWN_VERSION_STRING = "--" +def parse_version_to_tuple(version_string: str) -> ParsedVersion: + """Safely converts a semantic version string to a comparable tuple of integers. + + Example: "4.25.8" -> (4, 25, 8) + Ignores non-numeric parts and handles common version formats. + + Args: + version_string: Version string in the format "x.y.z" or "x.y.z" + + Returns: + Tuple of integers for the parsed version string. + """ + parts = [] + for part in version_string.split("."): + try: + parts.append(int(part)) + except ValueError: + # If it's a non-numeric part (e.g., '1.0.0b1' -> 'b1'), stop here. + # This is a simplification compared to 'packaging.parse_version', but sufficient + # for comparing strictly numeric semantic versions. + break + return tuple(parts) + + def get_dependency_version( dependency_name: str, ) -> DependencyVersion: """Get the parsed version of an installed package dependency. This function checks for an installed package and returns its version - as a `packaging.version.Version` object for safe comparison. It handles + as a comparable tuple of integers object for safe comparison. It handles both modern (Python 3.8+) and legacy (Python 3.7) environments. Args: dependency_name: The distribution name of the package (e.g., 'requests'). Returns: - A DependencyVersion namedtuple with `version` and + A DependencyVersion namedtuple with `version` (a tuple of integers) and `version_string` attributes, or `DependencyVersion(None, UNKNOWN_VERSION_STRING)` if the package is not found or another error occurs during version discovery. """ try: - if sys.version_info >= (3, 8): - from importlib import metadata - - version_string = metadata.version(dependency_name) - return DependencyVersion(parse_version(version_string), version_string) - - # TODO(https://github.com/googleapis/python-api-core/issues/835): Remove - # this code path once we drop support for Python 3.7 - else: # pragma: NO COVER - # Use pkg_resources, which is part of setuptools. - import pkg_resources - - version_string = pkg_resources.get_distribution(dependency_name).version - return DependencyVersion(parse_version(version_string), version_string) - + version_string: str = metadata.version(dependency_name) + parsed_version = parse_version_to_tuple(version_string) + return DependencyVersion(parsed_version, version_string) except Exception: + # Catch exceptions from metadata.version() (e.g., PackageNotFoundError) + # or errors during parse_version_to_tuple return DependencyVersion(None, UNKNOWN_VERSION_STRING) @@ -132,10 +153,14 @@ def warn_deprecation_for_versions_less_than( or not minimum_fully_supported_version ): # pragma: NO COVER return + dependency_version = get_dependency_version(dependency_import_package) if not dependency_version.version: return - if dependency_version.version < parse_version(minimum_fully_supported_version): + + if dependency_version.version < parse_version_to_tuple( + minimum_fully_supported_version + ): ( dependency_package, dependency_distribution_package, diff --git a/noxfile.py b/noxfile.py index ac21330ef..04347a4f3 100644 --- a/noxfile.py +++ b/noxfile.py @@ -127,6 +127,7 @@ def default(session, install_grpc=True, prerelease=False, install_async_rest=Fal "mock; python_version=='3.7'", "pytest", "pytest-cov", + "pytest-mock", "pytest-xdist", ) diff --git a/pyproject.toml b/pyproject.toml index 71ce72245..0132afe05 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,6 +51,9 @@ dependencies = [ "proto-plus >= 1.25.0, < 2.0.0; python_version >= '3.13'", "google-auth >= 2.14.1, < 3.0.0", "requests >= 2.18.0, < 3.0.0", + # TODO(https://github.com/googleapis/python-api-core/issues/835): Remove + # `importlib_metadata` once we drop support for Python 3.7 + "importlib_metadata>=1.4; python_version<'3.8'", ] dynamic = ["version"] diff --git a/testing/constraints-3.7.txt b/testing/constraints-3.7.txt index 4ce1c8999..1a9b85d12 100644 --- a/testing/constraints-3.7.txt +++ b/testing/constraints-3.7.txt @@ -13,3 +13,4 @@ grpcio==1.33.2 grpcio-status==1.33.2 grpcio-gcp==0.2.2 proto-plus==1.22.3 +importlib_metadata==1.4 diff --git a/tests/unit/test_python_package_support.py b/tests/unit/test_python_package_support.py index 569903658..6a93e7154 100644 --- a/tests/unit/test_python_package_support.py +++ b/tests/unit/test_python_package_support.py @@ -14,12 +14,12 @@ import sys import warnings -from unittest.mock import patch, MagicMock +from unittest.mock import patch import pytest -from packaging.version import parse as parse_version from google.api_core._python_package_support import ( + parse_version_to_tuple, get_dependency_version, warn_deprecation_for_versions_less_than, check_dependency_versions, @@ -28,39 +28,28 @@ ) -# TODO(https://github.com/googleapis/python-api-core/issues/835): Remove -# this mark once we drop support for Python 3.7 -@pytest.mark.skipif(sys.version_info < (3, 8), reason="requires python3.8 or higher") -@patch("importlib.metadata.version") -def test_get_dependency_version_py38_plus(mock_version): - """Test get_dependency_version on Python 3.8+.""" - mock_version.return_value = "1.2.3" - expected = DependencyVersion(parse_version("1.2.3"), "1.2.3") +@pytest.mark.parametrize("version_string_to_test", ["1.2.3", "1.2.3b1"]) +def test_get_dependency_version(mocker, version_string_to_test): + """Test get_dependency_version.""" + if sys.version_info >= (3, 8): + mock_importlib = mocker.patch( + "importlib.metadata.version", return_value=version_string_to_test + ) + else: + # TODO(https://github.com/googleapis/python-api-core/issues/835): Remove + # `importlib_metadata` once we drop support for Python 3.7 + mock_importlib = mocker.patch( + "importlib_metadata.version", return_value=version_string_to_test + ) + expected = DependencyVersion( + parse_version_to_tuple(version_string_to_test), version_string_to_test + ) assert get_dependency_version("some-package") == expected - mock_version.assert_called_once_with("some-package") - - # Test package not found - mock_version.side_effect = ImportError - assert get_dependency_version("not-a-package") == DependencyVersion(None, "--") - -# TODO(https://github.com/googleapis/python-api-core/issues/835): Remove -# this test function once we drop support for Python 3.7 -@pytest.mark.skipif(sys.version_info >= (3, 8), reason="requires python3.7") -@patch("pkg_resources.get_distribution") -def test_get_dependency_version_py37(mock_get_distribution): - """Test get_dependency_version on Python 3.7.""" - mock_dist = MagicMock() - mock_dist.version = "4.5.6" - mock_get_distribution.return_value = mock_dist - expected = DependencyVersion(parse_version("4.5.6"), "4.5.6") - assert get_dependency_version("another-package") == expected - mock_get_distribution.assert_called_once_with("another-package") + mock_importlib.assert_called_once_with("some-package") # Test package not found - mock_get_distribution.side_effect = ( - Exception # pkg_resources has its own exception types - ) + mock_importlib.side_effect = ImportError assert get_dependency_version("not-a-package") == DependencyVersion(None, "--") @@ -74,7 +63,9 @@ def test_warn_deprecation_for_versions_less_than(mock_get_version, mock_get_pack ("my-package (my.package)", "my-package"), ] - mock_get_version.return_value = DependencyVersion(parse_version("1.0.0"), "1.0.0") + mock_get_version.return_value = DependencyVersion( + parse_version_to_tuple("1.0.0"), "1.0.0" + ) with pytest.warns(FutureWarning) as record: warn_deprecation_for_versions_less_than("my.package", "dep.package", "2.0.0") assert len(record) == 1 @@ -90,14 +81,14 @@ def test_warn_deprecation_for_versions_less_than(mock_get_version, mock_get_pack # Case 2: Installed version is equal to required, should not warn. mock_get_packages.reset_mock() mock_get_version.return_value = DependencyVersion( - parse_version("2.0.0"), "2.0.0" + parse_version_to_tuple("2.0.0"), "2.0.0" ) warn_deprecation_for_versions_less_than("my.package", "dep.package", "2.0.0") # Case 3: Installed version is greater than required, should not warn. mock_get_packages.reset_mock() mock_get_version.return_value = DependencyVersion( - parse_version("3.0.0"), "3.0.0" + parse_version_to_tuple("3.0.0"), "3.0.0" ) warn_deprecation_for_versions_less_than("my.package", "dep.package", "2.0.0") @@ -115,7 +106,9 @@ def test_warn_deprecation_for_versions_less_than(mock_get_version, mock_get_pack ("dep-package (dep.package)", "dep-package"), ("my-package (my.package)", "my-package"), ] - mock_get_version.return_value = DependencyVersion(parse_version("1.0.0"), "1.0.0") + mock_get_version.return_value = DependencyVersion( + parse_version_to_tuple("1.0.0"), "1.0.0" + ) template = "Custom warning for {dependency_package} used by {consumer_package}." with pytest.warns(FutureWarning) as record: warn_deprecation_for_versions_less_than( From b8b9a29e062fa0344f5f84c363b0e1b7cf7f1ffd Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 14:28:32 -0700 Subject: [PATCH 089/101] chore(main): release 2.28.1 (#854) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ google/api_core/version.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08fdc19f1..7b2a256bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://pypi.org/project/google-api-core/#history +## [2.28.1](https://github.com/googleapis/python-api-core/compare/v2.28.0...v2.28.1) (2025-10-28) + + +### Bug Fixes + +* Remove dependency on packaging and pkg_resources ([#852](https://github.com/googleapis/python-api-core/issues/852)) ([ca59a86](https://github.com/googleapis/python-api-core/commit/ca59a863b08a79c2bf0607f9085de1417422820b)) + ## [2.28.0](https://github.com/googleapis/python-api-core/compare/v2.27.0...v2.28.0) (2025-10-24) diff --git a/google/api_core/version.py b/google/api_core/version.py index 10e7fb4f5..967959b05 100644 --- a/google/api_core/version.py +++ b/google/api_core/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "2.28.0" +__version__ = "2.28.1" From a4b291f962c4679b7f8fc62edede688806ab14e4 Mon Sep 17 00:00:00 2001 From: ohmayr Date: Wed, 29 Oct 2025 14:23:53 -0700 Subject: [PATCH 090/101] chore(librarian): onboard to librarian (#856) * chore: onboard to librarian * chore: cleanup --- .github/.OwlBot.lock.yaml | 17 ----------------- .github/.OwlBot.yaml | 19 ------------------- .github/release-please.yml | 11 ----------- .github/release-trigger.yml | 2 -- .librarian/config.yaml | 6 ++++++ .librarian/state.yaml | 10 ++++++++++ 6 files changed, 16 insertions(+), 49 deletions(-) delete mode 100644 .github/.OwlBot.lock.yaml delete mode 100644 .github/.OwlBot.yaml delete mode 100644 .github/release-please.yml delete mode 100644 .github/release-trigger.yml create mode 100644 .librarian/config.yaml create mode 100644 .librarian/state.yaml diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml deleted file mode 100644 index 4a311db02..000000000 --- a/.github/.OwlBot.lock.yaml +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright 2025 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -docker: - image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest - digest: sha256:543e209e7c1c1ffe720eb4db1a3f045a75099304fb19aa11a47dc717b8aae2a9 -# created: 2025-10-09T14:48:42.914384887Z diff --git a/.github/.OwlBot.yaml b/.github/.OwlBot.yaml deleted file mode 100644 index c8b40cc7f..000000000 --- a/.github/.OwlBot.yaml +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright 2021 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -docker: - image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest - -begin-after-commit-hash: 7af2cb8b2b725641ac0d07e2f256d453682802e6 - diff --git a/.github/release-please.yml b/.github/release-please.yml deleted file mode 100644 index 29601ad46..000000000 --- a/.github/release-please.yml +++ /dev/null @@ -1,11 +0,0 @@ -releaseType: python -handleGHRelease: true -# NOTE: this section is generated by synthtool.languages.python -# See https://github.com/googleapis/synthtool/blob/master/synthtool/languages/python.py -branches: -- branch: v1 - handleGHRelease: true - releaseType: python -- branch: v0 - handleGHRelease: true - releaseType: python diff --git a/.github/release-trigger.yml b/.github/release-trigger.yml deleted file mode 100644 index 50e8bd300..000000000 --- a/.github/release-trigger.yml +++ /dev/null @@ -1,2 +0,0 @@ -enabled: true -multiScmName: python-api-core diff --git a/.librarian/config.yaml b/.librarian/config.yaml new file mode 100644 index 000000000..111f94dd5 --- /dev/null +++ b/.librarian/config.yaml @@ -0,0 +1,6 @@ +global_files_allowlist: + # Allow the container to read and write the root `CHANGELOG.md` + # file during the `release` step to update the latest client library + # versions which are hardcoded in the file. + - path: "CHANGELOG.md" + permissions: "read-write" diff --git a/.librarian/state.yaml b/.librarian/state.yaml new file mode 100644 index 000000000..6ac8b0a64 --- /dev/null +++ b/.librarian/state.yaml @@ -0,0 +1,10 @@ +image: us-central1-docker.pkg.dev/cloud-sdk-librarian-prod/images-prod/python-librarian-generator:latest +libraries: + - id: google-api-core + version: 2.28.1 + apis: [] + source_roots: + - . + preserve_regex: [] + remove_regex: [] + tag_format: v{version} From c97b3a004044ebf8b35c2a7ba97409d7795e11b0 Mon Sep 17 00:00:00 2001 From: pujawadare Date: Thu, 30 Oct 2025 23:48:29 +0530 Subject: [PATCH 091/101] fix: closes tailing streams in bidi classes. (#851) Always put `None` into the request queue when closing a bidi stream. This ensures that the request queue is always signaled as closed, even if the underlying gRPC call object is not yet available. --- google/api_core/bidi.py | 6 +++--- google/api_core/bidi_async.py | 6 +++--- tests/asyncio/test_bidi_async.py | 15 +++++++++++++++ tests/unit/test_bidi.py | 11 ++++++++++- 4 files changed, 31 insertions(+), 7 deletions(-) diff --git a/google/api_core/bidi.py b/google/api_core/bidi.py index 270ad0915..7f45c2af1 100644 --- a/google/api_core/bidi.py +++ b/google/api_core/bidi.py @@ -281,11 +281,11 @@ def open(self): def close(self): """Closes the stream.""" - if self.call is None: - return + if self.call is not None: + self.call.cancel() + # Put None in request queue to signal termination. self._request_queue.put(None) - self.call.cancel() self._request_generator = None self._initial_request = None self._callbacks = [] diff --git a/google/api_core/bidi_async.py b/google/api_core/bidi_async.py index d73b4c98d..3770f69dd 100644 --- a/google/api_core/bidi_async.py +++ b/google/api_core/bidi_async.py @@ -197,11 +197,11 @@ async def open(self) -> None: async def close(self) -> None: """Closes the stream.""" - if self.call is None: - return + if self.call is not None: + self.call.cancel() + # Put None in request queue to signal termination. await self._request_queue.put(None) - self.call.cancel() self._request_generator = None self._initial_request = None self._callbacks = [] diff --git a/tests/asyncio/test_bidi_async.py b/tests/asyncio/test_bidi_async.py index 696113dbf..add685a96 100644 --- a/tests/asyncio/test_bidi_async.py +++ b/tests/asyncio/test_bidi_async.py @@ -255,6 +255,21 @@ async def test_close(self): assert bidi_rpc._initial_request is None assert not bidi_rpc._callbacks + @pytest.mark.asyncio + async def test_close_with_no_rpc(self): + bidi_rpc = bidi_async.AsyncBidiRpc(None) + + await bidi_rpc.close() + + assert bidi_rpc.call is None + assert bidi_rpc.is_active is False + # ensure the request queue was signaled to stop. + assert bidi_rpc.pending_requests == 1 + assert await bidi_rpc._request_queue.get() is None + # ensure request and callbacks are cleaned up + assert bidi_rpc._initial_request is None + assert not bidi_rpc._callbacks + @pytest.mark.asyncio async def test_close_no_rpc(self): bidi_rpc = bidi_async.AsyncBidiRpc(None) diff --git a/tests/unit/test_bidi.py b/tests/unit/test_bidi.py index b51db249a..4a8eb74fa 100644 --- a/tests/unit/test_bidi.py +++ b/tests/unit/test_bidi.py @@ -301,10 +301,19 @@ def test_close(self): assert bidi_rpc._initial_request is None assert not bidi_rpc._callbacks - def test_close_no_rpc(self): + def test_close_with_no_rpc(self): bidi_rpc = bidi.BidiRpc(None) bidi_rpc.close() + assert bidi_rpc.call is None + assert bidi_rpc.is_active is False + # ensure the request queue was signaled to stop. + assert bidi_rpc.pending_requests == 1 + assert bidi_rpc._request_queue.get() is None + # ensure request and callbacks are cleaned up + assert bidi_rpc._initial_request is None + assert not bidi_rpc._callbacks + def test_send(self): rpc, call = make_rpc() bidi_rpc = bidi.BidiRpc(rpc) From 6493118cae2720696c3d0097274edfd7fe2bce67 Mon Sep 17 00:00:00 2001 From: Reuben <60552974+ReubenFrankel@users.noreply.github.com> Date: Tue, 4 Nov 2025 21:05:06 +0000 Subject: [PATCH 092/101] fix: Log version check errors (#858) * Use error log over `print` to avoid stdout write * Move common return to end of function * Update google/api_core/_python_version_support.py Co-authored-by: Chalmer Lowe * Fix lint error --------- Co-authored-by: Chalmer Lowe --- google/api_core/_python_version_support.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/google/api_core/_python_version_support.py b/google/api_core/_python_version_support.py index 9fb92af6f..2c56364bb 100644 --- a/google/api_core/_python_version_support.py +++ b/google/api_core/_python_version_support.py @@ -16,12 +16,16 @@ import datetime import enum +import logging import warnings import sys import textwrap from typing import Any, List, NamedTuple, Optional, Dict, Tuple +_LOGGER = logging.getLogger(__name__) + + class PythonVersionStatus(enum.Enum): """Support status of a Python version in this client library artifact release. @@ -168,11 +172,14 @@ def _get_pypi_package_name(module_name): if module_name in module_to_distributions: # pragma: NO COVER # The value is a list of distribution names, take the first one return module_to_distributions[module_name][0] - else: - return None # Module not found in the mapping except Exception as e: - print(f"An error occurred: {e}") - return None + _LOGGER.info( + "An error occurred while determining PyPI package name for %s: %s", + module_name, + e, + ) + + return None def _get_distribution_and_import_packages(import_package: str) -> Tuple[str, Any]: From 628003e217d9a881d24f3316aecfd48c244a73f0 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Thu, 6 Nov 2025 13:30:26 -0500 Subject: [PATCH 093/101] fix: remove call to importlib.metadata.packages_distributions() for py38/py39 (#859) * fix: remove call to importlib.metadata.packages_distributions() for py38/py39 * cover * update comment --- google/api_core/_python_version_support.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/google/api_core/_python_version_support.py b/google/api_core/_python_version_support.py index 2c56364bb..d0c0dfe1b 100644 --- a/google/api_core/_python_version_support.py +++ b/google/api_core/_python_version_support.py @@ -151,9 +151,11 @@ def _flatten_message(text: str) -> str: return " ".join(textwrap.dedent(text).strip().split()) -# TODO(https://github.com/googleapis/python-api-core/issues/835): Remove once we -# no longer support Python 3.7 -if sys.version_info < (3, 8): +# TODO(https://github.com/googleapis/python-api-core/issues/835): +# Remove once we no longer support Python 3.9. +# `importlib.metadata.packages_distributions()` is only supported in Python 3.10 and newer +# https://docs.python.org/3/library/importlib.metadata.html#importlib.metadata.packages_distributions +if sys.version_info < (3, 10): def _get_pypi_package_name(module_name): # pragma: NO COVER """Determine the PyPI package name for a given module name.""" @@ -172,7 +174,7 @@ def _get_pypi_package_name(module_name): if module_name in module_to_distributions: # pragma: NO COVER # The value is a list of distribution names, take the first one return module_to_distributions[module_name][0] - except Exception as e: + except Exception as e: # pragma: NO COVER _LOGGER.info( "An error occurred while determining PyPI package name for %s: %s", module_name, From 4f68f9303f43ca33638dd98e17e0786e729edeea Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Thu, 13 Nov 2025 12:57:29 -0500 Subject: [PATCH 094/101] chore(librarian): clean up owlbot files and pin image sha (#861) Additional clean up following https://github.com/googleapis/python-api-core/pull/856 --------- Co-authored-by: ohmayr Co-authored-by: Victor Chudnovsky --- .github/auto-approve.yml | 3 -- .librarian/config.yaml | 6 --- .librarian/state.yaml | 2 +- owlbot.py | 40 ----------------- pyproject.toml | 2 + tests/asyncio/gapic/test_method_async.py | 2 +- tests/asyncio/test_grpc_helpers_async.py | 33 ++++++++------ tests/helpers.py | 11 +++++ .../test_operations_rest_client.py | 45 ++++++++++++------- tests/unit/test_client_options.py | 36 ++++++++------- tests/unit/test_grpc_helpers.py | 29 +++++++----- tests/unit/test_python_version_support.py | 20 +++++---- 12 files changed, 112 insertions(+), 117 deletions(-) delete mode 100644 .github/auto-approve.yml delete mode 100644 .librarian/config.yaml delete mode 100644 owlbot.py diff --git a/.github/auto-approve.yml b/.github/auto-approve.yml deleted file mode 100644 index 311ebbb85..000000000 --- a/.github/auto-approve.yml +++ /dev/null @@ -1,3 +0,0 @@ -# https://github.com/googleapis/repo-automation-bots/tree/main/packages/auto-approve -processes: - - "OwlBotTemplateChanges" diff --git a/.librarian/config.yaml b/.librarian/config.yaml deleted file mode 100644 index 111f94dd5..000000000 --- a/.librarian/config.yaml +++ /dev/null @@ -1,6 +0,0 @@ -global_files_allowlist: - # Allow the container to read and write the root `CHANGELOG.md` - # file during the `release` step to update the latest client library - # versions which are hardcoded in the file. - - path: "CHANGELOG.md" - permissions: "read-write" diff --git a/.librarian/state.yaml b/.librarian/state.yaml index 6ac8b0a64..1a97264b7 100644 --- a/.librarian/state.yaml +++ b/.librarian/state.yaml @@ -1,4 +1,4 @@ -image: us-central1-docker.pkg.dev/cloud-sdk-librarian-prod/images-prod/python-librarian-generator:latest +image: us-central1-docker.pkg.dev/cloud-sdk-librarian-prod/images-prod/python-librarian-generator@sha256:c8612d3fffb3f6a32353b2d1abd16b61e87811866f7ec9d65b59b02eb452a620 libraries: - id: google-api-core version: 2.28.1 diff --git a/owlbot.py b/owlbot.py deleted file mode 100644 index 58bc75170..000000000 --- a/owlbot.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""This script is used to synthesize generated parts of this library.""" - -import synthtool as s -from synthtool import gcp -from synthtool.languages import python - -common = gcp.CommonTemplates() - -# ---------------------------------------------------------------------------- -# Add templated files -# ---------------------------------------------------------------------------- -excludes = [ - "noxfile.py", # pytype - "setup.cfg", # pytype - ".coveragerc", # layout - "CONTRIBUTING.rst", # no systests - ".github/workflows/unittest.yml", # exclude unittest gh action - ".github/workflows/lint.yml", # exclude lint gh action - "README.rst", -] -templated_files = common.py_library(microgenerator=True, cov_level=100) -s.move(templated_files, excludes=excludes) - -python.configure_previous_major_version_branches() - -s.shell.run(["nox", "-s", "blacken"], hide_output=False) diff --git a/pyproject.toml b/pyproject.toml index 0132afe05..31f82052a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -91,6 +91,8 @@ ignore_missing_imports = true filterwarnings = [ # treat all warnings as errors "error", + # Prevent Python version warnings from interfering with tests + "ignore:.* Python version .*:FutureWarning", # Remove once https://github.com/pytest-dev/pytest-cov/issues/621 is fixed "ignore:.*The --rsyncdir command line argument and rsyncdirs config variable are deprecated:DeprecationWarning", # Remove once https://github.com/protocolbuffers/protobuf/issues/12186 is fixed diff --git a/tests/asyncio/gapic/test_method_async.py b/tests/asyncio/gapic/test_method_async.py index 3edf8b6d4..40dd168a0 100644 --- a/tests/asyncio/gapic/test_method_async.py +++ b/tests/asyncio/gapic/test_method_async.py @@ -260,7 +260,7 @@ async def test_wrap_method_with_overriding_timeout_as_a_number(): actual_timeout = method.call_args[1]["timeout"] metadata = method.call_args[1]["metadata"] assert metadata == mock.ANY - assert actual_timeout == pytest.approx(22, abs=0.01) + assert actual_timeout == pytest.approx(22, abs=0.05) @pytest.mark.asyncio diff --git a/tests/asyncio/test_grpc_helpers_async.py b/tests/asyncio/test_grpc_helpers_async.py index aa8d5d10b..43700f22b 100644 --- a/tests/asyncio/test_grpc_helpers_async.py +++ b/tests/asyncio/test_grpc_helpers_async.py @@ -17,6 +17,7 @@ from unittest.mock import AsyncMock # pragma: NO COVER # noqa: F401 except ImportError: # pragma: NO COVER import mock # type: ignore +from ..helpers import warn_deprecated_credentials_file import pytest # noqa: I202 try: @@ -522,11 +523,12 @@ def test_create_channel_explicit_with_duplicate_credentials(): target = "example:443" with pytest.raises(exceptions.DuplicateCredentialArgs) as excinfo: - grpc_helpers_async.create_channel( - target, - credentials_file="credentials.json", - credentials=mock.sentinel.credentials, - ) + with warn_deprecated_credentials_file(): + grpc_helpers_async.create_channel( + target, + credentials_file="credentials.json", + credentials=mock.sentinel.credentials, + ) assert "mutually exclusive" in str(excinfo.value) @@ -641,9 +643,10 @@ def test_create_channel_with_credentials_file( credentials_file = "/path/to/credentials/file.json" composite_creds = composite_creds_call.return_value - channel = grpc_helpers_async.create_channel( - target, credentials_file=credentials_file - ) + with warn_deprecated_credentials_file(): + channel = grpc_helpers_async.create_channel( + target, credentials_file=credentials_file + ) google.auth.load_credentials_from_file.assert_called_once_with( credentials_file, scopes=None, default_scopes=None @@ -670,9 +673,10 @@ def test_create_channel_with_credentials_file_and_scopes( credentials_file = "/path/to/credentials/file.json" composite_creds = composite_creds_call.return_value - channel = grpc_helpers_async.create_channel( - target, credentials_file=credentials_file, scopes=scopes - ) + with warn_deprecated_credentials_file(): + channel = grpc_helpers_async.create_channel( + target, credentials_file=credentials_file, scopes=scopes + ) google.auth.load_credentials_from_file.assert_called_once_with( credentials_file, scopes=scopes, default_scopes=None @@ -699,9 +703,10 @@ def test_create_channel_with_credentials_file_and_default_scopes( credentials_file = "/path/to/credentials/file.json" composite_creds = composite_creds_call.return_value - channel = grpc_helpers_async.create_channel( - target, credentials_file=credentials_file, default_scopes=default_scopes - ) + with warn_deprecated_credentials_file(): + channel = grpc_helpers_async.create_channel( + target, credentials_file=credentials_file, default_scopes=default_scopes + ) google.auth.load_credentials_from_file.assert_called_once_with( credentials_file, scopes=None, default_scopes=default_scopes diff --git a/tests/helpers.py b/tests/helpers.py index 3429d511e..4c7d5db3e 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -14,7 +14,9 @@ """Helpers for tests""" +import functools import logging +import pytest # noqa: I202 from typing import List import proto @@ -69,3 +71,12 @@ def parse_responses(response_message_cls, all_responses: List[proto.Message]) -> logging.info(f"Sending JSON stream: {json_responses}") ret_val = "[{}]".format(",".join(json_responses)) return bytes(ret_val, "utf-8") + + +warn_deprecated_credentials_file = functools.partial( + # This is used to test that the auth credentials file deprecation + # warning is emitted as expected. + pytest.warns, + DeprecationWarning, + match="argument is deprecated because of a potential security risk", +) diff --git a/tests/unit/operations_v1/test_operations_rest_client.py b/tests/unit/operations_v1/test_operations_rest_client.py index 4e8ef4073..87523c5dd 100644 --- a/tests/unit/operations_v1/test_operations_rest_client.py +++ b/tests/unit/operations_v1/test_operations_rest_client.py @@ -23,6 +23,7 @@ import pytest from typing import Any, List +from ...helpers import warn_deprecated_credentials_file try: import grpc # noqa: F401 @@ -369,7 +370,8 @@ def test_operations_client_client_options( ) # Check the case credentials_file is provided - options = client_options.ClientOptions(credentials_file="credentials.json") + with warn_deprecated_credentials_file(): + options = client_options.ClientOptions(credentials_file="credentials.json") with mock.patch.object(transport_class, "__init__") as patched: patched.return_value = None client = client_class(client_options=options, transport=transport_name) @@ -539,11 +541,14 @@ def test_operations_client_client_options_credentials_file( client_class, transport_class, transport_name ): # Check the case credentials file is provided. - options = client_options.ClientOptions(credentials_file="credentials.json") + with warn_deprecated_credentials_file(): + options = client_options.ClientOptions(credentials_file="credentials.json") if "async" in str(client_class): # TODO(): Add support for credentials file to async REST transport. with pytest.raises(core_exceptions.AsyncRestUnsupportedParameterError): - client_class(client_options=options, transport=transport_name) + with warn_deprecated_credentials_file(): + + client_class(client_options=options, transport=transport_name) else: with mock.patch.object(transport_class, "__init__") as patched: patched.return_value = None @@ -570,10 +575,18 @@ def test_operations_client_client_options_credentials_file( return_value=(mock.sentinel.credentials, mock.sentinel.project), ) def test_list_operations_rest(google_auth_default, credentials_file): - sync_transport = transports.rest.OperationsRestTransport( - credentials_file=credentials_file, - http_options=HTTP_OPTIONS, - ) + if credentials_file: + with warn_deprecated_credentials_file(): + sync_transport = transports.rest.OperationsRestTransport( + credentials_file=credentials_file, + http_options=HTTP_OPTIONS, + ) + else: + # no warning expected + sync_transport = transports.rest.OperationsRestTransport( + credentials_file=credentials_file, + http_options=HTTP_OPTIONS, + ) client = AbstractOperationsClient(transport=sync_transport) @@ -1130,10 +1143,11 @@ def test_transport_adc(client_class, transport_class, credentials): def test_operations_base_transport_error(): # Passing both a credentials object and credentials_file should raise an error with pytest.raises(core_exceptions.DuplicateCredentialArgs): - transports.OperationsTransport( - credentials=ga_credentials.AnonymousCredentials(), - credentials_file="credentials.json", - ) + with warn_deprecated_credentials_file(): + transports.OperationsTransport( + credentials=ga_credentials.AnonymousCredentials(), + credentials_file="credentials.json", + ) def test_operations_base_transport(): @@ -1171,10 +1185,11 @@ def test_operations_base_transport_with_credentials_file(): ) as Transport: Transport.return_value = None load_creds.return_value = (ga_credentials.AnonymousCredentials(), None) - transports.OperationsTransport( - credentials_file="credentials.json", - quota_project_id="octopus", - ) + with warn_deprecated_credentials_file(): + transports.OperationsTransport( + credentials_file="credentials.json", + quota_project_id="octopus", + ) load_creds.assert_called_once_with( "credentials.json", scopes=None, diff --git a/tests/unit/test_client_options.py b/tests/unit/test_client_options.py index 396d66271..58b0286cb 100644 --- a/tests/unit/test_client_options.py +++ b/tests/unit/test_client_options.py @@ -14,6 +14,7 @@ from re import match import pytest +from ..helpers import warn_deprecated_credentials_file from google.api_core import client_options @@ -27,19 +28,19 @@ def get_client_encrypted_cert(): def test_constructor(): - - options = client_options.ClientOptions( - api_endpoint="foo.googleapis.com", - client_cert_source=get_client_cert, - quota_project_id="quote-proj", - credentials_file="path/to/credentials.json", - scopes=[ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/cloud-platform.read-only", - ], - api_audience="foo2.googleapis.com", - universe_domain="googleapis.com", - ) + with warn_deprecated_credentials_file(): + options = client_options.ClientOptions( + api_endpoint="foo.googleapis.com", + client_cert_source=get_client_cert, + quota_project_id="quote-proj", + credentials_file="path/to/credentials.json", + scopes=[ + "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/cloud-platform.read-only", + ], + api_audience="foo2.googleapis.com", + universe_domain="googleapis.com", + ) assert options.api_endpoint == "foo.googleapis.com" assert options.client_cert_source() == (b"cert", b"key") @@ -102,10 +103,11 @@ def test_constructor_with_api_key(): def test_constructor_with_both_api_key_and_credentials_file(): with pytest.raises(ValueError): - client_options.ClientOptions( - api_key="api-key", - credentials_file="path/to/credentials.json", - ) + with warn_deprecated_credentials_file(): + client_options.ClientOptions( + api_key="api-key", + credentials_file="path/to/credentials.json", + ) def test_from_dict(): diff --git a/tests/unit/test_grpc_helpers.py b/tests/unit/test_grpc_helpers.py index 8de9d8c0b..ed4a92249 100644 --- a/tests/unit/test_grpc_helpers.py +++ b/tests/unit/test_grpc_helpers.py @@ -15,6 +15,7 @@ from unittest import mock import pytest +from ..helpers import warn_deprecated_credentials_file try: import grpc @@ -581,11 +582,12 @@ def test_create_channel_explicit_with_duplicate_credentials(): target = "example.com:443" with pytest.raises(exceptions.DuplicateCredentialArgs): - grpc_helpers.create_channel( - target, - credentials_file="credentials.json", - credentials=mock.sentinel.credentials, - ) + with warn_deprecated_credentials_file(): + grpc_helpers.create_channel( + target, + credentials_file="credentials.json", + credentials=mock.sentinel.credentials, + ) @mock.patch("grpc.compute_engine_channel_credentials") @@ -710,7 +712,8 @@ def test_create_channel_with_credentials_file( credentials_file = "/path/to/credentials/file.json" composite_creds = composite_creds_call.return_value - channel = grpc_helpers.create_channel(target, credentials_file=credentials_file) + with warn_deprecated_credentials_file(): + channel = grpc_helpers.create_channel(target, credentials_file=credentials_file) google.auth.load_credentials_from_file.assert_called_once_with( credentials_file, scopes=None, default_scopes=None @@ -742,9 +745,10 @@ def test_create_channel_with_credentials_file_and_scopes( credentials_file = "/path/to/credentials/file.json" composite_creds = composite_creds_call.return_value - channel = grpc_helpers.create_channel( - target, credentials_file=credentials_file, scopes=scopes - ) + with warn_deprecated_credentials_file(): + channel = grpc_helpers.create_channel( + target, credentials_file=credentials_file, scopes=scopes + ) google.auth.load_credentials_from_file.assert_called_once_with( credentials_file, scopes=scopes, default_scopes=None @@ -776,9 +780,10 @@ def test_create_channel_with_credentials_file_and_default_scopes( credentials_file = "/path/to/credentials/file.json" composite_creds = composite_creds_call.return_value - channel = grpc_helpers.create_channel( - target, credentials_file=credentials_file, default_scopes=default_scopes - ) + with warn_deprecated_credentials_file(): + channel = grpc_helpers.create_channel( + target, credentials_file=credentials_file, default_scopes=default_scopes + ) load_credentials_from_file.assert_called_once_with( credentials_file, scopes=None, default_scopes=default_scopes diff --git a/tests/unit/test_python_version_support.py b/tests/unit/test_python_version_support.py index c38160b78..76eb821e0 100644 --- a/tests/unit/test_python_version_support.py +++ b/tests/unit/test_python_version_support.py @@ -178,17 +178,20 @@ def test_override_gapic_end_only(): "google.api_core._python_version_support.PYTHON_VERSION_INFO", {version_tuple: overridden_info}, ): - result_before_boundary = check_python_version( - today=custom_gapic_end + datetime.timedelta(days=-1) - ) + with pytest.warns(FutureWarning, match="past its end of life"): + result_before_boundary = check_python_version( + today=custom_gapic_end + datetime.timedelta(days=-1) + ) assert result_before_boundary == PythonVersionStatus.PYTHON_VERSION_EOL - result_at_boundary = check_python_version(today=custom_gapic_end) + with pytest.warns(FutureWarning, match="past its end of life"): + result_at_boundary = check_python_version(today=custom_gapic_end) assert result_at_boundary == PythonVersionStatus.PYTHON_VERSION_EOL - result_after_boundary = check_python_version( - today=custom_gapic_end + datetime.timedelta(days=1) - ) + with pytest.warns(FutureWarning, match="non-supported Python version"): + result_after_boundary = check_python_version( + today=custom_gapic_end + datetime.timedelta(days=1) + ) assert ( result_after_boundary == PythonVersionStatus.PYTHON_VERSION_UNSUPPORTED ) @@ -217,7 +220,8 @@ def test_override_gapic_deprecation_only(): result_before_boundary == PythonVersionStatus.PYTHON_VERSION_SUPPORTED ) - result_at_boundary = check_python_version(today=custom_gapic_dep) + with pytest.warns(FutureWarning, match="Google will stop supporting"): + result_at_boundary = check_python_version(today=custom_gapic_dep) assert result_at_boundary == PythonVersionStatus.PYTHON_VERSION_DEPRECATED From c969186f2b66bde1df5e25bbedc5868e27d136f9 Mon Sep 17 00:00:00 2001 From: Victor Chudnovsky Date: Thu, 13 Nov 2025 11:07:18 -0800 Subject: [PATCH 095/101] feat: make parse_version_to_tuple public (#864) --- google/api_core/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/google/api_core/__init__.py b/google/api_core/__init__.py index 85811a157..a52ffe874 100644 --- a/google/api_core/__init__.py +++ b/google/api_core/__init__.py @@ -30,6 +30,7 @@ # expose dependency checks for external callers check_python_version = _python_version_support.check_python_version check_dependency_versions = _python_package_support.check_dependency_versions +parse_version_to_tuple = _python_package_support.parse_version_to_tuple warn_deprecation_for_versions_less_than = ( _python_package_support.warn_deprecation_for_versions_less_than ) From 93404080f853699b9217e4b76391a13525db4e3e Mon Sep 17 00:00:00 2001 From: Faizal Kassamali Date: Mon, 17 Nov 2025 11:27:27 -0800 Subject: [PATCH 096/101] fix: flaky tests due to imprecision in floating point calculation and performance test setup (#865) Fix flaky tests due to imprecision in floating point calculation and performance test setup --- tests/unit/gapic/test_method.py | 21 +++++++++++++++++++++ tests/unit/gapic/test_routing_header.py | 4 ++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/tests/unit/gapic/test_method.py b/tests/unit/gapic/test_method.py index 927902bf0..29e8fc217 100644 --- a/tests/unit/gapic/test_method.py +++ b/tests/unit/gapic/test_method.py @@ -192,6 +192,7 @@ def test_wrap_method_with_overriding_retry_timeout_compression(unused_sleep): ) +@pytest.mark.skip(reason="Known flaky due to floating point comparison. #866") def test_wrap_method_with_overriding_timeout_as_a_number(): method = mock.Mock(spec=["__call__"], return_value=42) default_retry = retry.Retry() @@ -200,6 +201,8 @@ def test_wrap_method_with_overriding_timeout_as_a_number(): method, default_retry, default_timeout ) + # Using "result = wrapped_method(timeout=22)" fails since wrapped_method + # does floating point calculations that results in 21.987.. instead of 22 result = wrapped_method(timeout=22) assert result == 42 @@ -210,6 +213,24 @@ def test_wrap_method_with_overriding_timeout_as_a_number(): assert actual_timeout == pytest.approx(22, abs=0.01) +def test_wrap_method_with_overriding_constant_timeout(): + method = mock.Mock(spec=["__call__"], return_value=42) + default_retry = retry.Retry() + default_timeout = timeout.ConstantTimeout(60) + wrapped_method = google.api_core.gapic_v1.method.wrap_method( + method, default_retry, default_timeout + ) + + result = wrapped_method(timeout=timeout.ConstantTimeout(22)) + + assert result == 42 + + actual_timeout = method.call_args[1]["timeout"] + metadata = method.call_args[1]["metadata"] + assert metadata == mock.ANY + assert actual_timeout == 22 + + def test_wrap_method_with_call(): method = mock.Mock() mock_call = mock.Mock() diff --git a/tests/unit/gapic/test_routing_header.py b/tests/unit/gapic/test_routing_header.py index 2c8c75469..f0ec82ec6 100644 --- a/tests/unit/gapic/test_routing_header.py +++ b/tests/unit/gapic/test_routing_header.py @@ -90,8 +90,8 @@ def test__urlencode_param(key, value, expected): def test__urlencode_param_caching_performance(): import time - key = "key" * 100 - value = "value" * 100 + key = "key" * 10000 + value = "value" * 10000 # time with empty cache start_time = time.perf_counter() routing_header._urlencode_param(key, value) From 54d1d364c1ba7426ae61d623f967314174758468 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Wed, 26 Nov 2025 11:47:40 -0500 Subject: [PATCH 097/101] tests: update default python runtime used in tests to 3.14 (#870) This change is needed as part of b/463296248 --- .github/workflows/lint.yml | 2 +- .github/workflows/mypy.yml | 2 +- .github/workflows/unittest.yml | 2 +- CONTRIBUTING.rst | 4 +++- google/api_core/grpc_helpers_async.py | 2 +- google/api_core/operation.py | 2 +- google/api_core/operations_v1/pagers.py | 2 +- google/api_core/operations_v1/pagers_async.py | 2 +- google/api_core/operations_v1/pagers_base.py | 2 +- noxfile.py | 6 +++--- tests/asyncio/test_rest_streaming_async.py | 2 -- tests/unit/operations_v1/test_operations_rest_client.py | 3 --- tests/unit/test_client_logging.py | 1 - tests/unit/test_client_options.py | 2 -- tests/unit/test_page_iterator.py | 1 - 15 files changed, 14 insertions(+), 21 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 1051da0bd..3ed755f00 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -12,7 +12,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.14" - name: Install nox run: | python -m pip install --upgrade setuptools pip wheel diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index e6a79291d..8363e7218 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -12,7 +12,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.14" - name: Install nox run: | python -m pip install --upgrade setuptools pip wheel diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index f260a6a55..f654277bb 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -66,7 +66,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.14" - name: Install coverage run: | python -m pip install --upgrade setuptools pip wheel diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 1a1f608b6..0ac24bc08 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -21,7 +21,7 @@ In order to add a feature: documentation. - The feature must work fully on the following CPython versions: - 3.7, 3.8, 3.9, 3.10, 3.11, 3.12 and 3.13 on both UNIX and Windows. + 3.7, 3.8, 3.9, 3.10, 3.11, 3.12, 3.13 and 3.14 on both UNIX and Windows. - The feature must not add unnecessary dependencies (where "unnecessary" is of course subjective, but new dependencies should @@ -204,6 +204,7 @@ We support: - `Python 3.11`_ - `Python 3.12`_ - `Python 3.13`_ +- `Python 3.14`_ .. _Python 3.7: https://docs.python.org/3.7/ .. _Python 3.8: https://docs.python.org/3.8/ @@ -212,6 +213,7 @@ We support: .. _Python 3.11: https://docs.python.org/3.11/ .. _Python 3.12: https://docs.python.org/3.12/ .. _Python 3.13: https://docs.python.org/3.13/ +.. _Python 3.14: https://docs.python.org/3.14/ Supported versions can be found in our ``noxfile.py`` `config`_. diff --git a/google/api_core/grpc_helpers_async.py b/google/api_core/grpc_helpers_async.py index 312d4df80..9e1ad1105 100644 --- a/google/api_core/grpc_helpers_async.py +++ b/google/api_core/grpc_helpers_async.py @@ -220,7 +220,7 @@ def create_channel( default_host=None, compression=None, attempt_direct_path: Optional[bool] = False, - **kwargs + **kwargs, ): """Create an AsyncIO secure channel with credentials. diff --git a/google/api_core/operation.py b/google/api_core/operation.py index 4b9c9a58b..5206243a7 100644 --- a/google/api_core/operation.py +++ b/google/api_core/operation.py @@ -78,7 +78,7 @@ def __init__( result_type, metadata_type=None, polling=polling.DEFAULT_POLLING, - **kwargs + **kwargs, ): super(Operation, self).__init__(polling=polling, **kwargs) self._operation = operation diff --git a/google/api_core/operations_v1/pagers.py b/google/api_core/operations_v1/pagers.py index 132f1c664..76efd5946 100644 --- a/google/api_core/operations_v1/pagers.py +++ b/google/api_core/operations_v1/pagers.py @@ -48,7 +48,7 @@ def __init__( request: operations_pb2.ListOperationsRequest, response: operations_pb2.ListOperationsResponse, *, - metadata: Sequence[Tuple[str, str]] = () + metadata: Sequence[Tuple[str, str]] = (), ): super().__init__( method=method, request=request, response=response, metadata=metadata diff --git a/google/api_core/operations_v1/pagers_async.py b/google/api_core/operations_v1/pagers_async.py index e2909dd50..4bb7f8c7d 100644 --- a/google/api_core/operations_v1/pagers_async.py +++ b/google/api_core/operations_v1/pagers_async.py @@ -48,7 +48,7 @@ def __init__( request: operations_pb2.ListOperationsRequest, response: operations_pb2.ListOperationsResponse, *, - metadata: Sequence[Tuple[str, str]] = () + metadata: Sequence[Tuple[str, str]] = (), ): super().__init__( method=method, request=request, response=response, metadata=metadata diff --git a/google/api_core/operations_v1/pagers_base.py b/google/api_core/operations_v1/pagers_base.py index 24caf74f2..5ef8384ef 100644 --- a/google/api_core/operations_v1/pagers_base.py +++ b/google/api_core/operations_v1/pagers_base.py @@ -47,7 +47,7 @@ def __init__( request: operations_pb2.ListOperationsRequest, response: operations_pb2.ListOperationsResponse, *, - metadata: Sequence[Tuple[str, str]] = () + metadata: Sequence[Tuple[str, str]] = (), ): """Instantiate the pager. diff --git a/noxfile.py b/noxfile.py index 04347a4f3..37ef2caf9 100644 --- a/noxfile.py +++ b/noxfile.py @@ -23,14 +23,14 @@ import nox # pytype: disable=import-error -BLACK_VERSION = "black==22.3.0" +BLACK_VERSION = "black==23.7.0" BLACK_PATHS = ["docs", "google", "tests", "noxfile.py", "setup.py"] # Black and flake8 clash on the syntax for ignoring flake8's F401 in this file. BLACK_EXCLUDES = ["--exclude", "^/google/api_core/operations_v1/__init__.py"] PYTHON_VERSIONS = ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] -DEFAULT_PYTHON_VERSION = "3.10" +DEFAULT_PYTHON_VERSION = "3.14" CURRENT_DIRECTORY = pathlib.Path(__file__).parent.absolute() # 'docfx' is excluded since it only needs to run in 'docs-presubmit' @@ -261,7 +261,7 @@ def unit_w_async_rest_extra(session): def lint_setup_py(session): """Verify that setup.py is valid (including RST check).""" - session.install("docutils", "Pygments") + session.install("docutils", "Pygments", "setuptools") session.run("python", "setup.py", "check", "--restructuredtext", "--strict") diff --git a/tests/asyncio/test_rest_streaming_async.py b/tests/asyncio/test_rest_streaming_async.py index c9caa2b15..13549c7f9 100644 --- a/tests/asyncio/test_rest_streaming_async.py +++ b/tests/asyncio/test_rest_streaming_async.py @@ -292,7 +292,6 @@ async def test_next_escaped_characters_in_string( @pytest.mark.asyncio @pytest.mark.parametrize("response_type", [EchoResponse, httpbody_pb2.HttpBody]) async def test_next_not_array(response_type): - data = '{"hello": 0}' with mock.patch.object( ResponseMock, "content", return_value=mock_async_gen(data) @@ -352,7 +351,6 @@ async def test_check_buffer(response_type, return_value): @pytest.mark.asyncio @pytest.mark.parametrize("response_type", [EchoResponse, httpbody_pb2.HttpBody]) async def test_next_html(response_type): - data = "" with mock.patch.object( ResponseMock, "content", return_value=mock_async_gen(data) diff --git a/tests/unit/operations_v1/test_operations_rest_client.py b/tests/unit/operations_v1/test_operations_rest_client.py index 87523c5dd..2b96d4edc 100644 --- a/tests/unit/operations_v1/test_operations_rest_client.py +++ b/tests/unit/operations_v1/test_operations_rest_client.py @@ -226,7 +226,6 @@ def test_operations_client_service_account_always_use_jwt(transport_class): PYPARAM_CLIENT, ) def test_operations_client_from_service_account_file(client_class): - if "async" in str(client_class): # TODO(): Add support for service account creds to async REST transport. with pytest.raises(NotImplementedError): @@ -547,7 +546,6 @@ def test_operations_client_client_options_credentials_file( # TODO(): Add support for credentials file to async REST transport. with pytest.raises(core_exceptions.AsyncRestUnsupportedParameterError): with warn_deprecated_credentials_file(): - client_class(client_options=options, transport=transport_name) else: with mock.patch.object(transport_class, "__init__") as patched: @@ -1089,7 +1087,6 @@ async def test_cancel_operation_rest_failure_async(): PYPARAM_CLIENT_TRANSPORT_CREDENTIALS, ) def test_credentials_transport_error(client_class, transport_class, credentials): - # It is an error to provide credentials and a transport instance. transport = transport_class(credentials=credentials) with pytest.raises(ValueError): diff --git a/tests/unit/test_client_logging.py b/tests/unit/test_client_logging.py index b3b0b5c8e..c73b269fe 100644 --- a/tests/unit/test_client_logging.py +++ b/tests/unit/test_client_logging.py @@ -88,7 +88,6 @@ def test_setup_logging_w_incorrect_scope(): def test_initialize_logging(): - with mock.patch("os.getenv", return_value="foogle.bar"): with mock.patch("google.api_core.client_logging._BASE_LOGGER_NAME", "foogle"): initialize_logging() diff --git a/tests/unit/test_client_options.py b/tests/unit/test_client_options.py index 58b0286cb..54558eea0 100644 --- a/tests/unit/test_client_options.py +++ b/tests/unit/test_client_options.py @@ -55,7 +55,6 @@ def test_constructor(): def test_constructor_with_encrypted_cert_source(): - options = client_options.ClientOptions( api_endpoint="foo.googleapis.com", client_encrypted_cert_source=get_client_encrypted_cert, @@ -79,7 +78,6 @@ def test_constructor_with_both_cert_sources(): def test_constructor_with_api_key(): - options = client_options.ClientOptions( api_endpoint="foo.googleapis.com", client_cert_source=get_client_cert, diff --git a/tests/unit/test_page_iterator.py b/tests/unit/test_page_iterator.py index 560722c5d..ba0fbba4f 100644 --- a/tests/unit/test_page_iterator.py +++ b/tests/unit/test_page_iterator.py @@ -360,7 +360,6 @@ def test__has_next_page_w_max_results_not_done(self): assert iterator._has_next_page() def test__has_next_page_w_max_results_done(self): - iterator = page_iterator.HTTPIterator( mock.sentinel.client, mock.sentinel.api_request, From 2196e2affabe670dd2bde448d7641084fad3d83c Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Wed, 26 Nov 2025 13:41:04 -0500 Subject: [PATCH 098/101] chore: remove sync-repo-settings.yaml which is not used (#872) As per[ this README](https://github.com/googleapis/repo-automation-bots/blob/main/packages/sync-repo-settings/README.md), the `sync-repo-settings` bot is deprecated. The bot has already been disabled for this repo and this configuration is now obsolete. --- .github/sync-repo-settings.yaml | 60 --------------------------------- 1 file changed, 60 deletions(-) delete mode 100644 .github/sync-repo-settings.yaml diff --git a/.github/sync-repo-settings.yaml b/.github/sync-repo-settings.yaml deleted file mode 100644 index b724bada2..000000000 --- a/.github/sync-repo-settings.yaml +++ /dev/null @@ -1,60 +0,0 @@ -# https://github.com/googleapis/repo-automation-bots/tree/main/packages/sync-repo-settings -# Rules for main branch protection -branchProtectionRules: -# Identifies the protection rule pattern. Name of the branch to be protected. -# Defaults to `main` -- pattern: main - requiresCodeOwnerReviews: true - requiresStrictStatusChecks: true - requiredStatusCheckContexts: - - 'cla/google' - # No Kokoro: the following are Github actions - - 'lint' - - 'mypy' - - 'unit_grpc_gcp-3.7' - - 'unit_grpc_gcp-3.8' - - 'unit_grpc_gcp-3.9' - - 'unit_grpc_gcp-3.10' - - 'unit_grpc_gcp-3.11' - - 'unit_grpc_gcp-3.12' - - 'unit_grpc_gcp-3.13' - - 'unit_grpc_gcp-3.14' - - 'unit-3.7' - - 'unit-3.8' - - 'unit-3.9' - - 'unit-3.10' - - 'unit-3.11' - - 'unit-3.12' - - 'unit-3.13' - - 'unit-3.14' - - 'unit_wo_grpc-3.10' - - 'unit_wo_grpc-3.11' - - 'unit_wo_grpc-3.12' - - 'unit_wo_grpc-3.13' - - 'unit_wo_grpc-3.14' - - 'unit_w_prerelease_deps-3.7' - - 'unit_w_prerelease_deps-3.8' - - 'unit_w_prerelease_deps-3.9' - - 'unit_w_prerelease_deps-3.10' - - 'unit_w_prerelease_deps-3.11' - - 'unit_w_prerelease_deps-3.12' - - 'unit_w_prerelease_deps-3.13' - - 'unit_w_prerelease_deps-3.14' - - 'unit_w_async_rest_extra-3.7' - - 'unit_w_async_rest_extra-3.8' - - 'unit_w_async_rest_extra-3.9' - - 'unit_w_async_rest_extra-3.10' - - 'unit_w_async_rest_extra-3.11' - - 'unit_w_async_rest_extra-3.12' - - 'unit_w_async_rest_extra-3.13' - - 'unit_w_async_rest_extra-3.14' - - 'cover' - - 'docs' - - 'docfx' -permissionRules: - - team: actools-python - permission: admin - - team: actools - permission: admin - - team: yoshi-python - permission: push From d211307fd5a418489837270ee4a0cdbf4ef3fe57 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Wed, 26 Nov 2025 14:00:41 -0500 Subject: [PATCH 099/101] tests: refactor unit test nox sessions (#873) This change is needed as part of b/463296248. Instead of having 4 different presubmits that run 4 different nox unit tests, we now have all 4 patterns tested under a single nox session. This follows the pattern that we have in google-cloud-python where there is a single unit test nox session. --- .github/workflows/unittest.yml | 44 +++++++++++++++-------- noxfile.py | 65 ++++++++++++++++------------------ 2 files changed, 60 insertions(+), 49 deletions(-) diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index f654277bb..83d808558 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -6,7 +6,31 @@ on: - main jobs: - run-unittests: + unit-prerelease: + name: prerelease_deps + runs-on: ubuntu-latest + strategy: + matrix: + python: ["3.14"] + option: ["prerelease"] + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + allow-prereleases: true + - name: Install nox + run: | + python -m pip install --upgrade setuptools pip wheel + python -m pip install nox + - name: Run ${{ matrix.option }} tests + env: + COVERAGE_FILE: .coverage${{ matrix.option }}-${{matrix.python }} + run: | + nox -s prerelease_deps + unit: name: unit${{ matrix.option }}-${{ matrix.python }} # TODO(https://github.com/googleapis/gapic-generator-python/issues/2303): use `ubuntu-latest` once this bug is fixed. # Use ubuntu-22.04 until Python 3.7 is removed from the test matrix @@ -14,7 +38,6 @@ jobs: runs-on: ubuntu-22.04 strategy: matrix: - option: ["", "_grpc_gcp", "_wo_grpc", "_w_prerelease_deps", "_w_async_rest_extra"] python: - "3.7" - "3.8" @@ -24,13 +47,6 @@ jobs: - "3.12" - "3.13" - "3.14" - exclude: - - option: "_wo_grpc" - python: 3.7 - - option: "_wo_grpc" - python: 3.8 - - option: "_wo_grpc" - python: 3.9 steps: - name: Checkout uses: actions/checkout@v4 @@ -45,21 +61,21 @@ jobs: python -m pip install nox - name: Run unit tests env: - COVERAGE_FILE: .coverage${{ matrix.option }}-${{matrix.python }} + COVERAGE_FILE: .coverage-${{matrix.python }} run: | - nox -s unit${{ matrix.option }}-${{ matrix.python }} + nox -s unit-${{ matrix.python }} - name: Upload coverage results uses: actions/upload-artifact@v4 with: - name: coverage-artifact-${{ matrix.option }}-${{ matrix.python }} - path: .coverage${{ matrix.option }}-${{ matrix.python }} + name: coverage-artifact-${{ matrix.python }} + path: .coverage-${{ matrix.python }} include-hidden-files: true report-coverage: name: cover runs-on: ubuntu-latest needs: - - run-unittests + - unit steps: - name: Checkout uses: actions/checkout@v4 diff --git a/noxfile.py b/noxfile.py index 37ef2caf9..9f53dbde0 100644 --- a/noxfile.py +++ b/noxfile.py @@ -215,46 +215,41 @@ def default(session, install_grpc=True, prerelease=False, install_async_rest=Fal @nox.session(python=PYTHON_VERSIONS) -def unit(session): +@nox.parametrize( + ["install_grpc_gcp", "install_grpc", "install_async_rest"], + [ + (False, True, False), # Run unit tests with grpcio installed + (True, True, False), # Run unit tests with grpcio/grpcio-gcp installed + (False, False, False), # Run unit tests without grpcio installed + (False, True, True), # Run unit tests with grpcio and async rest installed + ], +) +def unit(session, install_grpc_gcp, install_grpc, install_async_rest): """Run the unit test suite.""" - default(session) - -@nox.session(python=PYTHON_VERSIONS) -def unit_w_prerelease_deps(session): - """Run the unit test suite.""" - default(session, prerelease=True) - - -@nox.session(python=PYTHON_VERSIONS) -def unit_grpc_gcp(session): - """ - Run the unit test suite with grpcio-gcp installed. - `grpcio-gcp` doesn't support protobuf 4+. - Remove extra `grpcgcp` when protobuf 3.x is dropped. - https://github.com/googleapis/python-api-core/issues/594 - """ - constraints_path = str( - CURRENT_DIRECTORY / "testing" / f"constraints-{session.python}.txt" + # `grpcio-gcp` doesn't support protobuf 4+. + # Remove extra `grpcgcp` when protobuf 3.x is dropped. + # https://github.com/googleapis/python-api-core/issues/594 + if install_grpc_gcp: + constraints_path = str( + CURRENT_DIRECTORY / "testing" / f"constraints-{session.python}.txt" + ) + # Install grpcio-gcp + session.install("-e", ".[grpcgcp]", "-c", constraints_path) + # Install protobuf < 4.0.0 + session.install("protobuf<4.0.0") + + default( + session=session, + install_grpc=install_grpc, + install_async_rest=install_async_rest, ) - # Install grpcio-gcp - session.install("-e", ".[grpcgcp]", "-c", constraints_path) - # Install protobuf < 4.0.0 - session.install("protobuf<4.0.0") - - default(session) - - -@nox.session(python=PYTHON_VERSIONS) -def unit_wo_grpc(session): - """Run the unit test suite w/o grpcio installed""" - default(session, install_grpc=False) -@nox.session(python=PYTHON_VERSIONS) -def unit_w_async_rest_extra(session): - """Run the unit test suite with the `async_rest` extra""" - default(session, install_async_rest=True) +@nox.session(python=DEFAULT_PYTHON_VERSION) +def prerelease_deps(session): + """Run the unit test suite.""" + default(session, prerelease=True) @nox.session(python=DEFAULT_PYTHON_VERSION) From f0188c6f9c4dc2036f50fa5d8a8b9433749872d2 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Wed, 26 Nov 2025 14:00:51 -0500 Subject: [PATCH 100/101] chore: update github action workflow permissions (#875) This PR addresses the feedback from Github in https://github.com/googleapis/python-api-core/pull/873 --- .github/workflows/docs.yml | 4 ++++ .github/workflows/lint.yml | 4 ++++ .github/workflows/mypy.yml | 4 ++++ .github/workflows/unittest.yml | 3 +++ 4 files changed, 15 insertions(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 2833fe98f..7ef5a2874 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -2,6 +2,10 @@ on: pull_request: branches: - main + +permissions: + contents: read + name: docs jobs: docs: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 3ed755f00..d4929b60f 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -2,6 +2,10 @@ on: pull_request: branches: - main + +permissions: + contents: read + name: lint jobs: lint: diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index 8363e7218..21621fe22 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -2,6 +2,10 @@ on: pull_request: branches: - main + +permissions: + contents: read + name: mypy jobs: mypy: diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 83d808558..f0f80d482 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -5,6 +5,9 @@ on: branches: - main +permissions: + contents: read + jobs: unit-prerelease: name: prerelease_deps From f8bf6f9610f3e0e7580f223794c3906513e1fa73 Mon Sep 17 00:00:00 2001 From: agrawalradhika-cell Date: Sat, 6 Dec 2025 03:09:33 +0530 Subject: [PATCH 101/101] feat: Auto enable mTLS when supported certificates are detected (#869) The Python SDK will use a hybrid approach for mTLS enablement: If the GOOGLE_API_USE_CLIENT_CERTIFICATE environment variable is set (either true or false or any value), the SDK will respect that setting. This is necessary for test scenarios and users who need to explicitly control mTLS behavior. If the GOOGLE_API_USE_CLIENT_CERTIFICATE environment variable is not set, the SDK will automatically enable mTLS only if it detects Managed Workload Identity (MWID) or X.509 Workforce Identity Federation (WIF) certificate sources. In other cases where the variable is not set, mTLS will remain disabled. --------- Signed-off-by: Radhika Agrawal --- .../abstract_operations_base_client.py | 22 ++++++++++------ .../test_operations_rest_client.py | 26 ++++++++++++++++--- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/google/api_core/operations_v1/abstract_operations_base_client.py b/google/api_core/operations_v1/abstract_operations_base_client.py index 160c2a88f..f62f60b04 100644 --- a/google/api_core/operations_v1/abstract_operations_base_client.py +++ b/google/api_core/operations_v1/abstract_operations_base_client.py @@ -300,16 +300,22 @@ def __init__( client_options = client_options_lib.ClientOptions() # Create SSL credentials for mutual TLS if needed. - use_client_cert = os.getenv( - "GOOGLE_API_USE_CLIENT_CERTIFICATE", "false" - ).lower() - if use_client_cert not in ("true", "false"): - raise ValueError( - "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be either `true` or `false`" - ) + if hasattr(mtls, "should_use_client_cert"): + use_client_cert = mtls.should_use_client_cert() + else: + # if unsupported, fallback to reading from env var + use_client_cert_str = os.getenv( + "GOOGLE_API_USE_CLIENT_CERTIFICATE", "false" + ).lower() + if use_client_cert_str not in ("true", "false"): + raise ValueError( + "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be" + " either `true` or `false`" + ) + use_client_cert = use_client_cert_str == "true" client_cert_source_func = None is_mtls = False - if use_client_cert == "true": + if use_client_cert: if client_options.client_cert_source: is_mtls = True client_cert_source_func = client_options.client_cert_source diff --git a/tests/unit/operations_v1/test_operations_rest_client.py b/tests/unit/operations_v1/test_operations_rest_client.py index 2b96d4edc..a3189cf5f 100644 --- a/tests/unit/operations_v1/test_operations_rest_client.py +++ b/tests/unit/operations_v1/test_operations_rest_client.py @@ -35,6 +35,7 @@ from google.api_core import client_options from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 +from google.api_core import parse_version_to_tuple from google.api_core.operations_v1 import AbstractOperationsClient import google.auth @@ -42,6 +43,7 @@ from google.api_core.operations_v1 import pagers_async from google.api_core.operations_v1 import transports from google.auth import credentials as ga_credentials +from google.auth import __version__ as auth_version from google.auth.exceptions import MutualTLSChannelError from google.longrunning import operations_pb2 from google.oauth2 import service_account @@ -345,12 +347,30 @@ def test_operations_client_client_options( with pytest.raises(MutualTLSChannelError): client = client_class() - # Check the case GOOGLE_API_USE_CLIENT_CERTIFICATE has unsupported value. + # Check the case GOOGLE_API_USE_CLIENT_CERTIFICATE has unsupported value with mock.patch.dict( os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "Unsupported"} ): - with pytest.raises(ValueError): - client = client_class() + # Test behavior for google.auth versions < 2.43.0. + # These versions do not have the updated mtls.should_use_client_cert logic. + # Verify that a ValueError is raised when GOOGLE_API_USE_CLIENT_CERTIFICATE + # is set to an unsupported value, as expected in these older versions. + if parse_version_to_tuple(auth_version) < (2, 43, 0): + with pytest.raises(ValueError): + client = client_class() + # Test behavior for google.auth versions >= 2.43.0. + # In these versions, if GOOGLE_API_USE_CLIENT_CERTIFICATE is set to an + # unsupported value (e.g., not 'true' or 'false'), the expected behavior + # of the internal google.auth.mtls.should_use_client_cert() function + # is to return False. Expect should_use_client_cert to return False, so + # client creation should proceed without requiring a client certificate. + else: + with mock.patch.object(transport_class, "__init__") as patched: + patched.return_value = None + client = client_class( + credentials=ga_credentials.AnonymousCredentials(), + transport=transport_name, + ) # Check the case quota_project_id is provided options = client_options.ClientOptions(quota_project_id="octopus")