From 4c657156a4e8d3ba58c7422152a599a0b446903b Mon Sep 17 00:00:00 2001 From: Daniel Goldman Date: Sun, 31 Dec 2023 10:12:43 -0500 Subject: [PATCH 01/33] skel --- llamazure/history/BUILD | 34 ++++++++++++++++++++++++++++++++++ llamazure/history/__init__.py | 0 llamazure/history/data.py | 0 llamazure/history/models.py | 0 llamazure/history/readme.md | 3 +++ llamazure/rid/BUILD | 1 + 6 files changed, 38 insertions(+) create mode 100644 llamazure/history/BUILD create mode 100644 llamazure/history/__init__.py create mode 100644 llamazure/history/data.py create mode 100644 llamazure/history/models.py create mode 100644 llamazure/history/readme.md diff --git a/llamazure/history/BUILD b/llamazure/history/BUILD new file mode 100644 index 0000000..be4ebd1 --- /dev/null +++ b/llamazure/history/BUILD @@ -0,0 +1,34 @@ +python_sources( + name="history", +) + +python_tests( + name="tests", +) + +python_distribution( + name="llamazure.history", + dependencies=[":history"], + long_description_path="llamazure/history/readme.md", + provides=python_artifact( + name="llamazure.history", + version="0.0.1", + description="Build a history of an Azure tenancy", + author="Daniel Goldman", + classifiers=[ + "Development Status :: 3 - Alpha", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Topic :: Utilities", + "Topic :: Internet :: Log Analysis", + ], + license="Round Robin 2.0.0", + long_description_content_type="text/markdown", + ), +) + +python_test_utils( + name="test_utils", +) diff --git a/llamazure/history/__init__.py b/llamazure/history/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/llamazure/history/data.py b/llamazure/history/data.py new file mode 100644 index 0000000..e69de29 diff --git a/llamazure/history/models.py b/llamazure/history/models.py new file mode 100644 index 0000000..e69de29 diff --git a/llamazure/history/readme.md b/llamazure/history/readme.md new file mode 100644 index 0000000..dbb39b0 --- /dev/null +++ b/llamazure/history/readme.md @@ -0,0 +1,3 @@ +# llamazure.history + +Build a history of an Azure tenancy \ No newline at end of file diff --git a/llamazure/rid/BUILD b/llamazure/rid/BUILD index 34c8aa0..354938a 100644 --- a/llamazure/rid/BUILD +++ b/llamazure/rid/BUILD @@ -20,6 +20,7 @@ python_distribution( "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Topic :: Utilities", "Topic :: Internet :: Log Analysis", ], From 823215ea4236efa9908c5f287545b2d51f59ef14 Mon Sep 17 00:00:00 2001 From: Daniel Goldman Date: Sun, 31 Dec 2023 10:21:23 -0500 Subject: [PATCH 02/33] add timescaledb container with docker-compose --- llamazure/history/docker-compose.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 llamazure/history/docker-compose.yml diff --git a/llamazure/history/docker-compose.yml b/llamazure/history/docker-compose.yml new file mode 100644 index 0000000..320535b --- /dev/null +++ b/llamazure/history/docker-compose.yml @@ -0,0 +1,18 @@ +version: '3' + +services: + timescaledb: + image: timescale/timescaledb:latest-pg13 + container_name: timescaledb + env_file: + - .env + environment: + - POSTGRES_DB=llamazure + ports: + - "5432:5432" + networks: + - timescaledb_network + +networks: + timescaledb_network: + driver: bridge From c5e44c0b4f4b8e89a6f441596cbfb589b2b505de Mon Sep 17 00:00:00 2001 From: Daniel Goldman Date: Sun, 31 Dec 2023 10:41:12 -0500 Subject: [PATCH 03/33] add psycopg2 dependency --- cicd/python-default.lock | 638 ++++++++++++++++++++++----------------- requirements.txt | 3 +- 2 files changed, 356 insertions(+), 285 deletions(-) diff --git a/cicd/python-default.lock b/cicd/python-default.lock index 007f109..c296e6a 100644 --- a/cicd/python-default.lock +++ b/cicd/python-default.lock @@ -13,6 +13,7 @@ // "click~=8.0", // "hypothesis<7,>=6", // "mypy>=1.5.0", +// "psycopg2~=2.9", // "pydantic>=2.0", // "pytest<8,>7", // "pyyaml~=6.0", @@ -60,30 +61,68 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04", - "url": "https://files.pythonhosted.org/packages/f0/eb/fcb708c7bf5056045e9e98f62b93bd7467eb718b0202e7698eb11d66416c/attrs-23.1.0-py3-none-any.whl" + "hash": "745843b39e829e108e518c489b31dc757de7d2131d53fac32bd8df268227bfee", + "url": "https://files.pythonhosted.org/packages/bf/cd/d6d9bb1dadf73e7af02d18225cbd2c93f8552e13130484f1c8dcfece292b/anyio-4.2.0-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015", - "url": "https://files.pythonhosted.org/packages/97/90/81f95d5f705be17872843536b1868f351805acf6971251ff07c1b8334dbb/attrs-23.1.0.tar.gz" + "hash": "e1875bb4b4e2de1669f4bc7869b6d3f54231cdced71605e6e64c9be77e3be50f", + "url": "https://files.pythonhosted.org/packages/2d/b8/7333d87d5f03247215d86a86362fd3e324111788c6cdd8d2e6196a6ba833/anyio-4.2.0.tar.gz" + } + ], + "project_name": "anyio", + "requires_dists": [ + "Sphinx>=7; extra == \"doc\"", + "anyio[trio]; extra == \"test\"", + "coverage[toml]>=7; extra == \"test\"", + "exceptiongroup>=1.0.2; python_version < \"3.11\"", + "exceptiongroup>=1.2.0; extra == \"test\"", + "hypothesis>=4.0; extra == \"test\"", + "idna>=2.8", + "packaging; extra == \"doc\"", + "psutil>=5.9; extra == \"test\"", + "pytest-mock>=3.6.1; extra == \"test\"", + "pytest>=7.0; extra == \"test\"", + "sniffio>=1.1", + "sphinx-autodoc-typehints>=1.2.0; extra == \"doc\"", + "sphinx-rtd-theme; extra == \"doc\"", + "trio>=0.23; extra == \"trio\"", + "trustme; extra == \"test\"", + "typing-extensions>=4.1; python_version < \"3.11\"", + "uvloop>=0.17; (platform_python_implementation == \"CPython\" and platform_system != \"Windows\") and extra == \"test\"" + ], + "requires_python": ">=3.8", + "version": "4.2.0" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1", + "url": "https://files.pythonhosted.org/packages/e0/44/827b2a91a5816512fcaf3cc4ebc465ccd5d598c45cefa6703fcf4a79018f/attrs-23.2.0-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30", + "url": "https://files.pythonhosted.org/packages/e3/fc/f800d51204003fa8ae392c4e8278f256206e7a919b708eef054f5f4b650d/attrs-23.2.0.tar.gz" } ], "project_name": "attrs", "requires_dists": [ - "attrs[docs,tests]; extra == \"dev\"", + "attrs[tests-mypy]; extra == \"tests-no-zope\"", "attrs[tests-no-zope]; extra == \"tests\"", "attrs[tests]; extra == \"cov\"", + "attrs[tests]; extra == \"dev\"", "cloudpickle; platform_python_implementation == \"CPython\" and extra == \"tests-no-zope\"", "coverage[toml]>=5.3; extra == \"cov\"", "furo; extra == \"docs\"", "hypothesis; extra == \"tests-no-zope\"", "importlib-metadata; python_version < \"3.8\"", - "mypy>=1.1.1; platform_python_implementation == \"CPython\" and extra == \"tests-no-zope\"", + "mypy>=1.6; (platform_python_implementation == \"CPython\" and python_version >= \"3.8\") and extra == \"tests-mypy\"", "myst-parser; extra == \"docs\"", "pre-commit; extra == \"dev\"", "pympler; extra == \"tests-no-zope\"", - "pytest-mypy-plugins; platform_python_implementation == \"CPython\" and python_version < \"3.11\" and extra == \"tests-no-zope\"", + "pytest-mypy-plugins; (platform_python_implementation == \"CPython\" and python_version >= \"3.8\") and extra == \"tests-mypy\"", "pytest-xdist[psutil]; extra == \"tests-no-zope\"", "pytest>=4.3.0; extra == \"tests-no-zope\"", "sphinx-notfound-page; extra == \"docs\"", @@ -94,30 +133,31 @@ "zope-interface; extra == \"tests\"" ], "requires_python": ">=3.7", - "version": "23.1.0" + "version": "23.2.0" }, { "artifacts": [ { "algorithm": "sha256", - "hash": "0fa04b7b1f7d44a4fb8468c4093deb2ea01fdf4faddbf802ed9205615f99d68c", - "url": "https://files.pythonhosted.org/packages/9c/f8/1cf23a75cb8c2755c539ac967f3a7f607887c4979d073808134803720f0f/azure_core-1.29.5-py3-none-any.whl" + "hash": "604a005bce6a49ba661bb7b2be84a9b169047e52fcfcd0a4e4770affab4178f7", + "url": "https://files.pythonhosted.org/packages/b0/e2/b6cdd23d8d9cc430410cc309879883aff67736c02528cd1fdc07c48158b1/azure_core-1.29.6-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "52983c89d394c6f881a121e5101c5fa67278ca3b1f339c8fb2ef39230c70e9ac", - "url": "https://files.pythonhosted.org/packages/e3/39/328faea9f656075dbb8ecf70f1a4697bc80510fcc70e3e8f0090c34fc00c/azure-core-1.29.5.tar.gz" + "hash": "13b485252ecd9384ae624894fe51cfa6220966207264c360beada239f88b738a", + "url": "https://files.pythonhosted.org/packages/ad/78/a1aeb8f80306101112810263e74ec81a99cdd50ecca1f03819716c1aedb3/azure-core-1.29.6.tar.gz" } ], "project_name": "azure-core", "requires_dists": [ "aiohttp>=3.0; extra == \"aio\"", - "requests>=2.18.4", + "anyio<5.0,>=3.0", + "requests>=2.21.0", "six>=1.11.0", "typing-extensions>=4.6.0" ], "requires_python": ">=3.7", - "version": "1.29.5" + "version": "1.29.6" }, { "artifacts": [ @@ -748,93 +788,93 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "fba1e91467c65fe64a82c689dc6cf58151158993b13eb7a7f3f4b7f395636723", - "url": "https://files.pythonhosted.org/packages/26/41/e5ebaad8b27f8662c92a7d4cb9bf16e488450cb4b6ee0fce5b78b3327679/cryptography-41.0.5-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl" + "hash": "c5ca78485a255e03c32b513f8c2bc39fedb7f5c5f8535545bdc223a03b24f248", + "url": "https://files.pythonhosted.org/packages/79/68/9767a3fb985515d3c34221c3671043cda57b1f691046ad8aae355fb2a8a5/cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl" }, { "algorithm": "sha256", - "hash": "8254962e6ba1f4d2090c44daf50a547cd5f0bf446dc658a8e5f8156cae0d8548", - "url": "https://files.pythonhosted.org/packages/05/40/ade6e708e6e90528dc50b215adce495fec49286f199bc11e4199b1666505/cryptography-41.0.5-cp37-abi3-musllinux_1_1_aarch64.whl" + "hash": "c7f3201ec47d5207841402594f1d7950879ef890c0c495052fa62f58283fde1a", + "url": "https://files.pythonhosted.org/packages/0d/bf/e7a1382034c4feaa77b35147138ff2bc8ae47a2fa7e2838fcdd41d2d0f2e/cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl" }, { "algorithm": "sha256", - "hash": "da6a0ff8f1016ccc7477e6339e1d50ce5f59b88905585f77193ebd5068f1e797", - "url": "https://files.pythonhosted.org/packages/0b/c1/2f1e8abb31ec0bf8b004052bbe0face0a8be386ed5ea30e5e300bfffd51a/cryptography-41.0.5-cp37-abi3-macosx_10_12_universal2.whl" + "hash": "841df4caa01008bad253bce2a6f7b47f86dc9f08df4b433c404def869f590a15", + "url": "https://files.pythonhosted.org/packages/14/fd/dd5bd6ab0d12476ebca579cbfd48d31bd90fa28fa257b209df585dcf62a0/cryptography-41.0.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, { "algorithm": "sha256", - "hash": "392cb88b597247177172e02da6b7a63deeff1937fa6fec3bbf902ebd75d97ec7", - "url": "https://files.pythonhosted.org/packages/16/a7/38fdcdd634515f589c8c723608c0f0b38d66c6c2320b3095967486f3045a/cryptography-41.0.5.tar.gz" + "hash": "079b85658ea2f59c4f43b70f8119a52414cdb7be34da5d019a77bf96d473b960", + "url": "https://files.pythonhosted.org/packages/26/ab/59f271c8f027b8068bbf4dfd6e3ad4c6fc20df0b108ee29c64a1036ba4ce/cryptography-41.0.7-pp310-pypy310_pp73-macosx_10_12_x86_64.whl" }, { "algorithm": "sha256", - "hash": "e886098619d3815e0ad5790c973afeee2c0e6e04b4da90b88e6bd06e2a0b1b72", - "url": "https://files.pythonhosted.org/packages/1b/30/24cf09530df7ee5d85a3070b5ef8de5810b49130d955ae8bce11720b8b2a/cryptography-41.0.5-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl" + "hash": "7a698cb1dac82c35fcf8fe3417a3aaba97de16a01ac914b89a0889d364d2f6be", + "url": "https://files.pythonhosted.org/packages/2c/5d/f9ae5e819dcd2618b2d3e671b22c26b5db1da30414af399e4f624b3fe6cd/cryptography-41.0.7-pp38-pypy38_pp73-macosx_10_12_x86_64.whl" }, { "algorithm": "sha256", - "hash": "3be3ca726e1572517d2bef99a818378bbcf7d7799d5372a46c79c29eb8d166c1", - "url": "https://files.pythonhosted.org/packages/2a/6d/33e42b8595da059bf10beb1529e501d5817ed4480b1d285303d155cdfccd/cryptography-41.0.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl" + "hash": "68a2dec79deebc5d26d617bfdf6e8aab065a4f34934b22d3b5010df3ba36612c", + "url": "https://files.pythonhosted.org/packages/3c/8f/9f5f4d9c00f030e81eda69e689c9777fe665bf34045cec2fd5e71b4859b6/cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl" }, { "algorithm": "sha256", - "hash": "573eb7128cbca75f9157dcde974781209463ce56b5804983e11a1c462f0f4e88", - "url": "https://files.pythonhosted.org/packages/2e/92/720491aae578d21d23934d816ef0620bd1081a1bfdc015f228cc8abccaf1/cryptography-41.0.5-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl" + "hash": "5429ec739a29df2e29e15d082f1d9ad683701f0ec7709ca479b3ff2708dae65a", + "url": "https://files.pythonhosted.org/packages/3e/81/ae2c51ea2b80d57d5756a12df67816230124faea0a762a7a6304fe3c819c/cryptography-41.0.7-cp37-abi3-manylinux_2_28_aarch64.whl" }, { "algorithm": "sha256", - "hash": "e270c04f4d9b5671ebcc792b3ba5d4488bf7c42c3c241a3748e2599776f29696", - "url": "https://files.pythonhosted.org/packages/3e/1b/1703679eface155413730f4a2313aebf846ae7496c15083ae9c07e7324b2/cryptography-41.0.5-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + "hash": "b640981bf64a3e978a56167594a0e97db71c89a479da8e175d8bb5be5178c003", + "url": "https://files.pythonhosted.org/packages/54/f4/3eec29ab2fdd673de8f44af3876b32248eb79550ced822363e35da1644d1/cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl" }, { "algorithm": "sha256", - "hash": "227ec057cd32a41c6651701abc0328135e472ed450f47c2766f23267b792a88e", - "url": "https://files.pythonhosted.org/packages/48/4b/ef0a674e8ea1d7946dfed0fd3a683bd3a8134af7c7b1e2b0d49205bb494b/cryptography-41.0.5-pp38-pypy38_pp73-macosx_10_12_x86_64.whl" + "hash": "43f2552a2378b44869fe8827aa19e69512e3245a219104438692385b0ee119d1", + "url": "https://files.pythonhosted.org/packages/62/bd/69628ab50368b1beb900eb1de5c46f8137169b75b2458affe95f2f470501/cryptography-41.0.7-cp37-abi3-manylinux_2_28_x86_64.whl" }, { "algorithm": "sha256", - "hash": "a48e74dad1fb349f3dc1d449ed88e0017d792997a7ad2ec9587ed17405667e6d", - "url": "https://files.pythonhosted.org/packages/4b/3d/081af0b323a8efd6cd9d9c5b248049b91a7c2cf5473fc67bae374f016781/cryptography-41.0.5-cp37-abi3-musllinux_1_1_x86_64.whl" + "hash": "5a1b41bc97f1ad230a41657d9155113c7521953869ae57ac39ac7f1bb471469a", + "url": "https://files.pythonhosted.org/packages/68/bb/475658ea92653a894589e657d6cea9ae01354db73405d62126ac5e74e2f8/cryptography-41.0.7-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" }, { "algorithm": "sha256", - "hash": "580afc7b7216deeb87a098ef0674d6ee34ab55993140838b14c9b83312b37b86", - "url": "https://files.pythonhosted.org/packages/4d/47/f8f1a8f762e4e7b772d1c9898caec7fa0a1ed4de5b9a536d7997fbb7133c/cryptography-41.0.5-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl" + "hash": "928258ba5d6f8ae644e764d0f996d61a8777559f72dfeb2eea7e2fe0ad6e782d", + "url": "https://files.pythonhosted.org/packages/a9/76/d705397d076fcbf5671544eb72a70b5a5ac83462d23dbd2a365a3bf3692a/cryptography-41.0.7-cp37-abi3-macosx_10_12_x86_64.whl" }, { "algorithm": "sha256", - "hash": "5a70187954ba7292c7876734183e810b728b4f3965fbe571421cb2434d279179", - "url": "https://files.pythonhosted.org/packages/59/34/d3023b52daacea96c4cb0514bd9712011cac444ee45d166919386f7ac13f/cryptography-41.0.5-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl" + "hash": "af03b32695b24d85a75d40e1ba39ffe7db7ffcb099fe507b39fd41a565f1b157", + "url": "https://files.pythonhosted.org/packages/b6/4a/1808333c5ea79cb6d51102036cbcf698704b1fc7a5ccd139957aeadd2311/cryptography-41.0.7-cp37-abi3-musllinux_1_1_aarch64.whl" }, { "algorithm": "sha256", - "hash": "b948e09fe5fb18517d99994184854ebd50b57248736fd4c720ad540560174ec5", - "url": "https://files.pythonhosted.org/packages/76/77/e5ed12b40bbb710137bec76dd43efa6151b43fdece233b647463349e38fa/cryptography-41.0.5-cp37-abi3-macosx_10_12_x86_64.whl" + "hash": "48a0476626da912a44cc078f9893f292f0b3e4c739caf289268168d8f4702a39", + "url": "https://files.pythonhosted.org/packages/b9/19/75d3e8b9b814c09eef76899fea542473273311ab9bfaa1ca4e22c112e660/cryptography-41.0.7-pp39-pypy39_pp73-macosx_10_12_x86_64.whl" }, { "algorithm": "sha256", - "hash": "7d208c21e47940369accfc9e85f0de7693d9a5d843c2509b3846b2db170dfd20", - "url": "https://files.pythonhosted.org/packages/85/62/48bcebd955945d8da3fe9b84a679dbf4bf179e1ac36e583b7eaa47506758/cryptography-41.0.5-cp37-abi3-manylinux_2_28_x86_64.whl" + "hash": "49f0805fc0b2ac8d4882dd52f4a3b935b210935d500b6b805f321addc8177406", + "url": "https://files.pythonhosted.org/packages/c5/07/826d66b6b03c5bfde8b451bea22c41e68d60aafff0ffa02c5f0819844319/cryptography-41.0.7-cp37-abi3-musllinux_1_1_x86_64.whl" }, { "algorithm": "sha256", - "hash": "d38e6031e113b7421db1de0c1b1f7739564a88f1684c6b89234fbf6c11b75147", - "url": "https://files.pythonhosted.org/packages/bb/36/5af9ca6e0b00bd0c40b0d0e3d95a1bfc4fb7e0b94e522d1394ff4f7505cc/cryptography-41.0.5-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + "hash": "37a138589b12069efb424220bf78eac59ca68b95696fc622b6ccc1c0a197204a", + "url": "https://files.pythonhosted.org/packages/c9/46/c488acbc62aedb14c1082c31bd5062caf8881530e97d2443ce33589f2053/cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl" }, { "algorithm": "sha256", - "hash": "ec3b055ff8f1dce8e6ef28f626e0972981475173d7973d63f271b29c8a2897da", - "url": "https://files.pythonhosted.org/packages/e3/21/958e33e2c149461e0a93ca358b794771d55f781ca808efcadb86a4c08e49/cryptography-41.0.5-cp37-abi3-manylinux_2_28_aarch64.whl" + "hash": "13f93ce9bea8016c253b34afc6bd6a75993e5c40672ed5405a9c832f0d4a00bc", + "url": "https://files.pythonhosted.org/packages/ce/b3/13a12ea7edb068de0f62bac88a8ffd92cc2901881b391839851846b84a81/cryptography-41.0.7.tar.gz" }, { "algorithm": "sha256", - "hash": "22892cc830d8b2c89ea60148227631bb96a7da0c1b722f2aac8824b1b7c0b6b8", - "url": "https://files.pythonhosted.org/packages/ea/04/a9b58dbaccbc226c3d81f8edfc3cb91497dc295f0cc28d693c4f524169a4/cryptography-41.0.5-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl" + "hash": "3c78451b78313fa81607fa1b3f1ae0a5ddd8014c38a02d9db0616133987b9cdf", + "url": "https://files.pythonhosted.org/packages/e4/73/5461318abd2fe426855a2f66775c063bbefd377729ece3c3ee048ddf19a5/cryptography-41.0.7-cp37-abi3-macosx_10_12_universal2.whl" }, { "algorithm": "sha256", - "hash": "c707f7afd813478e2019ae32a7c49cd932dd60ab2d2a93e796f68236b7e1fbf1", - "url": "https://files.pythonhosted.org/packages/ed/d9/97ca3b4ee56a77fee0ec7ecb2a354c260a11ad5bc50d1e3de165e71f2ec4/cryptography-41.0.5-pp39-pypy39_pp73-macosx_10_12_x86_64.whl" + "hash": "e3114da6d7f95d2dee7d3f4eec16dacff819740bbab931aff8648cb13c5ff5e7", + "url": "https://files.pythonhosted.org/packages/f4/2b/96fd47dff43cdeb3e24e962fec9ed6ffa1369320146eb95524088265fa00/cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl" } ], "project_name": "cryptography", @@ -860,7 +900,7 @@ "twine>=1.12.0; extra == \"docstest\"" ], "requires_python": ">=3.7", - "version": "41.0.5" + "version": "41.0.7" }, { "artifacts": [ @@ -886,18 +926,18 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "4d7d3d3d5e4e4a9954b448fc8220cd73573e3e32adb00059f6907de6b55dcd5e", - "url": "https://files.pythonhosted.org/packages/5a/e9/d49f3a13d1bcc173eff044850b90dd2ff0628c70ff96960ecbf02d2662de/hypothesis-6.90.0-py3-none-any.whl" + "hash": "d335044492acb03fa1fdb4edacb81cca2e578049fc7306345bc0e8947fef15a9", + "url": "https://files.pythonhosted.org/packages/f7/08/0c2cc8eaebd65b660817bd39747d1c50c21a623eb27fb71ba035e1a2a386/hypothesis-6.92.2-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "0ab33900b9362318bd03d911a77a0dda8629c1877420074d87ae466919f6e4c0", - "url": "https://files.pythonhosted.org/packages/57/2f/a22ec98e4c338a61ae98a3fd247c88b136ae9682ac263ab7079889f7aae6/hypothesis-6.90.0.tar.gz" + "hash": "841f89a486c43bdab55698de8929bd2635639ec20bf6ce98ccd75622d7ee6d41", + "url": "https://files.pythonhosted.org/packages/39/28/e957e56aaaabb7b16586230a814a9ecc007a234baad784b3f7376f7744c5/hypothesis-6.92.2.tar.gz" } ], "project_name": "hypothesis", "requires_dists": [ - "attrs>=19.2.0", + "attrs>=22.2.0", "backports.zoneinfo>=0.2.1; python_version < \"3.9\" and extra == \"all\"", "backports.zoneinfo>=0.2.1; python_version < \"3.9\" and extra == \"zoneinfo\"", "black>=19.10b0; extra == \"all\"", @@ -933,7 +973,7 @@ "tzdata>=2023.3; sys_platform == \"win32\" and extra == \"zoneinfo\"" ], "requires_python": ">=3.8", - "version": "6.90.0" + "version": "6.92.2" }, { "artifacts": [ @@ -975,13 +1015,13 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "386df621becb506bc315a713ec3d4d5b5d6163116955c7dde23622f156b81af6", - "url": "https://files.pythonhosted.org/packages/2a/45/d80a35ce701c1b3b53ab57a585813636acba39f3a8ed87ac01e0f1dfa3c1/msal-1.25.0-py2.py3-none-any.whl" + "hash": "be77ba6a8f49c9ff598bbcdc5dfcf1c9842f3044300109af738e8c3e371065b5", + "url": "https://files.pythonhosted.org/packages/b7/61/2756b963e84db6946e4b93a8e288595106286fc11c7129fcb869267ead67/msal-1.26.0-py2.py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "f44329fdb59f4f044c779164a34474b8a44ad9e4940afbc4c3a3a2bbe90324d9", - "url": "https://files.pythonhosted.org/packages/df/55/2e3047c723a2e3ed880b8a37ab020419c2bae1c0ba3b994fefe0508cb351/msal-1.25.0.tar.gz" + "hash": "224756079fe338be838737682b49f8ebc20a87c1c5eeaf590daae4532b83de15", + "url": "https://files.pythonhosted.org/packages/bb/45/c4dfbe24dd546d141287fa26476ce3206d461d8e4a24be77c84b835e647d/msal-1.26.0.tar.gz" } ], "project_name": "msal", @@ -993,144 +1033,142 @@ "requests<3,>=2.0.0" ], "requires_python": ">=2.7", - "version": "1.25.0" + "version": "1.26.0" }, { "artifacts": [ { "algorithm": "sha256", - "hash": "91e3db9620b822d0ed2b4d1850056a0f133cba04455e62f11612e40f5502f2ee", - "url": "https://files.pythonhosted.org/packages/52/34/a8995d6f0fa626ff6b28dbd9c90f6c2a46bd484bc7ab343d078b0c6ff1a7/msal_extensions-1.0.0-py2.py3-none-any.whl" + "hash": "01be9711b4c0b1a151450068eeb2c4f0997df3bba085ac299de3a66f585e382f", + "url": "https://files.pythonhosted.org/packages/78/8d/ecd0eb93196f25c722ba1b923fd54d190366feccfa5b159d48dacf2b1fee/msal_extensions-1.1.0-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "c676aba56b0cce3783de1b5c5ecfe828db998167875126ca4b47dc6436451354", - "url": "https://files.pythonhosted.org/packages/33/5e/2e23593c67df0b21ffb141c485ca0ae955569203d7ff5064040af968cb81/msal-extensions-1.0.0.tar.gz" + "hash": "6ab357867062db7b253d0bd2df6d411c7891a0ee7308d54d1e4317c1d1c54252", + "url": "https://files.pythonhosted.org/packages/cb/ba/618771542cdc4bc5314c395076c397d67e2bdcd88564c6ca712a2497d1c6/msal-extensions-1.1.0.tar.gz" } ], "project_name": "msal-extensions", "requires_dists": [ "msal<2.0.0,>=0.4.1", - "pathlib2; python_version < \"3.0\"", - "portalocker<2,>=1.0; python_version == \"2.7\" and platform_system != \"Windows\"", - "portalocker<2,>=1.6; python_version == \"2.7\" and platform_system == \"Windows\"", - "portalocker<3,>=1.0; python_version >= \"3.5\" and platform_system != \"Windows\"", - "portalocker<3,>=1.6; python_version >= \"3.5\" and platform_system == \"Windows\"" + "packaging", + "portalocker<3,>=1.0; platform_system != \"Windows\"", + "portalocker<3,>=1.6; platform_system == \"Windows\"" ], - "requires_python": null, - "version": "1.0.0" + "requires_python": ">=3.7", + "version": "1.1.0" }, { "artifacts": [ { "algorithm": "sha256", - "hash": "f7c5d642db47376a0cc130f0de6d055056e010debdaf0707cd2b0fc7e7ef30ea", - "url": "https://files.pythonhosted.org/packages/10/df/92bb67911c6c1d3faa46e4c9a5d0a93dd343dcf56022d1fb97a0c0ee65eb/mypy-1.7.1-py3-none-any.whl" + "hash": "538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d", + "url": "https://files.pythonhosted.org/packages/3a/e3/b582bff8e2fc7056a8a00ec06d2ac3509fc9595af9954099ed70e0418ac3/mypy-1.8.0-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "5cf3f0c5ac72139797953bd50bc6c95ac13075e62dbfcc923571180bebb662e9", - "url": "https://files.pythonhosted.org/packages/02/d8/782be41850c227cfcbc5e65a911fb76724161736aecd8a53a4537d41f9e2/mypy-1.7.1-cp38-cp38-musllinux_1_1_x86_64.whl" + "hash": "4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66", + "url": "https://files.pythonhosted.org/packages/04/8a/1b8c19dd00eb21ad3170762202e4cb82de7c4af0fbd4a4fb7524606858ba/mypy-1.8.0-cp38-cp38-macosx_11_0_arm64.whl" }, { "algorithm": "sha256", - "hash": "2f7f6985d05a4e3ce8255396df363046c28bea790e40617654e91ed580ca7c51", - "url": "https://files.pythonhosted.org/packages/10/ee/8d3add501af4905f986fc54b761b227d245b853c254e24fedda26c9152c9/mypy-1.7.1-cp311-cp311-macosx_11_0_arm64.whl" + "hash": "52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd", + "url": "https://files.pythonhosted.org/packages/08/24/83d9e62ab2031593e94438fdbfd2c32996f4d818be26d2dc33be6870a3a0/mypy-1.8.0-cp312-cp312-macosx_10_9_x86_64.whl" }, { "algorithm": "sha256", - "hash": "f2c2521a8e4d6d769e3234350ba7b65ff5d527137cdcde13ff4d99114b0c8e7d", - "url": "https://files.pythonhosted.org/packages/29/6d/8ffee8037d5371008d729d28ae7e700984db96ed2b6b4cbcc49318b73fda/mypy-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl" + "hash": "df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4", + "url": "https://files.pythonhosted.org/packages/08/d1/a9c12c6890c789fd965ade8b37bef1989f649e87c62fde3df658dff394fc/mypy-1.8.0-cp310-cp310-macosx_11_0_arm64.whl" }, { "algorithm": "sha256", - "hash": "84860e06ba363d9c0eeabd45ac0fde4b903ad7aa4f93cd8b648385a888e23200", - "url": "https://files.pythonhosted.org/packages/2a/01/fcd9d04f5d37fe04c7ab75fed028debd9fdf5b26636bac25adfd4366c02e/mypy-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl" + "hash": "6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07", + "url": "https://files.pythonhosted.org/packages/16/22/25fac51008f0a4b2186da0dba3039128bd75d3fab8c07acd3ea5894f95cc/mypy-1.8.0.tar.gz" }, { "algorithm": "sha256", - "hash": "31902408f4bf54108bbfb2e35369877c01c95adc6192958684473658c322c8a5", - "url": "https://files.pythonhosted.org/packages/44/ae/e45078b06648e42d61461faf1070c7615fe39f904dcef741431beb410b39/mypy-1.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + "hash": "d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817", + "url": "https://files.pythonhosted.org/packages/19/c6/256f253cb3fc6b30b93a9836cf3c816a3ec09f934f7b567f693e5666d14f/mypy-1.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, { "algorithm": "sha256", - "hash": "9c7ac372232c928fff0645d85f273a726970c014749b924ce5710d7d89763a28", - "url": "https://files.pythonhosted.org/packages/67/47/5bee43b465abd613cd21fd5220a6a35547a42ce0d312c7a5e887bb11e9c2/mypy-1.7.1-cp311-cp311-musllinux_1_1_x86_64.whl" + "hash": "42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3", + "url": "https://files.pythonhosted.org/packages/33/14/902484951fa662ee6e044087a50dab4b16b534920dda2eea9380ce2e7b2d/mypy-1.8.0-cp312-cp312-musllinux_1_1_x86_64.whl" }, { "algorithm": "sha256", - "hash": "2643d145af5292ee956aa0a83c2ce1038a3bdb26e033dadeb2f7066fb0c9abce", - "url": "https://files.pythonhosted.org/packages/72/d4/097f61229f6eb148f34b6a61b8d1536fc7de32e46d0d4021ec740278f4d3/mypy-1.7.1-cp39-cp39-macosx_11_0_arm64.whl" + "hash": "ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592", + "url": "https://files.pythonhosted.org/packages/41/6b/25e22dfc730bf698be85600339edefd5d07efe7436cce765631c170a9c31/mypy-1.8.0-cp39-cp39-musllinux_1_1_x86_64.whl" }, { "algorithm": "sha256", - "hash": "d9b338c19fa2412f76e17525c1b4f2c687a55b156320acb588df79f2e6fa9fea", - "url": "https://files.pythonhosted.org/packages/83/9c/51effe3396740868649364ebea5001c694af29ec891bc90aae6944886c45/mypy-1.7.1-cp312-cp312-musllinux_1_1_x86_64.whl" + "hash": "7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6", + "url": "https://files.pythonhosted.org/packages/54/46/4681859453851b40e1c135ba589cde1fce915177c8f213e2aaeb57e1f209/mypy-1.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, { "algorithm": "sha256", - "hash": "6dbdec441c60699288adf051f51a5d512b0d818526d1dcfff5a41f8cd8b4aaf1", - "url": "https://files.pythonhosted.org/packages/88/43/2a04b1cc1a27b1fe623cae9c6840f333e47422598a3556d3824a07d8c2c4/mypy-1.7.1-cp312-cp312-macosx_10_9_x86_64.whl" + "hash": "9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d", + "url": "https://files.pythonhosted.org/packages/66/19/e0c9373258f3e84e1e24af357e5663e6b0058bb5c307287e9d1a473a9687/mypy-1.8.0-cp311-cp311-musllinux_1_1_x86_64.whl" }, { "algorithm": "sha256", - "hash": "75aa828610b67462ffe3057d4d8a4112105ed211596b750b53cbfe182f44777a", - "url": "https://files.pythonhosted.org/packages/90/f6/7a5cd1e2b4095249efae755f0bc2b5fd518da6626050e1d76a00fbcd598b/mypy-1.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + "hash": "4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259", + "url": "https://files.pythonhosted.org/packages/6a/86/e37ae331e2ec831619db70db4e32e9635dc669db940318c297cf248832d8/mypy-1.8.0-cp39-cp39-macosx_11_0_arm64.whl" }, { "algorithm": "sha256", - "hash": "8c5091ebd294f7628eb25ea554852a52058ac81472c921150e3a61cdd68f75a7", - "url": "https://files.pythonhosted.org/packages/a2/bd/16ba65d605058ba897a707db78f6d939dd7cda4eaffe3d53e48d83e8e3e9/mypy-1.7.1-cp38-cp38-macosx_11_0_arm64.whl" + "hash": "485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3", + "url": "https://files.pythonhosted.org/packages/6d/6c/c33a5d50776a769be7ed7ca6709003c99aecd43913b9d82914bc72f154d8/mypy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl" }, { "algorithm": "sha256", - "hash": "1484b8fa2c10adf4474f016e09d7a159602f3239075c7bf9f1627f5acf40ad49", - "url": "https://files.pythonhosted.org/packages/a6/8c/034b199b5b07cfa1adbe3b629fced2549cbb1b95919de7a3bd3b349ad425/mypy-1.7.1-cp310-cp310-macosx_11_0_arm64.whl" + "hash": "f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55", + "url": "https://files.pythonhosted.org/packages/74/e8/30c42199bb5aefb37e02a9bece41f6a62a60a1c427cab8643bc0e7886df1/mypy-1.8.0-cp312-cp312-macosx_11_0_arm64.whl" }, { "algorithm": "sha256", - "hash": "40716d1f821b89838589e5b3106ebbc23636ffdef5abc31f7cd0266db936067e", - "url": "https://files.pythonhosted.org/packages/aa/5e/eaee23820d6ca6d7fc75a894839bce98594bb80821161e37a70b489bfc88/mypy-1.7.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + "hash": "8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9", + "url": "https://files.pythonhosted.org/packages/76/5c/663409829016ca450b68b163cc36c67e0690c546e44923764043b85c175d/mypy-1.8.0-cp310-cp310-musllinux_1_1_x86_64.whl" }, { "algorithm": "sha256", - "hash": "fcb6d9afb1b6208b4c712af0dafdc650f518836065df0d4fb1d800f5d6773db2", - "url": "https://files.pythonhosted.org/packages/ae/30/05a7c016431b3fdbaf0bcf663aee7c5e4b3d2293cd4e0568140cecae4967/mypy-1.7.1.tar.gz" + "hash": "5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8", + "url": "https://files.pythonhosted.org/packages/77/66/c79c051c1cc01c275e5d71acadf831aeef3099272e78c7d8b0685be0a567/mypy-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl" }, { "algorithm": "sha256", - "hash": "944bdc21ebd620eafefc090cdf83158393ec2b1391578359776c00de00e8907a", - "url": "https://files.pythonhosted.org/packages/af/9a/ad4b219cf27496653c2312407d6a47593f25f5b53e2d163ade5961cac78c/mypy-1.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + "hash": "7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b", + "url": "https://files.pythonhosted.org/packages/86/5c/cbf921a0048926c4386410539ff4c3f08448684a92d9c8e73e692f1db154/mypy-1.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, { "algorithm": "sha256", - "hash": "2c6e4464ed5f01dc44dc9821caf67b60a4e5c3b04278286a85c067010653a0eb", - "url": "https://files.pythonhosted.org/packages/b6/ab/39f476f18a45b2a74b38722107ac5c8e50a5ee41f39f7ac46ac50caa630e/mypy-1.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + "hash": "afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218", + "url": "https://files.pythonhosted.org/packages/a6/70/49e9dc3d4ef98c22e09f1d7b0195833ad7eeda19a24fcc42bf1b62c89110/mypy-1.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, { "algorithm": "sha256", - "hash": "ee5d62d28b854eb61889cde4e1dbc10fbaa5560cb39780c3995f6737f7e82120", - "url": "https://files.pythonhosted.org/packages/cb/ed/a96d30f4d5b9d78508765b267058305043985818f771963a455adca73af3/mypy-1.7.1-cp39-cp39-musllinux_1_1_x86_64.whl" + "hash": "4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3", + "url": "https://files.pythonhosted.org/packages/bb/b7/882110d1345847ce660c51fc83b3b590b9512ec2ea44e6cfd629a7d66146/mypy-1.8.0-cp311-cp311-macosx_11_0_arm64.whl" }, { "algorithm": "sha256", - "hash": "4fc3d14ee80cd22367caaaf6e014494415bf440980a3045bf5045b525680ac33", - "url": "https://files.pythonhosted.org/packages/cc/b7/5ae852edccf9eba4d71b3df212928f15759142894a4bb84c1d3cb0ade729/mypy-1.7.1-cp312-cp312-macosx_11_0_arm64.whl" + "hash": "028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6", + "url": "https://files.pythonhosted.org/packages/c4/8f/2042e7e7f19d78ce1ba7fc671700e0ba95d8b8299a86dd2646d2a1f84644/mypy-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl" }, { "algorithm": "sha256", - "hash": "75c4d2a6effd015786c87774e04331b6da863fc3fc4e8adfc3b40aa55ab516fe", - "url": "https://files.pythonhosted.org/packages/de/0e/07025ca7d3fa0c89456cd62b470473341693e205a2cd99add370b22c4012/mypy-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl" + "hash": "42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d", + "url": "https://files.pythonhosted.org/packages/cf/e6/ff8f978edb778452748a3228c014b55d6585cccf62f80323eab391d2b811/mypy-1.8.0-cp38-cp38-musllinux_1_1_x86_64.whl" }, { "algorithm": "sha256", - "hash": "4b901927f16224d0d143b925ce9a4e6b3a758010673eeded9b748f250cf4e8f7", - "url": "https://files.pythonhosted.org/packages/e1/c6/99b845e9eaf7290ff6bdab40e3bbbb5134bebfa56ca622003c24596325c7/mypy-1.7.1-cp311-cp311-macosx_10_9_x86_64.whl" + "hash": "855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae", + "url": "https://files.pythonhosted.org/packages/d6/c4/2ce11ff9ba6c9c9e89df5049ab2325c85e60274194d6816e352926de5684/mypy-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl" }, { "algorithm": "sha256", - "hash": "12cce78e329838d70a204293e7b29af9faa3ab14899aec397798a4b41be7f340", - "url": "https://files.pythonhosted.org/packages/ea/a1/4d821de78ad8c78b2e159359faabd70cc85dcccb0f1c9cd71d5ea5ae332a/mypy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl" + "hash": "2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d", + "url": "https://files.pythonhosted.org/packages/f1/48/e78aa47176bf7c24beb321031043d7c9c99035d816a6eca32d13cc59736f/mypy-1.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" } ], "project_name": "mypy", @@ -1144,7 +1182,7 @@ "typing-extensions>=4.1.0" ], "requires_python": ">=3.8", - "version": "1.7.1" + "version": "1.8.0" }, { "artifacts": [ @@ -1234,6 +1272,19 @@ "requires_python": ">=3.8", "version": "2.8.2" }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "d1454bde93fb1e224166811694d600e746430c006fbb031ea06ecc2ea41bf156", + "url": "https://files.pythonhosted.org/packages/c9/5e/dc6acaf46d78979d6b03458b7a1618a68e152a6776fce95daac5e0f0301b/psycopg2-2.9.9.tar.gz" + } + ], + "project_name": "psycopg2", + "requires_dists": [], + "requires_python": ">=3.7", + "version": "2.9.9" + }, { "artifacts": [ { @@ -1256,13 +1307,13 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "80c50fb8e3dcecfddae1adbcc00ec5822918490c99ab31f6cf6140ca1c1429f0", - "url": "https://files.pythonhosted.org/packages/0a/2b/64066de1c4cf3d4ed623beeb3bbf3f8d0cc26661f1e7d180ec5eb66b75a5/pydantic-2.5.2-py3-none-any.whl" + "hash": "d0caf5954bee831b6bfe7e338c32b9e30c85dfe080c843680783ac2b631673b4", + "url": "https://files.pythonhosted.org/packages/dd/b7/9aea7ee6c01fe3f3c03b8ca3c7797c866df5fecece9d6cb27caa138db2e2/pydantic-2.5.3-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "ff177ba64c6faf73d7afa2e8cad38fd456c0dbe01c9954e71038001cd15a6edd", - "url": "https://files.pythonhosted.org/packages/b7/41/3c8108f79fb7da2d2b17f35744232af4ffcd9e764ebe1e3fd4b26669b325/pydantic-2.5.2.tar.gz" + "hash": "b3ef57c62535b0941697cce638c08900d87fcb67e29cfa99e8a68f747f393f7a", + "url": "https://files.pythonhosted.org/packages/aa/3f/56142232152145ecbee663d70a19a45d078180633321efb3847d2562b490/pydantic-2.5.3.tar.gz" } ], "project_name": "pydantic", @@ -1270,373 +1321,373 @@ "annotated-types>=0.4.0", "email-validator>=2.0.0; extra == \"email\"", "importlib-metadata; python_version == \"3.7\"", - "pydantic-core==2.14.5", + "pydantic-core==2.14.6", "typing-extensions>=4.6.1" ], "requires_python": ">=3.7", - "version": "2.5.2" + "version": "2.5.3" }, { "artifacts": [ { "algorithm": "sha256", - "hash": "f5e412d717366e0677ef767eac93566582518fe8be923361a5c204c1a62eaafe", - "url": "https://files.pythonhosted.org/packages/ef/e9/ffaec12924f90d4f2f589b0f6f510b671a561b02dce47ce9fad559b41ac3/pydantic_core-2.14.5-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl" + "hash": "23598acb8ccaa3d1d875ef3b35cb6376535095e9405d91a3d57a8c7db5d29341", + "url": "https://files.pythonhosted.org/packages/e9/b8/5baba04b116546302bc0a07ba0989326a167aeec29fd6f5cadc7deb758b1/pydantic_core-2.14.6-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl" }, { "algorithm": "sha256", - "hash": "79e0a2cdbdc7af3f4aee3210b1172ab53d7ddb6a2d8c24119b5706e622b346d0", - "url": "https://files.pythonhosted.org/packages/00/47/88baa62574f06e2dd5b9c0285b5b9b300c79e3d808c5d5a81f04e0817b82/pydantic_core-2.14.5-pp310-pypy310_pp73-macosx_10_7_x86_64.whl" + "hash": "a983cca5ed1dd9a35e9e42ebf9f278d344603bfcb174ff99a5815f953925140a", + "url": "https://files.pythonhosted.org/packages/0b/d0/adf341fb8ed080bf5abb91c42752ffa099d8439e45d3fa40a21f259f724c/pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" }, { "algorithm": "sha256", - "hash": "c2027d05c8aebe61d898d4cffd774840a9cb82ed356ba47a90d99ad768f39789", - "url": "https://files.pythonhosted.org/packages/03/99/f7eb0cc34ea21e94aa0610a9c0794064847adc38ab824c8722e9fe35ebba/pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + "hash": "6c8edaea3089bf908dd27da8f5d9e395c5b4dc092dbcce9b65e7156099b4b937", + "url": "https://files.pythonhosted.org/packages/0d/18/7c17d33b2c8dea2189b2547bafcb70a69a3e537eec12429cc0abfedab683/pydantic_core-2.14.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl" }, { "algorithm": "sha256", - "hash": "774de879d212db5ce02dfbf5b0da9a0ea386aeba12b0b95674a4ce0593df3d07", - "url": "https://files.pythonhosted.org/packages/05/7b/9083133f247b9f712f5718c66b3e39194ea679fbe85567bf4dc9d08557bb/pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + "hash": "b89f4477d915ea43b4ceea6756f63f0288941b6443a2b28c69004fe07fde0d0d", + "url": "https://files.pythonhosted.org/packages/13/33/9f761908fde3a6bb10ac865459a6931e53a2cde622782d243365e70981b7/pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl" }, { "algorithm": "sha256", - "hash": "2d0ae0d8670164e10accbeb31d5ad45adb71292032d0fdb9079912907f0085f4", - "url": "https://files.pythonhosted.org/packages/0b/32/0a6ee79ed34e8934a54548495883017dfaf3fc742b0d0d02afa154f1f49d/pydantic_core-2.14.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl" + "hash": "1b027c86c66b8627eb90e57aee1f526df77dc6d8b354ec498be9a757d513b92b", + "url": "https://files.pythonhosted.org/packages/14/53/7844d20be3a334ea46cdcde8a480cf47e31026d4117d7415a0144d7379c9/pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" }, { "algorithm": "sha256", - "hash": "59986de5710ad9613ff61dd9b02bdd2f615f1a7052304b79cc8fa2eb4e336d2d", - "url": "https://files.pythonhosted.org/packages/10/89/bbb9bb3bd59b1cb36a87c2f6b6e3b2858fdb6ac438539f67a6c93a91ba5e/pydantic_core-2.14.5-cp312-cp312-musllinux_1_1_x86_64.whl" + "hash": "75ec284328b60a4e91010c1acade0c30584f28a1f345bc8f72fe8b9e46ec6a96", + "url": "https://files.pythonhosted.org/packages/24/1d/601f861c0d76154217ea6b066e39f04159a761b9c3a7ca56b0dd0267ce3a/pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" }, { "algorithm": "sha256", - "hash": "4e40f2bd0d57dac3feb3a3aed50f17d83436c9e6b09b16af271b6230a2915459", - "url": "https://files.pythonhosted.org/packages/12/00/bd693e0bf24fa016c7194ac9ca671903b0938a5aa2603f7b5779070a15a0/pydantic_core-2.14.5-cp311-cp311-macosx_10_7_x86_64.whl" + "hash": "36026d8f99c58d7044413e1b819a67ca0e0b8ebe0f25e775e6c3d1fabb3c38fb", + "url": "https://files.pythonhosted.org/packages/26/4b/da4ed701ee2ff392916f19149f8fb6d705282d96971cbf256142d0c11594/pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" }, { "algorithm": "sha256", - "hash": "ca61d858e4107ce5e1330a74724fe757fc7135190eb5ce5c9d0191729f033209", - "url": "https://files.pythonhosted.org/packages/19/1c/d9ba54c20c76706eb04491187d2d22ce56982ec3d999c6915ceb16755ebd/pydantic_core-2.14.5-cp39-cp39-musllinux_1_1_x86_64.whl" + "hash": "d5c28525c19f5bb1e09511669bb57353d22b94cf8b65f3a8d141c389a55dec95", + "url": "https://files.pythonhosted.org/packages/28/1e/04ede6259a552777a859d2d5828aedd540ca0db967641d61be864a49671a/pydantic_core-2.14.6-pp310-pypy310_pp73-macosx_10_7_x86_64.whl" }, { "algorithm": "sha256", - "hash": "ba6b6b3846cfc10fdb4c971980a954e49d447cd215ed5a77ec8190bc93dd7bc5", - "url": "https://files.pythonhosted.org/packages/1a/b8/7f1ca7c80dcb44bd525ba5e5feba5e45be686daeee535b434628be0f6cd7/pydantic_core-2.14.5-cp39-cp39-musllinux_1_1_aarch64.whl" + "hash": "72f9a942d739f09cd42fffe5dc759928217649f070056f03c70df14f5770acf9", + "url": "https://files.pythonhosted.org/packages/28/fc/bfb0da2b2d5b44e49c4c0ce99b07bbfd9f1a4dc92fd3e328a5cf1144467e/pydantic_core-2.14.6-cp310-cp310-macosx_10_7_x86_64.whl" }, { "algorithm": "sha256", - "hash": "09b0e985fbaf13e6b06a56d21694d12ebca6ce5414b9211edf6f17738d82b0f8", - "url": "https://files.pythonhosted.org/packages/1d/0f/bb0bd20e5bbabdf99d0a25858cf77b74926826a75d0458dc4842cf360ea5/pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + "hash": "f85f3843bdb1fe80e8c206fe6eed7a1caeae897e496542cee499c374a85c6e08", + "url": "https://files.pythonhosted.org/packages/29/5c/63eb74c7a97daf0ee45dc876f0b0d9cdea9c5c9d64e92508a765cb802e14/pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" }, { "algorithm": "sha256", - "hash": "c97bee68898f3f4344eb02fec316db93d9700fb1e6a5b760ffa20d71d9a46ce3", - "url": "https://files.pythonhosted.org/packages/1f/f0/a588fd5d66c9c3bf16d63cac3437e2260cbddd7df7a089ca58b8e94dcb3e/pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + "hash": "b6ca36c12a5120bad343eef193cc0122928c5c7466121da7c20f41160ba00ba2", + "url": "https://files.pythonhosted.org/packages/2a/09/c39be628d6068952f30b381576a4392af2024505747572cd70b19f6d9bde/pydantic_core-2.14.6-cp311-cp311-musllinux_1_1_x86_64.whl" }, { "algorithm": "sha256", - "hash": "60b7607753ba62cf0739177913b858140f11b8af72f22860c28eabb2f0a61937", - "url": "https://files.pythonhosted.org/packages/22/11/3f332887a888217e28b23c115c343ef89ccf5f49bbbd88d9317c707b00ac/pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl" + "hash": "a3dde6cac75e0b0902778978d3b1646ca9f438654395a362cb21d9ad34b24acf", + "url": "https://files.pythonhosted.org/packages/31/76/ee3c136138fbda5f58c3c49371503b42f3a9c678ef284a0b39be17253d78/pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl" }, { "algorithm": "sha256", - "hash": "dcf4e6d85614f7a4956c2de5a56531f44efb973d2fe4a444d7251df5d5c4dcfd", - "url": "https://files.pythonhosted.org/packages/28/27/83ad40b64e8503b0eaeb88f6206225d0a3be1bd1d852dfdc4437f7e02a69/pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + "hash": "8ed1af8692bd8d2a29d702f1a2e6065416d76897d726e45a1775b1444f5928a7", + "url": "https://files.pythonhosted.org/packages/39/10/dc849eb0c1890c99958d3ae2cfacb502e4d0ab0360c63c7a20231ea04b32/pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl" }, { "algorithm": "sha256", - "hash": "9bd18fee0923ca10f9a3ff67d4851c9d3e22b7bc63d1eddc12f439f436f2aada", - "url": "https://files.pythonhosted.org/packages/28/81/f5452ccf3b15aa280188fbf2b6ab39ed700623df4fcc28675f19eee9634a/pydantic_core-2.14.5-cp311-cp311-musllinux_1_1_aarch64.whl" + "hash": "4f86f1f318e56f5cbb282fe61eb84767aee743ebe32c7c0834690ebea50c0a6b", + "url": "https://files.pythonhosted.org/packages/3d/cf/d2e97b2bfd0bff7c4e9086fab03956003e906557c9c52941c15fed75152d/pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl" }, { "algorithm": "sha256", - "hash": "439c9afe34638ace43a49bf72d201e0ffc1a800295bed8420c2a9ca8d5e3dbb3", - "url": "https://files.pythonhosted.org/packages/2a/83/05756b6656c3478e34e5dd5fcb693034f586bb1d437365928f6989bb0050/pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl" + "hash": "3860c62057acd95cc84044e758e47b18dcd8871a328ebc8ccdefd18b0d26a21b", + "url": "https://files.pythonhosted.org/packages/43/39/cf14a183949bf162ab13a327b2f3a0f757e610f9c378a850e195d71bcfa0/pydantic_core-2.14.6-cp38-cp38-macosx_11_0_arm64.whl" }, { "algorithm": "sha256", - "hash": "c339dabd8ee15f8259ee0f202679b6324926e5bc9e9a40bf981ce77c038553db", - "url": "https://files.pythonhosted.org/packages/2a/b7/f85e5fd4504fae0df3eadd4bf9e0c495ecbdb804dc9be65653119454571e/pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + "hash": "399ac0891c284fa8eb998bcfa323f2234858f5d2efca3950ae58c8f88830f145", + "url": "https://files.pythonhosted.org/packages/48/64/de5432d19c42adbb26c4513866e2639c37c9081687c670bf8dc16cedfb6f/pydantic_core-2.14.6-pp38-pypy38_pp73-macosx_11_0_arm64.whl" }, { "algorithm": "sha256", - "hash": "6b9ff467ffbab9110e80e8c8de3bcfce8e8b0fd5661ac44a09ae5901668ba997", - "url": "https://files.pythonhosted.org/packages/2c/43/d94f10d82ccffc86bd69bfac73c54589703008236d63965dd40005a80af9/pydantic_core-2.14.5-cp310-cp310-musllinux_1_1_aarch64.whl" + "hash": "9c6a5c79b28003543db3ba67d1df336f253a87d3112dac3a51b94f7d48e4c0e1", + "url": "https://files.pythonhosted.org/packages/51/47/9f996e867123189f0b12364b00057887b61193d3d004a4391450e980512f/pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" }, { "algorithm": "sha256", - "hash": "b53e9ad053cd064f7e473a5f29b37fc4cc9dc6d35f341e6afc0155ea257fc911", - "url": "https://files.pythonhosted.org/packages/36/53/d4ae1f5273cbc83d5a4c158916a9235c1bfc8194be63958b4b5ff11bf838/pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" + "hash": "eedf97be7bc3dbc8addcef4142f4b4164066df0c6f36397ae4aaed3eb187d8ab", + "url": "https://files.pythonhosted.org/packages/55/0f/45626f8bf7f7973320531bb384ac302eb9b05a70885b9db2bf1db4cf447b/pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, { "algorithm": "sha256", - "hash": "e60f112ac88db9261ad3a52032ea46388378034f3279c643499edb982536a093", - "url": "https://files.pythonhosted.org/packages/3a/dd/fc81e3ea962a356a705fa06965a7dbc0b204da014f238df95f1cd276bfab/pydantic_core-2.14.5-cp312-cp312-macosx_10_7_x86_64.whl" + "hash": "7f5025db12fc6de7bc1104d826d5aee1d172f9ba6ca936bf6474c2148ac336c1", + "url": "https://files.pythonhosted.org/packages/55/d1/a291cef89adaa3d82b89055a010bd60560a7bda798e2e729d3dfeb875236/pydantic_core-2.14.6-cp38-cp38-musllinux_1_1_aarch64.whl" }, { "algorithm": "sha256", - "hash": "c6eae413494a1c3f89055da7a5515f32e05ebc1a234c27674a6956755fb2236f", - "url": "https://files.pythonhosted.org/packages/3c/5e/2a822aa3f3dd68fa45129d4d50290625e97b9b223cf76bafeb765430a0bc/pydantic_core-2.14.5-cp38-cp38-macosx_11_0_arm64.whl" + "hash": "86c963186ca5e50d5c8287b1d1c9d3f8f024cbe343d048c5bd282aec2d8641f2", + "url": "https://files.pythonhosted.org/packages/59/f6/1e7193769d24b32b19139fb875693d7a351af17f10354e7583a0f7b61a49/pydantic_core-2.14.6-pp39-pypy39_pp73-macosx_10_7_x86_64.whl" }, { "algorithm": "sha256", - "hash": "2248485b0322c75aee7565d95ad0e16f1c67403a470d02f94da7344184be770f", - "url": "https://files.pythonhosted.org/packages/41/0a/1c0372929f3723587d66c188cbdd0c47d269447e0ac8f231f0db0f9bb03c/pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" + "hash": "00646784f6cd993b1e1c0e7b0fdcbccc375d539db95555477771c27555e3c556", + "url": "https://files.pythonhosted.org/packages/5c/7a/ceb3c9228ad9ff009ee70fd09ffb9160a45a8adaac5c9a90bc9496a1020e/pydantic_core-2.14.6-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl" }, { "algorithm": "sha256", - "hash": "d2ae91f50ccc5810b2f1b6b858257c9ad2e08da70bf890dee02de1775a387c66", - "url": "https://files.pythonhosted.org/packages/46/df/5159aa30c4b2128f14634f3b3e9e19df228364c2107cda7910d058cc1bca/pydantic_core-2.14.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl" + "hash": "3dcf1978be02153c6a31692d4fbcc2a3f1db9da36039ead23173bc256ee3b91b", + "url": "https://files.pythonhosted.org/packages/5d/ca/e8fe62da4eb4b538c380900372021c560c3514514677d6d328ac5b95da7c/pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl" }, { "algorithm": "sha256", - "hash": "ba39688799094c75ea8a16a6b544eb57b5b0f3328697084f3f2790892510d144", - "url": "https://files.pythonhosted.org/packages/47/85/190ee74d99149a6d16bf14016d0011b629702d37b955070a5fabaa3be8a8/pydantic_core-2.14.5-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl" + "hash": "cb92f9061657287eded380d7dc455bbf115430b3aa4741bdc662d02977e7d0af", + "url": "https://files.pythonhosted.org/packages/5e/58/7cac843607f3b2d0af1768fae90ef219413db163a7cfb7557344edfeed2f/pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl" }, { "algorithm": "sha256", - "hash": "c2d97e906b4ff36eb464d52a3bc7d720bd6261f64bc4bcdbcd2c557c02081ed2", - "url": "https://files.pythonhosted.org/packages/4a/5c/cc41dad06acd213f093581454812d6bb20311524ecf265f893e05e4fbe84/pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl" + "hash": "9862bf828112e19685b76ca499b379338fd4c5c269d897e218b2ae8fcb80139d", + "url": "https://files.pythonhosted.org/packages/5f/0c/3aeafa496aaf656be3682cbcacbfe3b4a4b366aaddac0ea74fb2c7c276a2/pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl" }, { "algorithm": "sha256", - "hash": "49b08aae5013640a3bfa25a8eebbd95638ec3f4b2eaf6ed82cf0c7047133f03b", - "url": "https://files.pythonhosted.org/packages/4f/10/c44d89cb2fa31a27766aeb39b11380ad2e01bdab7f4bf63b18dfea20ec00/pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + "hash": "26a92ae76f75d1915806b77cf459811e772d8f71fd1e4339c99750f0e7f6324f", + "url": "https://files.pythonhosted.org/packages/69/ed/6a318d3846ac45e4e8d7c81a4c4f9cad341f4715521cc2cc7baecd6be9c0/pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl" }, { "algorithm": "sha256", - "hash": "16e29bad40bcf97aac682a58861249ca9dcc57c3f6be22f506501833ddb8939c", - "url": "https://files.pythonhosted.org/packages/57/03/0f238853ad2c93ba344ad702234ee02ff8daa10b7cd680523a40a851499d/pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl" + "hash": "0348b1dc6b76041516e8a854ff95b21c55f5a411c3297d2ca52f5528e49d8411", + "url": "https://files.pythonhosted.org/packages/79/ae/ec8eaa6d9a1305100321d7b9c3c87e015ae61da02a877cfd16b0366b18ff/pydantic_core-2.14.6-cp39-cp39-musllinux_1_1_x86_64.whl" }, { "algorithm": "sha256", - "hash": "615a0a4bff11c45eb3c1996ceed5bdaa2f7b432425253a7c2eed33bb86d80abc", - "url": "https://files.pythonhosted.org/packages/5a/cf/1348242330768c4014ba26c51a847c23db105da6b21bdcefbc9087926af3/pydantic_core-2.14.5-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl" + "hash": "6a31d98c0d69776c2576dda4b77b8e0c69ad08e8b539c25c7d0ca0dc19a50d6c", + "url": "https://files.pythonhosted.org/packages/7d/3a/46913f3134aff44d11edd7bdbba88efe6081f963014e6eaccf83fd8de2d7/pydantic_core-2.14.6-cp310-cp310-macosx_11_0_arm64.whl" }, { "algorithm": "sha256", - "hash": "c2adbe22ab4babbca99c75c5d07aaf74f43c3195384ec07ccbd2f9e3bddaecec", - "url": "https://files.pythonhosted.org/packages/62/5c/de43c71edd1cda67e5cc194873ee84483230ac9cf576d6020ee945e0494e/pydantic_core-2.14.5-cp38-cp38-musllinux_1_1_aarch64.whl" + "hash": "dea7fcd62915fb150cdc373212141a30037e11b761fbced340e9db3379b892d4", + "url": "https://files.pythonhosted.org/packages/7d/77/cbfa02b5f46c5ec6be131d97ae93eef883e25d61b4f4d0a058c792b7e3a2/pydantic_core-2.14.6-cp311-cp311-macosx_10_7_x86_64.whl" }, { "algorithm": "sha256", - "hash": "823fcc638f67035137a5cd3f1584a4542d35a951c3cc68c6ead1df7dac825c26", - "url": "https://files.pythonhosted.org/packages/63/e6/8887679b7f923290db2638bf80733c609aaefaae29b9fe99b83f800c1910/pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl" + "hash": "27e524624eace5c59af499cd97dc18bb201dc6a7a2da24bfc66ef151c69a5f2a", + "url": "https://files.pythonhosted.org/packages/7f/3d/91a26a7004a57f374d85d837b4b06dde818045ddba34bc19909e04e2a14d/pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, { "algorithm": "sha256", - "hash": "6d30226dfc816dd0fdf120cae611dd2215117e4f9b124af8c60ab9093b6e8e71", - "url": "https://files.pythonhosted.org/packages/64/26/cffb93fe9c6b5a91c497f37fae14a4b073ecbc47fc36a9979c7aa888b245/pydantic_core-2.14.5.tar.gz" + "hash": "4ce8299b481bcb68e5c82002b96e411796b844d72b3e92a3fbedfe8e19813eab", + "url": "https://files.pythonhosted.org/packages/80/8c/d40937f7f7ccfe9776d1e32b36cebe606da9f11624927bd26722c43ea9cb/pydantic_core-2.14.6-cp39-cp39-macosx_10_7_x86_64.whl" }, { "algorithm": "sha256", - "hash": "61ea96a78378e3bd5a0be99b0e5ed00057b71f66115f5404d0dae4819f495093", - "url": "https://files.pythonhosted.org/packages/66/11/f3e35b74745b5167df5f1dc15bd2368dbaa9e70d2ad8438a0c9485b78da5/pydantic_core-2.14.5-cp310-cp310-musllinux_1_1_x86_64.whl" + "hash": "172de779e2a153d36ee690dbc49c6db568d7b33b18dc56b69a7514aecbcf380d", + "url": "https://files.pythonhosted.org/packages/84/e4/da29895abb136eea169944eb81f866d783255c4a6fd581c667c15743b171/pydantic_core-2.14.6-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl" }, { "algorithm": "sha256", - "hash": "27548e16c79702f1e03f5628589c6057c9ae17c95b4c449de3c66b589ead0520", - "url": "https://files.pythonhosted.org/packages/66/44/ed210be2a055e612d58146be167017e43a76ff79807c753a264d7084d24d/pydantic_core-2.14.5-pp38-pypy38_pp73-macosx_11_0_arm64.whl" + "hash": "8b93785eadaef932e4fe9c6e12ba67beb1b3f1e5495631419c784ab87e975670", + "url": "https://files.pythonhosted.org/packages/88/bb/58bd737b1f4a3b567410fd7a55f2e0ed4ba3209bb1a7a35856714a322a04/pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" }, { "algorithm": "sha256", - "hash": "7f8210297b04e53bc3da35db08b7302a6a1f4889c79173af69b72ec9754796b8", - "url": "https://files.pythonhosted.org/packages/6c/ba/f3eee66c90f2e4f468fc01cace46ec633f9d47d53e1610ef3bc6003fc936/pydantic_core-2.14.5-pp39-pypy39_pp73-macosx_10_7_x86_64.whl" + "hash": "5aa90562bc079c6c290f0512b21768967f9968e4cfea84ea4ff5af5d917016e4", + "url": "https://files.pythonhosted.org/packages/8f/2d/919d3642da44bc9d9c60a2e7bbda04633fc3ffbd6768c355ac0d7e2424d7/pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" }, { "algorithm": "sha256", - "hash": "a33324437018bf6ba1bb0f921788788641439e0ed654b233285b9c69704c27b4", - "url": "https://files.pythonhosted.org/packages/75/cf/2f6e6410ae735c11df32c391948a6c601a22f40f414b5dfc24f2def8c064/pydantic_core-2.14.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl" + "hash": "036137b5ad0cb0004c75b579445a1efccd072387a36c7f217bb8efd1afbe5245", + "url": "https://files.pythonhosted.org/packages/90/28/3c6843e6b203999be2660d3f114be196f2182dcac533dc764ad320c9184d/pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, { "algorithm": "sha256", - "hash": "0f6116a558fd06d1b7c2902d1c4cf64a5bd49d67c3540e61eccca93f41418124", - "url": "https://files.pythonhosted.org/packages/76/b3/54001e0b49c3eb135cccb1d353c8bd758b77b60d3c610b47888ac1e12fa6/pydantic_core-2.14.5-cp38-cp38-musllinux_1_1_x86_64.whl" + "hash": "db453f2da3f59a348f514cfbfeb042393b68720787bbef2b4c6068ea362c8149", + "url": "https://files.pythonhosted.org/packages/92/2a/8cff567680c0d5e03ef4da218656a61286add825b4733476e6ba13ffeee9/pydantic_core-2.14.6-cp38-cp38-macosx_10_7_x86_64.whl" }, { "algorithm": "sha256", - "hash": "8aa1768c151cf562a9992462239dfc356b3d1037cc5a3ac829bb7f3bda7cc1f9", - "url": "https://files.pythonhosted.org/packages/78/ef/4fd3b40a82ea729a2566575aeec119449b0bf1b4c13d9255e8ac2a40a58b/pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl" + "hash": "86ce5fcfc3accf3a07a729779d0b86c5d0309a4764c897d86c11089be61da160", + "url": "https://files.pythonhosted.org/packages/93/57/9a77cc69f05f725a2b492a18209a43ba4e8b9ee179d3c27a8b6b3ab2f921/pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" }, { "algorithm": "sha256", - "hash": "b9b759b77f5337b4ea024f03abc6464c9f35d9718de01cfe6bae9f2e139c397e", - "url": "https://files.pythonhosted.org/packages/79/73/d1d3846f19b11a7d62e93e5c38c5386c42f3e42abad46c0d1904ccdf8fef/pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + "hash": "599c87d79cab2a6a2a9df4aefe0455e61e7d2aeede2f8577c1b7c0aec643ee8e", + "url": "https://files.pythonhosted.org/packages/97/9e/f42db0e2931cd67bf990d22215ec50444e31aa6e80e63b8531ab1a5f3ffb/pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, { "algorithm": "sha256", - "hash": "eac5c82fc632c599f4639a5886f96867ffced74458c7db61bc9a66ccb8ee3113", - "url": "https://files.pythonhosted.org/packages/7c/f5/3e59681bd53955da311a7f4efbb6315d01006e9d18b8a06b527a22d3d923/pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + "hash": "370ffecb5316ed23b667d99ce4debe53ea664b99cc37bfa2af47bc769056d534", + "url": "https://files.pythonhosted.org/packages/9b/cd/a2db754b0124e64ad7912160d9c9db310cbd52a990841ef121b53453992d/pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl" }, { "algorithm": "sha256", - "hash": "ebb4e035e28f49b6f1a7032920bb9a0c064aedbbabe52c543343d39341a5b2a3", - "url": "https://files.pythonhosted.org/packages/7d/de/df454233c7960a899846f037209204df1d8ab761bb81a7561abb4daf2288/pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl" + "hash": "dfcebb950aa7e667ec226a442722134539e77c575f6cfaa423f24371bb8d2e94", + "url": "https://files.pythonhosted.org/packages/9d/21/32afbed9bfedf916dff87846e10ecd8711ba63c88cd6c9bcfc3297ef22ca/pydantic_core-2.14.6-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl" }, { "algorithm": "sha256", - "hash": "c1452a1acdf914d194159439eb21e56b89aa903f2e1c65c60b9d874f9b950e5d", - "url": "https://files.pythonhosted.org/packages/7e/ff/72d57544a70f4f37a06c40cfe1c4a038bc21db308e916a277faa1854a1d8/pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl" + "hash": "cf10b7d58ae4a1f07fccbf4a0a956d705356fea05fb4c70608bb6fa81d103cda", + "url": "https://files.pythonhosted.org/packages/9e/0a/c56318f1668de782f31b6e9798217e2e5a99d4cce7a8eddffb60bebe3c09/pydantic_core-2.14.6-pp38-pypy38_pp73-macosx_10_7_x86_64.whl" }, { "algorithm": "sha256", - "hash": "6637560562134b0e17de333d18e69e312e0458ee4455bdad12c37100b7cad706", - "url": "https://files.pythonhosted.org/packages/80/ee/c1ce56f63f08bf261f243d7f5faed5b1d2215d231996e74f7dd89559e9e5/pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl" + "hash": "e8a5ac97ea521d7bde7621d86c30e86b798cdecd985723c4ed737a2aa9e77d0c", + "url": "https://files.pythonhosted.org/packages/9f/7a/2e906fc1a5e4ca45e730118f0afb4878a39a1d505d895835d8cc5452446c/pydantic_core-2.14.6-cp312-cp312-musllinux_1_1_x86_64.whl" }, { "algorithm": "sha256", - "hash": "206ed23aecd67c71daf5c02c3cd19c0501b01ef3cbf7782db9e4e051426b3d0d", - "url": "https://files.pythonhosted.org/packages/84/01/079cd694491f1e05a1caae15a2ee32321a8fa748a34a183f6a38bf885af9/pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + "hash": "71d72ca5eaaa8d38c8df16b7deb1a2da4f650c41b58bb142f3fb75d5ad4a611f", + "url": "https://files.pythonhosted.org/packages/a2/7e/4af14122c7ea67ad5582fddae56f7827044f6b43cca6c7e7421686cca3de/pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" }, { "algorithm": "sha256", - "hash": "103ef8d5b58596a731b690112819501ba1db7a36f4ee99f7892c40da02c3e189", - "url": "https://files.pythonhosted.org/packages/89/5c/e0584d534863639757e05479a3c1172550e3d3dab0c39b79e41692d1804d/pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl" + "hash": "2f6ffc6701a0eb28648c845f4945a194dc7ab3c651f535b81793251e1185ac3d", + "url": "https://files.pythonhosted.org/packages/a5/5c/289261738045fa6b97e75d8c2ee110fab5c2d1025f7d345816f0f56f1c1e/pydantic_core-2.14.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl" }, { "algorithm": "sha256", - "hash": "cb4679d4c2b089e5ef89756bc73e1926745e995d76e11925e3e96a76d5fa51fc", - "url": "https://files.pythonhosted.org/packages/8f/af/b202d44845f89e9c997f2f351be35a76ff78304eb926b1bdb33929de40db/pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + "hash": "cdee837710ef6b56ebd20245b83799fce40b265b3b406e51e8ccc5b85b9099b7", + "url": "https://files.pythonhosted.org/packages/a5/f8/07a2563f40b863ba97f3db648697f3f1d7b7edf1bd679f210064cb556e74/pydantic_core-2.14.6-cp312-cp312-macosx_11_0_arm64.whl" }, { "algorithm": "sha256", - "hash": "70f4b4851dbb500129681d04cc955be2a90b2248d69273a787dda120d5cf1f69", - "url": "https://files.pythonhosted.org/packages/90/6f/52cb83061430628878c34fdb199ccc8313a104f1390d99bff4a29b2ff6fe/pydantic_core-2.14.5-cp312-cp312-musllinux_1_1_aarch64.whl" + "hash": "982487f8931067a32e72d40ab6b47b1628a9c5d344be7f1a4e668fb462d2da42", + "url": "https://files.pythonhosted.org/packages/ab/3d/f4739255d8676debf398116e8ded523cf9bc9289a14734b3dc10645da67d/pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl" }, { "algorithm": "sha256", - "hash": "ccd4d5702bb90b84df13bd491be8d900b92016c5a455b7e14630ad7449eb03f8", - "url": "https://files.pythonhosted.org/packages/94/cd/de236ed3c5a2a0f5545cf78e7a6aaa04d8ee10dc3b738cc516bfc59dfb18/pydantic_core-2.14.5-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl" + "hash": "43e166ad47ba900f2542a80d83f9fc65fe99eb63ceec4debec160ae729824052", + "url": "https://files.pythonhosted.org/packages/ae/91/b5d718de2fc191a1937470e79b53535cf0c3a87b2f21ee927710f4dd4570/pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl" }, { "algorithm": "sha256", - "hash": "a6a16f4a527aae4f49c875da3cdc9508ac7eef26e7977952608610104244e1b7", - "url": "https://files.pythonhosted.org/packages/9a/e1/c33fcdbdad7f5c29376fa2e57f8d60f966c44fc77fc36a70d0ae03bbe813/pydantic_core-2.14.5-cp39-cp39-macosx_10_7_x86_64.whl" + "hash": "fd9e98b408384989ea4ab60206b8e100d8687da18b5c813c11e92fd8212a98e0", + "url": "https://files.pythonhosted.org/packages/b1/1c/ab01fa05c9fc885a73357116c494feafe1207035f13848e4a772fc9d6154/pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" }, { "algorithm": "sha256", - "hash": "c949f04ecad823f81b1ba94e7d189d9dfb81edbb94ed3f8acfce41e682e48cef", - "url": "https://files.pythonhosted.org/packages/9c/52/2fc8b7e07f360993bc3d5f9ea743aac9f59287002035887c7d4f45bc6fb6/pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" + "hash": "667aa2eac9cd0700af1ddb38b7b1ef246d8cf94c85637cbb03d7757ca4c3fdec", + "url": "https://files.pythonhosted.org/packages/b1/26/4bd7ac215215322a693c178a022993450ebf7b1e91b26941f72407e1e9a1/pydantic_core-2.14.6-cp312-cp312-macosx_10_7_x86_64.whl" }, { "algorithm": "sha256", - "hash": "f0cbc7fff06a90bbd875cc201f94ef0ee3929dfbd5c55a06674b60857b8b85ed", - "url": "https://files.pythonhosted.org/packages/9e/f3/9e3d334976b5051cd18e3feef06516ead3230efb8b9af8514bc52b2795b1/pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + "hash": "747265448cb57a9f37572a488a57d873fd96bf51e5bb7edb52cfb37124516da4", + "url": "https://files.pythonhosted.org/packages/b2/4a/3be721510f2fea9ce56b25812e6d6ecea9833c06fa8ae479cd41beb404f5/pydantic_core-2.14.6-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl" }, { "algorithm": "sha256", - "hash": "aae7ea3a1c5bb40c93cad361b3e869b180ac174656120c42b9fadebf685d121b", - "url": "https://files.pythonhosted.org/packages/a4/09/90f5a03ab19e21601c6fec11fc9dea30e3228731e12b2f75f58d02430b85/pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + "hash": "1fd0c1d395372843fba13a51c28e3bb9d59bd7aebfeb17358ffaaa1e4dbbe948", + "url": "https://files.pythonhosted.org/packages/b2/7d/8304d8471cfe4288f95a3065ebda56f9790d087edc356ad5bd83c89e2d79/pydantic_core-2.14.6.tar.gz" }, { "algorithm": "sha256", - "hash": "678265f7b14e138d9a541ddabbe033012a2953315739f8cfa6d754cc8063e8ca", - "url": "https://files.pythonhosted.org/packages/a6/c6/01758bde5022817fd202ee9de506ea5ba3cedc9aa4b421edabda0d1b9fa4/pydantic_core-2.14.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl" + "hash": "36099c69f6b14fc2c49d7996cbf4f87ec4f0e66d1c74aa05228583225a07b590", + "url": "https://files.pythonhosted.org/packages/b3/c5/2accf5bbc145b890454d4eaf8dcd6423d406fc9f64147fd9020618363866/pydantic_core-2.14.6-cp310-cp310-musllinux_1_1_x86_64.whl" }, { "algorithm": "sha256", - "hash": "8c8a8812fe6f43a3a5b054af6ac2d7b8605c7bcab2804a8a7d68b53f3cd86e00", - "url": "https://files.pythonhosted.org/packages/a7/be/6be1245f78b72da970cf52cf4c55d8abcfd1655114d122ee6cf5641fc3f5/pydantic_core-2.14.5-pp39-pypy39_pp73-macosx_11_0_arm64.whl" + "hash": "7e90d6cc4aad2cc1f5e16ed56e46cebf4877c62403a311af20459c15da76fd91", + "url": "https://files.pythonhosted.org/packages/b7/53/101aac1d63a743284cdae804ceb6f561879c385f355caf20d2d87da6d36d/pydantic_core-2.14.6-cp312-cp312-musllinux_1_1_aarch64.whl" }, { "algorithm": "sha256", - "hash": "95b15e855ae44f0c6341ceb74df61b606e11f1087e87dcb7482377374aac6abe", - "url": "https://files.pythonhosted.org/packages/a9/2b/f1dca235271785f19e0f3696b31140d6a69ff5349970253c034f9c603b8e/pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + "hash": "8a14c192c1d724c3acbfb3f10a958c55a2638391319ce8078cb36c02283959b9", + "url": "https://files.pythonhosted.org/packages/ba/09/8078e77e73dda7df0d5cca8541d1fb731a52bc00188806676c3635c344a9/pydantic_core-2.14.6-cp39-cp39-musllinux_1_1_aarch64.whl" }, { "algorithm": "sha256", - "hash": "ab1cdb0f14dc161ebc268c09db04d2c9e6f70027f3b42446fa11c153521c0e88", - "url": "https://files.pythonhosted.org/packages/ab/43/77d8f56eb332e84097f18fc294346d214e9f0d22fb9ec67ebed4b8e90e35/pydantic_core-2.14.5-cp311-cp311-macosx_11_0_arm64.whl" + "hash": "2c5bcf3414367e29f83fd66f7de64509a8fd2368b1edf4351e862910727d3e51", + "url": "https://files.pythonhosted.org/packages/ba/98/fb42628ed811643c364e05353d3a015c74859402994420aeba8e3e34a54c/pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" }, { "algorithm": "sha256", - "hash": "77fa384d8e118b3077cccfcaf91bf83c31fe4dc850b5e6ee3dc14dc3d61bdba1", - "url": "https://files.pythonhosted.org/packages/ab/a6/e6e660299765ae03a55375935d5c6edc9d3e4798e63642f6c3030e15fddf/pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" + "hash": "0c08de15d50fa190d577e8591f0329a643eeaed696d7771760295998aca6bc66", + "url": "https://files.pythonhosted.org/packages/bb/32/a2f381c8ae08a9682d4e7943ba1f5b518e6f2bdd8261c23721691b332966/pydantic_core-2.14.6-cp310-cp310-musllinux_1_1_aarch64.whl" }, { "algorithm": "sha256", - "hash": "853a2295c00f1d4429db4c0fb9475958543ee80cfd310814b5c0ef502de24dda", - "url": "https://files.pythonhosted.org/packages/af/ab/79c2126e5504a3f0ecc0b1d97768594f9baa090134b0053309a2d938efaa/pydantic_core-2.14.5-cp311-cp311-musllinux_1_1_x86_64.whl" + "hash": "a874f21f87c485310944b2b2734cd6d318765bcbb7515eead33af9641816506e", + "url": "https://files.pythonhosted.org/packages/bc/7f/20ddc4eb15708cc6832c0cc2e398d0fa642aaf28d6ebcbcfb2d284ec6824/pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, { "algorithm": "sha256", - "hash": "3128e0bbc8c091ec4375a1828d6118bc20404883169ac95ffa8d983b293611e6", - "url": "https://files.pythonhosted.org/packages/b2/83/ae5698f7a8121599b239ea547f58f7b135e299e87cfe1a88fb1e6319d57c/pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" + "hash": "478e9e7b360dfec451daafe286998d4a1eeaecf6d69c427b834ae771cad4b622", + "url": "https://files.pythonhosted.org/packages/c1/7b/a1cfe9d3fdedf2b33d41960500c17ccba025b483720c79965b73d607687f/pydantic_core-2.14.6-cp311-cp311-musllinux_1_1_aarch64.whl" }, { "algorithm": "sha256", - "hash": "531f4b4252fac6ca476fbe0e6f60f16f5b65d3e6b583bc4d87645e4e5ddde331", - "url": "https://files.pythonhosted.org/packages/ba/95/d1104b88d5e3ad42db30935a4c258da2385139dd216ec8dfbc347a32dbff/pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + "hash": "d5f916acf8afbcab6bacbb376ba7dc61f845367901ecd5e328fc4d4aef2fcab0", + "url": "https://files.pythonhosted.org/packages/ce/95/d0bc7df3de0eaad08de467c50d1dc423839864f32e78da1cf57af3bbb2cc/pydantic_core-2.14.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl" }, { "algorithm": "sha256", - "hash": "4641e8ad4efb697f38a9b64ca0523b557c7931c5f84e0fd377a9a3b05121f0de", - "url": "https://files.pythonhosted.org/packages/ba/9b/5246600a17467ad8071174250d7727b34f5dc0dfe74abf3e99dbdf1beee1/pydantic_core-2.14.5-cp310-cp310-macosx_11_0_arm64.whl" + "hash": "ef633add81832f4b56d3b4c9408b43d530dfca29e68fb1b797dcb861a2c734cd", + "url": "https://files.pythonhosted.org/packages/d0/21/7ca5edf46bc6706152d459b560d669cfd72afe0dda24292408f1be8008d6/pydantic_core-2.14.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl" }, { "algorithm": "sha256", - "hash": "abf058be9517dc877227ec3223f0300034bd0e9f53aebd63cf4456c8cb1e0863", - "url": "https://files.pythonhosted.org/packages/bf/d2/4820db26970effb5d6fdee68f578585448b2eb6dd7344ab55b20958a0874/pydantic_core-2.14.5-cp39-cp39-macosx_11_0_arm64.whl" + "hash": "dab03ed811ed1c71d700ed08bde8431cf429bbe59e423394f0f4055f1ca0ea60", + "url": "https://files.pythonhosted.org/packages/d7/8a/d2c7668e15d3be9157e8328712db22568770640fdcc3a13f4ff0cdd87ee9/pydantic_core-2.14.6-cp38-cp38-musllinux_1_1_x86_64.whl" }, { "algorithm": "sha256", - "hash": "96581cfefa9123accc465a5fd0cc833ac4d75d55cc30b633b402e00e7ced00a6", - "url": "https://files.pythonhosted.org/packages/bf/ed/ee221482b51f368884ea6453f3784eeaeb17f5b737589d39d68a89bffde7/pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + "hash": "2dbe357bc4ddda078f79d2a36fc1dd0494a7f2fad83a0a684465b6f24b46fe80", + "url": "https://files.pythonhosted.org/packages/dd/3d/1a5936fc5558521e8aae22dfb7f0ae6b649040b5fcef7f25be1371d02752/pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, { "algorithm": "sha256", - "hash": "7e88f5696153dc516ba6e79f82cc4747e87027205f0e02390c21f7cb3bd8abfd", - "url": "https://files.pythonhosted.org/packages/c0/d2/b31c030802f29c35fa0c8ab92891bee9dcedd2793df560041b6d38f5fd49/pydantic_core-2.14.5-cp310-cp310-macosx_10_7_x86_64.whl" + "hash": "314ccc4264ce7d854941231cf71b592e30d8d368a71e50197c905874feacc8a8", + "url": "https://files.pythonhosted.org/packages/df/ea/435b1ad6890eec709e49dbcc5c0a72ca62ff8c6e62cfc45b7386e5e4cecc/pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" }, { "algorithm": "sha256", - "hash": "cf9d3fe53b1ee360e2421be95e62ca9b3296bf3f2fb2d3b83ca49ad3f925835e", - "url": "https://files.pythonhosted.org/packages/cb/96/27421976cde52555eb20636d59743621d4fa3bba278a0e4dbb4751e3f5c1/pydantic_core-2.14.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl" + "hash": "3a0b5db001b98e1c649dd55afa928e75aa4087e587b9524a4992316fa23c9fba", + "url": "https://files.pythonhosted.org/packages/e2/6d/789f2495c66c99a98b7a09a96145d5f3408941f839de7751995d9a5a8428/pydantic_core-2.14.6-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl" }, { "algorithm": "sha256", - "hash": "3387277f1bf659caf1724e1afe8ee7dbc9952a82d90f858ebb931880216ea955", - "url": "https://files.pythonhosted.org/packages/d2/d7/0f13f8cce749c4c5484ddfe60239bcce21a2a6cdcea250f13ae471cb86cb/pydantic_core-2.14.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl" + "hash": "b2602177668f89b38b9f84b7b3435d0a72511ddef45dc14446811759b82235a1", + "url": "https://files.pythonhosted.org/packages/e7/84/2dc88180fc6f0d13aab2a47a53b89c2dbc239e2a87d0a58e31077e111e82/pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, { "algorithm": "sha256", - "hash": "88e74ab0cdd84ad0614e2750f903bb0d610cc8af2cc17f72c28163acfcf372a4", - "url": "https://files.pythonhosted.org/packages/d4/bb/923eeeb3e87ba9024e311e0f3d1f0a4baad609ed7bfc7da7341e95981bd4/pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl" + "hash": "7e1f4744eea1501404b20b0ac059ff7e3f96a97d3e3f48ce27a139e053bb370b", + "url": "https://files.pythonhosted.org/packages/e8/5e/a30d56bb6b19e84bcde76cba2d6df45779f127ec73fa2e6d91f0ad3d4bc2/pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl" }, { "algorithm": "sha256", - "hash": "074f3d86f081ce61414d2dc44901f4f83617329c6f3ab49d2bc6c96948b2c26b", - "url": "https://files.pythonhosted.org/packages/e5/15/5ccdb37835f710819305024fb07512bf202da1a247b4ffdbdb82a6c34f7a/pydantic_core-2.14.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl" + "hash": "00b1087dabcee0b0ffd104f9f53d7d3eaddfaa314cdd6726143af6bc713aa27e", + "url": "https://files.pythonhosted.org/packages/f1/7b/0fd3444362f31c5f42b655c1ed734480433aa9f8bde97daa19cee0bc2844/pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl" }, { "algorithm": "sha256", - "hash": "038c9f763e650712b899f983076ce783175397c848da04985658e7628cbe873b", - "url": "https://files.pythonhosted.org/packages/e6/bc/e5cd49beafe7bf0f640bfd0a1b42e00b17b81ab072dea77c4a60cf986127/pydantic_core-2.14.5-pp38-pypy38_pp73-macosx_10_7_x86_64.whl" + "hash": "e4ace1e220b078c8e48e82c081e35002038657e4b37d403ce940fa679e57113b", + "url": "https://files.pythonhosted.org/packages/f3/62/076e6c43735950e911d80c6edf215314a8cf9b8adefe9613b72b09ccb1ee/pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, { "algorithm": "sha256", - "hash": "6e227c40c02fd873c2a73a98c1280c10315cbebe26734c196ef4514776120aeb", - "url": "https://files.pythonhosted.org/packages/eb/45/5eef8d36c2bf4c63e73e598fe523a0bc15069a97994481e27bef933ff423/pydantic_core-2.14.5-cp312-cp312-macosx_11_0_arm64.whl" + "hash": "92879bce89f91f4b2416eba4429c7b5ca22c45ef4a499c39f0c5c69257522c7c", + "url": "https://files.pythonhosted.org/packages/f3/7e/f1c1cf229bd404f5daf972345030f0c205424a326e67ae888c4a5a9066bd/pydantic_core-2.14.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl" }, { "algorithm": "sha256", - "hash": "40180930807ce806aa71eda5a5a5447abb6b6a3c0b4b3b1b1962651906484d68", - "url": "https://files.pythonhosted.org/packages/ed/b0/afd8f57e4ac5eaa4f1562b6f04cf10140cd6596c97d378aae2af6a236234/pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl" + "hash": "b9a9d92f10772d2a181b5ca339dee066ab7d1c9a34ae2421b2a52556e719756f", + "url": "https://files.pythonhosted.org/packages/f4/cd/252101e88458f4e7c4d2c44400050f92a0b13960ed3c489b513c97aaa7a6/pydantic_core-2.14.6-cp39-cp39-macosx_11_0_arm64.whl" }, { "algorithm": "sha256", - "hash": "3ad873900297bb36e4b6b3f7029d88ff9829ecdc15d5cf20161775ce12306f8a", - "url": "https://files.pythonhosted.org/packages/f2/a4/fcb082e0723f9e4fcdbc5564879255c7f6de1f3d4d6acdd1b8799a86aa97/pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl" + "hash": "78d0768ee59baa3de0f4adac9e3748b4b1fffc52143caebddfd5ea2961595277", + "url": "https://files.pythonhosted.org/packages/f9/84/c53d351f926630753b8dcf37ec2edf8b55a5a1724b3edc5104e06d3e54f1/pydantic_core-2.14.6-pp310-pypy310_pp73-macosx_11_0_arm64.whl" }, { "algorithm": "sha256", - "hash": "d37f8ec982ead9ba0a22a996129594938138a1503237b87318392a48882d50b7", - "url": "https://files.pythonhosted.org/packages/fb/84/f7e4556343ea0a483fa4e18505efaf10002581d2e980867a5b1ed22bfd21/pydantic_core-2.14.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl" + "hash": "ffff855100bc066ff2cd3aa4a60bc9534661816b110f0243e59503ec2df38421", + "url": "https://files.pythonhosted.org/packages/fb/17/3e4908cf8cb5a1d189f9dfa7cb5698d945e9a4db6b9138e3fef3c32c1f68/pydantic_core-2.14.6-cp311-cp311-macosx_11_0_arm64.whl" }, { "algorithm": "sha256", - "hash": "ef98ca7d5995a82f43ec0ab39c4caf6a9b994cb0b53648ff61716370eadc43cf", - "url": "https://files.pythonhosted.org/packages/fd/83/65e9db6549a01a369202fadac682c1a9f5ec57a637e672554ee50ef7f625/pydantic_core-2.14.5-cp38-cp38-macosx_10_7_x86_64.whl" + "hash": "e0641b506486f0b4cd1500a2a65740243e8670a2549bb02bc4556a83af84ae03", + "url": "https://files.pythonhosted.org/packages/fb/ff/812893fd262a98f0291f6afd87a530eb87c75ddc92034b938b8d15aa5ff4/pydantic_core-2.14.6-pp39-pypy39_pp73-macosx_11_0_arm64.whl" } ], "project_name": "pydantic-core", @@ -1644,7 +1695,7 @@ "typing-extensions!=4.7.0,>=4.6.0" ], "requires_python": ">=3.7", - "version": "2.14.5" + "version": "2.14.6" }, { "artifacts": [ @@ -1683,13 +1734,13 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac", - "url": "https://files.pythonhosted.org/packages/f3/8c/f16efd81ca8e293b2cc78f111190a79ee539d0d5d36ccd49975cb3beac60/pytest-7.4.3-py3-none-any.whl" + "hash": "b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8", + "url": "https://files.pythonhosted.org/packages/51/ff/f6e8b8f39e08547faece4bd80f89d5a8de68a38b2d179cc1c4490ffa3286/pytest-7.4.4-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5", - "url": "https://files.pythonhosted.org/packages/38/d4/174f020da50c5afe9f5963ad0fc5b56a4287e3586e3de5b3c8bce9c547b4/pytest-7.4.3.tar.gz" + "hash": "2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280", + "url": "https://files.pythonhosted.org/packages/80/1f/9d8e98e4133ffb16c90f3b405c43e38d3abb715bb5d7a63a5a684f7e46a3/pytest-7.4.4.tar.gz" } ], "project_name": "pytest", @@ -1712,7 +1763,7 @@ "xmlschema; extra == \"testing\"" ], "requires_python": ">=3.7", - "version": "7.4.3" + "version": "7.4.4" }, { "artifacts": [ @@ -1905,6 +1956,24 @@ "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7", "version": "1.16.0" }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384", + "url": "https://files.pythonhosted.org/packages/c3/a0/5dba8ed157b0136607c7f2151db695885606968d1fae123dc3391e0cfdbf/sniffio-1.3.0-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101", + "url": "https://files.pythonhosted.org/packages/cd/50/d49c388cae4ec10e8109b1b833fd265511840706808576df3ada99ecb0ac/sniffio-1.3.0.tar.gz" + } + ], + "project_name": "sniffio", + "requires_dists": [], + "requires_python": ">=3.7", + "version": "1.3.0" + }, { "artifacts": [ { @@ -1963,13 +2032,13 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "b32b9a86beffa876c0c3ac99a4cd3b8b51e973fb8e3bd4e0a6bb32c7efad80fc", - "url": "https://files.pythonhosted.org/packages/1d/d6/1281c1d7b03a127562d6644ebff081e85045f0025b1fe26dcdd82811ad1a/types_requests-2.31.0.10-py3-none-any.whl" + "hash": "2e2230c7bc8dd63fa3153c1c0ae335f8a368447f0582fc332f17d54f88e69027", + "url": "https://files.pythonhosted.org/packages/1b/23/126ffd0c885926fbd95eac1148093a4d87e9698a1f938be16d109e63ca64/types_requests-2.31.0.20231231-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "dc5852a76f1eaf60eafa81a2e50aefa3d1f015c34cf0cba130930866b1b22a92", - "url": "https://files.pythonhosted.org/packages/0d/a0/578870e05da99902bf6b75bc37f845cc359bb0278eb0d926c6f3f10bb869/types-requests-2.31.0.10.tar.gz" + "hash": "0f8c0c9764773384122813548d9eea92a5c4e1f33ed54556b508968ec5065cee", + "url": "https://files.pythonhosted.org/packages/e4/93/8ec4213d536465b0454bfc0fcab4aecfed91a1bdb4f232d2ab7f1d996040/types-requests-2.31.0.20231231.tar.gz" } ], "project_name": "types-requests", @@ -1977,25 +2046,25 @@ "urllib3>=2" ], "requires_python": ">=3.7", - "version": "2.31.0.10" + "version": "2.31.0.20231231" }, { "artifacts": [ { "algorithm": "sha256", - "hash": "8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0", - "url": "https://files.pythonhosted.org/packages/24/21/7d397a4b7934ff4028987914ac1044d3b7d52712f30e2ac7a2ae5bc86dd0/typing_extensions-4.8.0-py3-none-any.whl" + "hash": "af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd", + "url": "https://files.pythonhosted.org/packages/b7/f4/6a90020cd2d93349b442bfcb657d0dc91eee65491600b2cb1d388bc98e6b/typing_extensions-4.9.0-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef", - "url": "https://files.pythonhosted.org/packages/1f/7a/8b94bb016069caa12fc9f587b28080ac33b4fbb8ca369b98bc0a4828543e/typing_extensions-4.8.0.tar.gz" + "hash": "23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", + "url": "https://files.pythonhosted.org/packages/0c/1d/eb26f5e75100d531d7399ae800814b069bc2ed2a7410834d57374d010d96/typing_extensions-4.9.0.tar.gz" } ], "project_name": "typing-extensions", "requires_dists": [], "requires_python": ">=3.8", - "version": "4.8.0" + "version": "4.9.0" }, { "artifacts": [ @@ -2033,6 +2102,7 @@ "click~=8.0", "hypothesis<7,>=6", "mypy>=1.5.0", + "psycopg2~=2.9", "pydantic>=2.0", "pytest<8,>7", "pyyaml~=6.0", diff --git a/requirements.txt b/requirements.txt index 0697d7a..9051ef2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ azure-identity>=1.4.0,<2.0.0 +click~=8.0 +psycopg2~=2.9 pydantic>=2.0 pyyaml~=6.0 requests>=2,<3 -click~=8.0 From ffc79bb384743f39fd331c1e717073d15c64e72b Mon Sep 17 00:00:00 2001 From: Daniel Goldman Date: Sun, 31 Dec 2023 11:48:56 -0500 Subject: [PATCH 04/33] create tables in timescaledb --- llamazure/history/__main__.py | 9 ++++++++ llamazure/history/data.py | 41 +++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 llamazure/history/__main__.py diff --git a/llamazure/history/__main__.py b/llamazure/history/__main__.py new file mode 100644 index 0000000..3b63220 --- /dev/null +++ b/llamazure/history/__main__.py @@ -0,0 +1,9 @@ +import os + +from llamazure.history.data import TSDB, DB + +if __name__ == "__main__": + tsdb = TSDB(connstr=os.environ.get("connstr")) + db = DB(tsdb) + + db.create_tables() diff --git a/llamazure/history/data.py b/llamazure/history/data.py index e69de29..baf1826 100644 --- a/llamazure/history/data.py +++ b/llamazure/history/data.py @@ -0,0 +1,41 @@ +from textwrap import dedent +from typing import List, Optional, Tuple + +import psycopg2 + + +class TSDB: + """TimescaleDB connection""" + + def __init__(self, connstr: str): + self.connstr = connstr + + def exec(self, q, data: Optional[Tuple] = None): + """Execute a query""" + with psycopg2.connect(self.connstr) as conn: + cur = conn.cursor() + cur.execute(q, data) + conn.commit() + + def create_hypertable(self, name: str, time_col: str): + """Convert a table into a hypertable""" + self.exec(f"""SELECT create_hypertable('{name}', by_range('{time_col}'), if_not_exists => TRUE)""") + + +class DB: + def __init__(self, db: TSDB): + self.db = db + + def create_tables(self): + self.db.exec( + dedent( + """\ + CREATE TABLE IF NOT EXISTS res ( + time TIMESTAMPTZ NOT NULL, + rid VARCHAR, + body JSONB + ) + """ + ) + ) + self.db.create_hypertable("res", "time") From b9cfee9ccf60bfac4837070ca1cc172ca17f6c47 Mon Sep 17 00:00:00 2001 From: Daniel Goldman Date: Sun, 31 Dec 2023 12:36:57 -0500 Subject: [PATCH 05/33] load everything into a tresource --- llamazure/history/__main__.py | 30 ++++++++++++++++++++++++++++++ llamazure/tresource/readme.md | 18 ++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/llamazure/history/__main__.py b/llamazure/history/__main__.py index 3b63220..e8a2d85 100644 --- a/llamazure/history/__main__.py +++ b/llamazure/history/__main__.py @@ -1,9 +1,39 @@ +import json import os +from dataclasses import is_dataclass, asdict +from azure.identity import DefaultAzureCredential + +from llamazure.azgraph.azgraph import Graph from llamazure.history.data import TSDB, DB +from llamazure.rid import mp +from llamazure.tresource.mp import TresourceMPData, MPData + + +class DataclassEncoder(json.JSONEncoder): + def default(self, obj): + if is_dataclass(obj): + return asdict(obj) + return super().default(obj) + + +def reformat(resources): + for r in resources: + path, azobj = mp.parse(r["id"]) + mpdata = MPData(azobj, r) + yield path, mpdata + if __name__ == "__main__": tsdb = TSDB(connstr=os.environ.get("connstr")) db = DB(tsdb) db.create_tables() + + g = Graph.from_credential(DefaultAzureCredential()) + resources = g.q("Resources") + + tree = TresourceMPData() + tree.add_many(reformat(resources)) + + print(json.dumps(tree, cls=DataclassEncoder, indent=2)) diff --git a/llamazure/tresource/readme.md b/llamazure/tresource/readme.md index ef1387f..0433eca 100644 --- a/llamazure/tresource/readme.md +++ b/llamazure/tresource/readme.md @@ -11,4 +11,22 @@ There are several variants of Tresources. ## Examples +Load all resources into a tresource, indexable by rid, including data: + +```python +from azure.identity import DefaultAzureCredential + +from llamazure.azgraph.azgraph import Graph +from llamazure.rid import mp +from llamazure.tresource.mp import TresourceMPData + +g = Graph.from_credential(DefaultAzureCredential()) +resources = g.q("Resources") + +t = TresourceMPData() + +for resource in resources: + t.set_data(mp.parse(resource["id"])[1], resource) +``` + ## Design notes From 8985090ec351f221ac2d5616329ce30dcfe5a15d Mon Sep 17 00:00:00 2001 From: Daniel Goldman Date: Sun, 31 Dec 2023 13:02:46 -0500 Subject: [PATCH 06/33] load everything into the DB --- llamazure/history/__main__.py | 10 ++++++++-- llamazure/history/data.py | 12 ++++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/llamazure/history/__main__.py b/llamazure/history/__main__.py index e8a2d85..5daa41e 100644 --- a/llamazure/history/__main__.py +++ b/llamazure/history/__main__.py @@ -1,6 +1,7 @@ import json import os from dataclasses import is_dataclass, asdict +import datetime from azure.identity import DefaultAzureCredential @@ -27,7 +28,6 @@ def reformat(resources): if __name__ == "__main__": tsdb = TSDB(connstr=os.environ.get("connstr")) db = DB(tsdb) - db.create_tables() g = Graph.from_credential(DefaultAzureCredential()) @@ -36,4 +36,10 @@ def reformat(resources): tree = TresourceMPData() tree.add_many(reformat(resources)) - print(json.dumps(tree, cls=DataclassEncoder, indent=2)) + snapshot_time = datetime.datetime.utcnow() + + for path, mpdata in tree.resources.items(): + db.insert_resource(snapshot_time, path, mpdata.data) + + + # print(json.dumps(tree, cls=DataclassEncoder, indent=2)) diff --git a/llamazure/history/data.py b/llamazure/history/data.py index baf1826..f7ba92b 100644 --- a/llamazure/history/data.py +++ b/llamazure/history/data.py @@ -1,7 +1,11 @@ from textwrap import dedent -from typing import List, Optional, Tuple +from typing import Optional, Tuple import psycopg2 +import psycopg2.extras +import psycopg2.extensions + +psycopg2.extensions.register_adapter(dict, psycopg2.extras.Json) class TSDB: @@ -33,9 +37,13 @@ def create_tables(self): CREATE TABLE IF NOT EXISTS res ( time TIMESTAMPTZ NOT NULL, rid VARCHAR, - body JSONB + data JSONB ) """ ) ) self.db.create_hypertable("res", "time") + + def insert_resource(self, time, rid, data): + """Insert a resource into the DB""" + self.db.exec("""INSERT INTO res (time, rid, data) values (%s, %s, %s)""", (time, rid, data),) From 21f1c228f8a777c3d47140183518149570994af2 Mon Sep 17 00:00:00 2001 From: Daniel Goldman Date: Sun, 31 Dec 2023 18:52:40 -0500 Subject: [PATCH 07/33] insert resources with reference to the snapshot that captured them --- llamazure/history/__main__.py | 4 +--- llamazure/history/data.py | 40 +++++++++++++++++++++++++++++------ 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/llamazure/history/__main__.py b/llamazure/history/__main__.py index 5daa41e..4f70782 100644 --- a/llamazure/history/__main__.py +++ b/llamazure/history/__main__.py @@ -38,8 +38,6 @@ def reformat(resources): snapshot_time = datetime.datetime.utcnow() - for path, mpdata in tree.resources.items(): - db.insert_resource(snapshot_time, path, mpdata.data) - + db.insert_snapshot(snapshot_time, ((path, mpdata.data) for path, mpdata in tree.resources.items())) # print(json.dumps(tree, cls=DataclassEncoder, indent=2)) diff --git a/llamazure/history/data.py b/llamazure/history/data.py index f7ba92b..f76ef5b 100644 --- a/llamazure/history/data.py +++ b/llamazure/history/data.py @@ -1,5 +1,6 @@ +import datetime from textwrap import dedent -from typing import Optional, Tuple +from typing import Optional, Tuple, Iterable, Any import psycopg2 import psycopg2.extras @@ -21,6 +22,15 @@ def exec(self, q, data: Optional[Tuple] = None): cur.execute(q, data) conn.commit() + def exec_returning(self, q, data: Optional[Tuple] = None) -> Any: + """Execute a query""" + with psycopg2.connect(self.connstr) as conn: + cur = conn.cursor() + cur.execute(q, data) + res = cur.fetchone()[0] + conn.commit() + return res + def create_hypertable(self, name: str, time_col: str): """Convert a table into a hypertable""" self.exec(f"""SELECT create_hypertable('{name}', by_range('{time_col}'), if_not_exists => TRUE)""") @@ -31,19 +41,37 @@ def __init__(self, db: TSDB): self.db = db def create_tables(self): + self.db.exec( + dedent( + """\ + CREATE TABLE IF NOT EXISTS snapshot ( + id SERIAL PRIMARY KEY, + time TIMESTAMPTZ NOT NULL + ) + """ + ) + ) + self.db.exec( dedent( """\ CREATE TABLE IF NOT EXISTS res ( - time TIMESTAMPTZ NOT NULL, - rid VARCHAR, - data JSONB + time TIMESTAMPTZ NOT NULL, + snapshot INTEGER, + rid VARCHAR, + data JSONB, + FOREIGN KEY (snapshot) REFERENCES snapshot (id) ) """ ) ) self.db.create_hypertable("res", "time") - def insert_resource(self, time, rid, data): + def insert_resource(self, time: datetime.datetime, snapshot_id, rid: str, data: dict): """Insert a resource into the DB""" - self.db.exec("""INSERT INTO res (time, rid, data) values (%s, %s, %s)""", (time, rid, data),) + self.db.exec("""INSERT INTO res (time, snapshot, rid, data) VALUES (%s, %s, %s, %s)""", (time, snapshot_id, rid, data),) + + def insert_snapshot(self, time: datetime.datetime, resources: Iterable[Tuple[str, dict]]): + snapshot_id = self.db.exec_returning("""INSERT INTO snapshot (time) VALUES (%s) RETURNING id""", (time,)) + for rid, data in resources: + self.insert_resource(time, snapshot_id, rid, data) From a55f16b2f888142cdf81299557a093c913dceb36 Mon Sep 17 00:00:00 2001 From: Daniel Goldman Date: Sun, 31 Dec 2023 19:37:42 -0500 Subject: [PATCH 08/33] read data from latest snapshot --- llamazure/history/__main__.py | 6 ++++-- llamazure/history/data.py | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/llamazure/history/__main__.py b/llamazure/history/__main__.py index 4f70782..1df84bb 100644 --- a/llamazure/history/__main__.py +++ b/llamazure/history/__main__.py @@ -11,10 +11,12 @@ from llamazure.tresource.mp import TresourceMPData, MPData -class DataclassEncoder(json.JSONEncoder): +class MyEncoder(json.JSONEncoder): def default(self, obj): if is_dataclass(obj): return asdict(obj) + if isinstance(obj, datetime.datetime): + return obj.isoformat() return super().default(obj) @@ -40,4 +42,4 @@ def reformat(resources): db.insert_snapshot(snapshot_time, ((path, mpdata.data) for path, mpdata in tree.resources.items())) - # print(json.dumps(tree, cls=DataclassEncoder, indent=2)) + print(json.dumps(db.read_at(datetime.datetime.utcnow()), indent=2, cls=MyEncoder)) diff --git a/llamazure/history/data.py b/llamazure/history/data.py index f76ef5b..acb47a9 100644 --- a/llamazure/history/data.py +++ b/llamazure/history/data.py @@ -21,6 +21,7 @@ def exec(self, q, data: Optional[Tuple] = None): cur = conn.cursor() cur.execute(q, data) conn.commit() + return cur def exec_returning(self, q, data: Optional[Tuple] = None) -> Any: """Execute a query""" @@ -75,3 +76,16 @@ def insert_snapshot(self, time: datetime.datetime, resources: Iterable[Tuple[str snapshot_id = self.db.exec_returning("""INSERT INTO snapshot (time) VALUES (%s) RETURNING id""", (time,)) for rid, data in resources: self.insert_resource(time, snapshot_id, rid, data) + + def read_at(self, time: datetime.datetime): + res = self.db.exec( + dedent( + """\ + WITH LatestSnapshot AS ( + SELECT id FROM snapshot WHERE time < %s ORDER BY time DESC LIMIT 1 + ) + SELECT * FROM res WHERE snapshot = (SELECT id FROM LatestSnapshot); + """), + (time,) + ).fetchall() + return res From e741cc0d700c62e08e9d0bbcf325dddc01388a5c Mon Sep 17 00:00:00 2001 From: Daniel Goldman Date: Sun, 31 Dec 2023 19:54:23 -0500 Subject: [PATCH 09/33] allow inserting deltas and reading latest state --- llamazure/history/__main__.py | 6 +++++- llamazure/history/data.py | 8 +++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/llamazure/history/__main__.py b/llamazure/history/__main__.py index 1df84bb..3fa22cd 100644 --- a/llamazure/history/__main__.py +++ b/llamazure/history/__main__.py @@ -42,4 +42,8 @@ def reformat(resources): db.insert_snapshot(snapshot_time, ((path, mpdata.data) for path, mpdata in tree.resources.items())) - print(json.dumps(db.read_at(datetime.datetime.utcnow()), indent=2, cls=MyEncoder)) + delta = g.q("Resources | take(1)")[0] + db.insert_delta(snapshot_time + datetime.timedelta(seconds=1), delta["id"].lower(), delta) + + print(json.dumps(db.read_latest(), indent=2, cls=MyEncoder)) + # print(json.dumps(db.read_snapshot(snapshot_time + datetime.timedelta(seconds=2)), indent=2, cls=MyEncoder)) diff --git a/llamazure/history/data.py b/llamazure/history/data.py index acb47a9..6bf2af4 100644 --- a/llamazure/history/data.py +++ b/llamazure/history/data.py @@ -77,7 +77,10 @@ def insert_snapshot(self, time: datetime.datetime, resources: Iterable[Tuple[str for rid, data in resources: self.insert_resource(time, snapshot_id, rid, data) - def read_at(self, time: datetime.datetime): + def insert_delta(self, time: datetime.datetime, rid: str, data: dict): + return self.insert_resource(time, None, rid, data) + + def read_snapshot(self, time: datetime.datetime): res = self.db.exec( dedent( """\ @@ -89,3 +92,6 @@ def read_at(self, time: datetime.datetime): (time,) ).fetchall() return res + + def read_latest(self): + return self.db.exec("""SELECT DISTINCT ON (rid) * FROM res ORDER BY rid, time DESC;""").fetchall() From 2c2da2815424a67ef8da12c6824723a606ed556d Mon Sep 17 00:00:00 2001 From: Daniel Goldman Date: Sun, 31 Dec 2023 19:59:42 -0500 Subject: [PATCH 10/33] read at a point in time --- llamazure/history/__main__.py | 8 ++++---- llamazure/history/data.py | 22 +++++++++++++++++----- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/llamazure/history/__main__.py b/llamazure/history/__main__.py index 3fa22cd..5e6bbad 100644 --- a/llamazure/history/__main__.py +++ b/llamazure/history/__main__.py @@ -1,14 +1,14 @@ +import datetime import json import os -from dataclasses import is_dataclass, asdict -import datetime +from dataclasses import asdict, is_dataclass from azure.identity import DefaultAzureCredential from llamazure.azgraph.azgraph import Graph -from llamazure.history.data import TSDB, DB +from llamazure.history.data import DB, TSDB from llamazure.rid import mp -from llamazure.tresource.mp import TresourceMPData, MPData +from llamazure.tresource.mp import MPData, TresourceMPData class MyEncoder(json.JSONEncoder): diff --git a/llamazure/history/data.py b/llamazure/history/data.py index 6bf2af4..ab843a5 100644 --- a/llamazure/history/data.py +++ b/llamazure/history/data.py @@ -1,10 +1,10 @@ import datetime from textwrap import dedent -from typing import Optional, Tuple, Iterable, Any +from typing import Any, Iterable, Optional, Tuple import psycopg2 -import psycopg2.extras import psycopg2.extensions +import psycopg2.extras psycopg2.extensions.register_adapter(dict, psycopg2.extras.Json) @@ -70,17 +70,23 @@ def create_tables(self): def insert_resource(self, time: datetime.datetime, snapshot_id, rid: str, data: dict): """Insert a resource into the DB""" - self.db.exec("""INSERT INTO res (time, snapshot, rid, data) VALUES (%s, %s, %s, %s)""", (time, snapshot_id, rid, data),) + self.db.exec( + """INSERT INTO res (time, snapshot, rid, data) VALUES (%s, %s, %s, %s)""", + (time, snapshot_id, rid, data), + ) def insert_snapshot(self, time: datetime.datetime, resources: Iterable[Tuple[str, dict]]): + """Insert a complete snapshot into the DB""" snapshot_id = self.db.exec_returning("""INSERT INTO snapshot (time) VALUES (%s) RETURNING id""", (time,)) for rid, data in resources: self.insert_resource(time, snapshot_id, rid, data) def insert_delta(self, time: datetime.datetime, rid: str, data: dict): + """Insert a single delta into the DB""" return self.insert_resource(time, None, rid, data) def read_snapshot(self, time: datetime.datetime): + """Read a complete snapshot. Does not include any deltas""" res = self.db.exec( dedent( """\ @@ -88,10 +94,16 @@ def read_snapshot(self, time: datetime.datetime): SELECT id FROM snapshot WHERE time < %s ORDER BY time DESC LIMIT 1 ) SELECT * FROM res WHERE snapshot = (SELECT id FROM LatestSnapshot); - """), - (time,) + """ + ), + (time,), ).fetchall() return res def read_latest(self): + """Read the latest information for all resources. Includes deltas.""" return self.db.exec("""SELECT DISTINCT ON (rid) * FROM res ORDER BY rid, time DESC;""").fetchall() + + def read_at(self, time: datetime.datetime): + """Read the information for all resources at a point in time. Includes deltas.""" + return self.db.exec("""SELECT DISTINCT ON (rid) * FROM res WHERE time < %s ORDER BY rid, time DESC;""", (time,)).fetchall() From c31c270fb01c37f3a8af1dc0d1cb706e03bcc53d Mon Sep 17 00:00:00 2001 From: Daniel Goldman Date: Sun, 31 Dec 2023 20:26:19 -0500 Subject: [PATCH 11/33] upgrade to psycopg3 --- cicd/python-default.lock | 74 +++++++++++++++++++++++++++++++---- llamazure/history/__main__.py | 19 +++++---- llamazure/history/data.py | 13 +++--- requirements.txt | 2 +- 4 files changed, 84 insertions(+), 24 deletions(-) diff --git a/cicd/python-default.lock b/cicd/python-default.lock index c296e6a..78f0408 100644 --- a/cicd/python-default.lock +++ b/cicd/python-default.lock @@ -13,7 +13,7 @@ // "click~=8.0", // "hypothesis<7,>=6", // "mypy>=1.5.0", -// "psycopg2~=2.9", +// "psycopg~=3.1", // "pydantic>=2.0", // "pytest<8,>7", // "pyyaml~=6.0", @@ -182,6 +182,37 @@ "requires_python": ">=3.7", "version": "1.15.0" }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9", + "url": "https://files.pythonhosted.org/packages/1a/ab/3e941e3fcf1b7d3ab3d0233194d99d6a0ed6b24f8f956fc81e47edc8c079/backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987", + "url": "https://files.pythonhosted.org/packages/4a/6d/eca004eeadcbf8bd64cc96feb9e355536147f0577420b44d80c7cac70767/backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2", + "url": "https://files.pythonhosted.org/packages/ad/85/475e514c3140937cf435954f78dedea1861aeab7662d11de232bdaa90655/backports.zoneinfo-0.2.1.tar.gz" + }, + { + "algorithm": "sha256", + "hash": "e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1", + "url": "https://files.pythonhosted.org/packages/c1/8f/9b1b920a6a95652463143943fa3b8c000cb0b932ab463764a6f2a2416560/backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl" + } + ], + "project_name": "backports-zoneinfo", + "requires_dists": [ + "importlib-resources; python_version < \"3.7\"", + "tzdata; extra == \"tzdata\"" + ], + "requires_python": ">=3.6", + "version": "0.2.1" + }, { "artifacts": [ { @@ -1276,14 +1307,43 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "d1454bde93fb1e224166811694d600e746430c006fbb031ea06ecc2ea41bf156", - "url": "https://files.pythonhosted.org/packages/c9/5e/dc6acaf46d78979d6b03458b7a1618a68e152a6776fce95daac5e0f0301b/psycopg2-2.9.9.tar.gz" + "hash": "0bfe9741f4fb1c8115cadd8fe832fa91ac277e81e0652ff7fa1400f0ef0f59ba", + "url": "https://files.pythonhosted.org/packages/e1/5c/94460062d3c1036bc9cbcfe12698f48bde31fcfb4f1fbce6c05effd4a5ec/psycopg-3.1.16-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "a34d922fd7df3134595e71c3428ba6f1bd5f4968db74857fe95de12db2d6b763", + "url": "https://files.pythonhosted.org/packages/3b/19/5c75740c48f77eb07e4e799c4f4fdec8918724ab610c03e9ae95005dca80/psycopg-3.1.16.tar.gz" } ], - "project_name": "psycopg2", - "requires_dists": [], + "project_name": "psycopg", + "requires_dists": [ + "Sphinx>=5.0; extra == \"docs\"", + "anyio<4.0,>=3.6.2; extra == \"test\"", + "backports.zoneinfo>=0.2.0; python_version < \"3.9\"", + "black>=23.1.0; extra == \"dev\"", + "codespell>=2.2; extra == \"dev\"", + "dnspython>=2.1; extra == \"dev\"", + "flake8>=4.0; extra == \"dev\"", + "furo==2022.6.21; extra == \"docs\"", + "mypy>=1.4.1; extra == \"dev\"", + "mypy>=1.4.1; extra == \"test\"", + "pproxy>=2.7; extra == \"test\"", + "psycopg-binary==3.1.16; implementation_name != \"pypy\" and extra == \"binary\"", + "psycopg-c==3.1.16; implementation_name != \"pypy\" and extra == \"c\"", + "psycopg-pool; extra == \"pool\"", + "pytest-cov>=3.0; extra == \"test\"", + "pytest-randomly>=3.5; extra == \"test\"", + "pytest>=6.2.5; extra == \"test\"", + "sphinx-autobuild>=2021.3.14; extra == \"docs\"", + "sphinx-autodoc-typehints>=1.12; extra == \"docs\"", + "types-setuptools>=57.4; extra == \"dev\"", + "typing-extensions>=4.1", + "tzdata; sys_platform == \"win32\"", + "wheel>=0.37; extra == \"dev\"" + ], "requires_python": ">=3.7", - "version": "2.9.9" + "version": "3.1.16" }, { "artifacts": [ @@ -2102,7 +2162,7 @@ "click~=8.0", "hypothesis<7,>=6", "mypy>=1.5.0", - "psycopg2~=2.9", + "psycopg~=3.1", "pydantic>=2.0", "pytest<8,>7", "pyyaml~=6.0", diff --git a/llamazure/history/__main__.py b/llamazure/history/__main__.py index 5e6bbad..4020365 100644 --- a/llamazure/history/__main__.py +++ b/llamazure/history/__main__.py @@ -12,15 +12,18 @@ class MyEncoder(json.JSONEncoder): - def default(self, obj): - if is_dataclass(obj): - return asdict(obj) - if isinstance(obj, datetime.datetime): - return obj.isoformat() - return super().default(obj) + """Encoder for more types""" + def default(self, o): + if is_dataclass(o): + return asdict(o) + if isinstance(o, datetime.datetime): + return o.isoformat() + return super().default(o) -def reformat(resources): + +def reformat_resources_for_tresource(resources): + """Reformat mp_resources for TresourceMPData""" for r in resources: path, azobj = mp.parse(r["id"]) mpdata = MPData(azobj, r) @@ -36,7 +39,7 @@ def reformat(resources): resources = g.q("Resources") tree = TresourceMPData() - tree.add_many(reformat(resources)) + tree.add_many(reformat_resources_for_tresource(resources)) snapshot_time = datetime.datetime.utcnow() diff --git a/llamazure/history/data.py b/llamazure/history/data.py index ab843a5..916a1f7 100644 --- a/llamazure/history/data.py +++ b/llamazure/history/data.py @@ -2,11 +2,8 @@ from textwrap import dedent from typing import Any, Iterable, Optional, Tuple -import psycopg2 -import psycopg2.extensions -import psycopg2.extras - -psycopg2.extensions.register_adapter(dict, psycopg2.extras.Json) +import psycopg +from psycopg.types.json import Jsonb class TSDB: @@ -17,7 +14,7 @@ def __init__(self, connstr: str): def exec(self, q, data: Optional[Tuple] = None): """Execute a query""" - with psycopg2.connect(self.connstr) as conn: + with psycopg.connect(self.connstr) as conn: cur = conn.cursor() cur.execute(q, data) conn.commit() @@ -25,7 +22,7 @@ def exec(self, q, data: Optional[Tuple] = None): def exec_returning(self, q, data: Optional[Tuple] = None) -> Any: """Execute a query""" - with psycopg2.connect(self.connstr) as conn: + with psycopg.connect(self.connstr) as conn: cur = conn.cursor() cur.execute(q, data) res = cur.fetchone()[0] @@ -72,7 +69,7 @@ def insert_resource(self, time: datetime.datetime, snapshot_id, rid: str, data: """Insert a resource into the DB""" self.db.exec( """INSERT INTO res (time, snapshot, rid, data) VALUES (%s, %s, %s, %s)""", - (time, snapshot_id, rid, data), + (time, snapshot_id, rid, Jsonb(data)), ) def insert_snapshot(self, time: datetime.datetime, resources: Iterable[Tuple[str, dict]]): diff --git a/requirements.txt b/requirements.txt index 9051ef2..a189cec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ azure-identity>=1.4.0,<2.0.0 click~=8.0 -psycopg2~=2.9 +psycopg~=3.1 pydantic>=2.0 pyyaml~=6.0 requests>=2,<3 From c520a0818a8128541be7c977fdb01e2f6762dc4e Mon Sep 17 00:00:00 2001 From: Daniel Goldman Date: Sun, 31 Dec 2023 20:45:27 -0500 Subject: [PATCH 12/33] lints and typehints --- llamazure/history/__main__.py | 16 ++++++++++++---- llamazure/history/data.py | 8 +++++++- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/llamazure/history/__main__.py b/llamazure/history/__main__.py index 4020365..d13bb40 100644 --- a/llamazure/history/__main__.py +++ b/llamazure/history/__main__.py @@ -1,11 +1,14 @@ +"""Demo for the `llamazure.history` application""" import datetime import json import os from dataclasses import asdict, is_dataclass +from typing import Dict, cast from azure.identity import DefaultAzureCredential from llamazure.azgraph.azgraph import Graph +from llamazure.azgraph.models import ResErr from llamazure.history.data import DB, TSDB from llamazure.rid import mp from llamazure.tresource.mp import MPData, TresourceMPData @@ -31,21 +34,26 @@ def reformat_resources_for_tresource(resources): if __name__ == "__main__": - tsdb = TSDB(connstr=os.environ.get("connstr")) + tsdb = TSDB(connstr=os.environ["connstr"]) db = DB(tsdb) db.create_tables() g = Graph.from_credential(DefaultAzureCredential()) resources = g.q("Resources") + if isinstance(resources, ResErr): + raise RuntimeError(ResErr) - tree = TresourceMPData() + tree: TresourceMPData[Dict] = TresourceMPData() tree.add_many(reformat_resources_for_tresource(resources)) snapshot_time = datetime.datetime.utcnow() - db.insert_snapshot(snapshot_time, ((path, mpdata.data) for path, mpdata in tree.resources.items())) + db.insert_snapshot(snapshot_time, ((cast(str, path), mpdata.data) for path, mpdata in tree.resources.items() if mpdata.data is not None)) - delta = g.q("Resources | take(1)")[0] + delta_q = g.q("Resources | take(1)") + if isinstance(delta_q, ResErr): + raise RuntimeError(ResErr) + delta = delta_q[0] db.insert_delta(snapshot_time + datetime.timedelta(seconds=1), delta["id"].lower(), delta) print(json.dumps(db.read_latest(), indent=2, cls=MyEncoder)) diff --git a/llamazure/history/data.py b/llamazure/history/data.py index 916a1f7..8d9e2d7 100644 --- a/llamazure/history/data.py +++ b/llamazure/history/data.py @@ -1,3 +1,4 @@ +"""Interface with the TimescaleDB""" import datetime from textwrap import dedent from typing import Any, Iterable, Optional, Tuple @@ -25,7 +26,9 @@ def exec_returning(self, q, data: Optional[Tuple] = None) -> Any: with psycopg.connect(self.connstr) as conn: cur = conn.cursor() cur.execute(q, data) - res = cur.fetchone()[0] + res = cur.fetchone() + if res is not None: + res = res[0] conn.commit() return res @@ -35,10 +38,13 @@ def create_hypertable(self, name: str, time_col: str): class DB: + """Store, load, and create tables""" + def __init__(self, db: TSDB): self.db = db def create_tables(self): + """Create the tables in TimescaleDB""" self.db.exec( dedent( """\ From f615519acef2f25e96b533a74c4627a3bf425c2e Mon Sep 17 00:00:00 2001 From: Daniel Goldman Date: Sun, 31 Dec 2023 23:24:22 -0500 Subject: [PATCH 13/33] make multitenant by storing the azure tenant ID --- llamazure/history/__main__.py | 18 +++++++++++++++--- llamazure/history/data.py | 29 ++++++++++++++++------------- 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/llamazure/history/__main__.py b/llamazure/history/__main__.py index d13bb40..f9986b8 100644 --- a/llamazure/history/__main__.py +++ b/llamazure/history/__main__.py @@ -4,11 +4,15 @@ import os from dataclasses import asdict, is_dataclass from typing import Dict, cast +from uuid import UUID from azure.identity import DefaultAzureCredential from llamazure.azgraph.azgraph import Graph from llamazure.azgraph.models import ResErr +from llamazure.azrest.azrest import AzRest +from llamazure.azrest.models import AzList +from llamazure.azrest.models import Req as AzReq from llamazure.history.data import DB, TSDB from llamazure.rid import mp from llamazure.tresource.mp import MPData, TresourceMPData @@ -22,6 +26,8 @@ def default(self, o): return asdict(o) if isinstance(o, datetime.datetime): return o.isoformat() + if isinstance(o, UUID): + return str(o) return super().default(o) @@ -38,7 +44,13 @@ def reformat_resources_for_tresource(resources): db = DB(tsdb) db.create_tables() - g = Graph.from_credential(DefaultAzureCredential()) + credential = DefaultAzureCredential() + g = Graph.from_credential(credential) + azr = AzRest.from_credential(credential) + + tenants = azr.call(AzReq.get("GetTenants", "/tenants", "2022-12-01", AzList[dict])) + tenant_id = UUID(tenants[0]["tenantId"]) + resources = g.q("Resources") if isinstance(resources, ResErr): raise RuntimeError(ResErr) @@ -48,13 +60,13 @@ def reformat_resources_for_tresource(resources): snapshot_time = datetime.datetime.utcnow() - db.insert_snapshot(snapshot_time, ((cast(str, path), mpdata.data) for path, mpdata in tree.resources.items() if mpdata.data is not None)) + db.insert_snapshot(snapshot_time, tenant_id, ((cast(str, path), mpdata.data) for path, mpdata in tree.resources.items() if mpdata.data is not None)) delta_q = g.q("Resources | take(1)") if isinstance(delta_q, ResErr): raise RuntimeError(ResErr) delta = delta_q[0] - db.insert_delta(snapshot_time + datetime.timedelta(seconds=1), delta["id"].lower(), delta) + db.insert_delta(snapshot_time + datetime.timedelta(seconds=1), tenant_id, delta["id"].lower(), delta) print(json.dumps(db.read_latest(), indent=2, cls=MyEncoder)) # print(json.dumps(db.read_snapshot(snapshot_time + datetime.timedelta(seconds=2)), indent=2, cls=MyEncoder)) diff --git a/llamazure/history/data.py b/llamazure/history/data.py index 8d9e2d7..dec2076 100644 --- a/llamazure/history/data.py +++ b/llamazure/history/data.py @@ -2,6 +2,7 @@ import datetime from textwrap import dedent from typing import Any, Iterable, Optional, Tuple +from uuid import UUID import psycopg from psycopg.types.json import Jsonb @@ -49,8 +50,9 @@ def create_tables(self): dedent( """\ CREATE TABLE IF NOT EXISTS snapshot ( - id SERIAL PRIMARY KEY, - time TIMESTAMPTZ NOT NULL + id SERIAL PRIMARY KEY, + time TIMESTAMPTZ NOT NULL, + azure_tenant UUID ) """ ) @@ -61,9 +63,10 @@ def create_tables(self): """\ CREATE TABLE IF NOT EXISTS res ( time TIMESTAMPTZ NOT NULL, - snapshot INTEGER, - rid VARCHAR, - data JSONB, + snapshot INTEGER, + azure_tenant UUID, + rid VARCHAR, + data JSONB, FOREIGN KEY (snapshot) REFERENCES snapshot (id) ) """ @@ -71,22 +74,22 @@ def create_tables(self): ) self.db.create_hypertable("res", "time") - def insert_resource(self, time: datetime.datetime, snapshot_id, rid: str, data: dict): + def insert_resource(self, time: datetime.datetime, azure_tenant: UUID, snapshot_id, rid: str, data: dict): """Insert a resource into the DB""" self.db.exec( - """INSERT INTO res (time, snapshot, rid, data) VALUES (%s, %s, %s, %s)""", - (time, snapshot_id, rid, Jsonb(data)), + """INSERT INTO res (time, snapshot, azure_tenant, rid, data) VALUES (%s, %s, %s, %s, %s)""", + (time, snapshot_id, azure_tenant, rid, Jsonb(data)), ) - def insert_snapshot(self, time: datetime.datetime, resources: Iterable[Tuple[str, dict]]): + def insert_snapshot(self, time: datetime.datetime, azure_tenant: UUID, resources: Iterable[Tuple[str, dict]]): """Insert a complete snapshot into the DB""" - snapshot_id = self.db.exec_returning("""INSERT INTO snapshot (time) VALUES (%s) RETURNING id""", (time,)) + snapshot_id = self.db.exec_returning("""INSERT INTO snapshot (time, azure_tenant) VALUES (%s, %s) RETURNING id""", (time, azure_tenant)) for rid, data in resources: - self.insert_resource(time, snapshot_id, rid, data) + self.insert_resource(time, azure_tenant, snapshot_id, rid, data) - def insert_delta(self, time: datetime.datetime, rid: str, data: dict): + def insert_delta(self, time: datetime.datetime, azure_tenant: UUID, rid: str, data: dict): """Insert a single delta into the DB""" - return self.insert_resource(time, None, rid, data) + return self.insert_resource(time, azure_tenant, None, rid, data) def read_snapshot(self, time: datetime.datetime): """Read a complete snapshot. Does not include any deltas""" From 227475fca1b69033149126821a3f81adac999f1f Mon Sep 17 00:00:00 2001 From: Daniel Goldman Date: Fri, 29 Mar 2024 02:39:56 -0400 Subject: [PATCH 14/33] add testcontainer for integration tests --- cicd/python-default.lock | 1025 +++++++++++++++++++++------------ llamazure/history/conftest.py | 84 +++ pants.toml | 2 +- requirements_test.txt | 1 + 4 files changed, 728 insertions(+), 384 deletions(-) create mode 100644 llamazure/history/conftest.py diff --git a/cicd/python-default.lock b/cicd/python-default.lock index 78f0408..97ba095 100644 --- a/cicd/python-default.lock +++ b/cicd/python-default.lock @@ -18,6 +18,7 @@ // "pytest<8,>7", // "pyyaml~=6.0", // "requests<3,>=2", +// "testcontainers<5,>=3", // "types-PyYAML", // "types-requests" // ], @@ -57,43 +58,6 @@ "requires_python": ">=3.8", "version": "0.6.0" }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "745843b39e829e108e518c489b31dc757de7d2131d53fac32bd8df268227bfee", - "url": "https://files.pythonhosted.org/packages/bf/cd/d6d9bb1dadf73e7af02d18225cbd2c93f8552e13130484f1c8dcfece292b/anyio-4.2.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "e1875bb4b4e2de1669f4bc7869b6d3f54231cdced71605e6e64c9be77e3be50f", - "url": "https://files.pythonhosted.org/packages/2d/b8/7333d87d5f03247215d86a86362fd3e324111788c6cdd8d2e6196a6ba833/anyio-4.2.0.tar.gz" - } - ], - "project_name": "anyio", - "requires_dists": [ - "Sphinx>=7; extra == \"doc\"", - "anyio[trio]; extra == \"test\"", - "coverage[toml]>=7; extra == \"test\"", - "exceptiongroup>=1.0.2; python_version < \"3.11\"", - "exceptiongroup>=1.2.0; extra == \"test\"", - "hypothesis>=4.0; extra == \"test\"", - "idna>=2.8", - "packaging; extra == \"doc\"", - "psutil>=5.9; extra == \"test\"", - "pytest-mock>=3.6.1; extra == \"test\"", - "pytest>=7.0; extra == \"test\"", - "sniffio>=1.1", - "sphinx-autodoc-typehints>=1.2.0; extra == \"doc\"", - "sphinx-rtd-theme; extra == \"doc\"", - "trio>=0.23; extra == \"trio\"", - "trustme; extra == \"test\"", - "typing-extensions>=4.1; python_version < \"3.11\"", - "uvloop>=0.17; (platform_python_implementation == \"CPython\" and platform_system != \"Windows\") and extra == \"test\"" - ], - "requires_python": ">=3.8", - "version": "4.2.0" - }, { "artifacts": [ { @@ -139,25 +103,24 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "604a005bce6a49ba661bb7b2be84a9b169047e52fcfcd0a4e4770affab4178f7", - "url": "https://files.pythonhosted.org/packages/b0/e2/b6cdd23d8d9cc430410cc309879883aff67736c02528cd1fdc07c48158b1/azure_core-1.29.6-py3-none-any.whl" + "hash": "7c5ee397e48f281ec4dd773d67a0a47a0962ed6fa833036057f9ea067f688e74", + "url": "https://files.pythonhosted.org/packages/d7/70/180df3b43ebc7a1ec957d9e5c2c76e6c54398ec61a67dff88d3e0131be80/azure_core-1.30.1-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "13b485252ecd9384ae624894fe51cfa6220966207264c360beada239f88b738a", - "url": "https://files.pythonhosted.org/packages/ad/78/a1aeb8f80306101112810263e74ec81a99cdd50ecca1f03819716c1aedb3/azure-core-1.29.6.tar.gz" + "hash": "26273a254131f84269e8ea4464f3560c731f29c0c1f69ac99010845f239c1a8f", + "url": "https://files.pythonhosted.org/packages/51/0d/b76383f028aa3570419edf97ab504cb84b839e3cbc8c8b2048f16bbea2d3/azure-core-1.30.1.tar.gz" } ], "project_name": "azure-core", "requires_dists": [ "aiohttp>=3.0; extra == \"aio\"", - "anyio<5.0,>=3.0", "requests>=2.21.0", "six>=1.11.0", "typing-extensions>=4.6.0" ], "requires_python": ">=3.7", - "version": "1.29.6" + "version": "1.30.1" }, { "artifacts": [ @@ -217,19 +180,19 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474", - "url": "https://files.pythonhosted.org/packages/64/62/428ef076be88fa93716b576e4a01f919d25968913e817077a386fcbe4f42/certifi-2023.11.17-py3-none-any.whl" + "hash": "dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1", + "url": "https://files.pythonhosted.org/packages/ba/06/a07f096c664aeb9f01624f858c3add0a4e913d6c96257acb4fce61e7de14/certifi-2024.2.2-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1", - "url": "https://files.pythonhosted.org/packages/d4/91/c89518dd4fe1f3a4e3f6ab7ff23cb00ef2e8c9adf99dacc618ad5e068e28/certifi-2023.11.17.tar.gz" + "hash": "0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f", + "url": "https://files.pythonhosted.org/packages/71/da/e94e26401b62acd6d91df2b52954aceb7f561743aa5ccc32152886c76c96/certifi-2024.2.2.tar.gz" } ], "project_name": "certifi", "requires_dists": [], "requires_python": ">=3.6", - "version": "2023.11.17" + "version": "2024.2.2" }, { "artifacts": [ @@ -819,102 +782,143 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "c5ca78485a255e03c32b513f8c2bc39fedb7f5c5f8535545bdc223a03b24f248", - "url": "https://files.pythonhosted.org/packages/79/68/9767a3fb985515d3c34221c3671043cda57b1f691046ad8aae355fb2a8a5/cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl" + "hash": "f12764b8fffc7a123f641d7d049d382b73f96a34117e0b637b80643169cec8ac", + "url": "https://files.pythonhosted.org/packages/50/be/92ce909d5d5b361780e21e0216502f72e5d8f9b2d73bcfde1ca5f791630b/cryptography-42.0.5-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "7367d7b2eca6513681127ebad53b2582911d1736dc2ffc19f2c3ae49997496bc", + "url": "https://files.pythonhosted.org/packages/0e/1d/62a2324882c0db89f64358dadfb95cae024ee3ba9fde3d5fd4d2f58af9f5/cryptography-42.0.5-cp39-abi3-manylinux_2_28_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "6fe07eec95dfd477eb9530aef5bead34fec819b3aaf6c5bd6d20565da607bfe1", + "url": "https://files.pythonhosted.org/packages/13/9e/a55763a32d340d7b06d045753c186b690e7d88780cafce5f88cb931536be/cryptography-42.0.5.tar.gz" + }, + { + "algorithm": "sha256", + "hash": "2bce03af1ce5a5567ab89bd90d11e7bbdff56b8af3acbbec1faded8f44cb06da", + "url": "https://files.pythonhosted.org/packages/2c/9c/821ef6144daf80360cf6093520bf07eec7c793103ed4b1bf3fa17d2b55d8/cryptography-42.0.5-cp37-abi3-musllinux_1_2_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "e807b3188f9eb0eaa7bbb579b462c5ace579f1cedb28107ce8b48a9f7ad3679e", + "url": "https://files.pythonhosted.org/packages/3f/ae/61d7c256bd8285263cdb5c9ebebcf66261bd0765ed255a074dc8d5304362/cryptography-42.0.5-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "cd2030f6650c089aeb304cf093f3244d34745ce0cfcc39f20c6fbfe030102e2a", + "url": "https://files.pythonhosted.org/packages/48/c8/c0962598c43d3cff2c9d6ac66d0c612bdfb1975be8d87b8889960cf8c81d/cryptography-42.0.5-cp39-abi3-manylinux_2_28_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "e4985a790f921508f36f81831817cbc03b102d643b5fcb81cd33df3fa291a1a1", + "url": "https://files.pythonhosted.org/packages/50/26/248cd8b6809635ed412159791c0d3869d8ec9dfdc57d428d500a14d425b7/cryptography-42.0.5-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "ba334e6e4b1d92442b75ddacc615c5476d4ad55cc29b15d590cc6b86efa487e2", + "url": "https://files.pythonhosted.org/packages/59/48/519ecd6b65dc9ea7c8111dfde7c9ed61aeb90fe59c6b4454900bcd3e3286/cryptography-42.0.5-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "b8cac287fafc4ad485b8a9b67d0ee80c66bf3574f655d3b97ef2e1082360faf1", + "url": "https://files.pythonhosted.org/packages/5b/3d/c3c21e3afaf43bacccc3ebf61d1a0d47cef6e2607dbba01662f6f9d8fc40/cryptography-42.0.5-cp37-abi3-musllinux_1_1_x86_64.whl" }, { "algorithm": "sha256", - "hash": "c7f3201ec47d5207841402594f1d7950879ef890c0c495052fa62f58283fde1a", - "url": "https://files.pythonhosted.org/packages/0d/bf/e7a1382034c4feaa77b35147138ff2bc8ae47a2fa7e2838fcdd41d2d0f2e/cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl" + "hash": "f8837fe1d6ac4a8052a9a8ddab256bc006242696f03368a4009be7ee3075cdb7", + "url": "https://files.pythonhosted.org/packages/64/f7/d3c83c79947cc6807e6acd3b2d9a1cbd312042777bc7eec50c869913df79/cryptography-42.0.5-cp37-abi3-manylinux_2_28_x86_64.whl" }, { "algorithm": "sha256", - "hash": "841df4caa01008bad253bce2a6f7b47f86dc9f08df4b433c404def869f590a15", - "url": "https://files.pythonhosted.org/packages/14/fd/dd5bd6ab0d12476ebca579cbfd48d31bd90fa28fa257b209df585dcf62a0/cryptography-41.0.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + "hash": "a2913c5375154b6ef2e91c10b5720ea6e21007412f6437504ffea2109b5a33d7", + "url": "https://files.pythonhosted.org/packages/69/f6/630eb71f246208103ffee754b8375b6b334eeedb28620b3ae57be815eeeb/cryptography-42.0.5-cp39-abi3-musllinux_1_1_aarch64.whl" }, { "algorithm": "sha256", - "hash": "079b85658ea2f59c4f43b70f8119a52414cdb7be34da5d019a77bf96d473b960", - "url": "https://files.pythonhosted.org/packages/26/ab/59f271c8f027b8068bbf4dfd6e3ad4c6fc20df0b108ee29c64a1036ba4ce/cryptography-41.0.7-pp310-pypy310_pp73-macosx_10_12_x86_64.whl" + "hash": "5e6275c09d2badf57aea3afa80d975444f4be8d3bc58f7f80d2a484c6f9485c8", + "url": "https://files.pythonhosted.org/packages/6d/4d/f7c14c7a49e35df829e04d451a57b843208be7442c8e087250c195775be1/cryptography-42.0.5-cp39-abi3-macosx_10_12_universal2.whl" }, { "algorithm": "sha256", - "hash": "7a698cb1dac82c35fcf8fe3417a3aaba97de16a01ac914b89a0889d364d2f6be", - "url": "https://files.pythonhosted.org/packages/2c/5d/f9ae5e819dcd2618b2d3e671b22c26b5db1da30414af399e4f624b3fe6cd/cryptography-41.0.7-pp38-pypy38_pp73-macosx_10_12_x86_64.whl" + "hash": "ba3e4a42397c25b7ff88cdec6e2a16c2be18720f317506ee25210f6d31925f9c", + "url": "https://files.pythonhosted.org/packages/6e/8d/6cce88bdeb26b4ec14b23ab9f0c2c7c0bf33ef4904bfa952c5db1749fd37/cryptography-42.0.5-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl" }, { "algorithm": "sha256", - "hash": "68a2dec79deebc5d26d617bfdf6e8aab065a4f34934b22d3b5010df3ba36612c", - "url": "https://files.pythonhosted.org/packages/3c/8f/9f5f4d9c00f030e81eda69e689c9777fe665bf34045cec2fd5e71b4859b6/cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl" + "hash": "c41fb5e6a5fe9ebcd58ca3abfeb51dffb5d83d6775405305bfa8715b76521922", + "url": "https://files.pythonhosted.org/packages/7d/bc/b6c691c960b5dcd54c5444e73af7f826e62af965ba59b6d7e9928b6489a2/cryptography-42.0.5-cp39-abi3-musllinux_1_1_x86_64.whl" }, { "algorithm": "sha256", - "hash": "5429ec739a29df2e29e15d082f1d9ad683701f0ec7709ca479b3ff2708dae65a", - "url": "https://files.pythonhosted.org/packages/3e/81/ae2c51ea2b80d57d5756a12df67816230124faea0a762a7a6304fe3c819c/cryptography-41.0.7-cp37-abi3-manylinux_2_28_aarch64.whl" + "hash": "b03c2ae5d2f0fc05f9a2c0c997e1bc18c8229f392234e8a0194f202169ccd278", + "url": "https://files.pythonhosted.org/packages/8c/50/9185cca136596448d9cc595ae22a9bd4412ad35d812550c37c1390d54673/cryptography-42.0.5-cp37-abi3-manylinux_2_28_aarch64.whl" }, { "algorithm": "sha256", - "hash": "b640981bf64a3e978a56167594a0e97db71c89a479da8e175d8bb5be5178c003", - "url": "https://files.pythonhosted.org/packages/54/f4/3eec29ab2fdd673de8f44af3876b32248eb79550ced822363e35da1644d1/cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl" + "hash": "9481ffe3cf013b71b2428b905c4f7a9a4f76ec03065b05ff499bb5682a8d9ad8", + "url": "https://files.pythonhosted.org/packages/9f/c3/3d2d9bb2ff9e15b5ababc370ae85b377eacc8e3d54fcb03225471e41a1d8/cryptography-42.0.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl" }, { "algorithm": "sha256", - "hash": "43f2552a2378b44869fe8827aa19e69512e3245a219104438692385b0ee119d1", - "url": "https://files.pythonhosted.org/packages/62/bd/69628ab50368b1beb900eb1de5c46f8137169b75b2458affe95f2f470501/cryptography-41.0.7-cp37-abi3-manylinux_2_28_x86_64.whl" + "hash": "3eaafe47ec0d0ffcc9349e1708be2aaea4c6dd4978d76bf6eb0cb2c13636c6fc", + "url": "https://files.pythonhosted.org/packages/c2/40/c7cb9d6819b90640ffc3c4028b28f46edc525feaeaa0d98ea23e843d446d/cryptography-42.0.5-cp39-abi3-musllinux_1_2_aarch64.whl" }, { "algorithm": "sha256", - "hash": "5a1b41bc97f1ad230a41657d9155113c7521953869ae57ac39ac7f1bb471469a", - "url": "https://files.pythonhosted.org/packages/68/bb/475658ea92653a894589e657d6cea9ae01354db73405d62126ac5e74e2f8/cryptography-41.0.7-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + "hash": "1b95b98b0d2af784078fa69f637135e3c317091b615cd0905f8b8a087e86fa30", + "url": "https://files.pythonhosted.org/packages/ca/2e/9f2c49bd6a18d46c05ec098b040e7d4599c61f50ced40a39adfae3f68306/cryptography-42.0.5-cp39-abi3-musllinux_1_2_x86_64.whl" }, { "algorithm": "sha256", - "hash": "928258ba5d6f8ae644e764d0f996d61a8777559f72dfeb2eea7e2fe0ad6e782d", - "url": "https://files.pythonhosted.org/packages/a9/76/d705397d076fcbf5671544eb72a70b5a5ac83462d23dbd2a365a3bf3692a/cryptography-41.0.7-cp37-abi3-macosx_10_12_x86_64.whl" + "hash": "a30596bae9403a342c978fb47d9b0ee277699fa53bbafad14706af51fe543d16", + "url": "https://files.pythonhosted.org/packages/d1/f1/fd98e6e79242d9aeaf6a5d49639a7e85f05741575af14d3f4a1d477f572e/cryptography-42.0.5-cp37-abi3-macosx_10_12_universal2.whl" }, { "algorithm": "sha256", - "hash": "af03b32695b24d85a75d40e1ba39ffe7db7ffcb099fe507b39fd41a565f1b157", - "url": "https://files.pythonhosted.org/packages/b6/4a/1808333c5ea79cb6d51102036cbcf698704b1fc7a5ccd139957aeadd2311/cryptography-41.0.7-cp37-abi3-musllinux_1_1_aarch64.whl" + "hash": "7cde5f38e614f55e28d831754e8a3bacf9ace5d1566235e39d91b35502d6936e", + "url": "https://files.pythonhosted.org/packages/d4/fa/057f9d7a5364c86ccb6a4bd4e5c58920dcb66532be0cc21da3f9c7617ec3/cryptography-42.0.5-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, { "algorithm": "sha256", - "hash": "48a0476626da912a44cc078f9893f292f0b3e4c739caf289268168d8f4702a39", - "url": "https://files.pythonhosted.org/packages/b9/19/75d3e8b9b814c09eef76899fea542473273311ab9bfaa1ca4e22c112e660/cryptography-41.0.7-pp39-pypy39_pp73-macosx_10_12_x86_64.whl" + "hash": "16a48c23a62a2f4a285699dba2e4ff2d1cff3115b9df052cdd976a18856d8e3d", + "url": "https://files.pythonhosted.org/packages/d8/b1/127ecb373d02db85a7a7de5093d7ac7b7714b8907d631f0591e8f002998d/cryptography-42.0.5-cp37-abi3-musllinux_1_2_aarch64.whl" }, { "algorithm": "sha256", - "hash": "49f0805fc0b2ac8d4882dd52f4a3b935b210935d500b6b805f321addc8177406", - "url": "https://files.pythonhosted.org/packages/c5/07/826d66b6b03c5bfde8b451bea22c41e68d60aafff0ffa02c5f0819844319/cryptography-41.0.7-cp37-abi3-musllinux_1_1_x86_64.whl" + "hash": "b7ffe927ee6531c78f81aa17e684e2ff617daeba7f189f911065b2ea2d526dec", + "url": "https://files.pythonhosted.org/packages/d9/f9/27dda069a9f9bfda7c75305e222d904cc2445acf5eab5c696ade57d36f1b/cryptography-42.0.5-cp37-abi3-macosx_10_12_x86_64.whl" }, { "algorithm": "sha256", - "hash": "37a138589b12069efb424220bf78eac59ca68b95696fc622b6ccc1c0a197204a", - "url": "https://files.pythonhosted.org/packages/c9/46/c488acbc62aedb14c1082c31bd5062caf8881530e97d2443ce33589f2053/cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl" + "hash": "2424ff4c4ac7f6b8177b53c17ed5d8fa74ae5955656867f5a8affaca36a27abb", + "url": "https://files.pythonhosted.org/packages/e2/59/61b2364f2a4d3668d933531bc30d012b9b2de1e534df4805678471287d57/cryptography-42.0.5-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" }, { "algorithm": "sha256", - "hash": "13f93ce9bea8016c253b34afc6bd6a75993e5c40672ed5405a9c832f0d4a00bc", - "url": "https://files.pythonhosted.org/packages/ce/b3/13a12ea7edb068de0f62bac88a8ffd92cc2901881b391839851846b84a81/cryptography-41.0.7.tar.gz" + "hash": "0270572b8bd2c833c3981724b8ee9747b3ec96f699a9665470018594301439ee", + "url": "https://files.pythonhosted.org/packages/e5/61/67e090a41c70ee526bd5121b1ccabab85c727574332d03326baaedea962d/cryptography-42.0.5-cp37-abi3-musllinux_1_1_aarch64.whl" }, { "algorithm": "sha256", - "hash": "3c78451b78313fa81607fa1b3f1ae0a5ddd8014c38a02d9db0616133987b9cdf", - "url": "https://files.pythonhosted.org/packages/e4/73/5461318abd2fe426855a2f66775c063bbefd377729ece3c3ee048ddf19a5/cryptography-41.0.7-cp37-abi3-macosx_10_12_universal2.whl" + "hash": "cd65d75953847815962c84a4654a84850b2bb4aed3f26fadcc1c13892e1e29f6", + "url": "https://files.pythonhosted.org/packages/ea/fa/b0cd7f1cd011b52196e01195581119d5e2b802a35e21f08f342d6640aaae/cryptography-42.0.5-pp39-pypy39_pp73-macosx_10_12_x86_64.whl" }, { "algorithm": "sha256", - "hash": "e3114da6d7f95d2dee7d3f4eec16dacff819740bbab931aff8648cb13c5ff5e7", - "url": "https://files.pythonhosted.org/packages/f4/2b/96fd47dff43cdeb3e24e962fec9ed6ffa1369320146eb95524088265fa00/cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl" + "hash": "329906dcc7b20ff3cad13c069a78124ed8247adcac44b10bea1130e36caae0b4", + "url": "https://files.pythonhosted.org/packages/fb/0b/14509319a1b49858425553d2fb3808579cfdfe98c1d71a3f046c1b4e0108/cryptography-42.0.5-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" } ], "project_name": "cryptography", "requires_dists": [ "bcrypt>=3.1.5; extra == \"ssh\"", - "black; extra == \"pep8test\"", "build; extra == \"sdist\"", - "cffi>=1.12", + "certifi; extra == \"test\"", + "cffi>=1.12; platform_python_implementation != \"PyPy\"", "check-sdist; extra == \"pep8test\"", + "click; extra == \"pep8test\"", "mypy; extra == \"pep8test\"", "nox; extra == \"nox\"", "pretend; extra == \"test\"", @@ -924,14 +928,59 @@ "pytest-randomly; extra == \"test-randomorder\"", "pytest-xdist; extra == \"test\"", "pytest>=6.2.0; extra == \"test\"", + "readme-renderer; extra == \"docstest\"", "ruff; extra == \"pep8test\"", "sphinx-rtd-theme>=1.1.1; extra == \"docs\"", "sphinx>=5.3.0; extra == \"docs\"", - "sphinxcontrib-spelling>=4.0.1; extra == \"docstest\"", - "twine>=1.12.0; extra == \"docstest\"" + "sphinxcontrib-spelling>=4.0.1; extra == \"docstest\"" ], "requires_python": ">=3.7", - "version": "41.0.7" + "version": "42.0.5" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a", + "url": "https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff", + "url": "https://files.pythonhosted.org/packages/5a/d3/8ae2869247df154b64c1884d7346d412fed0c49df84db635aab2d1c40e62/deprecation-2.1.0.tar.gz" + } + ], + "project_name": "deprecation", + "requires_dists": [ + "packaging" + ], + "requires_python": null, + "version": "2.1.0" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "12ba681f2777a0ad28ffbcc846a69c31b4dfd9752b47eb425a274ee269c5e14b", + "url": "https://files.pythonhosted.org/packages/18/bd/9706c10bb12e05043ef138dc8d412cfd17f29c8df0fb28ad71c96a98785d/docker-7.0.0-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "323736fb92cd9418fc5e7133bc953e11a9da04f4483f828b527db553f1e7e5a3", + "url": "https://files.pythonhosted.org/packages/25/14/7d40f8f64ceca63c741ee5b5611ead4fb8d3bcaf3e6ab57d2ab0f01712bc/docker-7.0.0.tar.gz" + } + ], + "project_name": "docker", + "requires_dists": [ + "packaging>=14.0", + "paramiko>=2.4.3; extra == \"ssh\"", + "pywin32>=304; sys_platform == \"win32\"", + "requests>=2.26.0", + "urllib3>=1.26.0", + "websocket-client>=1.3.0; extra == \"websockets\"" + ], + "requires_python": ">=3.8", + "version": "7.0.0" }, { "artifacts": [ @@ -957,13 +1006,13 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "d335044492acb03fa1fdb4edacb81cca2e578049fc7306345bc0e8947fef15a9", - "url": "https://files.pythonhosted.org/packages/f7/08/0c2cc8eaebd65b660817bd39747d1c50c21a623eb27fb71ba035e1a2a386/hypothesis-6.92.2-py3-none-any.whl" + "hash": "b538df1d22365df84f94c38fb2d9c41a222373594c2a910cc8f4ddc68240a62f", + "url": "https://files.pythonhosted.org/packages/f7/5c/02b1360a6fd4b31cdea03422af6873d0d5293ea0af3215656c580df76640/hypothesis-6.99.13-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "841f89a486c43bdab55698de8929bd2635639ec20bf6ce98ccd75622d7ee6d41", - "url": "https://files.pythonhosted.org/packages/39/28/e957e56aaaabb7b16586230a814a9ecc007a234baad784b3f7376f7744c5/hypothesis-6.92.2.tar.gz" + "hash": "e425e8a3f1912e44f62ff3e2768dca19c79f46d43ec70fa56e96e2d7194ccd2d", + "url": "https://files.pythonhosted.org/packages/66/c6/8896a3dbe1732b29f419d3442df9fe920cf912aa05e399bf460ddfc6fa5c/hypothesis-6.99.13.tar.gz" } ], "project_name": "hypothesis", @@ -976,11 +1025,15 @@ "black>=19.10b0; extra == \"ghostwriter\"", "click>=7.0; extra == \"all\"", "click>=7.0; extra == \"cli\"", + "crosshair-tool>=0.0.53; extra == \"all\"", + "crosshair-tool>=0.0.53; extra == \"crosshair\"", "django>=3.2; extra == \"all\"", "django>=3.2; extra == \"django\"", "dpcontracts>=0.4; extra == \"all\"", "dpcontracts>=0.4; extra == \"dpcontracts\"", "exceptiongroup>=1.0.0; python_version < \"3.11\"", + "hypothesis-crosshair>=0.0.2; extra == \"all\"", + "hypothesis-crosshair>=0.0.2; extra == \"crosshair\"", "lark>=0.10.1; extra == \"all\"", "lark>=0.10.1; extra == \"lark\"", "libcst>=0.3.16; extra == \"all\"", @@ -1000,11 +1053,11 @@ "rich>=9.0.0; extra == \"all\"", "rich>=9.0.0; extra == \"cli\"", "sortedcontainers<3.0.0,>=2.1.0", - "tzdata>=2023.3; sys_platform == \"win32\" and extra == \"all\"", - "tzdata>=2023.3; sys_platform == \"win32\" and extra == \"zoneinfo\"" + "tzdata>=2024.1; (sys_platform == \"win32\" or sys_platform == \"emscripten\") and extra == \"all\"", + "tzdata>=2024.1; (sys_platform == \"win32\" or sys_platform == \"emscripten\") and extra == \"zoneinfo\"" ], "requires_python": ">=3.8", - "version": "6.92.2" + "version": "6.99.13" }, { "artifacts": [ @@ -1046,25 +1099,24 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "be77ba6a8f49c9ff598bbcdc5dfcf1c9842f3044300109af738e8c3e371065b5", - "url": "https://files.pythonhosted.org/packages/b7/61/2756b963e84db6946e4b93a8e288595106286fc11c7129fcb869267ead67/msal-1.26.0-py2.py3-none-any.whl" + "hash": "3064f80221a21cd535ad8c3fafbb3a3582cd9c7e9af0bb789ae14f726a0ca99b", + "url": "https://files.pythonhosted.org/packages/40/41/646c00154efa437bf01b30444421285fb29ef624e86b2446e71eff50b7a9/msal-1.28.0-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "224756079fe338be838737682b49f8ebc20a87c1c5eeaf590daae4532b83de15", - "url": "https://files.pythonhosted.org/packages/bb/45/c4dfbe24dd546d141287fa26476ce3206d461d8e4a24be77c84b835e647d/msal-1.26.0.tar.gz" + "hash": "80bbabe34567cb734efd2ec1869b2d98195c927455369d8077b3c542088c5c9d", + "url": "https://files.pythonhosted.org/packages/b3/2a/76e64e6a5f0d3d12f4b3ab2cfc8b5e4fcb6982d15213aad9c8e6b20ebeae/msal-1.28.0.tar.gz" } ], "project_name": "msal", "requires_dists": [ "PyJWT[crypto]<3,>=1.0.0", - "cryptography<44,>=0.6", - "mock; python_version < \"3.3\"", - "pymsalruntime<0.14,>=0.13.2; (python_version >= \"3.6\" and platform_system == \"Windows\") and extra == \"broker\"", + "cryptography<45,>=0.6", + "pymsalruntime<0.15,>=0.13.2; (python_version >= \"3.6\" and platform_system == \"Windows\") and extra == \"broker\"", "requests<3,>=2.0.0" ], - "requires_python": ">=2.7", - "version": "1.26.0" + "requires_python": ">=3.7", + "version": "1.28.0" }, { "artifacts": [ @@ -1093,113 +1145,113 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d", - "url": "https://files.pythonhosted.org/packages/3a/e3/b582bff8e2fc7056a8a00ec06d2ac3509fc9595af9954099ed70e0418ac3/mypy-1.8.0-py3-none-any.whl" + "hash": "a260627a570559181a9ea5de61ac6297aa5af202f06fd7ab093ce74e7181e43e", + "url": "https://files.pythonhosted.org/packages/60/db/0ba2eaedca52bf5276275e8489951c26206030b3d31bf06f00875ae75d5d/mypy-1.9.0-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66", - "url": "https://files.pythonhosted.org/packages/04/8a/1b8c19dd00eb21ad3170762202e4cb82de7c4af0fbd4a4fb7524606858ba/mypy-1.8.0-cp38-cp38-macosx_11_0_arm64.whl" + "hash": "f8a67616990062232ee4c3952f41c779afac41405806042a8126fe96e098419f", + "url": "https://files.pythonhosted.org/packages/1a/a7/0b180ef81daebabd6ef011f12ecd1ba4c0747aa8c460a8caf79f38789b90/mypy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl" }, { "algorithm": "sha256", - "hash": "52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd", - "url": "https://files.pythonhosted.org/packages/08/24/83d9e62ab2031593e94438fdbfd2c32996f4d818be26d2dc33be6870a3a0/mypy-1.8.0-cp312-cp312-macosx_10_9_x86_64.whl" + "hash": "190da1ee69b427d7efa8aa0d5e5ccd67a4fb04038c380237a0d96829cb157913", + "url": "https://files.pythonhosted.org/packages/1c/1b/3e962a201d2f0f57c9fa1990e0dd6076f4f2f94954ab56e4a701ec3cc070/mypy-1.9.0-cp312-cp312-musllinux_1_1_x86_64.whl" }, { "algorithm": "sha256", - "hash": "df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4", - "url": "https://files.pythonhosted.org/packages/08/d1/a9c12c6890c789fd965ade8b37bef1989f649e87c62fde3df658dff394fc/mypy-1.8.0-cp310-cp310-macosx_11_0_arm64.whl" + "hash": "e49499be624dead83927e70c756970a0bc8240e9f769389cdf5714b0784ca6bf", + "url": "https://files.pythonhosted.org/packages/36/35/b5ca875ce1a1aa724916ea4bcb7cc0bb53fda2f135915426f8ea077c1984/mypy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl" }, { "algorithm": "sha256", - "hash": "6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07", - "url": "https://files.pythonhosted.org/packages/16/22/25fac51008f0a4b2186da0dba3039128bd75d3fab8c07acd3ea5894f95cc/mypy-1.8.0.tar.gz" + "hash": "b685154e22e4e9199fc95f298661deea28aaede5ae16ccc8cbb1045e716b3e04", + "url": "https://files.pythonhosted.org/packages/3c/95/352a56a3fb8373bde12c2f2a55fbdd643644033ac0294184c63ade3ab97b/mypy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl" }, { "algorithm": "sha256", - "hash": "d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817", - "url": "https://files.pythonhosted.org/packages/19/c6/256f253cb3fc6b30b93a9836cf3c816a3ec09f934f7b567f693e5666d14f/mypy-1.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + "hash": "48533cdd345c3c2e5ef48ba3b0d3880b257b423e7995dada04248725c6f77374", + "url": "https://files.pythonhosted.org/packages/3d/23/b4282a2b59b74a3bf4a16713491348f72d843e218a73a12399bc98754c48/mypy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl" }, { "algorithm": "sha256", - "hash": "42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3", - "url": "https://files.pythonhosted.org/packages/33/14/902484951fa662ee6e044087a50dab4b16b534920dda2eea9380ce2e7b2d/mypy-1.8.0-cp312-cp312-musllinux_1_1_x86_64.whl" + "hash": "f88566144752999351725ac623471661c9d1cd8caa0134ff98cceeea181789f4", + "url": "https://files.pythonhosted.org/packages/61/e9/d18add3d83a363fb890944c95de9bf7ac89dceb265edb2304a50099866ee/mypy-1.9.0-cp39-cp39-macosx_11_0_arm64.whl" }, { "algorithm": "sha256", - "hash": "ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592", - "url": "https://files.pythonhosted.org/packages/41/6b/25e22dfc730bf698be85600339edefd5d07efe7436cce765631c170a9c31/mypy-1.8.0-cp39-cp39-musllinux_1_1_x86_64.whl" + "hash": "653265f9a2784db65bfca694d1edd23093ce49740b2244cde583aeb134c008f3", + "url": "https://files.pythonhosted.org/packages/6d/ce/c62c0c0d83b8a936ad6d5e0294e956e881acc5d680deb4929ea259fb50f6/mypy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl" }, { "algorithm": "sha256", - "hash": "7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6", - "url": "https://files.pythonhosted.org/packages/54/46/4681859453851b40e1c135ba589cde1fce915177c8f213e2aaeb57e1f209/mypy-1.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + "hash": "aceb1db093b04db5cd390821464504111b8ec3e351eb85afd1433490163d60cd", + "url": "https://files.pythonhosted.org/packages/6e/96/40f0f605b1d4e2ad1fb11d21988ce3a3e205886c0fcbd35c9789a214de9a/mypy-1.9.0-cp312-cp312-macosx_10_9_x86_64.whl" }, { "algorithm": "sha256", - "hash": "9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d", - "url": "https://files.pythonhosted.org/packages/66/19/e0c9373258f3e84e1e24af357e5663e6b0058bb5c307287e9d1a473a9687/mypy-1.8.0-cp311-cp311-musllinux_1_1_x86_64.whl" + "hash": "3cc5da0127e6a478cddd906068496a97a7618a21ce9b54bde5bf7e539c7af974", + "url": "https://files.pythonhosted.org/packages/72/1e/a587a862c766a755a58b62d8c00aed11b74a15dc415c1bf5da7b607b0efd/mypy-1.9.0.tar.gz" }, { "algorithm": "sha256", - "hash": "4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259", - "url": "https://files.pythonhosted.org/packages/6a/86/e37ae331e2ec831619db70db4e32e9635dc669db940318c297cf248832d8/mypy-1.8.0-cp39-cp39-macosx_11_0_arm64.whl" + "hash": "d4d5ddc13421ba3e2e082a6c2d74c2ddb3979c39b582dacd53dd5d9431237185", + "url": "https://files.pythonhosted.org/packages/85/a5/b7dc7eb69eda899fd07e71403b51b598a1f4df0f452d1da5844374082bcd/mypy-1.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, { "algorithm": "sha256", - "hash": "485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3", - "url": "https://files.pythonhosted.org/packages/6d/6c/c33a5d50776a769be7ed7ca6709003c99aecd43913b9d82914bc72f154d8/mypy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl" + "hash": "2418488264eb41f69cc64a69a745fad4a8f86649af4b1041a4c64ee61fc61129", + "url": "https://files.pythonhosted.org/packages/a1/81/97e8539d6cdcfb3a8ae7eb1438c6983a9fc434ef9664572bfa7fd285cab9/mypy-1.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, { "algorithm": "sha256", - "hash": "f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55", - "url": "https://files.pythonhosted.org/packages/74/e8/30c42199bb5aefb37e02a9bece41f6a62a60a1c427cab8643bc0e7886df1/mypy-1.8.0-cp312-cp312-macosx_11_0_arm64.whl" + "hash": "81a10926e5473c5fc3da8abb04119a1f5811a236dc3a38d92015cb1e6ba4cb9e", + "url": "https://files.pythonhosted.org/packages/c7/9f/a2cec898515478f69a5996eb9df74513dd1d9658e7e83fb224d3a0b2cf0f/mypy-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, { "algorithm": "sha256", - "hash": "8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9", - "url": "https://files.pythonhosted.org/packages/76/5c/663409829016ca450b68b163cc36c67e0690c546e44923764043b85c175d/mypy-1.8.0-cp310-cp310-musllinux_1_1_x86_64.whl" + "hash": "e54396d70be04b34f31d2edf3362c1edd023246c82f1730bbf8768c28db5361b", + "url": "https://files.pythonhosted.org/packages/cb/a3/c6d971f07b312117073ca77f006337fc8b074eb304bdd43fbf9971cacbb3/mypy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl" }, { "algorithm": "sha256", - "hash": "5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8", - "url": "https://files.pythonhosted.org/packages/77/66/c79c051c1cc01c275e5d71acadf831aeef3099272e78c7d8b0685be0a567/mypy-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl" + "hash": "d357423fa57a489e8c47b7c85dfb96698caba13d66e086b412298a1a0ea3b0ed", + "url": "https://files.pythonhosted.org/packages/d0/41/87f727fdbb43a1f975df5fe5d038dad552440b1e5c21f999bce0d83fd847/mypy-1.9.0-cp310-cp310-macosx_11_0_arm64.whl" }, { "algorithm": "sha256", - "hash": "7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b", - "url": "https://files.pythonhosted.org/packages/86/5c/cbf921a0048926c4386410539ff4c3f08448684a92d9c8e73e692f1db154/mypy-1.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + "hash": "61758fabd58ce4b0720ae1e2fea5cfd4431591d6d590b197775329264f86311d", + "url": "https://files.pythonhosted.org/packages/d5/61/0433cb518d7f0eb1978834d23bcc178036e9629449cab9cecd2b2a46f0b3/mypy-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, { "algorithm": "sha256", - "hash": "afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218", - "url": "https://files.pythonhosted.org/packages/a6/70/49e9dc3d4ef98c22e09f1d7b0195833ad7eeda19a24fcc42bf1b62c89110/mypy-1.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + "hash": "587ce887f75dd9700252a3abbc9c97bbe165a4a630597845c61279cf32dfbf02", + "url": "https://files.pythonhosted.org/packages/d6/3f/213223cab830d9d593bb8764db51c00e528e6c20c2a48bb2f69e6dc3c003/mypy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl" }, { "algorithm": "sha256", - "hash": "4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3", - "url": "https://files.pythonhosted.org/packages/bb/b7/882110d1345847ce660c51fc83b3b590b9512ec2ea44e6cfd629a7d66146/mypy-1.8.0-cp311-cp311-macosx_11_0_arm64.whl" + "hash": "0235391f1c6f6ce487b23b9dbd1327b4ec33bb93934aa986efe8a9563d9349e6", + "url": "https://files.pythonhosted.org/packages/d7/d2/072e40384b53051106b4fcf03537fb88e2a6ad0757d2ab7f6c8c2f188a69/mypy-1.9.0-cp312-cp312-macosx_11_0_arm64.whl" }, { "algorithm": "sha256", - "hash": "028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6", - "url": "https://files.pythonhosted.org/packages/c4/8f/2042e7e7f19d78ce1ba7fc671700e0ba95d8b8299a86dd2646d2a1f84644/mypy-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl" + "hash": "3a3c007ff3ee90f69cf0a15cbcdf0995749569b86b6d2f327af01fd1b8aee9dc", + "url": "https://files.pythonhosted.org/packages/da/e2/1864612774cf8a445f6d42ce73ce0f1492a37ed2af1c908e989f1ec7d349/mypy-1.9.0-cp311-cp311-macosx_11_0_arm64.whl" }, { "algorithm": "sha256", - "hash": "42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d", - "url": "https://files.pythonhosted.org/packages/cf/e6/ff8f978edb778452748a3228c014b55d6585cccf62f80323eab391d2b811/mypy-1.8.0-cp38-cp38-musllinux_1_1_x86_64.whl" + "hash": "49c87c15aed320de9b438ae7b00c1ac91cd393c1b854c2ce538e2a72d55df150", + "url": "https://files.pythonhosted.org/packages/e1/87/b508b34309359daa00e0e76d9a0dbe43031866af49b279861f69c76e5d70/mypy-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, { "algorithm": "sha256", - "hash": "855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae", - "url": "https://files.pythonhosted.org/packages/d6/c4/2ce11ff9ba6c9c9e89df5049ab2325c85e60274194d6816e352926de5684/mypy-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl" + "hash": "5e6061f44f2313b94f920e91b204ec600982961e07a17e0f6cd83371cb23f5c2", + "url": "https://files.pythonhosted.org/packages/ee/2d/a081526f63444e6520dfcc0810326c44052b9d7e93d46549132f86b929e0/mypy-1.9.0-cp38-cp38-macosx_11_0_arm64.whl" }, { "algorithm": "sha256", - "hash": "2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d", - "url": "https://files.pythonhosted.org/packages/f1/48/e78aa47176bf7c24beb321031043d7c9c99035d816a6eca32d13cc59736f/mypy-1.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + "hash": "68edad3dc7d70f2f17ae4c6c1b9471a56138ca22722487eebacfd1eb5321d612", + "url": "https://files.pythonhosted.org/packages/ef/cf/43c1e29b9d3b2bf6c75e32d021d7db4631c516e4c0bd72b75bc8836680d8/mypy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl" } ], "project_name": "mypy", @@ -1213,7 +1265,7 @@ "typing-extensions>=4.1.0" ], "requires_python": ">=3.8", - "version": "1.8.0" + "version": "1.9.0" }, { "artifacts": [ @@ -1237,31 +1289,31 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7", - "url": "https://files.pythonhosted.org/packages/ec/1a/610693ac4ee14fcdf2d9bf3c493370e4f2ef7ae2e19217d7a237ff42367d/packaging-23.2-py3-none-any.whl" + "hash": "2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", + "url": "https://files.pythonhosted.org/packages/49/df/1fceb2f8900f8639e278b056416d49134fb8d84c5942ffaa01ad34782422/packaging-24.0-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", - "url": "https://files.pythonhosted.org/packages/fb/2b/9b9c33ffed44ee921d0967086d653047286054117d584f1b1a7c22ceaf7b/packaging-23.2.tar.gz" + "hash": "eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9", + "url": "https://files.pythonhosted.org/packages/ee/b5/b43a27ac7472e1818c4bafd44430e69605baefe1f34440593e0332ec8b4d/packaging-24.0.tar.gz" } ], "project_name": "packaging", "requires_dists": [], "requires_python": ">=3.7", - "version": "23.2" + "version": "24.0" }, { "artifacts": [ { "algorithm": "sha256", - "hash": "d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7", - "url": "https://files.pythonhosted.org/packages/05/b8/42ed91898d4784546c5f06c60506400548db3f7a4b3fb441cba4e5c17952/pluggy-1.3.0-py3-none-any.whl" + "hash": "7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981", + "url": "https://files.pythonhosted.org/packages/a5/5b/0cc789b59e8cc1bf288b38111d002d8c5917123194d45b29dcdac64723cc/pluggy-1.4.0-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12", - "url": "https://files.pythonhosted.org/packages/36/51/04defc761583568cae5fd533abda3d40164cbdcf22dee5b7126ffef68a40/pluggy-1.3.0.tar.gz" + "hash": "8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be", + "url": "https://files.pythonhosted.org/packages/54/c6/43f9d44d92aed815e781ca25ba8c174257e27253a94630d21be8725a2b59/pluggy-1.4.0.tar.gz" } ], "project_name": "pluggy", @@ -1272,7 +1324,7 @@ "tox; extra == \"dev\"" ], "requires_python": ">=3.8", - "version": "1.3.0" + "version": "1.4.0" }, { "artifacts": [ @@ -1307,13 +1359,13 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "0bfe9741f4fb1c8115cadd8fe832fa91ac277e81e0652ff7fa1400f0ef0f59ba", - "url": "https://files.pythonhosted.org/packages/e1/5c/94460062d3c1036bc9cbcfe12698f48bde31fcfb4f1fbce6c05effd4a5ec/psycopg-3.1.16-py3-none-any.whl" + "hash": "4d5a0a5a8590906daa58ebd5f3cfc34091377354a1acced269dd10faf55da60e", + "url": "https://files.pythonhosted.org/packages/fc/ca/dff5452be0b31fab93808b1ff0047bebc08d1ac65939be104c98e081b069/psycopg-3.1.18-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "a34d922fd7df3134595e71c3428ba6f1bd5f4968db74857fe95de12db2d6b763", - "url": "https://files.pythonhosted.org/packages/3b/19/5c75740c48f77eb07e4e799c4f4fdec8918724ab610c03e9ae95005dca80/psycopg-3.1.16.tar.gz" + "hash": "31144d3fb4c17d78094d9e579826f047d4af1da6a10427d91dfcfb6ecdf6f12b", + "url": "https://files.pythonhosted.org/packages/31/4d/43deb2a892b95875672df8fb34fcbff1345214f96d94ff49206871576fc0/psycopg-3.1.18.tar.gz" } ], "project_name": "psycopg", @@ -1321,7 +1373,7 @@ "Sphinx>=5.0; extra == \"docs\"", "anyio<4.0,>=3.6.2; extra == \"test\"", "backports.zoneinfo>=0.2.0; python_version < \"3.9\"", - "black>=23.1.0; extra == \"dev\"", + "black>=24.1.0; extra == \"dev\"", "codespell>=2.2; extra == \"dev\"", "dnspython>=2.1; extra == \"dev\"", "flake8>=4.0; extra == \"dev\"", @@ -1329,8 +1381,8 @@ "mypy>=1.4.1; extra == \"dev\"", "mypy>=1.4.1; extra == \"test\"", "pproxy>=2.7; extra == \"test\"", - "psycopg-binary==3.1.16; implementation_name != \"pypy\" and extra == \"binary\"", - "psycopg-c==3.1.16; implementation_name != \"pypy\" and extra == \"c\"", + "psycopg-binary==3.1.18; implementation_name != \"pypy\" and extra == \"binary\"", + "psycopg-c==3.1.18; implementation_name != \"pypy\" and extra == \"c\"", "psycopg-pool; extra == \"pool\"", "pytest-cov>=3.0; extra == \"test\"", "pytest-randomly>=3.5; extra == \"test\"", @@ -1343,7 +1395,7 @@ "wheel>=0.37; extra == \"dev\"" ], "requires_python": ">=3.7", - "version": "3.1.16" + "version": "3.1.18" }, { "artifacts": [ @@ -1367,395 +1419,359 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "d0caf5954bee831b6bfe7e338c32b9e30c85dfe080c843680783ac2b631673b4", - "url": "https://files.pythonhosted.org/packages/dd/b7/9aea7ee6c01fe3f3c03b8ca3c7797c866df5fecece9d6cb27caa138db2e2/pydantic-2.5.3-py3-none-any.whl" + "hash": "cc46fce86607580867bdc3361ad462bab9c222ef042d3da86f2fb333e1d916c5", + "url": "https://files.pythonhosted.org/packages/e5/f3/8296f550276194a58c5500d55b19a27ae0a5a3a51ffef66710c58544b32d/pydantic-2.6.4-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "b3ef57c62535b0941697cce638c08900d87fcb67e29cfa99e8a68f747f393f7a", - "url": "https://files.pythonhosted.org/packages/aa/3f/56142232152145ecbee663d70a19a45d078180633321efb3847d2562b490/pydantic-2.5.3.tar.gz" + "hash": "b1704e0847db01817624a6b86766967f552dd9dbf3afba4004409f908dcc84e6", + "url": "https://files.pythonhosted.org/packages/4b/de/38b517edac45dd022e5d139aef06f9be4762ec2e16e2b14e1634ba28886b/pydantic-2.6.4.tar.gz" } ], "project_name": "pydantic", "requires_dists": [ "annotated-types>=0.4.0", "email-validator>=2.0.0; extra == \"email\"", - "importlib-metadata; python_version == \"3.7\"", - "pydantic-core==2.14.6", + "pydantic-core==2.16.3", "typing-extensions>=4.6.1" ], - "requires_python": ">=3.7", - "version": "2.5.3" + "requires_python": ">=3.8", + "version": "2.6.4" }, { "artifacts": [ { "algorithm": "sha256", - "hash": "23598acb8ccaa3d1d875ef3b35cb6376535095e9405d91a3d57a8c7db5d29341", - "url": "https://files.pythonhosted.org/packages/e9/b8/5baba04b116546302bc0a07ba0989326a167aeec29fd6f5cadc7deb758b1/pydantic_core-2.14.6-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "a983cca5ed1dd9a35e9e42ebf9f278d344603bfcb174ff99a5815f953925140a", - "url": "https://files.pythonhosted.org/packages/0b/d0/adf341fb8ed080bf5abb91c42752ffa099d8439e45d3fa40a21f259f724c/pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" - }, - { - "algorithm": "sha256", - "hash": "6c8edaea3089bf908dd27da8f5d9e395c5b4dc092dbcce9b65e7156099b4b937", - "url": "https://files.pythonhosted.org/packages/0d/18/7c17d33b2c8dea2189b2547bafcb70a69a3e537eec12429cc0abfedab683/pydantic_core-2.14.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl" - }, - { - "algorithm": "sha256", - "hash": "b89f4477d915ea43b4ceea6756f63f0288941b6443a2b28c69004fe07fde0d0d", - "url": "https://files.pythonhosted.org/packages/13/33/9f761908fde3a6bb10ac865459a6931e53a2cde622782d243365e70981b7/pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl" - }, - { - "algorithm": "sha256", - "hash": "1b027c86c66b8627eb90e57aee1f526df77dc6d8b354ec498be9a757d513b92b", - "url": "https://files.pythonhosted.org/packages/14/53/7844d20be3a334ea46cdcde8a480cf47e31026d4117d7415a0144d7379c9/pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" - }, - { - "algorithm": "sha256", - "hash": "75ec284328b60a4e91010c1acade0c30584f28a1f345bc8f72fe8b9e46ec6a96", - "url": "https://files.pythonhosted.org/packages/24/1d/601f861c0d76154217ea6b066e39f04159a761b9c3a7ca56b0dd0267ce3a/pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" - }, - { - "algorithm": "sha256", - "hash": "36026d8f99c58d7044413e1b819a67ca0e0b8ebe0f25e775e6c3d1fabb3c38fb", - "url": "https://files.pythonhosted.org/packages/26/4b/da4ed701ee2ff392916f19149f8fb6d705282d96971cbf256142d0c11594/pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" - }, - { - "algorithm": "sha256", - "hash": "d5c28525c19f5bb1e09511669bb57353d22b94cf8b65f3a8d141c389a55dec95", - "url": "https://files.pythonhosted.org/packages/28/1e/04ede6259a552777a859d2d5828aedd540ca0db967641d61be864a49671a/pydantic_core-2.14.6-pp310-pypy310_pp73-macosx_10_7_x86_64.whl" + "hash": "ed25e1835c00a332cb10c683cd39da96a719ab1dfc08427d476bce41b92531fc", + "url": "https://files.pythonhosted.org/packages/5b/ff/8d5c1b17206098a92e57751c4fea3cefa6c3f215a32765faadce832012d1/pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl" }, { "algorithm": "sha256", - "hash": "72f9a942d739f09cd42fffe5dc759928217649f070056f03c70df14f5770acf9", - "url": "https://files.pythonhosted.org/packages/28/fc/bfb0da2b2d5b44e49c4c0ce99b07bbfd9f1a4dc92fd3e328a5cf1144467e/pydantic_core-2.14.6-cp310-cp310-macosx_10_7_x86_64.whl" + "hash": "0f56ae86b60ea987ae8bcd6654a887238fd53d1384f9b222ac457070b7ac4cff", + "url": "https://files.pythonhosted.org/packages/03/c8/9afd3a316123806d7bff177beba7906ab9dd267845ae42f98f051d2250a0/pydantic_core-2.16.3-cp312-cp312-macosx_10_12_x86_64.whl" }, { "algorithm": "sha256", - "hash": "f85f3843bdb1fe80e8c206fe6eed7a1caeae897e496542cee499c374a85c6e08", - "url": "https://files.pythonhosted.org/packages/29/5c/63eb74c7a97daf0ee45dc876f0b0d9cdea9c5c9d64e92508a765cb802e14/pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" + "hash": "936e5db01dd49476fa8f4383c259b8b1303d5dd5fb34c97de194560698cc2c5e", + "url": "https://files.pythonhosted.org/packages/0d/84/5e157e382cf8e2a5854802211ab954662841a82e3d3b9ff1be08b3fd7298/pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_aarch64.whl" }, { "algorithm": "sha256", - "hash": "b6ca36c12a5120bad343eef193cc0122928c5c7466121da7c20f41160ba00ba2", - "url": "https://files.pythonhosted.org/packages/2a/09/c39be628d6068952f30b381576a4392af2024505747572cd70b19f6d9bde/pydantic_core-2.14.6-cp311-cp311-musllinux_1_1_x86_64.whl" + "hash": "2a72fb9963cba4cd5793854fd12f4cfee731e86df140f59ff52a49b3552db241", + "url": "https://files.pythonhosted.org/packages/10/72/7574e1ef407fde0aa70fc02acdd09ea791366f69194827096a7072fa88a0/pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" }, { "algorithm": "sha256", - "hash": "a3dde6cac75e0b0902778978d3b1646ca9f438654395a362cb21d9ad34b24acf", - "url": "https://files.pythonhosted.org/packages/31/76/ee3c136138fbda5f58c3c49371503b42f3a9c678ef284a0b39be17253d78/pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl" + "hash": "4384a8f68ddb31a0b0c3deae88765f5868a1b9148939c3f4121233314ad5532c", + "url": "https://files.pythonhosted.org/packages/12/11/e7ababda30c736572300058c4b52cb2323f4d2bdabf26ec895361439792d/pydantic_core-2.16.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl" }, { "algorithm": "sha256", - "hash": "8ed1af8692bd8d2a29d702f1a2e6065416d76897d726e45a1775b1444f5928a7", - "url": "https://files.pythonhosted.org/packages/39/10/dc849eb0c1890c99958d3ae2cfacb502e4d0ab0360c63c7a20231ea04b32/pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl" + "hash": "a6b1bb0827f56654b4437955555dc3aeeebeddc47c2d7ed575477f082622c49e", + "url": "https://files.pythonhosted.org/packages/18/0e/1e39cfbffa57e92ab9f1f0869b32ead8a48ab11e4a373421d625f25fcb26/pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, { "algorithm": "sha256", - "hash": "4f86f1f318e56f5cbb282fe61eb84767aee743ebe32c7c0834690ebea50c0a6b", - "url": "https://files.pythonhosted.org/packages/3d/cf/d2e97b2bfd0bff7c4e9086fab03956003e906557c9c52941c15fed75152d/pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl" + "hash": "0df446663464884297c793874573549229f9eca73b59360878f382a0fc085979", + "url": "https://files.pythonhosted.org/packages/1d/fd/a59e201dc75125a91328e90b9156f31562c11075fffc9399cb9072a3a148/pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_aarch64.whl" }, { "algorithm": "sha256", - "hash": "3860c62057acd95cc84044e758e47b18dcd8871a328ebc8ccdefd18b0d26a21b", - "url": "https://files.pythonhosted.org/packages/43/39/cf14a183949bf162ab13a327b2f3a0f757e610f9c378a850e195d71bcfa0/pydantic_core-2.14.6-cp38-cp38-macosx_11_0_arm64.whl" + "hash": "a425479ee40ff021f8216c9d07a6a3b54b31c8267c6e17aa88b70d7ebd0e5e5b", + "url": "https://files.pythonhosted.org/packages/1e/d6/64d0b9569d8d176d1df5ff74e87aaf460093a57f93bae2aa4140c015cbab/pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl" }, { "algorithm": "sha256", - "hash": "399ac0891c284fa8eb998bcfa323f2234858f5d2efca3950ae58c8f88830f145", - "url": "https://files.pythonhosted.org/packages/48/64/de5432d19c42adbb26c4513866e2639c37c9081687c670bf8dc16cedfb6f/pydantic_core-2.14.6-pp38-pypy38_pp73-macosx_11_0_arm64.whl" + "hash": "5c5cbc703168d1b7a838668998308018a2718c2130595e8e190220238addc96f", + "url": "https://files.pythonhosted.org/packages/21/af/ff8366baf078e9b14fb780c004d1a5bf00d2c4fee3d04319991a66ace636/pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl" }, { "algorithm": "sha256", - "hash": "9c6a5c79b28003543db3ba67d1df336f253a87d3112dac3a51b94f7d48e4c0e1", - "url": "https://files.pythonhosted.org/packages/51/47/9f996e867123189f0b12364b00057887b61193d3d004a4391450e980512f/pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + "hash": "7233d65d9d651242a68801159763d09e9ec96e8a158dbf118dc090cd77a104c9", + "url": "https://files.pythonhosted.org/packages/37/c7/d006cca0650d39b887489f5b0c62c767d6614d2eceef12b378e25f0f68ec/pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl" }, { "algorithm": "sha256", - "hash": "eedf97be7bc3dbc8addcef4142f4b4164066df0c6f36397ae4aaed3eb187d8ab", - "url": "https://files.pythonhosted.org/packages/55/0f/45626f8bf7f7973320531bb384ac302eb9b05a70885b9db2bf1db4cf447b/pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + "hash": "b60cc1a081f80a2105a59385b92d82278b15d80ebb3adb200542ae165cd7d183", + "url": "https://files.pythonhosted.org/packages/39/ac/bb3fe0960707ba7ef18eb242ca193df59bc7eec925adbda1dc28df723c03/pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, { "algorithm": "sha256", - "hash": "7f5025db12fc6de7bc1104d826d5aee1d172f9ba6ca936bf6474c2148ac336c1", - "url": "https://files.pythonhosted.org/packages/55/d1/a291cef89adaa3d82b89055a010bd60560a7bda798e2e729d3dfeb875236/pydantic_core-2.14.6-cp38-cp38-musllinux_1_1_aarch64.whl" + "hash": "7ee8d5f878dccb6d499ba4d30d757111847b6849ae07acdd1205fffa1fc1253c", + "url": "https://files.pythonhosted.org/packages/3a/49/895d128c60d6af044c3c0d6a6850fd3bae35d07d2ab5bd03ab7364429eb2/pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" }, { "algorithm": "sha256", - "hash": "86c963186ca5e50d5c8287b1d1c9d3f8f024cbe343d048c5bd282aec2d8641f2", - "url": "https://files.pythonhosted.org/packages/59/f6/1e7193769d24b32b19139fb875693d7a351af17f10354e7583a0f7b61a49/pydantic_core-2.14.6-pp39-pypy39_pp73-macosx_10_7_x86_64.whl" + "hash": "4204e773b4b408062960e65468d5346bdfe139247ee5f1ca2a378983e11388a2", + "url": "https://files.pythonhosted.org/packages/3c/82/b79a75a6f5b19f7f43b08671f6b818a335b5d970b9e50a39acd3f07aed32/pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" }, { "algorithm": "sha256", - "hash": "00646784f6cd993b1e1c0e7b0fdcbccc375d539db95555477771c27555e3c556", - "url": "https://files.pythonhosted.org/packages/5c/7a/ceb3c9228ad9ff009ee70fd09ffb9160a45a8adaac5c9a90bc9496a1020e/pydantic_core-2.14.6-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl" + "hash": "287073c66748f624be4cef893ef9174e3eb88fe0b8a78dc22e88eca4bc357ca6", + "url": "https://files.pythonhosted.org/packages/41/d4/484a806ce1108c531e1ebfc08cd6d08e42b879c4dc54121309f6fa52c1dc/pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl" }, { "algorithm": "sha256", - "hash": "3dcf1978be02153c6a31692d4fbcc2a3f1db9da36039ead23173bc256ee3b91b", - "url": "https://files.pythonhosted.org/packages/5d/ca/e8fe62da4eb4b538c380900372021c560c3514514677d6d328ac5b95da7c/pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl" + "hash": "b1f6f5938d63c6139860f044e2538baeee6f0b251a1816e7adb6cbce106a1f01", + "url": "https://files.pythonhosted.org/packages/45/8b/3e9105cf60fcdd59cb130cd807331d66b04d4f6a438142469b0a827d80bc/pydantic_core-2.16.3-cp38-cp38-macosx_10_12_x86_64.whl" }, { "algorithm": "sha256", - "hash": "cb92f9061657287eded380d7dc455bbf115430b3aa4741bdc662d02977e7d0af", - "url": "https://files.pythonhosted.org/packages/5e/58/7cac843607f3b2d0af1768fae90ef219413db163a7cfb7557344edfeed2f/pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl" + "hash": "d33dd21f572545649f90c38c227cc8631268ba25c460b5569abebdd0ec5974ca", + "url": "https://files.pythonhosted.org/packages/46/28/cb10d96904bd7483a6237855e427876e72c369ec100d6c946d468257bbb8/pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_aarch64.whl" }, { "algorithm": "sha256", - "hash": "9862bf828112e19685b76ca499b379338fd4c5c269d897e218b2ae8fcb80139d", - "url": "https://files.pythonhosted.org/packages/5f/0c/3aeafa496aaf656be3682cbcacbfe3b4a4b366aaddac0ea74fb2c7c276a2/pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl" + "hash": "13dcc4802961b5f843a9385fc821a0b0135e8c07fc3d9949fd49627c1a5e6ae5", + "url": "https://files.pythonhosted.org/packages/4b/8c/569a5db6be43934d7ed88d0f64b9746217c5eca26cb7b6d70fcc01e81b9c/pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_x86_64.whl" }, { "algorithm": "sha256", - "hash": "26a92ae76f75d1915806b77cf459811e772d8f71fd1e4339c99750f0e7f6324f", - "url": "https://files.pythonhosted.org/packages/69/ed/6a318d3846ac45e4e8d7c81a4c4f9cad341f4715521cc2cc7baecd6be9c0/pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl" + "hash": "4ac6b4ce1e7283d715c4b729d8f9dab9627586dafce81d9eaa009dd7f25dd972", + "url": "https://files.pythonhosted.org/packages/4c/59/98f7929c35df068b872070bdb257404a60d5ddc8dabb9ac06af079576acd/pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_x86_64.whl" }, { "algorithm": "sha256", - "hash": "0348b1dc6b76041516e8a854ff95b21c55f5a411c3297d2ca52f5528e49d8411", - "url": "https://files.pythonhosted.org/packages/79/ae/ec8eaa6d9a1305100321d7b9c3c87e015ae61da02a877cfd16b0366b18ff/pydantic_core-2.14.6-cp39-cp39-musllinux_1_1_x86_64.whl" + "hash": "cbcc558401de90a746d02ef330c528f2e668c83350f045833543cd57ecead1ad", + "url": "https://files.pythonhosted.org/packages/4e/08/cf75dd8f8a87220f428cd03023369c9645a6005f88f9bf423cfa1825f746/pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl" }, { "algorithm": "sha256", - "hash": "6a31d98c0d69776c2576dda4b77b8e0c69ad08e8b539c25c7d0ca0dc19a50d6c", - "url": "https://files.pythonhosted.org/packages/7d/3a/46913f3134aff44d11edd7bdbba88efe6081f963014e6eaccf83fd8de2d7/pydantic_core-2.14.6-cp310-cp310-macosx_11_0_arm64.whl" + "hash": "99b6add4c0b39a513d323d3b93bc173dac663c27b99860dd5bf491b240d26137", + "url": "https://files.pythonhosted.org/packages/51/98/22a3052e4fc2189c3eda813eaad173139963cc84dfded0cc0695282f68aa/pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" }, { "algorithm": "sha256", - "hash": "dea7fcd62915fb150cdc373212141a30037e11b761fbced340e9db3379b892d4", - "url": "https://files.pythonhosted.org/packages/7d/77/cbfa02b5f46c5ec6be131d97ae93eef883e25d61b4f4d0a058c792b7e3a2/pydantic_core-2.14.6-cp311-cp311-macosx_10_7_x86_64.whl" + "hash": "36fa178aacbc277bc6b62a2c3da95226520da4f4e9e206fdf076484363895d2c", + "url": "https://files.pythonhosted.org/packages/51/b2/ecf41e6e365c946145a4e88efa7e60e6c1173cb93e1cb3a107166bb09efc/pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl" }, { "algorithm": "sha256", - "hash": "27e524624eace5c59af499cd97dc18bb201dc6a7a2da24bfc66ef151c69a5f2a", - "url": "https://files.pythonhosted.org/packages/7f/3d/91a26a7004a57f374d85d837b4b06dde818045ddba34bc19909e04e2a14d/pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + "hash": "2acca2be4bb2f2147ada8cac612f8a98fc09f41c89f87add7256ad27332c2fda", + "url": "https://files.pythonhosted.org/packages/54/18/7dd9308ad022d0b47b41f5506e179e563e7cf04a04d1574598e756c83b2a/pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl" }, { "algorithm": "sha256", - "hash": "4ce8299b481bcb68e5c82002b96e411796b844d72b3e92a3fbedfe8e19813eab", - "url": "https://files.pythonhosted.org/packages/80/8c/d40937f7f7ccfe9776d1e32b36cebe606da9f11624927bd26722c43ea9cb/pydantic_core-2.14.6-cp39-cp39-macosx_10_7_x86_64.whl" + "hash": "8e47755d8152c1ab5b55928ab422a76e2e7b22b5ed8e90a7d584268dd49e9c6b", + "url": "https://files.pythonhosted.org/packages/54/c0/7ecafb2dad658078bf28e4045a29a7b2de76319ebbc8cf7ef177d17e4d9e/pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl" }, { "algorithm": "sha256", - "hash": "172de779e2a153d36ee690dbc49c6db568d7b33b18dc56b69a7514aecbcf380d", - "url": "https://files.pythonhosted.org/packages/84/e4/da29895abb136eea169944eb81f866d783255c4a6fd581c667c15743b171/pydantic_core-2.14.6-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl" + "hash": "578114bc803a4c1ff9946d977c221e4376620a46cf78da267d946397dc9514a8", + "url": "https://files.pythonhosted.org/packages/59/7b/de628f7afabf4c0d5cd3bbc871b90843c08b3ca1a5d3e465f70428c7eaab/pydantic_core-2.16.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl" }, { "algorithm": "sha256", - "hash": "8b93785eadaef932e4fe9c6e12ba67beb1b3f1e5495631419c784ab87e975670", - "url": "https://files.pythonhosted.org/packages/88/bb/58bd737b1f4a3b567410fd7a55f2e0ed4ba3209bb1a7a35856714a322a04/pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + "hash": "f651dd19363c632f4abe3480a7c87a9773be27cfe1341aef06e8759599454120", + "url": "https://files.pythonhosted.org/packages/60/7e/5bdb72aa8f1de0a0e38194dd261b5335747ef8d9bf3421fc960498442830/pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl" }, { "algorithm": "sha256", - "hash": "5aa90562bc079c6c290f0512b21768967f9968e4cfea84ea4ff5af5d917016e4", - "url": "https://files.pythonhosted.org/packages/8f/2d/919d3642da44bc9d9c60a2e7bbda04633fc3ffbd6768c355ac0d7e2424d7/pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + "hash": "f4cb85f693044e0f71f394ff76c98ddc1bc0953e48c061725e540396d5c8a2e1", + "url": "https://files.pythonhosted.org/packages/62/0a/f4c40eccecd08677b3b7b96dc87c6705a56f546c2a5404241de01ffa9da9/pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl" }, { "algorithm": "sha256", - "hash": "036137b5ad0cb0004c75b579445a1efccd072387a36c7f217bb8efd1afbe5245", - "url": "https://files.pythonhosted.org/packages/90/28/3c6843e6b203999be2660d3f114be196f2182dcac533dc764ad320c9184d/pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + "hash": "b926dd38db1519ed3043a4de50214e0d600d404099c3392f098a7f9d75029ff8", + "url": "https://files.pythonhosted.org/packages/62/c1/c0e7984c1e06d53dc48231f052699ba62ec97a1429413295f883c66bfda8/pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" }, { "algorithm": "sha256", - "hash": "db453f2da3f59a348f514cfbfeb042393b68720787bbef2b4c6068ea362c8149", - "url": "https://files.pythonhosted.org/packages/92/2a/8cff567680c0d5e03ef4da218656a61286add825b4733476e6ba13ffeee9/pydantic_core-2.14.6-cp38-cp38-macosx_10_7_x86_64.whl" + "hash": "b5b6079cc452a7c53dd378c6f881ac528246b3ac9aae0f8eef98498a75657805", + "url": "https://files.pythonhosted.org/packages/63/5a/c38280dff597924f0962195bbb11318d710271e76f12661f8a4de3f9738f/pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl" }, { "algorithm": "sha256", - "hash": "86ce5fcfc3accf3a07a729779d0b86c5d0309a4764c897d86c11089be61da160", - "url": "https://files.pythonhosted.org/packages/93/57/9a77cc69f05f725a2b492a18209a43ba4e8b9ee179d3c27a8b6b3ab2f921/pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" + "hash": "704d35ecc7e9c31d48926150afada60401c55efa3b46cd1ded5a01bdffaf1d48", + "url": "https://files.pythonhosted.org/packages/64/b7/b6ae4b8d37e4695e74e0578dd842c94cef406f4ebad9c98a2f248a0057d2/pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" }, { "algorithm": "sha256", - "hash": "599c87d79cab2a6a2a9df4aefe0455e61e7d2aeede2f8577c1b7c0aec643ee8e", - "url": "https://files.pythonhosted.org/packages/97/9e/f42db0e2931cd67bf990d22215ec50444e31aa6e80e63b8531ab1a5f3ffb/pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + "hash": "1cac689f80a3abab2d3c0048b29eea5751114054f032a941a32de4c852c59cad", + "url": "https://files.pythonhosted.org/packages/77/3f/65dbe5231946fe02b4e6ea92bc303d2462f45d299890fd5e8bfe4d1c3d66/pydantic_core-2.16.3.tar.gz" }, { "algorithm": "sha256", - "hash": "370ffecb5316ed23b667d99ce4debe53ea664b99cc37bfa2af47bc769056d534", - "url": "https://files.pythonhosted.org/packages/9b/cd/a2db754b0124e64ad7912160d9c9db310cbd52a990841ef121b53453992d/pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl" + "hash": "162e498303d2b1c036b957a1278fa0899d02b2842f1ff901b6395104c5554a45", + "url": "https://files.pythonhosted.org/packages/78/7e/e8d64c813b1a632c8d545b0208182361597973ad8a4f5082cc66dcdcef51/pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" }, { "algorithm": "sha256", - "hash": "dfcebb950aa7e667ec226a442722134539e77c575f6cfaa423f24371bb8d2e94", - "url": "https://files.pythonhosted.org/packages/9d/21/32afbed9bfedf916dff87846e10ecd8711ba63c88cd6c9bcfc3297ef22ca/pydantic_core-2.14.6-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl" + "hash": "dcca5d2bf65c6fb591fff92da03f94cd4f315972f97c21975398bd4bd046854a", + "url": "https://files.pythonhosted.org/packages/7a/48/6853dfcf23693ac14af1ff381e17f318c2ef381db1fedb157b30fd540644/pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl" }, { "algorithm": "sha256", - "hash": "cf10b7d58ae4a1f07fccbf4a0a956d705356fea05fb4c70608bb6fa81d103cda", - "url": "https://files.pythonhosted.org/packages/9e/0a/c56318f1668de782f31b6e9798217e2e5a99d4cce7a8eddffb60bebe3c09/pydantic_core-2.14.6-pp38-pypy38_pp73-macosx_10_7_x86_64.whl" + "hash": "cf6204fe865da605285c34cf1172879d0314ff267b1c35ff59de7154f35fdc2e", + "url": "https://files.pythonhosted.org/packages/7c/6e/3c188b11eef09d15702f3808bf6d0b2828a4268fb4be19ac7a2ef4f6a8c7/pydantic_core-2.16.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl" }, { "algorithm": "sha256", - "hash": "e8a5ac97ea521d7bde7621d86c30e86b798cdecd985723c4ed737a2aa9e77d0c", - "url": "https://files.pythonhosted.org/packages/9f/7a/2e906fc1a5e4ca45e730118f0afb4878a39a1d505d895835d8cc5452446c/pydantic_core-2.14.6-cp312-cp312-musllinux_1_1_x86_64.whl" + "hash": "d8f99b147ff3fcf6b3cc60cb0c39ea443884d5559a30b1481e92495f2310ff2b", + "url": "https://files.pythonhosted.org/packages/7d/d2/66392daf79515c506a1742f935d01861b945e023dcfc5c8cea14bd6aa6c1/pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_aarch64.whl" }, { "algorithm": "sha256", - "hash": "71d72ca5eaaa8d38c8df16b7deb1a2da4f650c41b58bb142f3fb75d5ad4a611f", - "url": "https://files.pythonhosted.org/packages/a2/7e/4af14122c7ea67ad5582fddae56f7827044f6b43cca6c7e7421686cca3de/pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + "hash": "75b81e678d1c1ede0785c7f46690621e4c6e63ccd9192af1f0bd9d504bbb6bf4", + "url": "https://files.pythonhosted.org/packages/87/07/7f0e613e287376a7a2673c31fa24e1891f750972290465bd2d8a73d1ba07/pydantic_core-2.16.3-cp310-cp310-macosx_10_12_x86_64.whl" }, { "algorithm": "sha256", - "hash": "2f6ffc6701a0eb28648c845f4945a194dc7ab3c651f535b81793251e1185ac3d", - "url": "https://files.pythonhosted.org/packages/a5/5c/289261738045fa6b97e75d8c2ee110fab5c2d1025f7d345816f0f56f1c1e/pydantic_core-2.14.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl" + "hash": "c6119dc90483a5cb50a1306adb8d52c66e447da88ea44f323e0ae1a5fcb14256", + "url": "https://files.pythonhosted.org/packages/8d/0c/91c3a51e5bd1480e1799322392d52f2c3164277051c9b22d581f2a85bbcc/pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, { "algorithm": "sha256", - "hash": "cdee837710ef6b56ebd20245b83799fce40b265b3b406e51e8ccc5b85b9099b7", - "url": "https://files.pythonhosted.org/packages/a5/f8/07a2563f40b863ba97f3db648697f3f1d7b7edf1bd679f210064cb556e74/pydantic_core-2.14.6-cp312-cp312-macosx_11_0_arm64.whl" + "hash": "bda1ee3e08252b8d41fa5537413ffdddd58fa73107171a126d3b9ff001b9b820", + "url": "https://files.pythonhosted.org/packages/8e/a4/00f38508c29cb7256388dc5c08bbb105f626d47a50c1be69521109b87d5e/pydantic_core-2.16.3-cp39-cp39-macosx_10_12_x86_64.whl" }, { "algorithm": "sha256", - "hash": "982487f8931067a32e72d40ab6b47b1628a9c5d344be7f1a4e668fb462d2da42", - "url": "https://files.pythonhosted.org/packages/ab/3d/f4739255d8676debf398116e8ded523cf9bc9289a14734b3dc10645da67d/pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl" + "hash": "519ae0312616026bf4cedc0fe459e982734f3ca82ee8c7246c19b650b60a5ee4", + "url": "https://files.pythonhosted.org/packages/8e/c7/d89b2692eaaebadc9aa792a8e22f085b7fc7ed11f4cff791a9572c3fae3f/pydantic_core-2.16.3-cp311-cp311-macosx_10_12_x86_64.whl" }, { "algorithm": "sha256", - "hash": "43e166ad47ba900f2542a80d83f9fc65fe99eb63ceec4debec160ae729824052", - "url": "https://files.pythonhosted.org/packages/ae/91/b5d718de2fc191a1937470e79b53535cf0c3a87b2f21ee927710f4dd4570/pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl" + "hash": "21b888c973e4f26b7a96491c0965a8a312e13be108022ee510248fe379a5fa23", + "url": "https://files.pythonhosted.org/packages/90/49/a8b478aaccd9cf9e93b2c492695fe626d376b32f0c5fad5431ed92e8ef25/pydantic_core-2.16.3-cp39-cp39-macosx_11_0_arm64.whl" }, { "algorithm": "sha256", - "hash": "fd9e98b408384989ea4ab60206b8e100d8687da18b5c813c11e92fd8212a98e0", - "url": "https://files.pythonhosted.org/packages/b1/1c/ab01fa05c9fc885a73357116c494feafe1207035f13848e4a772fc9d6154/pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + "hash": "be0ec334369316fa73448cc8c982c01e5d2a81c95969d58b8f6e272884df0074", + "url": "https://files.pythonhosted.org/packages/94/18/56735aa19a265916684cda6b77c716ff51a37d4cba79685853d0203cc928/pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" }, { "algorithm": "sha256", - "hash": "667aa2eac9cd0700af1ddb38b7b1ef246d8cf94c85637cbb03d7757ca4c3fdec", - "url": "https://files.pythonhosted.org/packages/b1/26/4bd7ac215215322a693c178a022993450ebf7b1e91b26941f72407e1e9a1/pydantic_core-2.14.6-cp312-cp312-macosx_10_7_x86_64.whl" + "hash": "75f76ee558751746d6a38f89d60b6228fa174e5172d143886af0f85aa306fd89", + "url": "https://files.pythonhosted.org/packages/9a/d8/7bd2fdf14e33ab0c16f45c4099917f30b37ad206bb2e0f6bb1b2d9c0f395/pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, { "algorithm": "sha256", - "hash": "747265448cb57a9f37572a488a57d873fd96bf51e5bb7edb52cfb37124516da4", - "url": "https://files.pythonhosted.org/packages/b2/4a/3be721510f2fea9ce56b25812e6d6ecea9833c06fa8ae479cd41beb404f5/pydantic_core-2.14.6-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl" + "hash": "e7c6ed0dc9d8e65f24f5824291550139fe6f37fac03788d4580da0d33bc00c97", + "url": "https://files.pythonhosted.org/packages/9d/1a/b550381063265588e7c54ff56a642a725ac3bfbb3c8a5a08409ccac1e810/pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl" }, { "algorithm": "sha256", - "hash": "1fd0c1d395372843fba13a51c28e3bb9d59bd7aebfeb17358ffaaa1e4dbbe948", - "url": "https://files.pythonhosted.org/packages/b2/7d/8304d8471cfe4288f95a3065ebda56f9790d087edc356ad5bd83c89e2d79/pydantic_core-2.14.6.tar.gz" + "hash": "a4b2bf78342c40b3dc830880106f54328928ff03e357935ad26c7128bbd66ce8", + "url": "https://files.pythonhosted.org/packages/a7/6b/c9cfc165e18222b226daedb2940889f3c3a22fdc11ebf29249bf8704b8b8/pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_aarch64.whl" }, { "algorithm": "sha256", - "hash": "36099c69f6b14fc2c49d7996cbf4f87ec4f0e66d1c74aa05228583225a07b590", - "url": "https://files.pythonhosted.org/packages/b3/c5/2accf5bbc145b890454d4eaf8dcd6423d406fc9f64147fd9020618363866/pydantic_core-2.14.6-cp310-cp310-musllinux_1_1_x86_64.whl" + "hash": "49d5d58abd4b83fb8ce763be7794d09b2f50f10aa65c0f0c1696c677edeb7cbf", + "url": "https://files.pythonhosted.org/packages/af/9b/3eb4c9dc8712543424b1731c44d3597f56ed4be3bdfbec3f9a45111b774a/pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_x86_64.whl" }, { "algorithm": "sha256", - "hash": "7e90d6cc4aad2cc1f5e16ed56e46cebf4877c62403a311af20459c15da76fd91", - "url": "https://files.pythonhosted.org/packages/b7/53/101aac1d63a743284cdae804ceb6f561879c385f355caf20d2d87da6d36d/pydantic_core-2.14.6-cp312-cp312-musllinux_1_1_aarch64.whl" + "hash": "c9803edf8e29bd825f43481f19c37f50d2b01899448273b3a7758441b512acf8", + "url": "https://files.pythonhosted.org/packages/b1/01/1e562fd1f8ebce512cb785964de1b14cdd09ce047c573488dcc8a19111c1/pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" }, { "algorithm": "sha256", - "hash": "8a14c192c1d724c3acbfb3f10a958c55a2638391319ce8078cb36c02283959b9", - "url": "https://files.pythonhosted.org/packages/ba/09/8078e77e73dda7df0d5cca8541d1fb731a52bc00188806676c3635c344a9/pydantic_core-2.14.6-cp39-cp39-musllinux_1_1_aarch64.whl" + "hash": "00ee1c97b5364b84cb0bd82e9bbf645d5e2871fb8c58059d158412fee2d33d8a", + "url": "https://files.pythonhosted.org/packages/b2/b4/7b0b21e39542db4abeb9a3470c4dfa528e5131ae1c5f487bfe46e60284f2/pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl" }, { "algorithm": "sha256", - "hash": "2c5bcf3414367e29f83fd66f7de64509a8fd2368b1edf4351e862910727d3e51", - "url": "https://files.pythonhosted.org/packages/ba/98/fb42628ed811643c364e05353d3a015c74859402994420aeba8e3e34a54c/pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + "hash": "9c865a7ee6f93783bd5d781af5a4c43dadc37053a5b42f7d18dc019f8c9d2bd1", + "url": "https://files.pythonhosted.org/packages/b3/9b/bab93756eb12a10e3db425d5e6bd603aa7089e596202713020bbb91b00e4/pydantic_core-2.16.3-cp310-cp310-macosx_11_0_arm64.whl" }, { "algorithm": "sha256", - "hash": "0c08de15d50fa190d577e8591f0329a643eeaed696d7771760295998aca6bc66", - "url": "https://files.pythonhosted.org/packages/bb/32/a2f381c8ae08a9682d4e7943ba1f5b518e6f2bdd8261c23721691b332966/pydantic_core-2.14.6-cp310-cp310-musllinux_1_1_aarch64.whl" + "hash": "4df8a199d9f6afc5ae9a65f8f95ee52cae389a8c6b20163762bde0426275b7db", + "url": "https://files.pythonhosted.org/packages/b5/d4/c26689ac08b4b935d11e395516403a7b77e68e94f4861300447d1b1c8de5/pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_x86_64.whl" }, { "algorithm": "sha256", - "hash": "a874f21f87c485310944b2b2734cd6d318765bcbb7515eead33af9641816506e", - "url": "https://files.pythonhosted.org/packages/bc/7f/20ddc4eb15708cc6832c0cc2e398d0fa642aaf28d6ebcbcfb2d284ec6824/pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + "hash": "fc4ad7f7ee1a13d9cb49d8198cd7d7e3aa93e425f371a68235f784e99741561f", + "url": "https://files.pythonhosted.org/packages/b8/be/a3c2edde00afcf5cdc0fb710ce0289f5af776273f420b4486cf005c94b57/pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, { "algorithm": "sha256", - "hash": "478e9e7b360dfec451daafe286998d4a1eeaecf6d69c427b834ae771cad4b622", - "url": "https://files.pythonhosted.org/packages/c1/7b/a1cfe9d3fdedf2b33d41960500c17ccba025b483720c79965b73d607687f/pydantic_core-2.14.6-cp311-cp311-musllinux_1_1_aarch64.whl" + "hash": "2f583bd01bbfbff4eaee0868e6fc607efdfcc2b03c1c766b06a707abbc856187", + "url": "https://files.pythonhosted.org/packages/bc/e7/e387bf771fac18e41893dc7e08f07dc3e93143b1befebc7af71cbd847004/pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl" }, { "algorithm": "sha256", - "hash": "d5f916acf8afbcab6bacbb376ba7dc61f845367901ecd5e328fc4d4aef2fcab0", - "url": "https://files.pythonhosted.org/packages/ce/95/d0bc7df3de0eaad08de467c50d1dc423839864f32e78da1cf57af3bbb2cc/pydantic_core-2.14.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl" + "hash": "e56f8186d6210ac7ece503193ec84104da7ceb98f68ce18c07282fcc2452e76f", + "url": "https://files.pythonhosted.org/packages/be/31/5f6b46d10f7624963630a38cf3ac97f5d62982000a656aa1976d2f84edbd/pydantic_core-2.16.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl" }, { "algorithm": "sha256", - "hash": "ef633add81832f4b56d3b4c9408b43d530dfca29e68fb1b797dcb861a2c734cd", - "url": "https://files.pythonhosted.org/packages/d0/21/7ca5edf46bc6706152d459b560d669cfd72afe0dda24292408f1be8008d6/pydantic_core-2.14.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl" + "hash": "1b662180108c55dfbf1280d865b2d116633d436cfc0bba82323554873967b340", + "url": "https://files.pythonhosted.org/packages/ce/68/50bfcf8fc9e51a9ca7e914bfcf8902008511e63f9922694474161ed028b9/pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" }, { "algorithm": "sha256", - "hash": "dab03ed811ed1c71d700ed08bde8431cf429bbe59e423394f0f4055f1ca0ea60", - "url": "https://files.pythonhosted.org/packages/d7/8a/d2c7668e15d3be9157e8328712db22568770640fdcc3a13f4ff0cdd87ee9/pydantic_core-2.14.6-cp38-cp38-musllinux_1_1_x86_64.whl" + "hash": "fee427241c2d9fb7192b658190f9f5fd6dfe41e02f3c1489d2ec1e6a5ab1e04a", + "url": "https://files.pythonhosted.org/packages/d1/43/430e8a0be9dfec1ff9fb7f2289da9bd684fdb8d15796888a53b540c5e3d6/pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl" }, { "algorithm": "sha256", - "hash": "2dbe357bc4ddda078f79d2a36fc1dd0494a7f2fad83a0a684465b6f24b46fe80", - "url": "https://files.pythonhosted.org/packages/dd/3d/1a5936fc5558521e8aae22dfb7f0ae6b649040b5fcef7f25be1371d02752/pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + "hash": "72282ad4892a9fb2da25defeac8c2e84352c108705c972db82ab121d15f14e6d", + "url": "https://files.pythonhosted.org/packages/d7/70/c05ec1dc13e2ec4247309ba1fe1b37847c24cdd7929e7116a55da62a25ad/pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl" }, { "algorithm": "sha256", - "hash": "314ccc4264ce7d854941231cf71b592e30d8d368a71e50197c905874feacc8a8", - "url": "https://files.pythonhosted.org/packages/df/ea/435b1ad6890eec709e49dbcc5c0a72ca62ff8c6e62cfc45b7386e5e4cecc/pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" + "hash": "8d62da299c6ecb04df729e4b5c52dc0d53f4f8430b4492b93aa8de1f541c4aac", + "url": "https://files.pythonhosted.org/packages/d7/ce/666885ab07e5184825b081095071297057b77c9dccd62616bf5b85a26365/pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" }, { "algorithm": "sha256", - "hash": "3a0b5db001b98e1c649dd55afa928e75aa4087e587b9524a4992316fa23c9fba", - "url": "https://files.pythonhosted.org/packages/e2/6d/789f2495c66c99a98b7a09a96145d5f3408941f839de7751995d9a5a8428/pydantic_core-2.14.6-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl" + "hash": "aaf09e615a0bf98d406657e0008e4a8701b11481840be7d31755dc9f97c44053", + "url": "https://files.pythonhosted.org/packages/d7/d9/b3d217a092bf23b143e59a691d61598c308386293c310ff6746a0c8ed6a5/pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" }, { "algorithm": "sha256", - "hash": "b2602177668f89b38b9f84b7b3435d0a72511ddef45dc14446811759b82235a1", - "url": "https://files.pythonhosted.org/packages/e7/84/2dc88180fc6f0d13aab2a47a53b89c2dbc239e2a87d0a58e31077e111e82/pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + "hash": "716b542728d4c742353448765aa7cdaa519a7b82f9564130e2b3f6766018c9ec", + "url": "https://files.pythonhosted.org/packages/d8/f1/831ee552713474daf89997b56f3c0e7157ad40fe599172b444750f50ca66/pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl" }, { "algorithm": "sha256", - "hash": "7e1f4744eea1501404b20b0ac059ff7e3f96a97d3e3f48ce27a139e053bb370b", - "url": "https://files.pythonhosted.org/packages/e8/5e/a30d56bb6b19e84bcde76cba2d6df45779f127ec73fa2e6d91f0ad3d4bc2/pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl" + "hash": "500960cb3a0543a724a81ba859da816e8cf01b0e6aaeedf2c3775d12ee49cade", + "url": "https://files.pythonhosted.org/packages/dc/df/cd1cdd79a307c06fbea11be2cd8f361604b82f9b28c7712bd1220c44f226/pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, { "algorithm": "sha256", - "hash": "00b1087dabcee0b0ffd104f9f53d7d3eaddfaa314cdd6726143af6bc713aa27e", - "url": "https://files.pythonhosted.org/packages/f1/7b/0fd3444362f31c5f42b655c1ed734480433aa9f8bde97daa19cee0bc2844/pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl" + "hash": "c9bd22a2a639e26171068f8ebb5400ce2c1bc7d17959f60a3b753ae13c632975", + "url": "https://files.pythonhosted.org/packages/e7/b2/b6eef8d0a914e44826785cc99cd7a1711c2eea2dfc69bc3aefc3be507234/pydantic_core-2.16.3-cp312-cp312-macosx_11_0_arm64.whl" }, { "algorithm": "sha256", - "hash": "e4ace1e220b078c8e48e82c081e35002038657e4b37d403ce940fa679e57113b", - "url": "https://files.pythonhosted.org/packages/f3/62/076e6c43735950e911d80c6edf215314a8cf9b8adefe9613b72b09ccb1ee/pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + "hash": "7f752826b5b8361193df55afcdf8ca6a57d0232653494ba473630a83ba50d8c9", + "url": "https://files.pythonhosted.org/packages/eb/20/c44de400d4906f75c11e8e447d1dba24ee7273fec02073686af8866f6e38/pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" }, { "algorithm": "sha256", - "hash": "92879bce89f91f4b2416eba4429c7b5ca22c45ef4a499c39f0c5c69257522c7c", - "url": "https://files.pythonhosted.org/packages/f3/7e/f1c1cf229bd404f5daf972345030f0c205424a326e67ae888c4a5a9066bd/pydantic_core-2.14.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl" + "hash": "d937653a696465677ed583124b94a4b2d79f5e30b2c46115a68e482c6a591c8a", + "url": "https://files.pythonhosted.org/packages/ee/16/9f724d8841bef4509f667143501529c75091760a4248c2e2459f64378b55/pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl" }, { "algorithm": "sha256", - "hash": "b9a9d92f10772d2a181b5ca339dee066ab7d1c9a34ae2421b2a52556e719756f", - "url": "https://files.pythonhosted.org/packages/f4/cd/252101e88458f4e7c4d2c44400050f92a0b13960ed3c489b513c97aaa7a6/pydantic_core-2.14.6-cp39-cp39-macosx_11_0_arm64.whl" + "hash": "2a1ef6a36fdbf71538142ed604ad19b82f67b05749512e47f247a6ddd06afdc7", + "url": "https://files.pythonhosted.org/packages/f0/d6/a8914af00eeb62444609f3c7acda8fd92b23ed5f14d272ad0f3fbe103730/pydantic_core-2.16.3-cp38-cp38-macosx_11_0_arm64.whl" }, { "algorithm": "sha256", - "hash": "78d0768ee59baa3de0f4adac9e3748b4b1fffc52143caebddfd5ea2961595277", - "url": "https://files.pythonhosted.org/packages/f9/84/c53d351f926630753b8dcf37ec2edf8b55a5a1724b3edc5104e06d3e54f1/pydantic_core-2.14.6-pp310-pypy310_pp73-macosx_11_0_arm64.whl" + "hash": "bd87f48924f360e5d1c5f770d6155ce0e7d83f7b4e10c2f9ec001c73cf475c99", + "url": "https://files.pythonhosted.org/packages/f1/35/a081d16848d303abaf2fdd98c65b3da0593455e5867c61d211626b5e8139/pydantic_core-2.16.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl" }, { "algorithm": "sha256", - "hash": "ffff855100bc066ff2cd3aa4a60bc9534661816b110f0243e59503ec2df38421", - "url": "https://files.pythonhosted.org/packages/fb/17/3e4908cf8cb5a1d189f9dfa7cb5698d945e9a4db6b9138e3fef3c32c1f68/pydantic_core-2.14.6-cp311-cp311-macosx_11_0_arm64.whl" + "hash": "33809aebac276089b78db106ee692bdc9044710e26f24a9a2eaa35a0f9fa70ba", + "url": "https://files.pythonhosted.org/packages/fe/18/ced020e55c75cfc514957bbe8fefe61d591673098c4385c53bcad183928f/pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_x86_64.whl" }, { "algorithm": "sha256", - "hash": "e0641b506486f0b4cd1500a2a65740243e8670a2549bb02bc4556a83af84ae03", - "url": "https://files.pythonhosted.org/packages/fb/ff/812893fd262a98f0291f6afd87a530eb87c75ddc92034b938b8d15aa5ff4/pydantic_core-2.14.6-pp39-pypy39_pp73-macosx_11_0_arm64.whl" + "hash": "b3992a322a5617ded0a9f23fd06dbc1e4bd7cf39bc4ccf344b10f80af58beacd", + "url": "https://files.pythonhosted.org/packages/ff/c7/e14e6ce2fe221d1046a7cc190b26b2bde2b1076d901154cdb8c20d88e6e0/pydantic_core-2.16.3-cp311-cp311-macosx_11_0_arm64.whl" } ], "project_name": "pydantic-core", "requires_dists": [ "typing-extensions!=4.7.0,>=4.6.0" ], - "requires_python": ">=3.7", - "version": "2.14.6" + "requires_python": ">=3.8", + "version": "2.16.3" }, { "artifacts": [ @@ -1942,6 +1958,11 @@ "hash": "28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696", "url": "https://files.pythonhosted.org/packages/c1/39/47ed4d65beec9ce07267b014be85ed9c204fa373515355d3efa62d19d892/PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl" }, + { + "algorithm": "sha256", + "hash": "a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef", + "url": "https://files.pythonhosted.org/packages/c7/4c/4a2908632fc980da6d918b9de9c1d9d7d7e70b2672b1ad5166ed27841ef7/PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + }, { "algorithm": "sha256", "hash": "7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735", @@ -2016,24 +2037,6 @@ "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7", "version": "1.16.0" }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384", - "url": "https://files.pythonhosted.org/packages/c3/a0/5dba8ed157b0136607c7f2151db695885606968d1fae123dc3391e0cfdbf/sniffio-1.3.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101", - "url": "https://files.pythonhosted.org/packages/cd/50/d49c388cae4ec10e8109b1b833fd265511840706808576df3ada99ecb0ac/sniffio-1.3.0.tar.gz" - } - ], - "project_name": "sniffio", - "requires_dists": [], - "requires_python": ">=3.7", - "version": "1.3.0" - }, { "artifacts": [ { @@ -2052,6 +2055,42 @@ "requires_python": null, "version": "2.4.0" }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "7f48cef4bf0ccd78f1a4534d4b701a003a3bace851f24eae58a32f9e3f0aeba0", + "url": "https://files.pythonhosted.org/packages/b3/37/38c595414d764cb1d9f3a0c907878c4146a21505ab974c63bcf3d8145807/testcontainers-3.7.1-py2.py3-none-any.whl" + } + ], + "project_name": "testcontainers", + "requires_dists": [ + "azure-storage-blob; extra == \"azurite\"", + "clickhouse-driver; extra == \"clickhouse\"", + "cx-Oracle; extra == \"oracle\"", + "deprecation", + "docker-compose; extra == \"docker-compose\"", + "docker>=4.0.0", + "google-cloud-pubsub<2; extra == \"google-cloud-pubsub\"", + "kafka-python; extra == \"kafka\"", + "neo4j; extra == \"neo4j\"", + "pika; extra == \"rabbitmq\"", + "psycopg2-binary; extra == \"postgresql\"", + "pymongo; extra == \"mongo\"", + "pymssql; extra == \"mssqlserver\"", + "pymysql; extra == \"mysql\"", + "python-arango; extra == \"arangodb\"", + "python-keycloak; extra == \"keycloak\"", + "redis; extra == \"redis\"", + "selenium; extra == \"selenium\"", + "sqlalchemy; extra == \"mysql\"", + "sqlalchemy; extra == \"oracle\"", + "sqlalchemy; extra == \"postgresql\"", + "wrapt" + ], + "requires_python": ">=3.7", + "version": "3.7.1" + }, { "artifacts": [ { @@ -2074,80 +2113,299 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "c05bc6c158facb0676674b7f11fe3960db4f389718e19e62bd2b84d6205cfd24", - "url": "https://files.pythonhosted.org/packages/9d/df/aabb870a04254ceb8a406b0a4222c1b14f7fdf3d2d7633ba49364aca27f3/types_PyYAML-6.0.12.12-py3-none-any.whl" + "hash": "b845b06a1c7e54b8e5b4c683043de0d9caf205e7434b3edc678ff2411979b8f6", + "url": "https://files.pythonhosted.org/packages/b3/9a/2b75087549910ebd2be9894bfd89450668b2455094a8f2ba2b67072f15a5/types_PyYAML-6.0.12.20240311-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "334373d392fde0fdf95af5c3f1661885fa10c52167b14593eb856289e1855062", - "url": "https://files.pythonhosted.org/packages/af/48/b3bbe63a129a80911b60f57929c5b243af909bc1c9590917434bca61a4a3/types-PyYAML-6.0.12.12.tar.gz" + "hash": "a9e0f0f88dc835739b0c1ca51ee90d04ca2a897a71af79de9aec5f38cb0a5342", + "url": "https://files.pythonhosted.org/packages/0a/3c/6f4c97d9eb2b58f57fc595c105ae0a53a851747cddfb7df30f3d7192c837/types-PyYAML-6.0.12.20240311.tar.gz" } ], "project_name": "types-pyyaml", "requires_dists": [], - "requires_python": null, - "version": "6.0.12.12" + "requires_python": ">=3.8", + "version": "6.0.12.20240311" }, { "artifacts": [ { "algorithm": "sha256", - "hash": "2e2230c7bc8dd63fa3153c1c0ae335f8a368447f0582fc332f17d54f88e69027", - "url": "https://files.pythonhosted.org/packages/1b/23/126ffd0c885926fbd95eac1148093a4d87e9698a1f938be16d109e63ca64/types_requests-2.31.0.20231231-py3-none-any.whl" + "hash": "47872893d65a38e282ee9f277a4ee50d1b28bd592040df7d1fdaffdf3779937d", + "url": "https://files.pythonhosted.org/packages/05/22/21c7918c9bb842faa92fd26108e9f669c3dee9b6b239e8f45dd5f673e6cf/types_requests-2.31.0.20240311-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "0f8c0c9764773384122813548d9eea92a5c4e1f33ed54556b508968ec5065cee", - "url": "https://files.pythonhosted.org/packages/e4/93/8ec4213d536465b0454bfc0fcab4aecfed91a1bdb4f232d2ab7f1d996040/types-requests-2.31.0.20231231.tar.gz" + "hash": "b1c1b66abfb7fa79aae09097a811c4aa97130eb8831c60e47aee4ca344731ca5", + "url": "https://files.pythonhosted.org/packages/d1/bb/05c62e972a5a89318ee014aed52af921800e3bdd9a0eabfb3851d9bf0beb/types-requests-2.31.0.20240311.tar.gz" } ], "project_name": "types-requests", "requires_dists": [ "urllib3>=2" ], - "requires_python": ">=3.7", - "version": "2.31.0.20231231" + "requires_python": ">=3.8", + "version": "2.31.0.20240311" }, { "artifacts": [ { "algorithm": "sha256", - "hash": "af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd", - "url": "https://files.pythonhosted.org/packages/b7/f4/6a90020cd2d93349b442bfcb657d0dc91eee65491600b2cb1d388bc98e6b/typing_extensions-4.9.0-py3-none-any.whl" + "hash": "69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", + "url": "https://files.pythonhosted.org/packages/f9/de/dc04a3ea60b22624b51c703a84bbe0184abcd1d0b9bc8074b5d6b7ab90bb/typing_extensions-4.10.0-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", - "url": "https://files.pythonhosted.org/packages/0c/1d/eb26f5e75100d531d7399ae800814b069bc2ed2a7410834d57374d010d96/typing_extensions-4.9.0.tar.gz" + "hash": "b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb", + "url": "https://files.pythonhosted.org/packages/16/3a/0d26ce356c7465a19c9ea8814b960f8a36c3b0d07c323176620b7b483e44/typing_extensions-4.10.0.tar.gz" } ], "project_name": "typing-extensions", "requires_dists": [], "requires_python": ">=3.8", - "version": "4.9.0" + "version": "4.10.0" }, { "artifacts": [ { "algorithm": "sha256", - "hash": "55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3", - "url": "https://files.pythonhosted.org/packages/96/94/c31f58c7a7f470d5665935262ebd7455c7e4c7782eb525658d3dbf4b9403/urllib3-2.1.0-py3-none-any.whl" + "hash": "450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", + "url": "https://files.pythonhosted.org/packages/a2/73/a68704750a7679d0b6d3ad7aa8d4da8e14e151ae82e6fee774e6e0d05ec8/urllib3-2.2.1-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54", - "url": "https://files.pythonhosted.org/packages/36/dd/a6b232f449e1bc71802a5b7950dc3675d32c6dbc2a1bd6d71f065551adb6/urllib3-2.1.0.tar.gz" + "hash": "d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19", + "url": "https://files.pythonhosted.org/packages/7a/50/7fd50a27caa0652cd4caf224aa87741ea41d3265ad13f010886167cfcc79/urllib3-2.2.1.tar.gz" } ], "project_name": "urllib3", "requires_dists": [ "brotli>=1.0.9; platform_python_implementation == \"CPython\" and extra == \"brotli\"", "brotlicffi>=0.8.0; platform_python_implementation != \"CPython\" and extra == \"brotli\"", + "h2<5,>=4; extra == \"h2\"", "pysocks!=1.5.7,<2.0,>=1.5.6; extra == \"socks\"", "zstandard>=0.18.0; extra == \"zstd\"" ], "requires_python": ">=3.8", - "version": "2.1.0" + "version": "2.2.1" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1", + "url": "https://files.pythonhosted.org/packages/ff/21/abdedb4cdf6ff41ebf01a74087740a709e2edb146490e4d9beea054b0b7a/wrapt-1.16.0-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136", + "url": "https://files.pythonhosted.org/packages/07/44/359e4724a92369b88dbf09878a7cde7393cf3da885567ea898e5904049a3/wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d", + "url": "https://files.pythonhosted.org/packages/0f/16/ea627d7817394db04518f62934a5de59874b587b792300991b3c347ff5e0/wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl" + }, + { + "algorithm": "sha256", + "hash": "6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956", + "url": "https://files.pythonhosted.org/packages/0f/ef/0ecb1fa23145560431b970418dce575cfaec555ab08617d82eb92afc7ccf/wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl" + }, + { + "algorithm": "sha256", + "hash": "d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3", + "url": "https://files.pythonhosted.org/packages/11/fb/18ec40265ab81c0e82a934de04596b6ce972c27ba2592c8b53d5585e6bcd/wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c", + "url": "https://files.pythonhosted.org/packages/14/26/93a9fa02c6f257df54d7570dfe8011995138118d11939a4ecd82cb849613/wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6", + "url": "https://files.pythonhosted.org/packages/15/4e/081f59237b620a124b035f1229f55db40841a9339fdb8ef60b4decc44df9/wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72", + "url": "https://files.pythonhosted.org/packages/19/d4/cd33d3a82df73a064c9b6401d14f346e1d2fb372885f0295516ec08ed2ee/wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d", + "url": "https://files.pythonhosted.org/packages/25/62/cd284b2b747f175b5a96cbd8092b32e7369edab0644c45784871528eb852/wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8", + "url": "https://files.pythonhosted.org/packages/28/d3/4f079f649c515727c127c987b2ec2e0816b80d95784f2d28d1a57d2a1029/wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020", + "url": "https://files.pythonhosted.org/packages/32/12/e11adfde33444986135d8881b401e4de6cbb4cced046edc6b464e6ad7547/wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl" + }, + { + "algorithm": "sha256", + "hash": "6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267", + "url": "https://files.pythonhosted.org/packages/34/49/589db6fa2d5d428b71716815bca8b39196fdaeea7c247a719ed2f93b0ab4/wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81", + "url": "https://files.pythonhosted.org/packages/49/4e/5d2f6d7b57fc9956bf06e944eb00463551f7d52fc73ca35cfc4c2cdb7aed/wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf", + "url": "https://files.pythonhosted.org/packages/49/83/b40bc1ad04a868b5b5bcec86349f06c1ee1ea7afe51dc3e46131e4f39308/wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb", + "url": "https://files.pythonhosted.org/packages/4a/cc/3402bcc897978be00fef608cd9e3e39ec8869c973feeb5e1e277670e5ad2/wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl" + }, + { + "algorithm": "sha256", + "hash": "dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e", + "url": "https://files.pythonhosted.org/packages/58/43/d72e625edb5926483c9868214d25b5e7d5858ace6a80c9dfddfbadf4d8f9/wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl" + }, + { + "algorithm": "sha256", + "hash": "98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b", + "url": "https://files.pythonhosted.org/packages/62/62/30ca2405de6a20448ee557ab2cd61ab9c5900be7cbd18a2639db595f0b98/wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0", + "url": "https://files.pythonhosted.org/packages/69/21/b2ba809bafc9b6265e359f9c259c6d9a52a16cf6be20c72d95e76da609dd/wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36", + "url": "https://files.pythonhosted.org/packages/6a/d7/cfcd73e8f4858079ac59d9db1ec5a1349bc486ae8e9ba55698cc1f4a1dff/wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl" + }, + { + "algorithm": "sha256", + "hash": "72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1", + "url": "https://files.pythonhosted.org/packages/6e/52/2da48b35193e39ac53cfb141467d9f259851522d0e8c87153f0ba4205fb1/wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440", + "url": "https://files.pythonhosted.org/packages/70/7d/3dcc4a7e96f8d3e398450ec7703db384413f79bd6c0196e0e139055ce00f/wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2", + "url": "https://files.pythonhosted.org/packages/70/cc/b92e1da2cad6a9f8ee481000ece07a35e3b24e041e60ff8b850c079f0ebf/wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202", + "url": "https://files.pythonhosted.org/packages/72/b5/0c9be75f826c8e8d583a4ab312552d63d9f7c0768710146a22ac59bda4a9/wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl" + }, + { + "algorithm": "sha256", + "hash": "94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73", + "url": "https://files.pythonhosted.org/packages/7e/79/5ff0a5c54bda5aec75b36453d06be4f83d5cd4932cc84b7cb2b52cee23e2/wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389", + "url": "https://files.pythonhosted.org/packages/7f/a7/f1212ba098f3de0fd244e2de0f8791ad2539c03bef6c05a9fcb03e45b089/wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b", + "url": "https://files.pythonhosted.org/packages/92/17/224132494c1e23521868cdd57cd1e903f3b6a7ba6996b7b8f077ff8ac7fe/wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d", + "url": "https://files.pythonhosted.org/packages/95/4c/063a912e20bcef7124e0df97282a8af3ff3e4b603ce84c481d6d7346be0a/wrapt-1.16.0.tar.gz" + }, + { + "algorithm": "sha256", + "hash": "db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f", + "url": "https://files.pythonhosted.org/packages/96/e8/27ef35cf61e5147c1c3abcb89cfbb8d691b2bb8364803fcc950140bc14d8/wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl" + }, + { + "algorithm": "sha256", + "hash": "b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c", + "url": "https://files.pythonhosted.org/packages/a3/1c/226c2a4932e578a2241dcb383f425995f80224b446f439c2e112eb51c3a6/wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl" + }, + { + "algorithm": "sha256", + "hash": "49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9", + "url": "https://files.pythonhosted.org/packages/a6/9b/c2c21b44ff5b9bf14a83252a8b973fb84923764ff63db3e6dfc3895cf2e0/wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl" + }, + { + "algorithm": "sha256", + "hash": "ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4", + "url": "https://files.pythonhosted.org/packages/a8/c6/5375258add3777494671d8cec27cdf5402abd91016dee24aa2972c61fedf/wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a", + "url": "https://files.pythonhosted.org/packages/b1/e7/459a8a4f40f2fa65eb73cb3f339e6d152957932516d18d0e996c7ae2d7ae/wrapt-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537", + "url": "https://files.pythonhosted.org/packages/b6/ad/7a0766341081bfd9f18a7049e4d6d45586ae5c5bb0a640f05e2f558e849c/wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060", + "url": "https://files.pythonhosted.org/packages/b7/96/bb5e08b3d6db003c9ab219c487714c13a237ee7dcc572a555eaf1ce7dc82/wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl" + }, + { + "algorithm": "sha256", + "hash": "f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809", + "url": "https://files.pythonhosted.org/packages/c4/81/e799bf5d419f422d8712108837c1d9bf6ebe3cb2a81ad94413449543a923/wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl" + }, + { + "algorithm": "sha256", + "hash": "1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca", + "url": "https://files.pythonhosted.org/packages/c5/40/3eabe06c8dc54fada7364f34e8caa562efe3bf3f769bf3258de9c785a27f/wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl" + }, + { + "algorithm": "sha256", + "hash": "2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487", + "url": "https://files.pythonhosted.org/packages/d1/c4/8dfdc3c2f0b38be85c8d9fdf0011ebad2f54e40897f9549a356bebb63a97/wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl" + }, + { + "algorithm": "sha256", + "hash": "5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664", + "url": "https://files.pythonhosted.org/packages/da/6f/6d0b3c4983f1fc764a422989dabc268ee87d937763246cd48aa92f1eed1e/wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0", + "url": "https://files.pythonhosted.org/packages/ef/58/2fde309415b5fa98fd8f5f4a11886cbf276824c4c64d45a39da342fff6fe/wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl" + }, + { + "algorithm": "sha256", + "hash": "941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f", + "url": "https://files.pythonhosted.org/packages/ef/c6/56e718e2c58a4078518c14d97e531ef1e9e8a5c1ddafdc0d264a92be1a1a/wrapt-1.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09", + "url": "https://files.pythonhosted.org/packages/fd/03/c188ac517f402775b90d6f312955a5e53b866c964b32119f2ed76315697e/wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0", + "url": "https://files.pythonhosted.org/packages/fe/9e/d3bc95e75670ba15c5b25ecf07fc49941843e2678d777ca59339348d1c96/wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl" + } + ], + "project_name": "wrapt", + "requires_dists": [], + "requires_python": ">=3.6", + "version": "1.16.0" } ], "platform_tag": null @@ -2167,6 +2425,7 @@ "pytest<8,>7", "pyyaml~=6.0", "requests<3,>=2", + "testcontainers<5,>=3", "types-PyYAML", "types-requests" ], diff --git a/llamazure/history/conftest.py b/llamazure/history/conftest.py new file mode 100644 index 0000000..2f5f72a --- /dev/null +++ b/llamazure/history/conftest.py @@ -0,0 +1,84 @@ +import random +import string +from typing import Any, Dict + +import pytest +from psycopg.conninfo import make_conninfo +from testcontainers.core.container import DockerContainer +from testcontainers.core.waiting_utils import wait_for + +from llamazure.history.data import TSDB + + +class TimescaledbContainer(DockerContainer): + """TimescaleDB Testcontainer""" + + _PORT = 5432 + _ADMIN_USER = "llamazure" + _ADMIN_PASSWORD = "".join(random.SystemRandom().choices(string.ascii_letters + string.digits + string.punctuation, k=32)) + _DB = "llamazure" + _IMAGE = "timescale/timescaledb:latest-pg16" + + def __init__( + self, + image: str = _IMAGE, + port: int = _PORT, + admin_user: str = _ADMIN_USER, + admin_password: str = _ADMIN_PASSWORD, + db: str = _DB, + config_overrides: Dict[str, Any] = None, + **kwargs, + ): + super().__init__(image, **kwargs) + self.conf = {} + # port + self.port = port + self.with_bind_ports(container=5432, host=port) + self.db = db + self._set_conf("POSTGRES_DB", db) + self.user = admin_user + self._set_conf("POSTGRES_USER", admin_user) + self.password = admin_password + self._set_conf("POSTGRES_PASSWORD", admin_password) + + if config_overrides: + self.conf.update(config_overrides) + + self._apply_conf() + + def _set_conf(self, k, v): + self.conf[k] = v + + def _apply_conf(self): + for k, v in self.conf.items(): + self.with_env(k, v) + + @property + def connstr(self) -> str: + """Get the connstr for connecting""" + return make_conninfo( + "", + **{ + "host": "localhost", + "user": self.user, + "port": self.port, + "dbname": self.db, + "password": self.password, + }, + ) + + def try_connecting(self) -> bool: + db = TSDB(connstr=self.connstr) + if not db.ping(): + raise ConnectionError() + + def start(self): + ret = super().start() + wait_for(self.try_connecting) + return ret + + +@pytest.fixture(scope="module") +def timescaledb_container() -> TimescaledbContainer: + with TimescaledbContainer() as tsdb: + yield tsdb diff --git a/pants.toml b/pants.toml index d4e9234..e9310a0 100644 --- a/pants.toml +++ b/pants.toml @@ -36,7 +36,7 @@ interpreter_constraints = ["CPython>=3.8"] [test] use_coverage = true -extra_env_vars = ["integration_test_secrets", "INTEGRATION_PRINT_OUTPUT", "AZURE_CONFIG_DIR"] +extra_env_vars = ["integration_test_secrets", "INTEGRATION_PRINT_OUTPUT", "AZURE_CONFIG_DIR", "DOCKER_HOST"] [coverage-py] report = ["xml", "html"] diff --git a/requirements_test.txt b/requirements_test.txt index 185f4a1..d7ff48b 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,2 +1,3 @@ hypothesis>=6,<7 pytest>7,<8 +testcontainers>=3,<5 From 1bafb418a562509d471ad0c4cf3c60899ec625c3 Mon Sep 17 00:00:00 2001 From: Daniel Goldman Date: Fri, 29 Mar 2024 02:40:21 -0400 Subject: [PATCH 15/33] add integration test --- llamazure/history/__main__.py | 12 +---- llamazure/history/app.py | 10 +++++ llamazure/history/data.py | 38 +++++++++++++--- llamazure/history/integration_test.py | 64 +++++++++++++++++++++++++++ 4 files changed, 109 insertions(+), 15 deletions(-) create mode 100644 llamazure/history/app.py create mode 100644 llamazure/history/integration_test.py diff --git a/llamazure/history/__main__.py b/llamazure/history/__main__.py index f9986b8..93560e5 100644 --- a/llamazure/history/__main__.py +++ b/llamazure/history/__main__.py @@ -13,9 +13,9 @@ from llamazure.azrest.azrest import AzRest from llamazure.azrest.models import AzList from llamazure.azrest.models import Req as AzReq +from llamazure.history.app import reformat_resources_for_tresource from llamazure.history.data import DB, TSDB -from llamazure.rid import mp -from llamazure.tresource.mp import MPData, TresourceMPData +from llamazure.tresource.mp import TresourceMPData class MyEncoder(json.JSONEncoder): @@ -31,14 +31,6 @@ def default(self, o): return super().default(o) -def reformat_resources_for_tresource(resources): - """Reformat mp_resources for TresourceMPData""" - for r in resources: - path, azobj = mp.parse(r["id"]) - mpdata = MPData(azobj, r) - yield path, mpdata - - if __name__ == "__main__": tsdb = TSDB(connstr=os.environ["connstr"]) db = DB(tsdb) diff --git a/llamazure/history/app.py b/llamazure/history/app.py new file mode 100644 index 0000000..32ab4fc --- /dev/null +++ b/llamazure/history/app.py @@ -0,0 +1,10 @@ +from llamazure.rid import mp +from llamazure.tresource.mp import MPData + + +def reformat_resources_for_tresource(resources): + """Reformat mp_resources for TresourceMPData""" + for r in resources: + path, azobj = mp.parse(r["id"]) + mpdata = MPData(azobj, r) + yield path, mpdata diff --git a/llamazure/history/data.py b/llamazure/history/data.py index dec2076..93d0fd8 100644 --- a/llamazure/history/data.py +++ b/llamazure/history/data.py @@ -1,13 +1,30 @@ """Interface with the TimescaleDB""" +from __future__ import annotations + import datetime +from dataclasses import dataclass from textwrap import dedent -from typing import Any, Iterable, Optional, Tuple +from typing import Any, Dict, Iterable, List, Optional, Tuple from uuid import UUID import psycopg +from psycopg import OperationalError from psycopg.types.json import Jsonb +@dataclass(frozen=True) +class Res: + cols: Dict[str, int] + rows: List[Tuple] + + @staticmethod + def decode(cursor: psycopg.cursor, result) -> Res: + return Res( + cols={desc[0]: i for i, desc in enumerate(cursor.description)}, + rows=result, + ) + + class TSDB: """TimescaleDB connection""" @@ -37,6 +54,15 @@ def create_hypertable(self, name: str, time_col: str): """Convert a table into a hypertable""" self.exec(f"""SELECT create_hypertable('{name}', by_range('{time_col}'), if_not_exists => TRUE)""") + def ping(self) -> bool: + """Check connectivity to postgresql""" + try: + r = self.exec_returning("select version();") + assert r + return True + except OperationalError: + return False + class DB: """Store, load, and create tables""" @@ -106,10 +132,12 @@ def read_snapshot(self, time: datetime.datetime): ).fetchall() return res - def read_latest(self): + def read_latest(self) -> Res: """Read the latest information for all resources. Includes deltas.""" - return self.db.exec("""SELECT DISTINCT ON (rid) * FROM res ORDER BY rid, time DESC;""").fetchall() + cur = self.db.exec("""SELECT DISTINCT ON (rid) * FROM res ORDER BY rid, time DESC;""") + return Res.decode(cur, cur.fetchall()) - def read_at(self, time: datetime.datetime): + def read_at(self, time: datetime.datetime) -> Res: """Read the information for all resources at a point in time. Includes deltas.""" - return self.db.exec("""SELECT DISTINCT ON (rid) * FROM res WHERE time < %s ORDER BY rid, time DESC;""", (time,)).fetchall() + cur = self.db.exec("""SELECT DISTINCT ON (rid) * FROM res WHERE time < %s ORDER BY rid, time DESC;""", (time,)) + return Res.decode(cur, cur.fetchall()) diff --git a/llamazure/history/integration_test.py b/llamazure/history/integration_test.py new file mode 100644 index 0000000..a9443dd --- /dev/null +++ b/llamazure/history/integration_test.py @@ -0,0 +1,64 @@ +import datetime +from typing import Dict, cast +from uuid import UUID + +from azure.identity import DefaultAzureCredential + +from llamazure.azgraph.azgraph import Graph +from llamazure.azgraph.models import ResErr +from llamazure.azrest.azrest import AzRest +from llamazure.azrest.models import AzList +from llamazure.azrest.models import Req as AzReq +from llamazure.history.app import reformat_resources_for_tresource +from llamazure.history.conftest import TimescaledbContainer +from llamazure.history.data import DB, TSDB +from llamazure.tresource.mp import TresourceMPData + + +def test_integration(timescaledb_container: TimescaledbContainer) -> None: + """ + End-to-end test that: + - creates tables + - loads data from azure + - converts to a tresource + - inserts into the tsdb + - synthesises a delta + - inserts a delta + """ + tsdb = TSDB(connstr=timescaledb_container.connstr) + db = DB(tsdb) + db.create_tables() + + credential = DefaultAzureCredential() + g = Graph.from_credential(credential) + azr = AzRest.from_credential(credential) + + tenants = azr.call(AzReq.get("GetTenants", "/tenants", "2022-12-01", AzList[dict])) + tenant_id = UUID(tenants[0]["tenantId"]) + + resources = g.q("Resources") + if isinstance(resources, ResErr): + raise RuntimeError(ResErr) + + tree: TresourceMPData[Dict] = TresourceMPData() + tree.add_many(reformat_resources_for_tresource(resources)) + + snapshot_time = datetime.datetime.now(datetime.timezone.utc) + + db.insert_snapshot(snapshot_time, tenant_id, ((cast(str, path), mpdata.data) for path, mpdata in tree.resources.items() if mpdata.data is not None)) + + delta_q = g.q("Resources | take(1)") + if isinstance(delta_q, ResErr): + raise RuntimeError(ResErr) + delta = delta_q[0] + delta_id = delta["id"].lower() + delta_time = snapshot_time + datetime.timedelta(seconds=1) + db.insert_delta(delta_time, tenant_id, delta_id, delta) + + latest = db.read_latest() + found_resources = {e[latest.cols["rid"]]: e for e in latest.rows} + + assert delta_id in found_resources, "did not find delta in resources" + found_delta = found_resources[delta_id] + assert found_delta[latest.cols["time"]] == delta_time + assert set(tree.resources) == set(found_resources), "snapshot did not contain same resources" From 61b3b041c6c11d9b744830a1c7aa4b7cedae1bc3 Mon Sep 17 00:00:00 2001 From: Daniel Goldman Date: Fri, 29 Mar 2024 12:55:10 -0400 Subject: [PATCH 16/33] reorganise test support --- llamazure/azgraph/integration_test.py | 24 ++-------- llamazure/history/__main__.py | 64 --------------------------- llamazure/history/conftest.py | 10 +++-- llamazure/history/data.py | 5 ++- llamazure/history/integration_test.py | 5 +-- llamazure/rbac/conftest.py | 35 +++++---------- llamazure/rbac/integration_test.py | 9 ---- llamazure/test/BUILD | 3 ++ llamazure/test/__init__.py | 0 llamazure/test/credentials.py | 22 +++++++++ llamazure/test/inspect.py | 25 +++++++++++ llamazure/test/util.py | 4 ++ pyproject.toml | 1 + 13 files changed, 81 insertions(+), 126 deletions(-) delete mode 100644 llamazure/history/__main__.py create mode 100644 llamazure/test/BUILD create mode 100644 llamazure/test/__init__.py create mode 100644 llamazure/test/credentials.py create mode 100644 llamazure/test/inspect.py create mode 100644 llamazure/test/util.py diff --git a/llamazure/azgraph/integration_test.py b/llamazure/azgraph/integration_test.py index b3275f9..25a78f8 100644 --- a/llamazure/azgraph/integration_test.py +++ b/llamazure/azgraph/integration_test.py @@ -1,37 +1,19 @@ """Integration test against a real, live Azure""" # pylint: disable=redefined-outer-name -import os -from typing import Any - import pytest -import yaml -from azure.identity import ClientSecretCredential from llamazure.azgraph.azgraph import Graph from llamazure.azgraph.models import Req, Res, ResErr - - -def print_output(name: str, output: Any): - should_print = os.environ.get("INTEGRATION_PRINT_OUTPUT", "False") == "True" - if should_print: - print(name, output) +from llamazure.test.credentials import credentials +from llamazure.test.inspect import print_output @pytest.fixture() @pytest.mark.integration def graph(): """Run integration test""" - - secrets = os.environ.get("integration_test_secrets") - if not secrets: - with open("cicd/secrets.yml", mode="r", encoding="utf-8") as f: - secrets = f.read() - secrets = yaml.safe_load(secrets) - client = secrets["azgraph"] - - credential = ClientSecretCredential(tenant_id=client["tenant"], client_id=client["appId"], client_secret=client["password"]) - + credential = credentials() g = Graph.from_credential(credential) return g diff --git a/llamazure/history/__main__.py b/llamazure/history/__main__.py deleted file mode 100644 index 93560e5..0000000 --- a/llamazure/history/__main__.py +++ /dev/null @@ -1,64 +0,0 @@ -"""Demo for the `llamazure.history` application""" -import datetime -import json -import os -from dataclasses import asdict, is_dataclass -from typing import Dict, cast -from uuid import UUID - -from azure.identity import DefaultAzureCredential - -from llamazure.azgraph.azgraph import Graph -from llamazure.azgraph.models import ResErr -from llamazure.azrest.azrest import AzRest -from llamazure.azrest.models import AzList -from llamazure.azrest.models import Req as AzReq -from llamazure.history.app import reformat_resources_for_tresource -from llamazure.history.data import DB, TSDB -from llamazure.tresource.mp import TresourceMPData - - -class MyEncoder(json.JSONEncoder): - """Encoder for more types""" - - def default(self, o): - if is_dataclass(o): - return asdict(o) - if isinstance(o, datetime.datetime): - return o.isoformat() - if isinstance(o, UUID): - return str(o) - return super().default(o) - - -if __name__ == "__main__": - tsdb = TSDB(connstr=os.environ["connstr"]) - db = DB(tsdb) - db.create_tables() - - credential = DefaultAzureCredential() - g = Graph.from_credential(credential) - azr = AzRest.from_credential(credential) - - tenants = azr.call(AzReq.get("GetTenants", "/tenants", "2022-12-01", AzList[dict])) - tenant_id = UUID(tenants[0]["tenantId"]) - - resources = g.q("Resources") - if isinstance(resources, ResErr): - raise RuntimeError(ResErr) - - tree: TresourceMPData[Dict] = TresourceMPData() - tree.add_many(reformat_resources_for_tresource(resources)) - - snapshot_time = datetime.datetime.utcnow() - - db.insert_snapshot(snapshot_time, tenant_id, ((cast(str, path), mpdata.data) for path, mpdata in tree.resources.items() if mpdata.data is not None)) - - delta_q = g.q("Resources | take(1)") - if isinstance(delta_q, ResErr): - raise RuntimeError(ResErr) - delta = delta_q[0] - db.insert_delta(snapshot_time + datetime.timedelta(seconds=1), tenant_id, delta["id"].lower(), delta) - - print(json.dumps(db.read_latest(), indent=2, cls=MyEncoder)) - # print(json.dumps(db.read_snapshot(snapshot_time + datetime.timedelta(seconds=2)), indent=2, cls=MyEncoder)) diff --git a/llamazure/history/conftest.py b/llamazure/history/conftest.py index 2f5f72a..d905fe6 100644 --- a/llamazure/history/conftest.py +++ b/llamazure/history/conftest.py @@ -1,6 +1,6 @@ import random import string -from typing import Any, Dict +from typing import Any, Dict, Optional import pytest from psycopg.conninfo import make_conninfo @@ -8,6 +8,7 @@ from testcontainers.core.waiting_utils import wait_for from llamazure.history.data import TSDB +from llamazure.test.util import Fixture class TimescaledbContainer(DockerContainer): @@ -26,7 +27,7 @@ def __init__( admin_user: str = _ADMIN_USER, admin_password: str = _ADMIN_PASSWORD, db: str = _DB, - config_overrides: Dict[str, Any] = None, + config_overrides: Optional[Dict[str, Any]] = None, **kwargs, ): super().__init__(image, **kwargs) @@ -68,17 +69,20 @@ def connstr(self) -> str: ) def try_connecting(self) -> bool: + """Attempt to connect to this container""" db = TSDB(connstr=self.connstr) if not db.ping(): raise ConnectionError() + return True def start(self): + """Start the container""" ret = super().start() wait_for(self.try_connecting) return ret @pytest.fixture(scope="module") -def timescaledb_container() -> TimescaledbContainer: +def timescaledb_container() -> Fixture[TimescaledbContainer]: with TimescaledbContainer() as tsdb: yield tsdb diff --git a/llamazure/history/data.py b/llamazure/history/data.py index 93d0fd8..020a7d9 100644 --- a/llamazure/history/data.py +++ b/llamazure/history/data.py @@ -8,7 +8,7 @@ from uuid import UUID import psycopg -from psycopg import OperationalError +from psycopg import Cursor, OperationalError from psycopg.types.json import Jsonb @@ -18,7 +18,8 @@ class Res: rows: List[Tuple] @staticmethod - def decode(cursor: psycopg.cursor, result) -> Res: + def decode(cursor: Cursor, result) -> Res: + assert cursor.description is not None return Res( cols={desc[0]: i for i, desc in enumerate(cursor.description)}, rows=result, diff --git a/llamazure/history/integration_test.py b/llamazure/history/integration_test.py index a9443dd..6265ae0 100644 --- a/llamazure/history/integration_test.py +++ b/llamazure/history/integration_test.py @@ -2,8 +2,6 @@ from typing import Dict, cast from uuid import UUID -from azure.identity import DefaultAzureCredential - from llamazure.azgraph.azgraph import Graph from llamazure.azgraph.models import ResErr from llamazure.azrest.azrest import AzRest @@ -12,6 +10,7 @@ from llamazure.history.app import reformat_resources_for_tresource from llamazure.history.conftest import TimescaledbContainer from llamazure.history.data import DB, TSDB +from llamazure.test.credentials import credentials from llamazure.tresource.mp import TresourceMPData @@ -29,7 +28,7 @@ def test_integration(timescaledb_container: TimescaledbContainer) -> None: db = DB(tsdb) db.create_tables() - credential = DefaultAzureCredential() + credential = credentials() g = Graph.from_credential(credential) azr = AzRest.from_credential(credential) diff --git a/llamazure/rbac/conftest.py b/llamazure/rbac/conftest.py index a1d772d..0993953 100644 --- a/llamazure/rbac/conftest.py +++ b/llamazure/rbac/conftest.py @@ -8,29 +8,16 @@ import pytest import yaml -from azure.identity import AzureCliCredential, ClientSecretCredential, CredentialUnavailableError from llamazure.azrest.azrest import AzRest from llamazure.msgraph.msgraph import Graph from llamazure.rbac.resources import Groups, Users from llamazure.rbac.roles import RoleAssignments, RoleDefinitions, RoleOps +from llamazure.test.credentials import credentials l = logging.getLogger(__name__) -@pytest.fixture -def credential(): - """Azure credential""" - try: - cli_credential = AzureCliCredential() - cli_credential.get_token("https://management.azure.com//.default") - return cli_credential - except CredentialUnavailableError: - secrets = yaml.safe_load(os.environ.get("integration_test_secrets")) - client = secrets["azgraph"] - return ClientSecretCredential(tenant_id=client["tenant"], client_id=client["appId"], client_secret=client["password"]) - - @pytest.fixture def scopes(): """Fixture: subscriptions and ids for testing""" @@ -42,33 +29,33 @@ def scopes(): @pytest.fixture -def rds(credential) -> RoleDefinitions: +def rds() -> RoleDefinitions: """Fixture: RoleDefinitions""" - return RoleDefinitions(AzRest.from_credential(credential)) + return RoleDefinitions(AzRest.from_credential(credentials())) @pytest.fixture -def ras(credential) -> RoleAssignments: +def ras() -> RoleAssignments: """Fixture: RoleAssignments""" - return RoleAssignments(AzRest.from_credential(credential)) + return RoleAssignments(AzRest.from_credential(credentials())) @pytest.fixture -def role_ops(credential) -> RoleOps: +def role_ops() -> RoleOps: """Fixture: RoleOps""" - return RoleOps(AzRest.from_credential(credential)) + return RoleOps(AzRest.from_credential(credentials())) @pytest.fixture -def users(credential) -> Users: +def users() -> Users: """Fixture: Users""" - return Users(Graph.from_credential(credential)) + return Users(Graph.from_credential(credentials())) @pytest.fixture -def groups(credential) -> Groups: +def groups() -> Groups: """Fixture: Users""" - return Groups(Graph.from_credential(credential)) + return Groups(Graph.from_credential(credentials())) @pytest.fixture diff --git a/llamazure/rbac/integration_test.py b/llamazure/rbac/integration_test.py index 69c0031..f080617 100644 --- a/llamazure/rbac/integration_test.py +++ b/llamazure/rbac/integration_test.py @@ -1,7 +1,5 @@ """Integration tests for roles""" import logging -import os -from typing import Any import pytest @@ -16,13 +14,6 @@ attempts = 5 -def print_output(name: str, output: Any): - """Print output if requested, so we don't always print potentially sensitive information to the logs""" - should_print = os.environ.get("INTEGRATION_PRINT_OUTPUT", "False") == "True" - if should_print: - print(name, output) - - class TestRoles: """Test combined aspects of roles""" diff --git a/llamazure/test/BUILD b/llamazure/test/BUILD new file mode 100644 index 0000000..bbb78fe --- /dev/null +++ b/llamazure/test/BUILD @@ -0,0 +1,3 @@ +python_test_utils( + sources=["*.py"] +) diff --git a/llamazure/test/__init__.py b/llamazure/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/llamazure/test/credentials.py b/llamazure/test/credentials.py new file mode 100644 index 0000000..3eaa2cb --- /dev/null +++ b/llamazure/test/credentials.py @@ -0,0 +1,22 @@ +import os + +import yaml +from azure.identity import AzureCliCredential, ClientSecretCredential, CredentialUnavailableError + + +def credentials(): + """Load credentials for a Service Principal""" + secrets = os.environ.get("integration_test_secrets") + if not secrets: + with open("cicd/secrets.yml", mode="r", encoding="utf-8") as f: + secrets = f.read() + secrets = yaml.safe_load(secrets) + try: + cli_credential = AzureCliCredential() + cli_credential.get_token("https://management.azure.com//.default") + return cli_credential + except CredentialUnavailableError: + client = secrets["auth"] + + credential = ClientSecretCredential(tenant_id=client["tenant"], client_id=client["appId"], client_secret=client["password"]) + return credential diff --git a/llamazure/test/inspect.py b/llamazure/test/inspect.py new file mode 100644 index 0000000..5b3523b --- /dev/null +++ b/llamazure/test/inspect.py @@ -0,0 +1,25 @@ +import datetime +import json +import os +from dataclasses import asdict, is_dataclass +from typing import Any +from uuid import UUID + + +def print_output(name: str, output: Any): + should_print = os.environ.get("INTEGRATION_PRINT_OUTPUT", "False") == "True" + if should_print: + print(name, output) + + +class MyEncoder(json.JSONEncoder): + """Encoder for more types""" + + def default(self, o): + if is_dataclass(o): + return asdict(o) + if isinstance(o, datetime.datetime): + return o.isoformat() + if isinstance(o, UUID): + return str(o) + return super().default(o) diff --git a/llamazure/test/util.py b/llamazure/test/util.py new file mode 100644 index 0000000..04b51f6 --- /dev/null +++ b/llamazure/test/util.py @@ -0,0 +1,4 @@ +from typing import Generator, TypeVar + +T = TypeVar("T") +Fixture = Generator[T, None, None] diff --git a/pyproject.toml b/pyproject.toml index e82f226..d681545 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ module = [ "ansible.errors", "azure.cli.*", "knack.*", + "testcontainers.*", ] ignore_missing_imports = true From db75c1b5a560f54a8f4847076dfa80e9754601a1 Mon Sep 17 00:00:00 2001 From: Daniel Goldman Date: Fri, 29 Mar 2024 12:56:56 -0400 Subject: [PATCH 17/33] upgrade test container --- llamazure/history/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/llamazure/history/docker-compose.yml b/llamazure/history/docker-compose.yml index 320535b..7744737 100644 --- a/llamazure/history/docker-compose.yml +++ b/llamazure/history/docker-compose.yml @@ -2,7 +2,7 @@ version: '3' services: timescaledb: - image: timescale/timescaledb:latest-pg13 + image: timescale/timescaledb:latest-pg16 container_name: timescaledb env_file: - .env From bb89c34b4c062a79b42096033e3a53e719b10699 Mon Sep 17 00:00:00 2001 From: Daniel Goldman Date: Fri, 29 Mar 2024 12:57:08 -0400 Subject: [PATCH 18/33] upgrade codecov task version --- .github/workflows/pants.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pants.yml b/.github/workflows/pants.yml index b99cb60..ab09be5 100644 --- a/.github/workflows/pants.yml +++ b/.github/workflows/pants.yml @@ -46,7 +46,7 @@ jobs: env: integration_test_secrets: ${{secrets.integration_test_secrets}} - name: Upload test coverage to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} files: dist/coverage/python/coverage.xml From 286af052687d612fac7ffec0c9eb873a24f0278a Mon Sep 17 00:00:00 2001 From: Daniel Goldman Date: Fri, 29 Mar 2024 18:14:47 -0400 Subject: [PATCH 19/33] extract history collection operation --- llamazure/history/app.py | 10 ----- llamazure/history/collect.py | 57 +++++++++++++++++++++++++++ llamazure/history/integration_test.py | 40 +++++++++---------- pants.toml | 1 + 4 files changed, 78 insertions(+), 30 deletions(-) delete mode 100644 llamazure/history/app.py create mode 100644 llamazure/history/collect.py diff --git a/llamazure/history/app.py b/llamazure/history/app.py deleted file mode 100644 index 32ab4fc..0000000 --- a/llamazure/history/app.py +++ /dev/null @@ -1,10 +0,0 @@ -from llamazure.rid import mp -from llamazure.tresource.mp import MPData - - -def reformat_resources_for_tresource(resources): - """Reformat mp_resources for TresourceMPData""" - for r in resources: - path, azobj = mp.parse(r["id"]) - mpdata = MPData(azobj, r) - yield path, mpdata diff --git a/llamazure/history/collect.py b/llamazure/history/collect.py new file mode 100644 index 0000000..7e18a12 --- /dev/null +++ b/llamazure/history/collect.py @@ -0,0 +1,57 @@ +import datetime +from dataclasses import dataclass +from typing import Dict, Generator, Tuple, cast +from uuid import UUID + +from llamazure.azgraph import azgraph +from llamazure.azrest.azrest import AzRest +from llamazure.history.data import DB +from llamazure.rid import mp +from llamazure.tresource.mp import MPData, TresourceMPData + + +def reformat_resources_for_tresource(resources): + """Reformat mp_resources for TresourceMPData""" + for r in resources: + path, azobj = mp.parse(r["id"]) + mpdata = MPData(azobj, r) + yield path, mpdata + + +def reformat_resources_for_db(tree: TresourceMPData) -> Generator[Tuple[str, Dict], None, None]: + return ((cast(str, path), mpdata.data) for path, mpdata in tree.resources.items() if mpdata.data is not None) + + +@dataclass +class Collector: + """Load data from Azure Resource Manager and put into the DB""" + g: azgraph.Graph + azr: AzRest + db: DB + tenant_id: UUID + + def take_snapshot(self): + """Take a snapshot and insert it into the DB""" + resources = self.g.q("Resources") + if isinstance(resources, azgraph.ResErr): + raise RuntimeError(azgraph.ResErr) + + tree: TresourceMPData[Dict] = TresourceMPData() + tree.add_many(reformat_resources_for_tresource(resources)) + + self.db.insert_snapshot( + time=self.snapshot_time(), + azure_tenant=self.tenant_id, + resources=reformat_resources_for_db(tree), + ) + + def insert_deltas(self, deltas): + tree: TresourceMPData[Dict] = TresourceMPData() + tree.add_many(reformat_resources_for_tresource(deltas)) + + for rid, data in reformat_resources_for_db(tree): + self.db.insert_delta(time=self.snapshot_time(), azure_tenant=self.tenant_id, rid=rid, data=data) + + @staticmethod + def snapshot_time() -> datetime.datetime: + return datetime.datetime.now(datetime.timezone.utc) diff --git a/llamazure/history/integration_test.py b/llamazure/history/integration_test.py index 6265ae0..0ab2aa6 100644 --- a/llamazure/history/integration_test.py +++ b/llamazure/history/integration_test.py @@ -1,5 +1,6 @@ import datetime -from typing import Dict, cast +from collections import defaultdict +from typing import Dict, Set from uuid import UUID from llamazure.azgraph.azgraph import Graph @@ -7,11 +8,17 @@ from llamazure.azrest.azrest import AzRest from llamazure.azrest.models import AzList from llamazure.azrest.models import Req as AzReq -from llamazure.history.app import reformat_resources_for_tresource +from llamazure.history.app import Collector from llamazure.history.conftest import TimescaledbContainer -from llamazure.history.data import DB, TSDB +from llamazure.history.data import DB, TSDB, Res from llamazure.test.credentials import credentials -from llamazure.tresource.mp import TresourceMPData + + +def group_by_time(snapshot: Res) -> Dict[datetime.datetime, Set[str]]: + out = defaultdict(set) + for r in snapshot.rows: + out[r[snapshot.cols["time"]]].add(r[snapshot.cols["rid"]]) + return out def test_integration(timescaledb_container: TimescaledbContainer) -> None: @@ -35,29 +42,22 @@ def test_integration(timescaledb_container: TimescaledbContainer) -> None: tenants = azr.call(AzReq.get("GetTenants", "/tenants", "2022-12-01", AzList[dict])) tenant_id = UUID(tenants[0]["tenantId"]) - resources = g.q("Resources") - if isinstance(resources, ResErr): - raise RuntimeError(ResErr) - - tree: TresourceMPData[Dict] = TresourceMPData() - tree.add_many(reformat_resources_for_tresource(resources)) - - snapshot_time = datetime.datetime.now(datetime.timezone.utc) - - db.insert_snapshot(snapshot_time, tenant_id, ((cast(str, path), mpdata.data) for path, mpdata in tree.resources.items() if mpdata.data is not None)) + history = Collector(g, azr, db, tenant_id) + history.take_snapshot() delta_q = g.q("Resources | take(1)") if isinstance(delta_q, ResErr): raise RuntimeError(ResErr) - delta = delta_q[0] - delta_id = delta["id"].lower() - delta_time = snapshot_time + datetime.timedelta(seconds=1) - db.insert_delta(delta_time, tenant_id, delta_id, delta) + history.insert_deltas(delta_q) latest = db.read_latest() found_resources = {e[latest.cols["rid"]]: e for e in latest.rows} + delta_id = delta_q[0]["id"].lower() + assert delta_id in found_resources, "did not find delta in resources" found_delta = found_resources[delta_id] - assert found_delta[latest.cols["time"]] == delta_time - assert set(tree.resources) == set(found_resources), "snapshot did not contain same resources" + found_by_time = group_by_time(latest) + found_delta_time = found_delta[latest.cols["time"]] + assert found_by_time[found_delta_time] == {delta_id} + assert {e["id"].lower() for e in g.q("Resources")} == set(found_resources), "snapshot did not contain same resources" diff --git a/pants.toml b/pants.toml index e9310a0..b00a5e3 100644 --- a/pants.toml +++ b/pants.toml @@ -6,6 +6,7 @@ backend_packages = [ "pants.backend.python.lint.black", "pants.backend.python.lint.isort", "pants.backend.python.lint.flake8", + "pants.backend.python.lint.autoflake", "pants.backend.python.lint.pylint", "pants.backend.python.typecheck.mypy", "pants.backend.experimental.adhoc", From d262e0af601ff7afeae61d22f18cc53904e5ae45 Mon Sep 17 00:00:00 2001 From: Daniel Goldman Date: Sat, 30 Mar 2024 12:54:47 -0400 Subject: [PATCH 20/33] make collector multitenant by tenant_id --- llamazure/history/collect.py | 11 +++++------ llamazure/history/integration_test.py | 8 ++++---- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/llamazure/history/collect.py b/llamazure/history/collect.py index 7e18a12..28985c2 100644 --- a/llamazure/history/collect.py +++ b/llamazure/history/collect.py @@ -1,6 +1,6 @@ import datetime from dataclasses import dataclass -from typing import Dict, Generator, Tuple, cast +from typing import Dict, Generator, Tuple, cast, List from uuid import UUID from llamazure.azgraph import azgraph @@ -28,9 +28,8 @@ class Collector: g: azgraph.Graph azr: AzRest db: DB - tenant_id: UUID - def take_snapshot(self): + def take_snapshot(self, tenant_id: UUID): """Take a snapshot and insert it into the DB""" resources = self.g.q("Resources") if isinstance(resources, azgraph.ResErr): @@ -41,16 +40,16 @@ def take_snapshot(self): self.db.insert_snapshot( time=self.snapshot_time(), - azure_tenant=self.tenant_id, + azure_tenant=tenant_id, resources=reformat_resources_for_db(tree), ) - def insert_deltas(self, deltas): + def insert_deltas(self, tenant_id: UUID, deltas: List[Dict]): tree: TresourceMPData[Dict] = TresourceMPData() tree.add_many(reformat_resources_for_tresource(deltas)) for rid, data in reformat_resources_for_db(tree): - self.db.insert_delta(time=self.snapshot_time(), azure_tenant=self.tenant_id, rid=rid, data=data) + self.db.insert_delta(time=self.snapshot_time(), azure_tenant=tenant_id, rid=rid, data=data) @staticmethod def snapshot_time() -> datetime.datetime: diff --git a/llamazure/history/integration_test.py b/llamazure/history/integration_test.py index 0ab2aa6..fd39cd7 100644 --- a/llamazure/history/integration_test.py +++ b/llamazure/history/integration_test.py @@ -8,7 +8,7 @@ from llamazure.azrest.azrest import AzRest from llamazure.azrest.models import AzList from llamazure.azrest.models import Req as AzReq -from llamazure.history.app import Collector +from llamazure.history.collect import Collector from llamazure.history.conftest import TimescaledbContainer from llamazure.history.data import DB, TSDB, Res from llamazure.test.credentials import credentials @@ -42,13 +42,13 @@ def test_integration(timescaledb_container: TimescaledbContainer) -> None: tenants = azr.call(AzReq.get("GetTenants", "/tenants", "2022-12-01", AzList[dict])) tenant_id = UUID(tenants[0]["tenantId"]) - history = Collector(g, azr, db, tenant_id) - history.take_snapshot() + history = Collector(g, azr, db) + history.take_snapshot(tenant_id) delta_q = g.q("Resources | take(1)") if isinstance(delta_q, ResErr): raise RuntimeError(ResErr) - history.insert_deltas(delta_q) + history.insert_deltas(tenant_id, delta_q) latest = db.read_latest() found_resources = {e[latest.cols["rid"]]: e for e in latest.rows} From 9fe544643c6e36db7cddf99a7c0106718d544263 Mon Sep 17 00:00:00 2001 From: Daniel Goldman Date: Sat, 30 Mar 2024 13:01:33 -0400 Subject: [PATCH 21/33] make multitenant with credentials --- llamazure/history/collect.py | 16 +++++++++++----- llamazure/history/conftest.py | 15 +++++++++++++++ llamazure/history/integration_test.py | 12 +++++++++--- 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/llamazure/history/collect.py b/llamazure/history/collect.py index 28985c2..e7083e5 100644 --- a/llamazure/history/collect.py +++ b/llamazure/history/collect.py @@ -1,10 +1,10 @@ import datetime +from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import Dict, Generator, Tuple, cast, List +from typing import Dict, Generator, List, Tuple, cast from uuid import UUID from llamazure.azgraph import azgraph -from llamazure.azrest.azrest import AzRest from llamazure.history.data import DB from llamazure.rid import mp from llamazure.tresource.mp import MPData, TresourceMPData @@ -22,16 +22,22 @@ def reformat_resources_for_db(tree: TresourceMPData) -> Generator[Tuple[str, Dic return ((cast(str, path), mpdata.data) for path, mpdata in tree.resources.items() if mpdata.data is not None) +class CredentialCache(ABC): + @abstractmethod + def azgraph(self, tenant_id: UUID) -> azgraph.Graph: + """Get the azgraph.Graph instance for this tenant""" + + @dataclass class Collector: """Load data from Azure Resource Manager and put into the DB""" - g: azgraph.Graph - azr: AzRest + + credentials: CredentialCache db: DB def take_snapshot(self, tenant_id: UUID): """Take a snapshot and insert it into the DB""" - resources = self.g.q("Resources") + resources = self.credentials.azgraph(tenant_id).q("Resources") if isinstance(resources, azgraph.ResErr): raise RuntimeError(azgraph.ResErr) diff --git a/llamazure/history/conftest.py b/llamazure/history/conftest.py index d905fe6..8c8f2c0 100644 --- a/llamazure/history/conftest.py +++ b/llamazure/history/conftest.py @@ -1,13 +1,19 @@ +"""Test fixtures for History""" import random import string +from dataclasses import dataclass from typing import Any, Dict, Optional +from uuid import UUID import pytest from psycopg.conninfo import make_conninfo from testcontainers.core.container import DockerContainer from testcontainers.core.waiting_utils import wait_for +from llamazure.azgraph import azgraph +from llamazure.history.collect import CredentialCache from llamazure.history.data import TSDB +from llamazure.test.credentials import credentials from llamazure.test.util import Fixture @@ -84,5 +90,14 @@ def start(self): @pytest.fixture(scope="module") def timescaledb_container() -> Fixture[TimescaledbContainer]: + """A running TimescaledbContainer fixture""" with TimescaledbContainer() as tsdb: yield tsdb + + +@dataclass +class CredentialCacheIntegrationTest(CredentialCache): + """Load credentials from the integration test secrets""" + + def azgraph(self, tenant_id: UUID) -> azgraph.Graph: + return azgraph.Graph.from_credential(credentials()) diff --git a/llamazure/history/integration_test.py b/llamazure/history/integration_test.py index fd39cd7..d561a0c 100644 --- a/llamazure/history/integration_test.py +++ b/llamazure/history/integration_test.py @@ -9,7 +9,7 @@ from llamazure.azrest.models import AzList from llamazure.azrest.models import Req as AzReq from llamazure.history.collect import Collector -from llamazure.history.conftest import TimescaledbContainer +from llamazure.history.conftest import CredentialCacheIntegrationTest, TimescaledbContainer from llamazure.history.data import DB, TSDB, Res from llamazure.test.credentials import credentials @@ -42,7 +42,7 @@ def test_integration(timescaledb_container: TimescaledbContainer) -> None: tenants = azr.call(AzReq.get("GetTenants", "/tenants", "2022-12-01", AzList[dict])) tenant_id = UUID(tenants[0]["tenantId"]) - history = Collector(g, azr, db) + history = Collector(CredentialCacheIntegrationTest(), db) history.take_snapshot(tenant_id) delta_q = g.q("Resources | take(1)") @@ -60,4 +60,10 @@ def test_integration(timescaledb_container: TimescaledbContainer) -> None: found_by_time = group_by_time(latest) found_delta_time = found_delta[latest.cols["time"]] assert found_by_time[found_delta_time] == {delta_id} - assert {e["id"].lower() for e in g.q("Resources")} == set(found_resources), "snapshot did not contain same resources" + + resources = g.q("Resources") + if isinstance(delta_q, ResErr): + raise RuntimeError(ResErr) + assert isinstance(resources, list) + assert len(resources) == len(found_resources), "snapshot and resources had different count" + assert {e["id"].lower() for e in resources} == set(found_resources), "snapshot did not contain same resources" From 293dec30a671ddb891224aae237baf5bed5a8125 Mon Sep 17 00:00:00 2001 From: Daniel Goldman Date: Sat, 30 Mar 2024 18:14:40 -0400 Subject: [PATCH 22/33] isolatable tests with new dbs in container --- llamazure/history/conftest.py | 21 +++++++++++++++++---- llamazure/history/integration_test.py | 2 +- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/llamazure/history/conftest.py b/llamazure/history/conftest.py index 8c8f2c0..5faf6b6 100644 --- a/llamazure/history/conftest.py +++ b/llamazure/history/conftest.py @@ -5,6 +5,7 @@ from typing import Any, Dict, Optional from uuid import UUID +import psycopg import pytest from psycopg.conninfo import make_conninfo from testcontainers.core.container import DockerContainer @@ -60,8 +61,7 @@ def _apply_conf(self): for k, v in self.conf.items(): self.with_env(k, v) - @property - def connstr(self) -> str: + def connstr(self, db: str) -> str: """Get the connstr for connecting""" return make_conninfo( "", @@ -69,14 +69,14 @@ def connstr(self) -> str: "host": "localhost", "user": self.user, "port": self.port, - "dbname": self.db, + "dbname": db, "password": self.password, }, ) def try_connecting(self) -> bool: """Attempt to connect to this container""" - db = TSDB(connstr=self.connstr) + db = TSDB(connstr=self.connstr(self.db)) if not db.ping(): raise ConnectionError() return True @@ -87,6 +87,19 @@ def start(self): wait_for(self.try_connecting) return ret + def new_db(self, db_name: Optional[str] = None) -> str: + """Create a new DB and return the connection info""" + if db_name is None: + db_name = "".join(random.choice(string.ascii_lowercase) for i in range(10)) + + with psycopg.connect(self.connstr(self.db), autocommit=True) as conn: + cur = conn.cursor() + cur.execute(f"""CREATE DATABASE {db_name};""") + cur.execute(f"""GRANT ALL PRIVILEGES ON DATABASE {db_name} TO {self.user}""") + conn.commit() + + return self.connstr(db_name) + @pytest.fixture(scope="module") def timescaledb_container() -> Fixture[TimescaledbContainer]: diff --git a/llamazure/history/integration_test.py b/llamazure/history/integration_test.py index d561a0c..316181c 100644 --- a/llamazure/history/integration_test.py +++ b/llamazure/history/integration_test.py @@ -31,7 +31,7 @@ def test_integration(timescaledb_container: TimescaledbContainer) -> None: - synthesises a delta - inserts a delta """ - tsdb = TSDB(connstr=timescaledb_container.connstr) + tsdb = TSDB(connstr=timescaledb_container.new_db()) db = DB(tsdb) db.create_tables() From 5e05af093402d6100466f2ea3a732c6f0a370f3c Mon Sep 17 00:00:00 2001 From: Daniel Goldman Date: Sat, 30 Mar 2024 18:15:06 -0400 Subject: [PATCH 23/33] use same time for inserting multiple deltas --- llamazure/history/collect.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/llamazure/history/collect.py b/llamazure/history/collect.py index e7083e5..935da15 100644 --- a/llamazure/history/collect.py +++ b/llamazure/history/collect.py @@ -54,8 +54,9 @@ def insert_deltas(self, tenant_id: UUID, deltas: List[Dict]): tree: TresourceMPData[Dict] = TresourceMPData() tree.add_many(reformat_resources_for_tresource(deltas)) + request_snapshot_time = self.snapshot_time() for rid, data in reformat_resources_for_db(tree): - self.db.insert_delta(time=self.snapshot_time(), azure_tenant=tenant_id, rid=rid, data=data) + self.db.insert_delta(time=request_snapshot_time, azure_tenant=tenant_id, rid=rid, data=data) @staticmethod def snapshot_time() -> datetime.datetime: From 3c7a454b8de72168002acfdf23bab30026e652a2 Mon Sep 17 00:00:00 2001 From: Daniel Goldman Date: Sat, 30 Mar 2024 18:28:21 -0400 Subject: [PATCH 24/33] automatically initialise tables for tests --- llamazure/history/conftest.py | 8 +++++--- llamazure/history/integration_test.py | 6 ++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/llamazure/history/conftest.py b/llamazure/history/conftest.py index 5faf6b6..03d1527 100644 --- a/llamazure/history/conftest.py +++ b/llamazure/history/conftest.py @@ -13,7 +13,7 @@ from llamazure.azgraph import azgraph from llamazure.history.collect import CredentialCache -from llamazure.history.data import TSDB +from llamazure.history.data import DB, TSDB from llamazure.test.credentials import credentials from llamazure.test.util import Fixture @@ -87,7 +87,7 @@ def start(self): wait_for(self.try_connecting) return ret - def new_db(self, db_name: Optional[str] = None) -> str: + def new_db(self, db_name: Optional[str] = None) -> DB: """Create a new DB and return the connection info""" if db_name is None: db_name = "".join(random.choice(string.ascii_lowercase) for i in range(10)) @@ -98,7 +98,9 @@ def new_db(self, db_name: Optional[str] = None) -> str: cur.execute(f"""GRANT ALL PRIVILEGES ON DATABASE {db_name} TO {self.user}""") conn.commit() - return self.connstr(db_name) + db = DB(TSDB(connstr=(self.connstr(db_name)))) + db.create_tables() + return db @pytest.fixture(scope="module") diff --git a/llamazure/history/integration_test.py b/llamazure/history/integration_test.py index 316181c..a44509d 100644 --- a/llamazure/history/integration_test.py +++ b/llamazure/history/integration_test.py @@ -10,7 +10,7 @@ from llamazure.azrest.models import Req as AzReq from llamazure.history.collect import Collector from llamazure.history.conftest import CredentialCacheIntegrationTest, TimescaledbContainer -from llamazure.history.data import DB, TSDB, Res +from llamazure.history.data import Res from llamazure.test.credentials import credentials @@ -31,9 +31,7 @@ def test_integration(timescaledb_container: TimescaledbContainer) -> None: - synthesises a delta - inserts a delta """ - tsdb = TSDB(connstr=timescaledb_container.new_db()) - db = DB(tsdb) - db.create_tables() + db = timescaledb_container.new_db() credential = credentials() g = Graph.from_credential(credential) From f64252b6151d0b5ab54f138e4b7cc1b324fd91bb Mon Sep 17 00:00:00 2001 From: Daniel Goldman Date: Sat, 30 Mar 2024 18:29:10 -0400 Subject: [PATCH 25/33] rough-in fastapi frontend --- cicd/python-default.lock | 228 +++++++++++++++++++++++++++++++++++++-- llamazure/history/app.py | 99 +++++++++++++++++ requirements.txt | 3 + 3 files changed, 322 insertions(+), 8 deletions(-) create mode 100644 llamazure/history/app.py diff --git a/cicd/python-default.lock b/cicd/python-default.lock index 97ba095..6f61962 100644 --- a/cicd/python-default.lock +++ b/cicd/python-default.lock @@ -11,16 +11,19 @@ // "generated_with_requirements": [ // "azure-identity<2.0.0,>=1.4.0", // "click~=8.0", +// "fastapi~=0.110.0", // "hypothesis<7,>=6", // "mypy>=1.5.0", // "psycopg~=3.1", +// "pydantic-settings>=2.0", // "pydantic>=2.0", // "pytest<8,>7", // "pyyaml~=6.0", // "requests<3,>=2", // "testcontainers<5,>=3", // "types-PyYAML", -// "types-requests" +// "types-requests", +// "uvicorn~=0.29" // ], // "manylinux": "manylinux2014", // "requirement_constraints": [], @@ -58,6 +61,43 @@ "requires_python": ">=3.8", "version": "0.6.0" }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8", + "url": "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6", + "url": "https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz" + } + ], + "project_name": "anyio", + "requires_dists": [ + "Sphinx>=7; extra == \"doc\"", + "anyio[trio]; extra == \"test\"", + "coverage[toml]>=7; extra == \"test\"", + "exceptiongroup>=1.0.2; python_version < \"3.11\"", + "exceptiongroup>=1.2.0; extra == \"test\"", + "hypothesis>=4.0; extra == \"test\"", + "idna>=2.8", + "packaging; extra == \"doc\"", + "psutil>=5.9; extra == \"test\"", + "pytest-mock>=3.6.1; extra == \"test\"", + "pytest>=7.0; extra == \"test\"", + "sniffio>=1.1", + "sphinx-autodoc-typehints>=1.2.0; extra == \"doc\"", + "sphinx-rtd-theme; extra == \"doc\"", + "trio>=0.23; extra == \"trio\"", + "trustme; extra == \"test\"", + "typing-extensions>=4.1; python_version < \"3.11\"", + "uvloop>=0.17; (platform_python_implementation == \"CPython\" and platform_system != \"Windows\") and extra == \"test\"" + ], + "requires_python": ">=3.8", + "version": "4.3.0" + }, { "artifacts": [ { @@ -1002,6 +1042,59 @@ "requires_python": ">=3.7", "version": "1.2.0" }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "87a1f6fb632a218222c5984be540055346a8f5d8a68e8f6fb647b1dc9934de4b", + "url": "https://files.pythonhosted.org/packages/f0/f7/ea860cb8aa18e326f411e32ab537424690a53db20de6bad73d70da611fae/fastapi-0.110.0-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "266775f0dcc95af9d3ef39bad55cff525329a931d5fd51930aadd4f428bf7ff3", + "url": "https://files.pythonhosted.org/packages/61/53/326977db62bf34bbdfc64acb9414e1881af7ea14e8a062fd1c11a8697616/fastapi-0.110.0.tar.gz" + } + ], + "project_name": "fastapi", + "requires_dists": [ + "email-validator>=2.0.0; extra == \"all\"", + "httpx>=0.23.0; extra == \"all\"", + "itsdangerous>=1.1.0; extra == \"all\"", + "jinja2>=2.11.2; extra == \"all\"", + "orjson>=3.2.1; extra == \"all\"", + "pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4", + "pydantic-extra-types>=2.0.0; extra == \"all\"", + "pydantic-settings>=2.0.0; extra == \"all\"", + "python-multipart>=0.0.7; extra == \"all\"", + "pyyaml>=5.3.1; extra == \"all\"", + "starlette<0.37.0,>=0.36.3", + "typing-extensions>=4.8.0", + "ujson!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,>=4.0.1; extra == \"all\"", + "uvicorn[standard]>=0.12.0; extra == \"all\"" + ], + "requires_python": ">=3.8", + "version": "0.110.0" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", + "url": "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", + "url": "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz" + } + ], + "project_name": "h11", + "requires_dists": [ + "typing-extensions; python_version < \"3.8\"" + ], + "requires_python": ">=3.7", + "version": "0.14.0" + }, { "artifacts": [ { @@ -1401,19 +1494,19 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", - "url": "https://files.pythonhosted.org/packages/62/d5/5f610ebe421e85889f2e55e33b7f9a6795bd982198517d912eb1c76e1a53/pycparser-2.21-py2.py3-none-any.whl" + "hash": "c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", + "url": "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206", - "url": "https://files.pythonhosted.org/packages/5e/0b/95d387f5f4433cb0f53ff7ad859bd2c6051051cebbb564f139a999ab46de/pycparser-2.21.tar.gz" + "hash": "491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", + "url": "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz" } ], "project_name": "pycparser", "requires_dists": [], - "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7", - "version": "2.21" + "requires_python": ">=3.8", + "version": "2.22" }, { "artifacts": [ @@ -1773,6 +1866,29 @@ "requires_python": ">=3.8", "version": "2.16.3" }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "0235391d26db4d2190cb9b31051c4b46882d28a51533f97440867f012d4da091", + "url": "https://files.pythonhosted.org/packages/99/ee/24ec87e3a91426497c5a2b9880662d19cfd640342d477334ebc60fc2c276/pydantic_settings-2.2.1-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "00b9f6a5e95553590434c0fa01ead0b216c3e10bc54ae02e37f359948643c5ed", + "url": "https://files.pythonhosted.org/packages/00/a4/89191c3cce6e6f79b734bfe81d3a8f176d21b57b034689cfbdc57d61c412/pydantic_settings-2.2.1.tar.gz" + } + ], + "project_name": "pydantic-settings", + "requires_dists": [ + "pydantic>=2.3.0", + "python-dotenv>=0.21.0", + "pyyaml>=6.0.1; extra == \"yaml\"", + "tomli>=2.0.1; extra == \"toml\"" + ], + "requires_python": ">=3.8", + "version": "2.2.1" + }, { "artifacts": [ { @@ -1841,6 +1957,26 @@ "requires_python": ">=3.7", "version": "7.4.4" }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", + "url": "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", + "url": "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz" + } + ], + "project_name": "python-dotenv", + "requires_dists": [ + "click>=5.0; extra == \"cli\"" + ], + "requires_python": ">=3.8", + "version": "1.0.1" + }, { "artifacts": [ { @@ -2037,6 +2173,24 @@ "requires_python": "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7", "version": "1.16.0" }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", + "url": "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", + "url": "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz" + } + ], + "project_name": "sniffio", + "requires_dists": [], + "requires_python": ">=3.7", + "version": "1.3.1" + }, { "artifacts": [ { @@ -2055,6 +2209,32 @@ "requires_python": null, "version": "2.4.0" }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "13d429aa93a61dc40bf503e8c801db1f1bca3dc706b10ef2434a36123568f044", + "url": "https://files.pythonhosted.org/packages/eb/f7/372e3953b6e6fbfe0b70a1bb52612eae16e943f4288516480860fcd4ac41/starlette-0.36.3-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "90a671733cfb35771d8cc605e0b679d23b992f8dcfad48cc60b38cb29aeb7080", + "url": "https://files.pythonhosted.org/packages/be/47/1bba49d42d63f4453f0a64a20acbf2d0bd2f5a8cde6a166ee66c074a08f8/starlette-0.36.3.tar.gz" + } + ], + "project_name": "starlette", + "requires_dists": [ + "anyio<5,>=3.4.0", + "httpx>=0.22.0; extra == \"full\"", + "itsdangerous; extra == \"full\"", + "jinja2; extra == \"full\"", + "python-multipart>=0.0.7; extra == \"full\"", + "pyyaml; extra == \"full\"", + "typing-extensions>=3.10.0; python_version < \"3.10\"" + ], + "requires_python": ">=3.8", + "version": "0.36.3" + }, { "artifacts": [ { @@ -2189,6 +2369,35 @@ "requires_python": ">=3.8", "version": "2.2.1" }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "2c2aac7ff4f4365c206fd773a39bf4ebd1047c238f8b8268ad996829323473de", + "url": "https://files.pythonhosted.org/packages/73/f5/cbb16fcbe277c1e0b8b3ddd188f2df0e0947f545c49119b589643632d156/uvicorn-0.29.0-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "6a69214c0b6a087462412670b3ef21224fa48cae0e452b5883e8e8bdfdd11dd0", + "url": "https://files.pythonhosted.org/packages/49/8d/5005d39cd79c9ae87baf7d7aafdcdfe0b13aa69d9a1e3b7f1c984a2ac6d2/uvicorn-0.29.0.tar.gz" + } + ], + "project_name": "uvicorn", + "requires_dists": [ + "click>=7.0", + "colorama>=0.4; sys_platform == \"win32\" and extra == \"standard\"", + "h11>=0.8", + "httptools>=0.5.0; extra == \"standard\"", + "python-dotenv>=0.13; extra == \"standard\"", + "pyyaml>=5.1; extra == \"standard\"", + "typing-extensions>=4.0; python_version < \"3.11\"", + "uvloop!=0.15.0,!=0.15.1,>=0.14.0; (sys_platform != \"win32\" and (sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\")) and extra == \"standard\"", + "watchfiles>=0.13; extra == \"standard\"", + "websockets>=10.4; extra == \"standard\"" + ], + "requires_python": ">=3.8", + "version": "0.29.0" + }, { "artifacts": [ { @@ -2418,16 +2627,19 @@ "requirements": [ "azure-identity<2.0.0,>=1.4.0", "click~=8.0", + "fastapi~=0.110.0", "hypothesis<7,>=6", "mypy>=1.5.0", "psycopg~=3.1", + "pydantic-settings>=2.0", "pydantic>=2.0", "pytest<8,>7", "pyyaml~=6.0", "requests<3,>=2", "testcontainers<5,>=3", "types-PyYAML", - "types-requests" + "types-requests", + "uvicorn~=0.29" ], "requires_python": [ ">=3.8" diff --git a/llamazure/history/app.py b/llamazure/history/app.py new file mode 100644 index 0000000..c85c2b9 --- /dev/null +++ b/llamazure/history/app.py @@ -0,0 +1,99 @@ +"""The llamazure.history application, a webserver to collect and present the history of Azure tenancies""" +from __future__ import annotations + +import datetime +from typing import List +from uuid import UUID + +from azure.identity import DefaultAzureCredential +from fastapi import Depends, FastAPI +from pydantic import BaseModel +from pydantic_settings import BaseSettings, SettingsConfigDict + +from llamazure.azgraph import azgraph +from llamazure.history.collect import Collector, CredentialCache +from llamazure.history.data import DB, TSDB, Res + + +class CredentialCacheDefault(CredentialCache): + """Load Azure credentials with default loader""" + + @staticmethod + def credential(): + """Get the Default Azure credential""" + return DefaultAzureCredential() + + def azgraph(self, tenant_id: UUID) -> azgraph.Graph: + return azgraph.Graph.from_credential(self.credential()) + + +class Settings(BaseSettings): + """Settings for llamazure.history""" + + model_config = SettingsConfigDict(env_nested_delimiter="__") + + class DB(BaseModel): + """Settings for DB""" + + connstr: str + + db: Settings.DB + + +settings = Settings() + + +def get_collector() -> Collector: + """FastAPI Dependency for Collector""" + yield Collector( + CredentialCacheDefault(), + DB(TSDB(settings.db.connstr)), + ) + + +def get_db() -> DB: + """FastAPI Dependency for DB""" + yield DB(TSDB(settings.db.connstr)) + + +app = FastAPI() + + +@app.post("/collect/snapshots") +async def collect_snapshot(tenant_id: UUID, collector: Collector = Depends(get_collector)): + """Dispatch the collection of a snapshot""" + collector.take_snapshot(tenant_id) + + +@app.post("/collect/delta") +async def collect_delta(tenant_id: UUID, delta: dict, collector: Collector = Depends(get_collector)): + """Insert a single delta""" + collector.insert_deltas(tenant_id, [delta]) + + +@app.post("/collect/deltas") +async def collect_deltas(tenant_id: UUID, deltas: List[dict], collector: Collector = Depends(get_collector)): + """Insert multiple deltas""" + collector.insert_deltas(tenant_id, deltas) + + +@app.get("/history") +async def read_history(db: DB = Depends(get_db), at: datetime.datetime = None) -> Res: + """Read history at a point in time""" + if at is None: + return db.read_latest() + else: + return db.read_at(at) + + +@app.get("/ping") +async def ping() -> str: + """PING this service for up check""" + now = datetime.datetime.now(datetime.timezone.utc).isoformat() + return f"PONG {now}" + + +@app.post("/admin/init_db") +async def init_db(db: DB = Depends(get_db)): + """Initialize the tables in the database""" + db.create_tables() diff --git a/requirements.txt b/requirements.txt index a189cec..332c69b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,9 @@ azure-identity>=1.4.0,<2.0.0 click~=8.0 +fastapi~=0.110.0 psycopg~=3.1 pydantic>=2.0 +pydantic-settings>=2.0 pyyaml~=6.0 requests>=2,<3 +uvicorn~=0.29 From 727b2df0992bc75963c01ba6fb17d50db9d10d25 Mon Sep 17 00:00:00 2001 From: Daniel Goldman Date: Sat, 30 Mar 2024 22:35:51 -0400 Subject: [PATCH 26/33] add some tests for snapshots --- llamazure/history/conftest.py | 84 ++++++++++++++++++++++++++++++++-- llamazure/history/data.py | 4 +- llamazure/history/data_test.py | 44 ++++++++++++++++++ 3 files changed, 127 insertions(+), 5 deletions(-) create mode 100644 llamazure/history/data_test.py diff --git a/llamazure/history/conftest.py b/llamazure/history/conftest.py index 03d1527..19b35c1 100644 --- a/llamazure/history/conftest.py +++ b/llamazure/history/conftest.py @@ -1,8 +1,10 @@ """Test fixtures for History""" +import datetime import random import string -from dataclasses import dataclass -from typing import Any, Dict, Optional +import uuid +from dataclasses import dataclass, field +from typing import Any, Callable, Dict, List, Optional, Tuple, Union from uuid import UUID import psycopg @@ -13,7 +15,7 @@ from llamazure.azgraph import azgraph from llamazure.history.collect import CredentialCache -from llamazure.history.data import DB, TSDB +from llamazure.history.data import DB, TSDB, Res from llamazure.test.credentials import credentials from llamazure.test.util import Fixture @@ -110,9 +112,85 @@ def timescaledb_container() -> Fixture[TimescaledbContainer]: yield tsdb +@pytest.fixture(scope="function") +def newdb(timescaledb_container: TimescaledbContainer) -> DB: + """Fixture for DB""" + return timescaledb_container.new_db() + + +@pytest.fixture +def now() -> datetime.datetime: + """Fixture for current time""" + return datetime.datetime.now(tz=datetime.timezone.utc) + + @dataclass class CredentialCacheIntegrationTest(CredentialCache): """Load credentials from the integration test secrets""" def azgraph(self, tenant_id: UUID) -> azgraph.Graph: return azgraph.Graph.from_credential(credentials()) + + +@dataclass +class FakeDataFactory: + """ + All the fake data you need. + + Generator functions accept a kw-only parameter `idx`. + This key is used to store the value generated. + Re-invoke this function to retrieve the generated value + """ + + tenants: dict = field(default_factory=dict) + resources: dict = field(default_factory=dict) + snapshots: dict = field(default_factory=dict) + + def _get_or_gen(self, db: dict, gen: Callable, idx: Union[str, int]): + if idx in db: + return db[idx] + else: + v = gen() + db[idx] = v + return v + + def tenant(self, *, idx: Union[str, int]) -> UUID: + """A fake tenant""" + return self._get_or_gen(self.tenants, uuid.uuid4, idx) + + def _resource(self, rev=0) -> dict: + return {"id": "/subscriptions/s0/fakeResource/", "k0": rev} + + def resource(self, rev=0, *, idx: Union[str, int]) -> dict: + """A single fake resource""" + return self._get_or_gen(self.resources, lambda: self._resource(rev), idx) + + def snapshot(self, i=4, *, idx: Union[str, int]) -> List[Tuple[str, dict]]: + """A fake snapshot""" + + def _mk_snapshot(): + resources = [self._resource(rev) for rev in range(0, i)] + return [(e["id"], e) for e in resources] + + return self._get_or_gen(self.snapshots, _mk_snapshot, idx) + + def res2snapshot(self, res: Res) -> List[Tuple[str, dict]]: + """Convert a Res into the original snapshot""" + return [(e[res.cols["rid"]], e[res.cols["data"]]) for e in res.rows] + + def compare_snapshot(self, r: Res, *idxs) -> bool: + """Assert that a result of reading a snapshot is the same as the snapshot inserted""" + merged = sum((self.snapshot(idx=idx) for idx in idxs), start=[]) + assert self.res2snapshot(r) == merged + return True + + def assert_snapshot_at(self, r: Res, at: datetime.datetime) -> bool: + """Assert that a snapshot happened at the given time""" + assert all(e[r.cols["time"]] == at for e in r.rows) + return True + + +@pytest.fixture(scope="function") +def fdf() -> FakeDataFactory: + """Fixture for FakeDataFactory""" + return FakeDataFactory() diff --git a/llamazure/history/data.py b/llamazure/history/data.py index 020a7d9..0926b7d 100644 --- a/llamazure/history/data.py +++ b/llamazure/history/data.py @@ -130,8 +130,8 @@ def read_snapshot(self, time: datetime.datetime): """ ), (time,), - ).fetchall() - return res + ) + return Res.decode(cur, cur.fetchall()) def read_latest(self) -> Res: """Read the latest information for all resources. Includes deltas.""" diff --git a/llamazure/history/data_test.py b/llamazure/history/data_test.py new file mode 100644 index 0000000..1955dee --- /dev/null +++ b/llamazure/history/data_test.py @@ -0,0 +1,44 @@ +"""Tests for DB functions""" +import datetime + +from llamazure.history.conftest import FakeDataFactory +from llamazure.history.data import DB + + +class TestSnapshotsSingleTenant: + """Test for reading snapshots where all snapshots are the same tenant""" + + def test_single(self, fdf: FakeDataFactory, newdb: DB, now: datetime.datetime): + """Test reading a single snapshot""" + newdb.insert_snapshot(now, fdf.tenant(idx=0), fdf.snapshot(idx=0)) + r = newdb.read_snapshot(now) + assert fdf.compare_snapshot(r, 0) + + def test_in_past(self, fdf: FakeDataFactory, newdb: DB, now: datetime): + """Test that we read the snapshot even if we're after the latest one""" + newdb.insert_snapshot(now, fdf.tenant(idx=0), fdf.snapshot(idx=0)) + r = newdb.read_snapshot(now + datetime.timedelta(days=1)) + assert fdf.compare_snapshot(r, 0) + + def test_multiple(self, fdf: FakeDataFactory, newdb: DB, now: datetime): + """Test with multiple snapshots""" + newdb.insert_snapshot(now, fdf.tenant(idx=0), fdf.snapshot(idx=0)) + time_2 = now + datetime.timedelta(days=2) + newdb.insert_snapshot(time_2, fdf.tenant(idx=0), fdf.snapshot(idx=1)) + + r_between = newdb.read_snapshot(now + datetime.timedelta(days=1)) + assert fdf.compare_snapshot(r_between, 0) + assert fdf.assert_snapshot_at(r_between, now) + + r_after = newdb.read_snapshot(now + datetime.timedelta(days=3)) + assert fdf.compare_snapshot(r_after, 1) + assert fdf.assert_snapshot_at(r_after, time_2) + + +class TestSnapshotMultiTenant: + def test_single_per_tenant(self, fdf: FakeDataFactory, newdb: DB, now: datetime): + newdb.insert_snapshot(now, fdf.tenant(idx=0), fdf.snapshot(idx=0)) + newdb.insert_snapshot(now, fdf.tenant(idx=1), fdf.snapshot(idx=1)) + + r = newdb.read_snapshot(now) + assert fdf.compare_snapshot(r, 0, 1) From 6d7e9b2e4df83c72b7d5d24790c74a925be85025 Mon Sep 17 00:00:00 2001 From: Daniel Goldman Date: Sat, 30 Mar 2024 22:36:15 -0400 Subject: [PATCH 27/33] fix read_snapshot not being multitenant --- llamazure/history/data.py | 62 ++++++++++++++++++++++++++++++---- llamazure/history/data_test.py | 1 + 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/llamazure/history/data.py b/llamazure/history/data.py index 0926b7d..ac084cf 100644 --- a/llamazure/history/data.py +++ b/llamazure/history/data.py @@ -14,11 +14,13 @@ @dataclass(frozen=True) class Res: + """Result of a Postgres query""" cols: Dict[str, int] rows: List[Tuple] @staticmethod def decode(cursor: Cursor, result) -> Res: + """Convert a cursor into a Res""" assert cursor.description is not None return Res( cols={desc[0]: i for i, desc in enumerate(cursor.description)}, @@ -118,15 +120,63 @@ def insert_delta(self, time: datetime.datetime, azure_tenant: UUID, rid: str, da """Insert a single delta into the DB""" return self.insert_resource(time, azure_tenant, None, rid, data) - def read_snapshot(self, time: datetime.datetime): - """Read a complete snapshot. Does not include any deltas""" - res = self.db.exec( + def list_snapshots(self) -> Res: + """List snapshots""" + cur = self.db.exec("""SELECT * FROM snapshot;""") + return Res.decode(cur, cur.fetchall()) + + def read_snapshot(self, time: datetime.datetime) -> Res: + """ + Read a complete snapshot. Does not include any deltas + + ```prql + let latest_snapshots = ( + from snapshot + filter s"time" <= $1 + group azure_tenant ( + sort { - s"time" } + take 1 + ) + select id + ) + + from res + join side:inner latest_snapshots (res.snapshot == latest_snapshots.id) + ``` + """ + + cur = self.db.exec( dedent( """\ - WITH LatestSnapshot AS ( - SELECT id FROM snapshot WHERE time < %s ORDER BY time DESC LIMIT 1 + WITH table_0 AS ( + SELECT + id, + ROW_NUMBER() OVER ( + PARTITION BY azure_tenant + ORDER BY + time DESC + ) AS _expr_0 + FROM + snapshot + WHERE + time <= %s + ), + latest_snapshots AS ( + SELECT + id + FROM + table_0 + WHERE + _expr_0 <= 1 ) - SELECT * FROM res WHERE snapshot = (SELECT id FROM LatestSnapshot); + SELECT + res.*, + latest_snapshots.id + FROM + res + JOIN latest_snapshots ON res.snapshot = latest_snapshots.id + + -- Generated by PRQL compiler version:0.11.3 (https://prql-lang.org) """ ), (time,), diff --git a/llamazure/history/data_test.py b/llamazure/history/data_test.py index 1955dee..8442af2 100644 --- a/llamazure/history/data_test.py +++ b/llamazure/history/data_test.py @@ -37,6 +37,7 @@ def test_multiple(self, fdf: FakeDataFactory, newdb: DB, now: datetime): class TestSnapshotMultiTenant: def test_single_per_tenant(self, fdf: FakeDataFactory, newdb: DB, now: datetime): + """Test that the latest is retrieved for each of multiple tenants""" newdb.insert_snapshot(now, fdf.tenant(idx=0), fdf.snapshot(idx=0)) newdb.insert_snapshot(now, fdf.tenant(idx=1), fdf.snapshot(idx=1)) From c3a8688698d1b6d2ddee5273afca95aa1ae9a1bb Mon Sep 17 00:00:00 2001 From: Daniel Goldman Date: Sat, 30 Mar 2024 22:54:57 -0400 Subject: [PATCH 28/33] fix test concurrency with testcontainer port binding --- llamazure/history/conftest.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/llamazure/history/conftest.py b/llamazure/history/conftest.py index 19b35c1..b66a07e 100644 --- a/llamazure/history/conftest.py +++ b/llamazure/history/conftest.py @@ -32,7 +32,6 @@ class TimescaledbContainer(DockerContainer): def __init__( self, image: str = _IMAGE, - port: int = _PORT, admin_user: str = _ADMIN_USER, admin_password: str = _ADMIN_PASSWORD, db: str = _DB, @@ -42,8 +41,7 @@ def __init__( super().__init__(image, **kwargs) self.conf = {} # port - self.port = port - self.with_bind_ports(container=5432, host=port) + self.with_exposed_ports(self._PORT) self.db = db self._set_conf("POSTGRES_DB", db) self.user = admin_user @@ -56,6 +54,10 @@ def __init__( self._apply_conf() + @property + def port(self) -> int: + return int(self.get_exposed_port(self._PORT)) + def _set_conf(self, k, v): self.conf[k] = v From f086fb138a8b83328f1933bdfb42157f064054af Mon Sep 17 00:00:00 2001 From: Daniel Goldman Date: Sat, 30 Mar 2024 23:33:02 -0400 Subject: [PATCH 29/33] fix fixture loading especially in non-integration tests --- .flake8 | 2 ++ llamazure/azgraph/integration_test.py | 4 ++-- llamazure/history/conftest.py | 4 ++-- llamazure/history/data.py | 3 ++- llamazure/history/data_test.py | 6 +++--- llamazure/history/integration_test.py | 11 +++++++++-- llamazure/rbac/conftest.py | 23 ++++++++++++----------- llamazure/test/BUILD | 4 +--- llamazure/test/conftest.py | 9 +++++++++ llamazure/test/credentials.py | 2 +- pyproject.toml | 4 ++++ 11 files changed, 47 insertions(+), 25 deletions(-) create mode 100644 llamazure/test/conftest.py diff --git a/.flake8 b/.flake8 index f5adf03..f944589 100644 --- a/.flake8 +++ b/.flake8 @@ -1,3 +1,5 @@ [flake8] ignore = W191,W503,E203,E741 max-line-length = 240 +per-file-ignores = + **/conftest.py:F811 \ No newline at end of file diff --git a/llamazure/azgraph/integration_test.py b/llamazure/azgraph/integration_test.py index 25a78f8..0ad767c 100644 --- a/llamazure/azgraph/integration_test.py +++ b/llamazure/azgraph/integration_test.py @@ -5,7 +5,7 @@ from llamazure.azgraph.azgraph import Graph from llamazure.azgraph.models import Req, Res, ResErr -from llamazure.test.credentials import credentials +from llamazure.test.credentials import load_credentials from llamazure.test.inspect import print_output @@ -13,7 +13,7 @@ @pytest.mark.integration def graph(): """Run integration test""" - credential = credentials() + credential = load_credentials() g = Graph.from_credential(credential) return g diff --git a/llamazure/history/conftest.py b/llamazure/history/conftest.py index b66a07e..693aa91 100644 --- a/llamazure/history/conftest.py +++ b/llamazure/history/conftest.py @@ -16,7 +16,7 @@ from llamazure.azgraph import azgraph from llamazure.history.collect import CredentialCache from llamazure.history.data import DB, TSDB, Res -from llamazure.test.credentials import credentials +from llamazure.test.credentials import load_credentials from llamazure.test.util import Fixture @@ -131,7 +131,7 @@ class CredentialCacheIntegrationTest(CredentialCache): """Load credentials from the integration test secrets""" def azgraph(self, tenant_id: UUID) -> azgraph.Graph: - return azgraph.Graph.from_credential(credentials()) + return azgraph.Graph.from_credential(load_credentials()) @dataclass diff --git a/llamazure/history/data.py b/llamazure/history/data.py index ac084cf..4362442 100644 --- a/llamazure/history/data.py +++ b/llamazure/history/data.py @@ -15,6 +15,7 @@ @dataclass(frozen=True) class Res: """Result of a Postgres query""" + cols: Dict[str, int] rows: List[Tuple] @@ -175,7 +176,7 @@ def read_snapshot(self, time: datetime.datetime) -> Res: FROM res JOIN latest_snapshots ON res.snapshot = latest_snapshots.id - + -- Generated by PRQL compiler version:0.11.3 (https://prql-lang.org) """ ), diff --git a/llamazure/history/data_test.py b/llamazure/history/data_test.py index 8442af2..dd0ae66 100644 --- a/llamazure/history/data_test.py +++ b/llamazure/history/data_test.py @@ -14,13 +14,13 @@ def test_single(self, fdf: FakeDataFactory, newdb: DB, now: datetime.datetime): r = newdb.read_snapshot(now) assert fdf.compare_snapshot(r, 0) - def test_in_past(self, fdf: FakeDataFactory, newdb: DB, now: datetime): + def test_in_past(self, fdf: FakeDataFactory, newdb: DB, now: datetime.datetime): """Test that we read the snapshot even if we're after the latest one""" newdb.insert_snapshot(now, fdf.tenant(idx=0), fdf.snapshot(idx=0)) r = newdb.read_snapshot(now + datetime.timedelta(days=1)) assert fdf.compare_snapshot(r, 0) - def test_multiple(self, fdf: FakeDataFactory, newdb: DB, now: datetime): + def test_multiple(self, fdf: FakeDataFactory, newdb: DB, now: datetime.datetime): """Test with multiple snapshots""" newdb.insert_snapshot(now, fdf.tenant(idx=0), fdf.snapshot(idx=0)) time_2 = now + datetime.timedelta(days=2) @@ -36,7 +36,7 @@ def test_multiple(self, fdf: FakeDataFactory, newdb: DB, now: datetime): class TestSnapshotMultiTenant: - def test_single_per_tenant(self, fdf: FakeDataFactory, newdb: DB, now: datetime): + def test_single_per_tenant(self, fdf: FakeDataFactory, newdb: DB, now: datetime.datetime): """Test that the latest is retrieved for each of multiple tenants""" newdb.insert_snapshot(now, fdf.tenant(idx=0), fdf.snapshot(idx=0)) newdb.insert_snapshot(now, fdf.tenant(idx=1), fdf.snapshot(idx=1)) diff --git a/llamazure/history/integration_test.py b/llamazure/history/integration_test.py index a44509d..8275daa 100644 --- a/llamazure/history/integration_test.py +++ b/llamazure/history/integration_test.py @@ -3,6 +3,8 @@ from typing import Dict, Set from uuid import UUID +import pytest + from llamazure.azgraph.azgraph import Graph from llamazure.azgraph.models import ResErr from llamazure.azrest.azrest import AzRest @@ -11,7 +13,7 @@ from llamazure.history.collect import Collector from llamazure.history.conftest import CredentialCacheIntegrationTest, TimescaledbContainer from llamazure.history.data import Res -from llamazure.test.credentials import credentials +from llamazure.test.credentials import load_credentials def group_by_time(snapshot: Res) -> Dict[datetime.datetime, Set[str]]: @@ -21,6 +23,11 @@ def group_by_time(snapshot: Res) -> Dict[datetime.datetime, Set[str]]: return out +def test_nothing(): + """Prevent collection problems for partitions""" + + +@pytest.mark.integration def test_integration(timescaledb_container: TimescaledbContainer) -> None: """ End-to-end test that: @@ -33,7 +40,7 @@ def test_integration(timescaledb_container: TimescaledbContainer) -> None: """ db = timescaledb_container.new_db() - credential = credentials() + credential = load_credentials() g = Graph.from_credential(credential) azr = AzRest.from_credential(credential) diff --git a/llamazure/rbac/conftest.py b/llamazure/rbac/conftest.py index 0993953..225067e 100644 --- a/llamazure/rbac/conftest.py +++ b/llamazure/rbac/conftest.py @@ -1,4 +1,5 @@ """Conftest""" +# noqa: F811 import logging # pylint: disable=redefined-outer-name @@ -13,7 +14,7 @@ from llamazure.msgraph.msgraph import Graph from llamazure.rbac.resources import Groups, Users from llamazure.rbac.roles import RoleAssignments, RoleDefinitions, RoleOps -from llamazure.test.credentials import credentials +from llamazure.test.conftest import credentials as credentials # noqa: F401 l = logging.getLogger(__name__) @@ -29,33 +30,33 @@ def scopes(): @pytest.fixture -def rds() -> RoleDefinitions: +def rds(credentials) -> RoleDefinitions: """Fixture: RoleDefinitions""" - return RoleDefinitions(AzRest.from_credential(credentials())) + return RoleDefinitions(AzRest.from_credential(credentials)) @pytest.fixture -def ras() -> RoleAssignments: +def ras(credentials) -> RoleAssignments: """Fixture: RoleAssignments""" - return RoleAssignments(AzRest.from_credential(credentials())) + return RoleAssignments(AzRest.from_credential(credentials)) @pytest.fixture -def role_ops() -> RoleOps: +def role_ops(credentials) -> RoleOps: """Fixture: RoleOps""" - return RoleOps(AzRest.from_credential(credentials())) + return RoleOps(AzRest.from_credential(credentials)) @pytest.fixture -def users() -> Users: +def users(credentials) -> Users: """Fixture: Users""" - return Users(Graph.from_credential(credentials())) + return Users(Graph.from_credential(credentials)) @pytest.fixture -def groups() -> Groups: +def groups(credentials) -> Groups: """Fixture: Users""" - return Groups(Graph.from_credential(credentials())) + return Groups(Graph.from_credential(credentials)) @pytest.fixture diff --git a/llamazure/test/BUILD b/llamazure/test/BUILD index bbb78fe..d4cea96 100644 --- a/llamazure/test/BUILD +++ b/llamazure/test/BUILD @@ -1,3 +1 @@ -python_test_utils( - sources=["*.py"] -) +python_test_utils(sources=["*.py"]) diff --git a/llamazure/test/conftest.py b/llamazure/test/conftest.py new file mode 100644 index 0000000..4b39639 --- /dev/null +++ b/llamazure/test/conftest.py @@ -0,0 +1,9 @@ +import pytest + +from llamazure.test.credentials import load_credentials + + +@pytest.fixture() +@pytest.mark.integration +def credentials(): + return load_credentials() diff --git a/llamazure/test/credentials.py b/llamazure/test/credentials.py index 3eaa2cb..962bb60 100644 --- a/llamazure/test/credentials.py +++ b/llamazure/test/credentials.py @@ -4,7 +4,7 @@ from azure.identity import AzureCliCredential, ClientSecretCredential, CredentialUnavailableError -def credentials(): +def load_credentials(): """Load credentials for a Service Principal""" secrets = os.environ.get("integration_test_secrets") if not secrets: diff --git a/pyproject.toml b/pyproject.toml index d681545..f7c3b43 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,3 +36,7 @@ markers = [ "integration: integration tests", "admin: tests that need some amount of admin", ] + +pytest_plugins = [ + "llamazure.tests", +] \ No newline at end of file From 37e120e589fc9afc3141fd818ac057cf8d4fb7ec Mon Sep 17 00:00:00 2001 From: Daniel Goldman Date: Mon, 1 Apr 2024 12:30:53 -0400 Subject: [PATCH 30/33] lint typing --- llamazure/history/app.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/llamazure/history/app.py b/llamazure/history/app.py index c85c2b9..59bc455 100644 --- a/llamazure/history/app.py +++ b/llamazure/history/app.py @@ -2,7 +2,7 @@ from __future__ import annotations import datetime -from typing import List +from typing import Generator, List, Optional, TypeVar from uuid import UUID from azure.identity import DefaultAzureCredential @@ -14,6 +14,9 @@ from llamazure.history.collect import Collector, CredentialCache from llamazure.history.data import DB, TSDB, Res +T = TypeVar("T") +Provider = Generator[T, None, None] + class CredentialCacheDefault(CredentialCache): """Load Azure credentials with default loader""" @@ -40,10 +43,10 @@ class DB(BaseModel): db: Settings.DB -settings = Settings() +settings = Settings() # type: ignore -def get_collector() -> Collector: +def get_collector() -> Provider[Collector]: """FastAPI Dependency for Collector""" yield Collector( CredentialCacheDefault(), @@ -51,7 +54,7 @@ def get_collector() -> Collector: ) -def get_db() -> DB: +def get_db() -> Provider[DB]: """FastAPI Dependency for DB""" yield DB(TSDB(settings.db.connstr)) @@ -78,7 +81,7 @@ async def collect_deltas(tenant_id: UUID, deltas: List[dict], collector: Collect @app.get("/history") -async def read_history(db: DB = Depends(get_db), at: datetime.datetime = None) -> Res: +async def read_history(db: DB = Depends(get_db), at: Optional[datetime.datetime] = None) -> Res: """Read history at a point in time""" if at is None: return db.read_latest() From b0ce4b40b36836cbed15cf8c5101adc7f12faba7 Mon Sep 17 00:00:00 2001 From: Daniel Goldman Date: Mon, 1 Apr 2024 16:25:30 -0400 Subject: [PATCH 31/33] feature: read changes for single resource --- llamazure/history/app.py | 12 +++++++++++- llamazure/history/conftest.py | 12 ++++++------ llamazure/history/data.py | 18 ++++++++++++++++++ llamazure/history/data_test.py | 33 +++++++++++++++++++++++++++++++++ llamazure/history/readme.md | 31 ++++++++++++++++++++++++++++++- 5 files changed, 98 insertions(+), 8 deletions(-) diff --git a/llamazure/history/app.py b/llamazure/history/app.py index 59bc455..bd1bd32 100644 --- a/llamazure/history/app.py +++ b/llamazure/history/app.py @@ -81,7 +81,7 @@ async def collect_deltas(tenant_id: UUID, deltas: List[dict], collector: Collect @app.get("/history") -async def read_history(db: DB = Depends(get_db), at: Optional[datetime.datetime] = None) -> Res: +async def read_history(at: Optional[datetime.datetime] = None, db: DB = Depends(get_db)) -> Res: """Read history at a point in time""" if at is None: return db.read_latest() @@ -89,6 +89,16 @@ async def read_history(db: DB = Depends(get_db), at: Optional[datetime.datetime] return db.read_at(at) +@app.get("/resources/{resource_id}") +async def read_resource( + resource_id: str, + ti: Optional[datetime.datetime] = None, + tf: Optional[datetime.datetime] = None, + db: DB = Depends(get_db), +) -> Res: + return db.read_resource(resource_id, ti, tf) + + @app.get("/ping") async def ping() -> str: """PING this service for up check""" diff --git a/llamazure/history/conftest.py b/llamazure/history/conftest.py index 693aa91..7810a97 100644 --- a/llamazure/history/conftest.py +++ b/llamazure/history/conftest.py @@ -160,18 +160,18 @@ def tenant(self, *, idx: Union[str, int]) -> UUID: """A fake tenant""" return self._get_or_gen(self.tenants, uuid.uuid4, idx) - def _resource(self, rev=0) -> dict: - return {"id": "/subscriptions/s0/fakeResource/", "k0": rev} + def _resource(self, name: str, rev=0) -> dict: + return {"id": f"/subscriptions/s0/fakeResource/{name}", "k0": rev} - def resource(self, rev=0, *, idx: Union[str, int]) -> dict: + def resource(self, name: str = "res", rev=0, *, idx: Union[str, int]) -> dict: """A single fake resource""" - return self._get_or_gen(self.resources, lambda: self._resource(rev), idx) + return self._get_or_gen(self.resources, lambda: self._resource(name, rev), idx) - def snapshot(self, i=4, *, idx: Union[str, int]) -> List[Tuple[str, dict]]: + def snapshot(self, count=4, rev=0, *, idx: Union[str, int]) -> List[Tuple[str, dict]]: """A fake snapshot""" def _mk_snapshot(): - resources = [self._resource(rev) for rev in range(0, i)] + resources = [self._resource(str(i), rev) for i in range(0, count)] return [(e["id"], e) for e in resources] return self._get_or_gen(self.snapshots, _mk_snapshot, idx) diff --git a/llamazure/history/data.py b/llamazure/history/data.py index 4362442..221e1b0 100644 --- a/llamazure/history/data.py +++ b/llamazure/history/data.py @@ -193,3 +193,21 @@ def read_at(self, time: datetime.datetime) -> Res: """Read the information for all resources at a point in time. Includes deltas.""" cur = self.db.exec("""SELECT DISTINCT ON (rid) * FROM res WHERE time < %s ORDER BY rid, time DESC;""", (time,)) return Res.decode(cur, cur.fetchall()) + + def read_resource(self, rid: str, ti: Optional[datetime.datetime] = None, tf: Optional[datetime.datetime] = None) -> Res: + """Read all the history of a resource, between specified time""" + data: tuple + if ti is None and tf is None: + q = """SELECT * FROM res WHERE rid = %s ORDER BY time ASC;""" + data = (rid,) + elif ti is not None and tf is None: + q = """SELECT * FROM res WHERE rid = %s AND time >= %s ORDER BY time ASC;""" + data = (rid, ti) + elif ti is None and tf is not None: + q = """SELECT * FROM res WHERE rid = %s AND time < %s ORDER BY time ASC;""" + data = (rid, tf) + else: + q = """SELECT * FROM res WHERE rid = %s AND time >= %s and time < %s ORDER BY time ASC;""" + data = (rid, ti, tf) + cur = self.db.exec(q, data) + return Res.decode(cur, cur.fetchall()) diff --git a/llamazure/history/data_test.py b/llamazure/history/data_test.py index dd0ae66..62ebac3 100644 --- a/llamazure/history/data_test.py +++ b/llamazure/history/data_test.py @@ -43,3 +43,36 @@ def test_single_per_tenant(self, fdf: FakeDataFactory, newdb: DB, now: datetime. r = newdb.read_snapshot(now) assert fdf.compare_snapshot(r, 0, 1) + + +class TestReadResource: + name = "readresource" + + def seed_db(self, fdf: FakeDataFactory, newdb: DB, now: datetime.datetime): + datas = [fdf.resource(name=self.name, rev=i, idx=i) for i in range(3)] + for i, d in enumerate(datas): + newdb.insert_delta(now + datetime.timedelta(days=i * 2), fdf.tenant(idx=0), d["id"], d) + + def test_no_bounds(self, fdf: FakeDataFactory, newdb: DB, now: datetime.datetime): + self.seed_db(fdf, newdb, now) + + r = newdb.read_resource(fdf.resource(idx=0)["id"]) + assert len(r.rows) == 3 + + def test_ti(self, fdf: FakeDataFactory, newdb: DB, now: datetime.datetime): + self.seed_db(fdf, newdb, now) + + r = newdb.read_resource(fdf.resource(idx=0)["id"], ti=now + datetime.timedelta(days=1)) + assert len(r.rows) == 2 + + def test_tf(self, fdf: FakeDataFactory, newdb: DB, now: datetime.datetime): + self.seed_db(fdf, newdb, now) + + r = newdb.read_resource(fdf.resource(idx=0)["id"], tf=now + datetime.timedelta(days=3)) + assert len(r.rows) == 2 + + def test_both(self, fdf: FakeDataFactory, newdb: DB, now: datetime.datetime): + self.seed_db(fdf, newdb, now) + + r = newdb.read_resource(fdf.resource(idx=0)["id"], ti=now + datetime.timedelta(days=1), tf=now + datetime.timedelta(days=3)) + assert len(r.rows) == 1 diff --git a/llamazure/history/readme.md b/llamazure/history/readme.md index dbb39b0..986eeaa 100644 --- a/llamazure/history/readme.md +++ b/llamazure/history/readme.md @@ -1,3 +1,32 @@ # llamazure.history -Build a history of an Azure tenancy \ No newline at end of file +Build a history of an Azure tenancy. + +The `llamazure.history` provides an application that can keep track of the history of Azure tenancies. +`llamazure.history` uses Postgres as a backend to store the resources. You can use the HTTP query interface in the app, or you can extend the app to run in-DB queries. You can also expose the DB directly for analytical queries. + +`llamazure.history` views changes in 2 phases: +- snapshots : coherent and complete views of a tenancy +- deltas : change events to individual resources + +Deltas keep the model up-to-date, and snapshots make up for missed deltas and provide a complete view of a tenancy. Snapshots can also improve performance if exact timing isn't necessary. For example, deltas allow a user to know exactly when a resource changed. But, if the delta message is missed, a snapshot ensures that change is noticed at some point. Since snapshots are complete, a user could request a snapshot to get all resources instantly, rather than the DB having to compute the latest version of every resource. + +## Usage + +### Writing + + + +### Customisation + +The app uses the `DefaultAzureCredential` for its Azure credential. You can implement an alternative `CredentialCache`. + +## Examples + +## References + +### FastAPI + +- [Getting started](https://fastapi.tiangolo.com/) : the basics for using fastapi +- [FastAPI Settings](https://fastapi.tiangolo.com/advanced/settings/) : FastAPI using Pydantic settings +- [Pydantic Settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/) : doc \ No newline at end of file From ed448360585e835f65d46f803bed83f2f2eecee5 Mon Sep 17 00:00:00 2001 From: Daniel Goldman Date: Tue, 2 Apr 2024 00:32:51 -0400 Subject: [PATCH 32/33] fix read_at to include the "at" time in the query range although this isn't compatible with the other range definitions, it does match the expectations of "at" --- llamazure/history/data.py | 2 +- llamazure/history/integration_test.py | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/llamazure/history/data.py b/llamazure/history/data.py index 221e1b0..e1c6a0d 100644 --- a/llamazure/history/data.py +++ b/llamazure/history/data.py @@ -191,7 +191,7 @@ def read_latest(self) -> Res: def read_at(self, time: datetime.datetime) -> Res: """Read the information for all resources at a point in time. Includes deltas.""" - cur = self.db.exec("""SELECT DISTINCT ON (rid) * FROM res WHERE time < %s ORDER BY rid, time DESC;""", (time,)) + cur = self.db.exec("""SELECT DISTINCT ON (rid) * FROM res WHERE time <= %s ORDER BY rid, time DESC;""", (time,)) return Res.decode(cur, cur.fetchall()) def read_resource(self, rid: str, ti: Optional[datetime.datetime] = None, tf: Optional[datetime.datetime] = None) -> Res: diff --git a/llamazure/history/integration_test.py b/llamazure/history/integration_test.py index 8275daa..b83159d 100644 --- a/llamazure/history/integration_test.py +++ b/llamazure/history/integration_test.py @@ -27,6 +27,10 @@ def test_nothing(): """Prevent collection problems for partitions""" +def _rids_in_res(res: Res) -> Set[str]: + return {e[res.cols["rid"]] for e in res.rows} + + @pytest.mark.integration def test_integration(timescaledb_container: TimescaledbContainer) -> None: """ @@ -49,6 +53,7 @@ def test_integration(timescaledb_container: TimescaledbContainer) -> None: history = Collector(CredentialCacheIntegrationTest(), db) history.take_snapshot(tenant_id) + original_resources = db.read_latest() delta_q = g.q("Resources | take(1)") if isinstance(delta_q, ResErr): @@ -60,15 +65,22 @@ def test_integration(timescaledb_container: TimescaledbContainer) -> None: delta_id = delta_q[0]["id"].lower() + # assert that our delta is correctly given to us assert delta_id in found_resources, "did not find delta in resources" found_delta = found_resources[delta_id] found_by_time = group_by_time(latest) found_delta_time = found_delta[latest.cols["time"]] assert found_by_time[found_delta_time] == {delta_id} + # assert that all the resources were included in the resources = g.q("Resources") if isinstance(delta_q, ResErr): raise RuntimeError(ResErr) assert isinstance(resources, list) assert len(resources) == len(found_resources), "snapshot and resources had different count" - assert {e["id"].lower() for e in resources} == set(found_resources), "snapshot did not contain same resources" + assert {e["id"].lower() for e in resources} == _rids_in_res(latest), "snapshot did not contain same resources" + + # assert that read_at in the past returns the resources from that time + original_time = original_resources.rows[0][original_resources.cols["time"]] + past_resources = db.read_at(original_time) + assert _rids_in_res(original_resources) == _rids_in_res(past_resources), "snapshot of past did not contain same contents as in past" \ No newline at end of file From 05046bae4c9f8c44372d956cdd91fe435c3d8ec7 Mon Sep 17 00:00:00 2001 From: Daniel Goldman Date: Tue, 2 Apr 2024 00:33:01 -0400 Subject: [PATCH 33/33] only load test secrets if necessary --- llamazure/test/credentials.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/llamazure/test/credentials.py b/llamazure/test/credentials.py index 962bb60..1260239 100644 --- a/llamazure/test/credentials.py +++ b/llamazure/test/credentials.py @@ -6,16 +6,17 @@ def load_credentials(): """Load credentials for a Service Principal""" - secrets = os.environ.get("integration_test_secrets") - if not secrets: - with open("cicd/secrets.yml", mode="r", encoding="utf-8") as f: - secrets = f.read() - secrets = yaml.safe_load(secrets) try: cli_credential = AzureCliCredential() cli_credential.get_token("https://management.azure.com//.default") return cli_credential except CredentialUnavailableError: + secrets = os.environ.get("integration_test_secrets") + if not secrets: + with open("cicd/secrets.yml", mode="r", encoding="utf-8") as f: + secrets = f.read() + secrets = yaml.safe_load(secrets) + client = secrets["auth"] credential = ClientSecretCredential(tenant_id=client["tenant"], client_id=client["appId"], client_secret=client["password"])