diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index adc28b117d..c0a3ac553a 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -123,7 +123,7 @@ jobs: - name: Vulture if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true' run: | - poetry run vulture --exclude "contrib" --min-confidence 100 . + poetry run vulture --exclude "contrib,tests,conftest.py" --min-confidence 100 . - name: Hadolint if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true' run: | diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d17e88db0d..c3a0602ba2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -85,6 +85,6 @@ repos: - id: vulture name: vulture description: "Vulture finds unused code in Python programs." - entry: bash -c 'poetry run vulture --exclude "contrib,.venv" --min-confidence 100 .' + entry: bash -c 'poetry run vulture --exclude "contrib,.venv,tests,conftest.py" --min-confidence 100 .' language: system files: '.*\.py' diff --git a/poetry.lock b/poetry.lock index 2fb0fb2af9..a00b2107a3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -13,98 +13,113 @@ files = [ [[package]] name = "aiohappyeyeballs" -version = "2.3.5" +version = "2.4.0" description = "Happy Eyeballs for asyncio" optional = false python-versions = ">=3.8" files = [ - {file = "aiohappyeyeballs-2.3.5-py3-none-any.whl", hash = "sha256:4d6dea59215537dbc746e93e779caea8178c866856a721c9c660d7a5a7b8be03"}, - {file = "aiohappyeyeballs-2.3.5.tar.gz", hash = "sha256:6fa48b9f1317254f122a07a131a86b71ca6946ca989ce6326fff54a99a920105"}, + {file = "aiohappyeyeballs-2.4.0-py3-none-any.whl", hash = "sha256:7ce92076e249169a13c2f49320d1967425eaf1f407522d707d59cac7628d62bd"}, + {file = "aiohappyeyeballs-2.4.0.tar.gz", hash = "sha256:55a1714f084e63d49639800f95716da97a1f173d46a16dfcfda0016abb93b6b2"}, ] [[package]] name = "aiohttp" -version = "3.10.3" +version = "3.10.5" description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.8" files = [ - {file = "aiohttp-3.10.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cc36cbdedf6f259371dbbbcaae5bb0e95b879bc501668ab6306af867577eb5db"}, - {file = "aiohttp-3.10.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85466b5a695c2a7db13eb2c200af552d13e6a9313d7fa92e4ffe04a2c0ea74c1"}, - {file = "aiohttp-3.10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:71bb1d97bfe7e6726267cea169fdf5df7658831bb68ec02c9c6b9f3511e108bb"}, - {file = "aiohttp-3.10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baec1eb274f78b2de54471fc4c69ecbea4275965eab4b556ef7a7698dee18bf2"}, - {file = "aiohttp-3.10.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:13031e7ec1188274bad243255c328cc3019e36a5a907978501256000d57a7201"}, - {file = "aiohttp-3.10.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2bbc55a964b8eecb341e492ae91c3bd0848324d313e1e71a27e3d96e6ee7e8e8"}, - {file = "aiohttp-3.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8cc0564b286b625e673a2615ede60a1704d0cbbf1b24604e28c31ed37dc62aa"}, - {file = "aiohttp-3.10.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f817a54059a4cfbc385a7f51696359c642088710e731e8df80d0607193ed2b73"}, - {file = "aiohttp-3.10.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8542c9e5bcb2bd3115acdf5adc41cda394e7360916197805e7e32b93d821ef93"}, - {file = "aiohttp-3.10.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:671efce3a4a0281060edf9a07a2f7e6230dca3a1cbc61d110eee7753d28405f7"}, - {file = "aiohttp-3.10.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0974f3b5b0132edcec92c3306f858ad4356a63d26b18021d859c9927616ebf27"}, - {file = "aiohttp-3.10.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:44bb159b55926b57812dca1b21c34528e800963ffe130d08b049b2d6b994ada7"}, - {file = "aiohttp-3.10.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6ae9ae382d1c9617a91647575255ad55a48bfdde34cc2185dd558ce476bf16e9"}, - {file = "aiohttp-3.10.3-cp310-cp310-win32.whl", hash = "sha256:aed12a54d4e1ee647376fa541e1b7621505001f9f939debf51397b9329fd88b9"}, - {file = "aiohttp-3.10.3-cp310-cp310-win_amd64.whl", hash = "sha256:b51aef59370baf7444de1572f7830f59ddbabd04e5292fa4218d02f085f8d299"}, - {file = "aiohttp-3.10.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e021c4c778644e8cdc09487d65564265e6b149896a17d7c0f52e9a088cc44e1b"}, - {file = "aiohttp-3.10.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:24fade6dae446b183e2410a8628b80df9b7a42205c6bfc2eff783cbeedc224a2"}, - {file = "aiohttp-3.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bc8e9f15939dacb0e1f2d15f9c41b786051c10472c7a926f5771e99b49a5957f"}, - {file = "aiohttp-3.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5a9ec959b5381271c8ec9310aae1713b2aec29efa32e232e5ef7dcca0df0279"}, - {file = "aiohttp-3.10.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2a5d0ea8a6467b15d53b00c4e8ea8811e47c3cc1bdbc62b1aceb3076403d551f"}, - {file = "aiohttp-3.10.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c9ed607dbbdd0d4d39b597e5bf6b0d40d844dfb0ac6a123ed79042ef08c1f87e"}, - {file = "aiohttp-3.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3e66d5b506832e56add66af88c288c1d5ba0c38b535a1a59e436b300b57b23e"}, - {file = "aiohttp-3.10.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fda91ad797e4914cca0afa8b6cccd5d2b3569ccc88731be202f6adce39503189"}, - {file = "aiohttp-3.10.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:61ccb867b2f2f53df6598eb2a93329b5eee0b00646ee79ea67d68844747a418e"}, - {file = "aiohttp-3.10.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6d881353264e6156f215b3cb778c9ac3184f5465c2ece5e6fce82e68946868ef"}, - {file = "aiohttp-3.10.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b031ce229114825f49cec4434fa844ccb5225e266c3e146cb4bdd025a6da52f1"}, - {file = "aiohttp-3.10.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5337cc742a03f9e3213b097abff8781f79de7190bbfaa987bd2b7ceb5bb0bdec"}, - {file = "aiohttp-3.10.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ab3361159fd3dcd0e48bbe804006d5cfb074b382666e6c064112056eb234f1a9"}, - {file = "aiohttp-3.10.3-cp311-cp311-win32.whl", hash = "sha256:05d66203a530209cbe40f102ebaac0b2214aba2a33c075d0bf825987c36f1f0b"}, - {file = "aiohttp-3.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:70b4a4984a70a2322b70e088d654528129783ac1ebbf7dd76627b3bd22db2f17"}, - {file = "aiohttp-3.10.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:166de65e2e4e63357cfa8417cf952a519ac42f1654cb2d43ed76899e2319b1ee"}, - {file = "aiohttp-3.10.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7084876352ba3833d5d214e02b32d794e3fd9cf21fdba99cff5acabeb90d9806"}, - {file = "aiohttp-3.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d98c604c93403288591d7d6d7d6cc8a63459168f8846aeffd5b3a7f3b3e5e09"}, - {file = "aiohttp-3.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d73b073a25a0bb8bf014345374fe2d0f63681ab5da4c22f9d2025ca3e3ea54fc"}, - {file = "aiohttp-3.10.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8da6b48c20ce78f5721068f383e0e113dde034e868f1b2f5ee7cb1e95f91db57"}, - {file = "aiohttp-3.10.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a9dcdccf50284b1b0dc72bc57e5bbd3cc9bf019060dfa0668f63241ccc16aa7"}, - {file = "aiohttp-3.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56fb94bae2be58f68d000d046172d8b8e6b1b571eb02ceee5535e9633dcd559c"}, - {file = "aiohttp-3.10.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bf75716377aad2c718cdf66451c5cf02042085d84522aec1f9246d3e4b8641a6"}, - {file = "aiohttp-3.10.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6c51ed03e19c885c8e91f574e4bbe7381793f56f93229731597e4a499ffef2a5"}, - {file = "aiohttp-3.10.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b84857b66fa6510a163bb083c1199d1ee091a40163cfcbbd0642495fed096204"}, - {file = "aiohttp-3.10.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c124b9206b1befe0491f48185fd30a0dd51b0f4e0e7e43ac1236066215aff272"}, - {file = "aiohttp-3.10.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3461d9294941937f07bbbaa6227ba799bc71cc3b22c40222568dc1cca5118f68"}, - {file = "aiohttp-3.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:08bd0754d257b2db27d6bab208c74601df6f21bfe4cb2ec7b258ba691aac64b3"}, - {file = "aiohttp-3.10.3-cp312-cp312-win32.whl", hash = "sha256:7f9159ae530297f61a00116771e57516f89a3de6ba33f314402e41560872b50a"}, - {file = "aiohttp-3.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:e1128c5d3a466279cb23c4aa32a0f6cb0e7d2961e74e9e421f90e74f75ec1edf"}, - {file = "aiohttp-3.10.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d1100e68e70eb72eadba2b932b185ebf0f28fd2f0dbfe576cfa9d9894ef49752"}, - {file = "aiohttp-3.10.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a541414578ff47c0a9b0b8b77381ea86b0c8531ab37fc587572cb662ccd80b88"}, - {file = "aiohttp-3.10.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d5548444ef60bf4c7b19ace21f032fa42d822e516a6940d36579f7bfa8513f9c"}, - {file = "aiohttp-3.10.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ba2e838b5e6a8755ac8297275c9460e729dc1522b6454aee1766c6de6d56e5e"}, - {file = "aiohttp-3.10.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:48665433bb59144aaf502c324694bec25867eb6630fcd831f7a893ca473fcde4"}, - {file = "aiohttp-3.10.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bac352fceed158620ce2d701ad39d4c1c76d114255a7c530e057e2b9f55bdf9f"}, - {file = "aiohttp-3.10.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b0f670502100cdc567188c49415bebba947eb3edaa2028e1a50dd81bd13363f"}, - {file = "aiohttp-3.10.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43b09f38a67679e32d380fe512189ccb0b25e15afc79b23fbd5b5e48e4fc8fd9"}, - {file = "aiohttp-3.10.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:cd788602e239ace64f257d1c9d39898ca65525583f0fbf0988bcba19418fe93f"}, - {file = "aiohttp-3.10.3-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:214277dcb07ab3875f17ee1c777d446dcce75bea85846849cc9d139ab8f5081f"}, - {file = "aiohttp-3.10.3-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:32007fdcaab789689c2ecaaf4b71f8e37bf012a15cd02c0a9db8c4d0e7989fa8"}, - {file = "aiohttp-3.10.3-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:123e5819bfe1b87204575515cf448ab3bf1489cdeb3b61012bde716cda5853e7"}, - {file = "aiohttp-3.10.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:812121a201f0c02491a5db335a737b4113151926a79ae9ed1a9f41ea225c0e3f"}, - {file = "aiohttp-3.10.3-cp38-cp38-win32.whl", hash = "sha256:b97dc9a17a59f350c0caa453a3cb35671a2ffa3a29a6ef3568b523b9113d84e5"}, - {file = "aiohttp-3.10.3-cp38-cp38-win_amd64.whl", hash = "sha256:3731a73ddc26969d65f90471c635abd4e1546a25299b687e654ea6d2fc052394"}, - {file = "aiohttp-3.10.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38d91b98b4320ffe66efa56cb0f614a05af53b675ce1b8607cdb2ac826a8d58e"}, - {file = "aiohttp-3.10.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9743fa34a10a36ddd448bba8a3adc2a66a1c575c3c2940301bacd6cc896c6bf1"}, - {file = "aiohttp-3.10.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7c126f532caf238031c19d169cfae3c6a59129452c990a6e84d6e7b198a001dc"}, - {file = "aiohttp-3.10.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:926e68438f05703e500b06fe7148ef3013dd6f276de65c68558fa9974eeb59ad"}, - {file = "aiohttp-3.10.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:434b3ab75833accd0b931d11874e206e816f6e6626fd69f643d6a8269cd9166a"}, - {file = "aiohttp-3.10.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d35235a44ec38109b811c3600d15d8383297a8fab8e3dec6147477ec8636712a"}, - {file = "aiohttp-3.10.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59c489661edbd863edb30a8bd69ecb044bd381d1818022bc698ba1b6f80e5dd1"}, - {file = "aiohttp-3.10.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50544fe498c81cb98912afabfc4e4d9d85e89f86238348e3712f7ca6a2f01dab"}, - {file = "aiohttp-3.10.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:09bc79275737d4dc066e0ae2951866bb36d9c6b460cb7564f111cc0427f14844"}, - {file = "aiohttp-3.10.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:af4dbec58e37f5afff4f91cdf235e8e4b0bd0127a2a4fd1040e2cad3369d2f06"}, - {file = "aiohttp-3.10.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b22cae3c9dd55a6b4c48c63081d31c00fc11fa9db1a20c8a50ee38c1a29539d2"}, - {file = "aiohttp-3.10.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ba562736d3fbfe9241dad46c1a8994478d4a0e50796d80e29d50cabe8fbfcc3f"}, - {file = "aiohttp-3.10.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f25d6c4e82d7489be84f2b1c8212fafc021b3731abdb61a563c90e37cced3a21"}, - {file = "aiohttp-3.10.3-cp39-cp39-win32.whl", hash = "sha256:b69d832e5f5fa15b1b6b2c8eb6a9fd2c0ec1fd7729cb4322ed27771afc9fc2ac"}, - {file = "aiohttp-3.10.3-cp39-cp39-win_amd64.whl", hash = "sha256:673bb6e3249dc8825df1105f6ef74e2eab779b7ff78e96c15cadb78b04a83752"}, - {file = "aiohttp-3.10.3.tar.gz", hash = "sha256:21650e7032cc2d31fc23d353d7123e771354f2a3d5b05a5647fc30fea214e696"}, + {file = "aiohttp-3.10.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:18a01eba2574fb9edd5f6e5fb25f66e6ce061da5dab5db75e13fe1558142e0a3"}, + {file = "aiohttp-3.10.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:94fac7c6e77ccb1ca91e9eb4cb0ac0270b9fb9b289738654120ba8cebb1189c6"}, + {file = "aiohttp-3.10.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2f1f1c75c395991ce9c94d3e4aa96e5c59c8356a15b1c9231e783865e2772699"}, + {file = "aiohttp-3.10.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f7acae3cf1a2a2361ec4c8e787eaaa86a94171d2417aae53c0cca6ca3118ff6"}, + {file = "aiohttp-3.10.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:94c4381ffba9cc508b37d2e536b418d5ea9cfdc2848b9a7fea6aebad4ec6aac1"}, + {file = "aiohttp-3.10.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c31ad0c0c507894e3eaa843415841995bf8de4d6b2d24c6e33099f4bc9fc0d4f"}, + {file = "aiohttp-3.10.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0912b8a8fadeb32ff67a3ed44249448c20148397c1ed905d5dac185b4ca547bb"}, + {file = "aiohttp-3.10.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d93400c18596b7dc4794d48a63fb361b01a0d8eb39f28800dc900c8fbdaca91"}, + {file = "aiohttp-3.10.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d00f3c5e0d764a5c9aa5a62d99728c56d455310bcc288a79cab10157b3af426f"}, + {file = "aiohttp-3.10.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d742c36ed44f2798c8d3f4bc511f479b9ceef2b93f348671184139e7d708042c"}, + {file = "aiohttp-3.10.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:814375093edae5f1cb31e3407997cf3eacefb9010f96df10d64829362ae2df69"}, + {file = "aiohttp-3.10.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8224f98be68a84b19f48e0bdc14224b5a71339aff3a27df69989fa47d01296f3"}, + {file = "aiohttp-3.10.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d9a487ef090aea982d748b1b0d74fe7c3950b109df967630a20584f9a99c0683"}, + {file = "aiohttp-3.10.5-cp310-cp310-win32.whl", hash = "sha256:d9ef084e3dc690ad50137cc05831c52b6ca428096e6deb3c43e95827f531d5ef"}, + {file = "aiohttp-3.10.5-cp310-cp310-win_amd64.whl", hash = "sha256:66bf9234e08fe561dccd62083bf67400bdbf1c67ba9efdc3dac03650e97c6088"}, + {file = "aiohttp-3.10.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8c6a4e5e40156d72a40241a25cc226051c0a8d816610097a8e8f517aeacd59a2"}, + {file = "aiohttp-3.10.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c634a3207a5445be65536d38c13791904fda0748b9eabf908d3fe86a52941cf"}, + {file = "aiohttp-3.10.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4aff049b5e629ef9b3e9e617fa6e2dfeda1bf87e01bcfecaf3949af9e210105e"}, + {file = "aiohttp-3.10.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1942244f00baaacaa8155eca94dbd9e8cc7017deb69b75ef67c78e89fdad3c77"}, + {file = "aiohttp-3.10.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e04a1f2a65ad2f93aa20f9ff9f1b672bf912413e5547f60749fa2ef8a644e061"}, + {file = "aiohttp-3.10.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7f2bfc0032a00405d4af2ba27f3c429e851d04fad1e5ceee4080a1c570476697"}, + {file = "aiohttp-3.10.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:424ae21498790e12eb759040bbb504e5e280cab64693d14775c54269fd1d2bb7"}, + {file = "aiohttp-3.10.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:975218eee0e6d24eb336d0328c768ebc5d617609affaca5dbbd6dd1984f16ed0"}, + {file = "aiohttp-3.10.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4120d7fefa1e2d8fb6f650b11489710091788de554e2b6f8347c7a20ceb003f5"}, + {file = "aiohttp-3.10.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b90078989ef3fc45cf9221d3859acd1108af7560c52397ff4ace8ad7052a132e"}, + {file = "aiohttp-3.10.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ba5a8b74c2a8af7d862399cdedce1533642fa727def0b8c3e3e02fcb52dca1b1"}, + {file = "aiohttp-3.10.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:02594361128f780eecc2a29939d9dfc870e17b45178a867bf61a11b2a4367277"}, + {file = "aiohttp-3.10.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8fb4fc029e135859f533025bc82047334e24b0d489e75513144f25408ecaf058"}, + {file = "aiohttp-3.10.5-cp311-cp311-win32.whl", hash = "sha256:e1ca1ef5ba129718a8fc827b0867f6aa4e893c56eb00003b7367f8a733a9b072"}, + {file = "aiohttp-3.10.5-cp311-cp311-win_amd64.whl", hash = "sha256:349ef8a73a7c5665cca65c88ab24abe75447e28aa3bc4c93ea5093474dfdf0ff"}, + {file = "aiohttp-3.10.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:305be5ff2081fa1d283a76113b8df7a14c10d75602a38d9f012935df20731487"}, + {file = "aiohttp-3.10.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3a1c32a19ee6bbde02f1cb189e13a71b321256cc1d431196a9f824050b160d5a"}, + {file = "aiohttp-3.10.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:61645818edd40cc6f455b851277a21bf420ce347baa0b86eaa41d51ef58ba23d"}, + {file = "aiohttp-3.10.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c225286f2b13bab5987425558baa5cbdb2bc925b2998038fa028245ef421e75"}, + {file = "aiohttp-3.10.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ba01ebc6175e1e6b7275c907a3a36be48a2d487549b656aa90c8a910d9f3178"}, + {file = "aiohttp-3.10.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8eaf44ccbc4e35762683078b72bf293f476561d8b68ec8a64f98cf32811c323e"}, + {file = "aiohttp-3.10.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1c43eb1ab7cbf411b8e387dc169acb31f0ca0d8c09ba63f9eac67829585b44f"}, + {file = "aiohttp-3.10.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de7a5299827253023c55ea549444e058c0eb496931fa05d693b95140a947cb73"}, + {file = "aiohttp-3.10.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4790f0e15f00058f7599dab2b206d3049d7ac464dc2e5eae0e93fa18aee9e7bf"}, + {file = "aiohttp-3.10.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:44b324a6b8376a23e6ba25d368726ee3bc281e6ab306db80b5819999c737d820"}, + {file = "aiohttp-3.10.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0d277cfb304118079e7044aad0b76685d30ecb86f83a0711fc5fb257ffe832ca"}, + {file = "aiohttp-3.10.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:54d9ddea424cd19d3ff6128601a4a4d23d54a421f9b4c0fff740505813739a91"}, + {file = "aiohttp-3.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4f1c9866ccf48a6df2b06823e6ae80573529f2af3a0992ec4fe75b1a510df8a6"}, + {file = "aiohttp-3.10.5-cp312-cp312-win32.whl", hash = "sha256:dc4826823121783dccc0871e3f405417ac116055bf184ac04c36f98b75aacd12"}, + {file = "aiohttp-3.10.5-cp312-cp312-win_amd64.whl", hash = "sha256:22c0a23a3b3138a6bf76fc553789cb1a703836da86b0f306b6f0dc1617398abc"}, + {file = "aiohttp-3.10.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7f6b639c36734eaa80a6c152a238242bedcee9b953f23bb887e9102976343092"}, + {file = "aiohttp-3.10.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f29930bc2921cef955ba39a3ff87d2c4398a0394ae217f41cb02d5c26c8b1b77"}, + {file = "aiohttp-3.10.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f489a2c9e6455d87eabf907ac0b7d230a9786be43fbe884ad184ddf9e9c1e385"}, + {file = "aiohttp-3.10.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:123dd5b16b75b2962d0fff566effb7a065e33cd4538c1692fb31c3bda2bfb972"}, + {file = "aiohttp-3.10.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b98e698dc34966e5976e10bbca6d26d6724e6bdea853c7c10162a3235aba6e16"}, + {file = "aiohttp-3.10.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3b9162bab7e42f21243effc822652dc5bb5e8ff42a4eb62fe7782bcbcdfacf6"}, + {file = "aiohttp-3.10.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1923a5c44061bffd5eebeef58cecf68096e35003907d8201a4d0d6f6e387ccaa"}, + {file = "aiohttp-3.10.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d55f011da0a843c3d3df2c2cf4e537b8070a419f891c930245f05d329c4b0689"}, + {file = "aiohttp-3.10.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:afe16a84498441d05e9189a15900640a2d2b5e76cf4efe8cbb088ab4f112ee57"}, + {file = "aiohttp-3.10.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f8112fb501b1e0567a1251a2fd0747baae60a4ab325a871e975b7bb67e59221f"}, + {file = "aiohttp-3.10.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1e72589da4c90337837fdfe2026ae1952c0f4a6e793adbbfbdd40efed7c63599"}, + {file = "aiohttp-3.10.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:4d46c7b4173415d8e583045fbc4daa48b40e31b19ce595b8d92cf639396c15d5"}, + {file = "aiohttp-3.10.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:33e6bc4bab477c772a541f76cd91e11ccb6d2efa2b8d7d7883591dfb523e5987"}, + {file = "aiohttp-3.10.5-cp313-cp313-win32.whl", hash = "sha256:c58c6837a2c2a7cf3133983e64173aec11f9c2cd8e87ec2fdc16ce727bcf1a04"}, + {file = "aiohttp-3.10.5-cp313-cp313-win_amd64.whl", hash = "sha256:38172a70005252b6893088c0f5e8a47d173df7cc2b2bd88650957eb84fcf5022"}, + {file = "aiohttp-3.10.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:f6f18898ace4bcd2d41a122916475344a87f1dfdec626ecde9ee802a711bc569"}, + {file = "aiohttp-3.10.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5ede29d91a40ba22ac1b922ef510aab871652f6c88ef60b9dcdf773c6d32ad7a"}, + {file = "aiohttp-3.10.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:673f988370f5954df96cc31fd99c7312a3af0a97f09e407399f61583f30da9bc"}, + {file = "aiohttp-3.10.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58718e181c56a3c02d25b09d4115eb02aafe1a732ce5714ab70326d9776457c3"}, + {file = "aiohttp-3.10.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b38b1570242fbab8d86a84128fb5b5234a2f70c2e32f3070143a6d94bc854cf"}, + {file = "aiohttp-3.10.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:074d1bff0163e107e97bd48cad9f928fa5a3eb4b9d33366137ffce08a63e37fe"}, + {file = "aiohttp-3.10.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd31f176429cecbc1ba499d4aba31aaccfea488f418d60376b911269d3b883c5"}, + {file = "aiohttp-3.10.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7384d0b87d4635ec38db9263e6a3f1eb609e2e06087f0aa7f63b76833737b471"}, + {file = "aiohttp-3.10.5-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8989f46f3d7ef79585e98fa991e6ded55d2f48ae56d2c9fa5e491a6e4effb589"}, + {file = "aiohttp-3.10.5-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:c83f7a107abb89a227d6c454c613e7606c12a42b9a4ca9c5d7dad25d47c776ae"}, + {file = "aiohttp-3.10.5-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cde98f323d6bf161041e7627a5fd763f9fd829bcfcd089804a5fdce7bb6e1b7d"}, + {file = "aiohttp-3.10.5-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:676f94c5480d8eefd97c0c7e3953315e4d8c2b71f3b49539beb2aa676c58272f"}, + {file = "aiohttp-3.10.5-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:2d21ac12dc943c68135ff858c3a989f2194a709e6e10b4c8977d7fcd67dfd511"}, + {file = "aiohttp-3.10.5-cp38-cp38-win32.whl", hash = "sha256:17e997105bd1a260850272bfb50e2a328e029c941c2708170d9d978d5a30ad9a"}, + {file = "aiohttp-3.10.5-cp38-cp38-win_amd64.whl", hash = "sha256:1c19de68896747a2aa6257ae4cf6ef59d73917a36a35ee9d0a6f48cff0f94db8"}, + {file = "aiohttp-3.10.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7e2fe37ac654032db1f3499fe56e77190282534810e2a8e833141a021faaab0e"}, + {file = "aiohttp-3.10.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f5bf3ead3cb66ab990ee2561373b009db5bc0e857549b6c9ba84b20bc462e172"}, + {file = "aiohttp-3.10.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1b2c16a919d936ca87a3c5f0e43af12a89a3ce7ccbce59a2d6784caba945b68b"}, + {file = "aiohttp-3.10.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad146dae5977c4dd435eb31373b3fe9b0b1bf26858c6fc452bf6af394067e10b"}, + {file = "aiohttp-3.10.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c5c6fa16412b35999320f5c9690c0f554392dc222c04e559217e0f9ae244b92"}, + {file = "aiohttp-3.10.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:95c4dc6f61d610bc0ee1edc6f29d993f10febfe5b76bb470b486d90bbece6b22"}, + {file = "aiohttp-3.10.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da452c2c322e9ce0cfef392e469a26d63d42860f829026a63374fde6b5c5876f"}, + {file = "aiohttp-3.10.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:898715cf566ec2869d5cb4d5fb4be408964704c46c96b4be267442d265390f32"}, + {file = "aiohttp-3.10.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:391cc3a9c1527e424c6865e087897e766a917f15dddb360174a70467572ac6ce"}, + {file = "aiohttp-3.10.5-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:380f926b51b92d02a34119d072f178d80bbda334d1a7e10fa22d467a66e494db"}, + {file = "aiohttp-3.10.5-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce91db90dbf37bb6fa0997f26574107e1b9d5ff939315247b7e615baa8ec313b"}, + {file = "aiohttp-3.10.5-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9093a81e18c45227eebe4c16124ebf3e0d893830c6aca7cc310bfca8fe59d857"}, + {file = "aiohttp-3.10.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ee40b40aa753d844162dcc80d0fe256b87cba48ca0054f64e68000453caead11"}, + {file = "aiohttp-3.10.5-cp39-cp39-win32.whl", hash = "sha256:03f2645adbe17f274444953bdea69f8327e9d278d961d85657cb0d06864814c1"}, + {file = "aiohttp-3.10.5-cp39-cp39-win_amd64.whl", hash = "sha256:d17920f18e6ee090bdd3d0bfffd769d9f2cb4c8ffde3eb203777a3895c128862"}, + {file = "aiohttp-3.10.5.tar.gz", hash = "sha256:f071854b47d39591ce9a17981c46790acb30518e2f83dfca8db2dfa091178691"}, ] [package.dependencies] @@ -238,13 +253,13 @@ tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] [[package]] name = "authlib" -version = "1.3.1" +version = "1.3.2" description = "The ultimate Python library in building OAuth and OpenID Connect servers and clients." optional = false python-versions = ">=3.8" files = [ - {file = "Authlib-1.3.1-py2.py3-none-any.whl", hash = "sha256:d35800b973099bbadc49b42b256ecb80041ad56b7fe1216a362c7943c088f377"}, - {file = "authlib-1.3.1.tar.gz", hash = "sha256:7ae843f03c06c5c0debd63c9db91f9fda64fa62a42a77419fa15fbb7e7a58917"}, + {file = "Authlib-1.3.2-py2.py3-none-any.whl", hash = "sha256:ede026a95e9f5cdc2d4364a52103f5405e75aa156357e831ef2bfd0bc5094dfc"}, + {file = "authlib-1.3.2.tar.gz", hash = "sha256:4b16130117f9eb82aa6eec97f6dd4673c3f960ac0283ccdae2897ee4bc030ba2"}, ] [package.dependencies] @@ -360,13 +375,13 @@ isodate = ">=0.6.1,<1.0.0" [[package]] name = "azure-mgmt-compute" -version = "32.0.0" +version = "33.0.0" description = "Microsoft Azure Compute Management Client Library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "azure-mgmt-compute-32.0.0.tar.gz", hash = "sha256:8d5a86e0116c71a07bcedd8e69d2e09270db3880932656521f3143c6f9475072"}, - {file = "azure_mgmt_compute-32.0.0-py3-none-any.whl", hash = "sha256:8578dbeee034a58c41331a71ddd2503e1e5c65a2cc233ebfe9adc5e16ca3d037"}, + {file = "azure-mgmt-compute-33.0.0.tar.gz", hash = "sha256:a3cc0fe4f09c8e1d3523c1bfb92620dfe263a0a893b0ac13a33d7057e9ddddd2"}, + {file = "azure_mgmt_compute-33.0.0-py3-none-any.whl", hash = "sha256:155f8d78a1fdedcea1725fd12b85b2d87fbcb6b53f8e77451c644f45701e3bcf"}, ] [package.dependencies] @@ -569,19 +584,20 @@ msrest = ">=0.7.1" [[package]] name = "azure-mgmt-web" -version = "7.3.0" +version = "7.3.1" description = "Microsoft Azure Web Apps Management Client Library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "azure-mgmt-web-7.3.0.tar.gz", hash = "sha256:2032bf4d50df0bbb822ea00cac3bfbe0e67487d2c9cc1182c0858f83149cdea9"}, - {file = "azure_mgmt_web-7.3.0-py3-none-any.whl", hash = "sha256:0e53db3fea6eae0d68d9a94eef2e19301f80d3d8a8a20eab599b75384ac0296e"}, + {file = "azure-mgmt-web-7.3.1.tar.gz", hash = "sha256:87b771436bc99a7a8df59d0ad185b96879a06dce14764a06b3fc3dafa8fcb56b"}, + {file = "azure_mgmt_web-7.3.1-py3-none-any.whl", hash = "sha256:ccf881e3ab31c3fdbf9cbff32773d9c0006b5dcd621ea074d7ec89e51049fb72"}, ] [package.dependencies] azure-common = ">=1.1" azure-mgmt-core = ">=1.3.2" isodate = ">=0.6.1" +typing-extensions = ">=4.6.0" [[package]] name = "azure-storage-blob" @@ -651,17 +667,17 @@ files = [ [[package]] name = "boto3" -version = "1.34.158" +version = "1.34.162" description = "The AWS SDK for Python" optional = false python-versions = ">=3.8" files = [ - {file = "boto3-1.34.158-py3-none-any.whl", hash = "sha256:c29e9b7e1034e8734ccaffb9f2b3f3df2268022fd8a93d836604019f8759ce27"}, - {file = "boto3-1.34.158.tar.gz", hash = "sha256:5b7b2ce0ec1e498933f600d29f3e1c641f8c44dd7e468c26795359d23d81fa39"}, + {file = "boto3-1.34.162-py3-none-any.whl", hash = "sha256:d6f6096bdab35a0c0deff469563b87d184a28df7689790f7fe7be98502b7c590"}, + {file = "boto3-1.34.162.tar.gz", hash = "sha256:873f8f5d2f6f85f1018cbb0535b03cceddc7b655b61f66a0a56995238804f41f"}, ] [package.dependencies] -botocore = ">=1.34.158,<1.35.0" +botocore = ">=1.34.162,<1.35.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.10.0,<0.11.0" @@ -670,13 +686,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.34.159" +version = "1.34.162" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.8" files = [ - {file = "botocore-1.34.159-py3-none-any.whl", hash = "sha256:7633062491457419a49f5860c014251ae85689f78266a3ce020c2c8688a76b97"}, - {file = "botocore-1.34.159.tar.gz", hash = "sha256:dc28806eb21e3c8d690c422530dff8b4b242ac033cbe98f160a9d37796c09cb1"}, + {file = "botocore-1.34.162-py3-none-any.whl", hash = "sha256:2d918b02db88d27a75b48275e6fb2506e9adaaddbec1ffa6a8a0898b34e769be"}, + {file = "botocore-1.34.162.tar.gz", hash = "sha256:adc23be4fb99ad31961236342b7cbf3c0bfc62532cd02852196032e8c0d682f3"}, ] [package.dependencies] @@ -689,13 +705,13 @@ crt = ["awscrt (==0.21.2)"] [[package]] name = "cachetools" -version = "5.4.0" +version = "5.5.0" description = "Extensible memoizing collections and decorators" optional = false python-versions = ">=3.7" files = [ - {file = "cachetools-5.4.0-py3-none-any.whl", hash = "sha256:3ae3b49a3d5e28a77a0be2b37dbcb89005058959cb2323858c2657c4a8cab474"}, - {file = "cachetools-5.4.0.tar.gz", hash = "sha256:b8adc2e7c07f105ced7bc56dbb6dfbe7c4a00acce20e2227b3f355be89bc6827"}, + {file = "cachetools-5.5.0-py3-none-any.whl", hash = "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292"}, + {file = "cachetools-5.5.0.tar.gz", hash = "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a"}, ] [[package]] @@ -1711,13 +1727,13 @@ files = [ [[package]] name = "google-api-core" -version = "2.19.1" +version = "2.19.2" description = "Google API client core library" optional = false python-versions = ">=3.7" files = [ - {file = "google-api-core-2.19.1.tar.gz", hash = "sha256:f4695f1e3650b316a795108a76a1c416e6afb036199d1c1f1f110916df479ffd"}, - {file = "google_api_core-2.19.1-py3-none-any.whl", hash = "sha256:f12a9b8309b5e21d92483bbd47ce2c445861ec7d269ef6784ecc0ea8c1fa6125"}, + {file = "google_api_core-2.19.2-py3-none-any.whl", hash = "sha256:53ec0258f2837dd53bbd3d3df50f5359281b3cc13f800c941dd15a9b5a415af4"}, + {file = "google_api_core-2.19.2.tar.gz", hash = "sha256:ca07de7e8aa1c98a8bfca9321890ad2340ef7f2eb136e558cee68f24b94b0a8f"}, ] [package.dependencies] @@ -1734,13 +1750,13 @@ grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] [[package]] name = "google-api-python-client" -version = "2.140.0" +version = "2.143.0" description = "Google API Client Library for Python" optional = false python-versions = ">=3.7" files = [ - {file = "google_api_python_client-2.140.0-py2.py3-none-any.whl", hash = "sha256:aeb4bb99e9fdd241473da5ff35464a0658fea0db76fe89c0f8c77ecfc3813404"}, - {file = "google_api_python_client-2.140.0.tar.gz", hash = "sha256:0bb973adccbe66a3d0a70abe4e49b3f2f004d849416bfec38d22b75649d389d8"}, + {file = "google_api_python_client-2.143.0-py2.py3-none-any.whl", hash = "sha256:d5654134522b9b574b82234e96f7e0aeeabcbf33643fbabcd449ef0068e3a476"}, + {file = "google_api_python_client-2.143.0.tar.gz", hash = "sha256:6a75441f9078e6e2fcdf4946a153fda1e2cc81b5e9c8d6e8c0750c85c7f8a566"}, ] [package.dependencies] @@ -1752,13 +1768,13 @@ uritemplate = ">=3.0.1,<5" [[package]] name = "google-auth" -version = "2.33.0" +version = "2.34.0" description = "Google Authentication Library" optional = false python-versions = ">=3.7" files = [ - {file = "google_auth-2.33.0-py2.py3-none-any.whl", hash = "sha256:8eff47d0d4a34ab6265c50a106a3362de6a9975bb08998700e389f857e4d39df"}, - {file = "google_auth-2.33.0.tar.gz", hash = "sha256:d6a52342160d7290e334b4d47ba390767e4438ad0d45b7630774533e82655b95"}, + {file = "google_auth-2.34.0-py2.py3-none-any.whl", hash = "sha256:72fd4733b80b6d777dcde515628a9eb4a577339437012874ea286bca7261ee65"}, + {file = "google_auth-2.34.0.tar.gz", hash = "sha256:8eb87396435c19b20d32abd2f984e31c191a15284af72eb922f10e5bde9c04cc"}, ] [package.dependencies] @@ -1768,7 +1784,7 @@ rsa = ">=3.1.4,<5" [package.extras] aiohttp = ["aiohttp (>=3.6.2,<4.0.0.dev0)", "requests (>=2.20.0,<3.0.0.dev0)"] -enterprise-cert = ["cryptography (==36.0.2)", "pyopenssl (==22.0.0)"] +enterprise-cert = ["cryptography", "pyopenssl"] pyopenssl = ["cryptography (>=38.0.3)", "pyopenssl (>=20.0.0)"] reauth = ["pyu2f (>=0.1.5)"] requests = ["requests (>=2.20.0,<3.0.0.dev0)"] @@ -1790,13 +1806,13 @@ httplib2 = ">=0.19.0" [[package]] name = "googleapis-common-protos" -version = "1.63.2" +version = "1.65.0" description = "Common protobufs used in Google APIs" optional = false python-versions = ">=3.7" files = [ - {file = "googleapis-common-protos-1.63.2.tar.gz", hash = "sha256:27c5abdffc4911f28101e635de1533fb4cfd2c37fbaa9174587c799fac90aa87"}, - {file = "googleapis_common_protos-1.63.2-py2.py3-none-any.whl", hash = "sha256:27a2499c7e8aff199665b22741997e485eccc8645aa9176c7c988e6fae507945"}, + {file = "googleapis_common_protos-1.65.0-py2.py3-none-any.whl", hash = "sha256:2972e6c496f435b92590fd54045060867f3fe9be2c82ab148fc8885035479a63"}, + {file = "googleapis_common_protos-1.65.0.tar.gz", hash = "sha256:334a29d07cddc3aa01dee4988f9afd9b2916ee2ff49d6b757155dc0d197852c0"}, ] [package.dependencies] @@ -1913,13 +1929,13 @@ pyparsing = {version = ">=2.4.2,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.0.2 || >3.0 [[package]] name = "httpx" -version = "0.27.0" +version = "0.27.2" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" files = [ - {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, - {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, + {file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"}, + {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"}, ] [package.dependencies] @@ -1935,6 +1951,7 @@ brotli = ["brotli", "brotlicffi"] cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] [[package]] name = "hyperframe" @@ -1949,24 +1966,24 @@ files = [ [[package]] name = "idna" -version = "3.7" +version = "3.8" description = "Internationalized Domain Names in Applications (IDNA)" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" files = [ - {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, - {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, + {file = "idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac"}, + {file = "idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603"}, ] [[package]] name = "importlib-metadata" -version = "8.0.0" +version = "8.4.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "importlib_metadata-8.0.0-py3-none-any.whl", hash = "sha256:15584cf2b1bf449d98ff8a6ff1abef57bf20f3ac6454f431736cd3e660921b2f"}, - {file = "importlib_metadata-8.0.0.tar.gz", hash = "sha256:188bd24e4c346d3f0a933f275c2fec67050326a856b9a359881d7c2a697e8812"}, + {file = "importlib_metadata-8.4.0-py3-none-any.whl", hash = "sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1"}, + {file = "importlib_metadata-8.4.0.tar.gz", hash = "sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5"}, ] [package.dependencies] @@ -2254,13 +2271,13 @@ files = [ [[package]] name = "marshmallow" -version = "3.21.3" +version = "3.22.0" description = "A lightweight library for converting complex datatypes to and from native Python datatypes." optional = false python-versions = ">=3.8" files = [ - {file = "marshmallow-3.21.3-py3-none-any.whl", hash = "sha256:86ce7fb914aa865001a4b2092c4c2872d13bc347f3d42673272cabfdbad386f1"}, - {file = "marshmallow-3.21.3.tar.gz", hash = "sha256:4f57c5e050a54d66361e826f94fba213eb10b67b2fdb02c3e0343ce207ba1662"}, + {file = "marshmallow-3.22.0-py3-none-any.whl", hash = "sha256:71a2dce49ef901c3f97ed296ae5051135fd3febd2bf43afe0ae9a82143a494d9"}, + {file = "marshmallow-3.22.0.tar.gz", hash = "sha256:4972f529104a220bb8637d595aa4c9762afbe7f7a77d82dc58c1615d70c5823e"}, ] [package.dependencies] @@ -2268,7 +2285,7 @@ packaging = ">=17.0" [package.extras] dev = ["marshmallow[tests]", "pre-commit (>=3.5,<4.0)", "tox"] -docs = ["alabaster (==0.7.16)", "autodocsumm (==0.2.12)", "sphinx (==7.3.7)", "sphinx-issues (==4.1.0)", "sphinx-version-warning (==1.1.2)"] +docs = ["alabaster (==1.0.0)", "autodocsumm (==0.2.13)", "sphinx (==8.0.2)", "sphinx-issues (==4.1.0)", "sphinx-version-warning (==1.1.2)"] tests = ["pytest", "pytz", "simplejson"] [[package]] @@ -2311,13 +2328,13 @@ std-uritemplate = ">=0.0.38" [[package]] name = "microsoft-kiota-authentication-azure" -version = "1.0.0" +version = "1.1.0" description = "Authentication provider for Kiota using Azure Identity" optional = false python-versions = "*" files = [ - {file = "microsoft_kiota_authentication_azure-1.0.0-py2.py3-none-any.whl", hash = "sha256:289fe002951ae661415a6d3fa7c422c096b739165acb32d786316988120a1b27"}, - {file = "microsoft_kiota_authentication_azure-1.0.0.tar.gz", hash = "sha256:752304f8d94b884cfec12583dd763ec0478805c7f80b29344e78c6d55a97bd01"}, + {file = "microsoft_kiota_authentication_azure-1.1.0-py2.py3-none-any.whl", hash = "sha256:22ecbf7e89120aca525cb3f89576617e6c6601b20b06ac720a8e66175698cf84"}, + {file = "microsoft_kiota_authentication_azure-1.1.0.tar.gz", hash = "sha256:8940084a3c1c25d1cde1d3b193ef7164dc6323e3b8200e2f695cbc937797bac6"}, ] [package.dependencies] @@ -2346,13 +2363,13 @@ opentelemetry-sdk = ">=1.20.0" [[package]] name = "microsoft-kiota-serialization-form" -version = "0.1.0" +version = "0.1.1" description = "Implementation of Kiota Serialization Interfaces for URI-Form encoded serialization" optional = false python-versions = "*" files = [ - {file = "microsoft_kiota_serialization_form-0.1.0-py2.py3-none-any.whl", hash = "sha256:5bc76fb2fc67d7c1f878f876d252ea814e4fc38df505099b9b86de52d974380a"}, - {file = "microsoft_kiota_serialization_form-0.1.0.tar.gz", hash = "sha256:663ece0cb1a41fe9ddfc9195aa3f15f219e14d2a1ee51e98c53ad8d795b2785d"}, + {file = "microsoft_kiota_serialization_form-0.1.1-py2.py3-none-any.whl", hash = "sha256:7a2da956edd3329ff74ec1a34eded910fd9cda57ce8c71f3797e359adc9993f7"}, + {file = "microsoft_kiota_serialization_form-0.1.1.tar.gz", hash = "sha256:f72dd50081250d1e49111682eccf620d3c2e335195d50c096b35ec5a5ef59a54"}, ] [package.dependencies] @@ -2361,13 +2378,13 @@ pendulum = ">=3.0.0" [[package]] name = "microsoft-kiota-serialization-json" -version = "1.3.0" +version = "1.3.1" description = "Implementation of Kiota Serialization interfaces for JSON" optional = false python-versions = "*" files = [ - {file = "microsoft_kiota_serialization_json-1.3.0-py2.py3-none-any.whl", hash = "sha256:fbf82835d8b77ef21b496aa711a512fe4494fa94dfe88f7fd014dffe33778e20"}, - {file = "microsoft_kiota_serialization_json-1.3.0.tar.gz", hash = "sha256:235b680e6eb646479ffb7b59d2a6f0216c4f7e1c2ff1219fd4d59e898fa6b124"}, + {file = "microsoft_kiota_serialization_json-1.3.1-py2.py3-none-any.whl", hash = "sha256:147f4413d1b3c8f622cc16fc228d739116de1af2dfe2c684c6220351ba73a4b4"}, + {file = "microsoft_kiota_serialization_json-1.3.1.tar.gz", hash = "sha256:17449cb935f9301098c14b659b3bf8ba2fc49a133808419e2e9946a25f277641"}, ] [package.dependencies] @@ -2672,56 +2689,56 @@ files = [ [[package]] name = "numpy" -version = "2.0.1" +version = "2.0.2" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.9" files = [ - {file = "numpy-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fbb536eac80e27a2793ffd787895242b7f18ef792563d742c2d673bfcb75134"}, - {file = "numpy-2.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:69ff563d43c69b1baba77af455dd0a839df8d25e8590e79c90fcbe1499ebde42"}, - {file = "numpy-2.0.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:1b902ce0e0a5bb7704556a217c4f63a7974f8f43e090aff03fcf262e0b135e02"}, - {file = "numpy-2.0.1-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:f1659887361a7151f89e79b276ed8dff3d75877df906328f14d8bb40bb4f5101"}, - {file = "numpy-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4658c398d65d1b25e1760de3157011a80375da861709abd7cef3bad65d6543f9"}, - {file = "numpy-2.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4127d4303b9ac9f94ca0441138acead39928938660ca58329fe156f84b9f3015"}, - {file = "numpy-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e5eeca8067ad04bc8a2a8731183d51d7cbaac66d86085d5f4766ee6bf19c7f87"}, - {file = "numpy-2.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9adbd9bb520c866e1bfd7e10e1880a1f7749f1f6e5017686a5fbb9b72cf69f82"}, - {file = "numpy-2.0.1-cp310-cp310-win32.whl", hash = "sha256:7b9853803278db3bdcc6cd5beca37815b133e9e77ff3d4733c247414e78eb8d1"}, - {file = "numpy-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:81b0893a39bc5b865b8bf89e9ad7807e16717f19868e9d234bdaf9b1f1393868"}, - {file = "numpy-2.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75b4e316c5902d8163ef9d423b1c3f2f6252226d1aa5cd8a0a03a7d01ffc6268"}, - {file = "numpy-2.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6e4eeb6eb2fced786e32e6d8df9e755ce5be920d17f7ce00bc38fcde8ccdbf9e"}, - {file = "numpy-2.0.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a1e01dcaab205fbece13c1410253a9eea1b1c9b61d237b6fa59bcc46e8e89343"}, - {file = "numpy-2.0.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:a8fc2de81ad835d999113ddf87d1ea2b0f4704cbd947c948d2f5513deafe5a7b"}, - {file = "numpy-2.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a3d94942c331dd4e0e1147f7a8699a4aa47dffc11bf8a1523c12af8b2e91bbe"}, - {file = "numpy-2.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15eb4eca47d36ec3f78cde0a3a2ee24cf05ca7396ef808dda2c0ddad7c2bde67"}, - {file = "numpy-2.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b83e16a5511d1b1f8a88cbabb1a6f6a499f82c062a4251892d9ad5d609863fb7"}, - {file = "numpy-2.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f87fec1f9bc1efd23f4227becff04bd0e979e23ca50cc92ec88b38489db3b55"}, - {file = "numpy-2.0.1-cp311-cp311-win32.whl", hash = "sha256:36d3a9405fd7c511804dc56fc32974fa5533bdeb3cd1604d6b8ff1d292b819c4"}, - {file = "numpy-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:08458fbf403bff5e2b45f08eda195d4b0c9b35682311da5a5a0a0925b11b9bd8"}, - {file = "numpy-2.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6bf4e6f4a2a2e26655717a1983ef6324f2664d7011f6ef7482e8c0b3d51e82ac"}, - {file = "numpy-2.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6fddc5fe258d3328cd8e3d7d3e02234c5d70e01ebe377a6ab92adb14039cb4"}, - {file = "numpy-2.0.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5daab361be6ddeb299a918a7c0864fa8618af66019138263247af405018b04e1"}, - {file = "numpy-2.0.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:ea2326a4dca88e4a274ba3a4405eb6c6467d3ffbd8c7d38632502eaae3820587"}, - {file = "numpy-2.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529af13c5f4b7a932fb0e1911d3a75da204eff023ee5e0e79c1751564221a5c8"}, - {file = "numpy-2.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6790654cb13eab303d8402354fabd47472b24635700f631f041bd0b65e37298a"}, - {file = "numpy-2.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cbab9fc9c391700e3e1287666dfd82d8666d10e69a6c4a09ab97574c0b7ee0a7"}, - {file = "numpy-2.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:99d0d92a5e3613c33a5f01db206a33f8fdf3d71f2912b0de1739894668b7a93b"}, - {file = "numpy-2.0.1-cp312-cp312-win32.whl", hash = "sha256:173a00b9995f73b79eb0191129f2455f1e34c203f559dd118636858cc452a1bf"}, - {file = "numpy-2.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:bb2124fdc6e62baae159ebcfa368708867eb56806804d005860b6007388df171"}, - {file = "numpy-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bfc085b28d62ff4009364e7ca34b80a9a080cbd97c2c0630bb5f7f770dae9414"}, - {file = "numpy-2.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8fae4ebbf95a179c1156fab0b142b74e4ba4204c87bde8d3d8b6f9c34c5825ef"}, - {file = "numpy-2.0.1-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:72dc22e9ec8f6eaa206deb1b1355eb2e253899d7347f5e2fae5f0af613741d06"}, - {file = "numpy-2.0.1-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:ec87f5f8aca726117a1c9b7083e7656a9d0d606eec7299cc067bb83d26f16e0c"}, - {file = "numpy-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f682ea61a88479d9498bf2091fdcd722b090724b08b31d63e022adc063bad59"}, - {file = "numpy-2.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8efc84f01c1cd7e34b3fb310183e72fcdf55293ee736d679b6d35b35d80bba26"}, - {file = "numpy-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3fdabe3e2a52bc4eff8dc7a5044342f8bd9f11ef0934fcd3289a788c0eb10018"}, - {file = "numpy-2.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:24a0e1befbfa14615b49ba9659d3d8818a0f4d8a1c5822af8696706fbda7310c"}, - {file = "numpy-2.0.1-cp39-cp39-win32.whl", hash = "sha256:f9cf5ea551aec449206954b075db819f52adc1638d46a6738253a712d553c7b4"}, - {file = "numpy-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:e9e81fa9017eaa416c056e5d9e71be93d05e2c3c2ab308d23307a8bc4443c368"}, - {file = "numpy-2.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:61728fba1e464f789b11deb78a57805c70b2ed02343560456190d0501ba37b0f"}, - {file = "numpy-2.0.1-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:12f5d865d60fb9734e60a60f1d5afa6d962d8d4467c120a1c0cda6eb2964437d"}, - {file = "numpy-2.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eacf3291e263d5a67d8c1a581a8ebbcfd6447204ef58828caf69a5e3e8c75990"}, - {file = "numpy-2.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2c3a346ae20cfd80b6cfd3e60dc179963ef2ea58da5ec074fd3d9e7a1e7ba97f"}, - {file = "numpy-2.0.1.tar.gz", hash = "sha256:485b87235796410c3519a699cfe1faab097e509e90ebb05dcd098db2ae87e7b3"}, + {file = "numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece"}, + {file = "numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04"}, + {file = "numpy-2.0.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8c5713284ce4e282544c68d1c3b2c7161d38c256d2eefc93c1d683cf47683e66"}, + {file = "numpy-2.0.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:becfae3ddd30736fe1889a37f1f580e245ba79a5855bff5f2a29cb3ccc22dd7b"}, + {file = "numpy-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2da5960c3cf0df7eafefd806d4e612c5e19358de82cb3c343631188991566ccd"}, + {file = "numpy-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:496f71341824ed9f3d2fd36cf3ac57ae2e0165c143b55c3a035ee219413f3318"}, + {file = "numpy-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a61ec659f68ae254e4d237816e33171497e978140353c0c2038d46e63282d0c8"}, + {file = "numpy-2.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d731a1c6116ba289c1e9ee714b08a8ff882944d4ad631fd411106a30f083c326"}, + {file = "numpy-2.0.2-cp310-cp310-win32.whl", hash = "sha256:984d96121c9f9616cd33fbd0618b7f08e0cfc9600a7ee1d6fd9b239186d19d97"}, + {file = "numpy-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:c7b0be4ef08607dd04da4092faee0b86607f111d5ae68036f16cc787e250a131"}, + {file = "numpy-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448"}, + {file = "numpy-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195"}, + {file = "numpy-2.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57"}, + {file = "numpy-2.0.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a"}, + {file = "numpy-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669"}, + {file = "numpy-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951"}, + {file = "numpy-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9"}, + {file = "numpy-2.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15"}, + {file = "numpy-2.0.2-cp311-cp311-win32.whl", hash = "sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4"}, + {file = "numpy-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc"}, + {file = "numpy-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:df55d490dea7934f330006d0f81e8551ba6010a5bf035a249ef61a94f21c500b"}, + {file = "numpy-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8df823f570d9adf0978347d1f926b2a867d5608f434a7cff7f7908c6570dcf5e"}, + {file = "numpy-2.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9a92ae5c14811e390f3767053ff54eaee3bf84576d99a2456391401323f4ec2c"}, + {file = "numpy-2.0.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a842d573724391493a97a62ebbb8e731f8a5dcc5d285dfc99141ca15a3302d0c"}, + {file = "numpy-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05e238064fc0610c840d1cf6a13bf63d7e391717d247f1bf0318172e759e692"}, + {file = "numpy-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0123ffdaa88fa4ab64835dcbde75dcdf89c453c922f18dced6e27c90d1d0ec5a"}, + {file = "numpy-2.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:96a55f64139912d61de9137f11bf39a55ec8faec288c75a54f93dfd39f7eb40c"}, + {file = "numpy-2.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded"}, + {file = "numpy-2.0.2-cp312-cp312-win32.whl", hash = "sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5"}, + {file = "numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a"}, + {file = "numpy-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9059e10581ce4093f735ed23f3b9d283b9d517ff46009ddd485f1747eb22653c"}, + {file = "numpy-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:423e89b23490805d2a5a96fe40ec507407b8ee786d66f7328be214f9679df6dd"}, + {file = "numpy-2.0.2-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:2b2955fa6f11907cf7a70dab0d0755159bca87755e831e47932367fc8f2f2d0b"}, + {file = "numpy-2.0.2-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:97032a27bd9d8988b9a97a8c4d2c9f2c15a81f61e2f21404d7e8ef00cb5be729"}, + {file = "numpy-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e795a8be3ddbac43274f18588329c72939870a16cae810c2b73461c40718ab1"}, + {file = "numpy-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b258c385842546006213344c50655ff1555a9338e2e5e02a0756dc3e803dd"}, + {file = "numpy-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fec9451a7789926bcf7c2b8d187292c9f93ea30284802a0ab3f5be8ab36865d"}, + {file = "numpy-2.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9189427407d88ff25ecf8f12469d4d39d35bee1db5d39fc5c168c6f088a6956d"}, + {file = "numpy-2.0.2-cp39-cp39-win32.whl", hash = "sha256:905d16e0c60200656500c95b6b8dca5d109e23cb24abc701d41c02d74c6b3afa"}, + {file = "numpy-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:a3f4ab0caa7f053f6797fcd4e1e25caee367db3112ef2b6ef82d749530768c73"}, + {file = "numpy-2.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7f0a0c6f12e07fa94133c8a67404322845220c06a9e80e85999afe727f7438b8"}, + {file = "numpy-2.0.2-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:312950fdd060354350ed123c0e25a71327d3711584beaef30cdaa93320c392d4"}, + {file = "numpy-2.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26df23238872200f63518dd2aa984cfca675d82469535dc7162dc2ee52d9dd5c"}, + {file = "numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385"}, + {file = "numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78"}, ] [[package]] @@ -2742,49 +2759,49 @@ signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] [[package]] name = "opentelemetry-api" -version = "1.26.0" +version = "1.27.0" description = "OpenTelemetry Python API" optional = false python-versions = ">=3.8" files = [ - {file = "opentelemetry_api-1.26.0-py3-none-any.whl", hash = "sha256:7d7ea33adf2ceda2dd680b18b1677e4152000b37ca76e679da71ff103b943064"}, - {file = "opentelemetry_api-1.26.0.tar.gz", hash = "sha256:2bd639e4bed5b18486fef0b5a520aaffde5a18fc225e808a1ac4df363f43a1ce"}, + {file = "opentelemetry_api-1.27.0-py3-none-any.whl", hash = "sha256:953d5871815e7c30c81b56d910c707588000fff7a3ca1c73e6531911d53065e7"}, + {file = "opentelemetry_api-1.27.0.tar.gz", hash = "sha256:ed673583eaa5f81b5ce5e86ef7cdaf622f88ef65f0b9aab40b843dcae5bef342"}, ] [package.dependencies] deprecated = ">=1.2.6" -importlib-metadata = ">=6.0,<=8.0.0" +importlib-metadata = ">=6.0,<=8.4.0" [[package]] name = "opentelemetry-sdk" -version = "1.26.0" +version = "1.27.0" description = "OpenTelemetry Python SDK" optional = false python-versions = ">=3.8" files = [ - {file = "opentelemetry_sdk-1.26.0-py3-none-any.whl", hash = "sha256:feb5056a84a88670c041ea0ded9921fca559efec03905dddeb3885525e0af897"}, - {file = "opentelemetry_sdk-1.26.0.tar.gz", hash = "sha256:c90d2868f8805619535c05562d699e2f4fb1f00dbd55a86dcefca4da6fa02f85"}, + {file = "opentelemetry_sdk-1.27.0-py3-none-any.whl", hash = "sha256:365f5e32f920faf0fd9e14fdfd92c086e317eaa5f860edba9cdc17a380d9197d"}, + {file = "opentelemetry_sdk-1.27.0.tar.gz", hash = "sha256:d525017dea0ccce9ba4e0245100ec46ecdc043f2d7b8315d56b19aff0904fa6f"}, ] [package.dependencies] -opentelemetry-api = "1.26.0" -opentelemetry-semantic-conventions = "0.47b0" +opentelemetry-api = "1.27.0" +opentelemetry-semantic-conventions = "0.48b0" typing-extensions = ">=3.7.4" [[package]] name = "opentelemetry-semantic-conventions" -version = "0.47b0" +version = "0.48b0" description = "OpenTelemetry Semantic Conventions" optional = false python-versions = ">=3.8" files = [ - {file = "opentelemetry_semantic_conventions-0.47b0-py3-none-any.whl", hash = "sha256:4ff9d595b85a59c1c1413f02bba320ce7ea6bf9e2ead2b0913c4395c7bbc1063"}, - {file = "opentelemetry_semantic_conventions-0.47b0.tar.gz", hash = "sha256:a8d57999bbe3495ffd4d510de26a97dadc1dace53e0275001b2c1b2f67992a7e"}, + {file = "opentelemetry_semantic_conventions-0.48b0-py3-none-any.whl", hash = "sha256:a0de9f45c413a8669788a38569c7e0a11ce6ce97861a628cca785deecdc32a1f"}, + {file = "opentelemetry_semantic_conventions-0.48b0.tar.gz", hash = "sha256:12d74983783b6878162208be57c9effcb89dc88691c64992d70bb89dc00daa1a"}, ] [package.dependencies] deprecated = ">=1.2.6" -opentelemetry-api = "1.26.0" +opentelemetry-api = "1.27.0" [[package]] name = "packaging" @@ -2871,13 +2888,13 @@ xml = ["lxml (>=4.9.2)"] [[package]] name = "pbr" -version = "6.0.0" +version = "6.1.0" description = "Python Build Reasonableness" optional = false python-versions = ">=2.6" files = [ - {file = "pbr-6.0.0-py2.py3-none-any.whl", hash = "sha256:4a7317d5e3b17a3dccb6a8cfe67dab65b20551404c52c8ed41279fa4f0cb4cda"}, - {file = "pbr-6.0.0.tar.gz", hash = "sha256:d1377122a5a00e2f940ee482999518efe16d745d423a670c27773dfbc3c9a7d9"}, + {file = "pbr-6.1.0-py2.py3-none-any.whl", hash = "sha256:a776ae228892d8013649c0aeccbb3d5f99ee15e005a4cbb7e61d55a067b28a2a"}, + {file = "pbr-6.1.0.tar.gz", hash = "sha256:788183e382e3d1d7707db08978239965e8b9e4e5ed42669bf4758186734d5f24"}, ] [[package]] @@ -3077,22 +3094,22 @@ testing = ["google-api-core (>=1.31.5)"] [[package]] name = "protobuf" -version = "5.27.3" +version = "5.28.0" description = "" optional = false python-versions = ">=3.8" files = [ - {file = "protobuf-5.27.3-cp310-abi3-win32.whl", hash = "sha256:dcb307cd4ef8fec0cf52cb9105a03d06fbb5275ce6d84a6ae33bc6cf84e0a07b"}, - {file = "protobuf-5.27.3-cp310-abi3-win_amd64.whl", hash = "sha256:16ddf3f8c6c41e1e803da7abea17b1793a97ef079a912e42351eabb19b2cffe7"}, - {file = "protobuf-5.27.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:68248c60d53f6168f565a8c76dc58ba4fa2ade31c2d1ebdae6d80f969cdc2d4f"}, - {file = "protobuf-5.27.3-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:b8a994fb3d1c11156e7d1e427186662b64694a62b55936b2b9348f0a7c6625ce"}, - {file = "protobuf-5.27.3-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:a55c48f2a2092d8e213bd143474df33a6ae751b781dd1d1f4d953c128a415b25"}, - {file = "protobuf-5.27.3-cp38-cp38-win32.whl", hash = "sha256:043853dcb55cc262bf2e116215ad43fa0859caab79bb0b2d31b708f128ece035"}, - {file = "protobuf-5.27.3-cp38-cp38-win_amd64.whl", hash = "sha256:c2a105c24f08b1e53d6c7ffe69cb09d0031512f0b72f812dd4005b8112dbe91e"}, - {file = "protobuf-5.27.3-cp39-cp39-win32.whl", hash = "sha256:c84eee2c71ed83704f1afbf1a85c3171eab0fd1ade3b399b3fad0884cbcca8bf"}, - {file = "protobuf-5.27.3-cp39-cp39-win_amd64.whl", hash = "sha256:af7c0b7cfbbb649ad26132e53faa348580f844d9ca46fd3ec7ca48a1ea5db8a1"}, - {file = "protobuf-5.27.3-py3-none-any.whl", hash = "sha256:8572c6533e544ebf6899c360e91d6bcbbee2549251643d32c52cf8a5de295ba5"}, - {file = "protobuf-5.27.3.tar.gz", hash = "sha256:82460903e640f2b7e34ee81a947fdaad89de796d324bcbc38ff5430bcdead82c"}, + {file = "protobuf-5.28.0-cp310-abi3-win32.whl", hash = "sha256:66c3edeedb774a3508ae70d87b3a19786445fe9a068dd3585e0cefa8a77b83d0"}, + {file = "protobuf-5.28.0-cp310-abi3-win_amd64.whl", hash = "sha256:6d7cc9e60f976cf3e873acb9a40fed04afb5d224608ed5c1a105db4a3f09c5b6"}, + {file = "protobuf-5.28.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:532627e8fdd825cf8767a2d2b94d77e874d5ddb0adefb04b237f7cc296748681"}, + {file = "protobuf-5.28.0-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:018db9056b9d75eb93d12a9d35120f97a84d9a919bcab11ed56ad2d399d6e8dd"}, + {file = "protobuf-5.28.0-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:6206afcb2d90181ae8722798dcb56dc76675ab67458ac24c0dd7d75d632ac9bd"}, + {file = "protobuf-5.28.0-cp38-cp38-win32.whl", hash = "sha256:eef7a8a2f4318e2cb2dee8666d26e58eaf437c14788f3a2911d0c3da40405ae8"}, + {file = "protobuf-5.28.0-cp38-cp38-win_amd64.whl", hash = "sha256:d001a73c8bc2bf5b5c1360d59dd7573744e163b3607fa92788b7f3d5fefbd9a5"}, + {file = "protobuf-5.28.0-cp39-cp39-win32.whl", hash = "sha256:dde9fcaa24e7a9654f4baf2a55250b13a5ea701493d904c54069776b99a8216b"}, + {file = "protobuf-5.28.0-cp39-cp39-win_amd64.whl", hash = "sha256:853db610214e77ee817ecf0514e0d1d052dff7f63a0c157aa6eabae98db8a8de"}, + {file = "protobuf-5.28.0-py3-none-any.whl", hash = "sha256:510ed78cd0980f6d3218099e874714cdf0d8a95582e7b059b06cabad855ed0a0"}, + {file = "protobuf-5.28.0.tar.gz", hash = "sha256:dde74af0fa774fa98892209992295adbfb91da3fa98c8f67a88afe8f5a349add"}, ] [[package]] @@ -3111,7 +3128,7 @@ azure-identity = "1.17.1" azure-keyvault-keys = "4.9.0" azure-mgmt-applicationinsights = "4.0.0" azure-mgmt-authorization = "4.0.0" -azure-mgmt-compute = "32.0.0" +azure-mgmt-compute = "33.0.0" azure-mgmt-containerservice = "31.0.0" azure-mgmt-cosmosdb = "9.5.1" azure-mgmt-keyvault = "10.3.1" @@ -3123,22 +3140,22 @@ azure-mgmt-security = "7.0.0" azure-mgmt-sql = "3.0.1" azure-mgmt-storage = "21.2.1" azure-mgmt-subscription = "3.1.1" -azure-mgmt-web = "7.3.0" +azure-mgmt-web = "7.3.1" azure-storage-blob = "12.22.0" -boto3 = "1.34.158" -botocore = "1.34.159" +boto3 = "1.34.162" +botocore = "1.34.162" colorama = "0.4.6" cryptography = "43.0.0" dash = "2.17.1" dash-bootstrap-components = "1.6.0" detect-secrets = "1.5.0" -google-api-python-client = "2.140.0" +google-api-python-client = "2.143.0" google-auth-httplib2 = ">=0.1,<0.3" jsonschema = "4.23.0" kubernetes = "30.1.0" microsoft-kiota-abstractions = "1.3.3" msgraph-sdk = "1.5.4" -numpy = "2.0.1" +numpy = "2.0.2" pandas = "2.2.2" py-ocsf-models = "0.1.1" pydantic = "1.10.15" @@ -3154,7 +3171,7 @@ tzlocal = "5.2" type = "git" url = "https://github.com/prowler-cloud/prowler.git" reference = "master" -resolved_reference = "52d83bd83b787baff1beb3b4ffa13768c57cd98e" +resolved_reference = "168d44d14beccf14c343db640b511d6618512951" [[package]] name = "psutil" @@ -3477,13 +3494,13 @@ testutils = ["gitpython (>3)"] [[package]] name = "pyparsing" -version = "3.1.2" +version = "3.1.4" description = "pyparsing module - Classes and methods to define and execute parsing grammars" optional = false python-versions = ">=3.6.8" files = [ - {file = "pyparsing-3.1.2-py3-none-any.whl", hash = "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742"}, - {file = "pyparsing-3.1.2.tar.gz", hash = "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad"}, + {file = "pyparsing-3.1.4-py3-none-any.whl", hash = "sha256:a6a7ee4235a3f944aa1fa2249307708f893fe5717dc603503c6c7969c070fb7c"}, + {file = "pyparsing-3.1.4.tar.gz", hash = "sha256:f86ec8d1a83f11977c9a6ea7598e8c27fc5cddfa5b07ea2241edbbde1d7bc032"}, ] [package.extras] @@ -3866,13 +3883,13 @@ six = ">=1.7.0" [[package]] name = "rich" -version = "13.7.1" +version = "13.8.0" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.7.0" files = [ - {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, - {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, + {file = "rich-13.8.0-py3-none-any.whl", hash = "sha256:2e85306a063b9492dffc86278197a60cbece75bcb766022f3436f567cae11bdc"}, + {file = "rich-13.8.0.tar.gz", hash = "sha256:a5ac1f1cd448ade0d59cc3356f7db7a7ccda2c8cbae9c7a90c28ff463d3e91f4"}, ] [package.dependencies] @@ -4164,13 +4181,13 @@ spdx = ["spdx-tools (>=0.8.2)"] [[package]] name = "safety-schemas" -version = "0.0.3" +version = "0.0.5" description = "Schemas for Safety tools" optional = false python-versions = ">=3.7" files = [ - {file = "safety_schemas-0.0.3-py3-none-any.whl", hash = "sha256:05b59d3973765911c64541ac6b5aca16418d56c1f043d983a5450ea265916061"}, - {file = "safety_schemas-0.0.3.tar.gz", hash = "sha256:b39554f9c010e4f11f7c9e5773eb31d2c96c164f7a595c273a98b074d0fb98f4"}, + {file = "safety_schemas-0.0.5-py3-none-any.whl", hash = "sha256:6ac9eb71e60f0d4e944597c01dd48d6d8cd3d467c94da4aba3702a05a3a6ab4f"}, + {file = "safety_schemas-0.0.5.tar.gz", hash = "sha256:0de5fc9a53d4423644a8ce9a17a2e474714aa27e57f3506146e95a41710ff104"}, ] [package.dependencies] @@ -4193,19 +4210,23 @@ files = [ [[package]] name = "setuptools" -version = "72.1.0" +version = "74.0.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-72.1.0-py3-none-any.whl", hash = "sha256:5a03e1860cf56bb6ef48ce186b0e557fdba433237481a9a625176c2831be15d1"}, - {file = "setuptools-72.1.0.tar.gz", hash = "sha256:8d243eff56d095e5817f796ede6ae32941278f542e0f941867cc05ae52b162ec"}, + {file = "setuptools-74.0.0-py3-none-any.whl", hash = "sha256:0274581a0037b638b9fc1c6883cc71c0210865aaa76073f7882376b641b84e8f"}, + {file = "setuptools-74.0.0.tar.gz", hash = "sha256:a85e96b8be2b906f3e3e789adec6a9323abf79758ecfa3065bd740d81158b11e"}, ] [package.extras] -core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.text (>=3.7)", "more-itertools (>=8.8)", "ordered-set (>=3.1.1)", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.11.*)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (<0.4)", "pytest-ruff (>=0.2.1)", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.5.2)"] +core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.text (>=3.7)", "more-itertools (>=8.8)", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.11.*)", "pytest-mypy"] [[package]] name = "shellingham" @@ -4300,17 +4321,17 @@ files = [ [[package]] name = "stevedore" -version = "5.2.0" +version = "5.3.0" description = "Manage dynamic plugins for Python applications" optional = false python-versions = ">=3.8" files = [ - {file = "stevedore-5.2.0-py3-none-any.whl", hash = "sha256:1c15d95766ca0569cad14cb6272d4d31dae66b011a929d7c18219c176ea1b5c9"}, - {file = "stevedore-5.2.0.tar.gz", hash = "sha256:46b93ca40e1114cea93d738a6c1e365396981bb6bb78c27045b7587c9473544d"}, + {file = "stevedore-5.3.0-py3-none-any.whl", hash = "sha256:1efd34ca08f474dad08d9b19e934a22c68bb6fe416926479ba29e5013bcc8f78"}, + {file = "stevedore-5.3.0.tar.gz", hash = "sha256:9a64265f4060312828151c204efbe9b7a9852a0d9228756344dbc7e4023e375a"}, ] [package.dependencies] -pbr = ">=2.0.0,<2.1.0 || >2.1.0" +pbr = ">=2.0.0" [[package]] name = "tabulate" @@ -4364,24 +4385,24 @@ testing = ["black", "mypy", "pytest", "pytest-gitignore", "pytest-mock", "respon [[package]] name = "tomlkit" -version = "0.13.0" +version = "0.13.2" description = "Style preserving TOML library" optional = false python-versions = ">=3.8" files = [ - {file = "tomlkit-0.13.0-py3-none-any.whl", hash = "sha256:7075d3042d03b80f603482d69bf0c8f345c2b30e41699fd8883227f89972b264"}, - {file = "tomlkit-0.13.0.tar.gz", hash = "sha256:08ad192699734149f5b97b45f1f18dad7eb1b6d16bc72ad0c2335772650d7b72"}, + {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, + {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, ] [[package]] name = "typer" -version = "0.12.3" +version = "0.12.5" description = "Typer, build great CLIs. Easy to code. Based on Python type hints." optional = false python-versions = ">=3.7" files = [ - {file = "typer-0.12.3-py3-none-any.whl", hash = "sha256:070d7ca53f785acbccba8e7d28b08dcd88f79f1fbda035ade0aecec71ca5c914"}, - {file = "typer-0.12.3.tar.gz", hash = "sha256:49e73131481d804288ef62598d97a1ceef3058905aa536a1134f90891ba35482"}, + {file = "typer-0.12.5-py3-none-any.whl", hash = "sha256:62fe4e471711b147e3365034133904df3e235698399bc4de2b36c8579298d52b"}, + {file = "typer-0.12.5.tar.gz", hash = "sha256:f592f089bedcc8ec1b974125d64851029c3b1af145f04aca64d69410f0c9b722"}, ] [package.dependencies] @@ -4457,6 +4478,17 @@ h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] +[[package]] +name = "uuid6" +version = "2024.7.10" +description = "New time-based UUID formats which are suited for use as a database key" +optional = false +python-versions = ">=3.8" +files = [ + {file = "uuid6-2024.7.10-py3-none-any.whl", hash = "sha256:93432c00ba403751f722829ad21759ff9db051dea140bf81493271e8e4dd18b7"}, + {file = "uuid6-2024.7.10.tar.gz", hash = "sha256:2d29d7f63f593caaeea0e0d0dd0ad8129c9c663b29e19bdf882e864bedf18fb0"}, +] + [[package]] name = "vine" version = "5.1.0" @@ -4508,13 +4540,13 @@ test = ["websockets"] [[package]] name = "werkzeug" -version = "3.0.3" +version = "3.0.4" description = "The comprehensive WSGI web application library." optional = false python-versions = ">=3.8" files = [ - {file = "werkzeug-3.0.3-py3-none-any.whl", hash = "sha256:fc9645dc43e03e4d630d23143a04a7f947a9a3b5727cd535fdfe155a17cc48c8"}, - {file = "werkzeug-3.0.3.tar.gz", hash = "sha256:097e5bfda9f0aba8da6b8545146def481d06aa7d3266e7448e2cccf67dd8bd18"}, + {file = "werkzeug-3.0.4-py3-none-any.whl", hash = "sha256:02c9eb92b7d6c06f31a782811505d2157837cea66aaede3e217c7c27c039476c"}, + {file = "werkzeug-3.0.4.tar.gz", hash = "sha256:34f2371506b250df4d4f84bfe7b0921e4762525762bbd936614909fe25cd7306"}, ] [package.dependencies] @@ -4718,20 +4750,24 @@ multidict = ">=4.0" [[package]] name = "zipp" -version = "3.20.0" +version = "3.20.1" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" files = [ - {file = "zipp-3.20.0-py3-none-any.whl", hash = "sha256:58da6168be89f0be59beb194da1250516fdaa062ccebd30127ac65d30045e10d"}, - {file = "zipp-3.20.0.tar.gz", hash = "sha256:0145e43d89664cfe1a2e533adc75adafed82fe2da404b4bbb6b026c0157bdb31"}, + {file = "zipp-3.20.1-py3-none-any.whl", hash = "sha256:9960cd8967c8f85a56f920d5d507274e74f9ff813a0ab8889a5b5be2daf44064"}, + {file = "zipp-3.20.1.tar.gz", hash = "sha256:c22b14cc4763c5a5b04134207736c107db42e9d3ef2d9779d465f5f1bcba572b"}, ] [package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.11,<3.13" -content-hash = "dff466ec2f564883234522f64551f778153997560c8a7d1b1e89a7e7b2fd9bf0" +content-hash = "739edc3fdfe9c9ac007914b97a33ae822b550b9d453d3d50677be54fd6caef4a" diff --git a/pyproject.toml b/pyproject.toml index fec471e6d2..14b714f4ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ psycopg2-binary = "2.9.9" pytest-celery = {extras = ["redis"], version = "^1.0.1"} # Needed for prowler compatibility python = ">=3.11,<3.13" +uuid6 = "2024.7.10" [tool.poetry.group.dev.dependencies] bandit = "1.7.9" diff --git a/src/backend/api/db_utils.py b/src/backend/api/db_utils.py index fcb674a5f4..da1920e1db 100644 --- a/src/backend/api/db_utils.py +++ b/src/backend/api/db_utils.py @@ -109,3 +109,27 @@ class ProviderEnum(EnumType): class ProviderEnumField(PostgresEnumField): def __init__(self, *args, **kwargs): super().__init__("provider", *args, **kwargs) + + +# Postgres enum definition for Scan.type + + +class ScanTypeEnum(EnumType): + enum_type_name = "scan_type" + + +class ScanTypeEnumField(PostgresEnumField): + def __init__(self, *args, **kwargs): + super().__init__("scan_type", *args, **kwargs) + + +# Postgres enum definition for state + + +class StateEnum(EnumType): + enum_type_name = "state" + + +class StateEnumField(PostgresEnumField): + def __init__(self, *args, **kwargs): + super().__init__("state", *args, **kwargs) diff --git a/src/backend/api/filters.py b/src/backend/api/filters.py index 8c94c6d15c..b2a92c8940 100644 --- a/src/backend/api/filters.py +++ b/src/backend/api/filters.py @@ -1,11 +1,44 @@ from django_filters.rest_framework import FilterSet, BooleanFilter, CharFilter from rest_framework_json_api.django_filters.backends import DjangoFilterBackend +from rest_framework_json_api.serializers import ValidationError from api.db_utils import ProviderEnumField -from api.models import Provider +from api.models import Provider, Scan from api.rls import Tenant +def provider_enum_filter(queryset, value, lookup_field: str = "provider"): + """ + Filter a queryset based on a provider value, using a specified lookup field. + + This function filters a given queryset by checking if the provided `value` + matches a valid choice in the `Provider.ProviderChoices` enum. If the `value` + is valid, the queryset is filtered using the specified `lookup_field`. + Otherwise, a `ValidationError` is raised. + + Args: + queryset (QuerySet): The Django queryset to be filtered. + value (str): The value to filter the queryset by, which must be a valid + option in `Provider.ProviderChoices`. + lookup_field (str): The field or lookup path within the model used for + filtering the queryset. Defaults to "provider". + + Returns: + QuerySet: A filtered queryset based on the provided `value` and `lookup_field`. + + Raises: + ValidationError: If the provided `value` is not a valid choice in + `Provider.ProviderChoices`. + """ + if value not in Provider.ProviderChoices: + raise ValidationError( + f"Invalid provider value: '{value}'. Valid values are: " + f"{', '.join(Provider.ProviderChoices)}" + ) + + return queryset.filter(**{lookup_field: value}) + + class CustomDjangoFilterBackend(DjangoFilterBackend): def to_html(self, _request, _queryset, _view): """Override this method to use the Browsable API in dev environments. @@ -27,6 +60,10 @@ class TenantFilter(FilterSet): class ProviderFilter(FilterSet): connected = BooleanFilter() + provider = CharFilter(method="filter_provider") + + def filter_provider(self, queryset, name, value): + return provider_enum_filter(queryset, value) class Meta: model = Provider @@ -42,3 +79,30 @@ class ProviderFilter(FilterSet): "filter_class": CharFilter, }, } + + +class ScanFilter(FilterSet): + provider = CharFilter(method="filter_provider") + type = CharFilter(method="filter_type") + + def filter_provider(self, queryset, name, value): + return provider_enum_filter(queryset, value, lookup_field="provider__provider") + + def filter_type(self, queryset, name, value): + if value not in Scan.TypeChoices: + raise ValidationError( + f"Invalid scan type value: '{value}'. Valid values are: " + f"{', '.join(Scan.TypeChoices)}" + ) + + return queryset.filter(type=value) + + class Meta: + model = Scan + fields = { + "provider": ["exact"], + "provider_id": ["exact"], + "name": ["exact", "icontains"], + "started_at": ["exact", "gte", "lte"], + "type": ["exact"], + } diff --git a/src/backend/api/migrations/0001_initial.py b/src/backend/api/migrations/0001_initial.py index b9af1b7d1e..df1701da4c 100644 --- a/src/backend/api/migrations/0001_initial.py +++ b/src/backend/api/migrations/0001_initial.py @@ -3,27 +3,47 @@ from functools import partial import django.core.validators import django.db.models.deletion +import django.utils.timezone from django.conf import settings from django.db import migrations, models +from uuid6 import uuid7 import api.rls from api.db_utils import ( PostgresEnumMigration, - register_enum, - ProviderEnumField, ProviderEnum, + ProviderEnumField, + ScanTypeEnum, + StateEnumField, + StateEnum, + ScanTypeEnumField, + register_enum, ) -from api.models import Provider +from api.models import Provider, Scan, StateChoices DB_NAME = settings.DATABASES["default"]["NAME"] -DB_USER_NAME = settings.DATABASES["default"]["USER"] -DB_USER_PASSWORD = settings.DATABASES["default"]["PASSWORD"] +DB_USER_NAME = ( + settings.DATABASES["prowler_user"]["USER"] if not settings.TESTING else "test" +) +DB_USER_PASSWORD = ( + settings.DATABASES["prowler_user"]["PASSWORD"] if not settings.TESTING else "test" +) ProviderEnumMigration = PostgresEnumMigration( enum_name="provider", enum_values=tuple(provider[0] for provider in Provider.ProviderChoices.choices), ) +ScanTypeEnumMigration = PostgresEnumMigration( + enum_name="scan_type", + enum_values=tuple(scan_type[0] for scan_type in Scan.TypeChoices.choices), +) + +StateEnumMigration = PostgresEnumMigration( + enum_name="state", + enum_values=tuple(state[0] for state in StateChoices.choices), +) + class Migration(migrations.Migration): initial = True @@ -54,6 +74,12 @@ class Migration(migrations.Migration): GRANT SELECT ON django_migrations TO {DB_USER_NAME}; """ ), + # Create and register State type + migrations.RunPython( + StateEnumMigration.create_enum_type, + reverse_code=StateEnumMigration.drop_enum_type, + ), + migrations.RunPython(partial(register_enum, enum_class=StateEnum)), migrations.CreateModel( name="Tenant", fields=[ @@ -162,4 +188,99 @@ class Migration(migrations.Migration): name="unique_provider_ids", ), ), + # Create and register ScanTypeEnum type + migrations.RunPython( + ScanTypeEnumMigration.create_enum_type, + reverse_code=ScanTypeEnumMigration.drop_enum_type, + ), + migrations.RunPython(partial(register_enum, enum_class=ScanTypeEnum)), + migrations.CreateModel( + name="Scan", + fields=[ + ( + "id", + models.UUIDField( + default=uuid7, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ( + "name", + models.CharField( + blank=True, + max_length=100, + null=True, + validators=[django.core.validators.MinLengthValidator(3)], + ), + ), + ( + "type", + ScanTypeEnumField( + choices=[("scheduled", "Scheduled"), ("manual", "Manual")] + ), + ), + ( + "state", + StateEnumField( + choices=[ + ("available", "Available"), + ("scheduled", "Scheduled"), + ("executing", "Executing"), + ("completed", "Completed"), + ("failed", "Failed"), + ("cancelled", "Cancelled"), + ], + default="available", + ), + ), + ("unique_resource_count", models.IntegerField(default=0)), + ("progress", models.IntegerField(default=0)), + ("scanner_args", models.JSONField(default=dict)), + ("duration", models.IntegerField(blank=True, null=True)), + ( + "scheduled_at", + models.DateTimeField(null=True, blank=True), + ), + ("inserted_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("started_at", models.DateTimeField(null=True, blank=True)), + ("completed_at", models.DateTimeField(null=True, blank=True)), + ( + "provider", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="scans", + related_query_name="scan", + to="api.provider", + ), + ), + ( + "tenant", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="api.tenant" + ), + ), + ], + options={ + "db_table": "scans", + "abstract": False, + }, + ), + migrations.AddConstraint( + model_name="scan", + constraint=api.rls.RowLevelSecurityConstraint( + "tenant_id", + name="rls_on_scan", + statements=["SELECT", "INSERT", "UPDATE", "DELETE"], + ), + ), + migrations.AddIndex( + model_name="scan", + index=models.Index( + fields=["provider", "state", "type", "scheduled_at"], + name="scans_prov_state_type_sche_idx", + ), + ), ] diff --git a/src/backend/api/models.py b/src/backend/api/models.py index 3737c6cb19..cfd545ca30 100644 --- a/src/backend/api/models.py +++ b/src/backend/api/models.py @@ -4,12 +4,23 @@ from uuid import uuid4, UUID from django.core.validators import MinLengthValidator from django.db import models from django.utils.translation import gettext_lazy as _ -from api.db_utils import ProviderEnumField +from uuid6 import uuid7 + +from api.db_utils import ProviderEnumField, StateEnumField, ScanTypeEnumField from api.exceptions import ModelValidationError from api.rls import RowLevelSecurityConstraint from api.rls import RowLevelSecurityProtectedModel +class StateChoices(models.TextChoices): + AVAILABLE = "available", _("Available") + SCHEDULED = "scheduled", _("Scheduled") + EXECUTING = "executing", _("Executing") + COMPLETED = "completed", _("Completed") + FAILED = "failed", _("Failed") + CANCELLED = "cancelled", _("Cancelled") + + class Provider(RowLevelSecurityProtectedModel): class ProviderChoices(models.TextChoices): AWS = "aws", _("AWS") @@ -96,3 +107,53 @@ class Provider(RowLevelSecurityProtectedModel): statements=["SELECT", "INSERT", "UPDATE", "DELETE"], ), ] + + +class Scan(RowLevelSecurityProtectedModel): + class TypeChoices(models.TextChoices): + SCHEDULED = "scheduled", _("Scheduled") + MANUAL = "manual", _("Manual") + + id = models.UUIDField(primary_key=True, default=uuid7, editable=False) + name = models.CharField( + blank=True, null=True, max_length=100, validators=[MinLengthValidator(3)] + ) + provider = models.ForeignKey( + Provider, + on_delete=models.CASCADE, + related_name="scans", + related_query_name="scan", + ) + type = ScanTypeEnumField( + choices=TypeChoices.choices, + ) + state = StateEnumField(choices=StateChoices.choices, default=StateChoices.AVAILABLE) + unique_resource_count = models.IntegerField(default=0) + progress = models.IntegerField(default=0) + scanner_args = models.JSONField(default=dict) + duration = models.IntegerField(null=True, blank=True) + scheduled_at = models.DateTimeField(null=True, blank=True) + inserted_at = models.DateTimeField(auto_now_add=True, editable=False) + updated_at = models.DateTimeField(auto_now=True, editable=False) + started_at = models.DateTimeField(null=True, blank=True) + completed_at = models.DateTimeField(null=True, blank=True) + # TODO: task foreign key + # TODO: mutelist foreign key + + class Meta(RowLevelSecurityProtectedModel.Meta): + db_table = "scans" + + constraints = [ + RowLevelSecurityConstraint( + field="tenant_id", + name="rls_on_%(class)s", + statements=["SELECT", "INSERT", "UPDATE", "DELETE"], + ), + ] + + indexes = [ + models.Index( + fields=["provider", "state", "type", "scheduled_at"], + name="scans_prov_state_type_sche_idx", + ), + ] diff --git a/src/backend/api/rls.py b/src/backend/api/rls.py index ed5d5a83c9..965b092b59 100644 --- a/src/backend/api/rls.py +++ b/src/backend/api/rls.py @@ -7,6 +7,10 @@ from django.db import DEFAULT_DB_ALIAS from django.db import models from django.db.backends.ddl_references import Statement, Table +DB_PROWLER_USER = ( + settings.DATABASES["default"]["USER"] if not settings.TESTING else "test" +) + class Tenant(models.Model): """ @@ -84,7 +88,7 @@ class RowLevelSecurityConstraint(models.BaseConstraint): full_create_sql_query, table_name=model._meta.db_table, field_column=field_column, - db_user=settings.DATABASES["default"]["USER"], + db_user=DB_PROWLER_USER, tenant_setting=self.TENANT_SETTING, ) @@ -98,7 +102,7 @@ class RowLevelSecurityConstraint(models.BaseConstraint): full_drop_sql_query, table_name=Table(model._meta.db_table, schema_editor.quote_name), field_column=field_column, - db_user=settings.DATABASES["default"]["USER"], + db_user=DB_PROWLER_USER, ) def __eq__(self, other: object) -> bool: diff --git a/src/backend/api/tests/integration/test_tenants.py b/src/backend/api/tests/integration/test_tenants.py new file mode 100644 index 0000000000..d1790f7134 --- /dev/null +++ b/src/backend/api/tests/integration/test_tenants.py @@ -0,0 +1,80 @@ +import pytest +from django.urls import reverse +from rest_framework.test import APIClient + + +@pytest.mark.django_db +def test_check_resources_between_different_tenants( + enforce_test_user_db_connection, tenants_fixture +): + client = APIClient() + + tenant1 = str(tenants_fixture[0].id) + tenant2 = str(tenants_fixture[1].id) + + # Create a provider on tenant 1 + provider_data = { + "data": { + "type": "Provider", + "attributes": { + "alias": "test_provider_tenant_1", + "provider": "aws", + "provider_id": "123456789012", + }, + } + } + provider1_response = client.post( + reverse("provider-list"), + data=provider_data, + format="vnd.api+json", + HTTP_X_TENANT_ID=tenant1, + ) + assert provider1_response.status_code == 201 + provider1_id = provider1_response.json()["data"]["id"] + + # Create a provider on tenant 2 + provider_data = { + "data": { + "type": "Provider", + "attributes": { + "alias": "test_provider_tenant_2", + "provider": "aws", + "provider_id": "123456789013", + }, + } + } + provider2_response = client.post( + reverse("provider-list"), + data=provider_data, + format="vnd.api+json", + HTTP_X_TENANT_ID=tenant2, + ) + assert provider2_response.status_code == 201 + provider2_id = provider2_response.json()["data"]["id"] + + # Try to get the provider from tenant 1 on tenant 2 and vice versa + tenant1_response = client.get( + reverse("provider-detail", kwargs={"pk": provider1_id}), + HTTP_X_TENANT_ID=tenant2, + ) + assert tenant1_response.status_code == 404 + tenant2_response = client.get( + reverse("provider-detail", kwargs={"pk": provider1_id}), + HTTP_X_TENANT_ID=tenant1, + ) + assert tenant2_response.status_code == 200 + assert tenant2_response.json()["data"]["id"] == provider1_id + + # Vice versa + + tenant2_response = client.get( + reverse("provider-detail", kwargs={"pk": provider2_id}), + HTTP_X_TENANT_ID=tenant1, + ) + assert tenant2_response.status_code == 404 + tenant1_response = client.get( + reverse("provider-detail", kwargs={"pk": provider2_id}), + HTTP_X_TENANT_ID=tenant2, + ) + assert tenant1_response.status_code == 200 + assert tenant1_response.json()["data"]["id"] == provider2_id diff --git a/src/backend/api/tests/test_database.py b/src/backend/api/tests/test_database.py index 396b144d57..e92d6bac19 100644 --- a/src/backend/api/tests/test_database.py +++ b/src/backend/api/tests/test_database.py @@ -18,14 +18,12 @@ class TestMainDatabaseRouter: @pytest.mark.parametrize("api_model", [Tenant]) def test_router_api_models(self, api_model, router): - assert router.db_for_read(api_model) == MainRouter.default_db - assert router.db_for_write(api_model) == MainRouter.default_db + assert router.db_for_read(api_model) == "default" + assert router.db_for_write(api_model) == "default" assert router.allow_migrate_model(MainRouter.admin_db, api_model) - assert not router.allow_migrate_model(MainRouter.default_db, api_model) + assert not router.allow_migrate_model("default", api_model) def test_router_django_models(self, router): assert router.db_for_read(MigrationRecorder.Migration) == MainRouter.admin_db - assert ( - not router.db_for_read(MigrationRecorder.Migration) == MainRouter.default_db - ) + assert not router.db_for_read(MigrationRecorder.Migration) == "default" diff --git a/src/backend/api/tests/test_utils.py b/src/backend/api/tests/test_utils.py new file mode 100644 index 0000000000..4ff5225ac9 --- /dev/null +++ b/src/backend/api/tests/test_utils.py @@ -0,0 +1,76 @@ +from api.utils import merge_dicts + + +class TestMergeDicts: + def test_simple_merge(self): + default_dict = {"key1": "value1", "key2": "value2"} + replacement_dict = {"key2": "new_value2", "key3": "value3"} + expected_result = {"key1": "value1", "key2": "new_value2", "key3": "value3"} + + result = merge_dicts(default_dict, replacement_dict) + assert result == expected_result + + def test_nested_merge(self): + default_dict = { + "key1": "value1", + "key2": {"nested_key1": "nested_value1", "nested_key2": "nested_value2"}, + } + replacement_dict = { + "key2": { + "nested_key2": "new_nested_value2", + "nested_key3": "nested_value3", + }, + "key3": "value3", + } + expected_result = { + "key1": "value1", + "key2": { + "nested_key1": "nested_value1", + "nested_key2": "new_nested_value2", + "nested_key3": "nested_value3", + }, + "key3": "value3", + } + + result = merge_dicts(default_dict, replacement_dict) + assert result == expected_result + + def test_no_overlap(self): + default_dict = {"key1": "value1"} + replacement_dict = {"key2": "value2"} + expected_result = {"key1": "value1", "key2": "value2"} + + result = merge_dicts(default_dict, replacement_dict) + assert result == expected_result + + def test_replacement_dict_empty(self): + default_dict = {"key1": "value1", "key2": "value2"} + replacement_dict = {} + expected_result = {"key1": "value1", "key2": "value2"} + + result = merge_dicts(default_dict, replacement_dict) + assert result == expected_result + + def test_default_dict_empty(self): + default_dict = {} + replacement_dict = {"key1": "value1", "key2": "value2"} + expected_result = {"key1": "value1", "key2": "value2"} + + result = merge_dicts(default_dict, replacement_dict) + assert result == expected_result + + def test_nested_empty_in_replacement_dict(self): + default_dict = {"key1": {"nested_key1": "nested_value1"}} + replacement_dict = {"key1": {}} + expected_result = {"key1": {}} + + result = merge_dicts(default_dict, replacement_dict) + assert result == expected_result + + def test_deep_nested_merge(self): + default_dict = {"key1": {"nested_key1": {"deep_key1": "deep_value1"}}} + replacement_dict = {"key1": {"nested_key1": {"deep_key1": "new_deep_value1"}}} + expected_result = {"key1": {"nested_key1": {"deep_key1": "new_deep_value1"}}} + + result = merge_dicts(default_dict, replacement_dict) + assert result == expected_result diff --git a/src/backend/api/tests/test_views.py b/src/backend/api/tests/test_views.py index bfcd825e1a..174034e5a2 100644 --- a/src/backend/api/tests/test_views.py +++ b/src/backend/api/tests/test_views.py @@ -4,30 +4,13 @@ import pytest from django.urls import reverse from rest_framework import status -from api.models import Provider +from api.models import Provider, Scan from api.rls import Tenant - -API_JSON_CONTENT_TYPE = "application/vnd.api+json" -# TODO Change to 401 when authentication/authorization is implemented -NO_TENANT_HTTP_STATUS = status.HTTP_403_FORBIDDEN +from conftest import API_JSON_CONTENT_TYPE, NO_TENANT_HTTP_STATUS @pytest.mark.django_db class TestTenantViewSet: - @pytest.fixture - def tenants(self): - tenant1 = Tenant.objects.create( - name="Tenant One", - inserted_at="2023-01-01T00:00:00Z", - updated_at="2023-01-02T00:00:00Z", - ) - tenant2 = Tenant.objects.create( - name="Tenant Two", - inserted_at="2023-01-03T00:00:00Z", - updated_at="2023-01-04T00:00:00Z", - ) - return tenant1, tenant2 - @pytest.fixture def valid_tenant_payload(self): return { @@ -44,13 +27,13 @@ class TestTenantViewSet: "updated_at": "2023-01-06T00:00:00Z", } - def test_tenants_list(self, client, tenants): + def test_tenants_list(self, client, tenants_fixture): response = client.get(reverse("tenant-list")) assert response.status_code == status.HTTP_200_OK - assert len(response.json()["data"]) == len(tenants) + assert len(response.json()["data"]) == len(tenants_fixture) - def test_tenants_retrieve(self, client, tenants): - tenant1, _ = tenants + def test_tenants_retrieve(self, client, tenants_fixture): + tenant1, _ = tenants_fixture response = client.get(reverse("tenant-detail", kwargs={"pk": tenant1.id})) assert response.status_code == status.HTTP_200_OK assert response.json()["data"]["attributes"]["name"] == tenant1.name @@ -76,8 +59,8 @@ class TestTenantViewSet: ) assert response.status_code == status.HTTP_400_BAD_REQUEST - def test_tenants_partial_update(self, client, tenants): - tenant1, _ = tenants + def test_tenants_partial_update(self, client, tenants_fixture): + tenant1, _ = tenants_fixture new_name = "This is the new name" payload = { "data": { @@ -95,15 +78,15 @@ class TestTenantViewSet: tenant1.refresh_from_db() assert tenant1.name == new_name - def test_tenants_partial_update_invalid_content_type(self, client, tenants): - tenant1, _ = tenants + def test_tenants_partial_update_invalid_content_type(self, client, tenants_fixture): + tenant1, _ = tenants_fixture response = client.patch( reverse("tenant-detail", kwargs={"pk": tenant1.id}), data={} ) assert response.status_code == status.HTTP_415_UNSUPPORTED_MEDIA_TYPE - def test_tenants_partial_update_invalid_content(self, client, tenants): - tenant1, _ = tenants + def test_tenants_partial_update_invalid_content(self, client, tenants_fixture): + tenant1, _ = tenants_fixture new_name = "This is the new name" payload = {"name": new_name} response = client.patch( @@ -113,8 +96,8 @@ class TestTenantViewSet: ) assert response.status_code == status.HTTP_400_BAD_REQUEST - def test_tenants_delete(self, client, tenants): - tenant1, _ = tenants + def test_tenants_delete(self, client, tenants_fixture): + tenant1, _ = tenants_fixture response = client.delete(reverse("tenant-detail", kwargs={"pk": tenant1.id})) assert response.status_code == status.HTTP_204_NO_CONTENT assert Tenant.objects.count() == 1 @@ -125,16 +108,16 @@ class TestTenantViewSet: # (user might not have permissions to see if the tenant exists or not -> 200 empty) assert response.status_code == status.HTTP_404_NOT_FOUND - def test_tenants_list_filter_search(self, client, tenants): - """Search is applied to tenants name.""" - tenant1, _ = tenants + def test_tenants_list_filter_search(self, client, tenants_fixture): + """Search is applied to tenants_fixture name.""" + tenant1, _ = tenants_fixture response = client.get(reverse("tenant-list"), {"filter[search]": tenant1.name}) assert response.status_code == status.HTTP_200_OK assert len(response.json()["data"]) == 1 assert response.json()["data"][0]["attributes"]["name"] == tenant1.name - def test_tenants_list_query_param_name(self, client, tenants): - tenant1, _ = tenants + def test_tenants_list_query_param_name(self, client, tenants_fixture): + tenant1, _ = tenants_fixture response = client.get(reverse("tenant-list"), {"name": tenant1.name}) assert response.status_code == status.HTTP_400_BAD_REQUEST @@ -142,8 +125,8 @@ class TestTenantViewSet: response = client.get(reverse("tenant-list"), {"random": "value"}) assert response.status_code == status.HTTP_400_BAD_REQUEST - def test_tenants_list_filter_name(self, client, tenants): - tenant1, _ = tenants + def test_tenants_list_filter_name(self, client, tenants_fixture): + tenant1, _ = tenants_fixture response = client.get(reverse("tenant-list"), {"filter[name]": tenant1.name}) assert response.status_code == status.HTTP_200_OK assert len(response.json()["data"]) == 1 @@ -153,16 +136,16 @@ class TestTenantViewSet: response = client.get(reverse("tenant-list"), {"filter[invalid]": "whatever"}) assert response.status_code == status.HTTP_400_BAD_REQUEST - def test_tenants_list_page_size(self, client, tenants): + def test_tenants_list_page_size(self, client, tenants_fixture): page_size = 1 response = client.get(reverse("tenant-list"), {"page[size]": page_size}) assert response.status_code == status.HTTP_200_OK assert len(response.json()["data"]) == page_size assert response.json()["meta"]["pagination"]["page"] == 1 - assert response.json()["meta"]["pagination"]["pages"] == len(tenants) + assert response.json()["meta"]["pagination"]["pages"] == len(tenants_fixture) - def test_tenants_list_page_number(self, client, tenants): + def test_tenants_list_page_number(self, client, tenants_fixture): page_size = 1 page_number = 2 @@ -173,10 +156,10 @@ class TestTenantViewSet: assert response.status_code == status.HTTP_200_OK assert len(response.json()["data"]) == page_size assert response.json()["meta"]["pagination"]["page"] == page_number - assert response.json()["meta"]["pagination"]["pages"] == len(tenants) + assert response.json()["meta"]["pagination"]["pages"] == len(tenants_fixture) - def test_tenants_list_sort_name(self, client, tenants): - _, tenant2 = tenants + def test_tenants_list_sort_name(self, client, tenants_fixture): + _, tenant2 = tenants_fixture response = client.get(reverse("tenant-list"), {"sort": "-name"}) assert response.status_code == status.HTTP_200_OK assert len(response.json()["data"]) == 2 @@ -185,53 +168,17 @@ class TestTenantViewSet: @pytest.mark.django_db class TestProviderViewSet: - @pytest.fixture - def providers(self, get_tenant): - tenant = get_tenant - provider1 = Provider.objects.create( - provider="aws", - provider_id="123456789012", - alias="aws_testing_1", - tenant_id=tenant.id, - ) - provider2 = Provider.objects.create( - provider="aws", - provider_id="123456789013", - alias="aws_testing_2", - tenant_id=tenant.id, - ) - provider3 = Provider.objects.create( - provider="gcp", - provider_id="a12322-test321", - alias="gcp_testing", - tenant_id=tenant.id, - ) - provider4 = Provider.objects.create( - provider="kubernetes", - provider_id="kubernetes-test-12345", - alias="k8s_testing", - tenant_id=tenant.id, - ) - provider5 = Provider.objects.create( - provider="azure", - provider_id="37b065f8-26b0-4218-a665-0b23d07b27d9", - alias="azure_testing", - tenant_id=tenant.id, - ) - - return provider1, provider2, provider3, provider4, provider5 - def test_providers_rls(self, client): response = client.get(reverse("provider-list")) assert response.status_code == NO_TENANT_HTTP_STATUS - def test_providers_list(self, client, providers, tenant_header): + def test_providers_list(self, client, providers_fixture, tenant_header): response = client.get(reverse("provider-list"), headers=tenant_header) assert response.status_code == status.HTTP_200_OK - assert len(response.json()["data"]) == len(providers) + assert len(response.json()["data"]) == len(providers_fixture) - def test_providers_retrieve(self, client, providers, tenant_header): - provider1, *_ = providers + def test_providers_retrieve(self, client, providers_fixture, tenant_header): + provider1, *_ = providers_fixture response = client.get( reverse("provider-detail", kwargs={"pk": provider1.id}), headers=tenant_header, @@ -286,12 +233,13 @@ class TestProviderViewSet: assert Provider.objects.get().alias == provider_json_payload["alias"] @pytest.mark.parametrize( - "provider_json_payload, error_code", + "provider_json_payload, error_code, error_pointer", ( [ ( {"provider": "aws", "provider_id": "1", "alias": "test"}, "min_length", + "provider_id", ), ( { @@ -300,14 +248,17 @@ class TestProviderViewSet: "alias": "test", }, "aws-provider-id", + "provider_id", ), ( {"provider": "aws", "provider_id": "aaaaaaaaaaaa", "alias": "test"}, "aws-provider-id", + "provider_id", ), ( {"provider": "gcp", "provider_id": "1234asdf", "alias": "test"}, "gcp-provider-id", + "provider_id", ), ( { @@ -316,6 +267,7 @@ class TestProviderViewSet: "alias": "test", }, "kubernetes-provider-id", + "provider_id", ), ( { @@ -324,12 +276,22 @@ class TestProviderViewSet: "alias": "test", }, "azure-provider-id", + "provider_id", + ), + ( + { + "provider": "does-not-exist", + "provider_id": "8851db6b-42e5-4533-aa9e-30a32d67e87", + "alias": "test", + }, + "invalid_choice", + "provider", ), ] ), ) def test_providers_invalid_create( - self, client, tenant_header, provider_json_payload, error_code + self, client, tenant_header, provider_json_payload, error_code, error_pointer ): response = client.post( reverse("provider-list"), @@ -341,11 +303,11 @@ class TestProviderViewSet: assert response.json()["errors"][0]["code"] == error_code assert ( response.json()["errors"][0]["source"]["pointer"] - == "/data/attributes/provider_id" + == f"/data/attributes/{error_pointer}" ) - def test_providers_partial_update(self, client, providers, tenant_header): - provider1, *_ = providers + def test_providers_partial_update(self, client, providers_fixture, tenant_header): + provider1, *_ = providers_fixture new_alias = "This is the new name" payload = { "data": { @@ -365,9 +327,9 @@ class TestProviderViewSet: assert provider1.alias == new_alias def test_providers_partial_update_invalid_content_type( - self, client, providers, tenant_header + self, client, providers_fixture, tenant_header ): - provider1, *_ = providers + provider1, *_ = providers_fixture response = client.patch( reverse("provider-detail", kwargs={"pk": provider1.id}), data={}, @@ -376,9 +338,9 @@ class TestProviderViewSet: assert response.status_code == status.HTTP_415_UNSUPPORTED_MEDIA_TYPE def test_providers_partial_update_invalid_content( - self, client, providers, tenant_header + self, client, providers_fixture, tenant_header ): - provider1, *_ = providers + provider1, *_ = providers_fixture new_name = "This is the new name" payload = {"alias": new_name} response = client.patch( @@ -397,9 +359,9 @@ class TestProviderViewSet: ], ) def test_providers_partial_update_invalid_fields( - self, client, providers, tenant_header, attribute_key, attribute_value + self, client, providers_fixture, tenant_header, attribute_key, attribute_value ): - provider1, *_ = providers + provider1, *_ = providers_fixture payload = { "data": { "type": "Provider", @@ -415,15 +377,15 @@ class TestProviderViewSet: ) assert response.status_code == status.HTTP_400_BAD_REQUEST - def test_providers_delete(self, client, providers, tenant_header): - provider1, *_ = providers + def test_providers_delete(self, client, providers_fixture, tenant_header): + provider1, *_ = providers_fixture response = client.delete( reverse("provider-detail", kwargs={"pk": provider1.id}), headers=tenant_header, ) assert response.status_code == status.HTTP_202_ACCEPTED assert "Content-Location" in response.headers - assert Provider.objects.count() == len(providers) - 1 + assert Provider.objects.count() == len(providers_fixture) - 1 # TODO Assert a task is returned when they are implemented def test_providers_delete_invalid(self, client, tenant_header): @@ -435,14 +397,14 @@ class TestProviderViewSet: @patch("tasks.tasks.check_provider_connection_task.delay") def test_providers_connection( - self, mock_provider_connection, client, providers, tenant_header + self, mock_provider_connection, client, providers_fixture, tenant_header ): task_mock = Mock() task_mock.id = "12345" task_mock.status = "PENDING" mock_provider_connection.return_value = task_mock - provider1, *_ = providers + provider1, *_ = providers_fixture assert provider1.connected is None assert provider1.connection_last_checked_at is None @@ -458,7 +420,7 @@ class TestProviderViewSet: assert response.headers["Content-Location"] == f"api/v1/tasks/{task_mock.id}" def test_providers_connection_invalid_provider( - self, client, providers, tenant_header + self, client, providers_fixture, tenant_header ): response = client.post( reverse("provider-connection", kwargs={"pk": "random_id"}), @@ -528,3 +490,244 @@ class TestProviderViewSet: reverse("provider-list"), {"sort": "invalid"}, headers=tenant_header ) assert response.status_code == status.HTTP_400_BAD_REQUEST + + +@pytest.mark.django_db +class TestScanViewSet: + def test_scans_list(self, client, scans_fixture, tenant_header): + response = client.get(reverse("scan-list"), headers=tenant_header) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()["data"]) == len(scans_fixture) + + def test_scans_retrieve(self, client, scans_fixture, tenant_header): + scan1, *_ = scans_fixture + response = client.get( + reverse("scan-detail", kwargs={"pk": scan1.id}), + headers=tenant_header, + ) + assert response.status_code == status.HTTP_200_OK + assert response.json()["data"]["attributes"]["name"] == scan1.name + assert response.json()["data"]["relationships"]["provider"]["data"][ + "id" + ] == str(scan1.provider.id) + + def test_scans_invalid_retrieve(self, client, tenant_header): + response = client.get( + reverse("scan-detail", kwargs={"pk": "random_id"}), + headers=tenant_header, + ) + assert response.status_code == status.HTTP_404_NOT_FOUND + + @pytest.mark.parametrize( + "scan_json_payload, expected_scanner_args", + [ + # Case 1: No scanner_args in payload (should use provider's scanner_args) + ( + { + "data": { + "type": "Scan", + "attributes": { + "name": "New Scan", + }, + "relationships": { + "provider": { + "data": {"type": "Provider", "id": "provider-id-1"} + } + }, + } + }, + {"key1": "value1", "key2": {"key21": "value21"}}, + ), + ( + { + "data": { + "type": "Scan", + "attributes": { + "name": "New Scan", + "scanner_args": { + "key2": {"key21": "test21"}, + "key3": "test3", + }, + }, + "relationships": { + "provider": { + "data": {"type": "Provider", "id": "provider-id-1"} + } + }, + } + }, + {"key1": "value1", "key2": {"key21": "test21"}, "key3": "test3"}, + ), + ], + ) + def test_scans_create_valid( + self, + client, + tenant_header, + scan_json_payload, + expected_scanner_args, + providers_fixture, + ): + *_, provider5 = providers_fixture + # Provider5 has these scanner_args + # scanner_args={"key1": "value1", "key2": {"key21": "value21"}} + + scan_json_payload["data"]["relationships"]["provider"]["data"]["id"] = str( + provider5.id + ) + + response = client.post( + reverse("scan-list"), + data=scan_json_payload, + content_type=API_JSON_CONTENT_TYPE, + headers=tenant_header, + ) + + assert response.status_code == status.HTTP_202_ACCEPTED + assert Scan.objects.count() == 1 + + scan = Scan.objects.get() + assert scan.name == scan_json_payload["data"]["attributes"]["name"] + assert scan.provider == provider5 + assert scan.type == Scan.TypeChoices.MANUAL + assert scan.scanner_args == expected_scanner_args + + @pytest.mark.parametrize( + "scan_json_payload, error_code", + [ + ( + { + "data": { + "type": "Scan", + "attributes": { + "name": "a", + "type": Scan.TypeChoices.MANUAL, + }, + "relationships": { + "provider": { + "data": {"type": "Provider", "id": "provider-id-1"} + } + }, + } + }, + "min_length", + ), + ], + ) + def test_scans_invalid_create( + self, client, tenant_header, scan_json_payload, providers_fixture, error_code + ): + provider1, *_ = providers_fixture + scan_json_payload["data"]["relationships"]["provider"]["data"]["id"] = str( + provider1.id + ) + response = client.post( + reverse("scan-list"), + data=scan_json_payload, + content_type=API_JSON_CONTENT_TYPE, + headers=tenant_header, + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json()["errors"][0]["code"] == error_code + assert ( + response.json()["errors"][0]["source"]["pointer"] == "/data/attributes/name" + ) + + def test_scans_partial_update(self, client, scans_fixture, tenant_header): + scan1, *_ = scans_fixture + new_name = "Updated Scan Name" + payload = { + "data": { + "type": "Scan", + "id": scan1.id, + "attributes": {"name": new_name}, + }, + } + response = client.patch( + reverse("scan-detail", kwargs={"pk": scan1.id}), + data=payload, + content_type=API_JSON_CONTENT_TYPE, + headers=tenant_header, + ) + assert response.status_code == status.HTTP_200_OK + scan1.refresh_from_db() + assert scan1.name == new_name + + def test_scans_partial_update_invalid_content_type( + self, client, scans_fixture, tenant_header + ): + scan1, *_ = scans_fixture + response = client.patch( + reverse("scan-detail", kwargs={"pk": scan1.id}), + data={}, + headers=tenant_header, + ) + assert response.status_code == status.HTTP_415_UNSUPPORTED_MEDIA_TYPE + + def test_scans_partial_update_invalid_content( + self, client, scans_fixture, tenant_header + ): + scan1, *_ = scans_fixture + new_name = "Updated Scan Name" + payload = {"name": new_name} + response = client.patch( + reverse("scan-detail", kwargs={"pk": scan1.id}), + data=payload, + content_type=API_JSON_CONTENT_TYPE, + headers=tenant_header, + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST + + @pytest.mark.parametrize( + "filter_name, filter_value", + [ + ("provider", "aws"), + ("type", Scan.TypeChoices.MANUAL), + ("name", "Scan 1"), + ("started_at", "2024-01-01 00:00:00"), + ], + ) + def test_scans_filters(self, client, tenant_header, filter_name, filter_value): + response = client.get( + reverse("scan-list"), + {f"filter[{filter_name}]": filter_value}, + headers=tenant_header, + ) + assert response.status_code == status.HTTP_200_OK + + @pytest.mark.parametrize( + "filter_name", + [ + "provider", # Valid filter, invalid value + "invalid", + ], + ) + def test_scans_filters_invalid(self, client, tenant_header, filter_name): + response = client.get( + reverse("scan-list"), + {f"filter[{filter_name}]": "invalid_value"}, + headers=tenant_header, + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST + + @pytest.mark.parametrize( + "sort_field", + [ + "provider_id", + "name", + "type", + "inserted_at", + "updated_at", + ], + ) + def test_scans_sort(self, client, tenant_header, sort_field): + response = client.get( + reverse("scan-list"), {"sort": sort_field}, headers=tenant_header + ) + assert response.status_code == status.HTTP_200_OK + + def test_scans_sort_invalid(self, client, tenant_header): + response = client.get( + reverse("scan-list"), {"sort": "invalid"}, headers=tenant_header + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST diff --git a/src/backend/api/utils.py b/src/backend/api/utils.py new file mode 100644 index 0000000000..c678ae9683 --- /dev/null +++ b/src/backend/api/utils.py @@ -0,0 +1,25 @@ +def merge_dicts(default_dict: dict, replacement_dict: dict) -> dict: + """ + Recursively merge two dictionaries, using `default_dict` as the base and `replacement_dict` for overriding values. + + Args: + default_dict (dict): The base dictionary containing default key-value pairs. + replacement_dict (dict): The dictionary containing values that should override those in `default_dict`. + + Returns: + dict: A new dictionary containing all keys from `default_dict` with values from `replacement_dict` replacing + any overlapping keys. If a key in both `default_dict` and `replacement_dict` contains dictionaries, + this function will merge them recursively. + """ + result = default_dict.copy() + + for key, value in replacement_dict.items(): + if key in result and isinstance(result[key], dict) and isinstance(value, dict): + if value: + result[key] = merge_dicts(result[key], value) + else: + result[key] = value + else: + result[key] = value + + return result diff --git a/src/backend/api/v1/serializers.py b/src/backend/api/v1/serializers.py index d45268dd67..93df31068d 100644 --- a/src/backend/api/v1/serializers.py +++ b/src/backend/api/v1/serializers.py @@ -2,8 +2,9 @@ from drf_spectacular.utils import extend_schema_field from rest_framework_json_api import serializers from rest_framework_json_api.serializers import ValidationError -from api.models import Provider +from api.models import StateChoices, Provider, Scan from api.rls import Tenant +from api.utils import merge_dicts # Base @@ -31,6 +32,12 @@ class RLSSerializer(BaseSerializerV1): return super().create(validated_data) +class StateEnumSerializerField(serializers.ChoiceField): + def __init__(self, **kwargs): + kwargs["choices"] = StateChoices.choices + super().__init__(**kwargs) + + # Tasks @@ -59,7 +66,7 @@ class TenantSerializer(BaseSerializerV1): # Providers -class ProviderEnumField(serializers.ChoiceField): +class ProviderEnumSerializerField(serializers.ChoiceField): def __init__(self, **kwargs): kwargs["choices"] = Provider.ProviderChoices.choices super().__init__(**kwargs) @@ -70,7 +77,7 @@ class ProviderSerializer(RLSSerializer): Serializer for the Provider model. """ - provider = ProviderEnumField(choices=Provider.ProviderChoices.choices) + provider = ProviderEnumSerializerField() connection = serializers.SerializerMethodField(read_only=True) class Meta: @@ -84,13 +91,8 @@ class ProviderSerializer(RLSSerializer): "alias", "connection", "scanner_args", + "url", ] - extra_kwargs = { - "id": {"read_only": True}, - "inserted_at": {"read_only": True}, - "updated_at": {"read_only": True}, - "connection": {"read_only": True}, - } @extend_schema_field( { @@ -108,6 +110,12 @@ class ProviderSerializer(RLSSerializer): } +class ProviderCreateSerializer(RLSSerializer, BaseWriteSerializer): + class Meta: + model = Provider + fields = ["alias", "provider", "provider_id", "scanner_args"] + + class ProviderUpdateSerializer(BaseWriteSerializer): """ Serializer for updating the Provider model. @@ -117,3 +125,79 @@ class ProviderUpdateSerializer(BaseWriteSerializer): class Meta: model = Provider fields = ["alias", "scanner_args"] + + +# Scans + + +class ScanTypeEnumSerializerField(serializers.ChoiceField): + def __init__(self, **kwargs): + kwargs["choices"] = Scan.TypeChoices.choices + super().__init__(**kwargs) + + +class ScanSerializer(RLSSerializer): + type_ = serializers.ChoiceField(choices=Scan.TypeChoices.choices, read_only=True) + state = StateEnumSerializerField(read_only=True) + + class Meta: + model = Scan + fields = [ + "id", + "name", + "type_", + "state", + "unique_resource_count", + "progress", + "scanner_args", + "duration", + "provider", + "started_at", + "completed_at", + "scheduled_at", + "url", + ] + + def get_fields(self): + """`type` is a Python reserved keyword.""" + fields = super().get_fields() + type_ = fields.pop("type_") + fields["type"] = type_ + return fields + + +class ScanCreateSerializer(RLSSerializer, BaseWriteSerializer): + class Meta: + model = Scan + # TODO: add mutelist when implemented + fields = ["provider", "scanner_args", "name"] + + def create(self, validated_data): + provider = validated_data.get("provider") + + if not validated_data.get("scanner_args"): + validated_data["scanner_args"] = provider.scanner_args + else: + validated_data["scanner_args"] = merge_dicts( + provider.scanner_args, validated_data["scanner_args"] + ) + + if not validated_data.get("type"): + validated_data["type"] = Scan.TypeChoices.MANUAL.value + + return RLSSerializer.create(self, validated_data) + + +class ScanUpdateSerializer(BaseWriteSerializer): + """ + Serializer for updating the Provider model. + Only allows "alias" and "scanner_args" fields to be updated. + """ + + class Meta: + model = Scan + # TODO: add mutelist when implemented + fields = ["id", "name"] + extra_kwargs = { + "id": {"read_only": True}, + } diff --git a/src/backend/api/v1/urls.py b/src/backend/api/v1/urls.py index 842e00ff13..15fd1b4685 100644 --- a/src/backend/api/v1/urls.py +++ b/src/backend/api/v1/urls.py @@ -2,12 +2,13 @@ from django.urls import path, include from drf_spectacular.views import SpectacularRedocView from rest_framework import routers -from api.v1.views import SchemaView, TenantViewSet, ProviderViewSet +from api.v1.views import SchemaView, TenantViewSet, ProviderViewSet, ScanViewSet router = routers.DefaultRouter(trailing_slash=False) router.register(r"tenants", TenantViewSet, basename="tenant") router.register(r"providers", ProviderViewSet, basename="provider") +router.register(r"scans", ScanViewSet, basename="scan") urlpatterns = [ path("", include(router.urls)), diff --git a/src/backend/api/v1/views.py b/src/backend/api/v1/views.py index 1dd44cad89..65d536ee6b 100644 --- a/src/backend/api/v1/views.py +++ b/src/backend/api/v1/views.py @@ -1,4 +1,5 @@ from django.conf import settings as django_settings +from django.urls import reverse from django.utils.decorators import method_decorator from django.views.decorators.cache import cache_control from drf_spectacular.settings import spectacular_settings @@ -10,14 +11,18 @@ from rest_framework.generics import get_object_or_404 from rest_framework_json_api.views import Response from api.base_views import BaseRLSViewSet, BaseViewSet -from api.filters import ProviderFilter, TenantFilter -from api.models import Provider +from api.filters import ProviderFilter, TenantFilter, ScanFilter +from api.models import Provider, Scan from api.rls import Tenant from api.v1.serializers import ( ProviderSerializer, + ProviderCreateSerializer, ProviderUpdateSerializer, TenantSerializer, DelayedTaskSerializer, + ScanSerializer, + ScanCreateSerializer, + ScanUpdateSerializer, ) from tasks.tasks import check_provider_connection_task @@ -106,6 +111,7 @@ class TenantViewSet(BaseViewSet): @method_decorator(CACHE_DECORATOR, name="retrieve") class ProviderViewSet(BaseRLSViewSet): queryset = Provider.objects.all() + serializer_class = ProviderSerializer http_method_names = ["get", "post", "patch", "delete"] filterset_class = ProviderFilter search_fields = ["provider", "provider_id", "alias"] @@ -123,17 +129,25 @@ class ProviderViewSet(BaseRLSViewSet): return Provider.objects.all() def get_serializer_class(self): - # TODO Add `destroy` when refactored - if self.action in {"connection"}: - return DelayedTaskSerializer - return ProviderSerializer + if self.action == "create": + return ProviderCreateSerializer + elif self.action == "partial_update": + return ProviderUpdateSerializer + return super().get_serializer_class() def partial_update(self, request, *args, **kwargs): instance = self.get_object() - serializer = ProviderUpdateSerializer(instance, data=request.data, partial=True) + serializer = self.get_serializer( + instance, + data=request.data, + partial=True, + context=self.get_serializer_context(), + ) serializer.is_valid(raise_exception=True) serializer.save() - read_serializer = ProviderSerializer(instance) + read_serializer = ProviderSerializer( + instance, context=self.get_serializer_context() + ) return Response(data=read_serializer.data, status=status.HTTP_200_OK) @extend_schema( @@ -168,3 +182,87 @@ class ProviderViewSet(BaseRLSViewSet): **response.headers, } return response + + +@extend_schema_view( + list=extend_schema( + summary="List all scans", + description="Retrieve a list of all scans with options for filtering by various criteria.", + ), + retrieve=extend_schema( + summary="Retrieve data from a specific scan", + description="Fetch detailed information about a specific scan by its ID.", + ), + create=extend_schema( + summary="Trigger a manual scan", + description=( + "Trigger a manual scan by providing the required scan details. " + "If `scanner_args` are not provided, the system will automatically use the default settings " + "from the associated provider. If you do provide `scanner_args`, these settings will be " + "merged with the provider's defaults. This means that your provided settings will override " + "the defaults only where they conflict, while the rest of the default settings will remain intact." + ), + ), + partial_update=extend_schema( + summary="Partially update a scan", + description="Update certain fields of an existing scan without affecting other fields.", + request=ScanUpdateSerializer, + responses={200: ScanSerializer}, + ), +) +@method_decorator(CACHE_DECORATOR, name="list") +@method_decorator(CACHE_DECORATOR, name="retrieve") +class ScanViewSet(BaseRLSViewSet): + queryset = Scan.objects.all() + serializer_class = ScanSerializer + http_method_names = ["get", "post", "patch"] + filterset_class = ScanFilter + ordering = ["inserted_at"] + ordering_fields = [ + "provider_id", + "name", + "type", + "attempted_at", + "scheduled_at", + "inserted_at", + "updated_at", + ] + + def get_queryset(self): + return Scan.objects.all() + + def get_serializer_class(self): + if self.action == "create": + return ScanCreateSerializer + elif self.action == "partial_update": + return ScanUpdateSerializer + return super().get_serializer_class() + + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + scan = serializer.save() + + # TODO: Run scan through task and return task info here + return Response( + data=serializer.data, + status=status.HTTP_202_ACCEPTED, + headers={ + "Content-Location": reverse("scan-detail", kwargs={"pk": scan.id}) + }, + ) + + def partial_update(self, request, *args, **kwargs): + instance = self.get_object() + serializer = self.get_serializer( + instance, + data=request.data, + partial=True, + context=self.get_serializer_context(), + ) + serializer.is_valid(raise_exception=True) + serializer.save() + read_serializer = ScanSerializer( + instance, context=self.get_serializer_context() + ) + return Response(data=read_serializer.data, status=status.HTTP_200_OK) diff --git a/src/backend/config/django/base.py b/src/backend/config/django/base.py index b8f2d66797..d1e833fbdf 100644 --- a/src/backend/config/django/base.py +++ b/src/backend/config/django/base.py @@ -151,3 +151,6 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" # Cache settings CACHE_MAX_AGE = env.int("DJANGO_CACHE_MAX_AGE", 3600) CACHE_STALE_WHILE_REVALIDATE = env.int("DJANGO_STALE_WHILE_REVALIDATE", 60) + + +TESTING = False diff --git a/src/backend/config/django/devel.py b/src/backend/config/django/devel.py index 99c34565f5..0424feb07a 100644 --- a/src/backend/config/django/devel.py +++ b/src/backend/config/django/devel.py @@ -7,7 +7,7 @@ ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=["*"]) # Database DATABASES = { - "default": { + "prowler_user": { "ENGINE": "django.db.backends.postgresql", "NAME": env("POSTGRES_DB", default="prowler_db"), "USER": env("POSTGRES_USER", default="prowler_user"), @@ -24,7 +24,7 @@ DATABASES = { "PORT": env("POSTGRES_PORT", default="5432"), }, } - +DATABASES["default"] = DATABASES["prowler_user"] REST_FRAMEWORK["DEFAULT_RENDERER_CLASSES"] = tuple( # noqa: F405 render_class diff --git a/src/backend/config/django/production.py b/src/backend/config/django/production.py index 0d505ea587..e80040a45a 100644 --- a/src/backend/config/django/production.py +++ b/src/backend/config/django/production.py @@ -8,7 +8,7 @@ ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=["localhost", "127.0.0. # Database # TODO Use Django database routers https://docs.djangoproject.com/en/5.0/topics/db/multi-db/#automatic-database-routing DATABASES = { - "default": { + "prowler_user": { "ENGINE": "django.db.backends.postgresql", "NAME": env("POSTGRES_DB"), "USER": env("POSTGRES_USER"), @@ -25,3 +25,4 @@ DATABASES = { "PORT": env("POSTGRES_PORT"), }, } +DATABASES["default"] = DATABASES["prowler_user"] diff --git a/src/backend/config/django/testing.py b/src/backend/config/django/testing.py index 79a8a48cca..ffa7a8a287 100644 --- a/src/backend/config/django/testing.py +++ b/src/backend/config/django/testing.py @@ -5,6 +5,7 @@ from config.env import env DEBUG = env.bool("DJANGO_DEBUG", default=False) ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=["localhost", "127.0.0.1"]) + DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", @@ -13,7 +14,8 @@ DATABASES = { "PASSWORD": env("POSTGRES_PASSWORD", default="S3cret"), "HOST": env("POSTGRES_HOST", default="localhost"), "PORT": env("POSTGRES_PORT", default="5432"), - } + }, } DATABASE_ROUTERS = [] +TESTING = True diff --git a/src/backend/conftest.py b/src/backend/conftest.py index 0c0973eb34..91c26557bd 100644 --- a/src/backend/conftest.py +++ b/src/backend/conftest.py @@ -1,9 +1,42 @@ import logging import pytest +from django.conf import settings +from django.db import connections as django_connections +from rest_framework import status +from api.models import Provider, Scan, StateChoices from api.rls import Tenant +API_JSON_CONTENT_TYPE = "application/vnd.api+json" +# TODO Change to 401 when authentication/authorization is implemented +NO_TENANT_HTTP_STATUS = status.HTTP_403_FORBIDDEN + + +@pytest.fixture(scope="module") +def enforce_test_user_db_connection(django_db_setup, django_db_blocker): + """Ensure tests use the test user for database connections.""" + with django_db_blocker.unblock(): + test_user = "test" + test_password = "test" + original_user = settings.DATABASES["default"]["USER"] + original_password = settings.DATABASES["default"]["PASSWORD"] + + django_connections["default"].settings_dict["USER"] = test_user + django_connections["default"].settings_dict["PASSWORD"] = test_password + + django_connections["default"].close() + django_connections["default"].connect() + + yield + + with django_db_blocker.unblock(): + django_connections["default"].settings_dict["USER"] = original_user + django_connections["default"].settings_dict["PASSWORD"] = original_password + + django_connections["default"].close() + django_connections["default"].connect() + @pytest.fixture(autouse=True) def disable_logging(): @@ -11,32 +44,87 @@ def disable_logging(): @pytest.fixture -def get_tenant(): - tenant = Tenant.objects.create( +def tenants_fixture(): + tenant1 = Tenant.objects.create( name="Tenant One", inserted_at="2023-01-01T00:00:00Z", updated_at="2023-01-02T00:00:00Z", ) - return tenant + tenant2 = Tenant.objects.create( + name="Tenant Two", + inserted_at="2023-01-03T00:00:00Z", + updated_at="2023-01-04T00:00:00Z", + ) + return tenant1, tenant2 @pytest.fixture -def tenant_header(get_tenant): - return {"X-Tenant-ID": str(get_tenant.id)} +def providers_fixture(tenants_fixture): + tenant, _ = tenants_fixture + provider1 = Provider.objects.create( + provider="aws", + provider_id="123456789012", + alias="aws_testing_1", + tenant_id=tenant.id, + ) + provider2 = Provider.objects.create( + provider="aws", + provider_id="123456789013", + alias="aws_testing_2", + tenant_id=tenant.id, + ) + provider3 = Provider.objects.create( + provider="gcp", + provider_id="a12322-test321", + alias="gcp_testing", + tenant_id=tenant.id, + ) + provider4 = Provider.objects.create( + provider="kubernetes", + provider_id="kubernetes-test-12345", + alias="k8s_testing", + tenant_id=tenant.id, + ) + provider5 = Provider.objects.create( + provider="azure", + provider_id="37b065f8-26b0-4218-a665-0b23d07b27d9", + alias="azure_testing", + tenant_id=tenant.id, + scanner_args={"key1": "value1", "key2": {"key21": "value21"}}, + ) + + return provider1, provider2, provider3, provider4, provider5 -# Put this in your conftest.py -@pytest.fixture(scope="session") -def celery_config(): - return { - "broker_url": "redis://", - "result_backend": "redis://", - "broker_connection_retry_on_startup": True, - } +@pytest.fixture +def scans_fixture(tenants_fixture, providers_fixture): + tenant, _ = tenants_fixture + provider, *_ = providers_fixture + + scan1 = Scan.objects.create( + name="Scan 1", + provider=provider, + type=Scan.TypeChoices.MANUAL, + state=StateChoices.AVAILABLE, + tenant_id=tenant.id, + ) + scan2 = Scan.objects.create( + name="Scan 2", + provider=provider, + type=Scan.TypeChoices.SCHEDULED, + state=StateChoices.FAILED, + tenant_id=tenant.id, + ) + scan3 = Scan.objects.create( + name="Scan 3", + provider=provider, + type=Scan.TypeChoices.SCHEDULED, + state=StateChoices.AVAILABLE, + tenant_id=tenant.id, + ) + return scan1, scan2, scan3 -@pytest.fixture(scope="session") -def celery_includes(): - return [ - "tasks", - ] +@pytest.fixture +def tenant_header(tenants_fixture): + return {"X-Tenant-ID": str(tenants_fixture[0].id)} diff --git a/src/backend/tasks/tests/test_connection.py b/src/backend/tasks/tests/test_connection.py index 18de0dd054..b2466cd025 100644 --- a/src/backend/tasks/tests/test_connection.py +++ b/src/backend/tasks/tests/test_connection.py @@ -17,8 +17,8 @@ from tasks.jobs.connection import check_provider_connection ], ) @pytest.mark.django_db -def test_check_provider_connection(get_tenant, provider_data, provider_class): - provider = Provider.objects.create(**provider_data, tenant_id=get_tenant.id) +def test_check_provider_connection(tenants_fixture, provider_data, provider_class): + provider = Provider.objects.create(**provider_data, tenant_id=tenants_fixture[0].id) mock_test_connection_result = MagicMock() mock_test_connection_result.is_connected = True