diff --git a/.github/workflows/kubernetes-e2e-tests.yaml b/.github/workflows/kubernetes-e2e-tests.yaml new file mode 100644 index 00000000000..7624e9f8469 --- /dev/null +++ b/.github/workflows/kubernetes-e2e-tests.yaml @@ -0,0 +1,42 @@ +# 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. + +name: Run Kubernetes e2e tests +on: [pull_request] + +permissions: read-all + +jobs: + build: + runs-on: ubuntu-24.04 + + steps: + - uses: actions/checkout@v4 + - run: | # Needed for git diff to work. + git fetch origin master --depth 1 + git symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/master + + - name: Setup python environment + uses: actions/setup-python@b55428b1882923874294fa556849718a1d7f2ca5 + with: + python-version: 3.11 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + + - name: Run Kubernetes e2e tests + run: ./local/tests/kubernetes_e2e_test.bash diff --git a/src/clusterfuzz/_internal/batch/kubernetes.py b/local/tests/kubernetes_e2e_test.bash old mode 100644 new mode 100755 similarity index 53% rename from src/clusterfuzz/_internal/batch/kubernetes.py rename to local/tests/kubernetes_e2e_test.bash index 69563d4101d..16a75bdc0a6 --- a/src/clusterfuzz/_internal/batch/kubernetes.py +++ b/local/tests/kubernetes_e2e_test.bash @@ -1,3 +1,5 @@ +#!/bin/bash -ex +# # Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -11,17 +13,17 @@ # 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. -"""Kubernetes batch client.""" -from clusterfuzz._internal.remote_task import RemoteTaskInterface +# This script is for running the Kubernetes end-to-end test in CI. + +pip install pipenv + +# Install dependencies. +pipenv --python 3.11 +pipenv install -class KubernetesJobClient(RemoteTaskInterface): - """A remote task execution client for Kubernetes. - - This class is a placeholder for a future implementation of a remote task - execution client that uses Kubernetes. It is not yet implemented. - """ +./local/install_deps.bash - def create_job(self, spec, input_urls): - """Creates a Kubernetes job.""" - raise NotImplementedError('Kubernetes batch client is not implemented yet.') +# Run the test. +export K8S_E2E=1 +pipenv run python butler.py py_unittest -t core -p k8s_service_e2e_test.py diff --git a/src/Pipfile b/src/Pipfile index f462d19f732..2a3efcb090f 100644 --- a/src/Pipfile +++ b/src/Pipfile @@ -27,6 +27,8 @@ google-crc32c = "==1.5.0" grpcio = "==1.62.2" httplib2 = "==0.19.0" jira = "==2.0.0" +Jinja2 = "==3.1.4" +kubernetes = "==34.1.0" mozprocess = "==1.3.1" oauth2client = "==4.1.3" psutil = "==5.9.4" @@ -34,7 +36,7 @@ protobuf = "==4.23.4" pygithub = "==1.55" pyOpenSSL = "==22.0.0" python-dateutil = "==2.8.1" -PyYAML = "==6.0" +PyYAML = "==6.0.1" pytz = "==2023.3" redis = "==4.6.0" requests = "==2.21.0" @@ -49,7 +51,6 @@ charset-normalizer = "==3.3.2" firebase-admin = "==6.2.0" Flask = "==2.2.2" itsdangerous = "==2.0.1" -Jinja2 = "==3.1.4" PyJWT = "==2.7.0" requests-toolbelt = "==0.9.1" werkzeug = "==2.2.2" diff --git a/src/Pipfile.lock b/src/Pipfile.lock index 637b11c3d43..e0891e25016 100644 --- a/src/Pipfile.lock +++ b/src/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "34863be9ebbc59b6df81493d51760b6aaf0648d8029638fd65cf0915139cb0dd" + "sha256": "3c4997a996de56b08da9534d8179e1e1c23b386e88a8b4c71443f2588f39c7cd" }, "pipfile-spec": 6, "requires": {}, @@ -117,15 +117,16 @@ "sha256:fd31f176429cecbc1ba499d4aba31aaccfea488f418d60376b911269d3b883c5" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==3.10.5" }, "aiosignal": { "hashes": [ - "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5", - "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54" + "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", + "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7" ], "markers": "python_version >= '3.9'", - "version": "==1.3.2" + "version": "==1.4.0" }, "antlr4-python3-runtime": { "hashes": [ @@ -136,11 +137,11 @@ }, "attrs": { "hashes": [ - "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", - "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b" + "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", + "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373" ], - "markers": "python_version >= '3.8'", - "version": "==25.3.0" + "markers": "python_version >= '3.9'", + "version": "==25.4.0" }, "bcrypt": { "hashes": [ @@ -173,6 +174,7 @@ "sha256:fbe188b878313d01b7718390f31528be4010fed1faa798c5a1d0469c9c48c369" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==4.1.2" }, "cachetools": { @@ -189,6 +191,7 @@ "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1" ], "index": "pypi", + "markers": "python_version >= '3.6'", "version": "==2024.2.2" }, "cffi": { @@ -294,6 +297,7 @@ "sha256:f8c0a6e9e1dd3eb0414ba320f85da6b0dcbd543126e30fcc546e7372a7fbf3b9" ], "index": "pypi", + "markers": "python_version >= '3.6'", "version": "==37.0.4" }, "defusedxml": { @@ -306,11 +310,11 @@ }, "deprecated": { "hashes": [ - "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d", - "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec" + "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", + "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.2.18" + "version": "==1.3.1" }, "distro": { "hashes": [ @@ -320,129 +324,167 @@ "markers": "python_version >= '3.6'", "version": "==1.9.0" }, + "durationpy": { + "hashes": [ + "sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba", + "sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286" + ], + "version": "==0.10" + }, "frozenlist": { "hashes": [ - "sha256:01bcaa305a0fdad12745502bfd16a1c75b14558dabae226852f9159364573117", - "sha256:03572933a1969a6d6ab509d509e5af82ef80d4a5d4e1e9f2e1cdd22c77a3f4d2", - "sha256:0dbae96c225d584f834b8d3cc688825911960f003a85cb0fd20b6e5512468c42", - "sha256:0e6f8653acb82e15e5443dba415fb62a8732b68fe09936bb6d388c725b57f812", - "sha256:0f2ca7810b809ed0f1917293050163c7654cefc57a49f337d5cd9de717b8fad3", - "sha256:118e97556306402e2b010da1ef21ea70cb6d6122e580da64c056b96f524fbd6a", - "sha256:1255d5d64328c5a0d066ecb0f02034d086537925f1f04b50b1ae60d37afbf572", - "sha256:1330f0a4376587face7637dfd245380a57fe21ae8f9d360c1c2ef8746c4195fa", - "sha256:1b8e8cd8032ba266f91136d7105706ad57770f3522eac4a111d77ac126a25a9b", - "sha256:1c6eceb88aaf7221f75be6ab498dc622a151f5f88d536661af3ffc486245a626", - "sha256:1d7fb014fe0fbfee3efd6a94fc635aeaa68e5e1720fe9e57357f2e2c6e1a647e", - "sha256:1db8b2fc7ee8a940b547a14c10e56560ad3ea6499dc6875c354e2335812f739d", - "sha256:2187248203b59625566cac53572ec8c2647a140ee2738b4e36772930377a533c", - "sha256:2b8cf4cfea847d6c12af06091561a89740f1f67f331c3fa8623391905e878530", - "sha256:2bdfe2d7e6c9281c6e55523acd6c2bf77963cb422fdc7d142fb0cb6621b66878", - "sha256:2e8246877afa3f1ae5c979fe85f567d220f86a50dc6c493b9b7d8191181ae01e", - "sha256:36d2fc099229f1e4237f563b2a3e0ff7ccebc3999f729067ce4e64a97a7f2869", - "sha256:37a8a52c3dfff01515e9bbbee0e6063181362f9de3db2ccf9bc96189b557cbfd", - "sha256:3e911391bffdb806001002c1f860787542f45916c3baf764264a52765d5a5603", - "sha256:431ef6937ae0f853143e2ca67d6da76c083e8b1fe3df0e96f3802fd37626e606", - "sha256:437cfd39564744ae32ad5929e55b18ebd88817f9180e4cc05e7d53b75f79ce85", - "sha256:46138f5a0773d064ff663d273b309b696293d7a7c00a0994c5c13a5078134b64", - "sha256:482fe06e9a3fffbcd41950f9d890034b4a54395c60b5e61fae875d37a699813f", - "sha256:49ba23817781e22fcbd45fd9ff2b9b8cdb7b16a42a4851ab8025cae7b22e96d0", - "sha256:4da6fc43048b648275a220e3a61c33b7fff65d11bdd6dcb9d9c145ff708b804c", - "sha256:4def87ef6d90429f777c9d9de3961679abf938cb6b7b63d4a7eb8a268babfce4", - "sha256:4e1be9111cb6756868ac242b3c2bd1f09d9aea09846e4f5c23715e7afb647103", - "sha256:52021b528f1571f98a7d4258c58aa8d4b1a96d4f01d00d51f1089f2e0323cb02", - "sha256:535eec9987adb04701266b92745d6cdcef2e77669299359c3009c3404dd5d191", - "sha256:536a1236065c29980c15c7229fbb830dedf809708c10e159b8136534233545f0", - "sha256:54dece0d21dce4fdb188a1ffc555926adf1d1c516e493c2914d7c370e454bc9e", - "sha256:56a0b8dd6d0d3d971c91f1df75e824986667ccce91e20dca2023683814344791", - "sha256:5c9e89bf19ca148efcc9e3c44fd4c09d5af85c8a7dd3dbd0da1cb83425ef4983", - "sha256:625170a91dd7261a1d1c2a0c1a353c9e55d21cd67d0852185a5fef86587e6f5f", - "sha256:62c828a5b195570eb4b37369fcbbd58e96c905768d53a44d13044355647838ff", - "sha256:62dd7df78e74d924952e2feb7357d826af8d2f307557a779d14ddf94d7311be8", - "sha256:654e4ba1d0b2154ca2f096bed27461cf6160bc7f504a7f9a9ef447c293caf860", - "sha256:69bbd454f0fb23b51cadc9bdba616c9678e4114b6f9fa372d462ff2ed9323ec8", - "sha256:6ac40ec76041c67b928ca8aaffba15c2b2ee3f5ae8d0cb0617b5e63ec119ca25", - "sha256:6ef8e7e8f2f3820c5f175d70fdd199b79e417acf6c72c5d0aa8f63c9f721646f", - "sha256:716bbba09611b4663ecbb7cd022f640759af8259e12a6ca939c0a6acd49eedba", - "sha256:75ecee69073312951244f11b8627e3700ec2bfe07ed24e3a685a5979f0412d24", - "sha256:7613d9977d2ab4a9141dde4a149f4357e4065949674c5649f920fec86ecb393e", - "sha256:777704c1d7655b802c7850255639672e90e81ad6fa42b99ce5ed3fbf45e338dd", - "sha256:77effc978947548b676c54bbd6a08992759ea6f410d4987d69feea9cd0919911", - "sha256:7b0f6cce16306d2e117cf9db71ab3a9e8878a28176aeaf0dbe35248d97b28d0c", - "sha256:7b8c4dc422c1a3ffc550b465090e53b0bf4839047f3e436a34172ac67c45d595", - "sha256:7daa508e75613809c7a57136dec4871a21bca3080b3a8fc347c50b187df4f00c", - "sha256:853ac025092a24bb3bf09ae87f9127de9fe6e0c345614ac92536577cf956dfcc", - "sha256:85ef8d41764c7de0dcdaf64f733a27352248493a85a80661f3c678acd27e31f2", - "sha256:89ffdb799154fd4d7b85c56d5fa9d9ad48946619e0eb95755723fffa11022d75", - "sha256:8b314faa3051a6d45da196a2c495e922f987dc848e967d8cfeaee8a0328b1cd4", - "sha256:8c952f69dd524558694818a461855f35d36cc7f5c0adddce37e962c85d06eac0", - "sha256:8f5fef13136c4e2dee91bfb9a44e236fff78fc2cd9f838eddfc470c3d7d90afe", - "sha256:920b6bd77d209931e4c263223381d63f76828bec574440f29eb497cf3394c249", - "sha256:94bb451c664415f02f07eef4ece976a2c65dcbab9c2f1705b7031a3a75349d8c", - "sha256:95b7a8a3180dfb280eb044fdec562f9b461614c0ef21669aea6f1d3dac6ee576", - "sha256:9799257237d0479736e2b4c01ff26b5c7f7694ac9692a426cb717f3dc02fff9b", - "sha256:9a0318c2068e217a8f5e3b85e35899f5a19e97141a45bb925bb357cfe1daf770", - "sha256:9a79713adfe28830f27a3c62f6b5406c37376c892b05ae070906f07ae4487046", - "sha256:9d124b38b3c299ca68433597ee26b7819209cb8a3a9ea761dfe9db3a04bba584", - "sha256:a2bda8be77660ad4089caf2223fdbd6db1858462c4b85b67fbfa22102021e497", - "sha256:a4d96dc5bcdbd834ec6b0f91027817214216b5b30316494d2b1aebffb87c534f", - "sha256:a66781d7e4cddcbbcfd64de3d41a61d6bdde370fc2e38623f30b2bd539e84a9f", - "sha256:aa733d123cc78245e9bb15f29b44ed9e5780dc6867cfc4e544717b91f980af3b", - "sha256:abc4e880a9b920bc5020bf6a431a6bb40589d9bca3975c980495f63632e8382f", - "sha256:ae8337990e7a45683548ffb2fee1af2f1ed08169284cd829cdd9a7fa7470530d", - "sha256:b11534872256e1666116f6587a1592ef395a98b54476addb5e8d352925cb5d4a", - "sha256:b35298b2db9c2468106278537ee529719228950a5fdda686582f68f247d1dc6e", - "sha256:b99655c32c1c8e06d111e7f41c06c29a5318cb1835df23a45518e02a47c63b68", - "sha256:ba7f8d97152b61f22d7f59491a781ba9b177dd9f318486c5fbc52cde2db12189", - "sha256:bb52c8166499a8150bfd38478248572c924c003cbb45fe3bcd348e5ac7c000f9", - "sha256:c444d824e22da6c9291886d80c7d00c444981a72686e2b59d38b285617cb52c8", - "sha256:c5b9e42ace7d95bf41e19b87cec8f262c41d3510d8ad7514ab3862ea2197bfb1", - "sha256:c6154c3ba59cda3f954c6333025369e42c3acd0c6e8b6ce31eb5c5b8116c07e0", - "sha256:c7c608f833897501dac548585312d73a7dca028bf3b8688f0d712b7acfaf7fb3", - "sha256:ca9973735ce9f770d24d5484dcb42f68f135351c2fc81a7a9369e48cf2998a29", - "sha256:cbb56587a16cf0fb8acd19e90ff9924979ac1431baea8681712716a8337577b0", - "sha256:cdb2c7f071e4026c19a3e32b93a09e59b12000751fc9b0b7758da899e657d215", - "sha256:d108e2d070034f9d57210f22fefd22ea0d04609fc97c5f7f5a686b3471028590", - "sha256:d18689b40cb3936acd971f663ccb8e2589c45db5e2c5f07e0ec6207664029a9c", - "sha256:d1a686d0b0949182b8faddea596f3fc11f44768d1f74d4cad70213b2e139d821", - "sha256:d1eb89bf3454e2132e046f9599fbcf0a4483ed43b40f545551a39316d0201cd1", - "sha256:d3ceb265249fb401702fce3792e6b44c1166b9319737d21495d3611028d95769", - "sha256:da5cb36623f2b846fb25009d9d9215322318ff1c63403075f812b3b2876c8506", - "sha256:da62fecac21a3ee10463d153549d8db87549a5e77eefb8c91ac84bb42bb1e4e3", - "sha256:e18036cb4caa17ea151fd5f3d70be9d354c99eb8cf817a3ccde8a7873b074348", - "sha256:e19c0fc9f4f030fcae43b4cdec9e8ab83ffe30ec10c79a4a43a04d1af6c5e1ad", - "sha256:e1c6bd2c6399920c9622362ce95a7d74e7f9af9bfec05fff91b8ce4b9647845a", - "sha256:e2ada1d8515d3ea5378c018a5f6d14b4994d4036591a52ceaf1a1549dec8e1ad", - "sha256:e4f9373c500dfc02feea39f7a56e4f543e670212102cc2eeb51d3a99c7ffbde6", - "sha256:e67ddb0749ed066b1a03fba812e2dcae791dd50e5da03be50b6a14d0c1a9ee45", - "sha256:e69bb81de06827147b7bfbaeb284d85219fa92d9f097e32cc73675f279d70188", - "sha256:e6e558ea1e47fd6fa8ac9ccdad403e5dd5ecc6ed8dda94343056fa4277d5c65e", - "sha256:ea8e59105d802c5a38bdbe7362822c522230b3faba2aa35c0fa1765239b7dd70", - "sha256:ed5e3a4462ff25ca84fb09e0fada8ea267df98a450340ead4c91b44857267d70", - "sha256:f1a39819a5a3e84304cd286e3dc62a549fe60985415851b3337b6f5cc91907f1", - "sha256:f27a9f9a86dcf00708be82359db8de86b80d029814e6693259befe82bb58a106", - "sha256:f2c7d5aa19714b1b01a0f515d078a629e445e667b9da869a3cd0e6fe7dec78bd", - "sha256:f3a7bb0fe1f7a70fb5c6f497dc32619db7d2cdd53164af30ade2f34673f8b1fc", - "sha256:f4b3cd7334a4bbc0c472164f3744562cb72d05002cc6fcf58adb104630bbc352", - "sha256:f88bc0a2b9c2a835cb888b32246c27cdab5740059fb3688852bf91e915399b91", - "sha256:fb3b309f1d4086b5533cf7bbcf3f956f0ae6469664522f1bde4feed26fba60f1", - "sha256:fc5e64626e6682638d6e44398c9baf1d6ce6bc236d40b4b57255c9d3f9761f1f" + "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", + "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", + "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", + "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", + "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", + "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", + "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", + "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", + "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", + "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", + "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", + "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", + "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4", + "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", + "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", + "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", + "sha256:1a7607e17ad33361677adcd1443edf6f5da0ce5e5377b798fba20fae194825f3", + "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", + "sha256:1aa77cb5697069af47472e39612976ed05343ff2e84a3dcf15437b232cbfd087", + "sha256:1b9290cf81e95e93fdf90548ce9d3c1211cf574b8e3f4b3b7cb0537cf2227068", + "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7", + "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", + "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", + "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", + "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", + "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", + "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", + "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", + "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", + "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", + "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", + "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", + "sha256:332db6b2563333c5671fecacd085141b5800cb866be16d5e3eb15a2086476675", + "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", + "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", + "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", + "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", + "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", + "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", + "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", + "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", + "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", + "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c", + "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", + "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", + "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", + "sha256:48e6d3f4ec5c7273dfe83ff27c91083c6c9065af655dc2684d2c200c94308bb5", + "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", + "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", + "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", + "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", + "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", + "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", + "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", + "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", + "sha256:59a6a5876ca59d1b63af8cd5e7ffffb024c3dc1e9cf9301b21a2e76286505c95", + "sha256:5a3a935c3a4e89c733303a2d5a7c257ea44af3a56c8202df486b7f5de40f37e1", + "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", + "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", + "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6", + "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", + "sha256:6dc4126390929823e2d2d9dc79ab4046ed74680360fc5f38b585c12c66cdf459", + "sha256:7398c222d1d405e796970320036b1b563892b65809d9e5261487bb2c7f7b5c6a", + "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", + "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", + "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", + "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", + "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186", + "sha256:7bf6cdf8e07c8151fba6fe85735441240ec7f619f935a5205953d58009aef8c6", + "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", + "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e", + "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", + "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", + "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450", + "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", + "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", + "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", + "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", + "sha256:940d4a017dbfed9daf46a3b086e1d2167e7012ee297fef9e1c545c4d022f5178", + "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", + "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", + "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", + "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", + "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", + "sha256:9ff15928d62a0b80bb875655c39bf517938c7d589554cbd2669be42d97c2cb61", + "sha256:a6483e309ca809f1efd154b4d37dc6d9f61037d6c6a81c2dc7a15cb22c8c5dca", + "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad", + "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", + "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a", + "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", + "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", + "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011", + "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", + "sha256:b4f3b365f31c6cd4af24545ca0a244a53688cad8834e32f56831c4923b50a103", + "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", + "sha256:b9be22a69a014bc47e78072d0ecae716f5eb56c15238acca0f43d6eb8e4a5bda", + "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", + "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", + "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e", + "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", + "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef", + "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", + "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", + "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", + "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", + "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", + "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", + "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", + "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", + "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", + "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", + "sha256:d8b7138e5cd0647e4523d6685b0eac5d4be9a184ae9634492f25c6eb38c12a47", + "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", + "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", + "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f", + "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff", + "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", + "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", + "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", + "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", + "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", + "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565", + "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", + "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", + "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2", + "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", + "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", + "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", + "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", + "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd" ], "markers": "python_version >= '3.9'", - "version": "==1.6.0" + "version": "==1.8.0" }, "future": { "hashes": [ "sha256:67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8" ], "index": "pypi", + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.17.1" }, "google-api-core": { + "extras": [ + "grpc" + ], "hashes": [ "sha256:25d29e05a0058ed5f19c61c0a78b1b53adea4d9364b464d014fbda941f6d1c9a", "sha256:d92a5a92dc36dd4f4b9ee4e55528a90e432b059f93aee6ad857f9de8cc7ae94a" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==2.11.1" }, "google-api-python-client": { @@ -451,6 +493,7 @@ "sha256:f34abb671afd488bd19d30721ea20fb30d3796ddd825d6f91f26d8c718a9f07d" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==2.93.0" }, "google-auth": { @@ -459,14 +502,16 @@ "sha256:d61d1b40897407b574da67da1a833bdc10d5a11642566e506565d1b1a46ba873" ], "index": "pypi", + "markers": "python_version >= '3.6'", "version": "==2.22.0" }, "google-auth-httplib2": { "hashes": [ - "sha256:38aa7badf48f974f1eb9861794e9c0cb2a0511a4ec0679b1f886d108f5640e05", - "sha256:b65a0a2123300dd71281a7bf6e64d65a0759287df52729bdd1ae2e47dc311a3d" + "sha256:177898a0175252480d5ed916aeea183c2df87c1f9c26705d74ae6b951c268b0b", + "sha256:426167e5df066e3f5a0fc7ea18768c08e7296046594ce4c8c409c2457dd1f776" ], - "version": "==0.2.0" + "markers": "python_version >= '3.7'", + "version": "==0.3.0" }, "google-auth-oauthlib": { "hashes": [ @@ -478,19 +523,19 @@ }, "google-cloud-appengine-logging": { "hashes": [ - "sha256:48f4dcf43000899c7b411bc27181f70240e81a958a44e44ce800ba8e5d5184ac", - "sha256:f97bde36c7f7ff541123c2570813158bdda0c3f2385c8d32fdf1211c561ae56d" + "sha256:84b705a69e4109fc2f68dfe36ce3df6a34d5c3d989eee6d0ac1b024dda0ba6f5", + "sha256:a4ce9ce94a9fd8c89ed07fa0b06fcf9ea3642f9532a1be1a8c7b5f82c0a70ec6" ], "markers": "python_version >= '3.7'", - "version": "==1.6.1" + "version": "==1.8.0" }, "google-cloud-audit-log": { "hashes": [ - "sha256:2598f1533a7d7cdd6c7bf448c12e5519c1d53162d78784e10bcdd1df67791bc3", - "sha256:daaedfb947a0d77f524e1bd2b560242ab4836fe1afd6b06b92f152b9658554ed" + "sha256:6b88e2349df45f8f4cc0993b687109b1388da1571c502dc1417efa4b66ec55e0", + "sha256:8467d4dcca9f3e6160520c24d71592e49e874838f174762272ec10e7950b6feb" ], "markers": "python_version >= '3.7'", - "version": "==0.3.2" + "version": "==0.4.0" }, "google-cloud-batch": { "hashes": [ @@ -498,6 +543,7 @@ "sha256:f102a7230dd201236e1e2804d4ca025995bd2141887df8a95a64cc419b33b476" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==0.11.0" }, "google-cloud-core": { @@ -506,6 +552,7 @@ "sha256:fbd11cad3e98a7e5b0343dc07cb1039a5ffd7a5bb96e1f1e27cee4bda4a90863" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==2.3.3" }, "google-cloud-datastore": { @@ -514,6 +561,7 @@ "sha256:710ef27ebdfb50340f4671c532ad4c1525665985e9421984ffcd6b96b57e34bb" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==2.16.1" }, "google-cloud-logging": { @@ -522,6 +570,7 @@ "sha256:6beb843cb88a9bc2f6df94ce69048243ebb360ee06979f5311ea7a0ec09bd097" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==3.6.0" }, "google-cloud-monitoring": { @@ -530,6 +579,7 @@ "sha256:4394e5e031f30d622a24739678ec48a45400ead94af9a6032cbff7f66194dc12" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==2.15.1" }, "google-cloud-ndb": { @@ -538,6 +588,7 @@ "sha256:e849b48c984f31616e99e1fcc797d1deb1d883a6f38f58f77768b814fb84b57e" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==2.2.1" }, "google-cloud-profiler": { @@ -553,6 +604,7 @@ "sha256:8254e2960c06a8dc91aeac3c894afba0893a674582f0e0ecfc22f894e6173c2f" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==2.17.0" }, "google-cloud-storage": { @@ -561,6 +613,7 @@ "sha256:9433cf28801671de1c80434238fb1e7e4a1ba3087470e90f70c928ea77c2b9d7" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==2.10.0" }, "google-crc32c": { @@ -635,31 +688,35 @@ "sha256:fe70e325aa68fa4b5edf7d1a4b6f691eb04bbccac0ace68e34820d283b5f80d4" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==1.5.0" }, "google-resumable-media": { "hashes": [ - "sha256:3ce7551e9fe6d99e9a126101d2536612bb73486721951e9562fee0f90c6ababa", - "sha256:5280aed4629f2b60b847b0d42f9857fd4935c11af266744df33d8074cae92fe0" + "sha256:dd14a116af303845a8d932ddae161a26e86cc229645bc98b39f026f9b1717582", + "sha256:f1157ed8b46994d60a1bc432544db62352043113684d4e030ee02e77ebe9a1ae" ], "markers": "python_version >= '3.7'", - "version": "==2.7.2" + "version": "==2.8.0" }, "googleapis-common-protos": { + "extras": [ + "grpc" + ], "hashes": [ - "sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257", - "sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8" + "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", + "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5" ], "markers": "python_version >= '3.7'", - "version": "==1.70.0" + "version": "==1.72.0" }, "grpc-google-iam-v1": { "hashes": [ - "sha256:a3171468459770907926d56a440b2bb643eec1d7ba215f48f3ecece42b4d8351", - "sha256:b3e1fc387a1a329e41672197d0ace9de22c78dd7d215048c4c78712073f7bd20" + "sha256:7a7f697e017a067206a3dfef44e4c634a34d3dee135fe7d7a4613fe3e59217e6", + "sha256:879ac4ef33136c5491a6300e27575a9ec760f6cdf9a2518798c1b8977a5dc389" ], "markers": "python_version >= '3.7'", - "version": "==0.14.2" + "version": "==0.14.3" }, "grpcio": { "hashes": [ @@ -719,6 +776,7 @@ "sha256:fa63245271920786f4cb44dcada4983a3516be8f470924528cf658731864c14b" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==1.62.2" }, "grpcio-status": { @@ -743,6 +801,15 @@ ], "version": "==2.8" }, + "jinja2": { + "hashes": [ + "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", + "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==3.1.4" + }, "jira": { "hashes": [ "sha256:9adeead4d5f5a6aff74c630787f8bd2d4b0e154f3a3036641298064e91b2d25d", @@ -751,6 +818,110 @@ "index": "pypi", "version": "==2.0.0" }, + "kubernetes": { + "hashes": [ + "sha256:8fe8edb0b5d290a2f3ac06596b23f87c658977d46b5f8df9d0f4ea83d0003912", + "sha256:bffba2272534e224e6a7a74d582deb0b545b7c9879d2cd9e4aae9481d1f2cc2a" + ], + "index": "pypi", + "markers": "python_version >= '3.6'", + "version": "==34.1.0" + }, + "markupsafe": { + "hashes": [ + "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", + "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", + "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", + "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", + "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", + "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c", + "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", + "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", + "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", + "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", + "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", + "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26", + "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", + "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", + "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", + "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", + "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", + "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", + "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", + "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", + "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", + "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", + "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", + "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", + "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", + "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758", + "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", + "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8", + "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", + "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", + "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", + "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", + "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", + "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", + "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", + "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", + "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2", + "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", + "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", + "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", + "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", + "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", + "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", + "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", + "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", + "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e", + "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", + "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", + "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", + "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", + "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", + "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", + "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b", + "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", + "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", + "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", + "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d", + "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", + "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", + "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", + "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", + "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", + "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", + "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", + "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", + "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", + "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", + "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", + "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", + "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", + "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", + "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", + "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", + "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7", + "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", + "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", + "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", + "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", + "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", + "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42", + "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", + "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", + "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", + "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", + "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", + "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", + "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc", + "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", + "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50" + ], + "markers": "python_version >= '3.9'", + "version": "==3.0.3" + }, "mozfile": { "hashes": [ "sha256:3b0afcda2fa8b802ef657df80a56f21619008f61fcc14b756124028d7b7adf5c", @@ -775,113 +946,155 @@ }, "multidict": { "hashes": [ - "sha256:032efeab3049e37eef2ff91271884303becc9e54d740b492a93b7e7266e23756", - "sha256:062428944a8dc69df9fdc5d5fc6279421e5f9c75a9ee3f586f274ba7b05ab3c8", - "sha256:0bb8f8302fbc7122033df959e25777b0b7659b1fd6bcb9cb6bed76b5de67afef", - "sha256:0d4b31f8a68dccbcd2c0ea04f0e014f1defc6b78f0eb8b35f2265e8716a6df0c", - "sha256:0ecdc12ea44bab2807d6b4a7e5eef25109ab1c82a8240d86d3c1fc9f3b72efd5", - "sha256:0ee1bf613c448997f73fc4efb4ecebebb1c02268028dd4f11f011f02300cf1e8", - "sha256:11990b5c757d956cd1db7cb140be50a63216af32cd6506329c2c59d732d802db", - "sha256:1535cec6443bfd80d028052e9d17ba6ff8a5a3534c51d285ba56c18af97e9713", - "sha256:1748cb2743bedc339d63eb1bca314061568793acd603a6e37b09a326334c9f44", - "sha256:1b2019317726f41e81154df636a897de1bfe9228c3724a433894e44cd2512378", - "sha256:1c152c49e42277bc9a2f7b78bd5fa10b13e88d1b0328221e7aef89d5c60a99a5", - "sha256:1f1c2f58f08b36f8475f3ec6f5aeb95270921d418bf18f90dffd6be5c7b0e676", - "sha256:1f4e0334d7a555c63f5c8952c57ab6f1c7b4f8c7f3442df689fc9f03df315c08", - "sha256:1f6f90700881438953eae443a9c6f8a509808bc3b185246992c4233ccee37fea", - "sha256:224b79471b4f21169ea25ebc37ed6f058040c578e50ade532e2066562597b8a9", - "sha256:236966ca6c472ea4e2d3f02f6673ebfd36ba3f23159c323f5a496869bc8e47c9", - "sha256:2427370f4a255262928cd14533a70d9738dfacadb7563bc3b7f704cc2360fc4e", - "sha256:24a8caa26521b9ad09732972927d7b45b66453e6ebd91a3c6a46d811eeb7349b", - "sha256:255dac25134d2b141c944b59a0d2f7211ca12a6d4779f7586a98b4b03ea80508", - "sha256:26ae9ad364fc61b936fb7bf4c9d8bd53f3a5b4417142cd0be5c509d6f767e2f1", - "sha256:2e329114f82ad4b9dd291bef614ea8971ec119ecd0f54795109976de75c9a852", - "sha256:3002a856367c0b41cad6784f5b8d3ab008eda194ed7864aaa58f65312e2abcac", - "sha256:30a3ebdc068c27e9d6081fca0e2c33fdf132ecea703a72ea216b81a66860adde", - "sha256:30c433a33be000dd968f5750722eaa0991037be0be4a9d453eba121774985bc8", - "sha256:31469d5832b5885adeb70982e531ce86f8c992334edd2f2254a10fa3182ac504", - "sha256:32a998bd8a64ca48616eac5a8c1cc4fa38fb244a3facf2eeb14abe186e0f6cc5", - "sha256:3307b48cd156153b117c0ea54890a3bdbf858a5b296ddd40dc3852e5f16e9b02", - "sha256:389cfefb599edf3fcfd5f64c0410da686f90f5f5e2c4d84e14f6797a5a337af4", - "sha256:3ada0b058c9f213c5f95ba301f922d402ac234f1111a7d8fd70f1b99f3c281ec", - "sha256:3b73e7227681f85d19dec46e5b881827cd354aabe46049e1a61d2f9aaa4e285a", - "sha256:3ccdde001578347e877ca4f629450973c510e88e8865d5aefbcb89b852ccc666", - "sha256:3cd06d88cb7398252284ee75c8db8e680aa0d321451132d0dba12bc995f0adcc", - "sha256:3cf62f8e447ea2c1395afa289b332e49e13d07435369b6f4e41f887db65b40bf", - "sha256:3d75e621e7d887d539d6e1d789f0c64271c250276c333480a9e1de089611f790", - "sha256:422a5ec315018e606473ba1f5431e064cf8b2a7468019233dcf8082fabad64c8", - "sha256:43173924fa93c7486402217fab99b60baf78d33806af299c56133a3755f69589", - "sha256:43fe10524fb0a0514be3954be53258e61d87341008ce4914f8e8b92bee6f875d", - "sha256:4543d8dc6470a82fde92b035a92529317191ce993533c3c0c68f56811164ed07", - "sha256:4eb33b0bdc50acd538f45041f5f19945a1f32b909b76d7b117c0c25d8063df56", - "sha256:5427a2679e95a642b7f8b0f761e660c845c8e6fe3141cddd6b62005bd133fc21", - "sha256:578568c4ba5f2b8abd956baf8b23790dbfdc953e87d5b110bce343b4a54fc9e7", - "sha256:59fe01ee8e2a1e8ceb3f6dbb216b09c8d9f4ef1c22c4fc825d045a147fa2ebc9", - "sha256:5e3929269e9d7eff905d6971d8b8c85e7dbc72c18fb99c8eae6fe0a152f2e343", - "sha256:61ed4d82f8a1e67eb9eb04f8587970d78fe7cddb4e4d6230b77eda23d27938f9", - "sha256:64bc2bbc5fba7b9db5c2c8d750824f41c6994e3882e6d73c903c2afa78d091e4", - "sha256:659318c6c8a85f6ecfc06b4e57529e5a78dfdd697260cc81f683492ad7e9435a", - "sha256:66eb80dd0ab36dbd559635e62fba3083a48a252633164857a1d1684f14326427", - "sha256:6b5a272bc7c36a2cd1b56ddc6bff02e9ce499f9f14ee4a45c45434ef083f2459", - "sha256:6d79cf5c0c6284e90f72123f4a3e4add52d6c6ebb4a9054e88df15b8d08444c6", - "sha256:7146a8742ea71b5d7d955bffcef58a9e6e04efba704b52a460134fefd10a8208", - "sha256:740915eb776617b57142ce0bb13b7596933496e2f798d3d15a20614adf30d229", - "sha256:75482f43465edefd8a5d72724887ccdcd0c83778ded8f0cb1e0594bf71736cc0", - "sha256:7a76534263d03ae0cfa721fea40fd2b5b9d17a6f85e98025931d41dc49504474", - "sha256:7d50d4abf6729921e9613d98344b74241572b751c6b37feed75fb0c37bd5a817", - "sha256:805031c2f599eee62ac579843555ed1ce389ae00c7e9f74c2a1b45e0564a88dd", - "sha256:8aac2eeff69b71f229a405c0a4b61b54bade8e10163bc7b44fcd257949620618", - "sha256:8b6fcf6054fc4114a27aa865f8840ef3d675f9316e81868e0ad5866184a6cba5", - "sha256:8bd2b875f4ca2bb527fe23e318ddd509b7df163407b0fb717df229041c6df5d3", - "sha256:8eac0c49df91b88bf91f818e0a24c1c46f3622978e2c27035bfdca98e0e18124", - "sha256:909f7d43ff8f13d1adccb6a397094adc369d4da794407f8dd592c51cf0eae4b1", - "sha256:995015cf4a3c0d72cbf453b10a999b92c5629eaf3a0c3e1efb4b5c1f602253bb", - "sha256:99592bd3162e9c664671fd14e578a33bfdba487ea64bcb41d281286d3c870ad7", - "sha256:9c64f4ddb3886dd8ab71b68a7431ad4aa01a8fa5be5b11543b29674f29ca0ba3", - "sha256:9e78006af1a7c8a8007e4f56629d7252668344442f66982368ac06522445e375", - "sha256:9f35de41aec4b323c71f54b0ca461ebf694fb48bec62f65221f52e0017955b39", - "sha256:a059ad6b80de5b84b9fa02a39400319e62edd39d210b4e4f8c4f1243bdac4752", - "sha256:a2b0fabae7939d09d7d16a711468c385272fa1b9b7fb0d37e51143585d8e72e0", - "sha256:a54ec568f1fc7f3c313c2f3b16e5db346bf3660e1309746e7fccbbfded856188", - "sha256:a62d78a1c9072949018cdb05d3c533924ef8ac9bcb06cbf96f6d14772c5cd451", - "sha256:a7bd27f7ab3204f16967a6f899b3e8e9eb3362c0ab91f2ee659e0345445e0078", - "sha256:a7be07e5df178430621c716a63151165684d3e9958f2bbfcb644246162007ab7", - "sha256:ab583ac203af1d09034be41458feeab7863c0635c650a16f15771e1386abf2d7", - "sha256:abcfed2c4c139f25c2355e180bcc077a7cae91eefbb8b3927bb3f836c9586f1f", - "sha256:acc9fa606f76fc111b4569348cc23a771cb52c61516dcc6bcef46d612edb483b", - "sha256:ae93e0ff43b6f6892999af64097b18561691ffd835e21a8348a441e256592e1f", - "sha256:b038f10e23f277153f86f95c777ba1958bcd5993194fda26a1d06fae98b2f00c", - "sha256:b128dbf1c939674a50dd0b28f12c244d90e5015e751a4f339a96c54f7275e291", - "sha256:b1b389ae17296dd739015d5ddb222ee99fd66adeae910de21ac950e00979d897", - "sha256:b57e28dbc031d13916b946719f213c494a517b442d7b48b29443e79610acd887", - "sha256:b90e27b4674e6c405ad6c64e515a505c6d113b832df52fdacb6b1ffd1fa9a1d1", - "sha256:b9cb19dfd83d35b6ff24a4022376ea6e45a2beba8ef3f0836b8a4b288b6ad685", - "sha256:ba46b51b6e51b4ef7bfb84b82f5db0dc5e300fb222a8a13b8cd4111898a869cf", - "sha256:be8751869e28b9c0d368d94f5afcb4234db66fe8496144547b4b6d6a0645cfc6", - "sha256:c23831bdee0a2a3cf21be057b5e5326292f60472fb6c6f86392bbf0de70ba731", - "sha256:c2e98c840c9c8e65c0e04b40c6c5066c8632678cd50c8721fdbcd2e09f21a507", - "sha256:c56c179839d5dcf51d565132185409d1d5dd8e614ba501eb79023a6cab25576b", - "sha256:c605a2b2dc14282b580454b9b5d14ebe0668381a3a26d0ac39daa0ca115eb2ae", - "sha256:ce5b3082e86aee80b3925ab4928198450d8e5b6466e11501fe03ad2191c6d777", - "sha256:d4e8535bd4d741039b5aad4285ecd9b902ef9e224711f0b6afda6e38d7ac02c7", - "sha256:daeac9dd30cda8703c417e4fddccd7c4dc0c73421a0b54a7da2713be125846be", - "sha256:dd53893675b729a965088aaadd6a1f326a72b83742b056c1065bdd2e2a42b4df", - "sha256:e1eb72c741fd24d5a28242ce72bb61bc91f8451877131fa3fe930edb195f7054", - "sha256:e413152e3212c4d39f82cf83c6f91be44bec9ddea950ce17af87fbf4e32ca6b2", - "sha256:ead46b0fa1dcf5af503a46e9f1c2e80b5d95c6011526352fa5f42ea201526124", - "sha256:eccb67b0e78aa2e38a04c5ecc13bab325a43e5159a181a9d1a6723db913cbb3c", - "sha256:edf74dc5e212b8c75165b435c43eb0d5e81b6b300a938a4eb82827119115e840", - "sha256:f2882bf27037eb687e49591690e5d491e677272964f9ec7bc2abbe09108bdfb8", - "sha256:f6f19170197cc29baccd33ccc5b5d6a331058796485857cf34f7635aa25fb0cd", - "sha256:f84627997008390dd15762128dcf73c3365f4ec0106739cde6c20a07ed198ec8", - "sha256:f901a5aace8e8c25d78960dcc24c870c8d356660d3b49b93a78bf38eb682aac3", - "sha256:f92c7f62d59373cd93bc9969d2da9b4b21f78283b1379ba012f7ee8127b3152e", - "sha256:fb6214fe1750adc2a1b801a199d64b5a67671bf76ebf24c730b157846d0e90d2", - "sha256:fbd8d737867912b6c5f99f56782b8cb81f978a97b4437a1c476de90a3e41c9a1", - "sha256:fbf226ac85f7d6b6b9ba77db4ec0704fde88463dc17717aec78ec3c8546c70ad" + "sha256:03ca744319864e92721195fa28c7a3b2bc7b686246b35e4078c1e4d0eb5466d3", + "sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec", + "sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd", + "sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b", + "sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb", + "sha256:08325c9e5367aa379a3496aa9a022fe8837ff22e00b94db256d3a1378c76ab32", + "sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f", + "sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7", + "sha256:096f52730c3fb8ed419db2d44391932b63891b2c5ed14850a7e215c0ba9ade36", + "sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd", + "sha256:0a13fb8e748dfc94749f622de065dd5c1def7e0d2216dba72b1d8069a389c6ff", + "sha256:0db4956f82723cc1c270de9c6e799b4c341d327762ec78ef82bb962f79cc07d8", + "sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d", + "sha256:14c9e076eede3b54c636f8ce1c9c252b5f057c62131211f0ceeec273810c9721", + "sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0", + "sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3", + "sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d", + "sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa", + "sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10", + "sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202", + "sha256:23bfeee5316266e5ee2d625df2d2c602b829435fc3a235c2ba2131495706e4a0", + "sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718", + "sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e", + "sha256:295a92a76188917c7f99cda95858c822f9e4aae5824246bba9b6b44004ddd0a6", + "sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1", + "sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2", + "sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754", + "sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c", + "sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390", + "sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128", + "sha256:329aa225b085b6f004a4955271a7ba9f1087e39dcb7e65f6284a988264a63912", + "sha256:363eb68a0a59bd2303216d2346e6c441ba10d36d1f9969fcb6f1ba700de7bb5c", + "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", + "sha256:3996b50c3237c4aec17459217c1e7bbdead9a22a0fcd3c365564fbd16439dde6", + "sha256:39f1719f57adbb767ef592a50ae5ebb794220d1188f9ca93de471336401c34d2", + "sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f", + "sha256:3ba3ef510467abb0667421a286dc906e30eb08569365f5cdb131d7aff7c2dd84", + "sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842", + "sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9", + "sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6", + "sha256:3e56d780c238f9e1ae66a22d2adf8d16f485381878250db8d496623cd38b22bd", + "sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8", + "sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599", + "sha256:478cc36476687bac1514d651cbbaa94b86b0732fb6855c60c673794c7dd2da62", + "sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec", + "sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34", + "sha256:4b73189894398d59131a66ff157837b1fafea9974be486d036bb3d32331fdbf0", + "sha256:4b7a9db5a870f780220e931d0002bbfd88fb53aceb6293251e2c839415c1b20e", + "sha256:4c09703000a9d0fa3c3404b27041e574cc7f4df4c6563873246d0e11812a94b6", + "sha256:4d409aa42a94c0b3fa617708ef5276dfe81012ba6753a0370fcc9d0195d0a1fc", + "sha256:4d72a9a2d885f5c208b0cb91ff2ed43636bb7e345ec839ff64708e04f69a13cc", + "sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c", + "sha256:51cb455de290ae462593e5b1cb1118c5c22ea7f0d3620d9940bf695cea5a4bd7", + "sha256:521f33e377ff64b96c4c556b81c55d0cfffb96a11c194fd0c3f1e56f3d8dd5a4", + "sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4", + "sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38", + "sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5", + "sha256:661709cdcd919a2ece2234f9bae7174e5220c80b034585d7d8a755632d3e2111", + "sha256:680878b9f3d45c31e1f730eef731f9b0bc1da456155688c6745ee84eb818e90e", + "sha256:6843b28b0364dc605f21481c90fadb5f60d9123b442eb8a726bb74feef588a84", + "sha256:68af405971779d8b37198726f2b6fe3955db846fee42db7a4286fc542203934c", + "sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1", + "sha256:6bdce131e14b04fd34a809b6380dbfd826065c3e2fe8a50dbae659fa0c390546", + "sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a", + "sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c", + "sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036", + "sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38", + "sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99", + "sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64", + "sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e", + "sha256:7e73299c99939f089dd9b2120a04a516b95cdf8c1cd2b18c53ebf0de80b1f18f", + "sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159", + "sha256:7f5170993a0dd3ab871c74f45c0a21a4e2c37a2f2b01b5f722a2ad9c6650469e", + "sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12", + "sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1", + "sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0", + "sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184", + "sha256:8b55d5497b51afdfde55925e04a022f1de14d4f4f25cdfd4f5d9b0aa96166851", + "sha256:8cfc12a8630a29d601f48d47787bd7eb730e475e83edb5d6c5084317463373eb", + "sha256:9281bf5b34f59afbc6b1e477a372e9526b66ca446f4bf62592839c195a718b32", + "sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b", + "sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288", + "sha256:95b5ffa4349df2887518bb839409bcf22caa72d82beec453216802f475b23c81", + "sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd", + "sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45", + "sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a", + "sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca", + "sha256:9cf41880c991716f3c7cec48e2f19ae4045fc9db5fc9cff27347ada24d710bb5", + "sha256:9d14baca2ee12c1a64740d4531356ba50b82543017f3ad6de0deb943c5979abb", + "sha256:9f474ad5acda359c8758c8accc22032c6abe6dc87a8be2440d097785e27a9349", + "sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b", + "sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f", + "sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32", + "sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5", + "sha256:a048ce45dcdaaf1defb76b2e684f997fb5abf74437b6cb7b22ddad934a964e34", + "sha256:a265acbb7bb33a3a2d626afbe756371dce0279e7b17f4f4eda406459c2b5ff1c", + "sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4", + "sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17", + "sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60", + "sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394", + "sha256:a90af66facec4cebe4181b9e62a68be65e45ac9b52b67de9eec118701856e7ff", + "sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00", + "sha256:afa8a2978ec65d2336305550535c9c4ff50ee527914328c8677b3973ade52b85", + "sha256:b15b3afff74f707b9275d5ba6a91ae8f6429c3ffb29bbfd216b0b375a56f13d7", + "sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304", + "sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13", + "sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e", + "sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e", + "sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792", + "sha256:b61189b29081a20c7e4e0b49b44d5d44bb0dc92be3c6d06a11cc043f81bf9329", + "sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb", + "sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b", + "sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000", + "sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6", + "sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62", + "sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63", + "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", + "sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e", + "sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c", + "sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827", + "sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8", + "sha256:ce8fdc2dca699f8dbf055a61d73eaa10482569ad20ee3c36ef9641f69afa8c91", + "sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96", + "sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad", + "sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6", + "sha256:d874eb056410ca05fed180b6642e680373688efafc7f077b2a2f61811e873a40", + "sha256:db99677b4457c7a5c5a949353e125ba72d62b35f74e26da141530fbb012218a7", + "sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4", + "sha256:df0e3bf7993bdbeca5ac25aa859cf40d39019e015c9c91809ba7093967f7a648", + "sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064", + "sha256:e2862408c99f84aa571ab462d25236ef9cb12a602ea959ba9c9009a54902fc73", + "sha256:e3aa16de190d29a0ea1b48253c57d99a68492c8dd8948638073ab9e74dc9410b", + "sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762", + "sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e", + "sha256:eb866162ef2f45063acc7a53a88ef6fe8bf121d45c30ea3c9cd87ce7e191a8d4", + "sha256:ec81878ddf0e98817def1e77d4f50dae5ef5b0e4fe796fae3bd674304172416e", + "sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546", + "sha256:f0e77e3c0008bc9316e662624535b88d360c3a5d3f81e15cf12c139a75250046", + "sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6", + "sha256:f470f68adc395e0183b92a2f4689264d1ea4b40504a24d9882c27375e6662bb9", + "sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d", + "sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf", + "sha256:f8e5c0031b90ca9ce555e2e8fd5c3b02a25f14989cbc310701823832c99eb687", + "sha256:fb287618b9c7aa3bf8d825f02d9201b2f13078a5ed3b293c8f4d953917d84d5e", + "sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885", + "sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7" ], "markers": "python_version >= '3.9'", - "version": "==6.4.3" + "version": "==6.7.0" }, "oauth2client": { "hashes": [ @@ -896,131 +1109,155 @@ "signedtoken" ], "hashes": [ - "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca", - "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918" + "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", + "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1" ], - "markers": "python_version >= '3.6'", - "version": "==3.2.2" + "markers": "python_version >= '3.8'", + "version": "==3.3.1" }, "pbr": { "hashes": [ - "sha256:38d4daea5d9fa63b3f626131b9d34947fd0c8be9b05a29276870580050a25a76", - "sha256:93ea72ce6989eb2eed99d0f75721474f69ad88128afdef5ac377eb797c4bf76b" + "sha256:b46004ec30a5324672683ec848aed9e8fc500b0d261d40a3229c2d2bbfcedc29", + "sha256:ff223894eb1cd271a98076b13d3badff3bb36c424074d26334cd25aebeecea6b" ], "markers": "python_version >= '2.6'", - "version": "==6.1.1" + "version": "==7.0.3" }, "propcache": { "hashes": [ - "sha256:050b571b2e96ec942898f8eb46ea4bfbb19bd5502424747e83badc2d4a99a44e", - "sha256:05543250deac8e61084234d5fc54f8ebd254e8f2b39a16b1dce48904f45b744b", - "sha256:069e7212890b0bcf9b2be0a03afb0c2d5161d91e1bf51569a64f629acc7defbf", - "sha256:09400e98545c998d57d10035ff623266927cb784d13dd2b31fd33b8a5316b85b", - "sha256:0c3c3a203c375b08fd06a20da3cf7aac293b834b6f4f4db71190e8422750cca5", - "sha256:0c86e7ceea56376216eba345aa1fc6a8a6b27ac236181f840d1d7e6a1ea9ba5c", - "sha256:0fbe94666e62ebe36cd652f5fc012abfbc2342de99b523f8267a678e4dfdee3c", - "sha256:17d1c688a443355234f3c031349da69444be052613483f3e4158eef751abcd8a", - "sha256:19a06db789a4bd896ee91ebc50d059e23b3639c25d58eb35be3ca1cbe967c3bf", - "sha256:1c5c7ab7f2bb3f573d1cb921993006ba2d39e8621019dffb1c5bc94cdbae81e8", - "sha256:1eb34d90aac9bfbced9a58b266f8946cb5935869ff01b164573a7634d39fbcb5", - "sha256:1f6cc0ad7b4560e5637eb2c994e97b4fa41ba8226069c9277eb5ea7101845b42", - "sha256:27c6ac6aa9fc7bc662f594ef380707494cb42c22786a558d95fcdedb9aa5d035", - "sha256:2d219b0dbabe75e15e581fc1ae796109b07c8ba7d25b9ae8d650da582bed01b0", - "sha256:2fce1df66915909ff6c824bbb5eb403d2d15f98f1518e583074671a30fe0c21e", - "sha256:319fa8765bfd6a265e5fa661547556da381e53274bc05094fc9ea50da51bfd46", - "sha256:359e81a949a7619802eb601d66d37072b79b79c2505e6d3fd8b945538411400d", - "sha256:3a02a28095b5e63128bcae98eb59025924f121f048a62393db682f049bf4ac24", - "sha256:3e19ea4ea0bf46179f8a3652ac1426e6dcbaf577ce4b4f65be581e237340420d", - "sha256:3e584b6d388aeb0001d6d5c2bd86b26304adde6d9bb9bfa9c4889805021b96de", - "sha256:40d980c33765359098837527e18eddefc9a24cea5b45e078a7f3bb5b032c6ecf", - "sha256:4114c4ada8f3181af20808bedb250da6bae56660e4b8dfd9cd95d4549c0962f7", - "sha256:43593c6772aa12abc3af7784bff4a41ffa921608dd38b77cf1dfd7f5c4e71371", - "sha256:47ef24aa6511e388e9894ec16f0fbf3313a53ee68402bc428744a367ec55b833", - "sha256:4cf9e93a81979f1424f1a3d155213dc928f1069d697e4353edb8a5eba67c6259", - "sha256:4d0dfdd9a2ebc77b869a0b04423591ea8823f791293b527dc1bb896c1d6f1136", - "sha256:563f9d8c03ad645597b8d010ef4e9eab359faeb11a0a2ac9f7b4bc8c28ebef25", - "sha256:58aa11f4ca8b60113d4b8e32d37e7e78bd8af4d1a5b5cb4979ed856a45e62005", - "sha256:5a0a9898fdb99bf11786265468571e628ba60af80dc3f6eb89a3545540c6b0ef", - "sha256:5aed8d8308215089c0734a2af4f2e95eeb360660184ad3912686c181e500b2e7", - "sha256:5b9145c35cc87313b5fd480144f8078716007656093d23059e8993d3a8fa730f", - "sha256:5cb5918253912e088edbf023788de539219718d3b10aef334476b62d2b53de53", - "sha256:5cdb0f3e1eb6dfc9965d19734d8f9c481b294b5274337a8cb5cb01b462dcb7e0", - "sha256:5ced33d827625d0a589e831126ccb4f5c29dfdf6766cac441d23995a65825dcb", - "sha256:603f1fe4144420374f1a69b907494c3acbc867a581c2d49d4175b0de7cc64566", - "sha256:61014615c1274df8da5991a1e5da85a3ccb00c2d4701ac6f3383afd3ca47ab0a", - "sha256:64a956dff37080b352c1c40b2966b09defb014347043e740d420ca1eb7c9b908", - "sha256:668ddddc9f3075af019f784456267eb504cb77c2c4bd46cc8402d723b4d200bf", - "sha256:6d8e309ff9a0503ef70dc9a0ebd3e69cf7b3894c9ae2ae81fc10943c37762458", - "sha256:6f173bbfe976105aaa890b712d1759de339d8a7cef2fc0a1714cc1a1e1c47f64", - "sha256:71ebe3fe42656a2328ab08933d420df5f3ab121772eef78f2dc63624157f0ed9", - "sha256:730178f476ef03d3d4d255f0c9fa186cb1d13fd33ffe89d39f2cda4da90ceb71", - "sha256:7d2d5a0028d920738372630870e7d9644ce437142197f8c827194fca404bf03b", - "sha256:7f30241577d2fef2602113b70ef7231bf4c69a97e04693bde08ddab913ba0ce5", - "sha256:813fbb8b6aea2fc9659815e585e548fe706d6f663fa73dff59a1677d4595a037", - "sha256:82de5da8c8893056603ac2d6a89eb8b4df49abf1a7c19d536984c8dd63f481d5", - "sha256:83be47aa4e35b87c106fc0c84c0fc069d3f9b9b06d3c494cd404ec6747544894", - "sha256:8638f99dca15b9dff328fb6273e09f03d1c50d9b6512f3b65a4154588a7595fe", - "sha256:87380fb1f3089d2a0b8b00f006ed12bd41bd858fabfa7330c954c70f50ed8757", - "sha256:88c423efef9d7a59dae0614eaed718449c09a5ac79a5f224a8b9664d603f04a3", - "sha256:89498dd49c2f9a026ee057965cdf8192e5ae070ce7d7a7bd4b66a8e257d0c976", - "sha256:8a17583515a04358b034e241f952f1715243482fc2c2945fd99a1b03a0bd77d6", - "sha256:916cd229b0150129d645ec51614d38129ee74c03293a9f3f17537be0029a9641", - "sha256:9532ea0b26a401264b1365146c440a6d78269ed41f83f23818d4b79497aeabe7", - "sha256:967a8eec513dbe08330f10137eacb427b2ca52118769e82ebcfcab0fba92a649", - "sha256:975af16f406ce48f1333ec5e912fe11064605d5c5b3f6746969077cc3adeb120", - "sha256:9979643ffc69b799d50d3a7b72b5164a2e97e117009d7af6dfdd2ab906cb72cd", - "sha256:9a8ecf38de50a7f518c21568c80f985e776397b902f1ce0b01f799aba1608b40", - "sha256:9cec3239c85ed15bfaded997773fdad9fb5662b0a7cbc854a43f291eb183179e", - "sha256:9e64e948ab41411958670f1093c0a57acfdc3bee5cf5b935671bbd5313bcf229", - "sha256:9f64d91b751df77931336b5ff7bafbe8845c5770b06630e27acd5dbb71e1931c", - "sha256:a0ab8cf8cdd2194f8ff979a43ab43049b1df0b37aa64ab7eca04ac14429baeb7", - "sha256:a110205022d077da24e60b3df8bcee73971be9575dec5573dd17ae5d81751111", - "sha256:a34aa3a1abc50740be6ac0ab9d594e274f59960d3ad253cd318af76b996dd654", - "sha256:a444192f20f5ce8a5e52761a031b90f5ea6288b1eef42ad4c7e64fef33540b8f", - "sha256:a461959ead5b38e2581998700b26346b78cd98540b5524796c175722f18b0294", - "sha256:a75801768bbe65499495660b777e018cbe90c7980f07f8aa57d6be79ea6f71da", - "sha256:aa8efd8c5adc5a2c9d3b952815ff8f7710cefdcaf5f2c36d26aff51aeca2f12f", - "sha256:aca63103895c7d960a5b9b044a83f544b233c95e0dcff114389d64d762017af7", - "sha256:b0313e8b923b3814d1c4a524c93dfecea5f39fa95601f6a9b1ac96cd66f89ea0", - "sha256:b23c11c2c9e6d4e7300c92e022046ad09b91fd00e36e83c44483df4afa990073", - "sha256:b303b194c2e6f171cfddf8b8ba30baefccf03d36a4d9cab7fd0bb68ba476a3d7", - "sha256:b655032b202028a582d27aeedc2e813299f82cb232f969f87a4fde491a233f11", - "sha256:bd39c92e4c8f6cbf5f08257d6360123af72af9f4da75a690bef50da77362d25f", - "sha256:bef100c88d8692864651b5f98e871fb090bd65c8a41a1cb0ff2322db39c96c27", - "sha256:c2fe5c910f6007e716a06d269608d307b4f36e7babee5f36533722660e8c4a70", - "sha256:c66d8ccbc902ad548312b96ed8d5d266d0d2c6d006fd0f66323e9d8f2dd49be7", - "sha256:cd6a55f65241c551eb53f8cf4d2f4af33512c39da5d9777694e9d9c60872f519", - "sha256:d249609e547c04d190e820d0d4c8ca03ed4582bcf8e4e160a6969ddfb57b62e5", - "sha256:d4e89cde74154c7b5957f87a355bb9c8ec929c167b59c83d90654ea36aeb6180", - "sha256:dc1915ec523b3b494933b5424980831b636fe483d7d543f7afb7b3bf00f0c10f", - "sha256:e1c4d24b804b3a87e9350f79e2371a705a188d292fd310e663483af6ee6718ee", - "sha256:e474fc718e73ba5ec5180358aa07f6aded0ff5f2abe700e3115c37d75c947e18", - "sha256:e4fe2a6d5ce975c117a6bb1e8ccda772d1e7029c1cca1acd209f91d30fa72815", - "sha256:e7fb9a84c9abbf2b2683fa3e7b0d7da4d8ecf139a1c635732a8bda29c5214b0e", - "sha256:e861ad82892408487be144906a368ddbe2dc6297074ade2d892341b35c59844a", - "sha256:ec314cde7314d2dd0510c6787326bbffcbdc317ecee6b7401ce218b3099075a7", - "sha256:ed5f6d2edbf349bd8d630e81f474d33d6ae5d07760c44d33cd808e2f5c8f4ae6", - "sha256:ef2e4e91fb3945769e14ce82ed53007195e616a63aa43b40fb7ebaaf907c8d4c", - "sha256:f011f104db880f4e2166bcdcf7f58250f7a465bc6b068dc84c824a3d4a5c94dc", - "sha256:f1528ec4374617a7a753f90f20e2f551121bb558fcb35926f99e3c42367164b8", - "sha256:f27785888d2fdd918bc36de8b8739f2d6c791399552333721b58193f68ea3e98", - "sha256:f35c7070eeec2cdaac6fd3fe245226ed2a6292d3ee8c938e5bb645b434c5f256", - "sha256:f3bbecd2f34d0e6d3c543fdb3b15d6b60dd69970c2b4c822379e5ec8f6f621d5", - "sha256:f6f1324db48f001c2ca26a25fa25af60711e09b9aaf4b28488602776f4f9a744", - "sha256:f78eb8422acc93d7b69964012ad7048764bb45a54ba7a39bb9e146c72ea29723", - "sha256:fb6e0faf8cb6b4beea5d6ed7b5a578254c6d7df54c36ccd3d8b3eb00d6770277", - "sha256:feccd282de1f6322f56f6845bf1207a537227812f0a9bf5571df52bb418d79d5" + "sha256:0002004213ee1f36cfb3f9a42b5066100c44276b9b72b4e1504cddd3d692e86e", + "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", + "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", + "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", + "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", + "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", + "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", + "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", + "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", + "sha256:182b51b421f0501952d938dc0b0eb45246a5b5153c50d42b495ad5fb7517c888", + "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37", + "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8", + "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60", + "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", + "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4", + "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", + "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7", + "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", + "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", + "sha256:357f5bb5c377a82e105e44bd3d52ba22b616f7b9773714bff93573988ef0a5fb", + "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", + "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", + "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", + "sha256:3d233076ccf9e450c8b3bc6720af226b898ef5d051a2d145f7d765e6e9f9bcff", + "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", + "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", + "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", + "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", + "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", + "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", + "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", + "sha256:4b536b39c5199b96fc6245eb5fb796c497381d3942f169e44e8e392b29c9ebcc", + "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", + "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", + "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", + "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", + "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", + "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", + "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", + "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", + "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", + "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", + "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", + "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5", + "sha256:5fd37c406dd6dc85aa743e214cef35dc54bbdd1419baac4f6ae5e5b1a2976938", + "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", + "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925", + "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", + "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", + "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", + "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", + "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", + "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", + "sha256:71b749281b816793678ae7f3d0d84bd36e694953822eaad408d682efc5ca18e0", + "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", + "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db", + "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", + "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", + "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", + "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", + "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900", + "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", + "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", + "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", + "sha256:948dab269721ae9a87fd16c514a0a2c2a1bdb23a9a61b969b0f9d9ee2968546f", + "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", + "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", + "sha256:99d43339c83aaf4d32bda60928231848eee470c6bda8d02599cc4cebe872d183", + "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", + "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21", + "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db", + "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", + "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb", + "sha256:a129e76735bc792794d5177069691c3217898b9f5cee2b2661471e52ffe13f19", + "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", + "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", + "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", + "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", + "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", + "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", + "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", + "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", + "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", + "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", + "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", + "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", + "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", + "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", + "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", + "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", + "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", + "sha256:cbc3b6dfc728105b2a57c06791eb07a94229202ea75c59db644d7d496b698cac", + "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", + "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", + "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", + "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", + "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581", + "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", + "sha256:daede9cd44e0f8bdd9e6cc9a607fc81feb80fae7a5fc6cecaff0e0bb32e42d00", + "sha256:db65d2af507bbfbdcedb254a11149f894169d90488dd3e7190f7cdcb2d6cd57a", + "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", + "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", + "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", + "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", + "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", + "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", + "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", + "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", + "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", + "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", + "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", + "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", + "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", + "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", + "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c", + "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", + "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", + "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", + "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f", + "sha256:fd2dbc472da1f772a4dae4fa24be938a6c544671a912e30529984dd80400cd88", + "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", + "sha256:fe49d0a85038f36ba9e3ffafa1103e61170b28e95b16622e11be0a0ea07c6781" ], "markers": "python_version >= '3.9'", - "version": "==0.3.1" + "version": "==0.4.1" }, "proto-plus": { "hashes": [ - "sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66", - "sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012" + "sha256:1baa7f81cf0f8acb8bc1f6d085008ba4171eaf669629d1b6d1673b21ed1c0a82", + "sha256:873af56dd0d7e91836aee871e5799e1c6f1bda86ac9a983e0bb9f0c266a568c4" ], "markers": "python_version >= '3.7'", - "version": "==1.26.1" + "version": "==1.27.0" }, "protobuf": { "hashes": [ @@ -1039,6 +1276,7 @@ "sha256:fee88269a090ada09ca63551bf2f573eb2424035bcf2cb1b121895b01a46594a" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==4.23.4" }, "psutil": { @@ -1059,15 +1297,16 @@ "sha256:fd8522436a6ada7b4aad6638662966de0d61d241cb821239b2ae7013d41a43d4" ], "index": "pypi", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==5.9.4" }, "pyasn1": { "hashes": [ - "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", - "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034" + "sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf", + "sha256:9b59a2b25ba7e4f8197db7686c09fb33e658b98339fadb826e9512629017833b" ], "markers": "python_version >= '3.8'", - "version": "==0.6.1" + "version": "==0.6.2" }, "pyasn1-modules": { "hashes": [ @@ -1079,11 +1318,11 @@ }, "pycparser": { "hashes": [ - "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", - "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc" + "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", + "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934" ], "markers": "python_version >= '3.8'", - "version": "==2.22" + "version": "==2.23" }, "pygithub": { "hashes": [ @@ -1091,6 +1330,7 @@ "sha256:2caf0054ea079b71e539741ae56c5a95e073b81fa472ce222e81667381b9601b" ], "index": "pypi", + "markers": "python_version >= '3.6'", "version": "==1.55" }, "pyjwt": { @@ -1111,19 +1351,36 @@ }, "pynacl": { "hashes": [ - "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858", - "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d", - "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93", - "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1", - "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92", - "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff", - "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba", - "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394", - "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b", - "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543" + "sha256:04f20784083014e265ad58c1b2dd562c3e35864b5394a14ab54f5d150ee9e53e", + "sha256:10d755cf2a455d8c0f8c767a43d68f24d163b8fe93ccfaabfa7bafd26be58d73", + "sha256:140373378e34a1f6977e573033d1dd1de88d2a5d90ec6958c9485b2fd9f3eb90", + "sha256:16c60daceee88d04f8d41d0a4004a7ed8d9a5126b997efd2933e08e93a3bd850", + "sha256:16dd347cdc8ae0b0f6187a2608c0af1c8b7ecbbe6b4a06bff8253c192f696990", + "sha256:25720bad35dfac34a2bcdd61d9e08d6bfc6041bebc7751d9c9f2446cf1e77d64", + "sha256:2d6cd56ce4998cb66a6c112fda7b1fdce5266c9f05044fa72972613bef376d15", + "sha256:347dcddce0b4d83ed3f32fd00379c83c425abee5a9d2cd0a2c84871334eaff64", + "sha256:4853c154dc16ea12f8f3ee4b7e763331876316cc3a9f06aeedf39bcdca8f9995", + "sha256:49c336dd80ea54780bcff6a03ee1a476be1612423010472e60af83452aa0f442", + "sha256:4a25cfede801f01e54179b8ff9514bd7b5944da560b7040939732d1804d25419", + "sha256:51fed9fe1bec9e7ff9af31cd0abba179d0e984a2960c77e8e5292c7e9b7f7b5d", + "sha256:536703b8f90e911294831a7fbcd0c062b837f3ccaa923d92a6254e11178aaf42", + "sha256:5789f016e08e5606803161ba24de01b5a345d24590a80323379fc4408832d290", + "sha256:6b08eab48c9669d515a344fb0ef27e2cbde847721e34bba94a343baa0f33f1f4", + "sha256:6b393bc5e5a0eb86bb85b533deb2d2c815666665f840a09e0aa3362bb6088736", + "sha256:84709cea8f888e618c21ed9a0efdb1a59cc63141c403db8bf56c469b71ad56f2", + "sha256:8bfaa0a28a1ab718bad6239979a5a57a8d1506d0caf2fba17e524dbb409441cf", + "sha256:bbcc4452a1eb10cd5217318c822fde4be279c9de8567f78bad24c773c21254f8", + "sha256:cb36deafe6e2bce3b286e5d1f3e1c246e0ccdb8808ddb4550bb2792f2df298f2", + "sha256:cf831615cc16ba324240de79d925eacae8265b7691412ac6b24221db157f6bd1", + "sha256:dcdeb41c22ff3c66eef5e63049abf7639e0db4edee57ba70531fc1b6b133185d", + "sha256:dea103a1afcbc333bc0e992e64233d360d393d1e63d0bc88554f572365664348", + "sha256:ef214b90556bb46a485b7da8258e59204c244b1b5b576fb71848819b468c44a7", + "sha256:f3482abf0f9815e7246d461fab597aa179b7524628a4bc36f86a7dc418d2608d", + "sha256:f46386c24a65383a9081d68e9c2de909b1834ec74ff3013271f1bca9c2d233eb", + "sha256:f4b3824920e206b4f52abd7de621ea7a44fd3cb5c8daceb7c3612345dfc54f2e" ], - "markers": "python_version >= '3.6'", - "version": "==1.5.0" + "markers": "python_version >= '3.8'", + "version": "==1.6.0" }, "pyopenssl": { "hashes": [ @@ -1131,6 +1388,7 @@ "sha256:ea252b38c87425b64116f808355e8da644ef9b07e429398bfece610f893ee2e0" ], "index": "pypi", + "markers": "python_version >= '3.6'", "version": "==22.0.0" }, "pyparsing": { @@ -1147,6 +1405,7 @@ "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" ], "index": "pypi", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.8.1" }, "python-http-client": { @@ -1167,49 +1426,61 @@ }, "pyyaml": { "hashes": [ - "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf", - "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", - "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", - "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", - "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b", - "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4", - "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07", - "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba", - "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9", - "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", - "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", - "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", - "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782", - "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", - "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", - "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", - "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", - "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", - "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1", - "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", - "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", - "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", - "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", - "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", - "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", - "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d", - "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", - "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", - "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7", - "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", - "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", - "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", - "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358", - "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", - "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", - "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", - "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", - "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f", - "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", - "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" + "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5", + "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", + "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df", + "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", + "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206", + "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27", + "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595", + "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62", + "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98", + "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696", + "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290", + "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9", + "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", + "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6", + "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867", + "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47", + "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486", + "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6", + "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3", + "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", + "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", + "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0", + "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c", + "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735", + "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", + "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28", + "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4", + "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba", + "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", + "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef", + "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5", + "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd", + "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3", + "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0", + "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", + "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c", + "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c", + "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", + "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", + "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", + "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859", + "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", + "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54", + "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", + "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b", + "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", + "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa", + "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c", + "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585", + "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", + "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f" ], "index": "pypi", - "version": "==6.0" + "markers": "python_version >= '3.6'", + "version": "==6.0.1" }, "redis": { "hashes": [ @@ -1217,6 +1488,7 @@ "sha256:e2b03db868160ee4591de3cb90d40ebb50a90dd302138775937f6a42b7ed183c" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==4.6.0" }, "requests": { @@ -1225,6 +1497,7 @@ "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" ], "index": "pypi", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.21.0" }, "requests-oauthlib": { @@ -1261,11 +1534,11 @@ }, "setuptools": { "hashes": [ - "sha256:5a78f61820bc088c8e4add52932ae6b8cf423da2aff268c23f813cfbb13b4006", - "sha256:6cdc8cb9a7d590b237dbe4493614a9b75d0559b888047c1f67d49ba50fc3edb2" + "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", + "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c" ], "markers": "python_version >= '3.9'", - "version": "==80.4.0" + "version": "==80.9.0" }, "six": { "hashes": [ @@ -1277,11 +1550,11 @@ }, "uritemplate": { "hashes": [ - "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0", - "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e" + "sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e", + "sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686" ], - "markers": "python_version >= '3.6'", - "version": "==4.1.1" + "markers": "python_version >= '3.9'", + "version": "==4.2.0" }, "urllib3": { "hashes": [ @@ -1291,6 +1564,14 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' and python_version < '4'", "version": "==1.24.3" }, + "websocket-client": { + "hashes": [ + "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", + "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef" + ], + "markers": "python_version >= '3.9'", + "version": "==1.9.0" + }, "wrapt": { "hashes": [ "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc", @@ -1365,127 +1646,154 @@ "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4" ], "index": "pypi", + "markers": "python_version >= '3.6'", "version": "==1.16.0" }, "yarl": { "hashes": [ - "sha256:04d8cfb12714158abf2618f792c77bc5c3d8c5f37353e79509608be4f18705c9", - "sha256:04d9c7a1dc0a26efb33e1acb56c8849bd57a693b85f44774356c92d610369efa", - "sha256:06d06c9d5b5bc3eb56542ceeba6658d31f54cf401e8468512447834856fb0e61", - "sha256:077989b09ffd2f48fb2d8f6a86c5fef02f63ffe6b1dd4824c76de7bb01e4f2e2", - "sha256:083ce0393ea173cd37834eb84df15b6853b555d20c52703e21fbababa8c129d2", - "sha256:087e9731884621b162a3e06dc0d2d626e1542a617f65ba7cc7aeab279d55ad33", - "sha256:0a6a1e6ae21cdd84011c24c78d7a126425148b24d437b5702328e4ba640a8902", - "sha256:0acfaf1da020253f3533526e8b7dd212838fdc4109959a2c53cafc6db611bff2", - "sha256:119bca25e63a7725b0c9d20ac67ca6d98fa40e5a894bd5d4686010ff73397914", - "sha256:123393db7420e71d6ce40d24885a9e65eb1edefc7a5228db2d62bcab3386a5c0", - "sha256:18e321617de4ab170226cd15006a565d0fa0d908f11f724a2c9142d6b2812ab0", - "sha256:1a06701b647c9939d7019acdfa7ebbfbb78ba6aa05985bb195ad716ea759a569", - "sha256:2137810a20b933b1b1b7e5cf06a64c3ed3b4747b0e5d79c9447c00db0e2f752f", - "sha256:25b3bc0763a7aca16a0f1b5e8ef0f23829df11fb539a1b70476dcab28bd83da7", - "sha256:27359776bc359ee6eaefe40cb19060238f31228799e43ebd3884e9c589e63b20", - "sha256:2a8f64df8ed5d04c51260dbae3cc82e5649834eebea9eadfd829837b8093eb00", - "sha256:33bb660b390a0554d41f8ebec5cd4475502d84104b27e9b42f5321c5192bfcd1", - "sha256:35d20fb919546995f1d8c9e41f485febd266f60e55383090010f272aca93edcc", - "sha256:3b2992fe29002fd0d4cbaea9428b09af9b8686a9024c840b8a2b8f4ea4abc16f", - "sha256:3b4e88d6c3c8672f45a30867817e4537df1bbc6f882a91581faf1f6d9f0f1b5a", - "sha256:3b60a86551669c23dc5445010534d2c5d8a4e012163218fc9114e857c0586fdd", - "sha256:3d7dbbe44b443b0c4aa0971cb07dcb2c2060e4a9bf8d1301140a33a93c98e18c", - "sha256:3e429857e341d5e8e15806118e0294f8073ba9c4580637e59ab7b238afca836f", - "sha256:40ed574b4df723583a26c04b298b283ff171bcc387bc34c2683235e2487a65a5", - "sha256:42fbe577272c203528d402eec8bf4b2d14fd49ecfec92272334270b850e9cd7d", - "sha256:4345f58719825bba29895011e8e3b545e6e00257abb984f9f27fe923afca2501", - "sha256:447c5eadd750db8389804030d15f43d30435ed47af1313303ed82a62388176d3", - "sha256:44869ee8538208fe5d9342ed62c11cc6a7a1af1b3d0bb79bb795101b6e77f6e0", - "sha256:484e7a08f72683c0f160270566b4395ea5412b4359772b98659921411d32ad26", - "sha256:4a34c52ed158f89876cba9c600b2c964dfc1ca52ba7b3ab6deb722d1d8be6df2", - "sha256:4ba5e59f14bfe8d261a654278a0f6364feef64a794bd456a8c9e823071e5061c", - "sha256:4c43030e4b0af775a85be1fa0433119b1565673266a70bf87ef68a9d5ba3174c", - "sha256:4c903e0b42aab48abfbac668b5a9d7b6938e721a6341751331bcd7553de2dcae", - "sha256:4d9949eaf05b4d30e93e4034a7790634bbb41b8be2d07edd26754f2e38e491de", - "sha256:4f1a350a652bbbe12f666109fbddfdf049b3ff43696d18c9ab1531fbba1c977a", - "sha256:53b2da3a6ca0a541c1ae799c349788d480e5144cac47dba0266c7cb6c76151fe", - "sha256:54ac15a8b60382b2bcefd9a289ee26dc0920cf59b05368c9b2b72450751c6eb8", - "sha256:5d0fe6af927a47a230f31e6004621fd0959eaa915fc62acfafa67ff7229a3124", - "sha256:5d3d6d14754aefc7a458261027a562f024d4f6b8a798adb472277f675857b1eb", - "sha256:5d9b980d7234614bc4674468ab173ed77d678349c860c3af83b1fffb6a837ddc", - "sha256:634b7ba6b4a85cf67e9df7c13a7fb2e44fa37b5d34501038d174a63eaac25ee2", - "sha256:65a4053580fe88a63e8e4056b427224cd01edfb5f951498bfefca4052f0ce0ac", - "sha256:686d51e51ee5dfe62dec86e4866ee0e9ed66df700d55c828a615640adc885307", - "sha256:69df35468b66c1a6e6556248e6443ef0ec5f11a7a4428cf1f6281f1879220f58", - "sha256:6d12b8945250d80c67688602c891237994d203d42427cb14e36d1a732eda480e", - "sha256:6d409e321e4addf7d97ee84162538c7258e53792eb7c6defd0c33647d754172e", - "sha256:70e0c580a0292c7414a1cead1e076c9786f685c1fc4757573d2967689b370e62", - "sha256:737e9f171e5a07031cbee5e9180f6ce21a6c599b9d4b2c24d35df20a52fabf4b", - "sha256:7595498d085becc8fb9203aa314b136ab0516c7abd97e7d74f7bb4eb95042abe", - "sha256:798a5074e656f06b9fad1a162be5a32da45237ce19d07884d0b67a0aa9d5fdda", - "sha256:7dc63ad0d541c38b6ae2255aaa794434293964677d5c1ec5d0116b0e308031f5", - "sha256:839de4c574169b6598d47ad61534e6981979ca2c820ccb77bf70f4311dd2cc64", - "sha256:84aeb556cb06c00652dbf87c17838eb6d92cfd317799a8092cee0e570ee11229", - "sha256:85a231fa250dfa3308f3c7896cc007a47bc76e9e8e8595c20b7426cac4884c62", - "sha256:866349da9d8c5290cfefb7fcc47721e94de3f315433613e01b435473be63daa6", - "sha256:8681700f4e4df891eafa4f69a439a6e7d480d64e52bf460918f58e443bd3da7d", - "sha256:86de313371ec04dd2531f30bc41a5a1a96f25a02823558ee0f2af0beaa7ca791", - "sha256:8a7f62f5dc70a6c763bec9ebf922be52aa22863d9496a9a30124d65b489ea672", - "sha256:8c12cd754d9dbd14204c328915e23b0c361b88f3cffd124129955e60a4fbfcfb", - "sha256:8d8a3d54a090e0fff5837cd3cc305dd8a07d3435a088ddb1f65e33b322f66a94", - "sha256:91bc450c80a2e9685b10e34e41aef3d44ddf99b3a498717938926d05ca493f6a", - "sha256:95b50910e496567434cb77a577493c26bce0f31c8a305135f3bda6a2483b8e10", - "sha256:95fc9876f917cac7f757df80a5dda9de59d423568460fe75d128c813b9af558e", - "sha256:9c2aa4387de4bc3a5fe158080757748d16567119bef215bec643716b4fbf53f9", - "sha256:9c366b254082d21cc4f08f522ac201d0d83a8b8447ab562732931d31d80eb2a5", - "sha256:a0bc5e05f457b7c1994cc29e83b58f540b76234ba6b9648a4971ddc7f6aa52da", - "sha256:a884b8974729e3899d9287df46f015ce53f7282d8d3340fa0ed57536b440621c", - "sha256:ab47acc9332f3de1b39e9b702d9c916af7f02656b2a86a474d9db4e53ef8fd7a", - "sha256:af4baa8a445977831cbaa91a9a84cc09debb10bc8391f128da2f7bd070fc351d", - "sha256:af5607159085dcdb055d5678fc2d34949bd75ae6ea6b4381e784bbab1c3aa195", - "sha256:b2586e36dc070fc8fad6270f93242124df68b379c3a251af534030a4a33ef594", - "sha256:b4230ac0b97ec5eeb91d96b324d66060a43fd0d2a9b603e3327ed65f084e41f8", - "sha256:b594113a301ad537766b4e16a5a6750fcbb1497dcc1bc8a4daae889e6402a634", - "sha256:b6c4c3d0d6a0ae9b281e492b1465c72de433b782e6b5001c8e7249e085b69051", - "sha256:b7fa0cb9fd27ffb1211cde944b41f5c67ab1c13a13ebafe470b1e206b8459da8", - "sha256:b9ae2fbe54d859b3ade40290f60fe40e7f969d83d482e84d2c31b9bff03e359e", - "sha256:bb769ae5760cd1c6a712135ee7915f9d43f11d9ef769cb3f75a23e398a92d384", - "sha256:bc906b636239631d42eb8a07df8359905da02704a868983265603887ed68c076", - "sha256:bdb77efde644d6f1ad27be8a5d67c10b7f769804fff7a966ccb1da5a4de4b656", - "sha256:bf099e2432131093cc611623e0b0bcc399b8cddd9a91eded8bfb50402ec35018", - "sha256:c27d98f4e5c4060582f44e58309c1e55134880558f1add7a87c1bc36ecfade19", - "sha256:c8703517b924463994c344dcdf99a2d5ce9eca2b6882bb640aa555fb5efc706a", - "sha256:c9471ca18e6aeb0e03276b5e9b27b14a54c052d370a9c0c04a68cefbd1455eb4", - "sha256:ce360ae48a5e9961d0c730cf891d40698a82804e85f6e74658fb175207a77cb2", - "sha256:d0bf955b96ea44ad914bc792c26a0edcd71b4668b93cbcd60f5b0aeaaed06c64", - "sha256:d2cbca6760a541189cf87ee54ff891e1d9ea6406079c66341008f7ef6ab61145", - "sha256:d4fad6e5189c847820288286732075f213eabf81be4d08d6cc309912e62be5b7", - "sha256:d88cc43e923f324203f6ec14434fa33b85c06d18d59c167a0637164863b8e995", - "sha256:db243357c6c2bf3cd7e17080034ade668d54ce304d820c2a58514a4e51d0cfd6", - "sha256:dd59c9dd58ae16eaa0f48c3d0cbe6be8ab4dc7247c3ff7db678edecbaf59327f", - "sha256:e06b9f6cdd772f9b665e5ba8161968e11e403774114420737f7884b5bd7bdf6f", - "sha256:e52d6ed9ea8fd3abf4031325dc714aed5afcbfa19ee4a89898d663c9976eb487", - "sha256:ea52f7328a36960ba3231c6677380fa67811b414798a6e071c7085c57b6d20a9", - "sha256:eaddd7804d8e77d67c28d154ae5fab203163bd0998769569861258e525039d2a", - "sha256:f0cf05ae2d3d87a8c9022f3885ac6dea2b751aefd66a4f200e408a61ae9b7f0d", - "sha256:f106e75c454288472dbe615accef8248c686958c2e7dd3b8d8ee2669770d020f", - "sha256:f166eafa78810ddb383e930d62e623d288fb04ec566d1b4790099ae0f31485f1", - "sha256:f1f6670b9ae3daedb325fa55fbe31c22c8228f6e0b513772c2e1c623caa6ab22", - "sha256:f4d3fa9b9f013f7050326e165c3279e22850d02ae544ace285674cb6174b5d6d", - "sha256:f8d8aa8dd89ffb9a831fedbcb27d00ffd9f4842107d52dc9d57e64cb34073d5c", - "sha256:f9d02b591a64e4e6ca18c5e3d925f11b559c763b950184a64cf47d74d7e41877", - "sha256:faa709b66ae0e24c8e5134033187a972d849d87ed0a12a0366bedcc6b5dc14a5", - "sha256:fb0caeac4a164aadce342f1597297ec0ce261ec4532bbc5a9ca8da5622f53867", - "sha256:fdb5204d17cb32b2de2d1e21c7461cabfacf17f3645e4b9039f210c5d3378bf3" + "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", + "sha256:029866bde8d7b0878b9c160e72305bbf0a7342bcd20b9999381704ae03308dc8", + "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b", + "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", + "sha256:07a524d84df0c10f41e3ee918846e1974aba4ec017f990dc735aad487a0bdfdf", + "sha256:088e4e08f033db4be2ccd1f34cf29fe994772fb54cfe004bbf54db320af56890", + "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", + "sha256:0cf71bf877efeac18b38d3930594c0948c82b64547c1cf420ba48722fe5509f6", + "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", + "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", + "sha256:10619d9fdee46d20edc49d3479e2f8269d0779f1b031e6f7c2aa1c76be04b7ed", + "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", + "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", + "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", + "sha256:14291620375b1060613f4aab9ebf21850058b6b1b438f386cc814813d901c60b", + "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", + "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511", + "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", + "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", + "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", + "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", + "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", + "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", + "sha256:2e4e1f6f0b4da23e61188676e3ed027ef0baa833a2e633c29ff8530800edccba", + "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", + "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", + "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", + "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", + "sha256:3aa27acb6de7a23785d81557577491f6c38a5209a254d1191519d07d8fe51748", + "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7", + "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", + "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503", + "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d", + "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", + "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", + "sha256:437840083abe022c978470b942ff832c3940b2ad3734d424b7eaffcd07f76737", + "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", + "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", + "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", + "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", + "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", + "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", + "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d", + "sha256:4dcc74149ccc8bba31ce1944acee24813e93cfdee2acda3c172df844948ddf7b", + "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", + "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6", + "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", + "sha256:595697f68bd1f0c1c159fcb97b661fc9c3f5db46498043555d04805430e79bea", + "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", + "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", + "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", + "sha256:5cdac20da754f3a723cceea5b3448e1a2074866406adeb4ef35b469d089adb8f", + "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", + "sha256:5dbeefd6ca588b33576a01b0ad58aa934bc1b41ef89dee505bf2932b22ddffba", + "sha256:62441e55958977b8167b2709c164c91a6363e25da322d87ae6dd9c6019ceecf9", + "sha256:663e1cadaddae26be034a6ab6072449a8426ddb03d500f43daf952b74553bba0", + "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6", + "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", + "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967", + "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", + "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", + "sha256:6dcbb0829c671f305be48a7227918cfcd11276c2d637a8033a99a02b67bf9eda", + "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", + "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", + "sha256:75976c6945d85dbb9ee6308cd7ff7b1fb9409380c82d6119bd778d8fcfe2931c", + "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", + "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028", + "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", + "sha256:80ddf7a5f8c86cb3eb4bc9028b07bbbf1f08a96c5c0bc1244be5e8fefcb94147", + "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", + "sha256:84fc3ec96fce86ce5aa305eb4aa9358279d1aa644b71fab7b8ed33fe3ba1a7ca", + "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", + "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", + "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", + "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", + "sha256:99b6fc1d55782461b78221e95fc357b47ad98b041e8e20f47c1411d0aacddc60", + "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", + "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", + "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", + "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", + "sha256:a4fcfc8eb2c34148c118dfa02e6427ca278bfd0f3df7c5f99e33d2c0e81eae3e", + "sha256:a899cbd98dce6f5d8de1aad31cb712ec0a530abc0a86bd6edaa47c1090138467", + "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca", + "sha256:af74f05666a5e531289cb1cc9c883d1de2088b8e5b4de48004e5ca8a830ac859", + "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", + "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e", + "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", + "sha256:b580e71cac3f8113d3135888770903eaf2f507e9421e5697d6ee6d8cd1c7f054", + "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376", + "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", + "sha256:b85b982afde6df99ecc996990d4ad7ccbdbb70e2a4ba4de0aecde5922ba98a0b", + "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", + "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65", + "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", + "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", + "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", + "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", + "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", + "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", + "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", + "sha256:c7bd6683587567e5a49ee6e336e0612bec8329be1b7d4c8af5687dcdeb67ee1e", + "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", + "sha256:cb95a9b1adaa48e41815a55ae740cfda005758104049a640a398120bf02515ca", + "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", + "sha256:d332fc2e3c94dad927f2112395772a4e4fedbcf8f80efc21ed7cdfae4d574fdb", + "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", + "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed", + "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", + "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", + "sha256:dd7afd3f8b0bfb4e0d9fc3c31bfe8a4ec7debe124cfd90619305def3c8ca8cd2", + "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", + "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", + "sha256:e1b329cb8146d7b736677a2440e422eadd775d1806a81db2d4cded80a48efc1a", + "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", + "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", + "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", + "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", + "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", + "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e", + "sha256:e81fda2fb4a07eda1a2252b216aa0df23ebcd4d584894e9612e80999a78fd95b", + "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", + "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", + "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", + "sha256:f0d97c18dfd9a9af4490631905a3f131a8e4c9e80a39353919e2cfed8f00aedc", + "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", + "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", + "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", + "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", + "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", + "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249" ], "markers": "python_version >= '3.9'", - "version": "==1.20.0" + "version": "==1.22.0" } }, "develop": { "cachecontrol": { "hashes": [ - "sha256:73e7efec4b06b20d9267b441c1f733664f989fb8688391b670ca812d70795d11", - "sha256:b35e44a3113f17d2a31c1e6b27b9de6d4405f84ae51baa8c1d3cc5b633010cae" + "sha256:b7ac014ff72ee199b5f8af1de29d60239954f223e948196fa3d84adaffc71d2b", + "sha256:e6220afafa4c22a47dd0badb319f84475d79108100d04e26e8542ef7d3ab05a1" ], - "markers": "python_version >= '3.9'", - "version": "==0.14.3" + "markers": "python_version >= '3.10'", + "version": "==0.14.4" }, "cachetools": { "hashes": [ @@ -1501,6 +1809,7 @@ "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1" ], "index": "pypi", + "markers": "python_version >= '3.6'", "version": "==2024.2.2" }, "cffi": { @@ -1573,6 +1882,13 @@ "index": "pypi", "version": "==1.15.1" }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, "charset-normalizer": { "hashes": [ "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", @@ -1667,15 +1983,16 @@ "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" ], "index": "pypi", + "markers": "python_full_version >= '3.7.0'", "version": "==3.3.2" }, "click": { "hashes": [ - "sha256:6b303f0b2aa85f1cb4e5303078fadcbcd4e476f114fab9b5007005711839325c", - "sha256:f5452aeddd9988eefa20f90f05ab66f17fce1ee2a36907fd30b05bbb5953814d" + "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", + "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6" ], "markers": "python_version >= '3.10'", - "version": "==8.2.0" + "version": "==8.3.1" }, "cryptography": { "hashes": [ @@ -1703,6 +2020,7 @@ "sha256:f8c0a6e9e1dd3eb0414ba320f85da6b0dcbd543126e30fcc546e7372a7fbf3b9" ], "index": "pypi", + "markers": "python_version >= '3.6'", "version": "==37.0.4" }, "firebase-admin": { @@ -1711,6 +2029,7 @@ "sha256:e3c42351fb6194d7279a6fd9209a947005fb4ee7e9037d19762e6cb3da4a82e1" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==6.2.0" }, "flask": { @@ -1719,14 +2038,19 @@ "sha256:b9c46cc36662a7949f34b52d8ec7bb59c0d74ba08ba6cb9ce9adc1d8676d9526" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==2.2.2" }, "google-api-core": { + "extras": [ + "grpc" + ], "hashes": [ "sha256:25d29e05a0058ed5f19c61c0a78b1b53adea4d9364b464d014fbda941f6d1c9a", "sha256:d92a5a92dc36dd4f4b9ee4e55528a90e432b059f93aee6ad857f9de8cc7ae94a" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==2.11.1" }, "google-api-python-client": { @@ -1735,6 +2059,7 @@ "sha256:f34abb671afd488bd19d30721ea20fb30d3796ddd825d6f91f26d8c718a9f07d" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==2.93.0" }, "google-auth": { @@ -1743,14 +2068,16 @@ "sha256:d61d1b40897407b574da67da1a833bdc10d5a11642566e506565d1b1a46ba873" ], "index": "pypi", + "markers": "python_version >= '3.6'", "version": "==2.22.0" }, "google-auth-httplib2": { "hashes": [ - "sha256:38aa7badf48f974f1eb9861794e9c0cb2a0511a4ec0679b1f886d108f5640e05", - "sha256:b65a0a2123300dd71281a7bf6e64d65a0759287df52729bdd1ae2e47dc311a3d" + "sha256:177898a0175252480d5ed916aeea183c2df87c1f9c26705d74ae6b951c268b0b", + "sha256:426167e5df066e3f5a0fc7ea18768c08e7296046594ce4c8c409c2457dd1f776" ], - "version": "==0.2.0" + "markers": "python_version >= '3.7'", + "version": "==0.3.0" }, "google-cloud-core": { "hashes": [ @@ -1758,15 +2085,16 @@ "sha256:fbd11cad3e98a7e5b0343dc07cb1039a5ffd7a5bb96e1f1e27cee4bda4a90863" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==2.3.3" }, "google-cloud-firestore": { "hashes": [ - "sha256:0ad2e33fa7da0ba8fb7ccc324f91d3f57866b770e24840bd62f6a272f747c5f9", - "sha256:0ff7b4c66e3ad2fe00f7d5d8c15127bf4ff8b316c6e4eb635ac51d9a9bcd828b" + "sha256:19f2326cb466b0d52aed9fabbd89758be431f6ce18c422966cfdb8326b424314", + "sha256:a9cffba7cdc6101111d6d54cde22d521c98f9e7d415e67486b137fa16f06aa03" ], "markers": "platform_python_implementation != 'PyPy'", - "version": "==2.20.2" + "version": "==2.23.0" }, "google-cloud-storage": { "hashes": [ @@ -1774,6 +2102,7 @@ "sha256:9433cf28801671de1c80434238fb1e7e4a1ba3087470e90f70c928ea77c2b9d7" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==2.10.0" }, "google-crc32c": { @@ -1848,23 +2177,27 @@ "sha256:fe70e325aa68fa4b5edf7d1a4b6f691eb04bbccac0ace68e34820d283b5f80d4" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==1.5.0" }, "google-resumable-media": { "hashes": [ - "sha256:3ce7551e9fe6d99e9a126101d2536612bb73486721951e9562fee0f90c6ababa", - "sha256:5280aed4629f2b60b847b0d42f9857fd4935c11af266744df33d8074cae92fe0" + "sha256:dd14a116af303845a8d932ddae161a26e86cc229645bc98b39f026f9b1717582", + "sha256:f1157ed8b46994d60a1bc432544db62352043113684d4e030ee02e77ebe9a1ae" ], "markers": "python_version >= '3.7'", - "version": "==2.7.2" + "version": "==2.8.0" }, "googleapis-common-protos": { + "extras": [ + "grpc" + ], "hashes": [ - "sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257", - "sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8" + "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", + "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5" ], "markers": "python_version >= '3.7'", - "version": "==1.70.0" + "version": "==1.72.0" }, "grpcio": { "hashes": [ @@ -1924,6 +2257,7 @@ "sha256:fa63245271920786f4cb44dcada4983a3516be8f470924528cf658731864c14b" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==1.62.2" }, "grpcio-status": { @@ -1954,6 +2288,7 @@ "sha256:9e724d68fc22902a1435351f84c3fb8623f303fffcc566a4cb952df8c572cff0" ], "index": "pypi", + "markers": "python_version >= '3.6'", "version": "==2.0.1" }, "jinja2": { @@ -1962,152 +2297,188 @@ "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==3.1.4" }, "markupsafe": { "hashes": [ - "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", - "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", - "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", - "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", - "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", - "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", - "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", - "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", - "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", - "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", - "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", - "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", - "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", - "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", - "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", - "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", - "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", - "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", - "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", - "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", - "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", - "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", - "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", - "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", - "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", - "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", - "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", - "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", - "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", - "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", - "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", - "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", - "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", - "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", - "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", - "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", - "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", - "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", - "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", - "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", - "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", - "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", - "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", - "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", - "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", - "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", - "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", - "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", - "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", - "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", - "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", - "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", - "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", - "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", - "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", - "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", - "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", - "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", - "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", - "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", - "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50" + "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", + "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", + "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", + "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", + "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", + "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c", + "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", + "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", + "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", + "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", + "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", + "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26", + "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", + "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", + "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", + "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", + "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", + "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", + "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", + "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", + "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", + "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", + "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", + "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", + "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", + "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758", + "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", + "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8", + "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", + "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", + "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", + "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", + "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", + "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", + "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", + "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", + "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2", + "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", + "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", + "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", + "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", + "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", + "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", + "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", + "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", + "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e", + "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", + "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", + "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", + "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", + "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", + "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", + "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b", + "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", + "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", + "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", + "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d", + "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", + "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", + "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", + "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", + "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", + "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", + "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", + "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", + "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", + "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", + "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", + "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", + "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", + "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", + "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", + "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", + "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7", + "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", + "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", + "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", + "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", + "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", + "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42", + "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", + "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", + "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", + "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", + "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", + "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", + "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc", + "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", + "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50" ], "markers": "python_version >= '3.9'", - "version": "==3.0.2" + "version": "==3.0.3" }, "msgpack": { "hashes": [ - "sha256:06f5fd2f6bb2a7914922d935d3b8bb4a7fff3a9a91cfce6d06c13bc42bec975b", - "sha256:071603e2f0771c45ad9bc65719291c568d4edf120b44eb36324dcb02a13bfddf", - "sha256:0907e1a7119b337971a689153665764adc34e89175f9a34793307d9def08e6ca", - "sha256:0f92a83b84e7c0749e3f12821949d79485971f087604178026085f60ce109330", - "sha256:115a7af8ee9e8cddc10f87636767857e7e3717b7a2e97379dc2054712693e90f", - "sha256:13599f8829cfbe0158f6456374e9eea9f44eee08076291771d8ae93eda56607f", - "sha256:17fb65dd0bec285907f68b15734a993ad3fc94332b5bb21b0435846228de1f39", - "sha256:2137773500afa5494a61b1208619e3871f75f27b03bcfca7b3a7023284140247", - "sha256:3180065ec2abbe13a4ad37688b61b99d7f9e012a535b930e0e683ad6bc30155b", - "sha256:398b713459fea610861c8a7b62a6fec1882759f308ae0795b5413ff6a160cf3c", - "sha256:3d364a55082fb2a7416f6c63ae383fbd903adb5a6cf78c5b96cc6316dc1cedc7", - "sha256:3df7e6b05571b3814361e8464f9304c42d2196808e0119f55d0d3e62cd5ea044", - "sha256:41c991beebf175faf352fb940bf2af9ad1fb77fd25f38d9142053914947cdbf6", - "sha256:42f754515e0f683f9c79210a5d1cad631ec3d06cea5172214d2176a42e67e19b", - "sha256:452aff037287acb1d70a804ffd022b21fa2bb7c46bee884dbc864cc9024128a0", - "sha256:4676e5be1b472909b2ee6356ff425ebedf5142427842aa06b4dfd5117d1ca8a2", - "sha256:46c34e99110762a76e3911fc923222472c9d681f1094096ac4102c18319e6468", - "sha256:471e27a5787a2e3f974ba023f9e265a8c7cfd373632247deb225617e3100a3c7", - "sha256:4a1964df7b81285d00a84da4e70cb1383f2e665e0f1f2a7027e683956d04b734", - "sha256:4b51405e36e075193bc051315dbf29168d6141ae2500ba8cd80a522964e31434", - "sha256:4d1b7ff2d6146e16e8bd665ac726a89c74163ef8cd39fa8c1087d4e52d3a2325", - "sha256:53258eeb7a80fc46f62fd59c876957a2d0e15e6449a9e71842b6d24419d88ca1", - "sha256:534480ee5690ab3cbed89d4c8971a5c631b69a8c0883ecfea96c19118510c846", - "sha256:58638690ebd0a06427c5fe1a227bb6b8b9fdc2bd07701bec13c2335c82131a88", - "sha256:58dfc47f8b102da61e8949708b3eafc3504509a5728f8b4ddef84bd9e16ad420", - "sha256:59caf6a4ed0d164055ccff8fe31eddc0ebc07cf7326a2aaa0dbf7a4001cd823e", - "sha256:5dbad74103df937e1325cc4bfeaf57713be0b4f15e1c2da43ccdd836393e2ea2", - "sha256:5e1da8f11a3dd397f0a32c76165cf0c4eb95b31013a94f6ecc0b280c05c91b59", - "sha256:646afc8102935a388ffc3914b336d22d1c2d6209c773f3eb5dd4d6d3b6f8c1cb", - "sha256:64fc9068d701233effd61b19efb1485587560b66fe57b3e50d29c5d78e7fef68", - "sha256:65553c9b6da8166e819a6aa90ad15288599b340f91d18f60b2061f402b9a4915", - "sha256:685ec345eefc757a7c8af44a3032734a739f8c45d1b0ac45efc5d8977aa4720f", - "sha256:6ad622bf7756d5a497d5b6836e7fc3752e2dd6f4c648e24b1803f6048596f701", - "sha256:73322a6cc57fcee3c0c57c4463d828e9428275fb85a27aa2aa1a92fdc42afd7b", - "sha256:74bed8f63f8f14d75eec75cf3d04ad581da6b914001b474a5d3cd3372c8cc27d", - "sha256:79ec007767b9b56860e0372085f8504db5d06bd6a327a335449508bbee9648fa", - "sha256:7a946a8992941fea80ed4beae6bff74ffd7ee129a90b4dd5cf9c476a30e9708d", - "sha256:7ad442d527a7e358a469faf43fda45aaf4ac3249c8310a82f0ccff9164e5dccd", - "sha256:7c9a35ce2c2573bada929e0b7b3576de647b0defbd25f5139dcdaba0ae35a4cc", - "sha256:7e7b853bbc44fb03fbdba34feb4bd414322180135e2cb5164f20ce1c9795ee48", - "sha256:879a7b7b0ad82481c52d3c7eb99bf6f0645dbdec5134a4bddbd16f3506947feb", - "sha256:8a706d1e74dd3dea05cb54580d9bd8b2880e9264856ce5068027eed09680aa74", - "sha256:8a84efb768fb968381e525eeeb3d92857e4985aacc39f3c47ffd00eb4509315b", - "sha256:8cf9e8c3a2153934a23ac160cc4cba0ec035f6867c8013cc6077a79823370346", - "sha256:8da4bf6d54ceed70e8861f833f83ce0814a2b72102e890cbdfe4b34764cdd66e", - "sha256:8e59bca908d9ca0de3dc8684f21ebf9a690fe47b6be93236eb40b99af28b6ea6", - "sha256:914571a2a5b4e7606997e169f64ce53a8b1e06f2cf2c3a7273aa106236d43dd5", - "sha256:a51abd48c6d8ac89e0cfd4fe177c61481aca2d5e7ba42044fd218cfd8ea9899f", - "sha256:a52a1f3a5af7ba1c9ace055b659189f6c669cf3657095b50f9602af3a3ba0fe5", - "sha256:ad33e8400e4ec17ba782f7b9cf868977d867ed784a1f5f2ab46e7ba53b6e1e1b", - "sha256:b4c01941fd2ff87c2a934ee6055bda4ed353a7846b8d4f341c428109e9fcde8c", - "sha256:bce7d9e614a04d0883af0b3d4d501171fbfca038f12c77fa838d9f198147a23f", - "sha256:c40ffa9a15d74e05ba1fe2681ea33b9caffd886675412612d93ab17b58ea2fec", - "sha256:c5a91481a3cc573ac8c0d9aace09345d989dc4a0202b7fcb312c88c26d4e71a8", - "sha256:c921af52214dcbb75e6bdf6a661b23c3e6417f00c603dd2070bccb5c3ef499f5", - "sha256:d46cf9e3705ea9485687aa4001a76e44748b609d260af21c4ceea7f2212a501d", - "sha256:d8ce0b22b890be5d252de90d0e0d119f363012027cf256185fc3d474c44b1b9e", - "sha256:dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e", - "sha256:e0856a2b7e8dcb874be44fea031d22e5b3a19121be92a1e098f46068a11b0870", - "sha256:e1f3c3d21f7cf67bcf2da8e494d30a75e4cf60041d98b3f79875afb5b96f3a3f", - "sha256:f1ba6136e650898082d9d5a5217d5906d1e138024f836ff48691784bbe1adf96", - "sha256:f3e9b4936df53b970513eac1758f3882c88658a220b58dcc1e39606dccaaf01c", - "sha256:f80bc7d47f76089633763f952e67f8214cb7b3ee6bfa489b3cb6a84cfac114cd", - "sha256:fd2906780f25c8ed5d7b323379f6138524ba793428db5d0e9d226d3fa6aa1788" + "sha256:0051fffef5a37ca2cd16978ae4f0aef92f164df86823871b5162812bebecd8e2", + "sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014", + "sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931", + "sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b", + "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b", + "sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999", + "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029", + "sha256:283ae72fc89da59aa004ba147e8fc2f766647b1251500182fac0350d8af299c0", + "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9", + "sha256:2e86a607e558d22985d856948c12a3fa7b42efad264dca8a3ebbcfa2735d786c", + "sha256:350ad5353a467d9e3b126d8d1b90fe05ad081e2e1cef5753f8c345217c37e7b8", + "sha256:354e81bcdebaab427c3df4281187edc765d5d76bfb3a7c125af9da7a27e8458f", + "sha256:365c0bbe981a27d8932da71af63ef86acc59ed5c01ad929e09a0b88c6294e28a", + "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42", + "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", + "sha256:41d1a5d875680166d3ac5c38573896453bbbea7092936d2e107214daf43b1d4f", + "sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7", + "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb", + "sha256:454e29e186285d2ebe65be34629fa0e8605202c60fbc7c4c650ccd41870896ef", + "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf", + "sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245", + "sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794", + "sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af", + "sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff", + "sha256:602b6740e95ffc55bfb078172d279de3773d7b7db1f703b2f1323566b878b90e", + "sha256:61c8aa3bd513d87c72ed0b37b53dd5c5a0f58f2ff9f26e1555d3bd7948fb7296", + "sha256:67016ae8c8965124fdede9d3769528ad8284f14d635337ffa6a713a580f6c030", + "sha256:6bde749afe671dc44893f8d08e83bf475a1a14570d67c4bb5cec5573463c8833", + "sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939", + "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa", + "sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90", + "sha256:7bc8813f88417599564fafa59fd6f95be417179f76b40325b500b3c98409757c", + "sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717", + "sha256:86f8136dfa5c116365a8a651a7d7484b65b13339731dd6faebb9a0242151c406", + "sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a", + "sha256:8b696e83c9f1532b4af884045ba7f3aa741a63b2bc22617293a2c6a7c645f251", + "sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2", + "sha256:94fd7dc7d8cb0a54432f296f2246bc39474e017204ca6f4ff345941d4ed285a7", + "sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e", + "sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b", + "sha256:9fba231af7a933400238cb357ecccf8ab5d51535ea95d94fc35b7806218ff844", + "sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9", + "sha256:a605409040f2da88676e9c9e5853b3449ba8011973616189ea5ee55ddbc5bc87", + "sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b", + "sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c", + "sha256:a8f6e7d30253714751aa0b0c84ae28948e852ee7fb0524082e6716769124bc23", + "sha256:ad09b984828d6b7bb52d1d1d0c9be68ad781fa004ca39216c8a1e63c0f34ba3c", + "sha256:bafca952dc13907bdfdedfc6a5f579bf4f292bdd506fadb38389afa3ac5b208e", + "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620", + "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69", + "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f", + "sha256:d198d275222dc54244bf3327eb8cbe00307d220241d9cec4d306d49a44e85f68", + "sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27", + "sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46", + "sha256:db6192777d943bdaaafb6ba66d44bf65aa0e9c5616fa1d2da9bb08828c6b39aa", + "sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00", + "sha256:e64c8d2f5e5d5fda7b842f55dec6133260ea8f53c4257d64494c534f306bf7a9", + "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84", + "sha256:ea5405c46e690122a76531ab97a079e184c0daf491e588592d6a23d3e32af99e", + "sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20", + "sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e", + "sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162" ], - "markers": "python_version >= '3.8'", - "version": "==1.1.0" + "markers": "python_version >= '3.9'", + "version": "==1.1.2" + }, + "parameterized": { + "hashes": [ + "sha256:4e0758e3d41bea3bbd05ec14fc2c24736723f243b28d702081aef438c9372b1b", + "sha256:7fc905272cefa4f364c1a3429cbbe9c0f98b793988efb5bf90aac80f08db09b1" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==0.9.0" }, "proto-plus": { "hashes": [ - "sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66", - "sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012" + "sha256:1baa7f81cf0f8acb8bc1f6d085008ba4171eaf669629d1b6d1673b21ed1c0a82", + "sha256:873af56dd0d7e91836aee871e5799e1c6f1bda86ac9a983e0bb9f0c266a568c4" ], "markers": "python_version >= '3.7'", - "version": "==1.26.1" + "version": "==1.27.0" }, "protobuf": { "hashes": [ @@ -2126,15 +2497,16 @@ "sha256:fee88269a090ada09ca63551bf2f573eb2424035bcf2cb1b121895b01a46594a" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==4.23.4" }, "pyasn1": { "hashes": [ - "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", - "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034" + "sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf", + "sha256:9b59a2b25ba7e4f8197db7686c09fb33e658b98339fadb826e9512629017833b" ], "markers": "python_version >= '3.8'", - "version": "==0.6.1" + "version": "==0.6.2" }, "pyasn1-modules": { "hashes": [ @@ -2146,11 +2518,11 @@ }, "pycparser": { "hashes": [ - "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", - "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc" + "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", + "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934" ], "markers": "python_version >= '3.8'", - "version": "==2.22" + "version": "==2.23" }, "pyjwt": { "hashes": [ @@ -2174,6 +2546,7 @@ "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" ], "index": "pypi", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.21.0" }, "requests-toolbelt": { @@ -2192,13 +2565,21 @@ "markers": "python_version >= '3.6' and python_version < '4'", "version": "==4.9.1" }, + "six": { + "hashes": [ + "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", + "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.17.0" + }, "uritemplate": { "hashes": [ - "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0", - "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e" + "sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e", + "sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686" ], - "markers": "python_version >= '3.6'", - "version": "==4.1.1" + "markers": "python_version >= '3.9'", + "version": "==4.2.0" }, "urllib3": { "hashes": [ @@ -2214,6 +2595,7 @@ "sha256:f979ab81f58d7318e064e99c4506445d60135ac5cd2e177a2de0089bfd4c9bd5" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==2.2.2" } } diff --git a/src/clusterfuzz/_internal/base/tasks/__init__.py b/src/clusterfuzz/_internal/base/tasks/__init__.py index eac1d204154..7ee875dacf4 100644 --- a/src/clusterfuzz/_internal/base/tasks/__init__.py +++ b/src/clusterfuzz/_internal/base/tasks/__init__.py @@ -64,6 +64,12 @@ 'regression': 24 * 60 * 60, } + +def get_task_duration(command): + """Gets the duration of a task.""" + return TASK_LEASE_SECONDS_BY_COMMAND.get(command, TASK_LEASE_SECONDS) + + TASK_QUEUE_DISPLAY_NAMES = { 'LINUX': 'Linux', 'LINUX_WITH_GPU': 'Linux (with GPU)', @@ -548,9 +554,9 @@ def lease(self, _event=None): # pylint: disable=arguments-differ finally: _event.set() leaser_thread.join() - + # If we get here the task succeeded in running. Acknowledge the message. - self._pubsub_message.ack() + self._pubsub_message.ack() track_task_end() def dont_retry(self): diff --git a/src/clusterfuzz/_internal/batch/gcp.py b/src/clusterfuzz/_internal/batch/gcp.py deleted file mode 100644 index 9b155cce7b5..00000000000 --- a/src/clusterfuzz/_internal/batch/gcp.py +++ /dev/null @@ -1,200 +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. -"""GCP Cloud Batch helpers. - -This module provides a client for interacting with the GCP Batch service. It is -used to run granular tasks that require a high degree of isolation, such as -executing untrusted code from fuzzing jobs. Each task is run in its own VM, -ensuring that any potential security issues are contained. -""" -import threading -from typing import List -from typing import Tuple -import uuid - -from google.cloud import batch_v1 as batch - -from clusterfuzz._internal.base import retry -from clusterfuzz._internal.google_cloud_utils import credentials -from clusterfuzz._internal.metrics import logs -from clusterfuzz._internal.remote_task import RemoteTaskInterface - -_local = threading.local() - -DEFAULT_RETRY_COUNT = 0 - -# Controls how many containers (ClusterFuzz tasks) can run on a single VM. -# THIS SHOULD BE 1 OR THERE WILL BE SECURITY PROBLEMS. -TASK_COUNT_PER_NODE = 1 - -# See https://cloud.google.com/batch/quotas#job_limits -MAX_CONCURRENT_VMS_PER_JOB = 1000 - - -def _create_batch_client_new(): - """Creates a batch client.""" - creds, _ = credentials.get_default() - return batch.BatchServiceClient(credentials=creds) - - -def _batch_client(): - """Gets the batch client, creating it if it does not exist.""" - if hasattr(_local, 'client'): - return _local.client - - _local.client = _create_batch_client_new() - return _local.client - - -def get_job_name(): - return 'j-' + str(uuid.uuid4()).lower() - - -def _get_task_spec(batch_workload_spec): - """Gets the task spec based on the batch workload spec.""" - runnable = batch.Runnable() - runnable.container = batch.Runnable.Container() - runnable.container.image_uri = batch_workload_spec.docker_image - clusterfuzz_release = batch_workload_spec.clusterfuzz_release - runnable.container.options = ( - '--memory-swappiness=40 --shm-size=1.9g --rm --net=host ' - '-e HOST_UID=1337 -P --privileged --cap-add=all ' - f'-e CLUSTERFUZZ_RELEASE={clusterfuzz_release} ' - '--name=clusterfuzz -e UNTRUSTED_WORKER=False -e UWORKER=True ' - '-e USE_GCLOUD_STORAGE_RSYNC=1 ' - '-e UWORKER_INPUT_DOWNLOAD_URL') - runnable.container.volumes = ['/var/scratch0:/mnt/scratch0'] - task_spec = batch.TaskSpec() - task_spec.runnables = [runnable] - if batch_workload_spec.retry: - # Tasks in general have 6 hours to run (except pruning which has 24). - # Our signed URLs last 24 hours. Therefore, the maxiumum number of retries - # is 4. This is a temporary solution anyway. - task_spec.max_retry_count = 4 - else: - task_spec.max_retry_count = DEFAULT_RETRY_COUNT - task_spec.max_run_duration = batch_workload_spec.max_run_duration - return task_spec - - -def _set_preemptible(instance_policy, batch_workload_spec) -> None: - if batch_workload_spec.preemptible: - instance_policy.provisioning_model = ( - batch.AllocationPolicy.ProvisioningModel.PREEMPTIBLE) - else: - instance_policy.provisioning_model = ( - batch.AllocationPolicy.ProvisioningModel.STANDARD) - - -def _get_allocation_policy(spec): - """Returns the allocation policy for a BatchWorkloadSpec.""" - disk = batch.AllocationPolicy.Disk() - disk.image = 'batch-cos' - disk.size_gb = spec.disk_size_gb - disk.type = spec.disk_type - instance_policy = batch.AllocationPolicy.InstancePolicy() - instance_policy.boot_disk = disk - instance_policy.machine_type = spec.machine_type - _set_preemptible(instance_policy, spec) - instances = batch.AllocationPolicy.InstancePolicyOrTemplate() - instances.policy = instance_policy - - # Don't use external ip addresses which use quota, cost money, and are - # unnecessary. - network_interface = batch.AllocationPolicy.NetworkInterface() - network_interface.no_external_ip_address = True - network_interface.network = spec.network - network_interface.subnetwork = spec.subnetwork - network_interfaces = [network_interface] - network_policy = batch.AllocationPolicy.NetworkPolicy() - network_policy.network_interfaces = network_interfaces - - allocation_policy = batch.AllocationPolicy() - allocation_policy.instances = [instances] - allocation_policy.network = network_policy - service_account = batch.ServiceAccount(email=spec.service_account_email) # pylint: disable=no-member - allocation_policy.service_account = service_account - return allocation_policy - - -@retry.wrap( - retries=3, - delay=2, - function='google_cloud_utils.batch._send_create_job_request') -def _send_create_job_request(create_request): - return _batch_client().create_job(create_request) - - -def count_queued_or_scheduled_tasks(project: str, - region: str) -> Tuple[int, int]: - """Counts the number of queued and scheduled tasks.""" - region = f'projects/{project}/locations/{region}' - jobs_filter = 'Status.State="SCHEDULED" OR Status.State="QUEUED"' - req = batch.types.ListJobsRequest(parent=region, filter=jobs_filter) - queued = 0 - scheduled = 0 - for job in _batch_client().list_jobs(request=req): - if job.status.state == batch.JobStatus.State.SCHEDULED: - scheduled += job.task_groups[0].task_count - elif job.status.state == batch.JobStatus.State.QUEUED: - queued += job.task_groups[0].task_count - return (queued, scheduled) - - -class GcpBatchClient(RemoteTaskInterface): - """A client for creating and managing jobs on the GCP Batch service. - - This client is responsible for translating ClusterFuzz task specifications - into GCP Batch jobs. It handles the configuration of the job, including - the machine type, disk size, and network settings, as well as the task - specification, which defines the container image and command to run. - """ - - def create_job(self, spec, input_urls: List[str]): - """Creates and starts a batch job from |spec| that executes all tasks. - - This method creates a new GCP Batch job with a single task group. The - task group is configured to run a containerized task for each of the - input URLs. The tasks are run in parallel, with each task having its - own VM, as defined by the TASK_COUNT_PER_NODE setting. - """ - task_group = batch.TaskGroup() - task_group.task_count = len(input_urls) - assert task_group.task_count < MAX_CONCURRENT_VMS_PER_JOB - task_environments = [ - batch.Environment(variables={'UWORKER_INPUT_DOWNLOAD_URL': input_url}) - for input_url in input_urls - ] - task_group.task_environments = task_environments - task_group.task_spec = _get_task_spec(spec) - task_group.task_count_per_node = TASK_COUNT_PER_NODE - assert task_group.task_count_per_node == 1, 'This is a security issue' - - job = batch.Job() - job.task_groups = [task_group] - job.allocation_policy = _get_allocation_policy(spec) - job.logs_policy = batch.LogsPolicy() - job.logs_policy.destination = batch.LogsPolicy.Destination.CLOUD_LOGGING - job.priority = spec.priority - - create_request = batch.CreateJobRequest() - create_request.job = job - job_name = get_job_name() - create_request.job_id = job_name - # The job's parent is the region in which the job will run - project_id = spec.project - create_request.parent = f'projects/{project_id}/locations/{spec.gce_region}' - job_result = _send_create_job_request(create_request) - logs.info(f'Created batch job id={job_name}.', spec=spec) - return job_result diff --git a/src/clusterfuzz/_internal/batch/service.py b/src/clusterfuzz/_internal/batch/service.py index be16cb6da31..dd853c1fcf5 100644 --- a/src/clusterfuzz/_internal/batch/service.py +++ b/src/clusterfuzz/_internal/batch/service.py @@ -18,24 +18,172 @@ and provides a simple interface for scheduling ClusterFuzz tasks. """ import collections +import threading from typing import Dict from typing import List +from typing import Tuple +import uuid +from google.cloud import batch_v1 as batch + +from clusterfuzz._internal.base import retry from clusterfuzz._internal.base import tasks from clusterfuzz._internal.base import utils from clusterfuzz._internal.base.tasks import task_utils -from clusterfuzz._internal.batch.data_structures import BatchTask -from clusterfuzz._internal.batch.data_structures import BatchWorkloadSpec -from clusterfuzz._internal.batch.gcp import GcpBatchClient from clusterfuzz._internal.config import local_config from clusterfuzz._internal.datastore import data_types from clusterfuzz._internal.datastore import ndb_utils +from clusterfuzz._internal.google_cloud_utils import credentials from clusterfuzz._internal.metrics import logs +from clusterfuzz._internal.remote_task import remote_task_types from clusterfuzz._internal.system import environment +# A named tuple that defines the execution environment for a batch workload. +# This includes details about the machine, disk, network, and container image, +# as well as ClusterFuzz-specific settings. +BatchWorkloadSpec = collections.namedtuple('BatchWorkloadSpec', [ + 'clusterfuzz_release', + 'disk_size_gb', + 'disk_type', + 'docker_image', + 'user_data', + 'service_account_email', + 'subnetwork', + 'preemptible', + 'project', + 'machine_type', + 'network', + 'gce_region', + 'priority', + 'max_run_duration', + 'retry', +]) + +WeightedSubconfig = collections.namedtuple('WeightedSubconfig', + ['name', 'weight']) + # See https://cloud.google.com/batch/quotas#job_limits MAX_CONCURRENT_VMS_PER_JOB = 1000 +_local = threading.local() + +DEFAULT_RETRY_COUNT = 0 + +# Controls how many containers (ClusterFuzz tasks) can run on a single VM. +# THIS SHOULD BE 1 OR THERE WILL BE SECURITY PROBLEMS. +TASK_COUNT_PER_NODE = 1 + + +def _create_batch_client_new(): + """Creates a batch client.""" + creds, _ = credentials.get_default() + return batch.BatchServiceClient(credentials=creds) + + +def _batch_client(): + """Gets the batch client, creating it if it does not exist.""" + if hasattr(_local, 'client'): + return _local.client + + _local.client = _create_batch_client_new() + return _local.client + + +def get_job_name(): + return 'j-' + str(uuid.uuid4()).lower() + + +def _get_task_spec(batch_workload_spec): + """Gets the task spec based on the batch workload spec.""" + runnable = batch.Runnable() + runnable.container = batch.Runnable.Container() + runnable.container.image_uri = batch_workload_spec.docker_image + clusterfuzz_release = batch_workload_spec.clusterfuzz_release + runnable.container.options = ( + '--memory-swappiness=40 --shm-size=1.9g --rm --net=host ' + '-e HOST_UID=1337 -P --privileged --cap-add=all ' + f'-e CLUSTERFUZZ_RELEASE={clusterfuzz_release} ' + '--name=clusterfuzz -e UNTRUSTED_WORKER=False -e UWORKER=True ' + '-e USE_GCLOUD_STORAGE_RSYNC=1 ' + '-e UWORKER_INPUT_DOWNLOAD_URL') + runnable.container.volumes = ['/var/scratch0:/mnt/scratch0'] + task_spec = batch.TaskSpec() + task_spec.runnables = [runnable] + if batch_workload_spec.retry: + # Tasks in general have 6 hours to run (except pruning which has 24). + # Our signed URLs last 24 hours. Therefore, the maxiumum number of retries + # is 4. This is a temporary solution anyway. + task_spec.max_retry_count = 4 + else: + task_spec.max_retry_count = DEFAULT_RETRY_COUNT + task_spec.max_run_duration = batch_workload_spec.max_run_duration + return task_spec + + +def _set_preemptible(instance_policy, batch_workload_spec) -> None: + if batch_workload_spec.preemptible: + instance_policy.provisioning_model = ( + batch.AllocationPolicy.ProvisioningModel.PREEMPTIBLE) + else: + instance_policy.provisioning_model = ( + batch.AllocationPolicy.ProvisioningModel.STANDARD) + + +def _get_allocation_policy(spec): + """Returns the allocation policy for a BatchWorkloadSpec.""" + disk = batch.AllocationPolicy.Disk() + disk.image = 'batch-cos' + disk.size_gb = spec.disk_size_gb + disk.type = spec.disk_type + instance_policy = batch.AllocationPolicy.InstancePolicy() + instance_policy.boot_disk = disk + instance_policy.machine_type = spec.machine_type + _set_preemptible(instance_policy, spec) + instances = batch.AllocationPolicy.InstancePolicyOrTemplate() + instances.policy = instance_policy + + # Don't use external ip addresses which use quota, cost money, and are + # unnecessary. + network_interface = batch.AllocationPolicy.NetworkInterface() + network_interface.no_external_ip_address = True + network_interface.network = spec.network + network_interface.subnetwork = spec.subnetwork + network_interfaces = [network_interface] + network_policy = batch.AllocationPolicy.NetworkPolicy() + network_policy.network_interfaces = network_interfaces + + allocation_policy = batch.AllocationPolicy() + allocation_policy.instances = [instances] + allocation_policy.network = network_policy + service_account = batch.ServiceAccount(email=spec.service_account_email) # pylint: disable=no-member + allocation_policy.service_account = service_account + return allocation_policy + + +@retry.wrap( + retries=3, + delay=2, + function='google_cloud_utils.batch._send_create_job_request') +def _send_create_job_request(create_request): + return _batch_client().create_job(create_request) + + +def count_queued_or_scheduled_tasks(project: str, + region: str) -> Tuple[int, int]: + """Counts the number of queued and scheduled tasks.""" + region = f'projects/{project}/locations/{region}' + jobs_filter = 'Status.State="SCHEDULED" OR Status.State="QUEUED"' + req = batch.remote_task_types.ListJobsRequest( + parent=region, filter=jobs_filter) + queued = 0 + scheduled = 0 + for job in _batch_client().list_jobs(request=req): + if job.status.state == batch.JobStatus.State.SCHEDULED: + scheduled += job.task_groups[0].task_count + elif job.status.state == batch.JobStatus.State.QUEUED: + queued += job.task_groups[0].task_count + return (queued, scheduled) + def _get_batch_config(): """Returns the batch config. This function was made to make mocking easier.""" @@ -49,13 +197,14 @@ def is_remote_task(command: str, job_name: str) -> bool: be found for the given command and job type. """ try: - _get_specs_from_config([BatchTask(command, job_name, None)]) + _get_specs_from_config( + [remote_task_types.RemoteTask(command, job_name, None)]) return True except ValueError: return False -def _get_config_names(batch_tasks: List[BatchTask]): +def _get_config_names(batch_tasks: List[remote_task_types.RemoteTask]): """"Gets the name of the configs for each batch_task. Returns a dict that is indexed by command and job_type for efficient lookup.""" job_names = {task.job_type for task in batch_tasks} @@ -65,7 +214,7 @@ def _get_config_names(batch_tasks: List[BatchTask]): config_map = {} for task in batch_tasks: if task.job_type not in job_map: - logs.error(f"{task.job_type} doesn't exist.") + logs.error(f'{task.job_type} doesn\'t exist.') continue if task.command == 'fuzz': suffix = '-PREEMPTIBLE-UNPRIVILEGED' @@ -93,15 +242,6 @@ def _get_config_names(batch_tasks: List[BatchTask]): return config_map -def _get_task_duration(command): - return tasks.TASK_LEASE_SECONDS_BY_COMMAND.get(command, - tasks.TASK_LEASE_SECONDS) - - -WeightedSubconfig = collections.namedtuple('WeightedSubconfig', - ['name', 'weight']) - - def _get_subconfig(batch_config, instance_spec): # TODO(metzman): Make this pick one at random or based on conditions. all_subconfigs = batch_config.get('subconfigs', {}) @@ -114,7 +254,8 @@ def _get_subconfig(batch_config, instance_spec): return all_subconfigs[weighted_subconfig.name] -def _get_specs_from_config(batch_tasks) -> Dict: +def _get_specs_from_config( + batch_tasks: List[remote_task_types.RemoteTask]) -> Dict: """Gets the configured specifications for a batch workload.""" if not batch_tasks: return {} @@ -148,7 +289,7 @@ def _get_specs_from_config(batch_tasks) -> Dict: # Lower numbers are a lower priority, meaning less likely to run From: # https://cloud.google.com/batch/docs/reference/rest/v1/projects.locations.jobs priority = 0 if task.command == 'fuzz' else 1 - max_run_duration = f'{_get_task_duration(task.command)}s' + max_run_duration = f'{tasks.get_task_duration(task.command)}s' # This saves us time and reduces fragementation, e.g. every linux fuzz task # run in this call will run in the same zone. if config_name not in subconfig_map: @@ -182,7 +323,7 @@ def _get_specs_from_config(batch_tasks) -> Dict: return specs -class BatchService: +class GcpBatchService(remote_task_types.RemoteTaskInterface): """A high-level service for creating and managing remote tasks. This service provides a simple interface for scheduling ClusterFuzz tasks on @@ -190,20 +331,58 @@ class BatchService: provides a way to check if a task is configured to run remotely. """ - def __init__(self): - self._client = GcpBatchClient() - - def create_uworker_main_batch_job(self, module: str, job_type: str, - input_download_url: str): + def create_job(self, spec: BatchWorkloadSpec, input_urls: List[str]): + """Creates and starts a batch job from |spec| that executes all tasks. + + This method creates a new GCP Batch job with a single task group. The + task group is configured to run a containerized task for each of the + input URLs. The tasks are run in parallel, with each task having its + own VM, as defined by the TASK_COUNT_PER_NODE setting. + """ + task_group = batch.TaskGroup() + task_group.task_count = len(input_urls) + assert task_group.task_count < MAX_CONCURRENT_VMS_PER_JOB + task_environments = [ + batch.Environment(variables={'UWORKER_INPUT_DOWNLOAD_URL': input_url}) + for input_url in input_urls + ] + task_group.task_environments = task_environments + task_group.task_spec = _get_task_spec(spec) + task_group.task_count_per_node = TASK_COUNT_PER_NODE + assert task_group.task_count_per_node == 1, 'This is a security issue' + + job = batch.Job() + job.task_groups = [task_group] + job.allocation_policy = _get_allocation_policy(spec) + job.logs_policy = batch.LogsPolicy() + job.logs_policy.destination = batch.LogsPolicy.Destination.CLOUD_LOGGING + job.priority = spec.priority + + create_request = batch.CreateJobRequest() + create_request.job = job + job_name = get_job_name() + create_request.job_id = job_name + # The job's parent is the region in which the job will run + project_id = spec.project + create_request.parent = f'projects/{project_id}/locations/{spec.gce_region}' + job_result = _send_create_job_request(create_request) + logs.info(f'Created batch job id={job_name}.', spec=spec) + return job_result + + def create_utask_main_job(self, module: str, job_type: str, + input_download_url: str): """Creates a single batch job for a uworker main task.""" command = task_utils.get_command_from_module(module) - batch_tasks = [BatchTask(command, job_type, input_download_url)] - result = self.create_uworker_main_batch_jobs(batch_tasks) + batch_tasks = [ + remote_task_types.RemoteTask(command, job_type, input_download_url) + ] + result = self.create_utask_main_jobs(batch_tasks) if result is None: return result return result[0] - def create_uworker_main_batch_jobs(self, batch_tasks: List[BatchTask]): + def create_utask_main_jobs(self, + remote_tasks: List[remote_task_types.RemoteTask]): """Creates a batch job for a list of uworker main tasks. This method groups the tasks by their workload specification and creates a @@ -211,11 +390,11 @@ def create_uworker_main_batch_jobs(self, batch_tasks: List[BatchTask]): requirements to be processed together, which can improve efficiency. """ job_specs = collections.defaultdict(list) - specs = _get_specs_from_config(batch_tasks) - for batch_task in batch_tasks: - logs.info(f'Scheduling {batch_task.command}, {batch_task.job_type}.') - spec = specs[(batch_task.command, batch_task.job_type)] - job_specs[spec].append(batch_task.input_download_url) + specs = _get_specs_from_config(remote_tasks) + for remote_task in remote_tasks: + logs.info(f'Scheduling {remote_task.command}, {remote_task.job_type}.') + spec = specs[(remote_task.command, remote_task.job_type)] + job_specs[spec].append(remote_task.input_download_url) logs.info('Creating batch jobs.') jobs = [] @@ -224,6 +403,6 @@ def create_uworker_main_batch_jobs(self, batch_tasks: List[BatchTask]): for spec, input_urls in job_specs.items(): for input_urls_portion in utils.batched(input_urls, MAX_CONCURRENT_VMS_PER_JOB - 1): - jobs.append(self._client.create_job(spec, input_urls_portion)) + jobs.append(self.create_job(spec, input_urls_portion).name) return jobs diff --git a/src/clusterfuzz/_internal/tests/core/google_cloud_utils/use_batch.py b/src/clusterfuzz/_internal/batch/use_batch.py similarity index 97% rename from src/clusterfuzz/_internal/tests/core/google_cloud_utils/use_batch.py rename to src/clusterfuzz/_internal/batch/use_batch.py index 783f04a772b..12585a7fb41 100644 --- a/src/clusterfuzz/_internal/tests/core/google_cloud_utils/use_batch.py +++ b/src/clusterfuzz/_internal/batch/use_batch.py @@ -43,4 +43,4 @@ def _send_test_job(_=None, get_config_directory=None, get_job=None): batch.BatchTask('variant', 'libfuzzer_chrome_asan', 'https://fake/') for _ in range(10) ] - batch.create_uworker_main_batch_jobs(tasks) + batch.create_utask_main_jobs(tasks) diff --git a/src/clusterfuzz/_internal/cron/schedule_fuzz.py b/src/clusterfuzz/_internal/cron/schedule_fuzz.py index 0a296970727..d003b47ee6a 100644 --- a/src/clusterfuzz/_internal/cron/schedule_fuzz.py +++ b/src/clusterfuzz/_internal/cron/schedule_fuzz.py @@ -25,7 +25,7 @@ from clusterfuzz._internal.base import tasks from clusterfuzz._internal.base import utils -from clusterfuzz._internal.batch import gcp as batch +from clusterfuzz._internal.batch import service as batch from clusterfuzz._internal.config import local_config from clusterfuzz._internal.datastore import data_types from clusterfuzz._internal.datastore import ndb_utils diff --git a/src/clusterfuzz/_internal/datastore/data_types.py b/src/clusterfuzz/_internal/datastore/data_types.py index 4e5af47dc24..e15966b61bf 100644 --- a/src/clusterfuzz/_internal/datastore/data_types.py +++ b/src/clusterfuzz/_internal/datastore/data_types.py @@ -1803,3 +1803,11 @@ class FuzzerTaskEvent(Model): def _pre_put_hook(self): self.ttl_expiry_timestamp = ( datetime.datetime.now() + self.FUZZER_EVENT_TTL) + + +class FeatureFlag(Model): + """Feature flag.""" + description = ndb.StringProperty(default='') + enabled = ndb.BooleanProperty(default=False) + value = ndb.FloatProperty() + string_value = ndb.StringProperty() diff --git a/src/clusterfuzz/_internal/datastore/feature_flags.py b/src/clusterfuzz/_internal/datastore/feature_flags.py new file mode 100644 index 00000000000..07c0dcff878 --- /dev/null +++ b/src/clusterfuzz/_internal/datastore/feature_flags.py @@ -0,0 +1,69 @@ +# Copyright 2026 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. +"""FeatureFlags.""" + +from enum import Enum + +from google.cloud import ndb + +from clusterfuzz._internal.datastore import data_types + + +class FeatureFlags(Enum): + """Feature flags""" + # Example flag. + TEST_FLAG = 'test_flag' + TEST_FLOAT_FLAG = 'test_float_flag' + + K8S_JOBS_FREQUENCY = 'k8s_jobs_frequency' + + @property + def flag(self): + """Get the feature flag.""" + flag = ndb.Key(data_types.FeatureFlag, self.value).get() + if not flag: + return None + return flag + + @property + def enabled(self): + """Check if a feature flag is enabled.""" + flag = self.flag + if not flag: + return False + return flag.enabled + + @property + def content(self): + """Get the feature flag content.""" + flag = self.flag + if not flag or flag.value is None: + return None + return flag.value + + @property + def description(self): + """Get the feature flag description.""" + flag = self.flag + if not flag or flag.description is None: + return '' + return flag.description + + @property + def string_value(self): + """Get the feature flag string value.""" + flag = self.flag + if not flag or flag.string_value is None: + return '' + return flag.string_value diff --git a/src/clusterfuzz/_internal/datastore/ndb_init.py b/src/clusterfuzz/_internal/datastore/ndb_init.py index 30840c44362..c91e5c45bad 100644 --- a/src/clusterfuzz/_internal/datastore/ndb_init.py +++ b/src/clusterfuzz/_internal/datastore/ndb_init.py @@ -51,16 +51,21 @@ def context(): # multiprocessing.set_start_method to not fork. context_module._state.context = None # pylint: disable=protected-access - with _client().context() as ndb_context: - # Disable NDB caching, as NDB on GCE VMs do not use memcache and therefore - # can't invalidate the memcache cache. - ndb_context.set_memcache_policy(False) + current_context = context_module.get_context(False) + if current_context: + yield current_context - # Disable the in-context cache, as it can use up a lot of memory for - # longer running tasks such as cron jobs. - ndb_context.set_cache_policy(False) + else: + with _client().context() as ndb_context: + # Disable NDB caching, as NDB on GCE VMs do not use memcache and therefore + # can't invalidate the memcache cache. + ndb_context.set_memcache_policy(False) - yield ndb_context + # Disable the in-context cache, as it can use up a lot of memory for + # longer running tasks such as cron jobs. + ndb_context.set_cache_policy(False) + + yield ndb_context def thread_wrapper(func): diff --git a/src/clusterfuzz/_internal/google_cloud_utils/batch.py b/src/clusterfuzz/_internal/google_cloud_utils/batch.py deleted file mode 100644 index e804044cef8..00000000000 --- a/src/clusterfuzz/_internal/google_cloud_utils/batch.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright 2023 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. -"""Cloud Batch helpers.""" -from typing import List - -from clusterfuzz._internal.batch.data_structures import BatchTask -from clusterfuzz._internal.batch.service import BatchService - - -def create_uworker_main_batch_job(module, job_type, input_download_url): - """Creates a batch job.""" - service = BatchService() - return service.create_uworker_main_batch_job(module, job_type, - input_download_url) - - -def create_uworker_main_batch_jobs(batch_tasks: List[BatchTask]): - """Creates batch jobs.""" - service = BatchService() - return service.create_uworker_main_batch_jobs(batch_tasks) diff --git a/src/clusterfuzz/_internal/k8s/__init__.py b/src/clusterfuzz/_internal/k8s/__init__.py new file mode 100644 index 00000000000..f179fd0a5b0 --- /dev/null +++ b/src/clusterfuzz/_internal/k8s/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2026 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. +"""Kubernetes service.""" diff --git a/src/clusterfuzz/_internal/k8s/job_template.yaml b/src/clusterfuzz/_internal/k8s/job_template.yaml new file mode 100644 index 00000000000..5d54de49df0 --- /dev/null +++ b/src/clusterfuzz/_internal/k8s/job_template.yaml @@ -0,0 +1,93 @@ +# Copyright 2026 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. +apiVersion: batch/v1 +kind: Job +metadata: + name: "{{job_name}}" + labels: + task_name: "{{task_name}}" + job_name: "{{clusterfuzz_job_name}}" +spec: + ttlSecondsAfterFinished: 100 + activeDeadlineSeconds: {{active_deadline_seconds}} + template: + metadata: + labels: + task_name: "{{task_name}}" + job_name: "{{clusterfuzz_job_name}}" + {% if is_kata %} + app.kubernetes.io/name: clusterfuzz-kata-job + {% endif %} + spec: + {% if is_kata %} + runtimeClassName: kata + {% endif %} + dnsPolicy: ClusterFirstWithHostNet + serviceAccountName: "{{service_account_name}}" + containers: + - name: "{{job_name}}" + image: "{{docker_image}}" + imagePullPolicy: IfNotPresent + lifecycle: + postStart: + exec: + command: + - /bin/sh + - -c + - mkdir -p /tmp/.X11-unix && chmod 1777 /tmp/.X11-unix + resources: + requests: + cpu: '2' + memory: 3.75Gi + limits: + cpu: '2' + memory: 3.75Gi + env: + - name: HOST_UID + value: '1337' + - name: CLUSTERFUZZ_RELEASE + value: "{{clusterfuzz_release}}" + - name: UNTRUSTED_WORKER + value: 'False' + - name: UWORKER + value: 'True' + - name: USE_GCLOUD_STORAGE_RSYNC + value: '1' + - name: UWORKER_INPUT_DOWNLOAD_URL + value: "{{input_url}}" + - name: IS_K8S_ENV + value: 'true' + - name: DISABLE_MOUNTS + value: 'true' + - name: UPDATE_WEB_TESTS + value: 'False' + securityContext: + privileged: true + capabilities: + add: + - ALL + volumeMounts: + - mountPath: /dev/shm + name: dshm + volumes: + - name: dshm + emptyDir: + medium: Memory + sizeLimit: 1.9Gi + {% if is_kata %} + nodeSelector: + cloud.google.com/gke-nodepool: kata-enabled-pool + {% endif %} + restartPolicy: "{{restart_policy}}" + backoffLimit: 0 \ No newline at end of file diff --git a/src/clusterfuzz/_internal/k8s/service.py b/src/clusterfuzz/_internal/k8s/service.py new file mode 100644 index 00000000000..55445f3c0bb --- /dev/null +++ b/src/clusterfuzz/_internal/k8s/service.py @@ -0,0 +1,310 @@ +# 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. +"""Kubernetes batch client.""" +import base64 +import collections +import os +import tempfile +import typing +import uuid + +import google.auth +from google.auth.transport import requests as google_requests +from googleapiclient import discovery +import jinja2 +from kubernetes import client as k8s_client +import yaml + +from clusterfuzz._internal.base import tasks +from clusterfuzz._internal.base import utils +from clusterfuzz._internal.base.tasks import task_utils +from clusterfuzz._internal.config import local_config +from clusterfuzz._internal.datastore import data_types +from clusterfuzz._internal.datastore import ndb_utils +from clusterfuzz._internal.metrics import logs +from clusterfuzz._internal.remote_task import remote_task_types +from clusterfuzz._internal.system import environment + +CLUSTER_NAME = 'clusterfuzz-cronjobs-gke' + +KubernetesJobConfig = collections.namedtuple('KubernetesJobConfig', [ + 'job_type', + 'docker_image', + 'command', + 'disk_size_gb', + 'service_account_email', + 'clusterfuzz_release', + 'is_kata', +]) + + +def _get_config_names(remote_tasks: typing.List[remote_task_types.RemoteTask]): + """"Gets the name of the configs for each batch_task. Returns a dict + + that is indexed by command and job_type for efficient lookup.""" + + job_names = {task.job_type for task in remote_tasks} + query = data_types.Job.query(data_types.Job.name.IN(list(job_names))) + jobs = ndb_utils.get_all_from_query(query) + job_map = {job.name: job for job in jobs} + config_map = {} + for task in remote_tasks: + if task.job_type not in job_map: + logs.error(f"{task.job_type} doesn't exist.") + continue + if task.command == 'fuzz': + suffix = '-PREEMPTIBLE-UNPRIVILEGED' + else: + suffix = '-NONPREEMPTIBLE-UNPRIVILEGED' + job = job_map[task.job_type] + platform = job.platform if not utils.is_oss_fuzz() else 'LINUX' + disk_size_gb = environment.get_value( + 'DISK_SIZE_GB', env=job.get_environment()) + # Get the OS version from the job, this is the least specific version. + base_os_version = job.base_os_version + # If we are running in the oss-fuzz context, the project-specific config + # is more specific and overrides the job-level one. + if utils.is_oss_fuzz(): + oss_fuzz_project = data_types.OssFuzzProject.query( + data_types.OssFuzzProject.name == job.project).get() + if oss_fuzz_project and oss_fuzz_project.base_os_version: + base_os_version = oss_fuzz_project.base_os_version + config_map[(task.command, task.job_type)] = (f'{platform}{suffix}', + disk_size_gb, base_os_version) + + return config_map + + +def _get_k8s_job_configs( + remote_tasks: typing.List[remote_task_types.RemoteTask] +) -> typing.Dict[typing.Tuple[str, str], KubernetesJobConfig]: + """Gets the configured specifications for a batch workload.""" + + if not remote_tasks: + return {} + # TODO(javanlacerda): Create remote task config + batch_config = local_config.BatchConfig() + config_map = _get_config_names(remote_tasks) + configs = {} + for task in remote_tasks: + if (task.command, task.job_type) in configs: + # Don't repeat work for no reason. + continue + config_name, disk_size_gb, base_os_version = config_map[(task.command, + task.job_type)] + instance_spec = batch_config.get('mapping').get(config_name) + if instance_spec is None: + raise ValueError(f'No mapping for {config_name}') + # Decide which docker image to use. + versioned_images_map = instance_spec.get('versioned_docker_images') + if (base_os_version and versioned_images_map and + base_os_version in versioned_images_map): + # New path: Use the versioned image if specified and available. + docker_image_uri = versioned_images_map[base_os_version] + else: + # Fallback/legacy path: Use the original docker_image key. + docker_image_uri = instance_spec['docker_image'] + disk_size_gb = (disk_size_gb or instance_spec['disk_size_gb']) + clusterfuzz_release = instance_spec.get('clusterfuzz_release', 'prod') + config = KubernetesJobConfig( + job_type=task.job_type, + docker_image=docker_image_uri, + command=task.command, + disk_size_gb=disk_size_gb, + service_account_email=instance_spec['service_account_email'], + clusterfuzz_release=clusterfuzz_release, + is_kata=instance_spec.get('is_kata', True), + ) + configs[(task.command, task.job_type)] = config + + return configs + + +def _create_job_body(config: KubernetesJobConfig, input_url: str, + service_account_name: str) -> dict: + """Creates the body of a Kubernetes job.""" + + job_name = f'cf-job-{str(uuid.uuid4())}' + job_name = job_name.lower() + + # Set up Jinja2 environment and load the template. + template_dir = os.path.dirname(__file__) + jinja_env = jinja2.Environment(loader=jinja2.FileSystemLoader(template_dir)) + template = jinja_env.get_template('job_template.yaml') + + # Define the context with all the dynamic values. + context = { + 'job_name': job_name, + 'active_deadline_seconds': tasks.get_task_duration(config.command), + 'service_account_name': service_account_name, + 'docker_image': config.docker_image, + 'clusterfuzz_release': config.clusterfuzz_release, + 'input_url': input_url, + 'is_kata': config.is_kata, + 'task_name': config.command, + 'clusterfuzz_job_name': config.job_type, + 'restart_policy': 'Never' if config.command == 'fuzz' else 'OnFailure' + } + + # Render the template and load as YAML. + rendered_spec = template.render(context) + return yaml.safe_load(rendered_spec) + + +class KubernetesService(remote_task_types.RemoteTaskInterface): + """A remote task execution client for Kubernetes.""" + + def __init__(self, k8s_config_loaded: bool = False): + # In e2e tests, the kubeconfig is already loaded by the test setup. + if not k8s_config_loaded: + self._load_gke_credentials() + + self._core_api = k8s_client.CoreV1Api() + self._batch_api = k8s_client.BatchV1Api() + + def _load_gke_credentials(self): + """Loads GKE credentials and configures the Kubernetes client.""" + credentials, _ = google.auth.default() + project = utils.get_application_id() + service = discovery.build('container', 'v1', credentials=credentials) + parent = f"projects/{project}/locations/-" + + try: + # pylint: disable=no-member + response = service.projects().locations().clusters().list( + parent=parent).execute() + clusters = response.get('clusters', []) + cluster = next((c for c in clusters if c['name'] == CLUSTER_NAME), None) + + if not cluster: + logs.error(f"Cluster {CLUSTER_NAME} not found in project {project}.") + print(f"DEBUG: Cluster {CLUSTER_NAME} not found in project {project}.") + return + + except Exception as e: + logs.error(f"Failed to list clusters in {project}: {e}") + return + + endpoint = cluster['endpoint'] + # ca_cert is base64 encoded. + ca_cert = base64.b64decode(cluster['masterAuth']['clusterCaCertificate']) + + # Write CA cert to a temporary file. + fd, ca_cert_path = tempfile.mkstemp() + with os.fdopen(fd, 'wb') as f: + f.write(ca_cert) + + configuration = k8s_client.Configuration() + configuration.host = f'https://{endpoint}' + configuration.ssl_ca_cert = ca_cert_path + configuration.verify_ssl = True + + def get_token(creds): + request = google_requests.Request() + if not creds.valid or creds.expired: + creds.refresh(request) + return {"authorization": "Bearer " + creds.token} + + configuration.refresh_api_key_hook = lambda _: get_token(credentials) + configuration.api_key = get_token(credentials) + + k8s_client.Configuration.set_default(configuration) + logs.info("GKE credentials loaded successfully.") + + def _create_service_account_if_needed(self, + service_account_email: str) -> str: + """Creates a Kubernetes Service Account if it doesn't exist.""" + service_account_name = service_account_email.split('@')[0] + namespace = 'default' + try: + self._core_api.read_namespaced_service_account(service_account_name, + namespace) + return service_account_name + except k8s_client.rest.ApiException as e: + if e.status != 404: + raise + + logs.info(f'Creating Service Account {service_account_name} for ' + f'{service_account_email}.') + metadata = k8s_client.V1ObjectMeta( + name=service_account_name, + annotations={'iam.gke.io/gcp-service-account': service_account_email}) + body = k8s_client.V1ServiceAccount(metadata=metadata) + self._core_api.create_namespaced_service_account(namespace, body) + return service_account_name + + def create_job(self, config: KubernetesJobConfig, input_url: str) -> str: + """Creates a Kubernetes job. + Args: + config: The Kubernetes job configuration. + input_url: The URL to be passed as an environment variable to the + job's container. + Returns: + The name of the created Kubernetes job. + """ + service_account_name = self._create_service_account_if_needed( + config.service_account_email) + job_body = _create_job_body(config, input_url, service_account_name) + self._batch_api.create_namespaced_job(body=job_body, namespace='default') + return job_body['metadata']['name'] + + def _get_pending_jobs_count(self) -> int: + """Returns the number of pending jobs.""" + try: + pods = self._core_api.list_namespaced_pod( + namespace='default', + label_selector='app.kubernetes.io/name=clusterfuzz-kata-job', + field_selector='status.phase=Pending') + logs.info(f"Found {len(pods.items)} pending jobs.") + return len(pods.items) + except Exception as e: + logs.error(f"Failed to list pods: {e}") + return 0 + + def create_utask_main_job(self, module: str, job_type: str, + input_download_url: str): + """Creates a single batch job for a uworker main task.""" + + command = task_utils.get_command_from_module(module) + batch_tasks = [ + remote_task_types.RemoteTask(command, job_type, input_download_url) + ] + result = self.create_utask_main_jobs(batch_tasks) + + if result is None: + return result + return result[0] + + def create_utask_main_jobs( + self, remote_tasks: typing.List[remote_task_types.RemoteTask]): + """Creates a batch job for a list of uworker main tasks. + + This method groups the tasks by their workload specification and creates a + separate batch job for each group. This allows tasks with similar + requirements to be processed together, which can improve efficiency. + """ + job_specs = collections.defaultdict(list) + configs = _get_k8s_job_configs(remote_tasks) + for remote_task in remote_tasks: + logs.info(f'Scheduling {remote_task.command}, {remote_task.job_type}.') + config = configs[(remote_task.command, remote_task.job_type)] + job_specs[config].append(remote_task.input_download_url) + logs.info('Creating batch jobs.') + jobs = [] + logs.info('Batching utask_mains.') + for config, input_urls in job_specs.items(): + for input_url in input_urls: + jobs.append(self.create_job(config, input_url)) + + return jobs diff --git a/src/clusterfuzz/_internal/platforms/kubernetes/__init__.py b/src/clusterfuzz/_internal/platforms/kubernetes/__init__.py new file mode 100644 index 00000000000..f74b38a1320 --- /dev/null +++ b/src/clusterfuzz/_internal/platforms/kubernetes/__init__.py @@ -0,0 +1,14 @@ +# 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. +"""Kubernetes platform specific code.""" diff --git a/src/clusterfuzz/_internal/remote_task/__init__.py b/src/clusterfuzz/_internal/remote_task/__init__.py index 25a47f021d2..a623b04d045 100644 --- a/src/clusterfuzz/_internal/remote_task/__init__.py +++ b/src/clusterfuzz/_internal/remote_task/__init__.py @@ -11,30 +11,4 @@ # 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. -"""Remote task interface. - -This module defines the interface for a remote task execution client. This -abstraction allows ClusterFuzz to support multiple remote execution -environments, such as GCP Batch and Kubernetes, without tightly coupling -the task creation logic to a specific implementation. -""" -import abc - - -class RemoteTaskInterface(abc.ABC): - """Interface for a remote task execution client. - - This interface defines the contract for a client that can create and manage - remote jobs. Each client is responsible for translating a ClusterFuzz task - specification into a job that can be executed in its target environment. - """ - - @abc.abstractmethod - def create_job(self, spec, input_urls): - """Creates a remote job. - - This method is responsible for creating a new job in the remote execution - environment. It takes a workload specification and a list of input URLs, - and returns a representation of the created job. - """ - raise NotImplementedError +"""Remote task package.""" diff --git a/src/clusterfuzz/_internal/remote_task/remote_task_adapters.py b/src/clusterfuzz/_internal/remote_task/remote_task_adapters.py new file mode 100644 index 00000000000..58ee205ecf9 --- /dev/null +++ b/src/clusterfuzz/_internal/remote_task/remote_task_adapters.py @@ -0,0 +1,44 @@ +# 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. +"""Remote task adapters.""" + +from enum import Enum + +from clusterfuzz._internal.batch import service as batch_service +from clusterfuzz._internal.datastore import feature_flags +from clusterfuzz._internal.k8s import service as k8s_service + + +class RemoteTaskAdapters(Enum): + """Defines the supported remote task execution backends. + + This enum serves as the single source of truth for all supported remote + execution platforms. Each member represents a different backend and holds + the necessary configuration for it. + + Attributes: + id: A unique string identifier for the adapter (e.g., 'kubernetes'). + service: The service class responsible for interacting with the backend. + feature_flag: The feature flag that controls the frequency of this backend. + default_weight: The default frequency if the feature flag is not set. + """ + KUBERNETES = ('kubernetes', k8s_service.KubernetesService, + feature_flags.FeatureFlags.K8S_JOBS_FREQUENCY, 0.0) + GCP_BATCH = ('gcp_batch', batch_service.GcpBatchService, None, 1.0) + + def __init__(self, adapter_id, service, feature_flag, default_weight): + self.id = adapter_id + self.feature_flag = feature_flag + self.default_weight = default_weight + self.service = service diff --git a/src/clusterfuzz/_internal/remote_task/remote_task_gate.py b/src/clusterfuzz/_internal/remote_task/remote_task_gate.py new file mode 100644 index 00000000000..802f2bccfc2 --- /dev/null +++ b/src/clusterfuzz/_internal/remote_task/remote_task_gate.py @@ -0,0 +1,158 @@ +# 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. +"""Remote task interface. + +This module defines the interface for a remote task execution client. This +abstraction allows ClusterFuzz to support multiple remote execution +environments, such as GCP Batch and Kubernetes, without tightly coupling +the task creation logic to a specific implementation. +""" + +import collections +import random +from typing import List + +from clusterfuzz._internal.metrics import logs +from clusterfuzz._internal.remote_task import remote_task_adapters +from clusterfuzz._internal.remote_task import remote_task_types + + +class RemoteTaskGate(remote_task_types.RemoteTaskInterface): + """A generic dispatcher for remote task execution. + + This class acts as a high-level manager that abstracts away the specific + details of the underlying remote execution backends. It uses the frequencies + defined in this module to dynamically choose a backend for each task, + allowing for flexible distribution and A/B testing. + """ + + def __init__(self): + # Instantiate and cache the service clients for each defined adapter. + self._service_map = { + adapter.id: adapter.service() + for adapter in remote_task_adapters.RemoteTaskAdapters + } + self._adapters = remote_task_adapters.RemoteTaskAdapters + + def _get_adapter(self) -> str: + """Performs a weighted random choice to select a remote backend. + + This method is used when creating a single task, ensuring that the + distribution of tasks over time aligns with the configured frequencies. + """ + frequencies = self.get_job_frequency() + population = list(frequencies.keys()) + weights = list(frequencies.values()) + return random.choices(population, weights)[0] + + def get_job_frequency(self): + """Returns the frequency distribution for all remote task adapters. + + This function calculates the proportion of tasks that should be sent to each + remote backend defined in the `RemoteTaskAdapters` enum. The calculation + is based on feature flags, default weights, and ensures the total + distribution sums to 1.0. + + The order of adapters in the enum matters, as this function processes them + sequentially, and any remaining weight to sum to 1.0 is assigned to the + last adapter. + + Returns: + A dictionary mapping each adapter's ID (e.g., 'gcp_batch') to its + calculated frequency (a float between 0.0 and 1.0). + """ + frequencies = {adapter.id: 0.0 for adapter in self._adapters} + total_weight = 0.0 + + for adapter in self._adapters: + default_weight = adapter.default_weight + feature_flag = adapter.feature_flag + weight = default_weight + + # A feature flag can override the default weight for an adapter, allowing + # for dynamic adjustments to task distribution. + if (feature_flag and feature_flag.enabled and + isinstance(feature_flag.content, float)): + feature_flag_weight = feature_flag.content + if 0 <= feature_flag_weight <= 1: + weight = feature_flag_weight + + if total_weight >= 1.0 and weight > 0.0: + logs.warning( + 'Total weight for jobs frequency bigger than 1.0. Adapter starving', + adapter=adapter.id) + break + + # Ensure the cumulative weight does not exceed 1.0. If adding the + # current weight would push the total over, we cap it. + if weight + total_weight > 1.0: + weight = 1.0 - total_weight + + total_weight += weight + frequencies[adapter.id] = weight if weight >= 0 else 0.0 + + logs.info('Job frequencies', frequencies=frequencies) + return frequencies + + def create_utask_main_job(self, module, job_type, input_download_url): + adapter_id = self._get_adapter() + service = self._service_map[adapter_id] + return service.create_utask_main_job(module, job_type, input_download_url) + + def create_utask_main_jobs(self, + remote_tasks: List[remote_task_types.RemoteTask]): + """Creates a batch of remote tasks, distributing them across backends. + + This method handles two cases: + 1. If there is only one task, it uses a weighted random choice to select + a backend, similar to `create_utask_main_job`. + 2. If there are multiple tasks, it distributes them deterministically + across the available backends based on their configured frequencies. + This ensures that a batch of 100 tasks with a 70/30 split sends + exactly 70 tasks to one backend and 30 to the other. + """ + tasks_by_adapter = collections.defaultdict(list) + + if len(remote_tasks) == 1: + # For a single task, use a random distribution. + adapter_id = self._get_adapter() + tasks_by_adapter[adapter_id].extend(remote_tasks) + else: + # For multiple tasks, use deterministic slicing to ensure the + # distribution precisely matches the frequency configuration. + frequencies = self.get_job_frequency() + start_index = 0 + for adapter_id, frequency in frequencies.items(): + count = int(len(remote_tasks) * frequency) + tasks_by_adapter[adapter_id].extend( + remote_tasks[start_index:start_index + count]) + start_index += count + + # Distribute any remainder tasks (due to rounding) one by one. This + # ensures that all tasks are assigned to a backend. + remaining_tasks = remote_tasks[start_index:] + for i, task in enumerate(remaining_tasks): + adapter_id = list(frequencies.keys())[i % len(frequencies)] + tasks_by_adapter[adapter_id].append(task) + + results = [] + for adapter_id, tasks in tasks_by_adapter.items(): + if tasks: + try: + logs.info(f'Sending {len(tasks)} tasks to {adapter_id}.') + service = self._service_map[adapter_id] + results.extend(service.create_utask_main_jobs(tasks)) + except Exception: # pylint: disable=broad-except + logs.error(f'Failed to send {len(tasks)} tasks to {adapter_id}.') + return results diff --git a/src/clusterfuzz/_internal/batch/data_structures.py b/src/clusterfuzz/_internal/remote_task/remote_task_types.py similarity index 51% rename from src/clusterfuzz/_internal/batch/data_structures.py rename to src/clusterfuzz/_internal/remote_task/remote_task_types.py index 24e90670eb2..669829bfa45 100644 --- a/src/clusterfuzz/_internal/batch/data_structures.py +++ b/src/clusterfuzz/_internal/remote_task/remote_task_types.py @@ -11,32 +11,12 @@ # 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. -"""Batch module data structures.""" -import collections - -# A named tuple that defines the execution environment for a batch workload. -# This includes details about the machine, disk, network, and container image, -# as well as ClusterFuzz-specific settings. -BatchWorkloadSpec = collections.namedtuple('BatchWorkloadSpec', [ - 'clusterfuzz_release', - 'disk_size_gb', - 'disk_type', - 'docker_image', - 'user_data', - 'service_account_email', - 'subnetwork', - 'preemptible', - 'project', - 'machine_type', - 'network', - 'gce_region', - 'priority', - 'max_run_duration', - 'retry', -]) - - -class BatchTask: +"""Remote task types.""" + +import abc + + +class RemoteTask: """Represents a single ClusterFuzz task to be executed on a remote worker. This class holds the necessary information to execute a ClusterFuzz command, @@ -44,7 +24,28 @@ class BatchTask: is used to enqueue tasks and track their state. """ - def __init__(self, command, job_type, input_download_url): + def __init__(self, command, job_type, input_download_url, pubsub_task=None): self.command = command self.job_type = job_type self.input_download_url = input_download_url + self.pubsub_task = pubsub_task + + +class RemoteTaskInterface(abc.ABC): + """Interface for a remote task execution client. + + This interface defines the contract for a client that can create and manage + remote jobs. Each client is responsible for translating a ClusterFuzz task + specification into a job that can be executed in its target environment. + """ + + @abc.abstractmethod + def create_utask_main_job(self, module: str, job_type: str, + input_download_url: str): + """Creates a single remote task for a uworker main task.""" + raise NotImplementedError + + @abc.abstractmethod + def create_utask_main_jobs(self, remote_tasks: list[RemoteTask]): + """Creates many remote tasks for uworker main tasks.""" + raise NotImplementedError diff --git a/src/clusterfuzz/_internal/tests/core/batch/service_test.py b/src/clusterfuzz/_internal/tests/core/batch/batch_service_test.py similarity index 86% rename from src/clusterfuzz/_internal/tests/core/batch/service_test.py rename to src/clusterfuzz/_internal/tests/core/batch/batch_service_test.py index 613a32603e6..e75969f27bc 100644 --- a/src/clusterfuzz/_internal/tests/core/batch/service_test.py +++ b/src/clusterfuzz/_internal/tests/core/batch/batch_service_test.py @@ -11,18 +11,18 @@ # 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. -"""Tests for the batch service.""" +"""Tests for the batch batch_service.""" import datetime import unittest from unittest import mock import uuid from google.cloud import batch_v1 as batch +from google.cloud.batch_v1.types import job as gcb_job -from clusterfuzz._internal.batch import data_structures -from clusterfuzz._internal.batch import gcp -from clusterfuzz._internal.batch import service +from clusterfuzz._internal.batch import service as batch_service from clusterfuzz._internal.datastore import data_types +from clusterfuzz._internal.remote_task import remote_task_types from clusterfuzz._internal.tests.test_libs import helpers from clusterfuzz._internal.tests.test_libs import test_utils @@ -50,7 +50,7 @@ def _get_expected_task_spec(batch_workload_spec): if batch_workload_spec.retry: task_spec.max_retry_count = 4 else: - task_spec.max_retry_count = gcp.DEFAULT_RETRY_COUNT + task_spec.max_retry_count = batch_service.DEFAULT_RETRY_COUNT task_spec.max_run_duration = datetime.timedelta( seconds=int(batch_workload_spec.max_run_duration[:-1])) @@ -126,7 +126,7 @@ def _get_expected_create_request(job_name_uuid, spec, input_urls): task_group.task_count = len(input_urls) task_group.task_environments = task_environments task_group.task_spec = task_spec - task_group.task_count_per_node = gcp.TASK_COUNT_PER_NODE + task_group.task_count_per_node = batch_service.TASK_COUNT_PER_NODE job = batch.Job() job.task_groups = [task_group] @@ -143,26 +143,24 @@ def _get_expected_create_request(job_name_uuid, spec, input_urls): @test_utils.with_cloud_emulators('datastore') -class BatchServiceTest(unittest.TestCase): - """Tests for BatchService.""" +class GcpBatchServiceTest(unittest.TestCase): + """Tests for GcpBatchService.""" def setUp(self): helpers.patch(self, [ - 'clusterfuzz._internal.batch.gcp._batch_client', + 'clusterfuzz._internal.batch.service._batch_client', 'clusterfuzz._internal.base.tasks.task_utils.get_command_from_module', 'uuid.uuid4', ]) - self.mock.GcpBatchClient = mock.Mock( - ) # Still need this for BatchService constructor self.mock_batch_client_instance = mock.Mock() self.mock._batch_client.return_value = self.mock_batch_client_instance - self.batch_service = service.BatchService() + self.batch_service = batch_service.GcpBatchService() self.mock.uuid4.side_effect = [uuid.UUID(u) for u in UUIDS] def test_create_uworker_main_batch_jobs(self): - """Tests that create_uworker_main_batch_jobs works as expected.""" + """Tests that create_utask_main_jobs works as expected.""" # Create mock data. - spec1 = service.BatchWorkloadSpec( + spec1 = batch_service.BatchWorkloadSpec( clusterfuzz_release='release1', disk_size_gb=10, disk_type='type1', @@ -178,7 +176,7 @@ def test_create_uworker_main_batch_jobs(self): priority=1, max_run_duration='1s', retry=False) - spec2 = service.BatchWorkloadSpec( + spec2 = batch_service.BatchWorkloadSpec( clusterfuzz_release='release2', disk_size_gb=20, disk_type='type2', @@ -201,13 +199,13 @@ def test_create_uworker_main_batch_jobs(self): ('command2', 'job2'): spec2, } tasks = [ - data_structures.BatchTask('command1', 'job1', 'url1'), - data_structures.BatchTask('command1', 'job1', 'url2'), - data_structures.BatchTask('command2', 'job2', 'url3'), + remote_task_types.RemoteTask('command1', 'job1', 'url1'), + remote_task_types.RemoteTask('command1', 'job1', 'url2'), + remote_task_types.RemoteTask('command2', 'job2', 'url3'), ] # Call the function. - self.batch_service.create_uworker_main_batch_jobs(tasks) + self.batch_service.create_utask_main_jobs(tasks) # Assert that create_job was called with the correct arguments. expected_create_request_1 = _get_expected_create_request( @@ -220,9 +218,9 @@ def test_create_uworker_main_batch_jobs(self): ]) def test_create_uworker_main_batch_job(self): - """Tests that create_uworker_main_batch_job works as expected.""" + """Tests that create_utask_main_job works as expected.""" # Create mock data. - spec1 = service.BatchWorkloadSpec( + spec1 = batch_service.BatchWorkloadSpec( clusterfuzz_release='release1', disk_size_gb=10, disk_type='type1', @@ -243,12 +241,13 @@ def test_create_uworker_main_batch_job(self): mock_get_specs_from_config.return_value = { ('fuzz', 'job1'): spec1, } - self.mock_batch_client_instance.create_job.return_value = 'job' + + self.mock_batch_client_instance.create_job.return_value = gcb_job.Job( + name='job') self.mock.get_command_from_module.return_value = 'fuzz' # Call the function. - result = self.batch_service.create_uworker_main_batch_job( - 'fuzz', 'job1', 'url1') + result = self.batch_service.create_utask_main_job('fuzz', 'job1', 'url1') # Assert that create_job was called with the correct arguments. expected_create_request = _get_expected_create_request( @@ -272,11 +271,11 @@ def test_is_remote_task(self): """Tests that is_remote_task works as expected.""" # Test when it is a remote task. self.mock._get_specs_from_config.return_value = {('fuzz', 'job'): True} - self.assertTrue(service.is_remote_task('fuzz', 'job')) + self.assertTrue(batch_service.is_remote_task('fuzz', 'job')) # Test when it is not a remote task. self.mock._get_specs_from_config.side_effect = ValueError - self.assertFalse(service.is_remote_task('progression', 'job')) + self.assertFalse(batch_service.is_remote_task('progression', 'job')) if __name__ == '__main__': @@ -296,7 +295,7 @@ def setUp(self): helpers.patch(self, [ 'clusterfuzz._internal.base.utils.random_weighted_choice', ]) - self.mock.random_weighted_choice.return_value = service.WeightedSubconfig( + self.mock.random_weighted_choice.return_value = batch_service.WeightedSubconfig( name='east4-network2', weight=1, ) @@ -305,7 +304,7 @@ def test_nonpreemptible(self): """Tests that _get_specs_from_config works for non-preemptibles as expected.""" spec = _get_spec_from_config('analyze', self.job.name) - expected_spec = service.BatchWorkloadSpec( + expected_spec = batch_service.BatchWorkloadSpec( clusterfuzz_release='prod', docker_image='gcr.io/clusterfuzz-images/base:a2f4dd6-202202070654', user_data='file://linux-init.yaml', @@ -331,7 +330,7 @@ def test_fuzz_get_specs_from_config(self): job = data_types.Job(name='libfuzzer_chrome_asan', platform='LINUX') job.put() spec = _get_spec_from_config('fuzz', job.name) - expected_spec = service.BatchWorkloadSpec( + expected_spec = batch_service.BatchWorkloadSpec( clusterfuzz_release='prod', docker_image='gcr.io/clusterfuzz-images/base:a2f4dd6-202202070654', user_data='file://linux-init.yaml', @@ -374,16 +373,16 @@ def test_get_specs_from_config_disk_size(self): platform='LINUX', name='libfuzzer_asan_test').put() - spec = service._get_specs_from_config( - [service.BatchTask('fuzz', 'libfuzzer_asan_test', None)]) + spec = batch_service._get_specs_from_config( + [remote_task_types.RemoteTask('fuzz', 'libfuzzer_asan_test', None)]) self.assertEqual(spec['fuzz', 'libfuzzer_asan_test'].disk_size_gb, size) def test_get_specs_from_config_no_disk_size(self): """Test that disk_size_gb isn't mandatory.""" data_types.Job(platform='LINUX', name='libfuzzer_asan_test').put() - spec = service._get_specs_from_config( - [service.BatchTask('fuzz', 'libfuzzer_asan_test', None)]) - conf = service._get_batch_config() + spec = batch_service._get_specs_from_config( + [remote_task_types.RemoteTask('fuzz', 'libfuzzer_asan_test', None)]) + conf = batch_service._get_batch_config() expected_size = ( conf.get('mapping')['LINUX-PREEMPTIBLE-UNPRIVILEGED']['disk_size_gb']) self.assertEqual(spec['fuzz', 'libfuzzer_asan_test'].disk_size_gb, @@ -406,8 +405,8 @@ def test_get_specs_from_config_with_disk_size_override(self): platform='LINUX', name=job_name).put() - spec = service._get_specs_from_config( - [service.BatchTask('fuzz', job_name, None)]) + spec = batch_service._get_specs_from_config( + [remote_task_types.RemoteTask('fuzz', job_name, None)]) self.assertEqual(spec['fuzz', job_name].disk_size_gb, overridden_size) @mock.patch('clusterfuzz._internal.batch.service.utils.is_oss_fuzz') @@ -422,8 +421,8 @@ def test_get_config_names_os_version(self, mock_get_all_from_query, job1 = data_types.Job( name='job1', platform='LINUX', base_os_version='job-os-ubuntu-20') mock_get_all_from_query.return_value = [job1] - config_map = service._get_config_names( - [service.BatchTask('fuzz', 'job1', None)]) + config_map = batch_service._get_config_names( + [remote_task_types.RemoteTask('fuzz', 'job1', None)]) self.assertEqual(config_map[('fuzz', 'job1')][2], 'job-os-ubuntu-20') # Test Case 2: OSS-Fuzz project, project-level version overrides job-level. @@ -437,24 +436,24 @@ def test_get_config_names_os_version(self, mock_get_all_from_query, name='my-project', base_os_version='project-os-ubuntu-24') mock_get_all_from_query.return_value = [job2] mock_oss_fuzz_project_query.return_value.get.return_value = project - config_map = service._get_config_names( - [service.BatchTask('fuzz', 'job2', None)]) + config_map = batch_service._get_config_names( + [remote_task_types.RemoteTask('fuzz', 'job2', None)]) self.assertEqual(config_map[('fuzz', 'job2')][2], 'project-os-ubuntu-24') # Test Case 3: OSS-Fuzz project, only project-level version exists. job3 = data_types.Job(name='job3', project='my-project', platform='LINUX') mock_get_all_from_query.return_value = [job3] mock_oss_fuzz_project_query.return_value.get.return_value = project - config_map = service._get_config_names( - [service.BatchTask('fuzz', 'job3', None)]) + config_map = batch_service._get_config_names( + [remote_task_types.RemoteTask('fuzz', 'job3', None)]) self.assertEqual(config_map[('fuzz', 'job3')][2], 'project-os-ubuntu-24') # Test Case 4: Internal project, no version is set, should be None. mock_is_oss_fuzz.return_value = False job4 = data_types.Job(name='job4', platform='LINUX') mock_get_all_from_query.return_value = [job4] - config_map = service._get_config_names( - [service.BatchTask('fuzz', 'job4', None)]) + config_map = batch_service._get_config_names( + [remote_task_types.RemoteTask('fuzz', 'job4', None)]) self.assertIsNone(config_map[('fuzz', 'job4')][2]) # Test Case 5: OSS-Fuzz project, but no versions are set anywhere. @@ -464,12 +463,12 @@ def test_get_config_names_os_version(self, mock_get_all_from_query, project_no_version = data_types.OssFuzzProject(name='my-project-no-version') mock_get_all_from_query.return_value = [job5] mock_oss_fuzz_project_query.return_value.get.return_value = project_no_version - config_map = service._get_config_names( - [service.BatchTask('fuzz', 'job5', None)]) + config_map = batch_service._get_config_names( + [remote_task_types.RemoteTask('fuzz', 'job5', None)]) self.assertIsNone(config_map[('fuzz', 'job5')][2]) def _get_spec_from_config(command, job_name): return list( - service._get_specs_from_config( - [service.BatchTask(command, job_name, None)]).values())[0] + batch_service._get_specs_from_config( + [remote_task_types.RemoteTask(command, job_name, None)]).values())[0] diff --git a/src/clusterfuzz/_internal/tests/core/datastore/ds_test_utils.py b/src/clusterfuzz/_internal/tests/core/datastore/ds_test_utils.py new file mode 100644 index 00000000000..d185fa26ea8 --- /dev/null +++ b/src/clusterfuzz/_internal/tests/core/datastore/ds_test_utils.py @@ -0,0 +1,41 @@ +# 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. +"""Datastore test utils.""" + +from functools import wraps + +from google.cloud import ndb + +from clusterfuzz._internal.datastore import data_types + + +def with_flags(**kwargs): + """Sets feature flags for the duration of a test.""" + + def decorator(test_func): + + @wraps(test_func) + def wrapper(self): + for key, value in kwargs.items(): + if isinstance(value, float): + data_types.FeatureFlag(id=key, enabled=True, value=value).put() + elif isinstance(value, bool): + data_types.FeatureFlag(id=key, enabled=value).put() + test_func(self) + for key in kwargs: + ndb.Key(data_types.FeatureFlag, key).delete() + + return wrapper + + return decorator diff --git a/src/clusterfuzz/_internal/tests/core/datastore/feature_flag_test.py b/src/clusterfuzz/_internal/tests/core/datastore/feature_flag_test.py new file mode 100644 index 00000000000..89e4a5dc496 --- /dev/null +++ b/src/clusterfuzz/_internal/tests/core/datastore/feature_flag_test.py @@ -0,0 +1,55 @@ +# 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. +"""FeatureFlag tests.""" +import unittest + +from clusterfuzz._internal.datastore import data_types +from clusterfuzz._internal.datastore import feature_flags +from clusterfuzz._internal.tests.test_libs import test_utils + + +@test_utils.with_cloud_emulators('datastore') +class FeatureFlagTest(unittest.TestCase): + """Test FeatureFlag.""" + + def test_get_and_set(self): + """Test getting and setting feature flags.""" + # Create a boolean feature flag. + flag = data_types.FeatureFlag(id=feature_flags.FeatureFlags.TEST_FLAG.value) + flag.description = 'A test boolean flag' + flag.enabled = True + flag.put() + + # Create a float feature flag. + flag_float = data_types.FeatureFlag( + id=feature_flags.FeatureFlags.TEST_FLOAT_FLAG.value) + flag_float.description = 'A test float flag' + flag_float.enabled = True + flag_float.value = 1.23 + flag_float.string_value = 'checker' + flag_float.put() + + # Retrieve and verify. + self.assertIsNotNone(feature_flags.FeatureFlags.TEST_FLAG.flag) + self.assertTrue(feature_flags.FeatureFlags.TEST_FLAG.enabled) + self.assertEqual(feature_flags.FeatureFlags.TEST_FLAG.description, + 'A test boolean flag') + self.assertIsNone(feature_flags.FeatureFlags.TEST_FLAG.content) + + retrieved_flag_float = feature_flags.FeatureFlags.TEST_FLOAT_FLAG.flag + self.assertIsNotNone(retrieved_flag_float) + self.assertTrue(feature_flags.FeatureFlags.TEST_FLOAT_FLAG.enabled) + self.assertEqual(feature_flags.FeatureFlags.TEST_FLOAT_FLAG.content, 1.23) + self.assertEqual(feature_flags.FeatureFlags.TEST_FLOAT_FLAG.string_value, + 'checker') diff --git a/src/clusterfuzz/_internal/tests/core/k8s/__init__.py b/src/clusterfuzz/_internal/tests/core/k8s/__init__.py new file mode 100644 index 00000000000..a705175a72d --- /dev/null +++ b/src/clusterfuzz/_internal/tests/core/k8s/__init__.py @@ -0,0 +1,14 @@ +# 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. +"""Kubernetes tests.""" diff --git a/src/clusterfuzz/_internal/tests/core/k8s/k8s_integration_test.py b/src/clusterfuzz/_internal/tests/core/k8s/k8s_integration_test.py new file mode 100644 index 00000000000..19f8c5f461f --- /dev/null +++ b/src/clusterfuzz/_internal/tests/core/k8s/k8s_integration_test.py @@ -0,0 +1,104 @@ +# 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. +"""Integration tests for KubernetesService.""" + +import base64 +import os +import unittest +from unittest import mock + +from kubernetes import client + +from clusterfuzz._internal.k8s import service + + +class KubernetesIntegrationTest(unittest.TestCase): + """Integration tests for KubernetesService.""" + # pylint: disable=protected-access + + @mock.patch('clusterfuzz._internal.base.utils.get_application_id') + @mock.patch('google.auth.default') + @mock.patch('googleapiclient.discovery.build') + def test_load_credentials(self, mock_discovery_build, mock_auth_default, + mock_get_application_id): + """Test that credentials can be loaded manually using the fallback logic.""" + # Ensure no kubeconfig interferes to force manual path (if local kubeconfig exists) + # Note: os.environ changes are process-local. + old_kubeconfig = os.environ.get('KUBECONFIG') + os.environ['KUBECONFIG'] = '/dev/null' + + # Mock GKE response + mock_service = mock.Mock() + mock_discovery_build.return_value = mock_service + mock_get_application_id.return_value = 'test-project' + + mock_creds = mock.Mock() + mock_creds.valid = True + mock_creds.expired = False + mock_creds.token = 'fake-token' + mock_auth_default.return_value = (mock_creds, 'test-project') + + mock_clusters_list = mock_service.projects().locations().clusters().list( + ).execute + mock_clusters_list.return_value = { + 'clusters': [{ + 'name': 'clusterfuzz-cronjobs-gke', + 'endpoint': '1.2.3.4', + 'masterAuth': { + 'clusterCaCertificate': + base64.b64encode(b'fake-cert').decode('utf-8') + } + }] + } + + # Mock list_namespaced_job to avoid actual network call to 1.2.3.4 + with mock.patch('kubernetes.client.BatchV1Api.list_namespaced_job'): + try: + # This will trigger _load_gke_credentials + # It should try load_kube_config (fail), load_incluster (fail), then manual. + k8s_service = service.KubernetesService() + + # Verify api client is initialized + self.assertIsNotNone(k8s_service._batch_api) + self.assertIsInstance(k8s_service._batch_api, client.BatchV1Api) + + # Verify configuration + config = client.Configuration.get_default_copy() + print(f'Loaded Host: {config.host}') + + # Check that we got a valid https endpoint + self.assertTrue(config.host.startswith('https://')) + self.assertTrue(config.verify_ssl) + self.assertIsNotNone(config.ssl_ca_cert) + + # Verify API key fix is present (Crucial for manual path) + self.assertIn('authorization', config.api_key) + + # Verify hook is present + self.assertIsNotNone(config.refresh_api_key_hook) + + # Verify actual connectivity and auth + print('Attempting to list jobs to verify authentication...') + k8s_service._batch_api.list_namespaced_job(namespace='default', limit=1) + print('Successfully listed jobs.') + + finally: + if old_kubeconfig: + os.environ['KUBECONFIG'] = old_kubeconfig + else: + del os.environ['KUBECONFIG'] + + +if __name__ == '__main__': + unittest.main() diff --git a/src/clusterfuzz/_internal/tests/core/k8s/k8s_service_e2e_test.py b/src/clusterfuzz/_internal/tests/core/k8s/k8s_service_e2e_test.py new file mode 100644 index 00000000000..e5e7c2e50c9 --- /dev/null +++ b/src/clusterfuzz/_internal/tests/core/k8s/k8s_service_e2e_test.py @@ -0,0 +1,192 @@ +# 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. +"""End-to-end tests for the Kubernetes service.""" + +# pylint: disable=unused-argument + +import os +import shutil +import subprocess +import tempfile +import time +import unittest +from unittest import mock + +from kubernetes import client as k8s_client +from kubernetes import config as k8s_config + +from clusterfuzz._internal.datastore import data_types +from clusterfuzz._internal.k8s import service as kubernetes_service +from clusterfuzz._internal.k8s.service import KubernetesJobConfig +from clusterfuzz._internal.remote_task import remote_task_types +from clusterfuzz._internal.tests.test_libs import test_utils + + +@test_utils.with_cloud_emulators('datastore') +class KubernetesServiceE2ETest(unittest.TestCase): + """End-to-end tests for the Kubernetes service.""" + + @classmethod + def setUpClass(cls): + """Set up the test environment.""" + if not os.getenv('K8S_E2E'): + raise unittest.SkipTest('K8S_E2E environment variable not set.') + + cls.cluster_name = 'test-cluster-for-e2e-test' + cls.image = 'gcr.io/clusterfuzz-images/base:000dc1f-202511191429' + + # Find `kind` executable. + cls.kind_path = ( + shutil.which('kind') or os.path.expanduser('~/.local/bin/kind')) + if not cls.kind_path or not os.path.exists(cls.kind_path): + raise unittest.SkipTest('kind executable not found.') + + # Ensure no old cluster exists and create a new one. + subprocess.run( + [cls.kind_path, 'delete', 'cluster', '--name', cls.cluster_name], + check=False) + subprocess.run( + [cls.kind_path, 'create', 'cluster', '--name', cls.cluster_name], + check=True) + + # Get kubeconfig and load it. + kubeconfig = subprocess.check_output( + [cls.kind_path, 'get', 'kubeconfig', '--name', + cls.cluster_name]).decode('utf-8') + + with tempfile.NamedTemporaryFile(mode='w', delete=False) as f: + f.write(kubeconfig) + cls.kubeconfig_path = f.name + k8s_config.load_kube_config(config_file=cls.kubeconfig_path) + + cls.api_client = k8s_client.BatchV1Api() + cls.kubernetes_client = kubernetes_service.KubernetesService( + k8s_config_loaded=True) + + # Setup dummy jobs in datastore. + data_types.Job(name='test-job', platform='LINUX').put() + data_types.Job(name='test-job1', platform='LINUX').put() + data_types.Job(name='test-job2', platform='LINUX').put() + + @classmethod + def tearDownClass(cls): + """Tear down the test environment.""" + if hasattr(cls, 'kubeconfig_path') and os.path.exists(cls.kubeconfig_path): + os.remove(cls.kubeconfig_path) + if hasattr(cls, 'kind_path') and cls.kind_path: + subprocess.run( + [cls.kind_path, 'delete', 'cluster', '--name', cls.cluster_name], + check=True) + + def _wait_for_job_and_delete(self, job_name): + """Waits for a job to start running and then deletes it.""" + # Wait for the job to be created in the API. + time.sleep(2) + + # Wait for the job to start running (at least one active pod). + job_running = False + for _ in range(60): + job = self.api_client.read_namespaced_job(job_name, 'default') + if job.status.active or job.status.succeeded: + job_running = True + break + time.sleep(1) + + self.assertTrue( + job_running, + f'Job {job_name} did not start running. Status: {job.status}') + + # Cleanup. + self.api_client.delete_namespaced_job( + name=job_name, + namespace='default', + body=k8s_client.V1DeleteOptions(propagation_policy='Foreground')) + + def test_create_job(self): + """Tests creating a job.""" + config = KubernetesJobConfig( + job_type='test-job', + docker_image=self.image, + command='fuzz', + disk_size_gb=10, + service_account_email='test-email', + clusterfuzz_release='prod', + is_kata=False) + actual_job_name = self.kubernetes_client.create_job(config, 'url') + self._wait_for_job_and_delete(actual_job_name) + + @mock.patch('clusterfuzz._internal.k8s.service._get_k8s_job_configs') + @mock.patch( + 'clusterfuzz._internal.base.tasks.task_utils.get_command_from_module') + def test_create_uworker_main_batch_job(self, mock_get_command_from_module, + mock_get_k8s_job_configs): + """Tests creating a single uworker main batch job.""" + mock_get_command_from_module.return_value = 'fuzz' + config = KubernetesJobConfig( + job_type='test-job', + docker_image=self.image, + command='fuzz', + disk_size_gb=10, + service_account_email='test-email', + clusterfuzz_release='prod', + is_kata=False) + mock_get_k8s_job_configs.return_value = {('fuzz', 'test-job'): config} + + actual_job_name = self.kubernetes_client.create_utask_main_job( + 'module', 'test-job', 'url1') + self._wait_for_job_and_delete(actual_job_name) + + @mock.patch('clusterfuzz._internal.k8s.service._get_k8s_job_configs') + @mock.patch( + 'clusterfuzz._internal.base.tasks.task_utils.get_command_from_module') + def test_create_uworker_main_batch_jobs(self, mock_get_command_from_module, + mock_get_k8s_job_configs): + """Tests creating multiple uworker main batch jobs.""" + mock_get_command_from_module.return_value = 'fuzz' + config1 = KubernetesJobConfig( + job_type='test-job1', + docker_image=self.image, + command='fuzz', + disk_size_gb=10, + service_account_email='test-email', + clusterfuzz_release='prod', + is_kata=False) + config2 = KubernetesJobConfig( + job_type='test-job2', + docker_image=self.image, + command='fuzz', + disk_size_gb=20, + service_account_email='test-email', + clusterfuzz_release='prod', + is_kata=False) + + mock_get_k8s_job_configs.return_value = { + ('fuzz', 'test-job1'): config1, + ('fuzz', 'test-job2'): config2 + } + + tasks = [ + remote_task_types.RemoteTask('fuzz', 'test-job1', 'url1'), + remote_task_types.RemoteTask('fuzz', 'test-job2', 'url2'), + ] + + actual_job_names = self.kubernetes_client.create_utask_main_jobs(tasks) + self.assertEqual(len(actual_job_names), 2) + + for job_name in actual_job_names: + self._wait_for_job_and_delete(job_name) + + +if __name__ == '__main__': + unittest.main() diff --git a/src/clusterfuzz/_internal/tests/core/k8s/k8s_service_test.py b/src/clusterfuzz/_internal/tests/core/k8s/k8s_service_test.py new file mode 100644 index 00000000000..65a77b38c71 --- /dev/null +++ b/src/clusterfuzz/_internal/tests/core/k8s/k8s_service_test.py @@ -0,0 +1,171 @@ +# 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. +"""Tests for the Kubernetes batch client.""" + +import unittest +from unittest import mock + +from clusterfuzz._internal.datastore import data_types +from clusterfuzz._internal.k8s import service +from clusterfuzz._internal.remote_task import remote_task_types +from clusterfuzz._internal.tests.test_libs import test_utils + + +@test_utils.with_cloud_emulators('datastore') +@mock.patch('kubernetes.config.load_kube_config') +class KubernetesServiceTest(unittest.TestCase): + """Tests for the KubernetesService class.""" + + # pylint: disable=protected-access + + def setUp(self): + patcher = mock.patch( + 'clusterfuzz._internal.k8s.service.KubernetesService._load_gke_credentials' + ) + self.addCleanup(patcher.stop) + self.mock_load_gke = patcher.start() + + data_types.Job(name='job1', platform='LINUX').put() + data_types.Job( + name='job2', platform='LINUX', + environment_string='CUSTOM_VAR = value').put() + + @mock.patch.object(service.KubernetesService, '_get_pending_jobs_count') + @mock.patch.object(service.KubernetesService, 'create_job') + def test_create_uworker_main_batch_jobs(self, mock_create_job, + mock_get_pending_count, _): + """Tests the creation of uworker main batch jobs.""" + mock_get_pending_count.return_value = 0 + tasks = [ + remote_task_types.RemoteTask('fuzz', 'job1', 'url1'), + remote_task_types.RemoteTask('fuzz', 'job1', 'url2'), + remote_task_types.RemoteTask('fuzz', 'job1', 'url3'), + ] + + kube_service = service.KubernetesService() + kube_service.create_utask_main_jobs(tasks) + + # Total 3 tasks, so 3 calls to create_job. + self.assertEqual(3, mock_create_job.call_count) + + urls = sorted([call.args[1] for call in mock_create_job.call_args_list]) + self.assertEqual(urls, ['url1', 'url2', 'url3']) + + @mock.patch('kubernetes.client.CoreV1Api') + def test_get_pending_jobs_count(self, mock_core_api_cls, _): + """Tests _get_pending_jobs_count.""" + mock_core_api = mock_core_api_cls.return_value + kube_service = service.KubernetesService() + + # Mock pods + mock_core_api.list_namespaced_pod.return_value.items = [ + mock.Mock(), mock.Mock() + ] + + self.assertEqual(2, kube_service._get_pending_jobs_count()) + mock_core_api.list_namespaced_pod.assert_called_with( + namespace='default', + label_selector='app.kubernetes.io/name=clusterfuzz-kata-job', + field_selector='status.phase=Pending') + + @mock.patch('kubernetes.client.BatchV1Api') + def test_create_job_kata(self, mock_batch_api_cls, _): + """Tests that create_job generates the correct spec for Kata.""" + mock_batch_api = mock_batch_api_cls.return_value + kube_service = service.KubernetesService() + # Mock _create_service_account_if_needed + kube_service._create_service_account_if_needed = mock.Mock( + return_value='test') + + config = service.KubernetesJobConfig( + job_type='test-job', + docker_image='test-image', + command='fuzz', + disk_size_gb=10, + service_account_email='test@clusterfuzz-test.iam.gserviceaccount.com', + clusterfuzz_release='prod', + is_kata=True) + + kube_service.create_job(config, 'input_url') + + self.assertTrue(mock_batch_api.create_namespaced_job.called) + call_args = mock_batch_api.create_namespaced_job.call_args + job_body = call_args.kwargs['body'] + + # Check Spec + pod_spec = job_body['spec']['template']['spec'] + container = pod_spec['containers'][0] + + # Check capabilities + self.assertEqual(['ALL'], + container['securityContext']['capabilities']['add']) + + # Check labels + self.assertEqual('fuzz', job_body['metadata']['labels']['task_name']) + self.assertEqual('test-job', job_body['metadata']['labels']['job_name']) + + # Check Kata specific fields + self.assertEqual('kata', pod_spec['runtimeClassName']) + self.assertEqual('ClusterFirstWithHostNet', pod_spec['dnsPolicy']) + self.assertIn('lifecycle', container) + + @mock.patch('kubernetes.client.BatchV1Api') + def test_create_job_standard(self, mock_batch_api_cls, _): + """Tests create_job for standard container.""" + mock_batch_api = mock_batch_api_cls.return_value + kube_service = service.KubernetesService() + kube_service._create_service_account_if_needed = mock.Mock( + return_value='test-sa') + + config = service.KubernetesJobConfig( + job_type='test-job', + docker_image='test-image', + command='fuzz', + disk_size_gb=10, + service_account_email='test-email', + clusterfuzz_release='prod', + is_kata=False) + + kube_service.create_job(config, 'input_url') + + self.assertTrue(mock_batch_api.create_namespaced_job.called) + call_args = mock_batch_api.create_namespaced_job.call_args + job_body = call_args.kwargs['body'] + + pod_spec = job_body['spec']['template']['spec'] + container = pod_spec['containers'][0] + + # Check Service Account + self.assertEqual('test-sa', pod_spec['serviceAccountName']) + + # Check that Kata specific fields are NOT present + self.assertNotIn('runtimeClassName', pod_spec) + self.assertIn('volumeMounts', container) + + @mock.patch( + 'clusterfuzz._internal.base.tasks.task_utils.get_command_from_module') + @mock.patch.object(service.KubernetesService, 'create_utask_main_jobs') + def test_create_uworker_main_batch_job(self, mock_create_batch_jobs, + mock_get_command, _): + """Tests the creation of a single uworker main batch job.""" + mock_get_command.return_value = 'command' + kube_service = service.KubernetesService() + kube_service.create_utask_main_job('module', 'job', 'url') + + self.assertEqual(1, mock_create_batch_jobs.call_count) + tasks = mock_create_batch_jobs.call_args[0][0] + self.assertEqual(1, len(tasks)) + self.assertEqual('command', tasks[0].command) + self.assertEqual('job', tasks[0].job_type) + self.assertEqual('url', tasks[0].input_download_url) diff --git a/src/clusterfuzz/_internal/tests/core/kubernetes/__init__.py b/src/clusterfuzz/_internal/tests/core/kubernetes/__init__.py new file mode 100644 index 00000000000..37b43b64d76 --- /dev/null +++ b/src/clusterfuzz/_internal/tests/core/kubernetes/__init__.py @@ -0,0 +1,14 @@ +# 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. +"""Kubernetes batch client tests.""" diff --git a/src/clusterfuzz/_internal/tests/core/kubernetes/kubernetes_test.py b/src/clusterfuzz/_internal/tests/core/kubernetes/kubernetes_test.py new file mode 100644 index 00000000000..815d87349f1 --- /dev/null +++ b/src/clusterfuzz/_internal/tests/core/kubernetes/kubernetes_test.py @@ -0,0 +1,94 @@ +# pylint: disable=protected-access +# 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. +"""Tests for the Kubernetes batch client.""" +import unittest +import uuid + +from clusterfuzz._internal.k8s import service as kubernetes_service +from clusterfuzz._internal.tests.test_libs import helpers + + +class MockRemoteTask(): + """Mock RemoteTask for testing.""" + job_type = 'test-job' + docker_image = 'test-image' + command = 'fuzz' + + +class KubernetesJobClientTest(unittest.TestCase): + """Tests for KubernetesJobClient.""" + + def setUp(self): + helpers.patch(self, [ + 'clusterfuzz._internal.k8s.service.KubernetesService._load_gke_credentials', + 'kubernetes.config.load_kube_config', + 'kubernetes.client.CoreV1Api', + 'kubernetes.client.BatchV1Api', + 'uuid.uuid4', + ]) + self.mock.uuid4.return_value = uuid.UUID( + 'a0b1c2d3-e4f5-6789-0123-456789abcdef') + self.job_spec = { + 'metadata': { + 'name': 'test-job' + }, + 'spec': { + 'template': { + 'spec': { + 'containers': [{ + 'name': 'test-container', + 'image': 'test-image', + 'env': [] + }] + } + } + } + } + self.k8s_client = kubernetes_service.KubernetesService() + + def test_create_job(self): + """Tests that create_job works as expected.""" + input_url = 'url1' + remote_task = MockRemoteTask() + + config = kubernetes_service.KubernetesJobConfig( + job_type=remote_task.job_type, + docker_image=remote_task.docker_image, + command=remote_task.command, + disk_size_gb=10, + service_account_email='test-email', + clusterfuzz_release='prod', + is_kata=True) + + self.k8s_client.create_job(config, input_url) + self.k8s_client._batch_api.create_namespaced_job.assert_called_once() + called_args, called_kwargs = self.k8s_client._batch_api.create_namespaced_job.call_args + self.assertEqual(called_args, ()) + job_body = called_kwargs['body'] + self.assertEqual(job_body['metadata']['name'], + 'cf-job-a0b1c2d3-e4f5-6789-0123-456789abcdef') + self.assertEqual(job_body['metadata']['labels']['task_name'], 'fuzz') + self.assertEqual(job_body['metadata']['labels']['job_name'], 'test-job') + self.assertEqual( + job_body['spec']['template']['spec']['containers'][0]['image'], + 'test-image') + self.assertIn({ + 'name': 'UWORKER_INPUT_DOWNLOAD_URL', + 'value': 'url1' + }, job_body['spec']['template']['spec']['containers'][0]['env']) + self.assertIn({ + 'name': 'CLUSTERFUZZ_RELEASE', + 'value': 'prod' + }, job_body['spec']['template']['spec']['containers'][0]['env']) diff --git a/src/clusterfuzz/_internal/tests/core/platforms/__init__.py b/src/clusterfuzz/_internal/tests/core/platforms/__init__.py index ddd71c00285..2d6bb48a93d 100644 --- a/src/clusterfuzz/_internal/tests/core/platforms/__init__.py +++ b/src/clusterfuzz/_internal/tests/core/platforms/__init__.py @@ -11,3 +11,4 @@ # 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. +"""Platforms tests.""" diff --git a/src/clusterfuzz/_internal/tests/core/remote_task/__init__.py b/src/clusterfuzz/_internal/tests/core/remote_task/__init__.py new file mode 100644 index 00000000000..14cb13c5dbf --- /dev/null +++ b/src/clusterfuzz/_internal/tests/core/remote_task/__init__.py @@ -0,0 +1,14 @@ +# 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. +"""Remote task tests.""" diff --git a/src/clusterfuzz/_internal/tests/core/remote_task/job_frequency_test.py b/src/clusterfuzz/_internal/tests/core/remote_task/job_frequency_test.py new file mode 100644 index 00000000000..0786114e91c --- /dev/null +++ b/src/clusterfuzz/_internal/tests/core/remote_task/job_frequency_test.py @@ -0,0 +1,77 @@ +# 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. +"""Tests for the remote task job frequency module.""" +import unittest +from unittest import mock + +from clusterfuzz._internal.remote_task import remote_task_gate +from clusterfuzz._internal.tests.core.datastore import ds_test_utils +from clusterfuzz._internal.tests.test_libs import test_utils + + +@test_utils.with_cloud_emulators('datastore') +class GetJobFrequencyTest(unittest.TestCase): + """Tests for the get_job_frequency function.""" + + def setUp(self): + mock.patch( + 'clusterfuzz._internal.k8s.service.KubernetesService._load_gke_credentials' + ).start() + self.gate = remote_task_gate.RemoteTaskGate() + + def test_get_job_frequency_defaults(self): + """Tests that the default frequencies are returned when no feature flags + are set.""" + + frequencies = self.gate.get_job_frequency() + self.assertEqual(frequencies['kubernetes'], 0.0) + self.assertEqual(frequencies['gcp_batch'], 1.0) + self.assertEqual(sum(frequencies.values()), 1.0) + + @ds_test_utils.with_flags(k8s_jobs_frequency=0.3) + def test_get_job_frequency_with_k8s_flag(self): + """Tests that the frequencies are correctly calculated when the + k8s_jobs_frequency flag is set.""" + + frequencies = self.gate.get_job_frequency() + self.assertEqual(frequencies['kubernetes'], 0.3) + self.assertEqual(frequencies['gcp_batch'], 0.7) + self.assertEqual(sum(frequencies.values()), 1.0) + + @ds_test_utils.with_flags(k8s_jobs_frequency=1.0) + def test_get_job_frequency_with_k8s_flag_full(self): + """Tests that the frequencies are correctly calculated when the + k8s_jobs_frequency flag is set to 1.0.""" + + frequencies = self.gate.get_job_frequency() + self.assertEqual(frequencies['kubernetes'], 1.0) + self.assertEqual(frequencies['gcp_batch'], 0.0) + self.assertEqual(sum(frequencies.values()), 1.0) + + @ds_test_utils.with_flags(k8s_jobs_frequency=0.0) + def test_get_job_frequency_with_k8s_flag_zero(self): + """Tests that the frequencies are correctly calculated when the + k8s_jobs_frequency flag is set to 0.0.""" + + frequencies = self.gate.get_job_frequency() + self.assertEqual(frequencies['kubernetes'], 0.0) + self.assertEqual(frequencies['gcp_batch'], 1.0) + self.assertEqual(sum(frequencies.values()), 1.0) + + @ds_test_utils.with_flags(k8s_jobs_frequency=0.5) + def test_get_job_frequency_sum_is_one(self): + """Tests that the sum of the frequencies is always 1.0.""" + + frequencies = self.gate.get_job_frequency() + self.assertEqual(sum(frequencies.values()), 1.0) diff --git a/src/clusterfuzz/_internal/tests/core/remote_task/remote_task_gate_test.py b/src/clusterfuzz/_internal/tests/core/remote_task/remote_task_gate_test.py new file mode 100644 index 00000000000..4408f2aa345 --- /dev/null +++ b/src/clusterfuzz/_internal/tests/core/remote_task/remote_task_gate_test.py @@ -0,0 +1,199 @@ +# 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. +"""Tests for the RemoteTaskGate class.""" + +# pylint: disable=protected-access, unused-argument + +import unittest +from unittest import mock + +from clusterfuzz._internal.batch.service import GcpBatchService +from clusterfuzz._internal.k8s.service import KubernetesService +from clusterfuzz._internal.remote_task import remote_task_adapters +from clusterfuzz._internal.remote_task import remote_task_gate +from clusterfuzz._internal.remote_task import remote_task_types + + +class RemoteTaskGateTest(unittest.TestCase): + """Tests for the RemoteTaskGate class.""" + + def setUp(self): + super().setUp() + self.mock_k8s_service = mock.Mock(spec=KubernetesService) + self.mock_gcp_batch_service = mock.Mock(spec=GcpBatchService) + + self.mock_k8s_service.create_utask_main_jobs.return_value = [] + self.mock_gcp_batch_service.create_utask_main_jobs.return_value = [] + + # Patch RemoteTaskAdapters to return our mock services + self.patcher = mock.patch.dict( + remote_task_adapters.RemoteTaskAdapters._member_map_, { + 'KUBERNETES': + mock.Mock( + id='kubernetes', + service=mock.Mock(return_value=self.mock_k8s_service), + feature_flag=None, + default_weight=0.0), + 'GCP_BATCH': + mock.Mock( + id='gcp_batch', + service=mock.Mock(return_value=self.mock_gcp_batch_service), + feature_flag=None, + default_weight=1.0), + }) + self.patcher.start() + self.addCleanup(self.patcher.stop) + + def test_init(self): + """Tests that the RemoteTaskGate initializes correctly and creates + service map.""" + gate = remote_task_gate.RemoteTaskGate() + self.assertIn('kubernetes', gate._service_map) + self.assertIn('gcp_batch', gate._service_map) + self.assertEqual(gate._service_map['kubernetes'], self.mock_k8s_service) + self.assertEqual(gate._service_map['gcp_batch'], + self.mock_gcp_batch_service) + + @mock.patch('random.choices') + @mock.patch.object(remote_task_gate.RemoteTaskGate, 'get_job_frequency') + def test_get_adapter(self, mock_get_job_frequency, mock_random_choices): + """Tests that _get_adapter returns the correct adapter based on + job_frequency.""" + mock_get_job_frequency.return_value = {'kubernetes': 0.3, 'gcp_batch': 0.7} + mock_random_choices.return_value = ['gcp_batch'] + + gate = remote_task_gate.RemoteTaskGate() + selected_adapter = gate._get_adapter() + + mock_get_job_frequency.assert_called_once() + mock_random_choices.assert_called_once_with(['kubernetes', 'gcp_batch'], + [0.3, 0.7]) + self.assertEqual(selected_adapter, 'gcp_batch') + + @mock.patch.object(remote_task_gate.RemoteTaskGate, '_get_adapter') + def test_create_utask_main_job_kubernetes(self, mock_get_adapter): + """Tests that create_utask_main_job calls the Kubernetes service + when kubernetes adapter is chosen.""" + mock_get_adapter.return_value = 'kubernetes' + gate = remote_task_gate.RemoteTaskGate() + gate.create_utask_main_job('module', 'job', 'url') + self.mock_k8s_service.create_utask_main_job.assert_called_once_with( + 'module', 'job', 'url') + self.mock_gcp_batch_service.create_utask_main_job.assert_not_called() + + @mock.patch.object(remote_task_gate.RemoteTaskGate, '_get_adapter') + def test_create_utask_main_job_gcp_batch(self, mock_get_adapter): + """Tests that create_utask_main_job calls the GCP Batch service + when gcp_batch adapter is chosen.""" + mock_get_adapter.return_value = 'gcp_batch' + gate = remote_task_gate.RemoteTaskGate() + gate.create_utask_main_job('module', 'job', 'url') + self.mock_gcp_batch_service.create_utask_main_job.assert_called_once_with( + 'module', + 'job', + 'url', + ) + self.mock_k8s_service.create_utask_main_job.assert_not_called() + + @mock.patch.object(remote_task_gate.RemoteTaskGate, '_get_adapter') + def test_create_utask_main_jobs_single_task(self, mock_get_adapter): + """Tests that create_utask_main_jobs correctly routes a single task + based on _get_adapter.""" + tasks = [ + remote_task_types.RemoteTask('command1', 'job1', 'url1'), + ] + mock_get_adapter.return_value = 'kubernetes' + gate = remote_task_gate.RemoteTaskGate() + gate.create_utask_main_jobs(tasks) + + self.mock_k8s_service.create_utask_main_jobs.assert_called_once_with(tasks) + self.mock_gcp_batch_service.create_utask_main_jobs.assert_not_called() + + @mock.patch.object(remote_task_gate.RemoteTaskGate, 'get_job_frequency') + def test_create_utask_main_jobs_multiple_tasks_slicing( + self, mock_get_job_frequency): + """Tests that create_utask_main_jobs correctly routes multiple tasks + using deterministic slicing.""" + tasks = [ + remote_task_types.RemoteTask('command', 'job1', 'url1'), + remote_task_types.RemoteTask('command', 'job1', 'url2'), + remote_task_types.RemoteTask('command', 'job1', 'url3'), + remote_task_types.RemoteTask('command', 'job1', 'url4'), + ] + + # 50% split + mock_get_job_frequency.return_value = {'kubernetes': 0.5, 'gcp_batch': 0.5} + + gate = remote_task_gate.RemoteTaskGate() + gate.create_utask_main_jobs(tasks) + + # 4 * 0.5 = 2 tasks for k8s, 2 for gcp_batch. + self.mock_k8s_service.create_utask_main_jobs.assert_called_once_with( + tasks[:2]) + self.mock_gcp_batch_service.create_utask_main_jobs.assert_called_once_with( + tasks[2:]) + + @mock.patch.object(remote_task_gate.RemoteTaskGate, 'get_job_frequency') + def test_create_utask_main_jobs_remainder_distribution( + self, mock_get_job_frequency): + """Tests that create_utask_main_jobs correctly distributes remainder + tasks.""" + tasks = [ + remote_task_types.RemoteTask('c', 'j', 'u1'), + remote_task_types.RemoteTask('c', 'j', 'u2'), + remote_task_types.RemoteTask('c', 'j', 'u3'), + ] + + # 33/33/33 split - one task will be a remainder + mock_get_job_frequency.return_value = { + 'kubernetes': 0.33, + 'gcp_batch': 0.33 + } + + gate = remote_task_gate.RemoteTaskGate() + gate.create_utask_main_jobs(tasks) + + # Expect 1 for k8s, 1 for gcp_batch, and 1 remainder distributed round robin. + # In this case, first k8s gets 1, then gcp_batch gets 1, then k8s gets the last one. + self.mock_k8s_service.create_utask_main_jobs.assert_called_once_with( + [tasks[0], tasks[2]]) + self.mock_gcp_batch_service.create_utask_main_jobs.assert_called_once_with( + [tasks[1]]) + + @mock.patch.object(remote_task_gate.RemoteTaskGate, 'get_job_frequency') + def test_create_utask_main_jobs_full_kubernetes(self, mock_get_job_frequency): + """Tests that all tasks are routed to Kubernetes when frequency is 1.0.""" + tasks = [ + remote_task_types.RemoteTask('c', 'j', 'u1'), + remote_task_types.RemoteTask('c', 'j', 'u2'), + ] + mock_get_job_frequency.return_value = {'kubernetes': 1.0, 'gcp_batch': 0.0} + gate = remote_task_gate.RemoteTaskGate() + gate.create_utask_main_jobs(tasks) + self.mock_k8s_service.create_utask_main_jobs.assert_called_once_with(tasks) + self.mock_gcp_batch_service.create_utask_main_jobs.assert_not_called() + + @mock.patch.object(remote_task_gate.RemoteTaskGate, 'get_job_frequency') + def test_create_utask_main_jobs_full_gcp_batch(self, mock_get_job_frequency): + """Tests that all tasks are routed to GCP Batch when frequency is 1.0.""" + tasks = [ + remote_task_types.RemoteTask('c', 'j', 'u1'), + remote_task_types.RemoteTask('c', 'j', 'u2'), + ] + mock_get_job_frequency.return_value = {'kubernetes': 0.0, 'gcp_batch': 1.0} + gate = remote_task_gate.RemoteTaskGate() + gate.create_utask_main_jobs(tasks) + self.mock_gcp_batch_service.create_utask_main_jobs.assert_called_once_with( + tasks) + self.mock_k8s_service.create_utask_main_jobs.assert_not_called() diff --git a/src/clusterfuzz/_internal/tests/core/remote_task/remote_task_test.py b/src/clusterfuzz/_internal/tests/core/remote_task/remote_task_test.py new file mode 100644 index 00000000000..545f702897f --- /dev/null +++ b/src/clusterfuzz/_internal/tests/core/remote_task/remote_task_test.py @@ -0,0 +1,106 @@ +# 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. +"""Tests for remote_task.""" + +import unittest +from unittest import mock + +from clusterfuzz._internal.k8s import service as k8s_service +from clusterfuzz._internal.remote_task import remote_task_gate +from clusterfuzz._internal.remote_task import remote_task_types +from clusterfuzz._internal.tests.test_libs import test_utils + + +@test_utils.with_cloud_emulators('datastore') +class RemoteTaskGateTest(unittest.TestCase): + """Tests for RemoteTaskGate.""" + + def setUp(self): + patcher = mock.patch('clusterfuzz._internal.base.utils.get_application_id') + self.addCleanup(patcher.stop) + self.mock_get_application_id = patcher.start() + self.mock_get_application_id.return_value = 'test-project' + + patcher = mock.patch('google.auth.default') + self.addCleanup(patcher.stop) + self.mock_auth_default = patcher.start() + mock_creds = mock.Mock() + mock_creds.valid = True + mock_creds.expired = False + mock_creds.token = 'fake-token' + self.mock_auth_default.return_value = (mock_creds, 'test-project') + + # Mock discovery.build to avoid network calls during KubernetesService init + patcher = mock.patch('googleapiclient.discovery.build') + self.addCleanup(patcher.stop) + self.mock_discovery_build = patcher.start() + mock_service = mock.Mock() + self.mock_discovery_build.return_value = mock_service + mock_service.projects().locations().clusters().list( + ).execute.return_value = { + 'clusters': [{ + 'name': 'clusterfuzz-cronjobs-gke', + 'endpoint': '1.2.3.4', + 'masterAuth': { + 'clusterCaCertificate': + 'ZmFrZS1jZXJ0' # base64 encoded 'fake-cert' + } + }] + } + + self.gate = remote_task_gate.RemoteTaskGate() + + @mock.patch.object(remote_task_gate.RemoteTaskGate, 'get_job_frequency') + @mock.patch.object(k8s_service.KubernetesService, 'create_utask_main_jobs') + @mock.patch( + 'clusterfuzz._internal.batch.service.GcpBatchService.create_utask_main_jobs' + ) + def test_create_uworker_main_batch_jobs_k8s_limit_reached( + self, mock_gcp_create, mock_k8s_create, mock_get_frequency): + """Test delegation when K8s limit is reached (handled by service).""" + # Setup tasks to go to Kubernetes + mock_get_frequency.return_value = {'kubernetes': 1.0} + + task = remote_task_types.RemoteTask('fuzz', 'job1', 'url1') + + # Simulate K8s service returning empty list (limit reached) + mock_k8s_create.return_value = [] + + result = self.gate.create_utask_main_jobs([task]) + + # Verify K8s was attempted + self.assertTrue(mock_k8s_create.called) + + # Verify GCP was NOT attempted + self.assertFalse(mock_gcp_create.called) + + # Verify result is empty list + self.assertEqual(result, []) + + @mock.patch.object(remote_task_gate.RemoteTaskGate, 'get_job_frequency') + @mock.patch.object(k8s_service.KubernetesService, 'create_utask_main_jobs') + @mock.patch( + 'clusterfuzz._internal.batch.service.GcpBatchService.create_utask_main_jobs' + ) + def test_create_uworker_main_batch_jobs_success(self, _, mock_k8s_create, + mock_get_frequency): + """Test successful creation.""" + mock_get_frequency.return_value = {'kubernetes': 1.0} + mock_pubsub_task = mock.Mock() + task = remote_task_types.RemoteTask( + 'fuzz', 'job1', 'url1', pubsub_task=mock_pubsub_task) + + self.gate.create_utask_main_jobs([task]) + + self.assertTrue(mock_k8s_create.called) diff --git a/src/local/butler/lint.py b/src/local/butler/lint.py index 0cab67824c6..8384675babc 100644 --- a/src/local/butler/lint.py +++ b/src/local/butler/lint.py @@ -51,7 +51,7 @@ _LICENSE_CHECK_IGNORE = 'LICENSE_CHECK_IGNORE' _PY_TEST_SUFFIX = '_test.py' _PY_INIT_FILENAME = '__init__.py' -_YAML_EXCEPTIONS = ['bad.yaml'] +_YAML_EXCEPTIONS = ['bad.yaml', 'job_template.yaml'] _error_occurred = False diff --git a/src/local/butler/scripts/run_remote_task.py b/src/local/butler/scripts/run_remote_task.py index ac0dbf49627..d178cdb2a5b 100644 --- a/src/local/butler/scripts/run_remote_task.py +++ b/src/local/butler/scripts/run_remote_task.py @@ -17,6 +17,8 @@ from clusterfuzz._internal.base import tasks from clusterfuzz._internal.metrics import monitoring_metrics +from clusterfuzz._internal.remote_task import remote_task_gate +from clusterfuzz._internal.remote_task import remote_task_types from clusterfuzz._internal.system import environment @@ -35,22 +37,25 @@ def lease_all_tasks(task_list): def schedule_utask_mains(): """Schedules utask_mains from preprocessed utasks on Google Cloud Batch.""" - from clusterfuzz._internal.google_cloud_utils import batch print('Attempting to combine batch tasks.') utask_mains = tasks.get_utask_mains() if not utask_mains: print('No utask mains.') - return [] # Return an empty list for consistency. + return [] print(f'Combining {len(utask_mains)} batch tasks.') - + results = [] with lease_all_tasks(utask_mains): batch_tasks = [ - batch.BatchTask(task.command, task.job, task.argument) + remote_task_types.RemoteTask(task.command, task.job, task.argument) for task in utask_mains ] - return batch.create_uworker_main_batch_jobs(batch_tasks) + + results = remote_task_gate.RemoteTaskGate().create_utask_main_jobs( + batch_tasks) + print('Created jobs:', results) + return results def execute(*args, **kwargs): # pylint: disable=unused-argument diff --git a/src/platform_requirements.txt b/src/platform_requirements.txt index 6732d4dd9ac..eeb19228808 100644 --- a/src/platform_requirements.txt +++ b/src/platform_requirements.txt @@ -1,3 +1,4 @@ grpcio==1.62.2 +kubernetes==34.1.0 protobuf==4.23.4 psutil==5.9.4 \ No newline at end of file diff --git a/src/python/bot/startup/run_bot.py b/src/python/bot/startup/run_bot.py index f36a5f7a8c0..a933cb29f5d 100644 --- a/src/python/bot/startup/run_bot.py +++ b/src/python/bot/startup/run_bot.py @@ -17,6 +17,8 @@ # to be able to import dependencies directly, but we must store these in # subdirectories of common so that they are shared with App Engine. from clusterfuzz._internal.base import modules +from clusterfuzz._internal.remote_task import remote_task_gate +from clusterfuzz._internal.remote_task import remote_task_types modules.fix_module_search_paths() @@ -86,7 +88,6 @@ def lease_all_tasks(task_list): def schedule_utask_mains(): """Schedules utask_mains from preprocessed utasks on Google Cloud Batch.""" - from clusterfuzz._internal.google_cloud_utils import batch logs.info('Attempting to combine batch tasks.') utask_mains = tasks.get_utask_mains() @@ -98,10 +99,11 @@ def schedule_utask_mains(): with lease_all_tasks(utask_mains): batch_tasks = [ - batch.BatchTask(task.command, task.job, task.argument) + remote_task_types.RemoteTask( + task.command, task.job, task.argument, pubsub_task=task) for task in utask_mains ] - batch.create_uworker_main_batch_jobs(batch_tasks) + remote_task_gate.RemoteTaskGate().create_utask_main_jobs(batch_tasks) def task_loop(): diff --git a/src/setup.py b/src/setup.py index 65059382788..e9e7cba3257 100644 --- a/src/setup.py +++ b/src/setup.py @@ -45,6 +45,7 @@ 'google-cloud-storage', 'grpcio', 'httplib2', + 'kubernetes==34.1.0', 'mozprocess', 'oauth2client', 'protobuf',