From d48ee86c07a64023aed168b12b063b1279745643 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Thu, 9 Jan 2025 15:40:21 +0000 Subject: [PATCH 001/125] OPS: Increase minor version number --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 698488848..b637661f6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "octue" -version = "0.61.2" +version = "0.62.0" description = "A package providing template applications for data services, and a python SDK to the Octue API." readme = "README.md" authors = ["Marcus Lugg ", "Thomas Clark "] From a74f75213d79e65d9c23ee78f953f566652a0a96 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Thu, 9 Jan 2025 15:42:25 +0000 Subject: [PATCH 002/125] DEP: Temporarily remove `numpy` and `pandas` dev dependencies --- pyproject.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b637661f6..b7fd3d590 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,10 +54,8 @@ tox = "^3.23" pre-commit = "^2.17" coverage = "^5" # Template app dependencies -numpy = "^1" dateparser = "1.1.1" stringcase = "1.2.0" -pandas = "^1.3" # Documentation Sphinx = ">=5,<8" sphinx-rtd-theme = ">=1,<2" From d40192b97b96860b4a2322b7498606ba12f848aa Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Thu, 9 Jan 2025 15:43:01 +0000 Subject: [PATCH 003/125] DEP: Drop support for python 3.8 and 3.9 --- pyproject.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b7fd3d590..03014f613 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,15 +13,15 @@ classifiers = [ "Intended Audience :: Developers", "Topic :: Software Development :: Libraries :: Python Modules", "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Operating System :: OS Independent", ] [tool.poetry.dependencies] -python = "^3.8" +python = "^3.10" click = ">=7,<9" coolname = "^2" Flask = "^2" From 7140f76211c7d60a8ee6c7833b8119c95a7de1bd Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Thu, 9 Jan 2025 15:44:02 +0000 Subject: [PATCH 004/125] DEP: Add `numpy` and `pandas` back to dev dependencies --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 03014f613..2ae836891 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,8 @@ tox = "^3.23" pre-commit = "^2.17" coverage = "^5" # Template app dependencies +numpy = "^2.2.1" +pandas = "^2.2.3" dateparser = "1.1.1" stringcase = "1.2.0" # Documentation From 52eddd0d8096d0223379b3e91a44766042f69342 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Thu, 9 Jan 2025 15:44:26 +0000 Subject: [PATCH 005/125] DEP: Update lock file skipci --- poetry.lock | 1603 +++++++++++++++++++++++++++++---------------------- 1 file changed, 916 insertions(+), 687 deletions(-) diff --git a/poetry.lock b/poetry.lock index a7de3f187..d46f5e880 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,14 +1,16 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.0.0 and should not be changed by hand. [[package]] name = "alabaster" -version = "0.7.13" -description = "A configurable sidebar-enabled Sphinx theme" +version = "0.7.16" +description = "A light, configurable Sphinx theme" optional = false -python-versions = ">=3.6" +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3"}, - {file = "alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2"}, + {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"}, + {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, ] [[package]] @@ -17,6 +19,8 @@ version = "1.4.1" description = "Handy tools for working with URLs and APIs." optional = false python-versions = ">=3.6.1" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "apeye-1.4.1-py3-none-any.whl", hash = "sha256:44e58a9104ec189bf42e76b3a7fe91e2b2879d96d48e9a77e5e32ff699c9204e"}, {file = "apeye-1.4.1.tar.gz", hash = "sha256:14ea542fad689e3bfdbda2189a354a4908e90aee4bf84c15ab75d68453d76a36"}, @@ -38,6 +42,8 @@ version = "1.1.5" description = "Core (offline) functionality for the apeye library." optional = false python-versions = ">=3.6.1" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "apeye_core-1.1.5-py3-none-any.whl", hash = "sha256:dc27a93f8c9e246b3b238c5ea51edf6115ab2618ef029b9f2d9a190ec8228fbf"}, {file = "apeye_core-1.1.5.tar.gz", hash = "sha256:5de72ed3d00cc9b20fea55e54b7ab8f5ef8500eb33a5368bc162a5585e238a55"}, @@ -53,6 +59,8 @@ version = "1.4.4" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false python-versions = "*" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, @@ -64,6 +72,8 @@ version = "24.3.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308"}, {file = "attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff"}, @@ -83,6 +93,8 @@ version = "0.2.14" description = "Extended sphinx autodoc including automatic autosummaries" optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "autodocsumm-0.2.14-py3-none-any.whl", hash = "sha256:3bad8717fc5190802c60392a7ab04b9f3c97aa9efa8b3780b3d81d615bfe5dc0"}, {file = "autodocsumm-0.2.14.tar.gz", hash = "sha256:2839a9d4facc3c4eccd306c08695540911042b46eeafcdc3203e6d0bab40bc77"}, @@ -97,51 +109,24 @@ version = "2.16.0" description = "Internationalization utilities" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b"}, {file = "babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"}, ] -[package.dependencies] -pytz = {version = ">=2015.7", markers = "python_version < \"3.9\""} - [package.extras] dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] -[[package]] -name = "backports-zoneinfo" -version = "0.2.1" -description = "Backport of the standard library zoneinfo module" -optional = false -python-versions = ">=3.6" -files = [ - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc"}, - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722"}, - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546"}, - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win32.whl", hash = "sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08"}, - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win32.whl", hash = "sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6"}, - {file = "backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"}, -] - -[package.extras] -tzdata = ["tzdata"] - [[package]] name = "beautifulsoup4" version = "4.12.3" description = "Screen-scraping library" optional = false python-versions = ">=3.6.0" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, @@ -159,24 +144,28 @@ lxml = ["lxml"] [[package]] name = "blinker" -version = "1.8.2" +version = "1.9.0" description = "Fast, simple object-to-object and broadcast signaling" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "blinker-1.8.2-py3-none-any.whl", hash = "sha256:1779309f71bf239144b9399d06ae925637cf6634cf6bd131104184531bf67c01"}, - {file = "blinker-1.8.2.tar.gz", hash = "sha256:8f77b09d3bf7c795e969e9486f39c2c5e9c39d4ee07424be2bc594ece9642d83"}, + {file = "blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"}, + {file = "blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf"}, ] [[package]] name = "cachecontrol" -version = "0.14.1" +version = "0.14.2" description = "httplib2 caching for requests" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "cachecontrol-0.14.1-py3-none-any.whl", hash = "sha256:65e3abd62b06382ce3894df60dde9e0deb92aeb734724f68fa4f3b91e97206b9"}, - {file = "cachecontrol-0.14.1.tar.gz", hash = "sha256:06ef916a1e4eb7dba9948cdfc9c76e749db2e02104a9a1277e8b642591a0f717"}, + {file = "cachecontrol-0.14.2-py3-none-any.whl", hash = "sha256:ebad2091bf12d0d200dfc2464330db638c5deb41d546f6d7aca079e87290f3b0"}, + {file = "cachecontrol-0.14.2.tar.gz", hash = "sha256:7d47d19f866409b98ff6025b6a0fca8e4c791fb31abbd95f622093894ce903a2"}, ] [package.dependencies] @@ -195,6 +184,8 @@ version = "5.5.0" description = "Extensible memoizing collections and decorators" optional = false python-versions = ">=3.7" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "cachetools-5.5.0-py3-none-any.whl", hash = "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292"}, {file = "cachetools-5.5.0.tar.gz", hash = "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a"}, @@ -206,6 +197,8 @@ version = "2024.12.14" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" +groups = ["main", "dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, @@ -217,6 +210,8 @@ version = "3.4.0" description = "Validate configuration and produce human readable error messages." optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, @@ -224,127 +219,118 @@ files = [ [[package]] name = "charset-normalizer" -version = "3.4.0" +version = "3.4.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false -python-versions = ">=3.7.0" -files = [ - {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"}, - {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, - {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, +python-versions = ">=3.7" +groups = ["main", "dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, + {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, + {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, ] [[package]] name = "click" -version = "8.1.7" +version = "8.1.8" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, ] [package.dependencies] @@ -356,10 +342,12 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main", "dev"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +markers = {main = "(python_version <= \"3.11\" or python_version >= \"3.12\") and platform_system == \"Windows\"", dev = "(sys_platform == \"win32\" or platform_system == \"Windows\") and (python_version <= \"3.11\" or python_version >= \"3.12\")"} [[package]] name = "coolname" @@ -367,6 +355,8 @@ version = "2.2.0" description = "Random name and slug generator" optional = false python-versions = "*" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "coolname-2.2.0-py2.py3-none-any.whl", hash = "sha256:4d1563186cfaf71b394d5df4c744f8c41303b6846413645e31d31915cdeb13e8"}, {file = "coolname-2.2.0.tar.gz", hash = "sha256:6c5d5731759104479e7ca195a9b64f7900ac5bead40183c09323c7d0be9e75c7"}, @@ -378,6 +368,8 @@ version = "5.5" description = "Code coverage measurement for Python" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "coverage-5.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf"}, {file = "coverage-5.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b"}, @@ -442,6 +434,8 @@ version = "2.11.1" description = "A CSS Cascading Style Sheets library for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "cssutils-2.11.1-py3-none-any.whl", hash = "sha256:a67bfdfdff4f3867fab43698ec4897c1a828eca5973f4073321b3bccaf1199b1"}, {file = "cssutils-2.11.1.tar.gz", hash = "sha256:0563a76513b6af6eebbe788c3bf3d01c920e46b3f90c8416738c5cfc773ff8e2"}, @@ -460,6 +454,8 @@ version = "1.1.1" description = "Date parsing library designed to parse dates from HTML pages" optional = false python-versions = ">=3.5" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "dateparser-1.1.1-py2.py3-none-any.whl", hash = "sha256:9600874312ff28a41f96ec7ccdc73be1d1c44435719da47fea3339d55ff5a628"}, {file = "dateparser-1.1.1.tar.gz", hash = "sha256:038196b1f12c7397e38aad3d61588833257f6f552baa63a1499e6987fa8d42d9"}, @@ -482,6 +478,8 @@ version = "1.2.15" description = "Python @deprecated decorator to deprecate old python classes, functions or methods." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "Deprecated-1.2.15-py2.py3-none-any.whl", hash = "sha256:353bc4a8ac4bfc96800ddab349d89c25dec1079f65fd53acdcc1e0b975b21320"}, {file = "deprecated-1.2.15.tar.gz", hash = "sha256:683e561a90de76239796e6b6feac66b99030d2dd3fcf61ef996330f14bbb9b0d"}, @@ -499,6 +497,8 @@ version = "0.3.0.post1" description = "A μ-library for constructing cascading style sheets from Python dictionaries." optional = false python-versions = ">=3.6" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "dict2css-0.3.0.post1-py3-none-any.whl", hash = "sha256:f006a6b774c3e31869015122ae82c491fd25e7de4a75607a62aa3e798f837e0d"}, {file = "dict2css-0.3.0.post1.tar.gz", hash = "sha256:89c544c21c4ca7472c3fffb9d37d3d926f606329afdb751dc1de67a411b70719"}, @@ -514,6 +514,8 @@ version = "0.3.9" description = "Distribution utilities" optional = false python-versions = "*" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, @@ -525,6 +527,8 @@ version = "0.18.1" description = "Docutils -- Python Documentation Utilities" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "docutils-0.18.1-py2.py3-none-any.whl", hash = "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c"}, {file = "docutils-0.18.1.tar.gz", hash = "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06"}, @@ -536,14 +540,14 @@ version = "3.9.0" description = "Helpful functions for Python 🐍 🛠️" optional = false python-versions = ">=3.6" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "domdf_python_tools-3.9.0-py3-none-any.whl", hash = "sha256:4e1ef365cbc24627d6d1e90cf7d46d8ab8df967e1237f4a26885f6986c78872e"}, {file = "domdf_python_tools-3.9.0.tar.gz", hash = "sha256:1f8a96971178333a55e083e35610d7688cd7620ad2b99790164e1fc1a3614c18"}, ] [package.dependencies] -importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.9\""} -importlib-resources = {version = ">=3.0.0", markers = "python_version < \"3.9\""} natsort = ">=7.0.1" typing-extensions = ">=3.7.4.1" @@ -557,6 +561,8 @@ version = "1.2.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version < \"3.11\"" files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, @@ -571,6 +577,8 @@ version = "3.16.1" description = "A platform independent file lock." optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, @@ -587,6 +595,8 @@ version = "2.3.3" description = "A simple framework for building complex web applications." optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "flask-2.3.3-py3-none-any.whl", hash = "sha256:f69fcd559dc907ed196ab9df0e48471709175e696d6e698dd4dbe940f96ce66b"}, {file = "flask-2.3.3.tar.gz", hash = "sha256:09c347a92aa7ff4a8e7f3206795f30d826654baf38b873d0744cd571ca609efc"}, @@ -595,7 +605,6 @@ files = [ [package.dependencies] blinker = ">=1.6.2" click = ">=8.1.3" -importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""} itsdangerous = ">=2.1.2" Jinja2 = ">=3.1.2" Werkzeug = ">=2.3.7" @@ -610,6 +619,8 @@ version = "2.4.16" description = "Python's filesystem abstraction layer" optional = false python-versions = "*" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "fs-2.4.16-py2.py3-none-any.whl", hash = "sha256:660064febbccda264ae0b6bace80a8d1be9e089e0a5eb2427b7d517f9a91545c"}, {file = "fs-2.4.16.tar.gz", hash = "sha256:ae97c7d51213f4b70b6a958292530289090de3a7e15841e108fbe144f069d313"}, @@ -629,6 +640,8 @@ version = "2022.6.11" description = "A stub emulator for the Google Cloud Storage API" optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "gcp-storage-emulator-2022.6.11.tar.gz", hash = "sha256:fbc896723df95b7527a4ced499bfdd374247dfc40d6992bfa34da67e66bad4be"}, {file = "gcp_storage_emulator-2022.6.11-py3-none-any.whl", hash = "sha256:81271737f099cec22b3b4d0ceca5d43f484bfc39770a7d98e6545eb94bf46a1b"}, @@ -644,6 +657,8 @@ version = "2.24.0" description = "Google API client core library" optional = false python-versions = ">=3.7" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "google_api_core-2.24.0-py3-none-any.whl", hash = "sha256:10d82ac0fca69c82a25b3efdeefccf6f28e02ebb97925a8cce8edbfe379929d9"}, {file = "google_api_core-2.24.0.tar.gz", hash = "sha256:e255640547a597a4da010876d333208ddac417d60add22b6851a0c66a831fcaf"}, @@ -653,16 +668,16 @@ files = [ google-auth = ">=2.14.1,<3.0.dev0" googleapis-common-protos = ">=1.56.2,<2.0.dev0" grpcio = [ - {version = ">=1.49.1,<2.0dev", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}, {version = ">=1.33.2,<2.0dev", optional = true, markers = "python_version < \"3.11\" and extra == \"grpc\""}, + {version = ">=1.49.1,<2.0dev", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}, ] grpcio-status = [ - {version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}, {version = ">=1.33.2,<2.0.dev0", optional = true, markers = "python_version < \"3.11\" and extra == \"grpc\""}, + {version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}, ] proto-plus = [ - {version = ">=1.25.0,<2.0.0dev", markers = "python_version >= \"3.13\""}, {version = ">=1.22.3,<2.0.0dev", markers = "python_version < \"3.13\""}, + {version = ">=1.25.0,<2.0.0dev", markers = "python_version >= \"3.13\""}, ] protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0.dev0" requests = ">=2.18.0,<3.0.0.dev0" @@ -679,6 +694,8 @@ version = "2.37.0" description = "Google Authentication Library" optional = false python-versions = ">=3.7" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "google_auth-2.37.0-py2.py3-none-any.whl", hash = "sha256:42664f18290a6be591be5329a96fe30184be1a1badb7292a7f686a9659de9ca0"}, {file = "google_auth-2.37.0.tar.gz", hash = "sha256:0054623abf1f9c83492c63d3f47e77f0a544caa3d40b2d98e099a611c2dd5d00"}, @@ -703,6 +720,8 @@ version = "3.27.0" description = "Google BigQuery API client library" optional = false python-versions = ">=3.7" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "google_cloud_bigquery-3.27.0-py2.py3-none-any.whl", hash = "sha256:b53b0431e5ba362976a4cd8acce72194b4116cdf8115030c7b339b884603fcc3"}, {file = "google_cloud_bigquery-3.27.0.tar.gz", hash = "sha256:379c524054d7b090fa56d0c22662cc6e6458a6229b6754c0e7177e3a73421d2c"}, @@ -734,6 +753,8 @@ version = "2.4.1" description = "Google Cloud API client core library" optional = false python-versions = ">=3.7" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "google-cloud-core-2.4.1.tar.gz", hash = "sha256:9b7749272a812bde58fff28868d0c5e2f585b82f37e09a1f6ed2d4d10f134073"}, {file = "google_cloud_core-2.4.1-py2.py3-none-any.whl", hash = "sha256:a9e6a4422b9ac5c29f79a0ede9485473338e2ce78d91f2370c01e730eab22e61"}, @@ -748,13 +769,15 @@ grpc = ["grpcio (>=1.38.0,<2.0dev)", "grpcio-status (>=1.38.0,<2.0.dev0)"] [[package]] name = "google-cloud-pubsub" -version = "2.27.1" +version = "2.27.2" description = "Google Cloud Pub/Sub API client library" optional = false python-versions = ">=3.7" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "google_cloud_pubsub-2.27.1-py2.py3-none-any.whl", hash = "sha256:3ca8980c198a847ee464845ab60f05478d4819cf693c9950ee89da96f0b80a41"}, - {file = "google_cloud_pubsub-2.27.1.tar.gz", hash = "sha256:7119dbc5af4b915ecdfa1289919f791a432927eaaa7bbfbeb740e6d7020c181e"}, + {file = "google_cloud_pubsub-2.27.2-py2.py3-none-any.whl", hash = "sha256:a919f84fdea683b0a02464e38dd32332edbcbc8e85da82070079a57791119fd6"}, + {file = "google_cloud_pubsub-2.27.2.tar.gz", hash = "sha256:d92c156c7ddd0e5125008f977898198d7b1ae766026056497271bec4909647fe"}, ] [package.dependencies] @@ -766,9 +789,9 @@ grpcio-status = ">=1.33.2" opentelemetry-api = {version = ">=1.27.0", markers = "python_version >= \"3.8\""} opentelemetry-sdk = {version = ">=1.27.0", markers = "python_version >= \"3.8\""} proto-plus = [ - {version = ">=1.25.0,<2.0.0dev", markers = "python_version >= \"3.13\""}, - {version = ">=1.22.2,<2.0.0dev", markers = "python_version >= \"3.11\" and python_version < \"3.13\""}, {version = ">=1.22.0,<2.0.0dev", markers = "python_version < \"3.11\""}, + {version = ">=1.22.2,<2.0.0dev", markers = "python_version >= \"3.11\" and python_version < \"3.13\""}, + {version = ">=1.25.0,<2.0.0dev", markers = "python_version >= \"3.13\""}, ] protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0dev" @@ -781,6 +804,8 @@ version = "2.22.0" description = "Google Cloud Secret Manager API client library" optional = false python-versions = ">=3.7" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "google_cloud_secret_manager-2.22.0-py2.py3-none-any.whl", hash = "sha256:9e23a8165ed718de56543723b1e21c394f2cee9ababddcac8ceecc9f427d2696"}, {file = "google_cloud_secret_manager-2.22.0.tar.gz", hash = "sha256:5dd95ac6243687f86fd803316c0768f507028958b8a2e69b3aa0ace7ac654bf4"}, @@ -791,8 +816,8 @@ google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0dev", extr google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0dev" grpc-google-iam-v1 = ">=0.12.4,<1.0.0dev" proto-plus = [ - {version = ">=1.25.0,<2.0.0dev", markers = "python_version >= \"3.13\""}, {version = ">=1.22.3,<2.0.0dev", markers = "python_version < \"3.13\""}, + {version = ">=1.25.0,<2.0.0dev", markers = "python_version >= \"3.13\""}, ] protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0dev" @@ -802,6 +827,8 @@ version = "2.19.0" description = "Google Cloud Storage API client library" optional = false python-versions = ">=3.7" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "google_cloud_storage-2.19.0-py2.py3-none-any.whl", hash = "sha256:aeb971b5c29cf8ab98445082cbfe7b161a1f48ed275822f59ed3f1524ea54fba"}, {file = "google_cloud_storage-2.19.0.tar.gz", hash = "sha256:cd05e9e7191ba6cb68934d8eb76054d9be4562aa89dbc4236feee4d7d51342b2"}, @@ -825,6 +852,8 @@ version = "1.3.0" description = "A python wrapper of the C library 'Google CRC32C'" optional = false python-versions = ">=3.6" +groups = ["main", "dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "google-crc32c-1.3.0.tar.gz", hash = "sha256:276de6273eb074a35bc598f8efbc00c7869c5cf2e29c90748fccc8c898c244df"}, {file = "google_crc32c-1.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cb6994fff247987c66a8a4e550ef374671c2b82e3c0d2115e689d21e511a652d"}, @@ -880,6 +909,8 @@ version = "2.7.2" description = "Utilities for Google Media Downloads and Resumable Uploads" optional = false python-versions = ">=3.7" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "google_resumable_media-2.7.2-py2.py3-none-any.whl", hash = "sha256:3ce7551e9fe6d99e9a126101d2536612bb73486721951e9562fee0f90c6ababa"}, {file = "google_resumable_media-2.7.2.tar.gz", hash = "sha256:5280aed4629f2b60b847b0d42f9857fd4935c11af266744df33d8074cae92fe0"}, @@ -898,6 +929,8 @@ version = "1.66.0" description = "Common protobufs used in Google APIs" optional = false python-versions = ">=3.7" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "googleapis_common_protos-1.66.0-py2.py3-none-any.whl", hash = "sha256:d7abcd75fabb2e0ec9f74466401f6c119a0b498e27370e9be4c94cb7e382b8ed"}, {file = "googleapis_common_protos-1.66.0.tar.gz", hash = "sha256:c3e7b33d15fdca5374cc0a7346dd92ffa847425cc4ea941d970f13680052ec8c"}, @@ -912,13 +945,15 @@ grpc = ["grpcio (>=1.44.0,<2.0.0.dev0)"] [[package]] name = "grpc-google-iam-v1" -version = "0.13.1" +version = "0.14.0" description = "IAM API client library" optional = false python-versions = ">=3.7" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "grpc-google-iam-v1-0.13.1.tar.gz", hash = "sha256:3ff4b2fd9d990965e410965253c0da6f66205d5a8291c4c31c6ebecca18a9001"}, - {file = "grpc_google_iam_v1-0.13.1-py2.py3-none-any.whl", hash = "sha256:c3e86151a981811f30d5e7330f271cee53e73bb87755e88cc3b6f0c7b5fe374e"}, + {file = "grpc_google_iam_v1-0.14.0-py2.py3-none-any.whl", hash = "sha256:fb4a084b30099ba3ab07d61d620a0d4429570b13ff53bd37bac75235f98b7da4"}, + {file = "grpc_google_iam_v1-0.14.0.tar.gz", hash = "sha256:c66e07aa642e39bb37950f9e7f491f70dad150ac9801263b42b2814307c2df99"}, ] [package.dependencies] @@ -928,85 +963,89 @@ protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4 [[package]] name = "grpcio" -version = "1.68.1" +version = "1.69.0" description = "HTTP/2-based RPC framework" optional = false python-versions = ">=3.8" -files = [ - {file = "grpcio-1.68.1-cp310-cp310-linux_armv7l.whl", hash = "sha256:d35740e3f45f60f3c37b1e6f2f4702c23867b9ce21c6410254c9c682237da68d"}, - {file = "grpcio-1.68.1-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:d99abcd61760ebb34bdff37e5a3ba333c5cc09feda8c1ad42547bea0416ada78"}, - {file = "grpcio-1.68.1-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:f8261fa2a5f679abeb2a0a93ad056d765cdca1c47745eda3f2d87f874ff4b8c9"}, - {file = "grpcio-1.68.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0feb02205a27caca128627bd1df4ee7212db051019a9afa76f4bb6a1a80ca95e"}, - {file = "grpcio-1.68.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:919d7f18f63bcad3a0f81146188e90274fde800a94e35d42ffe9eadf6a9a6330"}, - {file = "grpcio-1.68.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:963cc8d7d79b12c56008aabd8b457f400952dbea8997dd185f155e2f228db079"}, - {file = "grpcio-1.68.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ccf2ebd2de2d6661e2520dae293298a3803a98ebfc099275f113ce1f6c2a80f1"}, - {file = "grpcio-1.68.1-cp310-cp310-win32.whl", hash = "sha256:2cc1fd04af8399971bcd4f43bd98c22d01029ea2e56e69c34daf2bf8470e47f5"}, - {file = "grpcio-1.68.1-cp310-cp310-win_amd64.whl", hash = "sha256:ee2e743e51cb964b4975de572aa8fb95b633f496f9fcb5e257893df3be854746"}, - {file = "grpcio-1.68.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:55857c71641064f01ff0541a1776bfe04a59db5558e82897d35a7793e525774c"}, - {file = "grpcio-1.68.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4b177f5547f1b995826ef529d2eef89cca2f830dd8b2c99ffd5fde4da734ba73"}, - {file = "grpcio-1.68.1-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:3522c77d7e6606d6665ec8d50e867f13f946a4e00c7df46768f1c85089eae515"}, - {file = "grpcio-1.68.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9d1fae6bbf0816415b81db1e82fb3bf56f7857273c84dcbe68cbe046e58e1ccd"}, - {file = "grpcio-1.68.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:298ee7f80e26f9483f0b6f94cc0a046caf54400a11b644713bb5b3d8eb387600"}, - {file = "grpcio-1.68.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cbb5780e2e740b6b4f2d208e90453591036ff80c02cc605fea1af8e6fc6b1bbe"}, - {file = "grpcio-1.68.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ddda1aa22495d8acd9dfbafff2866438d12faec4d024ebc2e656784d96328ad0"}, - {file = "grpcio-1.68.1-cp311-cp311-win32.whl", hash = "sha256:b33bd114fa5a83f03ec6b7b262ef9f5cac549d4126f1dc702078767b10c46ed9"}, - {file = "grpcio-1.68.1-cp311-cp311-win_amd64.whl", hash = "sha256:7f20ebec257af55694d8f993e162ddf0d36bd82d4e57f74b31c67b3c6d63d8b2"}, - {file = "grpcio-1.68.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:8829924fffb25386995a31998ccbbeaa7367223e647e0122043dfc485a87c666"}, - {file = "grpcio-1.68.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:3aed6544e4d523cd6b3119b0916cef3d15ef2da51e088211e4d1eb91a6c7f4f1"}, - {file = "grpcio-1.68.1-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:4efac5481c696d5cb124ff1c119a78bddbfdd13fc499e3bc0ca81e95fc573684"}, - {file = "grpcio-1.68.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ab2d912ca39c51f46baf2a0d92aa265aa96b2443266fc50d234fa88bf877d8e"}, - {file = "grpcio-1.68.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95c87ce2a97434dffe7327a4071839ab8e8bffd0054cc74cbe971fba98aedd60"}, - {file = "grpcio-1.68.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:e4842e4872ae4ae0f5497bf60a0498fa778c192cc7a9e87877abd2814aca9475"}, - {file = "grpcio-1.68.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:255b1635b0ed81e9f91da4fcc8d43b7ea5520090b9a9ad9340d147066d1d3613"}, - {file = "grpcio-1.68.1-cp312-cp312-win32.whl", hash = "sha256:7dfc914cc31c906297b30463dde0b9be48e36939575eaf2a0a22a8096e69afe5"}, - {file = "grpcio-1.68.1-cp312-cp312-win_amd64.whl", hash = "sha256:a0c8ddabef9c8f41617f213e527254c41e8b96ea9d387c632af878d05db9229c"}, - {file = "grpcio-1.68.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:a47faedc9ea2e7a3b6569795c040aae5895a19dde0c728a48d3c5d7995fda385"}, - {file = "grpcio-1.68.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:390eee4225a661c5cd133c09f5da1ee3c84498dc265fd292a6912b65c421c78c"}, - {file = "grpcio-1.68.1-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:66a24f3d45c33550703f0abb8b656515b0ab777970fa275693a2f6dc8e35f1c1"}, - {file = "grpcio-1.68.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c08079b4934b0bf0a8847f42c197b1d12cba6495a3d43febd7e99ecd1cdc8d54"}, - {file = "grpcio-1.68.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8720c25cd9ac25dd04ee02b69256d0ce35bf8a0f29e20577427355272230965a"}, - {file = "grpcio-1.68.1-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:04cfd68bf4f38f5bb959ee2361a7546916bd9a50f78617a346b3aeb2b42e2161"}, - {file = "grpcio-1.68.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c28848761a6520c5c6071d2904a18d339a796ebe6b800adc8b3f474c5ce3c3ad"}, - {file = "grpcio-1.68.1-cp313-cp313-win32.whl", hash = "sha256:77d65165fc35cff6e954e7fd4229e05ec76102d4406d4576528d3a3635fc6172"}, - {file = "grpcio-1.68.1-cp313-cp313-win_amd64.whl", hash = "sha256:a8040f85dcb9830d8bbb033ae66d272614cec6faceee88d37a88a9bd1a7a704e"}, - {file = "grpcio-1.68.1-cp38-cp38-linux_armv7l.whl", hash = "sha256:eeb38ff04ab6e5756a2aef6ad8d94e89bb4a51ef96e20f45c44ba190fa0bcaad"}, - {file = "grpcio-1.68.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8a3869a6661ec8f81d93f4597da50336718bde9eb13267a699ac7e0a1d6d0bea"}, - {file = "grpcio-1.68.1-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:2c4cec6177bf325eb6faa6bd834d2ff6aa8bb3b29012cceb4937b86f8b74323c"}, - {file = "grpcio-1.68.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12941d533f3cd45d46f202e3667be8ebf6bcb3573629c7ec12c3e211d99cfccf"}, - {file = "grpcio-1.68.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80af6f1e69c5e68a2be529990684abdd31ed6622e988bf18850075c81bb1ad6e"}, - {file = "grpcio-1.68.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e8dbe3e00771bfe3d04feed8210fc6617006d06d9a2679b74605b9fed3e8362c"}, - {file = "grpcio-1.68.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:83bbf5807dc3ee94ce1de2dfe8a356e1d74101e4b9d7aa8c720cc4818a34aded"}, - {file = "grpcio-1.68.1-cp38-cp38-win32.whl", hash = "sha256:8cb620037a2fd9eeee97b4531880e439ebfcd6d7d78f2e7dcc3726428ab5ef63"}, - {file = "grpcio-1.68.1-cp38-cp38-win_amd64.whl", hash = "sha256:52fbf85aa71263380d330f4fce9f013c0798242e31ede05fcee7fbe40ccfc20d"}, - {file = "grpcio-1.68.1-cp39-cp39-linux_armv7l.whl", hash = "sha256:cb400138e73969eb5e0535d1d06cae6a6f7a15f2cc74add320e2130b8179211a"}, - {file = "grpcio-1.68.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a1b988b40f2fd9de5c820f3a701a43339d8dcf2cb2f1ca137e2c02671cc83ac1"}, - {file = "grpcio-1.68.1-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:96f473cdacfdd506008a5d7579c9f6a7ff245a9ade92c3c0265eb76cc591914f"}, - {file = "grpcio-1.68.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:37ea3be171f3cf3e7b7e412a98b77685eba9d4fd67421f4a34686a63a65d99f9"}, - {file = "grpcio-1.68.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ceb56c4285754e33bb3c2fa777d055e96e6932351a3082ce3559be47f8024f0"}, - {file = "grpcio-1.68.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:dffd29a2961f3263a16d73945b57cd44a8fd0b235740cb14056f0612329b345e"}, - {file = "grpcio-1.68.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:025f790c056815b3bf53da850dd70ebb849fd755a4b1ac822cb65cd631e37d43"}, - {file = "grpcio-1.68.1-cp39-cp39-win32.whl", hash = "sha256:1098f03dedc3b9810810568060dea4ac0822b4062f537b0f53aa015269be0a76"}, - {file = "grpcio-1.68.1-cp39-cp39-win_amd64.whl", hash = "sha256:334ab917792904245a028f10e803fcd5b6f36a7b2173a820c0b5b076555825e1"}, - {file = "grpcio-1.68.1.tar.gz", hash = "sha256:44a8502dd5de653ae6a73e2de50a401d84184f0331d0ac3daeb044e66d5c5054"}, +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "grpcio-1.69.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:2060ca95a8db295ae828d0fc1c7f38fb26ccd5edf9aa51a0f44251f5da332e97"}, + {file = "grpcio-1.69.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:2e52e107261fd8fa8fa457fe44bfadb904ae869d87c1280bf60f93ecd3e79278"}, + {file = "grpcio-1.69.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:316463c0832d5fcdb5e35ff2826d9aa3f26758d29cdfb59a368c1d6c39615a11"}, + {file = "grpcio-1.69.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:26c9a9c4ac917efab4704b18eed9082ed3b6ad19595f047e8173b5182fec0d5e"}, + {file = "grpcio-1.69.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90b3646ced2eae3a0599658eeccc5ba7f303bf51b82514c50715bdd2b109e5ec"}, + {file = "grpcio-1.69.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3b75aea7c6cb91b341c85e7c1d9db1e09e1dd630b0717f836be94971e015031e"}, + {file = "grpcio-1.69.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5cfd14175f9db33d4b74d63de87c64bb0ee29ce475ce3c00c01ad2a3dc2a9e51"}, + {file = "grpcio-1.69.0-cp310-cp310-win32.whl", hash = "sha256:9031069d36cb949205293cf0e243abd5e64d6c93e01b078c37921493a41b72dc"}, + {file = "grpcio-1.69.0-cp310-cp310-win_amd64.whl", hash = "sha256:cc89b6c29f3dccbe12d7a3b3f1b3999db4882ae076c1c1f6df231d55dbd767a5"}, + {file = "grpcio-1.69.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:8de1b192c29b8ce45ee26a700044717bcbbd21c697fa1124d440548964328561"}, + {file = "grpcio-1.69.0-cp311-cp311-macosx_10_14_universal2.whl", hash = "sha256:7e76accf38808f5c5c752b0ab3fd919eb14ff8fafb8db520ad1cc12afff74de6"}, + {file = "grpcio-1.69.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:d5658c3c2660417d82db51e168b277e0ff036d0b0f859fa7576c0ffd2aec1442"}, + {file = "grpcio-1.69.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5494d0e52bf77a2f7eb17c6da662886ca0a731e56c1c85b93505bece8dc6cf4c"}, + {file = "grpcio-1.69.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ed866f9edb574fd9be71bf64c954ce1b88fc93b2a4cbf94af221e9426eb14d6"}, + {file = "grpcio-1.69.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c5ba38aeac7a2fe353615c6b4213d1fbb3a3c34f86b4aaa8be08baaaee8cc56d"}, + {file = "grpcio-1.69.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f79e05f5bbf551c4057c227d1b041ace0e78462ac8128e2ad39ec58a382536d2"}, + {file = "grpcio-1.69.0-cp311-cp311-win32.whl", hash = "sha256:bf1f8be0da3fcdb2c1e9f374f3c2d043d606d69f425cd685110dd6d0d2d61258"}, + {file = "grpcio-1.69.0-cp311-cp311-win_amd64.whl", hash = "sha256:fb9302afc3a0e4ba0b225cd651ef8e478bf0070cf11a529175caecd5ea2474e7"}, + {file = "grpcio-1.69.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:fc18a4de8c33491ad6f70022af5c460b39611e39578a4d84de0fe92f12d5d47b"}, + {file = "grpcio-1.69.0-cp312-cp312-macosx_10_14_universal2.whl", hash = "sha256:0f0270bd9ffbff6961fe1da487bdcd594407ad390cc7960e738725d4807b18c4"}, + {file = "grpcio-1.69.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:dc48f99cc05e0698e689b51a05933253c69a8c8559a47f605cff83801b03af0e"}, + {file = "grpcio-1.69.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e925954b18d41aeb5ae250262116d0970893b38232689c4240024e4333ac084"}, + {file = "grpcio-1.69.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87d222569273720366f68a99cb62e6194681eb763ee1d3b1005840678d4884f9"}, + {file = "grpcio-1.69.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:b62b0f41e6e01a3e5082000b612064c87c93a49b05f7602fe1b7aa9fd5171a1d"}, + {file = "grpcio-1.69.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:db6f9fd2578dbe37db4b2994c94a1d9c93552ed77dca80e1657bb8a05b898b55"}, + {file = "grpcio-1.69.0-cp312-cp312-win32.whl", hash = "sha256:b192b81076073ed46f4b4dd612b8897d9a1e39d4eabd822e5da7b38497ed77e1"}, + {file = "grpcio-1.69.0-cp312-cp312-win_amd64.whl", hash = "sha256:1227ff7836f7b3a4ab04e5754f1d001fa52a730685d3dc894ed8bc262cc96c01"}, + {file = "grpcio-1.69.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:a78a06911d4081a24a1761d16215a08e9b6d4d29cdbb7e427e6c7e17b06bcc5d"}, + {file = "grpcio-1.69.0-cp313-cp313-macosx_10_14_universal2.whl", hash = "sha256:dc5a351927d605b2721cbb46158e431dd49ce66ffbacb03e709dc07a491dde35"}, + {file = "grpcio-1.69.0-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:3629d8a8185f5139869a6a17865d03113a260e311e78fbe313f1a71603617589"}, + {file = "grpcio-1.69.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9a281878feeb9ae26db0622a19add03922a028d4db684658f16d546601a4870"}, + {file = "grpcio-1.69.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cc614e895177ab7e4b70f154d1a7c97e152577ea101d76026d132b7aaba003b"}, + {file = "grpcio-1.69.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:1ee76cd7e2e49cf9264f6812d8c9ac1b85dda0eaea063af07292400f9191750e"}, + {file = "grpcio-1.69.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:0470fa911c503af59ec8bc4c82b371ee4303ececbbdc055f55ce48e38b20fd67"}, + {file = "grpcio-1.69.0-cp313-cp313-win32.whl", hash = "sha256:b650f34aceac8b2d08a4c8d7dc3e8a593f4d9e26d86751ebf74ebf5107d927de"}, + {file = "grpcio-1.69.0-cp313-cp313-win_amd64.whl", hash = "sha256:028337786f11fecb5d7b7fa660475a06aabf7e5e52b5ac2df47414878c0ce7ea"}, + {file = "grpcio-1.69.0-cp38-cp38-linux_armv7l.whl", hash = "sha256:b7f693db593d6bf285e015d5538bf1c86cf9c60ed30b6f7da04a00ed052fe2f3"}, + {file = "grpcio-1.69.0-cp38-cp38-macosx_10_14_universal2.whl", hash = "sha256:8b94e83f66dbf6fd642415faca0608590bc5e8d30e2c012b31d7d1b91b1de2fd"}, + {file = "grpcio-1.69.0-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:b634851b92c090763dde61df0868c730376cdb73a91bcc821af56ae043b09596"}, + {file = "grpcio-1.69.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bf5f680d3ed08c15330d7830d06bc65f58ca40c9999309517fd62880d70cb06e"}, + {file = "grpcio-1.69.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:200e48a6e7b00f804cf00a1c26292a5baa96507c7749e70a3ec10ca1a288936e"}, + {file = "grpcio-1.69.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:45a4704339b6e5b24b0e136dea9ad3815a94f30eb4f1e1d44c4ac484ef11d8dd"}, + {file = "grpcio-1.69.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:85d347cb8237751b23539981dbd2d9d8f6e9ff90082b427b13022b948eb6347a"}, + {file = "grpcio-1.69.0-cp38-cp38-win32.whl", hash = "sha256:60e5de105dc02832dc8f120056306d0ef80932bcf1c0e2b4ca3b676de6dc6505"}, + {file = "grpcio-1.69.0-cp38-cp38-win_amd64.whl", hash = "sha256:282f47d0928e40f25d007f24eb8fa051cb22551e3c74b8248bc9f9bea9c35fe0"}, + {file = "grpcio-1.69.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:dd034d68a2905464c49479b0c209c773737a4245d616234c79c975c7c90eca03"}, + {file = "grpcio-1.69.0-cp39-cp39-macosx_10_14_universal2.whl", hash = "sha256:01f834732c22a130bdf3dc154d1053bdbc887eb3ccb7f3e6285cfbfc33d9d5cc"}, + {file = "grpcio-1.69.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:a7f4ed0dcf202a70fe661329f8874bc3775c14bb3911d020d07c82c766ce0eb1"}, + {file = "grpcio-1.69.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd7ea241b10bc5f0bb0f82c0d7896822b7ed122b3ab35c9851b440c1ccf81588"}, + {file = "grpcio-1.69.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f03dc9b4da4c0dc8a1db7a5420f575251d7319b7a839004d8916257ddbe4816"}, + {file = "grpcio-1.69.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ca71d73a270dff052fe4edf74fef142d6ddd1f84175d9ac4a14b7280572ac519"}, + {file = "grpcio-1.69.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5ccbed100dc43704e94ccff9e07680b540d64e4cc89213ab2832b51b4f68a520"}, + {file = "grpcio-1.69.0-cp39-cp39-win32.whl", hash = "sha256:1514341def9c6ec4b7f0b9628be95f620f9d4b99331b7ef0a1845fd33d9b579c"}, + {file = "grpcio-1.69.0-cp39-cp39-win_amd64.whl", hash = "sha256:c1fea55d26d647346acb0069b08dca70984101f2dc95066e003019207212e303"}, + {file = "grpcio-1.69.0.tar.gz", hash = "sha256:936fa44241b5379c5afc344e1260d467bee495747eaf478de825bab2791da6f5"}, ] [package.extras] -protobuf = ["grpcio-tools (>=1.68.1)"] +protobuf = ["grpcio-tools (>=1.69.0)"] [[package]] name = "grpcio-status" -version = "1.68.1" +version = "1.69.0" description = "Status proto mapping for gRPC" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "grpcio_status-1.68.1-py3-none-any.whl", hash = "sha256:66f3d8847f665acfd56221333d66f7ad8927903d87242a482996bdb45e8d28fd"}, - {file = "grpcio_status-1.68.1.tar.gz", hash = "sha256:e1378d036c81a1610d7b4c7a146cd663dd13fcc915cf4d7d053929dba5bbb6e1"}, + {file = "grpcio_status-1.69.0-py3-none-any.whl", hash = "sha256:d6b2a3c9562c03a817c628d7ba9a925e209c228762d6d7677ae5c9401a542853"}, + {file = "grpcio_status-1.69.0.tar.gz", hash = "sha256:595ef84e5178d6281caa732ccf68ff83259241608d26b0e9c40a5e66eee2a2d2"}, ] [package.dependencies] googleapis-common-protos = ">=1.5.5" -grpcio = ">=1.68.1" +grpcio = ">=1.69.0" protobuf = ">=5.26.1,<6.0dev" [[package]] @@ -1015,6 +1054,8 @@ version = "22.0.0" description = "WSGI HTTP Server for UNIX" optional = false python-versions = ">=3.7" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "gunicorn-22.0.0-py3-none-any.whl", hash = "sha256:350679f91b24062c86e386e198a15438d53a7a8207235a78ba1b53df4c4378d9"}, {file = "gunicorn-22.0.0.tar.gz", hash = "sha256:4a0b436239ff76fb33f11c07a16482c521a7e09c1ce3cc293c2330afe01bec63"}, @@ -1032,36 +1073,43 @@ tornado = ["tornado (>=0.2)"] [[package]] name = "h5py" -version = "3.11.0" +version = "3.12.1" description = "Read and write HDF5 files from Python" optional = true -python-versions = ">=3.8" -files = [ - {file = "h5py-3.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1625fd24ad6cfc9c1ccd44a66dac2396e7ee74940776792772819fc69f3a3731"}, - {file = "h5py-3.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c072655ad1d5fe9ef462445d3e77a8166cbfa5e599045f8aa3c19b75315f10e5"}, - {file = "h5py-3.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77b19a40788e3e362b54af4dcf9e6fde59ca016db2c61360aa30b47c7b7cef00"}, - {file = "h5py-3.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:ef4e2f338fc763f50a8113890f455e1a70acd42a4d083370ceb80c463d803972"}, - {file = "h5py-3.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bbd732a08187a9e2a6ecf9e8af713f1d68256ee0f7c8b652a32795670fb481ba"}, - {file = "h5py-3.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75bd7b3d93fbeee40860fd70cdc88df4464e06b70a5ad9ce1446f5f32eb84007"}, - {file = "h5py-3.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52c416f8eb0daae39dabe71415cb531f95dce2d81e1f61a74537a50c63b28ab3"}, - {file = "h5py-3.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:083e0329ae534a264940d6513f47f5ada617da536d8dccbafc3026aefc33c90e"}, - {file = "h5py-3.11.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a76cae64080210389a571c7d13c94a1a6cf8cb75153044fd1f822a962c97aeab"}, - {file = "h5py-3.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3736fe21da2b7d8a13fe8fe415f1272d2a1ccdeff4849c1421d2fb30fd533bc"}, - {file = "h5py-3.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa6ae84a14103e8dc19266ef4c3e5d7c00b68f21d07f2966f0ca7bdb6c2761fb"}, - {file = "h5py-3.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:21dbdc5343f53b2e25404673c4f00a3335aef25521bd5fa8c707ec3833934892"}, - {file = "h5py-3.11.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:754c0c2e373d13d6309f408325343b642eb0f40f1a6ad21779cfa9502209e150"}, - {file = "h5py-3.11.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:731839240c59ba219d4cb3bc5880d438248533366f102402cfa0621b71796b62"}, - {file = "h5py-3.11.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ec9df3dd2018904c4cc06331951e274f3f3fd091e6d6cc350aaa90fa9b42a76"}, - {file = "h5py-3.11.0-cp38-cp38-win_amd64.whl", hash = "sha256:55106b04e2c83dfb73dc8732e9abad69d83a436b5b82b773481d95d17b9685e1"}, - {file = "h5py-3.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f4e025e852754ca833401777c25888acb96889ee2c27e7e629a19aee288833f0"}, - {file = "h5py-3.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c4b760082626120031d7902cd983d8c1f424cdba2809f1067511ef283629d4b"}, - {file = "h5py-3.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67462d0669f8f5459529de179f7771bd697389fcb3faab54d63bf788599a48ea"}, - {file = "h5py-3.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:d9c944d364688f827dc889cf83f1fca311caf4fa50b19f009d1f2b525edd33a3"}, - {file = "h5py-3.11.0.tar.gz", hash = "sha256:7b7e8f78072a2edec87c9836f25f34203fd492a4475709a18b417a33cfb21fa9"}, +python-versions = ">=3.9" +groups = ["main"] +markers = "(python_version <= \"3.11\" or python_version >= \"3.12\") and extra == \"hdf5\"" +files = [ + {file = "h5py-3.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f0f1a382cbf494679c07b4371f90c70391dedb027d517ac94fa2c05299dacda"}, + {file = "h5py-3.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cb65f619dfbdd15e662423e8d257780f9a66677eae5b4b3fc9dca70b5fd2d2a3"}, + {file = "h5py-3.12.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b15d8dbd912c97541312c0e07438864d27dbca857c5ad634de68110c6beb1c2"}, + {file = "h5py-3.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59685fe40d8c1fbbee088c88cd4da415a2f8bee5c270337dc5a1c4aa634e3307"}, + {file = "h5py-3.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:577d618d6b6dea3da07d13cc903ef9634cde5596b13e832476dd861aaf651f3e"}, + {file = "h5py-3.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ccd9006d92232727d23f784795191bfd02294a4f2ba68708825cb1da39511a93"}, + {file = "h5py-3.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ad8a76557880aed5234cfe7279805f4ab5ce16b17954606cca90d578d3e713ef"}, + {file = "h5py-3.12.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1473348139b885393125126258ae2d70753ef7e9cec8e7848434f385ae72069e"}, + {file = "h5py-3.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:018a4597f35092ae3fb28ee851fdc756d2b88c96336b8480e124ce1ac6fb9166"}, + {file = "h5py-3.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:3fdf95092d60e8130ba6ae0ef7a9bd4ade8edbe3569c13ebbaf39baefffc5ba4"}, + {file = "h5py-3.12.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:06a903a4e4e9e3ebbc8b548959c3c2552ca2d70dac14fcfa650d9261c66939ed"}, + {file = "h5py-3.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7b3b8f3b48717e46c6a790e3128d39c61ab595ae0a7237f06dfad6a3b51d5351"}, + {file = "h5py-3.12.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:050a4f2c9126054515169c49cb900949814987f0c7ae74c341b0c9f9b5056834"}, + {file = "h5py-3.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c4b41d1019322a5afc5082864dfd6359f8935ecd37c11ac0029be78c5d112c9"}, + {file = "h5py-3.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:e4d51919110a030913201422fb07987db4338eba5ec8c5a15d6fab8e03d443fc"}, + {file = "h5py-3.12.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:513171e90ed92236fc2ca363ce7a2fc6f2827375efcbb0cc7fbdd7fe11fecafc"}, + {file = "h5py-3.12.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:59400f88343b79655a242068a9c900001a34b63e3afb040bd7cdf717e440f653"}, + {file = "h5py-3.12.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3e465aee0ec353949f0f46bf6c6f9790a2006af896cee7c178a8c3e5090aa32"}, + {file = "h5py-3.12.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba51c0c5e029bb5420a343586ff79d56e7455d496d18a30309616fdbeed1068f"}, + {file = "h5py-3.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:52ab036c6c97055b85b2a242cb540ff9590bacfda0c03dd0cf0661b311f522f8"}, + {file = "h5py-3.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d2b8dd64f127d8b324f5d2cd1c0fd6f68af69084e9e47d27efeb9e28e685af3e"}, + {file = "h5py-3.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4532c7e97fbef3d029735db8b6f5bf01222d9ece41e309b20d63cfaae2fb5c4d"}, + {file = "h5py-3.12.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fdf6d7936fa824acfa27305fe2d9f39968e539d831c5bae0e0d83ed521ad1ac"}, + {file = "h5py-3.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84342bffd1f82d4f036433e7039e241a243531a1d3acd7341b35ae58cdab05bf"}, + {file = "h5py-3.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:62be1fc0ef195891949b2c627ec06bc8e837ff62d5b911b6e42e38e0f20a897d"}, + {file = "h5py-3.12.1.tar.gz", hash = "sha256:326d70b53d31baa61f00b8aa5f95c2fcb9621a3ee8365d770c551a13dbbcbfdf"}, ] [package.dependencies] -numpy = ">=1.17.3" +numpy = ">=1.19.3" [[package]] name = "html5lib" @@ -1069,6 +1117,8 @@ version = "1.1" description = "HTML parser based on the WHATWG HTML specification" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "html5lib-1.1-py2.py3-none-any.whl", hash = "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d"}, {file = "html5lib-1.1.tar.gz", hash = "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f"}, @@ -1086,13 +1136,15 @@ lxml = ["lxml"] [[package]] name = "identify" -version = "2.6.1" +version = "2.6.5" description = "File identification library for Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0"}, - {file = "identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98"}, + {file = "identify-2.6.5-py2.py3-none-any.whl", hash = "sha256:14181a47091eb75b337af4c23078c9d09225cd4c48929f521f3bf16b09d02566"}, + {file = "identify-2.6.5.tar.gz", hash = "sha256:c10b33f250e5bba374fae86fb57f3adcebf1161bce7cdf92031915fd480c13bc"}, ] [package.extras] @@ -1104,6 +1156,8 @@ version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" +groups = ["main", "dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -1118,6 +1172,8 @@ version = "1.4.1" description = "Getting image size from png/jpeg/jpeg2000/gif file" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, @@ -1129,6 +1185,8 @@ version = "8.5.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, @@ -1146,34 +1204,14 @@ perf = ["ipython"] test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] type = ["pytest-mypy"] -[[package]] -name = "importlib-resources" -version = "6.4.5" -description = "Read resources from Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "importlib_resources-6.4.5-py3-none-any.whl", hash = "sha256:ac29d5f956f01d5e4bb63102a5a19957f1b9175e45649977264a1416783bb717"}, - {file = "importlib_resources-6.4.5.tar.gz", hash = "sha256:980862a1d16c9e147a59603677fa2aa5fd82b87f223b6cb870695bcfce830065"}, -] - -[package.dependencies] -zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["jaraco.test (>=5.4)", "pytest (>=6,!=8.1.*)", "zipp (>=3.17)"] -type = ["pytest-mypy"] - [[package]] name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, @@ -1185,6 +1223,8 @@ version = "2.2.0" description = "Safely pass data to untrusted environments and back." optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, @@ -1192,13 +1232,15 @@ files = [ [[package]] name = "jinja2" -version = "3.1.4" +version = "3.1.5" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" +groups = ["main", "dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, - {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, + {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"}, + {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, ] [package.dependencies] @@ -1213,6 +1255,8 @@ version = "4.23.0" description = "An implementation of JSON Schema validation for Python" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566"}, {file = "jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4"}, @@ -1220,9 +1264,7 @@ files = [ [package.dependencies] attrs = ">=22.2.0" -importlib-resources = {version = ">=1.4.0", markers = "python_version < \"3.9\""} jsonschema-specifications = ">=2023.03.6" -pkgutil-resolve-name = {version = ">=1.3.10", markers = "python_version < \"3.9\""} referencing = ">=0.28.4" rpds-py = ">=0.7.1" @@ -1232,86 +1274,90 @@ format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339- [[package]] name = "jsonschema-specifications" -version = "2023.12.1" +version = "2024.10.1" description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "jsonschema_specifications-2023.12.1-py3-none-any.whl", hash = "sha256:87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c"}, - {file = "jsonschema_specifications-2023.12.1.tar.gz", hash = "sha256:48a76787b3e70f5ed53f1160d2b81f586e4ca6d1548c5de7085d1682674764cc"}, + {file = "jsonschema_specifications-2024.10.1-py3-none-any.whl", hash = "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf"}, + {file = "jsonschema_specifications-2024.10.1.tar.gz", hash = "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272"}, ] [package.dependencies] -importlib-resources = {version = ">=1.4.0", markers = "python_version < \"3.9\""} referencing = ">=0.31.0" [[package]] name = "markupsafe" -version = "2.1.5" +version = "3.0.2" description = "Safely add untrusted strings to HTML/XML markup." optional = false -python-versions = ">=3.7" -files = [ - {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, - {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, +python-versions = ">=3.9" +groups = ["main", "dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, ] [[package]] @@ -1320,6 +1366,8 @@ version = "10.5.0" description = "More routines for operating on iterables, beyond itertools" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "more-itertools-10.5.0.tar.gz", hash = "sha256:5482bfef7849c25dc3c6dd53a6173ae4795da2a41a80faea6700d9f5846c5da6"}, {file = "more_itertools-10.5.0-py3-none-any.whl", hash = "sha256:037b0d3203ce90cca8ab1defbbdac29d5f993fc20131f3664dc8d6acfa872aef"}, @@ -1331,6 +1379,8 @@ version = "1.1.0" description = "MessagePack serializer" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7ad442d527a7e358a469faf43fda45aaf4ac3249c8310a82f0ccff9164e5dccd"}, {file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:74bed8f63f8f14d75eec75cf3d04ad581da6b914001b474a5d3cd3372c8cc27d"}, @@ -1404,6 +1454,8 @@ version = "8.4.0" description = "Simple yet flexible natural sorting in Python." optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "natsort-8.4.0-py3-none-any.whl", hash = "sha256:4732914fb471f56b5cce04d7bae6f164a592c7712e1c85f9ef585e197299521c"}, {file = "natsort-8.4.0.tar.gz", hash = "sha256:45312c4a0e5507593da193dedd04abb1469253b601ecaf63445ad80f0a1ea581"}, @@ -1419,6 +1471,8 @@ version = "1.9.1" description = "Node.js virtual environment builder" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, @@ -1426,39 +1480,68 @@ files = [ [[package]] name = "numpy" -version = "1.24.4" +version = "2.2.1" description = "Fundamental package for array computing in Python" optional = false -python-versions = ">=3.8" -files = [ - {file = "numpy-1.24.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0bfb52d2169d58c1cdb8cc1f16989101639b34c7d3ce60ed70b19c63eba0b64"}, - {file = "numpy-1.24.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed094d4f0c177b1b8e7aa9cba7d6ceed51c0e569a5318ac0ca9a090680a6a1b1"}, - {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79fc682a374c4a8ed08b331bef9c5f582585d1048fa6d80bc6c35bc384eee9b4"}, - {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ffe43c74893dbf38c2b0a1f5428760a1a9c98285553c89e12d70a96a7f3a4d6"}, - {file = "numpy-1.24.4-cp310-cp310-win32.whl", hash = "sha256:4c21decb6ea94057331e111a5bed9a79d335658c27ce2adb580fb4d54f2ad9bc"}, - {file = "numpy-1.24.4-cp310-cp310-win_amd64.whl", hash = "sha256:b4bea75e47d9586d31e892a7401f76e909712a0fd510f58f5337bea9572c571e"}, - {file = "numpy-1.24.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f136bab9c2cfd8da131132c2cf6cc27331dd6fae65f95f69dcd4ae3c3639c810"}, - {file = "numpy-1.24.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2926dac25b313635e4d6cf4dc4e51c8c0ebfed60b801c799ffc4c32bf3d1254"}, - {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:222e40d0e2548690405b0b3c7b21d1169117391c2e82c378467ef9ab4c8f0da7"}, - {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7215847ce88a85ce39baf9e89070cb860c98fdddacbaa6c0da3ffb31b3350bd5"}, - {file = "numpy-1.24.4-cp311-cp311-win32.whl", hash = "sha256:4979217d7de511a8d57f4b4b5b2b965f707768440c17cb70fbf254c4b225238d"}, - {file = "numpy-1.24.4-cp311-cp311-win_amd64.whl", hash = "sha256:b7b1fc9864d7d39e28f41d089bfd6353cb5f27ecd9905348c24187a768c79694"}, - {file = "numpy-1.24.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1452241c290f3e2a312c137a9999cdbf63f78864d63c79039bda65ee86943f61"}, - {file = "numpy-1.24.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:04640dab83f7c6c85abf9cd729c5b65f1ebd0ccf9de90b270cd61935eef0197f"}, - {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5425b114831d1e77e4b5d812b69d11d962e104095a5b9c3b641a218abcc050e"}, - {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd80e219fd4c71fc3699fc1dadac5dcf4fd882bfc6f7ec53d30fa197b8ee22dc"}, - {file = "numpy-1.24.4-cp38-cp38-win32.whl", hash = "sha256:4602244f345453db537be5314d3983dbf5834a9701b7723ec28923e2889e0bb2"}, - {file = "numpy-1.24.4-cp38-cp38-win_amd64.whl", hash = "sha256:692f2e0f55794943c5bfff12b3f56f99af76f902fc47487bdfe97856de51a706"}, - {file = "numpy-1.24.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2541312fbf09977f3b3ad449c4e5f4bb55d0dbf79226d7724211acc905049400"}, - {file = "numpy-1.24.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9667575fb6d13c95f1b36aca12c5ee3356bf001b714fc354eb5465ce1609e62f"}, - {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a86ed21e4f87050382c7bc96571755193c4c1392490744ac73d660e8f564a9"}, - {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d11efb4dbecbdf22508d55e48d9c8384db795e1b7b51ea735289ff96613ff74d"}, - {file = "numpy-1.24.4-cp39-cp39-win32.whl", hash = "sha256:6620c0acd41dbcb368610bb2f4d83145674040025e5536954782467100aa8835"}, - {file = "numpy-1.24.4-cp39-cp39-win_amd64.whl", hash = "sha256:befe2bf740fd8373cf56149a5c23a0f601e82869598d41f8e188a0e9869926f8"}, - {file = "numpy-1.24.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:31f13e25b4e304632a4619d0e0777662c2ffea99fcae2029556b17d8ff958aef"}, - {file = "numpy-1.24.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95f7ac6540e95bc440ad77f56e520da5bf877f87dca58bd095288dce8940532a"}, - {file = "numpy-1.24.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e98f220aa76ca2a977fe435f5b04d7b3470c0a2e6312907b37ba6068f26787f2"}, - {file = "numpy-1.24.4.tar.gz", hash = "sha256:80f5e3a4e498641401868df4208b74581206afbee7cf7b8329daae82676d9463"}, +python-versions = ">=3.10" +groups = ["main", "dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "numpy-2.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5edb4e4caf751c1518e6a26a83501fda79bff41cc59dac48d70e6d65d4ec4440"}, + {file = "numpy-2.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aa3017c40d513ccac9621a2364f939d39e550c542eb2a894b4c8da92b38896ab"}, + {file = "numpy-2.2.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:61048b4a49b1c93fe13426e04e04fdf5a03f456616f6e98c7576144677598675"}, + {file = "numpy-2.2.1-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:7671dc19c7019103ca44e8d94917eba8534c76133523ca8406822efdd19c9308"}, + {file = "numpy-2.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4250888bcb96617e00bfa28ac24850a83c9f3a16db471eca2ee1f1714df0f957"}, + {file = "numpy-2.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7746f235c47abc72b102d3bce9977714c2444bdfaea7888d241b4c4bb6a78bf"}, + {file = "numpy-2.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:059e6a747ae84fce488c3ee397cee7e5f905fd1bda5fb18c66bc41807ff119b2"}, + {file = "numpy-2.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f62aa6ee4eb43b024b0e5a01cf65a0bb078ef8c395e8713c6e8a12a697144528"}, + {file = "numpy-2.2.1-cp310-cp310-win32.whl", hash = "sha256:48fd472630715e1c1c89bf1feab55c29098cb403cc184b4859f9c86d4fcb6a95"}, + {file = "numpy-2.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:b541032178a718c165a49638d28272b771053f628382d5e9d1c93df23ff58dbf"}, + {file = "numpy-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:40f9e544c1c56ba8f1cf7686a8c9b5bb249e665d40d626a23899ba6d5d9e1484"}, + {file = "numpy-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9b57eaa3b0cd8db52049ed0330747b0364e899e8a606a624813452b8203d5f7"}, + {file = "numpy-2.2.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:bc8a37ad5b22c08e2dbd27df2b3ef7e5c0864235805b1e718a235bcb200cf1cb"}, + {file = "numpy-2.2.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:9036d6365d13b6cbe8f27a0eaf73ddcc070cae584e5ff94bb45e3e9d729feab5"}, + {file = "numpy-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51faf345324db860b515d3f364eaa93d0e0551a88d6218a7d61286554d190d73"}, + {file = "numpy-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38efc1e56b73cc9b182fe55e56e63b044dd26a72128fd2fbd502f75555d92591"}, + {file = "numpy-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:31b89fa67a8042e96715c68e071a1200c4e172f93b0fbe01a14c0ff3ff820fc8"}, + {file = "numpy-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4c86e2a209199ead7ee0af65e1d9992d1dce7e1f63c4b9a616500f93820658d0"}, + {file = "numpy-2.2.1-cp311-cp311-win32.whl", hash = "sha256:b34d87e8a3090ea626003f87f9392b3929a7bbf4104a05b6667348b6bd4bf1cd"}, + {file = "numpy-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:360137f8fb1b753c5cde3ac388597ad680eccbbbb3865ab65efea062c4a1fd16"}, + {file = "numpy-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:694f9e921a0c8f252980e85bce61ebbd07ed2b7d4fa72d0e4246f2f8aa6642ab"}, + {file = "numpy-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3683a8d166f2692664262fd4900f207791d005fb088d7fdb973cc8d663626faa"}, + {file = "numpy-2.2.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:780077d95eafc2ccc3ced969db22377b3864e5b9a0ea5eb347cc93b3ea900315"}, + {file = "numpy-2.2.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:55ba24ebe208344aa7a00e4482f65742969a039c2acfcb910bc6fcd776eb4355"}, + {file = "numpy-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b1d07b53b78bf84a96898c1bc139ad7f10fda7423f5fd158fd0f47ec5e01ac7"}, + {file = "numpy-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5062dc1a4e32a10dc2b8b13cedd58988261416e811c1dc4dbdea4f57eea61b0d"}, + {file = "numpy-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:fce4f615f8ca31b2e61aa0eb5865a21e14f5629515c9151850aa936c02a1ee51"}, + {file = "numpy-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:67d4cda6fa6ffa073b08c8372aa5fa767ceb10c9a0587c707505a6d426f4e046"}, + {file = "numpy-2.2.1-cp312-cp312-win32.whl", hash = "sha256:32cb94448be47c500d2c7a95f93e2f21a01f1fd05dd2beea1ccd049bb6001cd2"}, + {file = "numpy-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:ba5511d8f31c033a5fcbda22dd5c813630af98c70b2661f2d2c654ae3cdfcfc8"}, + {file = "numpy-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f1d09e520217618e76396377c81fba6f290d5f926f50c35f3a5f72b01a0da780"}, + {file = "numpy-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ecc47cd7f6ea0336042be87d9e7da378e5c7e9b3c8ad0f7c966f714fc10d821"}, + {file = "numpy-2.2.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f419290bc8968a46c4933158c91a0012b7a99bb2e465d5ef5293879742f8797e"}, + {file = "numpy-2.2.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5b6c390bfaef8c45a260554888966618328d30e72173697e5cabe6b285fb2348"}, + {file = "numpy-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:526fc406ab991a340744aad7e25251dd47a6720a685fa3331e5c59fef5282a59"}, + {file = "numpy-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f74e6fdeb9a265624ec3a3918430205dff1df7e95a230779746a6af78bc615af"}, + {file = "numpy-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:53c09385ff0b72ba79d8715683c1168c12e0b6e84fb0372e97553d1ea91efe51"}, + {file = "numpy-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f3eac17d9ec51be534685ba877b6ab5edc3ab7ec95c8f163e5d7b39859524716"}, + {file = "numpy-2.2.1-cp313-cp313-win32.whl", hash = "sha256:9ad014faa93dbb52c80d8f4d3dcf855865c876c9660cb9bd7553843dd03a4b1e"}, + {file = "numpy-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:164a829b6aacf79ca47ba4814b130c4020b202522a93d7bff2202bfb33b61c60"}, + {file = "numpy-2.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4dfda918a13cc4f81e9118dea249e192ab167a0bb1966272d5503e39234d694e"}, + {file = "numpy-2.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:733585f9f4b62e9b3528dd1070ec4f52b8acf64215b60a845fa13ebd73cd0712"}, + {file = "numpy-2.2.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:89b16a18e7bba224ce5114db863e7029803c179979e1af6ad6a6b11f70545008"}, + {file = "numpy-2.2.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:676f4eebf6b2d430300f1f4f4c2461685f8269f94c89698d832cdf9277f30b84"}, + {file = "numpy-2.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27f5cdf9f493b35f7e41e8368e7d7b4bbafaf9660cba53fb21d2cd174ec09631"}, + {file = "numpy-2.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1ad395cf254c4fbb5b2132fee391f361a6e8c1adbd28f2cd8e79308a615fe9d"}, + {file = "numpy-2.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:08ef779aed40dbc52729d6ffe7dd51df85796a702afbf68a4f4e41fafdc8bda5"}, + {file = "numpy-2.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:26c9c4382b19fcfbbed3238a14abf7ff223890ea1936b8890f058e7ba35e8d71"}, + {file = "numpy-2.2.1-cp313-cp313t-win32.whl", hash = "sha256:93cf4e045bae74c90ca833cba583c14b62cb4ba2cba0abd2b141ab52548247e2"}, + {file = "numpy-2.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:bff7d8ec20f5f42607599f9994770fa65d76edca264a87b5e4ea5629bce12268"}, + {file = "numpy-2.2.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7ba9cc93a91d86365a5d270dee221fdc04fb68d7478e6bf6af650de78a8339e3"}, + {file = "numpy-2.2.1-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:3d03883435a19794e41f147612a77a8f56d4e52822337844fff3d4040a142964"}, + {file = "numpy-2.2.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4511d9e6071452b944207c8ce46ad2f897307910b402ea5fa975da32e0102800"}, + {file = "numpy-2.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5c5cc0cbabe9452038ed984d05ac87910f89370b9242371bd9079cb4af61811e"}, + {file = "numpy-2.2.1.tar.gz", hash = "sha256:45681fd7128c8ad1c379f0ca0776a8b0c6583d2f69889ddac01559dfe4390918"}, ] [[package]] @@ -1467,6 +1550,8 @@ version = "1.29.0" description = "OpenTelemetry Python API" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "opentelemetry_api-1.29.0-py3-none-any.whl", hash = "sha256:5fcd94c4141cc49c736271f3e1efb777bebe9cc535759c54c936cca4f1b312b8"}, {file = "opentelemetry_api-1.29.0.tar.gz", hash = "sha256:d04a6cf78aad09614f52964ecb38021e248f5714dc32c2e0d8fd99517b4d69cf"}, @@ -1482,6 +1567,8 @@ version = "1.29.0" description = "OpenTelemetry Python SDK" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "opentelemetry_sdk-1.29.0-py3-none-any.whl", hash = "sha256:173be3b5d3f8f7d671f20ea37056710217959e774e2749d984355d1f9391a30a"}, {file = "opentelemetry_sdk-1.29.0.tar.gz", hash = "sha256:b0787ce6aade6ab84315302e72bd7a7f2f014b0fb1b7c3295b88afe014ed0643"}, @@ -1498,6 +1585,8 @@ version = "0.50b0" description = "OpenTelemetry Semantic Conventions" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "opentelemetry_semantic_conventions-0.50b0-py3-none-any.whl", hash = "sha256:e87efba8fdb67fb38113efea6a349531e75ed7ffc01562f65b802fcecb5e115e"}, {file = "opentelemetry_semantic_conventions-0.50b0.tar.gz", hash = "sha256:02dc6dbcb62f082de9b877ff19a3f1ffaa3c306300fa53bfac761c4567c83d38"}, @@ -1513,6 +1602,8 @@ version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, @@ -1520,62 +1611,91 @@ files = [ [[package]] name = "pandas" -version = "1.5.3" +version = "2.2.3" description = "Powerful data structures for data analysis, time series, and statistics" optional = false -python-versions = ">=3.8" -files = [ - {file = "pandas-1.5.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3749077d86e3a2f0ed51367f30bf5b82e131cc0f14260c4d3e499186fccc4406"}, - {file = "pandas-1.5.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:972d8a45395f2a2d26733eb8d0f629b2f90bebe8e8eddbb8829b180c09639572"}, - {file = "pandas-1.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:50869a35cbb0f2e0cd5ec04b191e7b12ed688874bd05dd777c19b28cbea90996"}, - {file = "pandas-1.5.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3ac844a0fe00bfaeb2c9b51ab1424e5c8744f89860b138434a363b1f620f354"}, - {file = "pandas-1.5.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a0a56cef15fd1586726dace5616db75ebcfec9179a3a55e78f72c5639fa2a23"}, - {file = "pandas-1.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:478ff646ca42b20376e4ed3fa2e8d7341e8a63105586efe54fa2508ee087f328"}, - {file = "pandas-1.5.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6973549c01ca91ec96199e940495219c887ea815b2083722821f1d7abfa2b4dc"}, - {file = "pandas-1.5.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c39a8da13cede5adcd3be1182883aea1c925476f4e84b2807a46e2775306305d"}, - {file = "pandas-1.5.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f76d097d12c82a535fda9dfe5e8dd4127952b45fea9b0276cb30cca5ea313fbc"}, - {file = "pandas-1.5.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e474390e60ed609cec869b0da796ad94f420bb057d86784191eefc62b65819ae"}, - {file = "pandas-1.5.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f2b952406a1588ad4cad5b3f55f520e82e902388a6d5a4a91baa8d38d23c7f6"}, - {file = "pandas-1.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:bc4c368f42b551bf72fac35c5128963a171b40dce866fb066540eeaf46faa003"}, - {file = "pandas-1.5.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:14e45300521902689a81f3f41386dc86f19b8ba8dd5ac5a3c7010ef8d2932813"}, - {file = "pandas-1.5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9842b6f4b8479e41968eced654487258ed81df7d1c9b7b870ceea24ed9459b31"}, - {file = "pandas-1.5.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:26d9c71772c7afb9d5046e6e9cf42d83dd147b5cf5bcb9d97252077118543792"}, - {file = "pandas-1.5.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fbcb19d6fceb9e946b3e23258757c7b225ba450990d9ed63ccceeb8cae609f7"}, - {file = "pandas-1.5.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:565fa34a5434d38e9d250af3c12ff931abaf88050551d9fbcdfafca50d62babf"}, - {file = "pandas-1.5.3-cp38-cp38-win32.whl", hash = "sha256:87bd9c03da1ac870a6d2c8902a0e1fd4267ca00f13bc494c9e5a9020920e1d51"}, - {file = "pandas-1.5.3-cp38-cp38-win_amd64.whl", hash = "sha256:41179ce559943d83a9b4bbacb736b04c928b095b5f25dd2b7389eda08f46f373"}, - {file = "pandas-1.5.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c74a62747864ed568f5a82a49a23a8d7fe171d0c69038b38cedf0976831296fa"}, - {file = "pandas-1.5.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c4c00e0b0597c8e4f59e8d461f797e5d70b4d025880516a8261b2817c47759ee"}, - {file = "pandas-1.5.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a50d9a4336a9621cab7b8eb3fb11adb82de58f9b91d84c2cd526576b881a0c5a"}, - {file = "pandas-1.5.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd05f7783b3274aa206a1af06f0ceed3f9b412cf665b7247eacd83be41cf7bf0"}, - {file = "pandas-1.5.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f69c4029613de47816b1bb30ff5ac778686688751a5e9c99ad8c7031f6508e5"}, - {file = "pandas-1.5.3-cp39-cp39-win32.whl", hash = "sha256:7cec0bee9f294e5de5bbfc14d0573f65526071029d036b753ee6507d2a21480a"}, - {file = "pandas-1.5.3-cp39-cp39-win_amd64.whl", hash = "sha256:dfd681c5dc216037e0b0a2c821f5ed99ba9f03ebcf119c7dac0e9a7b960b9ec9"}, - {file = "pandas-1.5.3.tar.gz", hash = "sha256:74a3fd7e5a7ec052f183273dc7b0acd3a863edf7520f5d3a1765c04ffdb3b0b1"}, +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5"}, + {file = "pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348"}, + {file = "pandas-2.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed"}, + {file = "pandas-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57"}, + {file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42"}, + {file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f"}, + {file = "pandas-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645"}, + {file = "pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039"}, + {file = "pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd"}, + {file = "pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698"}, + {file = "pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc"}, + {file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3"}, + {file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32"}, + {file = "pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5"}, + {file = "pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9"}, + {file = "pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4"}, + {file = "pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3"}, + {file = "pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319"}, + {file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8"}, + {file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a"}, + {file = "pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13"}, + {file = "pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015"}, + {file = "pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28"}, + {file = "pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0"}, + {file = "pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24"}, + {file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659"}, + {file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb"}, + {file = "pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d"}, + {file = "pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468"}, + {file = "pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18"}, + {file = "pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2"}, + {file = "pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4"}, + {file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d"}, + {file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a"}, + {file = "pandas-2.2.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc6b93f9b966093cb0fd62ff1a7e4c09e6d546ad7c1de191767baffc57628f39"}, + {file = "pandas-2.2.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5dbca4c1acd72e8eeef4753eeca07de9b1db4f398669d5994086f788a5d7cc30"}, + {file = "pandas-2.2.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8cd6d7cc958a3910f934ea8dbdf17b2364827bb4dafc38ce6eef6bb3d65ff09c"}, + {file = "pandas-2.2.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99df71520d25fade9db7c1076ac94eb994f4d2673ef2aa2e86ee039b6746d20c"}, + {file = "pandas-2.2.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:31d0ced62d4ea3e231a9f228366919a5ea0b07440d9d4dac345376fd8e1477ea"}, + {file = "pandas-2.2.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7eee9e7cea6adf3e3d24e304ac6b8300646e2a5d1cd3a3c2abed9101b0846761"}, + {file = "pandas-2.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:4850ba03528b6dd51d6c5d273c46f183f39a9baf3f0143e566b89450965b105e"}, + {file = "pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667"}, ] [package.dependencies] numpy = [ - {version = ">=1.23.2", markers = "python_version >= \"3.11\""}, - {version = ">=1.20.3", markers = "python_version < \"3.10\""}, - {version = ">=1.21.0", markers = "python_version >= \"3.10\" and python_version < \"3.11\""}, + {version = ">=1.22.4", markers = "python_version < \"3.11\""}, + {version = ">=1.23.2", markers = "python_version == \"3.11\""}, + {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, ] -python-dateutil = ">=2.8.1" +python-dateutil = ">=2.8.2" pytz = ">=2020.1" +tzdata = ">=2022.7" [package.extras] -test = ["hypothesis (>=5.5.3)", "pytest (>=6.0)", "pytest-xdist (>=1.31)"] - -[[package]] -name = "pkgutil-resolve-name" -version = "1.3.10" -description = "Resolve a name to an object." -optional = false -python-versions = ">=3.6" -files = [ - {file = "pkgutil_resolve_name-1.3.10-py3-none-any.whl", hash = "sha256:ca27cc078d25c5ad71a9de0a7a330146c4e014c2462d9af19c6b828280649c5e"}, - {file = "pkgutil_resolve_name-1.3.10.tar.gz", hash = "sha256:357d6c9e6a755653cfd78893817c0853af365dd51ec97f3d358a819373bbd174"}, -] +all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)", "beautifulsoup4 (>=4.11.2)", "bottleneck (>=1.3.6)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=2022.12.0)", "fsspec (>=2022.11.0)", "gcsfs (>=2022.11.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.9.2)", "matplotlib (>=3.6.3)", "numba (>=0.56.4)", "numexpr (>=2.8.4)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "pandas-gbq (>=0.19.0)", "psycopg2 (>=2.9.6)", "pyarrow (>=10.0.1)", "pymysql (>=1.0.2)", "pyreadstat (>=1.2.0)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "qtpy (>=2.3.0)", "s3fs (>=2022.11.0)", "scipy (>=1.10.0)", "tables (>=3.8.0)", "tabulate (>=0.9.0)", "xarray (>=2022.12.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)", "zstandard (>=0.19.0)"] +aws = ["s3fs (>=2022.11.0)"] +clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.3.0)"] +compression = ["zstandard (>=0.19.0)"] +computation = ["scipy (>=1.10.0)", "xarray (>=2022.12.0)"] +consortium-standard = ["dataframe-api-compat (>=0.1.7)"] +excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)"] +feather = ["pyarrow (>=10.0.1)"] +fss = ["fsspec (>=2022.11.0)"] +gcp = ["gcsfs (>=2022.11.0)", "pandas-gbq (>=0.19.0)"] +hdf5 = ["tables (>=3.8.0)"] +html = ["beautifulsoup4 (>=4.11.2)", "html5lib (>=1.1)", "lxml (>=4.9.2)"] +mysql = ["SQLAlchemy (>=2.0.0)", "pymysql (>=1.0.2)"] +output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.9.0)"] +parquet = ["pyarrow (>=10.0.1)"] +performance = ["bottleneck (>=1.3.6)", "numba (>=0.56.4)", "numexpr (>=2.8.4)"] +plot = ["matplotlib (>=3.6.3)"] +postgresql = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "psycopg2 (>=2.9.6)"] +pyarrow = ["pyarrow (>=10.0.1)"] +spss = ["pyreadstat (>=1.2.0)"] +sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)"] +test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] +xml = ["lxml (>=4.9.2)"] [[package]] name = "platformdirs" @@ -1583,6 +1703,8 @@ version = "4.3.6" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, @@ -1599,6 +1721,8 @@ version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -1614,6 +1738,8 @@ version = "2.21.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"}, {file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"}, @@ -1632,6 +1758,8 @@ version = "1.25.0" description = "Beautiful, Pythonic protocol buffers." optional = false python-versions = ">=3.7" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "proto_plus-1.25.0-py3-none-any.whl", hash = "sha256:c91fc4a65074ade8e458e95ef8bac34d4008daa7cce4a12d6707066fca648961"}, {file = "proto_plus-1.25.0.tar.gz", hash = "sha256:fbb17f57f7bd05a68b7707e745e26528b0b3c34e378db91eef93912c54982d91"}, @@ -1645,22 +1773,24 @@ testing = ["google-api-core (>=1.31.5)"] [[package]] name = "protobuf" -version = "5.29.1" +version = "5.29.3" description = "" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "protobuf-5.29.1-cp310-abi3-win32.whl", hash = "sha256:22c1f539024241ee545cbcb00ee160ad1877975690b16656ff87dde107b5f110"}, - {file = "protobuf-5.29.1-cp310-abi3-win_amd64.whl", hash = "sha256:1fc55267f086dd4050d18ef839d7bd69300d0d08c2a53ca7df3920cc271a3c34"}, - {file = "protobuf-5.29.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:d473655e29c0c4bbf8b69e9a8fb54645bc289dead6d753b952e7aa660254ae18"}, - {file = "protobuf-5.29.1-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:b5ba1d0e4c8a40ae0496d0e2ecfdbb82e1776928a205106d14ad6985a09ec155"}, - {file = "protobuf-5.29.1-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:8ee1461b3af56145aca2800e6a3e2f928108c749ba8feccc6f5dd0062c410c0d"}, - {file = "protobuf-5.29.1-cp38-cp38-win32.whl", hash = "sha256:50879eb0eb1246e3a5eabbbe566b44b10348939b7cc1b267567e8c3d07213853"}, - {file = "protobuf-5.29.1-cp38-cp38-win_amd64.whl", hash = "sha256:027fbcc48cea65a6b17028510fdd054147057fa78f4772eb547b9274e5219331"}, - {file = "protobuf-5.29.1-cp39-cp39-win32.whl", hash = "sha256:5a41deccfa5e745cef5c65a560c76ec0ed8e70908a67cc8f4da5fce588b50d57"}, - {file = "protobuf-5.29.1-cp39-cp39-win_amd64.whl", hash = "sha256:012ce28d862ff417fd629285aca5d9772807f15ceb1a0dbd15b88f58c776c98c"}, - {file = "protobuf-5.29.1-py3-none-any.whl", hash = "sha256:32600ddb9c2a53dedc25b8581ea0f1fd8ea04956373c0c07577ce58d312522e0"}, - {file = "protobuf-5.29.1.tar.gz", hash = "sha256:683be02ca21a6ffe80db6dd02c0b5b2892322c59ca57fd6c872d652cb80549cb"}, + {file = "protobuf-5.29.3-cp310-abi3-win32.whl", hash = "sha256:3ea51771449e1035f26069c4c7fd51fba990d07bc55ba80701c78f886bf9c888"}, + {file = "protobuf-5.29.3-cp310-abi3-win_amd64.whl", hash = "sha256:a4fa6f80816a9a0678429e84973f2f98cbc218cca434abe8db2ad0bffc98503a"}, + {file = "protobuf-5.29.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a8434404bbf139aa9e1300dbf989667a83d42ddda9153d8ab76e0d5dcaca484e"}, + {file = "protobuf-5.29.3-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:daaf63f70f25e8689c072cfad4334ca0ac1d1e05a92fc15c54eb9cf23c3efd84"}, + {file = "protobuf-5.29.3-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:c027e08a08be10b67c06bf2370b99c811c466398c357e615ca88c91c07f0910f"}, + {file = "protobuf-5.29.3-cp38-cp38-win32.whl", hash = "sha256:84a57163a0ccef3f96e4b6a20516cedcf5bb3a95a657131c5c3ac62200d23252"}, + {file = "protobuf-5.29.3-cp38-cp38-win_amd64.whl", hash = "sha256:b89c115d877892a512f79a8114564fb435943b59067615894c3b13cd3e1fa107"}, + {file = "protobuf-5.29.3-cp39-cp39-win32.whl", hash = "sha256:0eb32bfa5219fc8d4111803e9a690658aa2e6366384fd0851064b963b6d1f2a7"}, + {file = "protobuf-5.29.3-cp39-cp39-win_amd64.whl", hash = "sha256:6ce8cc3389a20693bfde6c6562e03474c40851b44975c9b2bf6df7d8c4f864da"}, + {file = "protobuf-5.29.3-py3-none-any.whl", hash = "sha256:0a18ed4a24198528f2333802eb075e59dea9d679ab7a6c5efb017a59004d849f"}, + {file = "protobuf-5.29.3.tar.gz", hash = "sha256:5da0f41edaf117bde316404bad1a486cb4ededf8e4a54891296f648e8e076620"}, ] [[package]] @@ -1669,6 +1799,8 @@ version = "1.11.0" description = "library with cross-python path, ini-parsing, io, code, log facilities" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, @@ -1680,6 +1812,8 @@ version = "0.6.1" description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"}, {file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"}, @@ -1691,6 +1825,8 @@ version = "0.4.1" description = "A collection of ASN.1-based protocols modules" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pyasn1_modules-0.4.1-py3-none-any.whl", hash = "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd"}, {file = "pyasn1_modules-0.4.1.tar.gz", hash = "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c"}, @@ -1705,6 +1841,8 @@ version = "8.0.4" description = "The kitchen sink of Python utility libraries for doing \"stuff\" in a functional way. Based on the Lo-Dash Javascript library." optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pydash-8.0.4-py3-none-any.whl", hash = "sha256:59d0c9ca0d22b4f8bcfab01bfe2e89b49f4c9e9fa75961caf156094670260999"}, {file = "pydash-8.0.4.tar.gz", hash = "sha256:a33fb17b4b06c617da5c57c711605d2dc8723311ee5388c8371f87cd44bf4112"}, @@ -1718,13 +1856,15 @@ dev = ["build", "coverage", "furo", "invoke", "mypy", "pytest", "pytest-cov", "p [[package]] name = "pygments" -version = "2.18.0" +version = "2.19.1" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, - {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, + {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, + {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, ] [package.extras] @@ -1736,6 +1876,8 @@ version = "7.4.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, @@ -1758,6 +1900,8 @@ version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main", "dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -1772,6 +1916,8 @@ version = "1.0.1" description = "Read key-value pairs from a .env file and set them as environment variables" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, @@ -1786,6 +1932,8 @@ version = "2024.2" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"}, {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, @@ -1797,6 +1945,8 @@ version = "6.0.2" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, @@ -1859,6 +2009,8 @@ version = "0.35.1" description = "JSON Referencing + Python" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "referencing-0.35.1-py3-none-any.whl", hash = "sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de"}, {file = "referencing-0.35.1.tar.gz", hash = "sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c"}, @@ -1874,6 +2026,8 @@ version = "2022.3.2" description = "Alternative regular expression module, to replace re." optional = false python-versions = ">=3.6" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "regex-2022.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ab69b4fe09e296261377d209068d52402fb85ef89dc78a9ac4a29a895f4e24a7"}, {file = "regex-2022.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5bc5f921be39ccb65fdda741e04b2555917a4bced24b4df14eddc7569be3b493"}, @@ -1957,6 +2111,8 @@ version = "2.32.3" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" +groups = ["main", "dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, @@ -1974,114 +2130,116 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "rpds-py" -version = "0.20.1" +version = "0.22.3" description = "Python bindings to Rust's persistent data structures (rpds)" optional = false -python-versions = ">=3.8" -files = [ - {file = "rpds_py-0.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a649dfd735fff086e8a9d0503a9f0c7d01b7912a333c7ae77e1515c08c146dad"}, - {file = "rpds_py-0.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f16bc1334853e91ddaaa1217045dd7be166170beec337576818461268a3de67f"}, - {file = "rpds_py-0.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14511a539afee6f9ab492b543060c7491c99924314977a55c98bfa2ee29ce78c"}, - {file = "rpds_py-0.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3ccb8ac2d3c71cda472b75af42818981bdacf48d2e21c36331b50b4f16930163"}, - {file = "rpds_py-0.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c142b88039b92e7e0cb2552e8967077e3179b22359e945574f5e2764c3953dcf"}, - {file = "rpds_py-0.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f19169781dddae7478a32301b499b2858bc52fc45a112955e798ee307e294977"}, - {file = "rpds_py-0.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13c56de6518e14b9bf6edde23c4c39dac5b48dcf04160ea7bce8fca8397cdf86"}, - {file = "rpds_py-0.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:925d176a549f4832c6f69fa6026071294ab5910e82a0fe6c6228fce17b0706bd"}, - {file = "rpds_py-0.20.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:78f0b6877bfce7a3d1ff150391354a410c55d3cdce386f862926a4958ad5ab7e"}, - {file = "rpds_py-0.20.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3dd645e2b0dcb0fd05bf58e2e54c13875847687d0b71941ad2e757e5d89d4356"}, - {file = "rpds_py-0.20.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4f676e21db2f8c72ff0936f895271e7a700aa1f8d31b40e4e43442ba94973899"}, - {file = "rpds_py-0.20.1-cp310-none-win32.whl", hash = "sha256:648386ddd1e19b4a6abab69139b002bc49ebf065b596119f8f37c38e9ecee8ff"}, - {file = "rpds_py-0.20.1-cp310-none-win_amd64.whl", hash = "sha256:d9ecb51120de61e4604650666d1f2b68444d46ae18fd492245a08f53ad2b7711"}, - {file = "rpds_py-0.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:762703bdd2b30983c1d9e62b4c88664df4a8a4d5ec0e9253b0231171f18f6d75"}, - {file = "rpds_py-0.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0b581f47257a9fce535c4567782a8976002d6b8afa2c39ff616edf87cbeff712"}, - {file = "rpds_py-0.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:842c19a6ce894493563c3bd00d81d5100e8e57d70209e84d5491940fdb8b9e3a"}, - {file = "rpds_py-0.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42cbde7789f5c0bcd6816cb29808e36c01b960fb5d29f11e052215aa85497c93"}, - {file = "rpds_py-0.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c8e9340ce5a52f95fa7d3b552b35c7e8f3874d74a03a8a69279fd5fca5dc751"}, - {file = "rpds_py-0.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ba6f89cac95c0900d932c9efb7f0fb6ca47f6687feec41abcb1bd5e2bd45535"}, - {file = "rpds_py-0.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a916087371afd9648e1962e67403c53f9c49ca47b9680adbeef79da3a7811b0"}, - {file = "rpds_py-0.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:200a23239781f46149e6a415f1e870c5ef1e712939fe8fa63035cd053ac2638e"}, - {file = "rpds_py-0.20.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:58b1d5dd591973d426cbb2da5e27ba0339209832b2f3315928c9790e13f159e8"}, - {file = "rpds_py-0.20.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6b73c67850ca7cae0f6c56f71e356d7e9fa25958d3e18a64927c2d930859b8e4"}, - {file = "rpds_py-0.20.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d8761c3c891cc51e90bc9926d6d2f59b27beaf86c74622c8979380a29cc23ac3"}, - {file = "rpds_py-0.20.1-cp311-none-win32.whl", hash = "sha256:cd945871335a639275eee904caef90041568ce3b42f402c6959b460d25ae8732"}, - {file = "rpds_py-0.20.1-cp311-none-win_amd64.whl", hash = "sha256:7e21b7031e17c6b0e445f42ccc77f79a97e2687023c5746bfb7a9e45e0921b84"}, - {file = "rpds_py-0.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:36785be22066966a27348444b40389f8444671630063edfb1a2eb04318721e17"}, - {file = "rpds_py-0.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:142c0a5124d9bd0e2976089484af5c74f47bd3298f2ed651ef54ea728d2ea42c"}, - {file = "rpds_py-0.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbddc10776ca7ebf2a299c41a4dde8ea0d8e3547bfd731cb87af2e8f5bf8962d"}, - {file = "rpds_py-0.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:15a842bb369e00295392e7ce192de9dcbf136954614124a667f9f9f17d6a216f"}, - {file = "rpds_py-0.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be5ef2f1fc586a7372bfc355986226484e06d1dc4f9402539872c8bb99e34b01"}, - {file = "rpds_py-0.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbcf360c9e3399b056a238523146ea77eeb2a596ce263b8814c900263e46031a"}, - {file = "rpds_py-0.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecd27a66740ffd621d20b9a2f2b5ee4129a56e27bfb9458a3bcc2e45794c96cb"}, - {file = "rpds_py-0.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0b937b2a1988f184a3e9e577adaa8aede21ec0b38320d6009e02bd026db04fa"}, - {file = "rpds_py-0.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6889469bfdc1eddf489729b471303739bf04555bb151fe8875931f8564309afc"}, - {file = "rpds_py-0.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:19b73643c802f4eaf13d97f7855d0fb527fbc92ab7013c4ad0e13a6ae0ed23bd"}, - {file = "rpds_py-0.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3c6afcf2338e7f374e8edc765c79fbcb4061d02b15dd5f8f314a4af2bdc7feb5"}, - {file = "rpds_py-0.20.1-cp312-none-win32.whl", hash = "sha256:dc73505153798c6f74854aba69cc75953888cf9866465196889c7cdd351e720c"}, - {file = "rpds_py-0.20.1-cp312-none-win_amd64.whl", hash = "sha256:8bbe951244a838a51289ee53a6bae3a07f26d4e179b96fc7ddd3301caf0518eb"}, - {file = "rpds_py-0.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6ca91093a4a8da4afae7fe6a222c3b53ee4eef433ebfee4d54978a103435159e"}, - {file = "rpds_py-0.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b9c2fe36d1f758b28121bef29ed1dee9b7a2453e997528e7d1ac99b94892527c"}, - {file = "rpds_py-0.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f009c69bc8c53db5dfab72ac760895dc1f2bc1b62ab7408b253c8d1ec52459fc"}, - {file = "rpds_py-0.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6740a3e8d43a32629bb9b009017ea5b9e713b7210ba48ac8d4cb6d99d86c8ee8"}, - {file = "rpds_py-0.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:32b922e13d4c0080d03e7b62991ad7f5007d9cd74e239c4b16bc85ae8b70252d"}, - {file = "rpds_py-0.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe00a9057d100e69b4ae4a094203a708d65b0f345ed546fdef86498bf5390982"}, - {file = "rpds_py-0.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49fe9b04b6fa685bd39237d45fad89ba19e9163a1ccaa16611a812e682913496"}, - {file = "rpds_py-0.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aa7ac11e294304e615b43f8c441fee5d40094275ed7311f3420d805fde9b07b4"}, - {file = "rpds_py-0.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aa97af1558a9bef4025f8f5d8c60d712e0a3b13a2fe875511defc6ee77a1ab7"}, - {file = "rpds_py-0.20.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:483b29f6f7ffa6af845107d4efe2e3fa8fb2693de8657bc1849f674296ff6a5a"}, - {file = "rpds_py-0.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:37fe0f12aebb6a0e3e17bb4cd356b1286d2d18d2e93b2d39fe647138458b4bcb"}, - {file = "rpds_py-0.20.1-cp313-none-win32.whl", hash = "sha256:a624cc00ef2158e04188df5e3016385b9353638139a06fb77057b3498f794782"}, - {file = "rpds_py-0.20.1-cp313-none-win_amd64.whl", hash = "sha256:b71b8666eeea69d6363248822078c075bac6ed135faa9216aa85f295ff009b1e"}, - {file = "rpds_py-0.20.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:5b48e790e0355865197ad0aca8cde3d8ede347831e1959e158369eb3493d2191"}, - {file = "rpds_py-0.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3e310838a5801795207c66c73ea903deda321e6146d6f282e85fa7e3e4854804"}, - {file = "rpds_py-0.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249280b870e6a42c0d972339e9cc22ee98730a99cd7f2f727549af80dd5a963"}, - {file = "rpds_py-0.20.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e79059d67bea28b53d255c1437b25391653263f0e69cd7dec170d778fdbca95e"}, - {file = "rpds_py-0.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b431c777c9653e569986ecf69ff4a5dba281cded16043d348bf9ba505486f36"}, - {file = "rpds_py-0.20.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da584ff96ec95e97925174eb8237e32f626e7a1a97888cdd27ee2f1f24dd0ad8"}, - {file = "rpds_py-0.20.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02a0629ec053fc013808a85178524e3cb63a61dbc35b22499870194a63578fb9"}, - {file = "rpds_py-0.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fbf15aff64a163db29a91ed0868af181d6f68ec1a3a7d5afcfe4501252840bad"}, - {file = "rpds_py-0.20.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:07924c1b938798797d60c6308fa8ad3b3f0201802f82e4a2c41bb3fafb44cc28"}, - {file = "rpds_py-0.20.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:4a5a844f68776a7715ecb30843b453f07ac89bad393431efbf7accca3ef599c1"}, - {file = "rpds_py-0.20.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:518d2ca43c358929bf08f9079b617f1c2ca6e8848f83c1225c88caeac46e6cbc"}, - {file = "rpds_py-0.20.1-cp38-none-win32.whl", hash = "sha256:3aea7eed3e55119635a74bbeb80b35e776bafccb70d97e8ff838816c124539f1"}, - {file = "rpds_py-0.20.1-cp38-none-win_amd64.whl", hash = "sha256:7dca7081e9a0c3b6490a145593f6fe3173a94197f2cb9891183ef75e9d64c425"}, - {file = "rpds_py-0.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b41b6321805c472f66990c2849e152aff7bc359eb92f781e3f606609eac877ad"}, - {file = "rpds_py-0.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a90c373ea2975519b58dece25853dbcb9779b05cc46b4819cb1917e3b3215b6"}, - {file = "rpds_py-0.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16d4477bcb9fbbd7b5b0e4a5d9b493e42026c0bf1f06f723a9353f5153e75d30"}, - {file = "rpds_py-0.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:84b8382a90539910b53a6307f7c35697bc7e6ffb25d9c1d4e998a13e842a5e83"}, - {file = "rpds_py-0.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4888e117dd41b9d34194d9e31631af70d3d526efc363085e3089ab1a62c32ed1"}, - {file = "rpds_py-0.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5265505b3d61a0f56618c9b941dc54dc334dc6e660f1592d112cd103d914a6db"}, - {file = "rpds_py-0.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e75ba609dba23f2c95b776efb9dd3f0b78a76a151e96f96cc5b6b1b0004de66f"}, - {file = "rpds_py-0.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1791ff70bc975b098fe6ecf04356a10e9e2bd7dc21fa7351c1742fdeb9b4966f"}, - {file = "rpds_py-0.20.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d126b52e4a473d40232ec2052a8b232270ed1f8c9571aaf33f73a14cc298c24f"}, - {file = "rpds_py-0.20.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:c14937af98c4cc362a1d4374806204dd51b1e12dded1ae30645c298e5a5c4cb1"}, - {file = "rpds_py-0.20.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3d089d0b88996df627693639d123c8158cff41c0651f646cd8fd292c7da90eaf"}, - {file = "rpds_py-0.20.1-cp39-none-win32.whl", hash = "sha256:653647b8838cf83b2e7e6a0364f49af96deec64d2a6578324db58380cff82aca"}, - {file = "rpds_py-0.20.1-cp39-none-win_amd64.whl", hash = "sha256:fa41a64ac5b08b292906e248549ab48b69c5428f3987b09689ab2441f267d04d"}, - {file = "rpds_py-0.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7a07ced2b22f0cf0b55a6a510078174c31b6d8544f3bc00c2bcee52b3d613f74"}, - {file = "rpds_py-0.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:68cb0a499f2c4a088fd2f521453e22ed3527154136a855c62e148b7883b99f9a"}, - {file = "rpds_py-0.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa3060d885657abc549b2a0f8e1b79699290e5d83845141717c6c90c2df38311"}, - {file = "rpds_py-0.20.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:95f3b65d2392e1c5cec27cff08fdc0080270d5a1a4b2ea1d51d5f4a2620ff08d"}, - {file = "rpds_py-0.20.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2cc3712a4b0b76a1d45a9302dd2f53ff339614b1c29603a911318f2357b04dd2"}, - {file = "rpds_py-0.20.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d4eea0761e37485c9b81400437adb11c40e13ef513375bbd6973e34100aeb06"}, - {file = "rpds_py-0.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f5179583d7a6cdb981151dd349786cbc318bab54963a192692d945dd3f6435d"}, - {file = "rpds_py-0.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fbb0ffc754490aff6dabbf28064be47f0f9ca0b9755976f945214965b3ace7e"}, - {file = "rpds_py-0.20.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:a94e52537a0e0a85429eda9e49f272ada715506d3b2431f64b8a3e34eb5f3e75"}, - {file = "rpds_py-0.20.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:92b68b79c0da2a980b1c4197e56ac3dd0c8a149b4603747c4378914a68706979"}, - {file = "rpds_py-0.20.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:93da1d3db08a827eda74356f9f58884adb254e59b6664f64cc04cdff2cc19b0d"}, - {file = "rpds_py-0.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:754bbed1a4ca48479e9d4182a561d001bbf81543876cdded6f695ec3d465846b"}, - {file = "rpds_py-0.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ca449520e7484534a2a44faf629362cae62b660601432d04c482283c47eaebab"}, - {file = "rpds_py-0.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:9c4cb04a16b0f199a8c9bf807269b2f63b7b5b11425e4a6bd44bd6961d28282c"}, - {file = "rpds_py-0.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63804105143c7e24cee7db89e37cb3f3941f8e80c4379a0b355c52a52b6780"}, - {file = "rpds_py-0.20.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:55cd1fa4ecfa6d9f14fbd97ac24803e6f73e897c738f771a9fe038f2f11ff07c"}, - {file = "rpds_py-0.20.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f8f741b6292c86059ed175d80eefa80997125b7c478fb8769fd9ac8943a16c0"}, - {file = "rpds_py-0.20.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fc212779bf8411667234b3cdd34d53de6c2b8b8b958e1e12cb473a5f367c338"}, - {file = "rpds_py-0.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ad56edabcdb428c2e33bbf24f255fe2b43253b7d13a2cdbf05de955217313e6"}, - {file = "rpds_py-0.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0a3a1e9ee9728b2c1734f65d6a1d376c6f2f6fdcc13bb007a08cc4b1ff576dc5"}, - {file = "rpds_py-0.20.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:e13de156137b7095442b288e72f33503a469aa1980ed856b43c353ac86390519"}, - {file = "rpds_py-0.20.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:07f59760ef99f31422c49038964b31c4dfcfeb5d2384ebfc71058a7c9adae2d2"}, - {file = "rpds_py-0.20.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:59240685e7da61fb78f65a9f07f8108e36a83317c53f7b276b4175dc44151684"}, - {file = "rpds_py-0.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:83cba698cfb3c2c5a7c3c6bac12fe6c6a51aae69513726be6411076185a8b24a"}, - {file = "rpds_py-0.20.1.tar.gz", hash = "sha256:e1791c4aabd117653530dccd24108fa03cc6baf21f58b950d0a73c3b3b29a350"}, +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "rpds_py-0.22.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:6c7b99ca52c2c1752b544e310101b98a659b720b21db00e65edca34483259967"}, + {file = "rpds_py-0.22.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:be2eb3f2495ba669d2a985f9b426c1797b7d48d6963899276d22f23e33d47e37"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70eb60b3ae9245ddea20f8a4190bd79c705a22f8028aaf8bbdebe4716c3fab24"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4041711832360a9b75cfb11b25a6a97c8fb49c07b8bd43d0d02b45d0b499a4ff"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64607d4cbf1b7e3c3c8a14948b99345eda0e161b852e122c6bb71aab6d1d798c"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e69b0a0e2537f26d73b4e43ad7bc8c8efb39621639b4434b76a3de50c6966e"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc27863442d388870c1809a87507727b799c8460573cfbb6dc0eeaef5a11b5ec"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e79dd39f1e8c3504be0607e5fc6e86bb60fe3584bec8b782578c3b0fde8d932c"}, + {file = "rpds_py-0.22.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e0fa2d4ec53dc51cf7d3bb22e0aa0143966119f42a0c3e4998293a3dd2856b09"}, + {file = "rpds_py-0.22.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fda7cb070f442bf80b642cd56483b5548e43d366fe3f39b98e67cce780cded00"}, + {file = "rpds_py-0.22.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cff63a0272fcd259dcc3be1657b07c929c466b067ceb1c20060e8d10af56f5bf"}, + {file = "rpds_py-0.22.3-cp310-cp310-win32.whl", hash = "sha256:9bd7228827ec7bb817089e2eb301d907c0d9827a9e558f22f762bb690b131652"}, + {file = "rpds_py-0.22.3-cp310-cp310-win_amd64.whl", hash = "sha256:9beeb01d8c190d7581a4d59522cd3d4b6887040dcfc744af99aa59fef3e041a8"}, + {file = "rpds_py-0.22.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d20cfb4e099748ea39e6f7b16c91ab057989712d31761d3300d43134e26e165f"}, + {file = "rpds_py-0.22.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:68049202f67380ff9aa52f12e92b1c30115f32e6895cd7198fa2a7961621fc5a"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb4f868f712b2dd4bcc538b0a0c1f63a2b1d584c925e69a224d759e7070a12d5"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bc51abd01f08117283c5ebf64844a35144a0843ff7b2983e0648e4d3d9f10dbb"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f3cec041684de9a4684b1572fe28c7267410e02450f4561700ca5a3bc6695a2"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7ef9d9da710be50ff6809fed8f1963fecdfecc8b86656cadfca3bc24289414b0"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59f4a79c19232a5774aee369a0c296712ad0e77f24e62cad53160312b1c1eaa1"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a60bce91f81ddaac922a40bbb571a12c1070cb20ebd6d49c48e0b101d87300d"}, + {file = "rpds_py-0.22.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e89391e6d60251560f0a8f4bd32137b077a80d9b7dbe6d5cab1cd80d2746f648"}, + {file = "rpds_py-0.22.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e3fb866d9932a3d7d0c82da76d816996d1667c44891bd861a0f97ba27e84fc74"}, + {file = "rpds_py-0.22.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1352ae4f7c717ae8cba93421a63373e582d19d55d2ee2cbb184344c82d2ae55a"}, + {file = "rpds_py-0.22.3-cp311-cp311-win32.whl", hash = "sha256:b0b4136a252cadfa1adb705bb81524eee47d9f6aab4f2ee4fa1e9d3cd4581f64"}, + {file = "rpds_py-0.22.3-cp311-cp311-win_amd64.whl", hash = "sha256:8bd7c8cfc0b8247c8799080fbff54e0b9619e17cdfeb0478ba7295d43f635d7c"}, + {file = "rpds_py-0.22.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:27e98004595899949bd7a7b34e91fa7c44d7a97c40fcaf1d874168bb652ec67e"}, + {file = "rpds_py-0.22.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1978d0021e943aae58b9b0b196fb4895a25cc53d3956b8e35e0b7682eefb6d56"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:655ca44a831ecb238d124e0402d98f6212ac527a0ba6c55ca26f616604e60a45"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:feea821ee2a9273771bae61194004ee2fc33f8ec7db08117ef9147d4bbcbca8e"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:22bebe05a9ffc70ebfa127efbc429bc26ec9e9b4ee4d15a740033efda515cf3d"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3af6e48651c4e0d2d166dc1b033b7042ea3f871504b6805ba5f4fe31581d8d38"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67ba3c290821343c192f7eae1d8fd5999ca2dc99994114643e2f2d3e6138b15"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:02fbb9c288ae08bcb34fb41d516d5eeb0455ac35b5512d03181d755d80810059"}, + {file = "rpds_py-0.22.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f56a6b404f74ab372da986d240e2e002769a7d7102cc73eb238a4f72eec5284e"}, + {file = "rpds_py-0.22.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0a0461200769ab3b9ab7e513f6013b7a97fdeee41c29b9db343f3c5a8e2b9e61"}, + {file = "rpds_py-0.22.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8633e471c6207a039eff6aa116e35f69f3156b3989ea3e2d755f7bc41754a4a7"}, + {file = "rpds_py-0.22.3-cp312-cp312-win32.whl", hash = "sha256:593eba61ba0c3baae5bc9be2f5232430453fb4432048de28399ca7376de9c627"}, + {file = "rpds_py-0.22.3-cp312-cp312-win_amd64.whl", hash = "sha256:d115bffdd417c6d806ea9069237a4ae02f513b778e3789a359bc5856e0404cc4"}, + {file = "rpds_py-0.22.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:ea7433ce7e4bfc3a85654aeb6747babe3f66eaf9a1d0c1e7a4435bbdf27fea84"}, + {file = "rpds_py-0.22.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6dd9412824c4ce1aca56c47b0991e65bebb7ac3f4edccfd3f156150c96a7bf25"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20070c65396f7373f5df4005862fa162db5d25d56150bddd0b3e8214e8ef45b4"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0b09865a9abc0ddff4e50b5ef65467cd94176bf1e0004184eb915cbc10fc05c5"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3453e8d41fe5f17d1f8e9c383a7473cd46a63661628ec58e07777c2fff7196dc"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f5d36399a1b96e1a5fdc91e0522544580dbebeb1f77f27b2b0ab25559e103b8b"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:009de23c9c9ee54bf11303a966edf4d9087cd43a6003672e6aa7def643d06518"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1aef18820ef3e4587ebe8b3bc9ba6e55892a6d7b93bac6d29d9f631a3b4befbd"}, + {file = "rpds_py-0.22.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f60bd8423be1d9d833f230fdbccf8f57af322d96bcad6599e5a771b151398eb2"}, + {file = "rpds_py-0.22.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:62d9cfcf4948683a18a9aff0ab7e1474d407b7bab2ca03116109f8464698ab16"}, + {file = "rpds_py-0.22.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9253fc214112405f0afa7db88739294295f0e08466987f1d70e29930262b4c8f"}, + {file = "rpds_py-0.22.3-cp313-cp313-win32.whl", hash = "sha256:fb0ba113b4983beac1a2eb16faffd76cb41e176bf58c4afe3e14b9c681f702de"}, + {file = "rpds_py-0.22.3-cp313-cp313-win_amd64.whl", hash = "sha256:c58e2339def52ef6b71b8f36d13c3688ea23fa093353f3a4fee2556e62086ec9"}, + {file = "rpds_py-0.22.3-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:f82a116a1d03628a8ace4859556fb39fd1424c933341a08ea3ed6de1edb0283b"}, + {file = "rpds_py-0.22.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3dfcbc95bd7992b16f3f7ba05af8a64ca694331bd24f9157b49dadeeb287493b"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59259dc58e57b10e7e18ce02c311804c10c5a793e6568f8af4dead03264584d1"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5725dd9cc02068996d4438d397e255dcb1df776b7ceea3b9cb972bdb11260a83"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99b37292234e61325e7a5bb9689e55e48c3f5f603af88b1642666277a81f1fbd"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:27b1d3b3915a99208fee9ab092b8184c420f2905b7d7feb4aeb5e4a9c509b8a1"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f612463ac081803f243ff13cccc648578e2279295048f2a8d5eb430af2bae6e3"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f73d3fef726b3243a811121de45193c0ca75f6407fe66f3f4e183c983573e130"}, + {file = "rpds_py-0.22.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3f21f0495edea7fdbaaa87e633a8689cd285f8f4af5c869f27bc8074638ad69c"}, + {file = "rpds_py-0.22.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:1e9663daaf7a63ceccbbb8e3808fe90415b0757e2abddbfc2e06c857bf8c5e2b"}, + {file = "rpds_py-0.22.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a76e42402542b1fae59798fab64432b2d015ab9d0c8c47ba7addddbaf7952333"}, + {file = "rpds_py-0.22.3-cp313-cp313t-win32.whl", hash = "sha256:69803198097467ee7282750acb507fba35ca22cc3b85f16cf45fb01cb9097730"}, + {file = "rpds_py-0.22.3-cp313-cp313t-win_amd64.whl", hash = "sha256:f5cf2a0c2bdadf3791b5c205d55a37a54025c6e18a71c71f82bb536cf9a454bf"}, + {file = "rpds_py-0.22.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:378753b4a4de2a7b34063d6f95ae81bfa7b15f2c1a04a9518e8644e81807ebea"}, + {file = "rpds_py-0.22.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3445e07bf2e8ecfeef6ef67ac83de670358abf2996916039b16a218e3d95e97e"}, + {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b2513ba235829860b13faa931f3b6846548021846ac808455301c23a101689d"}, + {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eaf16ae9ae519a0e237a0f528fd9f0197b9bb70f40263ee57ae53c2b8d48aeb3"}, + {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:583f6a1993ca3369e0f80ba99d796d8e6b1a3a2a442dd4e1a79e652116413091"}, + {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4617e1915a539a0d9a9567795023de41a87106522ff83fbfaf1f6baf8e85437e"}, + {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c150c7a61ed4a4f4955a96626574e9baf1adf772c2fb61ef6a5027e52803543"}, + {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fa4331c200c2521512595253f5bb70858b90f750d39b8cbfd67465f8d1b596d"}, + {file = "rpds_py-0.22.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:214b7a953d73b5e87f0ebece4a32a5bd83c60a3ecc9d4ec8f1dca968a2d91e99"}, + {file = "rpds_py-0.22.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f47ad3d5f3258bd7058d2d506852217865afefe6153a36eb4b6928758041d831"}, + {file = "rpds_py-0.22.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f276b245347e6e36526cbd4a266a417796fc531ddf391e43574cf6466c492520"}, + {file = "rpds_py-0.22.3-cp39-cp39-win32.whl", hash = "sha256:bbb232860e3d03d544bc03ac57855cd82ddf19c7a07651a7c0fdb95e9efea8b9"}, + {file = "rpds_py-0.22.3-cp39-cp39-win_amd64.whl", hash = "sha256:cfbc454a2880389dbb9b5b398e50d439e2e58669160f27b60e5eca11f68ae17c"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:d48424e39c2611ee1b84ad0f44fb3b2b53d473e65de061e3f460fc0be5f1939d"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:24e8abb5878e250f2eb0d7859a8e561846f98910326d06c0d51381fed59357bd"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b232061ca880db21fa14defe219840ad9b74b6158adb52ddf0e87bead9e8493"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac0a03221cdb5058ce0167ecc92a8c89e8d0decdc9e99a2ec23380793c4dcb96"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb0c341fa71df5a4595f9501df4ac5abfb5a09580081dffbd1ddd4654e6e9123"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf9db5488121b596dbfc6718c76092fda77b703c1f7533a226a5a9f65248f8ad"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b8db6b5b2d4491ad5b6bdc2bc7c017eec108acbf4e6785f42a9eb0ba234f4c9"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b3d504047aba448d70cf6fa22e06cb09f7cbd761939fdd47604f5e007675c24e"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:e61b02c3f7a1e0b75e20c3978f7135fd13cb6cf551bf4a6d29b999a88830a338"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:e35ba67d65d49080e8e5a1dd40101fccdd9798adb9b050ff670b7d74fa41c566"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:26fd7cac7dd51011a245f29a2cc6489c4608b5a8ce8d75661bb4a1066c52dfbe"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:177c7c0fce2855833819c98e43c262007f42ce86651ffbb84f37883308cb0e7d"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:bb47271f60660803ad11f4c61b42242b8c1312a31c98c578f79ef9387bbde21c"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:70fb28128acbfd264eda9bf47015537ba3fe86e40d046eb2963d75024be4d055"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44d61b4b7d0c2c9ac019c314e52d7cbda0ae31078aabd0f22e583af3e0d79723"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f0e260eaf54380380ac3808aa4ebe2d8ca28b9087cf411649f96bad6900c728"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b25bc607423935079e05619d7de556c91fb6adeae9d5f80868dde3468657994b"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fb6116dfb8d1925cbdb52595560584db42a7f664617a1f7d7f6e32f138cdf37d"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a63cbdd98acef6570c62b92a1e43266f9e8b21e699c363c0fef13bd530799c11"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2b8f60e1b739a74bab7e01fcbe3dddd4657ec685caa04681df9d562ef15b625f"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2e8b55d8517a2fda8d95cb45d62a5a8bbf9dd0ad39c5b25c8833efea07b880ca"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:2de29005e11637e7a2361fa151f780ff8eb2543a0da1413bb951e9f14b699ef3"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:666ecce376999bf619756a24ce15bb14c5bfaf04bf00abc7e663ce17c3f34fe7"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:5246b14ca64a8675e0a7161f7af68fe3e910e6b90542b4bfb5439ba752191df6"}, + {file = "rpds_py-0.22.3.tar.gz", hash = "sha256:e32fee8ab45d3c2db6da19a5323bc3362237c8b653c70194414b892fd06a080d"}, ] [[package]] @@ -2090,6 +2248,8 @@ version = "4.9" description = "Pure-Python RSA implementation" optional = false python-versions = ">=3.6,<4" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, @@ -2100,13 +2260,15 @@ pyasn1 = ">=0.1.3" [[package]] name = "ruamel-yaml" -version = "0.18.6" +version = "0.18.10" description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "ruamel.yaml-0.18.6-py3-none-any.whl", hash = "sha256:57b53ba33def16c4f3d807c0ccbc00f8a6081827e81ba2491691b76882d0c636"}, - {file = "ruamel.yaml-0.18.6.tar.gz", hash = "sha256:8b27e6a217e786c6fbe5634d8f3f11bc63e0f80f6a5890f28863d9c45aac311b"}, + {file = "ruamel.yaml-0.18.10-py3-none-any.whl", hash = "sha256:30f22513ab2301b3d2b577adc121c6471f28734d3d9728581245f1e76468b4f1"}, + {file = "ruamel.yaml-0.18.10.tar.gz", hash = "sha256:20c86ab29ac2153f80a428e1254a8adf686d3383df04490514ca3b79a362db58"}, ] [package.dependencies] @@ -2118,61 +2280,54 @@ jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] [[package]] name = "ruamel-yaml-clib" -version = "0.2.8" +version = "0.2.12" description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" optional = false -python-versions = ">=3.6" -files = [ - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b42169467c42b692c19cf539c38d4602069d8c1505e97b86387fcf7afb766e1d"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:07238db9cbdf8fc1e9de2489a4f68474e70dffcb32232db7c08fa61ca0c7c462"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fff3573c2db359f091e1589c3d7c5fc2f86f5bdb6f24252c2d8e539d4e45f412"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:aa2267c6a303eb483de8d02db2871afb5c5fc15618d894300b88958f729ad74f"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:840f0c7f194986a63d2c2465ca63af8ccbbc90ab1c6001b1978f05119b5e7334"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:024cfe1fc7c7f4e1aff4a81e718109e13409767e4f871443cbff3dba3578203d"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-win32.whl", hash = "sha256:c69212f63169ec1cfc9bb44723bf2917cbbd8f6191a00ef3410f5a7fe300722d"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-win_amd64.whl", hash = "sha256:cabddb8d8ead485e255fe80429f833172b4cadf99274db39abc080e068cbcc31"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bef08cd86169d9eafb3ccb0a39edb11d8e25f3dae2b28f5c52fd997521133069"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:b16420e621d26fdfa949a8b4b47ade8810c56002f5389970db4ddda51dbff248"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:25c515e350e5b739842fc3228d662413ef28f295791af5e5110b543cf0b57d9b"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_24_aarch64.whl", hash = "sha256:1707814f0d9791df063f8c19bb51b0d1278b8e9a2353abbb676c2f685dee6afe"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:46d378daaac94f454b3a0e3d8d78cafd78a026b1d71443f4966c696b48a6d899"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:09b055c05697b38ecacb7ac50bdab2240bfca1a0c4872b0fd309bb07dc9aa3a9"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-win32.whl", hash = "sha256:53a300ed9cea38cf5a2a9b069058137c2ca1ce658a874b79baceb8f892f915a7"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-win_amd64.whl", hash = "sha256:c2a72e9109ea74e511e29032f3b670835f8a59bbdc9ce692c5b4ed91ccf1eedb"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:ebc06178e8821efc9692ea7544aa5644217358490145629914d8020042c24aa1"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:edaef1c1200c4b4cb914583150dcaa3bc30e592e907c01117c08b13a07255ec2"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d176b57452ab5b7028ac47e7b3cf644bcfdc8cacfecf7e71759f7f51a59e5c92"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_24_aarch64.whl", hash = "sha256:1dc67314e7e1086c9fdf2680b7b6c2be1c0d8e3a8279f2e993ca2a7545fecf62"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3213ece08ea033eb159ac52ae052a4899b56ecc124bb80020d9bbceeb50258e9"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aab7fd643f71d7946f2ee58cc88c9b7bfc97debd71dcc93e03e2d174628e7e2d"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-win32.whl", hash = "sha256:5c365d91c88390c8d0a8545df0b5857172824b1c604e867161e6b3d59a827eaa"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-win_amd64.whl", hash = "sha256:1758ce7d8e1a29d23de54a16ae867abd370f01b5a69e1a3ba75223eaa3ca1a1b"}, - {file = "ruamel.yaml.clib-0.2.8-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a5aa27bad2bb83670b71683aae140a1f52b0857a2deff56ad3f6c13a017a26ed"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c58ecd827313af6864893e7af0a3bb85fd529f862b6adbefe14643947cfe2942"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-macosx_12_0_arm64.whl", hash = "sha256:f481f16baec5290e45aebdc2a5168ebc6d35189ae6fea7a58787613a25f6e875"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:77159f5d5b5c14f7c34073862a6b7d34944075d9f93e681638f6d753606c6ce6"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7f67a1ee819dc4562d444bbafb135832b0b909f81cc90f7aa00260968c9ca1b3"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4ecbf9c3e19f9562c7fdd462e8d18dd902a47ca046a2e64dba80699f0b6c09b7"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:87ea5ff66d8064301a154b3933ae406b0863402a799b16e4a1d24d9fbbcbe0d3"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-win32.whl", hash = "sha256:75e1ed13e1f9de23c5607fe6bd1aeaae21e523b32d83bb33918245361e9cc51b"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-win_amd64.whl", hash = "sha256:3f215c5daf6a9d7bbed4a0a4f760f3113b10e82ff4c5c44bec20a68c8014f675"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1b617618914cb00bf5c34d4357c37aa15183fa229b24767259657746c9077615"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:a6a9ffd280b71ad062eae53ac1659ad86a17f59a0fdc7699fd9be40525153337"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:305889baa4043a09e5b76f8e2a51d4ffba44259f6b4c72dec8ca56207d9c6fe1"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:700e4ebb569e59e16a976857c8798aee258dceac7c7d6b50cab63e080058df91"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e2b4c44b60eadec492926a7270abb100ef9f72798e18743939bdbf037aab8c28"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e79e5db08739731b0ce4850bed599235d601701d5694c36570a99a0c5ca41a9d"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-win32.whl", hash = "sha256:955eae71ac26c1ab35924203fda6220f84dce57d6d7884f189743e2abe3a9fbe"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-win_amd64.whl", hash = "sha256:56f4252222c067b4ce51ae12cbac231bce32aee1d33fbfc9d17e5b8d6966c312"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:03d1162b6d1df1caa3a4bd27aa51ce17c9afc2046c31b0ad60a0a96ec22f8001"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:bba64af9fa9cebe325a62fa398760f5c7206b215201b0ec825005f1b18b9bccf"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:a1a45e0bb052edf6a1d3a93baef85319733a888363938e1fc9924cb00c8df24c"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:da09ad1c359a728e112d60116f626cc9f29730ff3e0e7db72b9a2dbc2e4beed5"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:184565012b60405d93838167f425713180b949e9d8dd0bbc7b49f074407c5a8b"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a75879bacf2c987c003368cf14bed0ffe99e8e85acfa6c0bfffc21a090f16880"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-win32.whl", hash = "sha256:84b554931e932c46f94ab306913ad7e11bba988104c5cff26d90d03f68258cd5"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-win_amd64.whl", hash = "sha256:25ac8c08322002b06fa1d49d1646181f0b2c72f5cbc15a85e80b4c30a544bb15"}, - {file = "ruamel.yaml.clib-0.2.8.tar.gz", hash = "sha256:beb2e0404003de9a4cab9753a8805a8fe9320ee6673136ed7f04255fe60bb512"}, +python-versions = ">=3.9" +groups = ["dev"] +markers = "platform_python_implementation == \"CPython\" and python_version < \"3.13\" and (python_version <= \"3.11\" or python_version >= \"3.12\")" +files = [ + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:11f891336688faf5156a36293a9c362bdc7c88f03a8a027c2c1d8e0bcde998e5"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:a606ef75a60ecf3d924613892cc603b154178ee25abb3055db5062da811fd969"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd5415dded15c3822597455bc02bcd66e81ef8b7a48cb71a33628fc9fdde39df"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win32.whl", hash = "sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:d84318609196d6bd6da0edfa25cedfbabd8dbde5140a0a23af29ad4b8f91fb1e"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb43a269eb827806502c7c8efb7ae7e9e9d0573257a46e8e952f4d4caba4f31e"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:943f32bc9dedb3abff9879edc134901df92cfce2c3d5c9348f172f62eb2d771d"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95c3829bb364fdb8e0332c9931ecf57d9be3519241323c5274bd82f709cebc0c"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl", hash = "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:e7e3736715fbf53e9be2a79eb4db68e4ed857017344d697e8b9749444ae57475"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b7e75b4965e1d4690e93021adfcecccbca7d61c7bddd8e22406ef2ff20d74ef"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win32.whl", hash = "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:fc4b630cd3fa2cf7fce38afa91d7cfe844a9f75d7f0f36393fa98815e911d987"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bc5f1e1c28e966d61d2519f2a3d451ba989f9ea0f2307de7bc45baa526de9e45"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a0e060aace4c24dcaf71023bbd7d42674e3b230f7e7b97317baf1e953e5b519"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2f1c3765db32be59d18ab3953f43ab62a761327aafc1594a2a1fbe038b8b8a7"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d85252669dc32f98ebcd5d36768f5d4faeaeaa2d655ac0473be490ecdae3c285"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e143ada795c341b56de9418c58d028989093ee611aa27ffb9b7f609c00d813ed"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win32.whl", hash = "sha256:beffaed67936fbbeffd10966a4eb53c402fafd3d6833770516bf7314bc6ffa12"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win_amd64.whl", hash = "sha256:040ae85536960525ea62868b642bdb0c2cc6021c9f9d507810c0c604e66f5a7b"}, + {file = "ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f"}, ] [[package]] @@ -2181,6 +2336,8 @@ version = "0.6.9" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "ruff-0.6.9-py3-none-linux_armv6l.whl", hash = "sha256:064df58d84ccc0ac0fcd63bc3090b251d90e2a372558c0f057c3f75ed73e1ccd"}, {file = "ruff-0.6.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:140d4b5c9f5fc7a7b074908a78ab8d384dd7f6510402267bc76c37195c02a7ec"}, @@ -2204,23 +2361,25 @@ files = [ [[package]] name = "setuptools" -version = "75.3.0" +version = "75.8.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "setuptools-75.3.0-py3-none-any.whl", hash = "sha256:f2504966861356aa38616760c0f66568e535562374995367b4e69c7143cf6bcd"}, - {file = "setuptools-75.3.0.tar.gz", hash = "sha256:fba5dd4d766e97be1b1681d98712680ae8f2f26d7881245f2ce9e40714f1a686"}, + {file = "setuptools-75.8.0-py3-none-any.whl", hash = "sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3"}, + {file = "setuptools-75.8.0.tar.gz", hash = "sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.5.2)"] -core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.collections", "jaraco.functools", "jaraco.text (>=3.7)", "more-itertools", "more-itertools (>=8.8)", "packaging", "packaging (>=24)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.8.0)"] +core = ["importlib_metadata (>=6)", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test (>=5.5)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.12.*)", "pytest-mypy"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.14.*)", "pytest-mypy"] [[package]] name = "six" @@ -2228,6 +2387,8 @@ version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main", "dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, @@ -2239,6 +2400,8 @@ version = "2.2.0" description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." optional = false python-versions = "*" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, @@ -2250,6 +2413,8 @@ version = "2.6" description = "A modern CSS selector implementation for Beautiful Soup." optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"}, {file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"}, @@ -2257,25 +2422,26 @@ files = [ [[package]] name = "sphinx" -version = "7.1.2" +version = "7.3.7" description = "Python documentation generator" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "sphinx-7.1.2-py3-none-any.whl", hash = "sha256:d170a81825b2fcacb6dfd5a0d7f578a053e45d3f2b153fecc948c37344eb4cbe"}, - {file = "sphinx-7.1.2.tar.gz", hash = "sha256:780f4d32f1d7d1126576e0e5ecc19dc32ab76cd24e950228dcf7b1f6d3d9e22f"}, + {file = "sphinx-7.3.7-py3-none-any.whl", hash = "sha256:413f75440be4cacf328f580b4274ada4565fb2187d696a84970c23f77b64d8c3"}, + {file = "sphinx-7.3.7.tar.gz", hash = "sha256:a4a7db75ed37531c05002d56ed6948d4c42f473a36f46e1382b0bd76ca9627bc"}, ] [package.dependencies] -alabaster = ">=0.7,<0.8" +alabaster = ">=0.7.14,<0.8.0" babel = ">=2.9" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} -docutils = ">=0.18.1,<0.21" +docutils = ">=0.18.1,<0.22" imagesize = ">=1.3" -importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""} Jinja2 = ">=3.0" packaging = ">=21.0" -Pygments = ">=2.13" +Pygments = ">=2.14" requests = ">=2.25.0" snowballstemmer = ">=2.0" sphinxcontrib-applehelp = "*" @@ -2283,31 +2449,34 @@ sphinxcontrib-devhelp = "*" sphinxcontrib-htmlhelp = ">=2.0.0" sphinxcontrib-jsmath = "*" sphinxcontrib-qthelp = "*" -sphinxcontrib-serializinghtml = ">=1.1.5" +sphinxcontrib-serializinghtml = ">=1.1.9" +tomli = {version = ">=2", markers = "python_version < \"3.11\""} [package.extras] docs = ["sphinxcontrib-websupport"] -lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-simplify", "isort", "mypy (>=0.990)", "ruff", "sphinx-lint", "types-requests"] -test = ["cython", "filelock", "html5lib", "pytest (>=4.6)"] +lint = ["flake8 (>=3.5.0)", "importlib_metadata", "mypy (==1.9.0)", "pytest (>=6.0)", "ruff (==0.3.7)", "sphinx-lint", "tomli", "types-docutils", "types-requests"] +test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=6.0)", "setuptools (>=67.0)"] [[package]] name = "sphinx-autodoc-typehints" -version = "2.0.1" +version = "2.3.0" description = "Type hints (PEP 484) support for the Sphinx autodoc extension" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "sphinx_autodoc_typehints-2.0.1-py3-none-any.whl", hash = "sha256:f73ae89b43a799e587e39266672c1075b2ef783aeb382d3ebed77c38a3fc0149"}, - {file = "sphinx_autodoc_typehints-2.0.1.tar.gz", hash = "sha256:60ed1e3b2c970acc0aa6e877be42d48029a9faec7378a17838716cacd8c10b12"}, + {file = "sphinx_autodoc_typehints-2.3.0-py3-none-any.whl", hash = "sha256:3098e2c6d0ba99eacd013eb06861acc9b51c6e595be86ab05c08ee5506ac0c67"}, + {file = "sphinx_autodoc_typehints-2.3.0.tar.gz", hash = "sha256:535c78ed2d6a1bad393ba9f3dfa2602cf424e2631ee207263e07874c38fde084"}, ] [package.dependencies] -sphinx = ">=7.1.2" +sphinx = ">=7.3.5" [package.extras] docs = ["furo (>=2024.1.29)"] numpy = ["nptyping (>=2.5)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.4.2)", "diff-cover (>=8.0.3)", "pytest (>=8.0.1)", "pytest-cov (>=4.1)", "sphobjinv (>=2.3.1)", "typing-extensions (>=4.9)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.4.4)", "defusedxml (>=0.7.1)", "diff-cover (>=9)", "pytest (>=8.1.1)", "pytest-cov (>=5)", "sphobjinv (>=2.3.1)", "typing-extensions (>=4.11)"] [[package]] name = "sphinx-jinja2-compat" @@ -2315,6 +2484,8 @@ version = "0.3.0" description = "Patches Jinja2 v3 to restore compatibility with earlier Sphinx versions." optional = false python-versions = ">=3.6" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "sphinx_jinja2_compat-0.3.0-py3-none-any.whl", hash = "sha256:b1e4006d8e1ea31013fa9946d1b075b0c8d2a42c6e3425e63542c1e9f8be9084"}, {file = "sphinx_jinja2_compat-0.3.0.tar.gz", hash = "sha256:f3c1590b275f42e7a654e081db5e3e5fb97f515608422bde94015ddf795dfe7c"}, @@ -2327,17 +2498,21 @@ standard-imghdr = {version = "3.10.14", markers = "python_version >= \"3.13\""} [[package]] name = "sphinx-prompt" -version = "1.5.0" +version = "1.8.0" description = "Sphinx directive to add unselectable prompt" optional = false -python-versions = "*" +python-versions = ">=3.9,<4.0" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "sphinx_prompt-1.5.0-py3-none-any.whl", hash = "sha256:fa4e90d8088b5a996c76087d701fc7e31175f8b9dc4aab03a507e45051067162"}, + {file = "sphinx_prompt-1.8.0-py3-none-any.whl", hash = "sha256:369ecc633f0711886f9b3a078c83264245be1adf46abeeb9b88b5519e4b51007"}, + {file = "sphinx_prompt-1.8.0.tar.gz", hash = "sha256:47482f86fcec29662fdfd23e7c04ef03582714195d01f5d565403320084372ed"}, ] [package.dependencies] +docutils = "*" pygments = "*" -Sphinx = "*" +Sphinx = ">=7.0.0,<8.0.0" [[package]] name = "sphinx-rtd-theme" @@ -2345,6 +2520,8 @@ version = "1.3.0" description = "Read the Docs theme for Sphinx" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "sphinx_rtd_theme-1.3.0-py2.py3-none-any.whl", hash = "sha256:46ddef89cc2416a81ecfbeaceab1881948c014b1b6e4450b815311a89fb977b0"}, {file = "sphinx_rtd_theme-1.3.0.tar.gz", hash = "sha256:590b030c7abb9cf038ec053b95e5380b5c70d61591eb0b552063fbe7c41f0931"}, @@ -2364,6 +2541,8 @@ version = "3.4.5" description = "Tabbed views for Sphinx" optional = false python-versions = "~=3.7" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "sphinx-tabs-3.4.5.tar.gz", hash = "sha256:ba9d0c1e3e37aaadd4b5678449eb08176770e0fc227e769b6ce747df3ceea531"}, {file = "sphinx_tabs-3.4.5-py3-none-any.whl", hash = "sha256:92cc9473e2ecf1828ca3f6617d0efc0aa8acb06b08c56ba29d1413f2f0f6cf09"}, @@ -2384,6 +2563,8 @@ version = "3.8.1" description = "Box of handy tools for Sphinx 🧰 📔" optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "sphinx_toolbox-3.8.1-py3-none-any.whl", hash = "sha256:53d8e77dd79e807d9ef18590c4b2960a5aa3c147415054b04c31a91afed8b88b"}, {file = "sphinx_toolbox-3.8.1.tar.gz", hash = "sha256:a4b39a6ea24fc8f10e24f052199bda17837a0bf4c54163a56f521552395f5e1a"}, @@ -2414,47 +2595,56 @@ testing = ["coincidence (>=0.4.3)", "pygments (>=2.7.4,<=2.13.0)"] [[package]] name = "sphinxcontrib-applehelp" -version = "1.0.4" +version = "2.0.0" description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "sphinxcontrib-applehelp-1.0.4.tar.gz", hash = "sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e"}, - {file = "sphinxcontrib_applehelp-1.0.4-py3-none-any.whl", hash = "sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228"}, + {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"}, + {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"}, ] [package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] test = ["pytest"] [[package]] name = "sphinxcontrib-devhelp" -version = "1.0.2" -description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." +version = "2.0.0" +description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" optional = false -python-versions = ">=3.5" +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, - {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, + {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"}, + {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"}, ] [package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] test = ["pytest"] [[package]] name = "sphinxcontrib-htmlhelp" -version = "2.0.1" +version = "2.1.0" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "sphinxcontrib-htmlhelp-2.0.1.tar.gz", hash = "sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff"}, - {file = "sphinxcontrib_htmlhelp-2.0.1-py3-none-any.whl", hash = "sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903"}, + {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"}, + {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"}, ] [package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] test = ["html5lib", "pytest"] [[package]] @@ -2463,6 +2653,8 @@ version = "4.1" description = "Extension to include jQuery on newer Sphinx releases" optional = false python-versions = ">=2.7" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a"}, {file = "sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae"}, @@ -2477,6 +2669,8 @@ version = "1.0.1" description = "A sphinx extension which renders display math in HTML via JavaScript" optional = false python-versions = ">=3.5" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, @@ -2487,32 +2681,38 @@ test = ["flake8", "mypy", "pytest"] [[package]] name = "sphinxcontrib-qthelp" -version = "1.0.3" -description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." +version = "2.0.0" +description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" optional = false -python-versions = ">=3.5" +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, - {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, + {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"}, + {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"}, ] [package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -test = ["pytest"] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["defusedxml (>=0.7.1)", "pytest"] [[package]] name = "sphinxcontrib-serializinghtml" -version = "1.1.5" -description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." +version = "2.0.0" +description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" optional = false -python-versions = ">=3.5" +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, - {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, + {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}, + {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, ] [package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] test = ["pytest"] [[package]] @@ -2521,6 +2721,8 @@ version = "3.10.14" description = "Standard library imghdr redistribution. \"dead battery\"." optional = false python-versions = "*" +groups = ["dev"] +markers = "python_version >= \"3.13\"" files = [ {file = "standard_imghdr-3.10.14-py3-none-any.whl", hash = "sha256:cdf6883163349624dee9a81d2853a20260337c4cd41c04e99c082e01833a08e2"}, {file = "standard_imghdr-3.10.14.tar.gz", hash = "sha256:2598fe2e7c540dbda34b233295e10957ab8dc8ac6f3bd9eaa8d38be167232e52"}, @@ -2532,6 +2734,8 @@ version = "1.2.0" description = "String case converter." optional = false python-versions = "*" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "stringcase-1.2.0.tar.gz", hash = "sha256:48a06980661908efe8d9d34eab2b6c13aefa2163b3ced26972902e3bdfd87008"}, ] @@ -2542,6 +2746,8 @@ version = "0.9.0" description = "Pretty-print tabular data" optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"}, {file = "tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c"}, @@ -2556,6 +2762,8 @@ version = "2.2.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version < \"3.11\"" files = [ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, @@ -2597,6 +2805,8 @@ version = "3.28.0" description = "tox is a generic virtualenv management and test command line tool" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "tox-3.28.0-py2.py3-none-any.whl", hash = "sha256:57b5ab7e8bb3074edc3c0c0b4b192a4f3799d3723b2c5b76f1fa9f2d40316eea"}, {file = "tox-3.28.0.tar.gz", hash = "sha256:d0d28f3fe6d6d7195c27f8b054c3e99d5451952b54abdae673b71609a581f640"}, @@ -2622,6 +2832,8 @@ version = "0.6.0" description = "A library to help digital twins and data services talk to one another." optional = false python-versions = "<4.0,>=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "twined-0.6.0-py3-none-any.whl", hash = "sha256:5f9d29c02bdeae7bb7ead80aafc1c650eb642fc864375fab529045ed53067aa1"}, {file = "twined-0.6.0.tar.gz", hash = "sha256:d7709a760f14f29946651a3a69abc28ae312f865cbe7d937d0afc427de34664a"}, @@ -2637,6 +2849,8 @@ version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, @@ -2648,6 +2862,8 @@ version = "2024.2" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, @@ -2659,13 +2875,14 @@ version = "5.2" description = "tzinfo object for the local timezone" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "tzlocal-5.2-py3-none-any.whl", hash = "sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8"}, {file = "tzlocal-5.2.tar.gz", hash = "sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e"}, ] [package.dependencies] -"backports.zoneinfo" = {version = "*", markers = "python_version < \"3.9\""} tzdata = {version = "*", markers = "platform_system == \"Windows\""} [package.extras] @@ -2673,13 +2890,15 @@ devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3) [[package]] name = "urllib3" -version = "2.2.3" +version = "2.3.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main", "dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, - {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, + {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, + {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, ] [package.extras] @@ -2690,13 +2909,15 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" -version = "20.28.0" +version = "20.28.1" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "virtualenv-20.28.0-py3-none-any.whl", hash = "sha256:23eae1b4516ecd610481eda647f3a7c09aea295055337331bb4e6892ecce47b0"}, - {file = "virtualenv-20.28.0.tar.gz", hash = "sha256:2c9c3262bb8e7b87ea801d715fae4495e6032450c71d2309be9550e7364049aa"}, + {file = "virtualenv-20.28.1-py3-none-any.whl", hash = "sha256:412773c85d4dab0409b83ec36f7a6499e72eaf08c80e81e9576bca61831c71cb"}, + {file = "virtualenv-20.28.1.tar.gz", hash = "sha256:5d34ab240fdb5d21549b76f9e8ff3af28252f5499fb6d6f031adac4e5a8c5329"}, ] [package.dependencies] @@ -2714,6 +2935,8 @@ version = "0.5.1" description = "Character encoding aliases for legacy web content" optional = false python-versions = "*" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, @@ -2721,13 +2944,15 @@ files = [ [[package]] name = "werkzeug" -version = "3.0.6" +version = "3.1.3" description = "The comprehensive WSGI web application library." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "werkzeug-3.0.6-py3-none-any.whl", hash = "sha256:1bc0c2310d2fbb07b1dd1105eba2f7af72f322e1e455f2f93c993bee8c8a5f17"}, - {file = "werkzeug-3.0.6.tar.gz", hash = "sha256:a8dd59d4de28ca70471a34cba79bed5f7ef2e036a76b3ab0835474246eb41f8d"}, + {file = "werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e"}, + {file = "werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746"}, ] [package.dependencies] @@ -2742,6 +2967,8 @@ version = "1.17.0" description = "Module for decorators, wrappers and monkey patching." optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "wrapt-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a0c23b8319848426f305f9cb0c98a6e32ee68a36264f45948ccf8e7d2b941f8"}, {file = "wrapt-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1ca5f060e205f72bec57faae5bd817a1560fcfc4af03f414b08fa29106b7e2d"}, @@ -2812,13 +3039,15 @@ files = [ [[package]] name = "zipp" -version = "3.20.2" +version = "3.21.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350"}, - {file = "zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29"}, + {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, + {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, ] [package.extras] @@ -2833,6 +3062,6 @@ type = ["pytest-mypy"] hdf5 = ["h5py"] [metadata] -lock-version = "2.0" -python-versions = "^3.8" -content-hash = "5539fa069affa26830f3a929dd3d0ae97af9ccad05b0a434466f415ca7c9c6a3" +lock-version = "2.1" +python-versions = "^3.10" +content-hash = "a40029a7ca7b9d45458ae24c22a9c1e6c15d1d9f35cfd2a81e4b142be244968f" From a4c145025e45165c1b9253242252d3eb05897ebd Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Thu, 9 Jan 2025 17:46:33 +0000 Subject: [PATCH 006/125] TST: Update test for python3.13 --- tests/mixins/test_identifiable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/mixins/test_identifiable.py b/tests/mixins/test_identifiable.py index 32b6a9dab..338e72e99 100644 --- a/tests/mixins/test_identifiable.py +++ b/tests/mixins/test_identifiable.py @@ -76,4 +76,4 @@ class Inherit(Identifiable): with self.assertRaises(AttributeError) as e: resource.id = "07d38e81-6b00-4079-901b-e250ea3c7773" - self.assertIn("can't set attribute", e.exception.args[0]) + self.assertIn("object has no setter", e.exception.args[0]) From cbe45adf5d3c5869f2ad2744ff013679b4cc8591 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Mon, 13 Jan 2025 09:54:20 +0000 Subject: [PATCH 007/125] FEA: Add `octue question ask` CLI command skipci --- octue/cli.py | 74 +++++++++++++++++++ octue/cloud/__init__.py | 5 ++ .../google/answer_pub_sub_question.py | 10 ++- octue/cloud/events/utils.py | 49 ++++++++++++ octue/cloud/pub_sub/service.py | 29 ++++---- pyproject.toml | 2 +- 6 files changed, 149 insertions(+), 20 deletions(-) create mode 100644 octue/cloud/events/utils.py diff --git a/octue/cli.py b/octue/cli.py index 1add4f528..a684b0a05 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -11,6 +11,8 @@ from google import auth from octue.cloud import pub_sub, storage +from octue.cloud.deployment.google.answer_pub_sub_question import answer_question +from octue.cloud.events.utils import make_originator_question_event from octue.cloud.pub_sub.service import Service from octue.cloud.service_id import create_sruid, get_sruid_parts from octue.cloud.storage import GoogleCloudStorageClient @@ -70,6 +72,78 @@ def octue_cli(id, logger_uri, log_level, force_reset): global_cli_context["log_handler"] = get_remote_handler(logger_uri=global_cli_context["logger_uri"]) +@octue_cli.group() +def question(): + """Ask and interact with questions to an Octue Twined data service.""" + + +@question.command() +@click.argument("sruid", type=str) +@click.option( + "-i", + "--input-values", + type=str, + default=None, + help="Any input values for the question as a JSON-encoded string.", +) +@click.option( + "-m", + "--input-manifest", + type=str, + default=None, + help="An optional input manifest for the question serialised as a JSON-encoded string.", +) +@click.option( + "-c", + "--service-config", + type=click.Path(dir_okay=False), + default=None, + help="The path to an `octue.yaml` file defining the service to run. If not provided, the " + "`OCTUE_SERVICE_CONFIGURATION_PATH` environment variable is used if present, otherwise the local path `octue.yaml` " + "is used.", +) +def ask(sruid, input_values, input_manifest, service_config): + """Ask a question to a local or remote Octue Twined service. + + SRUID should be: + + - For remote services: a valid service revision unique identifier for an existing Octue Twined service + + e.g. octue question ask octue/example-service:1.0.3 + + - For a local service: "local" + + e.g. octue question ask local + """ + if sruid == "local": + service_configuration, app_configuration = load_service_and_app_configuration(service_config) + service_namespace, service_name, service_revision_tag = get_sruid_parts(service_configuration) + + child_sruid = create_sruid(namespace=service_namespace, name=service_name, revision_tag=service_revision_tag) + parent_sruid = "local/local:local" + + question = make_originator_question_event( + input_values=input_values, + input_manifest=input_manifest, + parent_sruid=parent_sruid, + child_sruid=child_sruid, + ) + + backend_configuration_values = (app_configuration.configuration_values or {}).get("backend") + + if backend_configuration_values: + backend_configuration_values = copy.deepcopy(backend_configuration_values) + backend = service_backends.get_backend(backend_configuration_values.pop("name"))( + **backend_configuration_values + ) + else: + # If no backend details are provided, use Google Pub/Sub with the default project. + _, project_name = auth.default() + backend = service_backends.get_backend()(project_name=project_name) + + answer_question(question=question, project_name=backend.project_name, service_configuration=service_config) + + @octue_cli.command() @click.option( "-c", diff --git a/octue/cloud/__init__.py b/octue/cloud/__init__.py index 1f32a2282..96fc05939 100644 --- a/octue/cloud/__init__.py +++ b/octue/cloud/__init__.py @@ -1,8 +1,13 @@ +import importlib.metadata + import octue.exceptions import twined.exceptions from octue.utils.exceptions import create_exceptions_mapping +LOCAL_SDK_VERSION = importlib.metadata.version("octue") + + EXCEPTIONS_MAPPING = create_exceptions_mapping( globals()["__builtins__"], vars(twined.exceptions), diff --git a/octue/cloud/deployment/google/answer_pub_sub_question.py b/octue/cloud/deployment/google/answer_pub_sub_question.py index 934fd87e9..afb1d5d79 100644 --- a/octue/cloud/deployment/google/answer_pub_sub_question.py +++ b/octue/cloud/deployment/google/answer_pub_sub_question.py @@ -11,14 +11,18 @@ logger = logging.getLogger(__name__) -def answer_question(question, project_name): +def answer_question(question, project_name, service_configuration=None, app_configuration=None): """Answer a question sent to an app deployed in Google Cloud. - :param dict|tuple question: + :param dict question: :param str project_name: + :param service_configuration: + :param app_configuration: :return None: """ - service_configuration, app_configuration = load_service_and_app_configuration() + if not service_configuration: + service_configuration, app_configuration = load_service_and_app_configuration() + service_namespace, service_name, service_revision_tag = get_sruid_parts(service_configuration) service_sruid = create_sruid( diff --git a/octue/cloud/events/utils.py b/octue/cloud/events/utils.py new file mode 100644 index 000000000..0c0ca7378 --- /dev/null +++ b/octue/cloud/events/utils.py @@ -0,0 +1,49 @@ +import datetime +import uuid + +from octue.cloud import LOCAL_SDK_VERSION + + +def make_originator_question_event(input_values, input_manifest, parent_sruid, child_sruid, question_uuid=None): + question_uuid = question_uuid or uuid.uuid4() + + return { + "event": { + "input_values": input_values, + "input_manifest": input_manifest, + }, + "attributes": make_attributes( + question_uuid=question_uuid, + parent_question_uuid=question_uuid, + originator_question_uuid=question_uuid, + parent=parent_sruid, + originator=parent_sruid, + sender=parent_sruid, + recipient=child_sruid, + ), + } + + +def make_attributes( + parent_question_uuid, + originator_question_uuid, + parent, + originator, + sender, + recipient, + question_uuid=None, + retry_count=0, +): + return { + "uuid": str(uuid.uuid4()), + "datetime": datetime.datetime.now(tz=datetime.timezone.utc).isoformat(), + "question_uuid": question_uuid or str(uuid.uuid4()), + "parent_question_uuid": parent_question_uuid, + "originator_question_uuid": originator_question_uuid, + "parent": parent, + "originator": originator, + "sender": sender, + "sender_sdk_version": LOCAL_SDK_VERSION, + "recipient": recipient, + "retry_count": retry_count, + } diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index 9d65d5633..ce695afbb 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -1,6 +1,5 @@ import concurrent.futures import copy -import datetime import functools import importlib.metadata import json @@ -15,7 +14,9 @@ from google.cloud import pubsub_v1 import octue.exceptions +from octue.cloud import LOCAL_SDK_VERSION from octue.cloud.events import OCTUE_SERVICES_PREFIX +from octue.cloud.events.utils import make_attributes from octue.cloud.events.validation import raise_if_event_is_invalid from octue.cloud.pub_sub import Subscription, Topic from octue.cloud.pub_sub.events import GoogleCloudPubSubEventHandler, extract_event_and_attributes_from_pub_sub_message @@ -85,7 +86,6 @@ def __init__(self, backend, service_id=None, run_function=None, service_registri self.service_registries = service_registries self._pub_sub_id = convert_service_id_to_pub_sub_form(self.id) - self._local_sdk_version = importlib.metadata.version("octue") self._event_handler = None def __repr__(self): @@ -288,7 +288,7 @@ def answer(self, question, heartbeat_interval=120, timeout=30): if heartbeater is not None: heartbeater.cancel() - warn_if_incompatible(child_sdk_version=self._local_sdk_version, parent_sdk_version=parent_sdk_version) + warn_if_incompatible(child_sdk_version=LOCAL_SDK_VERSION, parent_sdk_version=parent_sdk_version) self.send_exception(timeout=timeout, **routing_metadata) raise error @@ -538,19 +538,16 @@ def _emit_event( attributes = attributes or {} attributes.update( - { - "uuid": str(uuid.uuid4()), - "datetime": datetime.datetime.now(tz=datetime.timezone.utc).isoformat(), - "question_uuid": question_uuid, - "parent_question_uuid": parent_question_uuid, - "originator_question_uuid": originator_question_uuid, - "parent": parent, - "originator": originator, - "sender": self.id, - "sender_sdk_version": self._local_sdk_version, - "recipient": recipient, - "retry_count": retry_count, - } + make_attributes( + question_uuid=question_uuid, + parent_question_uuid=parent_question_uuid, + originator_question_uuid=originator_question_uuid, + parent=parent, + originator=originator, + sender=self.id, + recipient=recipient, + retry_count=retry_count, + ) ) converted_attributes = {} diff --git a/pyproject.toml b/pyproject.toml index 2ae836891..173256551 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "octue" -version = "0.62.0" +version = "0.63.0" description = "A package providing template applications for data services, and a python SDK to the Octue API." readme = "README.md" authors = ["Marcus Lugg ", "Thomas Clark "] From 96b145eae1f761465d3ea3b9c42ae55e9de80f9d Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Mon, 13 Jan 2025 10:23:55 +0000 Subject: [PATCH 008/125] ENH: Deserialise `octue question ask` inputs from JSON skipci --- octue/cli.py | 54 +++++++++++++++++++++++++++++----------------------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/octue/cli.py b/octue/cli.py index a684b0a05..389fb974f 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -115,33 +115,39 @@ def ask(sruid, input_values, input_manifest, service_config): e.g. octue question ask local """ - if sruid == "local": - service_configuration, app_configuration = load_service_and_app_configuration(service_config) - service_namespace, service_name, service_revision_tag = get_sruid_parts(service_configuration) - - child_sruid = create_sruid(namespace=service_namespace, name=service_name, revision_tag=service_revision_tag) - parent_sruid = "local/local:local" - - question = make_originator_question_event( - input_values=input_values, - input_manifest=input_manifest, - parent_sruid=parent_sruid, - child_sruid=child_sruid, - ) + if sruid != "local": + pass - backend_configuration_values = (app_configuration.configuration_values or {}).get("backend") + service_configuration, app_configuration = load_service_and_app_configuration(service_config) - if backend_configuration_values: - backend_configuration_values = copy.deepcopy(backend_configuration_values) - backend = service_backends.get_backend(backend_configuration_values.pop("name"))( - **backend_configuration_values - ) - else: - # If no backend details are provided, use Google Pub/Sub with the default project. - _, project_name = auth.default() - backend = service_backends.get_backend()(project_name=project_name) + if input_values: + input_values = json.loads(input_values) + + if input_manifest: + input_manifest = Manifest.deserialise(input_manifest, from_string=True) + + service_namespace, service_name, service_revision_tag = get_sruid_parts(service_configuration) + child_sruid = create_sruid(namespace=service_namespace, name=service_name, revision_tag=service_revision_tag) + parent_sruid = "local/local:local" + + question = make_originator_question_event( + input_values=input_values, + input_manifest=input_manifest, + parent_sruid=parent_sruid, + child_sruid=child_sruid, + ) + + backend_configuration_values = (app_configuration.configuration_values or {}).get("backend") + + if backend_configuration_values: + backend_configuration_values = copy.deepcopy(backend_configuration_values) + backend = service_backends.get_backend(backend_configuration_values.pop("name"))(**backend_configuration_values) + else: + # If no backend details are provided, use Google Pub/Sub with the default project. + _, project_name = auth.default() + backend = service_backends.get_backend()(project_name=project_name) - answer_question(question=question, project_name=backend.project_name, service_configuration=service_config) + answer_question(question=question, project_name=backend.project_name, service_configuration=service_config) @octue_cli.command() From 665fb411fd07e87c4fac3e5f3c15844805ae300a Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Mon, 13 Jan 2025 10:37:28 +0000 Subject: [PATCH 009/125] FEA: Add ability to ask remote question from CLI --- octue/cli.py | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/octue/cli.py b/octue/cli.py index 389fb974f..41a39eb75 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -20,8 +20,9 @@ from octue.definitions import MANIFEST_FILENAME, VALUES_FILENAME from octue.exceptions import ServiceAlreadyExists from octue.log_handlers import apply_log_handler, get_remote_handler -from octue.resources import Manifest, service_backends +from octue.resources import Manifest, service_backends, Child from octue.runner import Runner +from octue.utils.decoders import OctueJSONDecoder from octue.utils.encoders import OctueJSONEncoder @@ -93,16 +94,24 @@ def question(): default=None, help="An optional input manifest for the question serialised as a JSON-encoded string.", ) +@click.option( + "-p", + "--project-name", + type="str", + default=None, + help="If asking a remote question, the name of the Google Cloud project the service is deployed in. If not " + "provided, the project name is detected from the local Google application credentials if present.", +) @click.option( "-c", "--service-config", type=click.Path(dir_okay=False), default=None, - help="The path to an `octue.yaml` file defining the service to run. If not provided, the " - "`OCTUE_SERVICE_CONFIGURATION_PATH` environment variable is used if present, otherwise the local path `octue.yaml` " - "is used.", + help="If asking a local question, the path to an `octue.yaml` file defining the service to run. If not provided, " + "the `OCTUE_SERVICE_CONFIGURATION_PATH` environment variable is used if present, otherwise the local path " + "`octue.yaml` is used. This argument is ignored if a remote question is being asked.", ) -def ask(sruid, input_values, input_manifest, service_config): +def ask(sruid, input_values, input_manifest, project_name, service_config): """Ask a question to a local or remote Octue Twined service. SRUID should be: @@ -115,17 +124,21 @@ def ask(sruid, input_values, input_manifest, service_config): e.g. octue question ask local """ - if sruid != "local": - pass - - service_configuration, app_configuration = load_service_and_app_configuration(service_config) - if input_values: - input_values = json.loads(input_values) + input_values = json.loads(input_values, cls=OctueJSONDecoder) if input_manifest: input_manifest = Manifest.deserialise(input_manifest, from_string=True) + if sruid != "local": + if not project_name: + _, project_name = auth.default() + + child = Child(id=sruid, backend=service_backends.get_backend()(project_name=project_name)) + answer, _ = child.ask(input_values=input_values, input_manifest=input_manifest) + return json.dumps(answer, cls=OctueJSONEncoder) + + service_configuration, app_configuration = load_service_and_app_configuration(service_config) service_namespace, service_name, service_revision_tag = get_sruid_parts(service_configuration) child_sruid = create_sruid(namespace=service_namespace, name=service_name, revision_tag=service_revision_tag) parent_sruid = "local/local:local" From eb0066aa31bf9a0c9bdcf1f872b53598f75e0c1c Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Mon, 13 Jan 2025 10:51:06 +0000 Subject: [PATCH 010/125] FIX: Fix incorrect type --- octue/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/octue/cli.py b/octue/cli.py index 41a39eb75..45c610a7a 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -97,7 +97,7 @@ def question(): @click.option( "-p", "--project-name", - type="str", + type=str, default=None, help="If asking a remote question, the name of the Google Cloud project the service is deployed in. If not " "provided, the project name is detected from the local Google application credentials if present.", From aa2fd4b9ee7b89f12f3a078f97036e54be9c1466 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Mon, 13 Jan 2025 10:52:10 +0000 Subject: [PATCH 011/125] REF: Split `octue question ask` into `ask` and `ask-local` commands --- octue/cli.py | 66 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 42 insertions(+), 24 deletions(-) diff --git a/octue/cli.py b/octue/cli.py index 45c610a7a..139b182b4 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -102,44 +102,62 @@ def question(): help="If asking a remote question, the name of the Google Cloud project the service is deployed in. If not " "provided, the project name is detected from the local Google application credentials if present.", ) +def ask(sruid, input_values, input_manifest, project_name): + """Ask a question to a remote Octue Twined service. + + SRUID should be a valid service revision unique identifier for an existing Octue Twined service + + e.g. octue question ask octue/example-service:1.0.3 + """ + if input_values: + input_values = json.loads(input_values, cls=OctueJSONDecoder) + + if input_manifest: + input_manifest = Manifest.deserialise(input_manifest, from_string=True) + + if not project_name: + _, project_name = auth.default() + + child = Child(id=sruid, backend=service_backends.get_backend()(project_name=project_name)) + answer, _ = child.ask(input_values=input_values, input_manifest=input_manifest) + return json.dumps(answer, cls=OctueJSONEncoder) + + +@question.command() +@click.option( + "-i", + "--input-values", + type=str, + default=None, + help="Any input values for the question as a JSON-encoded string.", +) +@click.option( + "-m", + "--input-manifest", + type=str, + default=None, + help="An optional input manifest for the question serialised as a JSON-encoded string.", +) @click.option( "-c", "--service-config", type=click.Path(dir_okay=False), default=None, - help="If asking a local question, the path to an `octue.yaml` file defining the service to run. If not provided, " - "the `OCTUE_SERVICE_CONFIGURATION_PATH` environment variable is used if present, otherwise the local path " - "`octue.yaml` is used. This argument is ignored if a remote question is being asked.", + help="The path to an `octue.yaml` file defining the service to run. If not provided, the " + "`OCTUE_SERVICE_CONFIGURATION_PATH` environment variable is used if present, otherwise the local path `octue.yaml` " + "is used.", ) -def ask(sruid, input_values, input_manifest, project_name, service_config): - """Ask a question to a local or remote Octue Twined service. - - SRUID should be: - - - For remote services: a valid service revision unique identifier for an existing Octue Twined service - - e.g. octue question ask octue/example-service:1.0.3 - - - For a local service: "local" - - e.g. octue question ask local - """ +def ask_local(input_values, input_manifest, service_config): + """Ask a question to a local Octue Twined service.""" if input_values: input_values = json.loads(input_values, cls=OctueJSONDecoder) if input_manifest: input_manifest = Manifest.deserialise(input_manifest, from_string=True) - if sruid != "local": - if not project_name: - _, project_name = auth.default() - - child = Child(id=sruid, backend=service_backends.get_backend()(project_name=project_name)) - answer, _ = child.ask(input_values=input_values, input_manifest=input_manifest) - return json.dumps(answer, cls=OctueJSONEncoder) - service_configuration, app_configuration = load_service_and_app_configuration(service_config) service_namespace, service_name, service_revision_tag = get_sruid_parts(service_configuration) + child_sruid = create_sruid(namespace=service_namespace, name=service_name, revision_tag=service_revision_tag) parent_sruid = "local/local:local" From 96939505edbec97e04a6e2b45f12bccd003d3925 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Mon, 13 Jan 2025 10:56:02 +0000 Subject: [PATCH 012/125] ENH: Add `async` option to `octue question ask` skipci --- octue/cli.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/octue/cli.py b/octue/cli.py index 139b182b4..f7c4818a5 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -102,7 +102,14 @@ def question(): help="If asking a remote question, the name of the Google Cloud project the service is deployed in. If not " "provided, the project name is detected from the local Google application credentials if present.", ) -def ask(sruid, input_values, input_manifest, project_name): +@click.option( + "--async", + is_flag=True, + default=True, + help="If provided, ask the question and detach (the result and other events can be retrieved from the event store " + "later).", +) +def ask(sruid, input_values, input_manifest, project_name, asynchronous): """Ask a question to a remote Octue Twined service. SRUID should be a valid service revision unique identifier for an existing Octue Twined service @@ -119,7 +126,7 @@ def ask(sruid, input_values, input_manifest, project_name): _, project_name = auth.default() child = Child(id=sruid, backend=service_backends.get_backend()(project_name=project_name)) - answer, _ = child.ask(input_values=input_values, input_manifest=input_manifest) + answer, _ = child.ask(input_values=input_values, input_manifest=input_manifest, asynchronous=asynchronous) return json.dumps(answer, cls=OctueJSONEncoder) From 37e20c14af60e479637c8cc4d1d8e424d858636f Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Mon, 13 Jan 2025 10:57:47 +0000 Subject: [PATCH 013/125] ENH: Deprecate `octue run` command --- octue/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/octue/cli.py b/octue/cli.py index f7c4818a5..e022a9a9c 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -188,7 +188,7 @@ def ask_local(input_values, input_manifest, service_config): answer_question(question=question, project_name=backend.project_name, service_configuration=service_config) -@octue_cli.command() +@octue_cli.command(deprecated=True) @click.option( "-c", "--service-config", From 823d6f5ffbb3bbf405a7c31c5e632f05307967f9 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Mon, 13 Jan 2025 11:00:58 +0000 Subject: [PATCH 014/125] FIX: Return question UUID for async `octue question ask` --- octue/cli.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/octue/cli.py b/octue/cli.py index e022a9a9c..92ec6bebc 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -126,7 +126,16 @@ def ask(sruid, input_values, input_manifest, project_name, asynchronous): _, project_name = auth.default() child = Child(id=sruid, backend=service_backends.get_backend()(project_name=project_name)) - answer, _ = child.ask(input_values=input_values, input_manifest=input_manifest, asynchronous=asynchronous) + + answer, question_uuid = child.ask( + input_values=input_values, + input_manifest=input_manifest, + asynchronous=asynchronous, + ) + + if asynchronous: + return question_uuid + return json.dumps(answer, cls=OctueJSONEncoder) From 8a4bfb68787d455806c3573b6ec524dc355c4198 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Mon, 13 Jan 2025 11:56:55 +0000 Subject: [PATCH 015/125] REF: Rename `answer_question` to `answer_pub_sub_question` --- octue/cli.py | 4 ++-- octue/cloud/deployment/google/answer_pub_sub_question.py | 2 +- octue/cloud/deployment/google/cloud_run/flask_app.py | 4 ++-- .../cloud/deployment/google/test_answer_pub_sub_question.py | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/octue/cli.py b/octue/cli.py index 92ec6bebc..d2330d716 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -11,7 +11,7 @@ from google import auth from octue.cloud import pub_sub, storage -from octue.cloud.deployment.google.answer_pub_sub_question import answer_question +from octue.cloud.deployment.google.answer_pub_sub_question import answer_pub_sub_question from octue.cloud.events.utils import make_originator_question_event from octue.cloud.pub_sub.service import Service from octue.cloud.service_id import create_sruid, get_sruid_parts @@ -194,7 +194,7 @@ def ask_local(input_values, input_manifest, service_config): _, project_name = auth.default() backend = service_backends.get_backend()(project_name=project_name) - answer_question(question=question, project_name=backend.project_name, service_configuration=service_config) + answer_pub_sub_question(question=question, project_name=backend.project_name, service_configuration=service_config) @octue_cli.command(deprecated=True) diff --git a/octue/cloud/deployment/google/answer_pub_sub_question.py b/octue/cloud/deployment/google/answer_pub_sub_question.py index afb1d5d79..4c6d2f886 100644 --- a/octue/cloud/deployment/google/answer_pub_sub_question.py +++ b/octue/cloud/deployment/google/answer_pub_sub_question.py @@ -11,7 +11,7 @@ logger = logging.getLogger(__name__) -def answer_question(question, project_name, service_configuration=None, app_configuration=None): +def answer_pub_sub_question(question, project_name, service_configuration=None, app_configuration=None): """Answer a question sent to an app deployed in Google Cloud. :param dict question: diff --git a/octue/cloud/deployment/google/cloud_run/flask_app.py b/octue/cloud/deployment/google/cloud_run/flask_app.py index ded293d9b..97aba296f 100644 --- a/octue/cloud/deployment/google/cloud_run/flask_app.py +++ b/octue/cloud/deployment/google/cloud_run/flask_app.py @@ -3,7 +3,7 @@ import google.api_core.exceptions from flask import Flask, request -from octue.cloud.deployment.google.answer_pub_sub_question import answer_question +from octue.cloud.deployment.google.answer_pub_sub_question import answer_pub_sub_question from octue.cloud.pub_sub.bigquery import get_events from octue.configuration import ServiceConfiguration @@ -43,7 +43,7 @@ def index(): return QUESTION_ACKNOWLEDGMENT_RESPONSE project_name = envelope["subscription"].split("/")[1] - answer_question(question=question, project_name=project_name) + answer_pub_sub_question(question=question, project_name=project_name) return QUESTION_ACKNOWLEDGMENT_RESPONSE diff --git a/tests/cloud/deployment/google/test_answer_pub_sub_question.py b/tests/cloud/deployment/google/test_answer_pub_sub_question.py index 37513b674..f3b7b1db9 100644 --- a/tests/cloud/deployment/google/test_answer_pub_sub_question.py +++ b/tests/cloud/deployment/google/test_answer_pub_sub_question.py @@ -5,7 +5,7 @@ import yaml -from octue.cloud.deployment.google.answer_pub_sub_question import answer_question +from octue.cloud.deployment.google.answer_pub_sub_question import answer_pub_sub_question from octue.cloud.emulators._pub_sub import MockTopic from octue.utils.patches import MultiPatcher from tests.mocks import MockOpen @@ -32,7 +32,7 @@ def test_with_no_app_configuration_file(self): with patch( "octue.cloud.deployment.google.answer_pub_sub_question.Runner.from_configuration" ) as mock_constructor: - answer_question( + answer_pub_sub_question( question={ "data": {}, "attributes": { @@ -96,7 +96,7 @@ class MockOpenForConfigurationFiles(MockOpen): patch.dict(os.environ, {"OCTUE_SERVICE_REVISION_TAG": "blah"}), ] ): - answer_question( + answer_pub_sub_question( question={ "data": {}, "attributes": { From 3d4c525192b40ade7c83023c32ced40d90a59f18 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Mon, 13 Jan 2025 13:07:33 +0000 Subject: [PATCH 016/125] FIX: Fix event and attributes generation --- octue/cloud/events/utils.py | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/octue/cloud/events/utils.py b/octue/cloud/events/utils.py index 0c0ca7378..b2ed5cd58 100644 --- a/octue/cloud/events/utils.py +++ b/octue/cloud/events/utils.py @@ -2,16 +2,14 @@ import uuid from octue.cloud import LOCAL_SDK_VERSION +from octue.utils.dictionaries import make_minimal_dictionary def make_originator_question_event(input_values, input_manifest, parent_sruid, child_sruid, question_uuid=None): - question_uuid = question_uuid or uuid.uuid4() + question_uuid = question_uuid or str(uuid.uuid4()) return { - "event": { - "input_values": input_values, - "input_manifest": input_manifest, - }, + "event": make_minimal_dictionary(input_values=input_values, input_manifest=input_manifest, kind="question"), "attributes": make_attributes( question_uuid=question_uuid, parent_question_uuid=question_uuid, @@ -20,6 +18,9 @@ def make_originator_question_event(input_values, input_manifest, parent_sruid, c originator=parent_sruid, sender=parent_sruid, recipient=child_sruid, + forward_logs=True, + save_diagnostics="SAVE_DIAGNOSTICS_ON", + sender_type="PARENT", ), } @@ -30,11 +31,14 @@ def make_attributes( parent, originator, sender, + sender_type, recipient, question_uuid=None, retry_count=0, + forward_logs=None, + save_diagnostics=None, ): - return { + attributes = { "uuid": str(uuid.uuid4()), "datetime": datetime.datetime.now(tz=datetime.timezone.utc).isoformat(), "question_uuid": question_uuid or str(uuid.uuid4()), @@ -43,7 +47,20 @@ def make_attributes( "parent": parent, "originator": originator, "sender": sender, + "sender_type": sender_type, "sender_sdk_version": LOCAL_SDK_VERSION, "recipient": recipient, "retry_count": retry_count, } + + if sender_type == "PARENT": + if forward_logs is None or save_diagnostics is None: + raise ValueError( + "`forward_logs` and `save_diagnostics` must be present in the attributes if the sender type is " + "'PARENT'." + ) + + attributes["forward_logs"] = forward_logs + attributes["save_diagnostics"] = save_diagnostics + + return attributes From bd07a3d61b31866fa23e7eaf5b117f2d5dfce8ac Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Mon, 13 Jan 2025 13:08:04 +0000 Subject: [PATCH 017/125] FIX: Add missing kwarg --- octue/cloud/pub_sub/service.py | 1 + 1 file changed, 1 insertion(+) diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index ce695afbb..3cb1ff71b 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -545,6 +545,7 @@ def _emit_event( parent=parent, originator=originator, sender=self.id, + sender_type=attributes["sender_type"], recipient=recipient, retry_count=retry_count, ) From f5e21f32557013caa0f912dc2338935f50bf20ed Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Mon, 13 Jan 2025 13:09:24 +0000 Subject: [PATCH 018/125] ENH: Support parsing already-extracted questions skipci --- octue/cloud/pub_sub/service.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index 3cb1ff71b..e0fa1ff72 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -764,11 +764,17 @@ def _parse_question(self, question): if hasattr(question, "ack"): question.ack() - event, attributes = extract_event_and_attributes_from_pub_sub_message(question) - event_for_validation = copy.deepcopy(event) + # Support already-extracted questions (e.g. from the `octue question ask-local` CLI command). + if "event" in question: + event = copy.deepcopy(question["event"]) + attributes = question["attributes"] + + # Extract question from Cloud Run or Pub/Sub format. + else: + event, attributes = extract_event_and_attributes_from_pub_sub_message(question) raise_if_event_is_invalid( - event=event_for_validation, + event=copy.deepcopy(event), attributes=attributes, recipient=self.id, # Don't assume the presence of specific attributes before validation. From 40c3dfa75a84dc8a7d616dbf47f9f72b9468a178 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Mon, 13 Jan 2025 14:39:46 +0000 Subject: [PATCH 019/125] ENH: Remove unnecessary log message --- octue/cloud/deployment/google/answer_pub_sub_question.py | 1 - 1 file changed, 1 deletion(-) diff --git a/octue/cloud/deployment/google/answer_pub_sub_question.py b/octue/cloud/deployment/google/answer_pub_sub_question.py index 4c6d2f886..b9b48f0a3 100644 --- a/octue/cloud/deployment/google/answer_pub_sub_question.py +++ b/octue/cloud/deployment/google/answer_pub_sub_question.py @@ -44,7 +44,6 @@ def answer_pub_sub_question(question, project_name, service_configuration=None, service.run_function = runner.run service.answer(question) - logger.info("Analysis successfully run and response sent for question %r.", question_uuid) except BaseException as error: # noqa service.send_exception( From ef684b753691f8b85c119b291237eed798101343 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Mon, 13 Jan 2025 15:18:23 +0000 Subject: [PATCH 020/125] FEA: Add `octue question events raw` command skipci --- octue/cli.py | 106 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 105 insertions(+), 1 deletion(-) diff --git a/octue/cli.py b/octue/cli.py index d2330d716..08191c861 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -13,10 +13,12 @@ from octue.cloud import pub_sub, storage from octue.cloud.deployment.google.answer_pub_sub_question import answer_pub_sub_question from octue.cloud.events.utils import make_originator_question_event +from octue.cloud.events.validation import VALID_EVENT_KINDS +from octue.cloud.pub_sub.bigquery import get_events from octue.cloud.pub_sub.service import Service from octue.cloud.service_id import create_sruid, get_sruid_parts from octue.cloud.storage import GoogleCloudStorageClient -from octue.configuration import load_service_and_app_configuration +from octue.configuration import load_service_and_app_configuration, ServiceConfiguration from octue.definitions import MANIFEST_FILENAME, VALUES_FILENAME from octue.exceptions import ServiceAlreadyExists from octue.log_handlers import apply_log_handler, get_remote_handler @@ -197,6 +199,108 @@ def ask_local(input_values, input_manifest, service_config): answer_pub_sub_question(question=question, project_name=backend.project_name, service_configuration=service_config) +@question.group() +def events(): + """Get and replay events from questions to Octue Twined services.""" + + +@events.command() +@click.option( + "--question-uuid", + type=str, + default=None, + help="The UUID of the question to get events for.", +) +@click.option( + "--parent-question-uuid", + type=str, + help="The UUID of a parent question to get the sub-question events for", +) +@click.option( + "--originator-question-uuid", + type=str, + help="The UUID of an originator question get the full tree of events for", +) +@click.option( + "-k", + "--kinds", + type=str, + default=None, + help="The kinds of event to get as a comma-separated list e.g. 'question,result'. If not provided, all event kinds " + "are returned. The valid kinds are " + f"{VALID_EVENT_KINDS!r}.", +) +@click.option( + "-e", + "--exclude-kinds", + type=str, + default=None, + help="The kinds of event to exclude as a comma-separated list e.g. 'question,result'. If not provided, all event " + "kinds are returned. The valid kinds are " + f"{VALID_EVENT_KINDS!r}.", +) +@click.option( + "--include-backend-metadata", + is_flag=True, + help="Include the service backend metadata.", +) +@click.option( + "-l", + "--limit", + type=int, + default=1000, + show_default=True, + help="Limit the number of events returned.", +) +@click.option( + "-c", + "--service-config", + type=click.Path(dir_okay=False), + default=None, + help="The path to an `octue.yaml` file defining the service to run. If not provided, the " + "`OCTUE_SERVICE_CONFIGURATION_PATH` environment variable is used if present, otherwise the local path `octue.yaml` " + "is used.", +) +def raw( + question_uuid, + parent_question_uuid, + originator_question_uuid, + kinds, + exclude_kinds, + include_backend_metadata, + limit, + service_config, +): + """Get the raw events emitted during a question. One of the following must be set: + + --question-uuid + + --parent-question-uuid + + --originator-question-uuid + """ + if kinds: + kinds = kinds.split(",") + + if exclude_kinds: + exclude_kinds = exclude_kinds.split(",") + + service_configuration = ServiceConfiguration.from_file(path=service_config) + + events = get_events( + table_id=service_configuration.event_store_table_id, + question_uuid=question_uuid, + parent_question_uuid=parent_question_uuid, + originator_question_uuid=originator_question_uuid, + kinds=kinds, + exclude_kinds=exclude_kinds, + include_backend_metadata=include_backend_metadata, + limit=limit, + ) + + click.echo(events) + + @octue_cli.command(deprecated=True) @click.option( "-c", From 386f508daaa3d9f33842e16316a9b3af5ce11524 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Mon, 13 Jan 2025 15:30:46 +0000 Subject: [PATCH 021/125] FEA: Add `octue question events replay` CLI command --- octue/cli.py | 128 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) diff --git a/octue/cli.py b/octue/cli.py index 08191c861..57798ffe2 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -12,6 +12,7 @@ from octue.cloud import pub_sub, storage from octue.cloud.deployment.google.answer_pub_sub_question import answer_pub_sub_question +from octue.cloud.events.replayer import EventReplayer from octue.cloud.events.utils import make_originator_question_event from octue.cloud.events.validation import VALID_EVENT_KINDS from octue.cloud.pub_sub.bigquery import get_events @@ -301,6 +302,133 @@ def raw( click.echo(events) +@events.command() +@click.option( + "--question-uuid", + type=str, + default=None, + help="The UUID of the question to get events for.", +) +@click.option( + "--parent-question-uuid", + type=str, + help="The UUID of a parent question to get the sub-question events for", +) +@click.option( + "--originator-question-uuid", + type=str, + help="The UUID of an originator question get the full tree of events for", +) +@click.option( + "-k", + "--kinds", + type=str, + default=None, + help="The kinds of event to get as a comma-separated list e.g. 'question,result'. If not provided, all event kinds " + "are returned. The valid kinds are " + f"{VALID_EVENT_KINDS!r}.", +) +@click.option( + "-e", + "--exclude-kinds", + type=str, + default=None, + help="The kinds of event to exclude as a comma-separated list e.g. 'question,result'. If not provided, all event " + "kinds are returned. The valid kinds are " + f"{VALID_EVENT_KINDS!r}.", +) +@click.option( + "-l", + "--limit", + type=int, + default=1000, + show_default=True, + help="Limit the number of events returned.", +) +@click.option( + "-c", + "--service-config", + type=click.Path(dir_okay=False), + default=None, + help="The path to an `octue.yaml` file defining the service to run. If not provided, the " + "`OCTUE_SERVICE_CONFIGURATION_PATH` environment variable is used if present, otherwise the local path `octue.yaml` " + "is used.", +) +@click.option( + "--include-service-metadata-in-logs", + is_flag=True, + help="Include the SRUIDs and question UUIDs of the service revisions involved in the question at the start of each " + "log message.", +) +@click.option( + "--exclude-logs-containing", + type=str, + default=None, + help="Skip handling log messages containing this string.", +) +@click.option( + "-r", + "--only-handle-result", + is_flag=True, + help="Skip non-result events and only handle the 'result' event if present.", +) +@click.option( + "--validate-events", + is_flag=True, + help="Validate events before attempting to handle them (this is off by default to speed up event handling)", +) +def replay( + question_uuid, + parent_question_uuid, + originator_question_uuid, + kinds, + exclude_kinds, + limit, + service_config, + include_service_metadata_in_logs, + exclude_logs_containing, + only_handle_result, + validate_events, +): + """Replay a question's events, returning the result if there is one. One of the following must be set: + + --question-uuid + + --parent-question-uuid + + --originator-question-uuid + """ + if kinds: + kinds = kinds.split(",") + + if exclude_kinds: + exclude_kinds = exclude_kinds.split(",") + + service_configuration = ServiceConfiguration.from_file(path=service_config) + + events = get_events( + table_id=service_configuration.event_store_table_id, + question_uuid=question_uuid, + parent_question_uuid=parent_question_uuid, + originator_question_uuid=originator_question_uuid, + kinds=kinds, + exclude_kinds=exclude_kinds, + limit=limit, + ) + + replayer = EventReplayer( + include_service_metadata_in_logs=include_service_metadata_in_logs, + exclude_logs_containing=exclude_logs_containing, + only_handle_result=only_handle_result, + validate_events=validate_events, + ) + + result = replayer.handle_events(events) + + if result: + click.echo(result) + + @octue_cli.command(deprecated=True) @click.option( "-c", From 1fce3babd024a2b3defbdd0596c3ba9997d430d6 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Mon, 13 Jan 2025 15:34:32 +0000 Subject: [PATCH 022/125] REF: Move `ask*` subcommands under `octue question ask` group skipci --- octue/cli.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/octue/cli.py b/octue/cli.py index 57798ffe2..11d0dd6f9 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -78,10 +78,15 @@ def octue_cli(id, logger_uri, log_level, force_reset): @octue_cli.group() def question(): - """Ask and interact with questions to an Octue Twined data service.""" + """Ask a new question to an Octue Twined data service or interact with a previous question.""" -@question.command() +@question.group() +def ask(): + """Ask a new question to an Octue Twined data service.""" + + +@ask.command() @click.argument("sruid", type=str) @click.option( "-i", @@ -112,7 +117,7 @@ def question(): help="If provided, ask the question and detach (the result and other events can be retrieved from the event store " "later).", ) -def ask(sruid, input_values, input_manifest, project_name, asynchronous): +def remote(sruid, input_values, input_manifest, project_name, asynchronous): """Ask a question to a remote Octue Twined service. SRUID should be a valid service revision unique identifier for an existing Octue Twined service @@ -142,7 +147,7 @@ def ask(sruid, input_values, input_manifest, project_name, asynchronous): return json.dumps(answer, cls=OctueJSONEncoder) -@question.command() +@ask.command() @click.option( "-i", "--input-values", @@ -166,7 +171,7 @@ def ask(sruid, input_values, input_manifest, project_name, asynchronous): "`OCTUE_SERVICE_CONFIGURATION_PATH` environment variable is used if present, otherwise the local path `octue.yaml` " "is used.", ) -def ask_local(input_values, input_manifest, service_config): +def local(input_values, input_manifest, service_config): """Ask a question to a local Octue Twined service.""" if input_values: input_values = json.loads(input_values, cls=OctueJSONDecoder) From d3faaf7dcf1259aee5115d4bf1200af6284f946b Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Mon, 13 Jan 2025 15:39:57 +0000 Subject: [PATCH 023/125] FIX: Fix argument name and backend format in CLI command --- octue/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/octue/cli.py b/octue/cli.py index 11d0dd6f9..cff97cb48 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -111,7 +111,7 @@ def ask(): "provided, the project name is detected from the local Google application credentials if present.", ) @click.option( - "--async", + "--asynchronous", is_flag=True, default=True, help="If provided, ask the question and detach (the result and other events can be retrieved from the event store " @@ -133,7 +133,7 @@ def remote(sruid, input_values, input_manifest, project_name, asynchronous): if not project_name: _, project_name = auth.default() - child = Child(id=sruid, backend=service_backends.get_backend()(project_name=project_name)) + child = Child(id=sruid, backend={"name": "GCPPubSubBackend", "project_name": project_name}) answer, question_uuid = child.ask( input_values=input_values, From f1c6dd96945f5902da5f9aad8e2eb3a89ce60407 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Mon, 13 Jan 2025 15:43:57 +0000 Subject: [PATCH 024/125] FIX: Fix `--asynchronous` option in command --- octue/cli.py | 1 - 1 file changed, 1 deletion(-) diff --git a/octue/cli.py b/octue/cli.py index cff97cb48..db256012e 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -113,7 +113,6 @@ def ask(): @click.option( "--asynchronous", is_flag=True, - default=True, help="If provided, ask the question and detach (the result and other events can be retrieved from the event store " "later).", ) From 390be588dad9af200f490652f99d5b15b5e01f64 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Mon, 13 Jan 2025 15:46:26 +0000 Subject: [PATCH 025/125] FIX: Fix setting attributes for question event --- octue/cloud/pub_sub/service.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index e0fa1ff72..65ea50ba3 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -537,18 +537,16 @@ def _emit_event( """ attributes = attributes or {} - attributes.update( - make_attributes( - question_uuid=question_uuid, - parent_question_uuid=parent_question_uuid, - originator_question_uuid=originator_question_uuid, - parent=parent, - originator=originator, - sender=self.id, - sender_type=attributes["sender_type"], - recipient=recipient, - retry_count=retry_count, - ) + attributes = make_attributes( + question_uuid=question_uuid, + parent_question_uuid=parent_question_uuid, + originator_question_uuid=originator_question_uuid, + parent=parent, + originator=originator, + sender=self.id, + recipient=recipient, + retry_count=retry_count, + **attributes, ) converted_attributes = {} From ecc4ef99fb781bd92e13a5ee566b07d86eb42d39 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Mon, 13 Jan 2025 15:48:41 +0000 Subject: [PATCH 026/125] ENH: Improve description of commands --- octue/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/octue/cli.py b/octue/cli.py index db256012e..a69c7386c 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -276,7 +276,7 @@ def raw( limit, service_config, ): - """Get the raw events emitted during a question. One of the following must be set: + """Get the raw events emitted during a question as JSON. One of the following must be set: --question-uuid @@ -394,7 +394,7 @@ def replay( only_handle_result, validate_events, ): - """Replay a question's events, returning the result if there is one. One of the following must be set: + """Replay a question's events, returning the result as JSON if there is one. One of the following must be set: --question-uuid From 69cada185b43bd2a8123347bbeadae446966557d Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Mon, 13 Jan 2025 15:50:06 +0000 Subject: [PATCH 027/125] REF: Rename `octue question events raw` to `octue question events get` skipci --- octue/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/octue/cli.py b/octue/cli.py index a69c7386c..4202d37c9 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -266,7 +266,7 @@ def events(): "`OCTUE_SERVICE_CONFIGURATION_PATH` environment variable is used if present, otherwise the local path `octue.yaml` " "is used.", ) -def raw( +def get( question_uuid, parent_question_uuid, originator_question_uuid, From bbcf14bfd087e2533d3c8db23f27b689076052d4 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Mon, 13 Jan 2025 17:08:59 +0000 Subject: [PATCH 028/125] ENH: Suppress google crc32c warning --- octue/cloud/storage/client.py | 5 ++++- octue/mixins/hashable.py | 5 ++++- octue/resources/datafile.py | 6 +++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/octue/cloud/storage/client.py b/octue/cloud/storage/client.py index 956f4802f..5e31e1d87 100644 --- a/octue/cloud/storage/client.py +++ b/octue/cloud/storage/client.py @@ -14,7 +14,10 @@ from google.cloud.storage import Client from google.cloud.storage.constants import _DEFAULT_TIMEOUT from google.cloud.storage.retry import DEFAULT_RETRY -from google_crc32c import Checksum + +with warnings.catch_warnings(): + warnings.simplefilter("ignore") + from google_crc32c import Checksum from octue.cloud import storage from octue.exceptions import CloudStorageBucketNotFound diff --git a/octue/mixins/hashable.py b/octue/mixins/hashable.py index d2689fea6..1efdc6497 100644 --- a/octue/mixins/hashable.py +++ b/octue/mixins/hashable.py @@ -1,8 +1,11 @@ import base64 import collections.abc import datetime +import warnings -from google_crc32c import Checksum +with warnings.catch_warnings(): + warnings.simplefilter("ignore") + from google_crc32c import Checksum EMPTY_STRING_HASH_VALUE = "AAAAAA==" diff --git a/octue/resources/datafile.py b/octue/resources/datafile.py index 1d89e8dff..f139bcdc9 100644 --- a/octue/resources/datafile.py +++ b/octue/resources/datafile.py @@ -7,9 +7,13 @@ import os import shutil import tempfile +import warnings import requests -from google_crc32c import Checksum + +with warnings.catch_warnings(): + warnings.simplefilter("ignore") + from google_crc32c import Checksum # The `h5py` package is only needed if dealing with HDF5 files. It's only available if the `hdf5` extra is provided From 4c4e559767727c7ffce13c1042139917d6372e4d Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Mon, 13 Jan 2025 17:16:10 +0000 Subject: [PATCH 029/125] FIX: Return data from `octue question ask remote` properly --- octue/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/octue/cli.py b/octue/cli.py index 4202d37c9..fb77810ea 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -141,9 +141,9 @@ def remote(sruid, input_values, input_manifest, project_name, asynchronous): ) if asynchronous: - return question_uuid + click.echo(question_uuid) - return json.dumps(answer, cls=OctueJSONEncoder) + click.echo(answer) @ask.command() From da628a520a43ef76138f8ca9835abd6a8f0060fc Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Mon, 13 Jan 2025 17:23:17 +0000 Subject: [PATCH 030/125] FIX: Await publishing of `result` event skipci --- octue/cloud/pub_sub/service.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index 65ea50ba3..d42f03353 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -273,7 +273,7 @@ def answer(self, question, heartbeat_interval=120, timeout=30): if analysis.output_manifest is not None: result["output_manifest"] = analysis.output_manifest.to_primitive() - self._emit_event( + future = self._emit_event( event=result, recipient=parent, attributes={"sender_type": CHILD_SENDER_TYPE}, @@ -281,6 +281,9 @@ def answer(self, question, heartbeat_interval=120, timeout=30): **routing_metadata, ) + # Await successful publishing of the result. + future.result() + heartbeater.cancel() logger.info("%r answered question %r.", self, question_uuid) From f49121012fd47e858e5db9081d65d695436c39ed Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Tue, 14 Jan 2025 13:07:55 +0000 Subject: [PATCH 031/125] FIX: Return early for async question in `octue question ask remote` --- octue/cli.py | 1 + 1 file changed, 1 insertion(+) diff --git a/octue/cli.py b/octue/cli.py index fb77810ea..683bb8345 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -142,6 +142,7 @@ def remote(sruid, input_values, input_manifest, project_name, asynchronous): if asynchronous: click.echo(question_uuid) + return click.echo(answer) From 444093f8ea3ed6a3fbaf3e9519a460ff9498d3ed Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Tue, 14 Jan 2025 13:15:48 +0000 Subject: [PATCH 032/125] ENH: Improve description of `octue question ask local` skipci --- octue/cli.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/octue/cli.py b/octue/cli.py index 683bb8345..af4018800 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -172,7 +172,14 @@ def remote(sruid, input_values, input_manifest, project_name, asynchronous): "is used.", ) def local(input_values, input_manifest, service_config): - """Ask a question to a local Octue Twined service.""" + """Ask a question to a local Octue Twined service. + + This command is similar to running `octue service start` and asking the resulting local service revision a question + via Pub/Sub. Instead of starting a local Pub/Sub service revision, however, no Pub/Sub subscription or subscriber is + created; the question is instead passed directly and to local the service revision without Pub/Sub being involved. + Everything after this runs the same, though, with any events emitted by the service revision emitted via Pub/Sub as + usual. + """ if input_values: input_values = json.loads(input_values, cls=OctueJSONDecoder) From e5fc542b038a1885325aeefeae5a96653cecd14a Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Thu, 23 Jan 2025 12:41:47 +0000 Subject: [PATCH 033/125] STY: Add missing ruff isort pre-commit check --- .pre-commit-config.yaml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 26e4dc519..e3dfeb7f0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,11 +13,18 @@ repos: - id: check-added-large-files - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.6.9 + rev: v0.9.2 hooks: - id: ruff + name: Ruff lint args: [--fix, --exit-non-zero-on-fix] + + - id: ruff + name: Ruff isort + args: [check, --select, I, --fix] + - id: ruff-format + name: Ruff format - repo: https://github.com/pre-commit/mirrors-prettier rev: v2.7.1 From 50a62e05b1d931e6fbfd79aef9a0ee73fb6e6cf1 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Thu, 23 Jan 2025 12:42:17 +0000 Subject: [PATCH 034/125] OPS: Correct version number skipci --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 173256551..2ae836891 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "octue" -version = "0.63.0" +version = "0.62.0" description = "A package providing template applications for data services, and a python SDK to the Octue API." readme = "README.md" authors = ["Marcus Lugg ", "Thomas Clark "] From 0d7c8c2290c7d6c5b26896dd85e46eaac405706a Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Thu, 23 Jan 2025 12:47:26 +0000 Subject: [PATCH 035/125] ENH: Move `get-diagnostics` command to `octue question diagnostics` skipci --- octue/cli.py | 145 +++++++++++++++++++-------------------- octue/resources/child.py | 3 +- octue/utils/testing.py | 2 +- tests/test_cli.py | 26 +++---- 4 files changed, 88 insertions(+), 88 deletions(-) diff --git a/octue/cli.py b/octue/cli.py index af4018800..b4f7c0a83 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -19,16 +19,15 @@ from octue.cloud.pub_sub.service import Service from octue.cloud.service_id import create_sruid, get_sruid_parts from octue.cloud.storage import GoogleCloudStorageClient -from octue.configuration import load_service_and_app_configuration, ServiceConfiguration +from octue.configuration import ServiceConfiguration, load_service_and_app_configuration from octue.definitions import MANIFEST_FILENAME, VALUES_FILENAME from octue.exceptions import ServiceAlreadyExists from octue.log_handlers import apply_log_handler, get_remote_handler -from octue.resources import Manifest, service_backends, Child +from octue.resources import Child, Manifest, service_backends from octue.runner import Runner from octue.utils.decoders import OctueJSONDecoder from octue.utils.encoders import OctueJSONEncoder - logger = logging.getLogger(__name__) global_cli_context = {} @@ -441,6 +440,76 @@ def replay( click.echo(result) +@question.command() +@click.argument( + "cloud_path", + type=str, +) +@click.option( + "--local-path", + type=click.Path(file_okay=False), + default=".", + help="The path to a directory to store the directory of diagnostics data in. Defaults to the current working " + "directory.", +) +@click.option( + "--download-datasets", + is_flag=True, + help="If provided, download any datasets from the diagnostics and update their paths in the configuration and " + "input manifests to the new local paths.", +) +def diagnostics(cloud_path, local_path, download_datasets): + """Download diagnostics for a question from the given directory in Google Cloud Storage. The cloud path should end + in the question ID. + + CLOUD_PATH: The path to the directory in Google Cloud Storage containing the diagnostics data. + """ + analysis_id = storage.path.split(cloud_path)[-1] + local_path = os.path.join(local_path, analysis_id) + + if download_datasets: + filter = None + else: + filter = lambda blob: any( + ( + blob.name.endswith(f"configuration_{VALUES_FILENAME}"), + blob.name.endswith(f"configuration_{MANIFEST_FILENAME}"), + blob.name.endswith(f"input_{VALUES_FILENAME}"), + blob.name.endswith(f"input_{MANIFEST_FILENAME}"), + blob.name.endswith("questions.json"), + ) + ) + + local_paths = GoogleCloudStorageClient().download_all_files( + local_path=local_path, + cloud_path=cloud_path, + filter=filter, + recursive=True, + ) + + if not local_paths: + logger.warning("No diagnostics found at %r.", cloud_path) + return + + # Update the manifests with the local paths of the datasets. + if download_datasets: + for manifest_type in ("configuration_manifest", "input_manifest"): + manifest_path = os.path.join(local_path, manifest_type + ".json") + + if not os.path.exists(manifest_path): + continue + + manifest = Manifest.from_file(manifest_path) + + manifest.update_dataset_paths( + path_generator=lambda dataset: os.path.join(local_path, f"{manifest_type}_datasets", dataset.name) + ) + + manifest.to_file(manifest_path) + + logger.info("Downloaded diagnostics from %r to %r.", cloud_path, local_path) + + @octue_cli.command(deprecated=True) @click.option( "-c", @@ -638,76 +707,6 @@ def start(service_config, revision_tag, timeout, no_rm): service.serve(timeout=timeout, delete_topic_and_subscription_on_exit=not no_rm) -@octue_cli.command() -@click.argument( - "cloud_path", - type=str, -) -@click.option( - "--local-path", - type=click.Path(file_okay=False), - default=".", - help="The path to a directory to store the directory of diagnostics data in. Defaults to the current working " - "directory.", -) -@click.option( - "--download-datasets", - is_flag=True, - help="If provided, download any datasets from the diagnostics and update their paths in the configuration and " - "input manifests to the new local paths.", -) -def get_diagnostics(cloud_path, local_path, download_datasets): - """Download diagnostics for a question from the given directory in Google Cloud Storage. The cloud path should end - in the question ID. - - CLOUD_PATH: The path to the directory in Google Cloud Storage containing the diagnostics data. - """ - analysis_id = storage.path.split(cloud_path)[-1] - local_path = os.path.join(local_path, analysis_id) - - if download_datasets: - filter = None - else: - filter = lambda blob: any( - ( - blob.name.endswith(f"configuration_{VALUES_FILENAME}"), - blob.name.endswith(f"configuration_{MANIFEST_FILENAME}"), - blob.name.endswith(f"input_{VALUES_FILENAME}"), - blob.name.endswith(f"input_{MANIFEST_FILENAME}"), - blob.name.endswith("questions.json"), - ) - ) - - local_paths = GoogleCloudStorageClient().download_all_files( - local_path=local_path, - cloud_path=cloud_path, - filter=filter, - recursive=True, - ) - - if not local_paths: - logger.warning("No diagnostics found at %r.", cloud_path) - return - - # Update the manifests with the local paths of the datasets. - if download_datasets: - for manifest_type in ("configuration_manifest", "input_manifest"): - manifest_path = os.path.join(local_path, manifest_type + ".json") - - if not os.path.exists(manifest_path): - continue - - manifest = Manifest.from_file(manifest_path) - - manifest.update_dataset_paths( - path_generator=lambda dataset: os.path.join(local_path, f"{manifest_type}_datasets", dataset.name) - ) - - manifest.to_file(manifest_path) - - logger.info("Downloaded diagnostics from %r to %r.", cloud_path, local_path) - - @octue_cli.group() def deploy(): """A collection of commands to aid deploying a python app to the cloud as an Octue service or digital twin.""" diff --git a/octue/resources/child.py b/octue/resources/child.py index c06917e40..bcdf93b5a 100644 --- a/octue/resources/child.py +++ b/octue/resources/child.py @@ -6,7 +6,6 @@ from octue.cloud.pub_sub.service import Service from octue.resources import service_backends - logger = logging.getLogger(__name__) BACKEND_TO_SERVICE_MAPPING = {"GCPPubSubBackend": Service} @@ -144,7 +143,7 @@ def ask( except Exception as e: logger.error( - "Question %r failed. Run 'octue get-diagnostics gs:///%s " + "Question %r failed. Run 'octue question diagnostics gs:///%s " "--download-datasets' to get the crash diagnostics.", question_uuid, question_uuid, diff --git a/octue/utils/testing.py b/octue/utils/testing.py index 8af330a2a..75f57d456 100644 --- a/octue/utils/testing.py +++ b/octue/utils/testing.py @@ -7,7 +7,7 @@ def load_test_fixture_from_diagnostics(path): - """Load a test fixture from service diagnostics downloaded using the `octue get-diagnostics` CLI + """Load a test fixture from service diagnostics downloaded using the `octue question diagnostics` CLI command. The configuration values, configuration manifest, input values, and input manifest are returned if available. A tuple of child emulators is returned if the service has children and asked questions to any of them. Each child emulator corresponds to one question asked to a child. The child emulators are in the same order as the diff --git a/tests/test_cli.py b/tests/test_cli.py index e1ce04b09..6cdde57aa 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -19,7 +19,6 @@ from tests import MOCK_SERVICE_REVISION_TAG, TEST_BUCKET_NAME, TESTS_DIR from tests.base import BaseTestCase - TWINE_FILE_PATH = os.path.join(TESTS_DIR, "data", "twines", "valid_schema_twine.json") @@ -57,7 +56,7 @@ def test_run(self): octue_cli, [ "run", - f'--input-dir={os.path.join(TESTS_DIR, "data", "data_dir_with_no_manifests", "input")}', + f"--input-dir={os.path.join(TESTS_DIR, 'data', 'data_dir_with_no_manifests', 'input')}", ], ) @@ -73,7 +72,7 @@ def test_run_with_output_values_file(self): octue_cli, [ "run", - f'--input-dir={os.path.join(TESTS_DIR, "data", "data_dir_with_no_manifests", "input")}', + f"--input-dir={os.path.join(TESTS_DIR, 'data', 'data_dir_with_no_manifests', 'input')}", "-o", temporary_file.name, ], @@ -107,7 +106,7 @@ def test_run_with_output_manifest(self): octue_cli, [ "run", - f'--input-dir={os.path.join(TESTS_DIR, "data", "data_dir_with_no_manifests", "input")}', + f"--input-dir={os.path.join(TESTS_DIR, 'data', 'data_dir_with_no_manifests', 'input')}", f"--output-manifest-file={temporary_manifest.name}", ], ) @@ -136,7 +135,7 @@ def test_run_with_monitor_messages_sent_to_file(self): octue_cli, [ "run", - f'--input-dir={os.path.join(TESTS_DIR, "data", "data_dir_with_no_manifests", "input")}', + f"--input-dir={os.path.join(TESTS_DIR, 'data', 'data_dir_with_no_manifests', 'input')}", f"--monitor-messages-file={monitor_messages_file.name}", ], ) @@ -155,7 +154,7 @@ def test_remote_logger_uri_can_be_set(self): [ "--logger-uri=wss://0.0.0.1:3000", "run", - f'--input-dir={os.path.join(TESTS_DIR, "data", "data_dir_with_no_manifests", "input")}', + f"--input-dir={os.path.join(TESTS_DIR, 'data', 'data_dir_with_no_manifests', 'input')}", ], ) @@ -253,7 +252,7 @@ class TestGetDiagnosticsCommand(BaseTestCase): @classmethod def setUpClass(cls): - """Upload the test diagnostics data to the cloud storage emulator so the `octue get-diagnostics` CLI command can + """Upload the test diagnostics data to the cloud storage emulator so the `octue question diagnostics` CLI command can be tested. :return None: @@ -274,7 +273,8 @@ def test_warning_logged_if_no_diagnostics_found(self): result = CliRunner().invoke( octue_cli, [ - "get-diagnostics", + "question", + "diagnostics", storage.path.join(self.DIAGNOSTICS_CLOUD_PATH, "9f4ccee3-15b0-4a03-b5ac-c19e1d66a709"), "--local-path", temporary_directory, @@ -297,13 +297,14 @@ def test_warning_logged_if_no_diagnostics_found(self): def test_get_diagnostics(self): """Test that only the values files, manifests, and questions file are downloaded when using the - `get-diagnostics` CLI command. + `question diagnostics` CLI command. """ with tempfile.TemporaryDirectory() as temporary_directory: result = CliRunner().invoke( octue_cli, [ - "get-diagnostics", + "question", + "diagnostics", storage.path.join(self.DIAGNOSTICS_CLOUD_PATH, self.ANALYSIS_ID), "--local-path", temporary_directory, @@ -349,13 +350,14 @@ def test_get_diagnostics(self): def test_get_diagnostics_with_datasets(self): """Test that datasets are downloaded as well as the values files, manifests, and questions file when the - `get-diagnostics` CLI command is run with the `--download-datasets` flag. + `question diagnostics` CLI command is run with the `--download-datasets` flag. """ with tempfile.TemporaryDirectory() as temporary_directory: result = CliRunner().invoke( octue_cli, [ - "get-diagnostics", + "question", + "diagnostics", storage.path.join(self.DIAGNOSTICS_CLOUD_PATH, self.ANALYSIS_ID), "--local-path", temporary_directory, From 41996c2a5f354565d90afae3c649a6a3790fc477 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Mon, 27 Jan 2025 17:17:02 +0000 Subject: [PATCH 036/125] ENH: Allow passing of attributes to `octue question ask local` skipci --- octue/cli.py | 30 ++++++++++++++++++++++-------- octue/cloud/events/utils.py | 32 ++++++++++++++++++++++++++------ 2 files changed, 48 insertions(+), 14 deletions(-) diff --git a/octue/cli.py b/octue/cli.py index b4f7c0a83..172ffbd82 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -13,7 +13,7 @@ from octue.cloud import pub_sub, storage from octue.cloud.deployment.google.answer_pub_sub_question import answer_pub_sub_question from octue.cloud.events.replayer import EventReplayer -from octue.cloud.events.utils import make_originator_question_event +from octue.cloud.events.utils import make_question_event from octue.cloud.events.validation import VALID_EVENT_KINDS from octue.cloud.pub_sub.bigquery import get_events from octue.cloud.pub_sub.service import Service @@ -152,14 +152,22 @@ def remote(sruid, input_values, input_manifest, project_name, asynchronous): "--input-values", type=str, default=None, - help="Any input values for the question as a JSON-encoded string.", + help="Any input values for the question, serialised as a JSON-encoded string.", ) @click.option( "-m", "--input-manifest", type=str, default=None, - help="An optional input manifest for the question serialised as a JSON-encoded string.", + help="An optional input manifest for the question, serialised as a JSON-encoded string.", +) +@click.option( + "-a", + "--attributes", + type=str, + default=None, + help="An optional full set of event attributes for the question, serialised as a JSON-encoded string. If not " + "provided, the question will be an originator question.", ) @click.option( "-c", @@ -170,7 +178,7 @@ def remote(sruid, input_values, input_manifest, project_name, asynchronous): "`OCTUE_SERVICE_CONFIGURATION_PATH` environment variable is used if present, otherwise the local path `octue.yaml` " "is used.", ) -def local(input_values, input_manifest, service_config): +def local(input_values, input_manifest, attributes, service_config): """Ask a question to a local Octue Twined service. This command is similar to running `octue service start` and asking the resulting local service revision a question @@ -186,16 +194,22 @@ def local(input_values, input_manifest, service_config): input_manifest = Manifest.deserialise(input_manifest, from_string=True) service_configuration, app_configuration = load_service_and_app_configuration(service_config) - service_namespace, service_name, service_revision_tag = get_sruid_parts(service_configuration) - child_sruid = create_sruid(namespace=service_namespace, name=service_name, revision_tag=service_revision_tag) - parent_sruid = "local/local:local" + if attributes: + attributes = json.loads(attributes, cls=OctueJSONDecoder) + parent_sruid = None + child_sruid = None + else: + parent_sruid = "local/local:local" + service_namespace, service_name, service_revision_tag = get_sruid_parts(service_configuration) + child_sruid = create_sruid(namespace=service_namespace, name=service_name, revision_tag=service_revision_tag) - question = make_originator_question_event( + question = make_question_event( input_values=input_values, input_manifest=input_manifest, parent_sruid=parent_sruid, child_sruid=child_sruid, + attributes=attributes, ) backend_configuration_values = (app_configuration.configuration_values or {}).get("backend") diff --git a/octue/cloud/events/utils.py b/octue/cloud/events/utils.py index b2ed5cd58..314a9df7d 100644 --- a/octue/cloud/events/utils.py +++ b/octue/cloud/events/utils.py @@ -5,12 +5,28 @@ from octue.utils.dictionaries import make_minimal_dictionary -def make_originator_question_event(input_values, input_manifest, parent_sruid, child_sruid, question_uuid=None): - question_uuid = question_uuid or str(uuid.uuid4()) +def make_question_event( + input_values, + input_manifest, + parent_sruid=None, + child_sruid=None, + question_uuid=None, + attributes=None, +): + """Make a question event. If the `attributes` argument isn't provided, the question will be an originator question. - return { - "event": make_minimal_dictionary(input_values=input_values, input_manifest=input_manifest, kind="question"), - "attributes": make_attributes( + :param dict input_values: + :param octue.resources.manifest.Manifest input_manifest: + :param str parent_sruid: + :param str child_sruid: + :param str question_uuid: + :param dict attributes: + :return dict: + """ + if not attributes: + question_uuid = question_uuid or str(uuid.uuid4()) + + attributes = make_attributes( question_uuid=question_uuid, parent_question_uuid=question_uuid, originator_question_uuid=question_uuid, @@ -21,7 +37,11 @@ def make_originator_question_event(input_values, input_manifest, parent_sruid, c forward_logs=True, save_diagnostics="SAVE_DIAGNOSTICS_ON", sender_type="PARENT", - ), + ) + + return { + "event": make_minimal_dictionary(input_values=input_values, input_manifest=input_manifest, kind="question"), + "attributes": attributes, } From b50b2071b85c03b8c008c0e55645f17a2d2d30f9 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Mon, 27 Jan 2025 17:27:02 +0000 Subject: [PATCH 037/125] TST: Update mock paths --- .../google/cloud_run/test_flask_app.py | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/tests/cloud/deployment/google/cloud_run/test_flask_app.py b/tests/cloud/deployment/google/cloud_run/test_flask_app.py index bef35f05a..573264e4d 100644 --- a/tests/cloud/deployment/google/cloud_run/test_flask_app.py +++ b/tests/cloud/deployment/google/cloud_run/test_flask_app.py @@ -1,9 +1,9 @@ import copy import logging import os -import uuid from unittest import TestCase from unittest.mock import patch +import uuid from google.api_core.exceptions import NotFound @@ -12,7 +12,6 @@ from octue.utils.patches import MultiPatcher from tests import TESTS_DIR - flask_app.app.testing = True @@ -54,7 +53,9 @@ def test_warning_logged_if_no_event_store_provided(self): mock_configuration.event_store_table_id = None with flask_app.app.test_client() as client: - with patch("octue.cloud.deployment.google.cloud_run.flask_app.answer_question") as mock_answer_question: + with patch( + "octue.cloud.deployment.google.cloud_run.flask_app.answer_pub_sub_question" + ) as mock_answer_question: with patch("octue.configuration.ServiceConfiguration.from_file", return_value=mock_configuration): with self.assertLogs(level=logging.WARNING) as logging_context: response = client.post( @@ -98,7 +99,9 @@ def test_warning_logged_if_event_store_not_found(self): ) with flask_app.app.test_client() as client: - with patch("octue.cloud.deployment.google.cloud_run.flask_app.answer_question") as mock_answer_question: + with patch( + "octue.cloud.deployment.google.cloud_run.flask_app.answer_pub_sub_question" + ) as mock_answer_question: with multi_patcher: with self.assertLogs(level=logging.WARNING) as logging_context: response = client.post( @@ -138,7 +141,9 @@ def test_new_question(self): ) with flask_app.app.test_client() as client: - with patch("octue.cloud.deployment.google.cloud_run.flask_app.answer_question") as mock_answer_question: + with patch( + "octue.cloud.deployment.google.cloud_run.flask_app.answer_pub_sub_question" + ) as mock_answer_question: with multi_patcher: with self.assertLogs() as logging_context: response = client.post( @@ -176,7 +181,9 @@ def test_redelivered_questions_are_acknowledged_and_dropped(self): ) with flask_app.app.test_client() as client: - with patch("octue.cloud.deployment.google.cloud_run.flask_app.answer_question") as mock_answer_question: + with patch( + "octue.cloud.deployment.google.cloud_run.flask_app.answer_pub_sub_question" + ) as mock_answer_question: with self.assertLogs(level=logging.WARNING) as logging_context: with multi_patcher: response = client.post( @@ -218,7 +225,9 @@ def test_retried_questions_are_allowed(self): ) with flask_app.app.test_client() as client: - with patch("octue.cloud.deployment.google.cloud_run.flask_app.answer_question") as mock_answer_question: + with patch( + "octue.cloud.deployment.google.cloud_run.flask_app.answer_pub_sub_question" + ) as mock_answer_question: with multi_patcher: response = client.post( "/", From fa86c28b10c0d4f0cf31a9af7f6e7e4b93c473e4 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Mon, 27 Jan 2025 17:38:43 +0000 Subject: [PATCH 038/125] FIX: Avoid assuming question is a dict --- octue/cloud/pub_sub/service.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index d42f03353..8cbd8d58c 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -8,12 +8,11 @@ import time import uuid -import google.api_core.exceptions -import jsonschema from google.api_core import retry +import google.api_core.exceptions from google.cloud import pubsub_v1 +import jsonschema -import octue.exceptions from octue.cloud import LOCAL_SDK_VERSION from octue.cloud.events import OCTUE_SERVICES_PREFIX from octue.cloud.events.utils import make_attributes @@ -30,12 +29,12 @@ validate_sruid, ) from octue.compatibility import warn_if_incompatible +import octue.exceptions from octue.utils.dictionaries import make_minimal_dictionary from octue.utils.encoders import OctueJSONEncoder from octue.utils.exceptions import convert_exception_to_primitives from octue.utils.threads import RepeatingTimer - logger = logging.getLogger(__name__) @@ -766,7 +765,7 @@ def _parse_question(self, question): question.ack() # Support already-extracted questions (e.g. from the `octue question ask-local` CLI command). - if "event" in question: + if isinstance(question, dict) and "event" in question: event = copy.deepcopy(question["event"]) attributes = question["attributes"] From cb867c4bd3d585eebf302451520af82dcc7f822e Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Mon, 27 Jan 2025 17:53:31 +0000 Subject: [PATCH 039/125] ENH: Allow setting of Octue services topic name via envvar BREAKING CHANGE: Update the services topic in production to be named `main.octue.services` or set the environment variable `OCTUE_SERVICES_TOPIC_NAME=octue.services` --- octue/cloud/events/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/octue/cloud/events/__init__.py b/octue/cloud/events/__init__.py index 66898c235..3793c3bd4 100644 --- a/octue/cloud/events/__init__.py +++ b/octue/cloud/events/__init__.py @@ -1 +1,3 @@ -OCTUE_SERVICES_PREFIX = "octue.services" +import os + +OCTUE_SERVICES_PREFIX = os.environ.get("OCTUE_SERVICE_TOPIC_NAME", "main.octue.services") From 23fa1018896b678ad6d7e880d297cc5993d4486a Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Mon, 27 Jan 2025 17:59:21 +0000 Subject: [PATCH 040/125] TST: Fix test for different python versions --- tests/mixins/test_identifiable.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/mixins/test_identifiable.py b/tests/mixins/test_identifiable.py index 338e72e99..21bcbbc48 100644 --- a/tests/mixins/test_identifiable.py +++ b/tests/mixins/test_identifiable.py @@ -76,4 +76,7 @@ class Inherit(Identifiable): with self.assertRaises(AttributeError) as e: resource.id = "07d38e81-6b00-4079-901b-e250ea3c7773" - self.assertIn("object has no setter", e.exception.args[0]) + # Make test work across python versions. + self.assertTrue( + ("object has no setter" in e.exception.args[0]) or ("object has no setter" in e.exception.args[0]) + ) From 6d157aaed7567698d6dc6da5141d18c884ae4fc9 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Mon, 27 Jan 2025 18:15:26 +0000 Subject: [PATCH 041/125] FIX: Fix typo in `OCTUE_SERVICES_TOPIC_NAME` envvar --- octue/cloud/events/__init__.py | 2 +- octue/cloud/pub_sub/__init__.py | 5 ++--- octue/cloud/pub_sub/service.py | 4 ++-- tests/base.py | 4 ++-- tests/test_cli.py | 4 ++-- 5 files changed, 9 insertions(+), 10 deletions(-) diff --git a/octue/cloud/events/__init__.py b/octue/cloud/events/__init__.py index 3793c3bd4..2762bb477 100644 --- a/octue/cloud/events/__init__.py +++ b/octue/cloud/events/__init__.py @@ -1,3 +1,3 @@ import os -OCTUE_SERVICES_PREFIX = os.environ.get("OCTUE_SERVICE_TOPIC_NAME", "main.octue.services") +OCTUE_SERVICES_TOPIC_NAME = os.environ.get("OCTUE_SERVICES_TOPIC_NAME", "main.octue.services") diff --git a/octue/cloud/pub_sub/__init__.py b/octue/cloud/pub_sub/__init__.py index 30c21aed7..c35ddc7eb 100644 --- a/octue/cloud/pub_sub/__init__.py +++ b/octue/cloud/pub_sub/__init__.py @@ -1,10 +1,9 @@ -from octue.cloud.events import OCTUE_SERVICES_PREFIX +from octue.cloud.events import OCTUE_SERVICES_TOPIC_NAME from octue.cloud.service_id import convert_service_id_to_pub_sub_form from .subscription import Subscription from .topic import Topic - __all__ = ["Subscription", "Topic"] @@ -34,7 +33,7 @@ def create_push_subscription( subscription = Subscription( name=convert_service_id_to_pub_sub_form(sruid), - topic=Topic(name=OCTUE_SERVICES_PREFIX, project_name=project_name), + topic=Topic(name=OCTUE_SERVICES_TOPIC_NAME, project_name=project_name), filter=subscription_filter, expiration_time=expiration_time, push_endpoint=push_endpoint, diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index 8cbd8d58c..e8226b59b 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -14,7 +14,7 @@ import jsonschema from octue.cloud import LOCAL_SDK_VERSION -from octue.cloud.events import OCTUE_SERVICES_PREFIX +from octue.cloud.events import OCTUE_SERVICES_TOPIC_NAME from octue.cloud.events.utils import make_attributes from octue.cloud.events.validation import raise_if_event_is_invalid from octue.cloud.pub_sub import Subscription, Topic @@ -116,7 +116,7 @@ def services_topic(self): :raise octue.exceptions.ServiceNotFound: if the topic doesn't exist in the project :return octue.cloud.pub_sub.topic.Topic: the Octue services topic for the project """ - topic = Topic(name=OCTUE_SERVICES_PREFIX, project_name=self.backend.project_name) + topic = Topic(name=OCTUE_SERVICES_TOPIC_NAME, project_name=self.backend.project_name) if not topic.exists(): raise octue.exceptions.ServiceNotFound( diff --git a/tests/base.py b/tests/base.py index ecf7e42dc..10c72e793 100644 --- a/tests/base.py +++ b/tests/base.py @@ -8,7 +8,7 @@ from octue.cloud.emulators._pub_sub import MockTopic from octue.cloud.emulators.cloud_storage import GoogleCloudStorageEmulatorTestResultModifier from octue.cloud.emulators.service import ServicePatcher -from octue.cloud.events import OCTUE_SERVICES_PREFIX +from octue.cloud.events import OCTUE_SERVICES_TOPIC_NAME from octue.cloud.storage import GoogleCloudStorageClient from octue.resources import Datafile, Dataset, Manifest from tests import TEST_BUCKET_NAME, TEST_PROJECT_NAME @@ -23,7 +23,7 @@ def startTestRun(self): super().startTestRun() with ServicePatcher(): - self.services_topic = MockTopic(name=OCTUE_SERVICES_PREFIX, project_name=TEST_PROJECT_NAME) + self.services_topic = MockTopic(name=OCTUE_SERVICES_TOPIC_NAME, project_name=TEST_PROJECT_NAME) self.services_topic.create(allow_existing=True) diff --git a/tests/test_cli.py b/tests/test_cli.py index 6cdde57aa..8b856bfa9 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -11,7 +11,7 @@ from octue.cloud import storage from octue.cloud.emulators._pub_sub import MockService, MockSubscription, MockTopic from octue.cloud.emulators.service import ServicePatcher -from octue.cloud.events import OCTUE_SERVICES_PREFIX +from octue.cloud.events import OCTUE_SERVICES_TOPIC_NAME from octue.cloud.pub_sub import Topic from octue.configuration import AppConfiguration, ServiceConfiguration from octue.resources import Dataset @@ -465,7 +465,7 @@ def test_create_push_subscription_when_already_exists(self): with patch("octue.cloud.pub_sub.Subscription", new=MockSubscription): subscription = MockSubscription( name=sruid, - topic=Topic(name=OCTUE_SERVICES_PREFIX, project_name="my-project"), + topic=Topic(name=OCTUE_SERVICES_TOPIC_NAME, project_name="my-project"), push_endpoint=push_endpoint, ) From 4cc32b24fa8704ae1e8ef62f5211250ab05fe418 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Mon, 27 Jan 2025 18:19:39 +0000 Subject: [PATCH 042/125] FIX: Remove non-existent service revision check for now skipci --- octue/cloud/pub_sub/service.py | 10 ---------- tests/cloud/pub_sub/test_service.py | 10 +--------- 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index e8226b59b..12cf14de3 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -347,16 +347,6 @@ def ask( service_registries=self.service_registries, ) - # If not using a service registry, check that the service revision exists by checking for its subscription. - elif service_revision_tag: - service_revision_subscription = Subscription( - name=convert_service_id_to_pub_sub_form(service_id), - topic=self.services_topic, - ) - - if not service_revision_subscription.exists(): - raise octue.exceptions.ServiceNotFound(f"Service revision {service_id!r} not found.") - else: raise octue.exceptions.InvalidServiceID( f"A service revision tag for {service_id!r} must be provided if service registries aren't being used." diff --git a/tests/cloud/pub_sub/test_service.py b/tests/cloud/pub_sub/test_service.py index 6e5e6a1b1..5e57dc3ec 100644 --- a/tests/cloud/pub_sub/test_service.py +++ b/tests/cloud/pub_sub/test_service.py @@ -11,7 +11,6 @@ import google.api_core.exceptions import requests -import twined.exceptions from octue import Runner, exceptions from octue.cloud.emulators._pub_sub import ( DifferentMockAnalysis, @@ -29,7 +28,7 @@ from octue.resources.service_backends import GCPPubSubBackend from tests import MOCK_SERVICE_REVISION_TAG, TEST_BUCKET_NAME, TEST_PROJECT_NAME from tests.base import BaseTestCase - +import twined.exceptions logger = logging.getLogger(__name__) @@ -122,13 +121,6 @@ def test_missing_services_topic_results_in_error(self): with self.assertRaises(exceptions.ServiceNotFound): service.services_topic - def test_error_raised_if_service_revision_not_found_when_not_using_service_registry(self): - """Test that an error is raised if a service revision isn't found when not using a service registry.""" - service = MockService(backend=BACKEND) - - with self.assertRaises(exceptions.ServiceNotFound): - service.ask("non/existent:service") - def test_ask_unregistered_service_revision_when_service_registries_specified_results_in_error(self): """Test that an error is raised if attempting to ask an unregistered service a question when service registries are being used. From dd47a92d9bf4ac83c4ed91be136355b385dc9931 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Mon, 27 Jan 2025 18:24:48 +0000 Subject: [PATCH 043/125] FIX: Add missing `elif` skipci --- octue/cloud/pub_sub/service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index 12cf14de3..c67c63c86 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -347,7 +347,7 @@ def ask( service_registries=self.service_registries, ) - else: + elif not service_revision_tag: raise octue.exceptions.InvalidServiceID( f"A service revision tag for {service_id!r} must be provided if service registries aren't being used." ) From 680ec6369f9b66a73d243df749ed63c93b822cc0 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Tue, 28 Jan 2025 16:09:41 +0000 Subject: [PATCH 044/125] FIX: Convert attributes to correct types in `make_attributes` skipci --- octue/cloud/events/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/octue/cloud/events/utils.py b/octue/cloud/events/utils.py index 314a9df7d..a0902dc53 100644 --- a/octue/cloud/events/utils.py +++ b/octue/cloud/events/utils.py @@ -70,7 +70,7 @@ def make_attributes( "sender_type": sender_type, "sender_sdk_version": LOCAL_SDK_VERSION, "recipient": recipient, - "retry_count": retry_count, + "retry_count": int(retry_count), } if sender_type == "PARENT": @@ -80,7 +80,7 @@ def make_attributes( "'PARENT'." ) - attributes["forward_logs"] = forward_logs + attributes["forward_logs"] = bool(forward_logs) attributes["save_diagnostics"] = save_diagnostics return attributes From f0bbd98e37faa37afe42496bb36f252a12f68cb2 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Tue, 28 Jan 2025 16:27:21 +0000 Subject: [PATCH 045/125] FIX: Extract attributes correctly from passed-in format skipci --- octue/cloud/pub_sub/events.py | 23 ++++++++++++++--------- octue/cloud/pub_sub/service.py | 9 +-------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/octue/cloud/pub_sub/events.py b/octue/cloud/pub_sub/events.py index cbb021073..3a9da708d 100644 --- a/octue/cloud/pub_sub/events.py +++ b/octue/cloud/pub_sub/events.py @@ -1,9 +1,9 @@ import base64 +from datetime import datetime, timedelta +from functools import cached_property import json import logging import time -from datetime import datetime, timedelta -from functools import cached_property from google.api_core import retry from google.cloud.pubsub_v1 import SubscriberClient @@ -14,7 +14,6 @@ from octue.utils.objects import get_nested_attribute from octue.utils.threads import RepeatingTimer - logger = logging.getLogger(__name__) MAX_SIMULTANEOUS_MESSAGES_PULL = 50 @@ -51,12 +50,18 @@ def extract_event_and_attributes_from_pub_sub_message(message): else: attributes["forward_logs"] = None - try: - # Parse event directly from Pub/Sub or Dataflow. - event = json.loads(message.data.decode(), cls=OctueJSONDecoder) - except Exception: - # Parse event from Google Cloud Run. - event = json.loads(base64.b64decode(message["data"]).decode("utf-8").strip(), cls=OctueJSONDecoder) + # Support already-extracted questions (e.g. from the `octue question ask local` CLI command). + if isinstance(message, dict) and "event" in message: + event = message["event"] + + # Extract question from Cloud Run or Pub/Sub format. + else: + try: + # Parse event directly from Pub/Sub or Dataflow. + event = json.loads(message.data.decode(), cls=OctueJSONDecoder) + except Exception: + # Parse event from Google Cloud Run. + event = json.loads(base64.b64decode(message["data"]).decode("utf-8").strip(), cls=OctueJSONDecoder) return event, attributes diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index c67c63c86..f300705ca 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -754,14 +754,7 @@ def _parse_question(self, question): if hasattr(question, "ack"): question.ack() - # Support already-extracted questions (e.g. from the `octue question ask-local` CLI command). - if isinstance(question, dict) and "event" in question: - event = copy.deepcopy(question["event"]) - attributes = question["attributes"] - - # Extract question from Cloud Run or Pub/Sub format. - else: - event, attributes = extract_event_and_attributes_from_pub_sub_message(question) + event, attributes = extract_event_and_attributes_from_pub_sub_message(question) raise_if_event_is_invalid( event=copy.deepcopy(event), From a94fb352e8994172edbea9a4eec5388e2192d563 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Tue, 28 Jan 2025 16:45:50 +0000 Subject: [PATCH 046/125] ENH: Log when question event extraction is complete skipci --- octue/cloud/pub_sub/service.py | 1 + 1 file changed, 1 insertion(+) diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index f300705ca..030b27e06 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -755,6 +755,7 @@ def _parse_question(self, question): question.ack() event, attributes = extract_event_and_attributes_from_pub_sub_message(question) + logger.info("Extracted question event and attributes.") raise_if_event_is_invalid( event=copy.deepcopy(event), From 0bd2f6844a8a5b87198301688030e401eb600721 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Mon, 3 Feb 2025 12:11:47 +0000 Subject: [PATCH 047/125] FIX: Explicitly pass credentials to publisher/subscriber clients skipci --- octue/cloud/pub_sub/credentials.py | 10 ++++++++++ octue/cloud/pub_sub/events.py | 3 ++- octue/cloud/pub_sub/service.py | 3 ++- octue/cloud/pub_sub/subscription.py | 9 ++++++--- octue/cloud/pub_sub/topic.py | 7 ++++--- 5 files changed, 24 insertions(+), 8 deletions(-) create mode 100644 octue/cloud/pub_sub/credentials.py diff --git a/octue/cloud/pub_sub/credentials.py b/octue/cloud/pub_sub/credentials.py new file mode 100644 index 000000000..fecea6e31 --- /dev/null +++ b/octue/cloud/pub_sub/credentials.py @@ -0,0 +1,10 @@ +import google.auth +import google.auth.transport.requests + + +def get_gcp_credentials(): + """Get the default credentials for Google Cloud Platform.""" + credentials, _ = google.auth.default() + # auth_request = google.auth.transport.requests.Request() + # credentials.refresh(auth_request) + return credentials diff --git a/octue/cloud/pub_sub/events.py b/octue/cloud/pub_sub/events.py index 3a9da708d..bc3c87d02 100644 --- a/octue/cloud/pub_sub/events.py +++ b/octue/cloud/pub_sub/events.py @@ -10,6 +10,7 @@ from octue.cloud.events.handler import AbstractEventHandler from octue.cloud.events.validation import SERVICE_COMMUNICATION_SCHEMA +from octue.cloud.pub_sub.credentials import get_gcp_credentials from octue.utils.decoders import OctueJSONDecoder from octue.utils.objects import get_nested_attribute from octue.utils.threads import RepeatingTimer @@ -118,7 +119,7 @@ def subscriber(self): :return google.cloud.pubsub_v1.SubscriberClient: """ - return SubscriberClient() + return SubscriberClient(credentials=get_gcp_credentials()) @property def total_run_time(self): diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index 030b27e06..61406ee89 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -18,6 +18,7 @@ from octue.cloud.events.utils import make_attributes from octue.cloud.events.validation import raise_if_event_is_invalid from octue.cloud.pub_sub import Subscription, Topic +from octue.cloud.pub_sub.credentials import get_gcp_credentials from octue.cloud.pub_sub.events import GoogleCloudPubSubEventHandler, extract_event_and_attributes_from_pub_sub_message from octue.cloud.pub_sub.logging import GoogleCloudPubSubHandler from octue.cloud.service_id import ( @@ -161,7 +162,7 @@ def serve(self, timeout=None, delete_topic_and_subscription_on_exit=False, allow except google.api_core.exceptions.AlreadyExists: raise octue.exceptions.ServiceAlreadyExists(f"A service with the ID {self.id!r} already exists.") - subscriber = pubsub_v1.SubscriberClient() + subscriber = pubsub_v1.SubscriberClient(credentials=get_gcp_credentials()) try: future = subscriber.subscribe(subscription=subscription.path, callback=self.answer) diff --git a/octue/cloud/pub_sub/subscription.py b/octue/cloud/pub_sub/subscription.py index ae8e1c87d..083b10d6a 100644 --- a/octue/cloud/pub_sub/subscription.py +++ b/octue/cloud/pub_sub/subscription.py @@ -1,5 +1,5 @@ -import logging from functools import cached_property +import logging import google.api_core.exceptions from google.cloud.pubsub_v1 import SubscriberClient @@ -9,10 +9,13 @@ ExpirationPolicy, PushConfig, RetryPolicy, - Subscription as _Subscription, UpdateSubscriptionRequest, ) +from google.pubsub_v1.types.pubsub import ( + Subscription as _Subscription, +) +from octue.cloud.pub_sub.credentials import get_gcp_credentials logger = logging.getLogger(__name__) @@ -83,7 +86,7 @@ def subscriber(self): :return google.cloud.pubsub_v1.SubscriberClient: """ - return SubscriberClient() + return SubscriberClient(credentials=get_gcp_credentials()) @property def creation_triggered_locally(self): diff --git a/octue/cloud/pub_sub/topic.py b/octue/cloud/pub_sub/topic.py index 5c7941920..539b2cda8 100644 --- a/octue/cloud/pub_sub/topic.py +++ b/octue/cloud/pub_sub/topic.py @@ -1,12 +1,13 @@ -import logging -import time from datetime import datetime from functools import cached_property +import logging +import time import google.api_core.exceptions from google.cloud.pubsub_v1 import PublisherClient from google.pubsub_v1.types.pubsub import Topic as Topic_ +from octue.cloud.pub_sub.credentials import get_gcp_credentials logger = logging.getLogger(__name__) @@ -35,7 +36,7 @@ def publisher(self): :return google.cloud.pubsub_v1.PublisherClient: """ - return PublisherClient() + return PublisherClient(credentials=get_gcp_credentials()) @property def creation_triggered_locally(self): From 89c53622d91d5f031bb39b1426e3fda351d64828 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Mon, 3 Feb 2025 12:24:44 +0000 Subject: [PATCH 048/125] FIX: Refresh credentials before returning skipci --- octue/cloud/pub_sub/credentials.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/octue/cloud/pub_sub/credentials.py b/octue/cloud/pub_sub/credentials.py index fecea6e31..78afcbe50 100644 --- a/octue/cloud/pub_sub/credentials.py +++ b/octue/cloud/pub_sub/credentials.py @@ -5,6 +5,6 @@ def get_gcp_credentials(): """Get the default credentials for Google Cloud Platform.""" credentials, _ = google.auth.default() - # auth_request = google.auth.transport.requests.Request() - # credentials.refresh(auth_request) + auth_request = google.auth.transport.requests.Request() + credentials.refresh(auth_request) return credentials From 9081704f8fdd47f52b34ec95aa2c0fd0a3818fe5 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Mon, 3 Feb 2025 12:33:11 +0000 Subject: [PATCH 049/125] REV: Revert "FIX: Refresh credentials before returning" This reverts commit 89c53622d91d5f031bb39b1426e3fda351d64828. skipci --- octue/cloud/pub_sub/credentials.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/octue/cloud/pub_sub/credentials.py b/octue/cloud/pub_sub/credentials.py index 78afcbe50..fecea6e31 100644 --- a/octue/cloud/pub_sub/credentials.py +++ b/octue/cloud/pub_sub/credentials.py @@ -5,6 +5,6 @@ def get_gcp_credentials(): """Get the default credentials for Google Cloud Platform.""" credentials, _ = google.auth.default() - auth_request = google.auth.transport.requests.Request() - credentials.refresh(auth_request) + # auth_request = google.auth.transport.requests.Request() + # credentials.refresh(auth_request) return credentials From 584d39832acccd67750aad4053dac6b060d80788 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Tue, 4 Feb 2025 11:41:20 +0000 Subject: [PATCH 050/125] REV: Revert "FIX: Explicitly pass credentials to publisher/subscriber clients" This reverts commit 0bd2f6844a8a5b87198301688030e401eb600721. --- octue/cloud/pub_sub/credentials.py | 10 ---------- octue/cloud/pub_sub/events.py | 3 +-- octue/cloud/pub_sub/service.py | 3 +-- octue/cloud/pub_sub/subscription.py | 9 +++------ octue/cloud/pub_sub/topic.py | 7 +++---- 5 files changed, 8 insertions(+), 24 deletions(-) delete mode 100644 octue/cloud/pub_sub/credentials.py diff --git a/octue/cloud/pub_sub/credentials.py b/octue/cloud/pub_sub/credentials.py deleted file mode 100644 index fecea6e31..000000000 --- a/octue/cloud/pub_sub/credentials.py +++ /dev/null @@ -1,10 +0,0 @@ -import google.auth -import google.auth.transport.requests - - -def get_gcp_credentials(): - """Get the default credentials for Google Cloud Platform.""" - credentials, _ = google.auth.default() - # auth_request = google.auth.transport.requests.Request() - # credentials.refresh(auth_request) - return credentials diff --git a/octue/cloud/pub_sub/events.py b/octue/cloud/pub_sub/events.py index bc3c87d02..3a9da708d 100644 --- a/octue/cloud/pub_sub/events.py +++ b/octue/cloud/pub_sub/events.py @@ -10,7 +10,6 @@ from octue.cloud.events.handler import AbstractEventHandler from octue.cloud.events.validation import SERVICE_COMMUNICATION_SCHEMA -from octue.cloud.pub_sub.credentials import get_gcp_credentials from octue.utils.decoders import OctueJSONDecoder from octue.utils.objects import get_nested_attribute from octue.utils.threads import RepeatingTimer @@ -119,7 +118,7 @@ def subscriber(self): :return google.cloud.pubsub_v1.SubscriberClient: """ - return SubscriberClient(credentials=get_gcp_credentials()) + return SubscriberClient() @property def total_run_time(self): diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index 61406ee89..030b27e06 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -18,7 +18,6 @@ from octue.cloud.events.utils import make_attributes from octue.cloud.events.validation import raise_if_event_is_invalid from octue.cloud.pub_sub import Subscription, Topic -from octue.cloud.pub_sub.credentials import get_gcp_credentials from octue.cloud.pub_sub.events import GoogleCloudPubSubEventHandler, extract_event_and_attributes_from_pub_sub_message from octue.cloud.pub_sub.logging import GoogleCloudPubSubHandler from octue.cloud.service_id import ( @@ -162,7 +161,7 @@ def serve(self, timeout=None, delete_topic_and_subscription_on_exit=False, allow except google.api_core.exceptions.AlreadyExists: raise octue.exceptions.ServiceAlreadyExists(f"A service with the ID {self.id!r} already exists.") - subscriber = pubsub_v1.SubscriberClient(credentials=get_gcp_credentials()) + subscriber = pubsub_v1.SubscriberClient() try: future = subscriber.subscribe(subscription=subscription.path, callback=self.answer) diff --git a/octue/cloud/pub_sub/subscription.py b/octue/cloud/pub_sub/subscription.py index 083b10d6a..ae8e1c87d 100644 --- a/octue/cloud/pub_sub/subscription.py +++ b/octue/cloud/pub_sub/subscription.py @@ -1,5 +1,5 @@ -from functools import cached_property import logging +from functools import cached_property import google.api_core.exceptions from google.cloud.pubsub_v1 import SubscriberClient @@ -9,13 +9,10 @@ ExpirationPolicy, PushConfig, RetryPolicy, - UpdateSubscriptionRequest, -) -from google.pubsub_v1.types.pubsub import ( Subscription as _Subscription, + UpdateSubscriptionRequest, ) -from octue.cloud.pub_sub.credentials import get_gcp_credentials logger = logging.getLogger(__name__) @@ -86,7 +83,7 @@ def subscriber(self): :return google.cloud.pubsub_v1.SubscriberClient: """ - return SubscriberClient(credentials=get_gcp_credentials()) + return SubscriberClient() @property def creation_triggered_locally(self): diff --git a/octue/cloud/pub_sub/topic.py b/octue/cloud/pub_sub/topic.py index 539b2cda8..5c7941920 100644 --- a/octue/cloud/pub_sub/topic.py +++ b/octue/cloud/pub_sub/topic.py @@ -1,13 +1,12 @@ -from datetime import datetime -from functools import cached_property import logging import time +from datetime import datetime +from functools import cached_property import google.api_core.exceptions from google.cloud.pubsub_v1 import PublisherClient from google.pubsub_v1.types.pubsub import Topic as Topic_ -from octue.cloud.pub_sub.credentials import get_gcp_credentials logger = logging.getLogger(__name__) @@ -36,7 +35,7 @@ def publisher(self): :return google.cloud.pubsub_v1.PublisherClient: """ - return PublisherClient(credentials=get_gcp_credentials()) + return PublisherClient() @property def creation_triggered_locally(self): From 521666beb7ee84cce029bb1b5e0207e5983ecff5 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Tue, 4 Feb 2025 12:50:54 +0000 Subject: [PATCH 051/125] ENH: Log question acknowledgement to pub/sub skipci --- octue/cloud/pub_sub/service.py | 1 + 1 file changed, 1 insertion(+) diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index 030b27e06..062279c39 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -753,6 +753,7 @@ def _parse_question(self, question): # Acknowledge the question if it's directly from Pub/Sub. if hasattr(question, "ack"): question.ack() + logger.info("Question acknowledged on Pub/Sub.") event, attributes = extract_event_and_attributes_from_pub_sub_message(question) logger.info("Extracted question event and attributes.") From 2ea1cc3def4fbb7e24721d35f263c0bb705d85fb Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Tue, 4 Feb 2025 16:29:52 +0000 Subject: [PATCH 052/125] ENH: Allow use of service registries with `octue question ask remote` skipci --- octue/cli.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/octue/cli.py b/octue/cli.py index 172ffbd82..adcbd7fe2 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -115,13 +115,26 @@ def ask(): help="If provided, ask the question and detach (the result and other events can be retrieved from the event store " "later).", ) -def remote(sruid, input_values, input_manifest, project_name, asynchronous): +@click.option( + "-c", + "--service-config", + type=click.Path(dir_okay=False), + default=None, + help="An optional path to an `octue.yaml` file specifying service registries.", +) +def remote(sruid, input_values, input_manifest, project_name, asynchronous, service_config): """Ask a question to a remote Octue Twined service. SRUID should be a valid service revision unique identifier for an existing Octue Twined service e.g. octue question ask octue/example-service:1.0.3 """ + if service_config: + service_configuration = ServiceConfiguration.from_file(service_config) + service_registries = service_configuration.service_registries + else: + service_registries = None + if input_values: input_values = json.loads(input_values, cls=OctueJSONDecoder) @@ -131,7 +144,11 @@ def remote(sruid, input_values, input_manifest, project_name, asynchronous): if not project_name: _, project_name = auth.default() - child = Child(id=sruid, backend={"name": "GCPPubSubBackend", "project_name": project_name}) + child = Child( + id=sruid, + backend={"name": "GCPPubSubBackend", "project_name": project_name}, + service_registries=service_registries, + ) answer, question_uuid = child.ask( input_values=input_values, From f6b9c94fd035f24059f01e34a81ce0d3f0509111 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Tue, 4 Feb 2025 16:32:23 +0000 Subject: [PATCH 053/125] FIX: Make service config optional for `octue question ask remote` skipci --- octue/cli.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/octue/cli.py b/octue/cli.py index adcbd7fe2..02a1e5a25 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -120,7 +120,9 @@ def ask(): "--service-config", type=click.Path(dir_okay=False), default=None, - help="An optional path to an `octue.yaml` file specifying service registries.", + help="An optional path to an `octue.yaml` file defining service registries to use. If not provided, the " + "`OCTUE_SERVICE_CONFIGURATION_PATH` environment variable is used if present, otherwise the local path `octue.yaml` " + "is used.", ) def remote(sruid, input_values, input_manifest, project_name, asynchronous, service_config): """Ask a question to a remote Octue Twined service. @@ -129,10 +131,10 @@ def remote(sruid, input_values, input_manifest, project_name, asynchronous, serv e.g. octue question ask octue/example-service:1.0.3 """ - if service_config: + try: service_configuration = ServiceConfiguration.from_file(service_config) service_registries = service_configuration.service_registries - else: + except FileNotFoundError: service_registries = None if input_values: From 28c72e8c9854d5d5b99625c0c8d0856a68ad964e Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Wed, 5 Feb 2025 13:56:52 +0000 Subject: [PATCH 054/125] FEA: Allow requesting compute resource amounts per question --- octue/cloud/emulators/_pub_sub.py | 15 +++++++++++++-- octue/cloud/events/utils.py | 6 ++++++ octue/cloud/pub_sub/events.py | 7 ++++++- octue/cloud/pub_sub/service.py | 15 +++++++++++++++ octue/resources/child.py | 9 +++++++++ 5 files changed, 49 insertions(+), 3 deletions(-) diff --git a/octue/cloud/emulators/_pub_sub.py b/octue/cloud/emulators/_pub_sub.py index 219db40fd..c9c01089c 100644 --- a/octue/cloud/emulators/_pub_sub.py +++ b/octue/cloud/emulators/_pub_sub.py @@ -1,7 +1,7 @@ +from collections import defaultdict import importlib.metadata import json import logging -from collections import defaultdict import google.api_core @@ -11,7 +11,6 @@ from octue.utils.dictionaries import make_minimal_dictionary from octue.utils.encoders import OctueJSONEncoder - logger = logging.getLogger(__name__) TOPICS = set() @@ -338,6 +337,9 @@ def ask( push_endpoint=None, asynchronous=False, retry_count=0, + cpus=None, + memory=None, + ephemeral_storage=None, timeout=86400, parent_sdk_version=importlib.metadata.version("octue"), ): @@ -358,6 +360,9 @@ def ask( :param str|None push_endpoint: :param bool asynchronous: :param int retry_count: the retry count of the question (this is zero if it's the first attempt at the question) + :param int|None cpus: + :param str|None memory: + :param str|None ephemeral_storage: :param float|None timeout: :param str parent_sdk_version: :return MockFuture, str: @@ -377,6 +382,9 @@ def ask( push_endpoint=push_endpoint, asynchronous=asynchronous, retry_count=retry_count, + cpus=cpus, + memory=memory, + ephemeral_storage=ephemeral_storage, timeout=timeout, ) @@ -416,6 +424,9 @@ def ask( "sender_sdk_version": parent_sdk_version, "recipient": service_id, "retry_count": retry_count, + "cpus": cpus, + "memory": memory, + "ephemeral_storage": ephemeral_storage, }, ) ) diff --git a/octue/cloud/events/utils.py b/octue/cloud/events/utils.py index a0902dc53..d6da8dffa 100644 --- a/octue/cloud/events/utils.py +++ b/octue/cloud/events/utils.py @@ -57,6 +57,9 @@ def make_attributes( retry_count=0, forward_logs=None, save_diagnostics=None, + cpus=None, + memory=None, + ephemeral_storage=None, ): attributes = { "uuid": str(uuid.uuid4()), @@ -71,6 +74,9 @@ def make_attributes( "sender_sdk_version": LOCAL_SDK_VERSION, "recipient": recipient, "retry_count": int(retry_count), + "cpus": cpus, + "memory": memory, + "ephemeral_storage": ephemeral_storage, } if sender_type == "PARENT": diff --git a/octue/cloud/pub_sub/events.py b/octue/cloud/pub_sub/events.py index 3a9da708d..b754dce3b 100644 --- a/octue/cloud/pub_sub/events.py +++ b/octue/cloud/pub_sub/events.py @@ -41,7 +41,7 @@ def extract_event_and_attributes_from_pub_sub_message(message): else: attributes["retry_count"] = None - # Required for question events. + # Question events have some extra optional attributes. if attributes.get("sender_type") == "PARENT": forward_logs = attributes.get("forward_logs") @@ -50,6 +50,11 @@ def extract_event_and_attributes_from_pub_sub_message(message): else: attributes["forward_logs"] = None + cpus = attributes.get("cpus") + + if cpus: + attributes["cpus"] = int(cpus) + # Support already-extracted questions (e.g. from the `octue question ask local` CLI command). if isinstance(message, dict) and "event" in message: event = message["event"] diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index 062279c39..5e7527d0b 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -310,6 +310,9 @@ def ask( push_endpoint=None, asynchronous=False, retry_count=0, + cpus=None, + memory=None, + ephemeral_storage=None, timeout=86400, ): """Ask a child a question (i.e. send it input values for it to analyse and produce output values for) and return @@ -330,6 +333,9 @@ def ask( :param str|None push_endpoint: if answers to the question should be pushed to an endpoint, provide its URL here (the returned subscription will be a push subscription); if not, leave this as `None` :param bool asynchronous: if `True` and not using a push endpoint, don't create an answer subscription :param int retry_count: the retry count of the question (this is zero if it's the first attempt at the question) + :param int|None cpus: + :param str|None memory: + :param str|None ephemeral_storage: :param float|None timeout: time in seconds to keep retrying sending the question :return (octue.cloud.pub_sub.subscription.Subscription|None, str): the answer subscription (if the question is synchronous or a push endpoint was used) and question UUID """ @@ -400,6 +406,9 @@ def ask( originator_question_uuid=originator_question_uuid, originator=originator, recipient=service_id, + cpus=cpus, + memory=memory, + ephemeral_storage=ephemeral_storage, retry_count=retry_count, ) @@ -576,6 +585,9 @@ def _send_question( originator, recipient, retry_count, + cpus, + memory, + ephemeral_storage, timeout=30, ): """Send a question to a child service. @@ -613,6 +625,9 @@ def _send_question( "forward_logs": forward_logs, "save_diagnostics": save_diagnostics, "sender_type": PARENT_SENDER_TYPE, + "cpus": cpus, + "memory": memory, + "ephemeral_storage": ephemeral_storage, }, timeout=timeout, ) diff --git a/octue/resources/child.py b/octue/resources/child.py index bcdf93b5a..00e17de0f 100644 --- a/octue/resources/child.py +++ b/octue/resources/child.py @@ -68,6 +68,9 @@ def ask( push_endpoint=None, asynchronous=False, retry_count=0, + cpus=None, + memory=None, + ephemeral_storage=None, raise_errors=True, max_retries=0, prevent_retries_when=None, @@ -96,6 +99,9 @@ def ask( :param str|None push_endpoint: if answers to the question should be pushed to an endpoint, provide its URL here (the returned subscription will be a push subscription); if not, leave this as `None` :param bool asynchronous: if `True`, don't wait for an answer or create an answer subscription (the result and other events can be retrieved from the event store later) :param int retry_count: the retry count of the question (this is zero if it's the first attempt at the question) + :param int|None cpus: + :param str|None memory: + :param str|None ephemeral_storage: :param bool raise_errors: if `True` and the question fails, raise the error; if False, return the error in place of the answer :param int max_retries: if `raise_errors=False` and the question fails, retry the question up to this number of times :param list(type)|None prevent_retries_when: if `raise_errors=False` and the question fails, prevent retrying the question if it fails with an exception type in this list @@ -122,6 +128,9 @@ def ask( "push_endpoint": push_endpoint, "asynchronous": asynchronous, "retry_count": retry_count, + "cpus": cpus, + "memory": memory, + "ephemeral_storage": ephemeral_storage, "timeout": timeout, } From 2ad4d009175c170a87809e8e55453f48b866e29d Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Wed, 5 Feb 2025 14:06:07 +0000 Subject: [PATCH 055/125] REF: Remove Cloud Run flask app --- .../deployment/google/cloud_run/flask_app.py | 111 -------- .../google/cloud_run/test_flask_app.py | 248 ------------------ 2 files changed, 359 deletions(-) delete mode 100644 octue/cloud/deployment/google/cloud_run/flask_app.py delete mode 100644 tests/cloud/deployment/google/cloud_run/test_flask_app.py diff --git a/octue/cloud/deployment/google/cloud_run/flask_app.py b/octue/cloud/deployment/google/cloud_run/flask_app.py deleted file mode 100644 index 97aba296f..000000000 --- a/octue/cloud/deployment/google/cloud_run/flask_app.py +++ /dev/null @@ -1,111 +0,0 @@ -import logging - -import google.api_core.exceptions -from flask import Flask, request - -from octue.cloud.deployment.google.answer_pub_sub_question import answer_pub_sub_question -from octue.cloud.pub_sub.bigquery import get_events -from octue.configuration import ServiceConfiguration - - -logger = logging.getLogger(__name__) -app = Flask(__name__) - - -QUESTION_ACKNOWLEDGMENT_RESPONSE = ("", 204) - - -@app.route("/", methods=["POST"]) -def index(): - """Receive questions from Google Cloud Run in the form of Google Pub/Sub messages. - - :return (str, int): - """ - envelope = request.get_json() - - if not envelope: - return _log_bad_request_and_return_400_response("No Pub/Sub message received.") - - if not isinstance(envelope, dict) or "message" not in envelope: - return _log_bad_request_and_return_400_response(f"Invalid Pub/Sub message format - received {envelope!r}.") - - question = envelope["message"] - - if "data" not in question or "attributes" not in question or "question_uuid" not in question["attributes"]: - return _log_bad_request_and_return_400_response(f"Invalid Pub/Sub message format - received {envelope!r}.") - - should_drop_question = _check_if_should_drop_question( - question_uuid=question["attributes"].get("question_uuid"), - retry_count=question["attributes"].get("retry_count"), - ) - - if should_drop_question: - return QUESTION_ACKNOWLEDGMENT_RESPONSE - - project_name = envelope["subscription"].split("/")[1] - answer_pub_sub_question(question=question, project_name=project_name) - return QUESTION_ACKNOWLEDGMENT_RESPONSE - - -def _log_bad_request_and_return_400_response(message): - """Log an error return a bad request (400) response. - - :param str message: - :return (str, int): - """ - logger.error(message) - return (f"Bad Request: {message}", 400) - - -def _check_if_should_drop_question(question_uuid, retry_count): - """Check if the question has been delivered before and should be dropped. If it's a new question or if the question - is an explicit retry (i.e. it's been received before but the retry count is unique), return `False`. - - To determine if a question is new, the event store is checked for a delivery acknowledgement for the question UUID. - If no event store is specified or the specified event store can't be found, return `False`. - - :param str question_uuid: the UUID of the question to check - :param str retry_count: the retry count of the question to check (an integer in string form) - :return bool: whether the question should be dropped - """ - service_configuration = ServiceConfiguration.from_file() - - if not service_configuration.event_store_table_id: - logger.warning( - "Cannot check if question has been redelivered as the 'event_store_table_id' key hasn't been set in the " - "service configuration (`octue.yaml` file)." - ) - return False - - try: - previous_question_attempts = get_events( - table_id=service_configuration.event_store_table_id, - question_uuid=question_uuid, - exclude_kinds=["question"], - ) - - except google.api_core.exceptions.NotFound: - logger.warning( - "Cannot check if question has been redelivered as no event store table was found with the ID %r; check " - "that the 'event_store_table_id' key in the service configuration (`octue.yaml` file) is correct.", - service_configuration.event_store_table_id, - ) - return False - - # If there are no events for this question UUID, assume this is the first attempt for the question. - if not previous_question_attempts: - logger.info("Question %r (retry count %s) is a new question.", question_uuid, retry_count) - return False - - # Acknowledge redelivered questions to stop further redundant redelivery and processing. - for event in previous_question_attempts: - if event["attributes"]["retry_count"] == retry_count: - logger.warning( - "Question %r (retry count %s) has already been received by the service. It will now be acknowledged " - "and dropped to prevent further redundant redelivery.", - question_uuid, - retry_count, - ) - return True - - return False diff --git a/tests/cloud/deployment/google/cloud_run/test_flask_app.py b/tests/cloud/deployment/google/cloud_run/test_flask_app.py deleted file mode 100644 index 573264e4d..000000000 --- a/tests/cloud/deployment/google/cloud_run/test_flask_app.py +++ /dev/null @@ -1,248 +0,0 @@ -import copy -import logging -import os -from unittest import TestCase -from unittest.mock import patch -import uuid - -from google.api_core.exceptions import NotFound - -from octue.cloud.deployment.google.cloud_run import flask_app -from octue.configuration import ServiceConfiguration -from octue.utils.patches import MultiPatcher -from tests import TESTS_DIR - -flask_app.app.testing = True - - -TWINE_FILE_PATH = os.path.join(TESTS_DIR, "data", "twines", "valid_schema_twine.json") - -MOCK_CONFIGURATION = ServiceConfiguration( - namespace="testing", - name="test-app", - app_source_path=os.path.join(TESTS_DIR, "test_app_modules", "app_module"), - twine_path=TWINE_FILE_PATH, - app_configuration_path="blah.json", - event_store_table_id="mock-event-store-table-id", -) - - -class TestInvalidPayloads(TestCase): - def test_post_to_index_with_no_payload_results_in_400_error(self): - """Test that a 400 (bad request) error code is returned if no payload is sent to the Flask endpoint.""" - with flask_app.app.test_client() as client: - response = client.post("/", json={"deliveryAttempt": 1}) - self.assertEqual(response.status_code, 400) - - def test_post_to_index_with_invalid_payload_results_in_400_error(self): - """Test that a 400 (bad request) error code is returned if an invalid payload is sent to the Flask endpoint.""" - with flask_app.app.test_client() as client: - response = client.post("/", json={"some": "data", "deliveryAttempt": 1}) - self.assertEqual(response.status_code, 400) - - response = client.post("/", json={"message": "data", "deliveryAttempt": 1}) - self.assertEqual(response.status_code, 400) - - -class TestQuestionRedelivery(TestCase): - def test_warning_logged_if_no_event_store_provided(self): - """Test that the question is allowed to proceed to analysis and a warning is logged if the event store cannot be - checked because one hasn't been specified in the service configuration. - """ - mock_configuration = copy.deepcopy(MOCK_CONFIGURATION) - mock_configuration.event_store_table_id = None - - with flask_app.app.test_client() as client: - with patch( - "octue.cloud.deployment.google.cloud_run.flask_app.answer_pub_sub_question" - ) as mock_answer_question: - with patch("octue.configuration.ServiceConfiguration.from_file", return_value=mock_configuration): - with self.assertLogs(level=logging.WARNING) as logging_context: - response = client.post( - "/", - json={ - "deliveryAttempt": 1, - "subscription": "projects/my-project/subscriptions/my-subscription", - "message": { - "data": {}, - "attributes": { - "question_uuid": str(uuid.uuid4()), - "forward_logs": "1", - "retry_count": "0", - }, - }, - }, - ) - - self.assertTrue( - logging_context.output[0].endswith( - "Cannot check if question has been redelivered as the 'event_store_table_id' key hasn't been set in " - "the service configuration (`octue.yaml` file)." - ) - ) - - self.assertEqual(response.status_code, 204) - mock_answer_question.assert_called_once() - - def test_warning_logged_if_event_store_not_found(self): - """Test that the question is allowed to proceed to analysis and a warning is logged if the event store cannot be - found. - """ - mock_configuration = copy.deepcopy(MOCK_CONFIGURATION) - mock_configuration.event_store_table_id = "nonexistent.table" - - multi_patcher = MultiPatcher( - patches=[ - patch("octue.configuration.ServiceConfiguration.from_file", return_value=mock_configuration), - patch("octue.cloud.deployment.google.cloud_run.flask_app.get_events", side_effect=NotFound("blah")), - ] - ) - - with flask_app.app.test_client() as client: - with patch( - "octue.cloud.deployment.google.cloud_run.flask_app.answer_pub_sub_question" - ) as mock_answer_question: - with multi_patcher: - with self.assertLogs(level=logging.WARNING) as logging_context: - response = client.post( - "/", - json={ - "deliveryAttempt": 1, - "subscription": "projects/my-project/subscriptions/my-subscription", - "message": { - "data": {}, - "attributes": { - "question_uuid": str(uuid.uuid4()), - "forward_logs": "1", - "retry_count": "0", - }, - }, - }, - ) - - self.assertTrue( - logging_context.output[0].endswith( - "Cannot check if question has been redelivered as no event store table was found with the ID " - "'nonexistent.table'; check that the 'event_store_table_id' key in the service configuration " - "(`octue.yaml` file) is correct." - ) - ) - - self.assertEqual(response.status_code, 204) - mock_answer_question.assert_called_once() - - def test_new_question(self): - """Test that a new question is checked against the event store and allowed to proceed to analysis.""" - multi_patcher = MultiPatcher( - patches=[ - patch("octue.configuration.ServiceConfiguration.from_file", return_value=MOCK_CONFIGURATION), - patch("octue.cloud.deployment.google.cloud_run.flask_app.get_events", return_value=[]), - ] - ) - - with flask_app.app.test_client() as client: - with patch( - "octue.cloud.deployment.google.cloud_run.flask_app.answer_pub_sub_question" - ) as mock_answer_question: - with multi_patcher: - with self.assertLogs() as logging_context: - response = client.post( - "/", - json={ - "deliveryAttempt": 1, - "subscription": "projects/my-project/subscriptions/my-subscription", - "message": { - "data": {}, - "attributes": { - "question_uuid": str(uuid.uuid4()), - "forward_logs": "1", - "retry_count": "0", - }, - }, - }, - ) - - self.assertTrue(logging_context.output[0].endswith("is a new question.")) - self.assertEqual(response.status_code, 204) - mock_answer_question.assert_called_once() - - def test_redelivered_questions_are_acknowledged_and_dropped(self): - """Test that questions undesirably redelivered by Pub/Sub are acknowledged and dropped.""" - question_uuid = "fcd7aad7-dbf0-47d2-8984-220d493df2c1" - - multi_patcher = MultiPatcher( - patches=[ - patch("octue.configuration.ServiceConfiguration.from_file", return_value=MOCK_CONFIGURATION), - patch( - "octue.cloud.deployment.google.cloud_run.flask_app.get_events", - return_value=[{"attributes": {"retry_count": "0"}}], - ), - ] - ) - - with flask_app.app.test_client() as client: - with patch( - "octue.cloud.deployment.google.cloud_run.flask_app.answer_pub_sub_question" - ) as mock_answer_question: - with self.assertLogs(level=logging.WARNING) as logging_context: - with multi_patcher: - response = client.post( - "/", - json={ - "subscription": "projects/my-project/subscriptions/my-subscription", - "message": { - "data": {}, - "attributes": { - "question_uuid": question_uuid, - "forward_logs": "1", - "retry_count": "0", - }, - }, - }, - ) - - self.assertIn( - "has already been received by the service. It will now be acknowledged and dropped to prevent further " - "redundant redelivery.", - logging_context.output[0], - ) - - self.assertEqual(response.status_code, 204) - mock_answer_question.assert_not_called() - - def test_retried_questions_are_allowed(self): - """Test that questions explicitly retried by the SDK are allowed to proceed to analysis.""" - question_uuid = "fcd7aad7-dbf0-47d2-8984-220d493df2c1" - - multi_patcher = MultiPatcher( - patches=[ - patch("octue.configuration.ServiceConfiguration.from_file", return_value=MOCK_CONFIGURATION), - patch( - "octue.cloud.deployment.google.cloud_run.flask_app.get_events", - return_value=[{"attributes": {"retry_count": "0"}}], - ), - ] - ) - - with flask_app.app.test_client() as client: - with patch( - "octue.cloud.deployment.google.cloud_run.flask_app.answer_pub_sub_question" - ) as mock_answer_question: - with multi_patcher: - response = client.post( - "/", - json={ - "subscription": "projects/my-project/subscriptions/my-subscription", - "message": { - "data": {}, - "attributes": { - "question_uuid": question_uuid, - "forward_logs": "1", - "retry_count": "1", - }, - }, - }, - ) - - self.assertEqual(response.status_code, 204) - mock_answer_question.assert_called_once() From 6b83793eda24697ac1a1802a93baf43b88bdf801 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Wed, 5 Feb 2025 14:06:25 +0000 Subject: [PATCH 056/125] REF: Move dockerfiles into their own directory --- .../{google/cloud_run => dockerfiles}/Dockerfile-python310 | 2 +- .../{google/cloud_run => dockerfiles}/Dockerfile-python311 | 2 +- .../{google/cloud_run => dockerfiles}/Dockerfile-python39 | 2 +- octue/cloud/deployment/google/cloud_run/__init__.py | 0 4 files changed, 3 insertions(+), 3 deletions(-) rename octue/cloud/deployment/{google/cloud_run => dockerfiles}/Dockerfile-python310 (98%) rename octue/cloud/deployment/{google/cloud_run => dockerfiles}/Dockerfile-python311 (98%) rename octue/cloud/deployment/{google/cloud_run => dockerfiles}/Dockerfile-python39 (98%) delete mode 100644 octue/cloud/deployment/google/cloud_run/__init__.py diff --git a/octue/cloud/deployment/google/cloud_run/Dockerfile-python310 b/octue/cloud/deployment/dockerfiles/Dockerfile-python310 similarity index 98% rename from octue/cloud/deployment/google/cloud_run/Dockerfile-python310 rename to octue/cloud/deployment/dockerfiles/Dockerfile-python310 index a3ca69a0b..b5ec4fc20 100644 --- a/octue/cloud/deployment/google/cloud_run/Dockerfile-python310 +++ b/octue/cloud/deployment/dockerfiles/Dockerfile-python310 @@ -27,7 +27,7 @@ RUN if [ -f "pyproject.toml" ]; then poetry install \ fi # Copy local code to the application root directory. -COPY . . +COPY ../google/cloud_run . # Install local packages if using poetry. Otherwise, install everything if using `setup.py` or `requirements.txt`. RUN if [ -f "pyproject.toml" ]; then poetry install --only main; \ diff --git a/octue/cloud/deployment/google/cloud_run/Dockerfile-python311 b/octue/cloud/deployment/dockerfiles/Dockerfile-python311 similarity index 98% rename from octue/cloud/deployment/google/cloud_run/Dockerfile-python311 rename to octue/cloud/deployment/dockerfiles/Dockerfile-python311 index 03d99cd73..bbe2367ad 100644 --- a/octue/cloud/deployment/google/cloud_run/Dockerfile-python311 +++ b/octue/cloud/deployment/dockerfiles/Dockerfile-python311 @@ -27,7 +27,7 @@ RUN if [ -f "pyproject.toml" ]; then poetry install \ fi # Copy local code to the application root directory. -COPY . . +COPY ../google/cloud_run . # Install local packages if using poetry. Otherwise, install everything if using `setup.py` or `requirements.txt`. RUN if [ -f "pyproject.toml" ]; then poetry install --only main; \ diff --git a/octue/cloud/deployment/google/cloud_run/Dockerfile-python39 b/octue/cloud/deployment/dockerfiles/Dockerfile-python39 similarity index 98% rename from octue/cloud/deployment/google/cloud_run/Dockerfile-python39 rename to octue/cloud/deployment/dockerfiles/Dockerfile-python39 index a76513322..683573489 100644 --- a/octue/cloud/deployment/google/cloud_run/Dockerfile-python39 +++ b/octue/cloud/deployment/dockerfiles/Dockerfile-python39 @@ -27,7 +27,7 @@ RUN if [ -f "pyproject.toml" ]; then poetry install \ fi # Copy local code to the application root directory. -COPY . . +COPY ../google/cloud_run . # Install local packages if using poetry. Otherwise, install everything if using `setup.py` or `requirements.txt`. RUN if [ -f "pyproject.toml" ]; then poetry install --only main; \ diff --git a/octue/cloud/deployment/google/cloud_run/__init__.py b/octue/cloud/deployment/google/cloud_run/__init__.py deleted file mode 100644 index e69de29bb..000000000 From a3afdda32cb5f12a3f905b73e78b9e4362adf465 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Wed, 5 Feb 2025 14:11:21 +0000 Subject: [PATCH 057/125] REF: Move `answer_pub_sub_question` module and rename --- octue/cli.py | 2 +- octue/cloud/deployment/google/__init__.py | 0 .../answer_question.py} | 1 - .../test_answer_question.py} | 14 +++++--------- 4 files changed, 6 insertions(+), 11 deletions(-) delete mode 100644 octue/cloud/deployment/google/__init__.py rename octue/cloud/{deployment/google/answer_pub_sub_question.py => pub_sub/answer_question.py} (99%) rename tests/cloud/{deployment/google/test_answer_pub_sub_question.py => pub_sub/test_answer_question.py} (91%) diff --git a/octue/cli.py b/octue/cli.py index 02a1e5a25..8c4d4dd5e 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -11,10 +11,10 @@ from google import auth from octue.cloud import pub_sub, storage -from octue.cloud.deployment.google.answer_pub_sub_question import answer_pub_sub_question from octue.cloud.events.replayer import EventReplayer from octue.cloud.events.utils import make_question_event from octue.cloud.events.validation import VALID_EVENT_KINDS +from octue.cloud.pub_sub.answer_question import answer_pub_sub_question from octue.cloud.pub_sub.bigquery import get_events from octue.cloud.pub_sub.service import Service from octue.cloud.service_id import create_sruid, get_sruid_parts diff --git a/octue/cloud/deployment/google/__init__.py b/octue/cloud/deployment/google/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/octue/cloud/deployment/google/answer_pub_sub_question.py b/octue/cloud/pub_sub/answer_question.py similarity index 99% rename from octue/cloud/deployment/google/answer_pub_sub_question.py rename to octue/cloud/pub_sub/answer_question.py index b9b48f0a3..23a324617 100644 --- a/octue/cloud/deployment/google/answer_pub_sub_question.py +++ b/octue/cloud/pub_sub/answer_question.py @@ -7,7 +7,6 @@ from octue.runner import Runner from octue.utils.objects import get_nested_attribute - logger = logging.getLogger(__name__) diff --git a/tests/cloud/deployment/google/test_answer_pub_sub_question.py b/tests/cloud/pub_sub/test_answer_question.py similarity index 91% rename from tests/cloud/deployment/google/test_answer_pub_sub_question.py rename to tests/cloud/pub_sub/test_answer_question.py index f3b7b1db9..cf1a79a2b 100644 --- a/tests/cloud/deployment/google/test_answer_pub_sub_question.py +++ b/tests/cloud/pub_sub/test_answer_question.py @@ -5,8 +5,8 @@ import yaml -from octue.cloud.deployment.google.answer_pub_sub_question import answer_pub_sub_question from octue.cloud.emulators._pub_sub import MockTopic +from octue.cloud.pub_sub.answer_question import answer_pub_sub_question from octue.utils.patches import MultiPatcher from tests.mocks import MockOpen @@ -25,13 +25,11 @@ def test_with_no_app_configuration_file(self): ), ), patch("octue.cloud.pub_sub.service.Topic", new=MockTopic), - patch("octue.cloud.deployment.google.answer_pub_sub_question.Service"), + patch("octue.cloud.pub_sub.answer_question.Service"), patch.dict(os.environ, {"OCTUE_SERVICE_REVISION_TAG": "blah"}), ] ): - with patch( - "octue.cloud.deployment.google.answer_pub_sub_question.Runner.from_configuration" - ) as mock_constructor: + with patch("octue.cloud.pub_sub.answer_question.Runner.from_configuration") as mock_constructor: answer_pub_sub_question( question={ "data": {}, @@ -85,14 +83,12 @@ class MockOpenForConfigurationFiles(MockOpen): "app_configuration.json": json.dumps({"configuration_values": {"hello": "configuration"}}), } - with patch( - "octue.cloud.deployment.google.answer_pub_sub_question.Runner.from_configuration" - ) as mock_constructor: + with patch("octue.cloud.pub_sub.answer_question.Runner.from_configuration") as mock_constructor: with MultiPatcher( patches=[ patch("octue.configuration.open", mock.mock_open(mock=MockOpenForConfigurationFiles)), patch("octue.cloud.pub_sub.service.Topic", new=MockTopic), - patch("octue.cloud.deployment.google.answer_pub_sub_question.Service"), + patch("octue.cloud.pub_sub.answer_question.Service"), patch.dict(os.environ, {"OCTUE_SERVICE_REVISION_TAG": "blah"}), ] ): From a909e23c64a9efd3c40d233697cfe3012696b4b8 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Wed, 5 Feb 2025 14:12:14 +0000 Subject: [PATCH 058/125] TST: Move cloud run deployment test skipci --- tests/cloud/deployment/google/__init__.py | 0 tests/cloud/deployment/google/cloud_run/__init__.py | 0 .../{google/cloud_run => }/test_cloud_run_deployment.py | 3 +-- 3 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 tests/cloud/deployment/google/__init__.py delete mode 100644 tests/cloud/deployment/google/cloud_run/__init__.py rename tests/cloud/deployment/{google/cloud_run => }/test_cloud_run_deployment.py (99%) diff --git a/tests/cloud/deployment/google/__init__.py b/tests/cloud/deployment/google/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/cloud/deployment/google/cloud_run/__init__.py b/tests/cloud/deployment/google/cloud_run/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/cloud/deployment/google/cloud_run/test_cloud_run_deployment.py b/tests/cloud/deployment/test_cloud_run_deployment.py similarity index 99% rename from tests/cloud/deployment/google/cloud_run/test_cloud_run_deployment.py rename to tests/cloud/deployment/test_cloud_run_deployment.py index 056d67a91..d7b0919af 100644 --- a/tests/cloud/deployment/google/cloud_run/test_cloud_run_deployment.py +++ b/tests/cloud/deployment/test_cloud_run_deployment.py @@ -3,12 +3,11 @@ import unittest from unittest import TestCase -import twined.exceptions from octue.cloud.events.replayer import EventReplayer from octue.cloud.events.validation import is_event_valid from octue.cloud.pub_sub.bigquery import get_events from octue.resources import Child - +import twined.exceptions EXAMPLE_SERVICE_SRUID = "octue/example-service:0.5.0" From 9dd7ea57b65d53542eaa12a4fb896f3fdf16d42a Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Wed, 5 Feb 2025 14:16:26 +0000 Subject: [PATCH 059/125] REF: Remove push subscription creation logic --- octue/cli.py | 72 +--------------------- octue/cloud/pub_sub/__init__.py | 39 ------------ tests/cloud/pub_sub/test_subscription.py | 17 ------ tests/test_cli.py | 76 +----------------------- 4 files changed, 2 insertions(+), 202 deletions(-) diff --git a/octue/cli.py b/octue/cli.py index 8c4d4dd5e..d83312bf0 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -10,7 +10,7 @@ import click from google import auth -from octue.cloud import pub_sub, storage +from octue.cloud import storage from octue.cloud.events.replayer import EventReplayer from octue.cloud.events.utils import make_question_event from octue.cloud.events.validation import VALID_EVENT_KINDS @@ -740,76 +740,6 @@ def start(service_config, revision_tag, timeout, no_rm): service.serve(timeout=timeout, delete_topic_and_subscription_on_exit=not no_rm) -@octue_cli.group() -def deploy(): - """A collection of commands to aid deploying a python app to the cloud as an Octue service or digital twin.""" - - -@deploy.command() -@click.argument("project_name") -@click.argument("service_namespace") -@click.argument("service_name") -@click.argument("push_endpoint") -@click.option( - "--expiration-time", - is_flag=False, - default=None, - show_default=True, - help="The number of seconds of inactivity after which the subscription should expire. If not provided, no " - "expiration time is applied to the subscription.", -) -@click.option( - "--revision-tag", - is_flag=False, - default=None, - show_default=True, - help="The service revision tag (e.g. 1.0.7). If this option isn't given, a random 'cool name' tag is generated e.g" - ". 'curious-capybara'.", -) -@click.option( - "--no-allow-existing", - is_flag=True, - help="If provided, raise an error if the push subscription already exists.", -) -def create_push_subscription( - project_name, - service_namespace, - service_name, - push_endpoint, - expiration_time, - revision_tag, - no_allow_existing, -): - """Create a Google Pub/Sub push subscription for an Octue service for it to receive questions from parents. The - subscription name is printed on completion. - - PROJECT_NAME is the name of the Google Cloud project in which the subscription will be created - - SERVICE_NAMESPACE is the namespace the service belongs to in kebab case - - SERVICE_NAME is the name of the service in kebab case, unique within its namespace - - PUSH_ENDPOINT is the HTTP/HTTPS endpoint of the service to push to. It should be fully formed and include the - 'https://' prefix - """ - sruid = create_sruid(namespace=service_namespace, name=service_name, revision_tag=revision_tag) - - subscription = pub_sub.create_push_subscription( - project_name, - sruid, - push_endpoint, - expiration_time=expiration_time, - subscription_filter=f'attributes.recipient = "{sruid}" AND attributes.sender_type = "PARENT"', - allow_existing=not no_allow_existing, - ) - - if subscription.creation_triggered_locally: - click.echo(f"Subscription for {sruid!r} created.") - return - - click.echo(f"Subscription for {sruid!r} already exists.") - - def _add_monitor_message_to_file(path, monitor_message): """Add a monitor message to the file at the given path. diff --git a/octue/cloud/pub_sub/__init__.py b/octue/cloud/pub_sub/__init__.py index c35ddc7eb..677e54157 100644 --- a/octue/cloud/pub_sub/__init__.py +++ b/octue/cloud/pub_sub/__init__.py @@ -1,43 +1,4 @@ -from octue.cloud.events import OCTUE_SERVICES_TOPIC_NAME -from octue.cloud.service_id import convert_service_id_to_pub_sub_form - from .subscription import Subscription from .topic import Topic __all__ = ["Subscription", "Topic"] - - -def create_push_subscription( - project_name, - sruid, - push_endpoint, - subscription_filter=None, - expiration_time=None, - allow_existing=True, -): - """Create a Google Pub/Sub push subscription for an Octue service for it to receive questions from parents. If a - corresponding topic doesn't exist, it will be created first. - - :param str project_name: the name of the Google Cloud project in which the subscription will be created - :param str sruid: the SRUID (service revision unique identifier) - :param str push_endpoint: the HTTP/HTTPS endpoint of the service to push to. It should be fully formed and include the 'https://' prefix - :param str|None subscription_filter: if specified, the filter to apply to the subscription; otherwise, no filter is applied - :param float|None expiration_time: the number of seconds of inactivity after which the subscription should expire. If not provided, no expiration time is applied to the subscription - :param bool allow_existing: if True, don't raise an error if the subscription already exists - :return octue.cloud.pub_sub.subscription.Subscription: - """ - if expiration_time: - expiration_time = float(expiration_time) - else: - expiration_time = None - - subscription = Subscription( - name=convert_service_id_to_pub_sub_form(sruid), - topic=Topic(name=OCTUE_SERVICES_TOPIC_NAME, project_name=project_name), - filter=subscription_filter, - expiration_time=expiration_time, - push_endpoint=push_endpoint, - ) - - subscription.create(allow_existing=allow_existing) - return subscription diff --git a/tests/cloud/pub_sub/test_subscription.py b/tests/cloud/pub_sub/test_subscription.py index 8c1be44fc..b9c2934cf 100644 --- a/tests/cloud/pub_sub/test_subscription.py +++ b/tests/cloud/pub_sub/test_subscription.py @@ -80,23 +80,6 @@ def test_create_pull_subscription(self): self.assertEqual(response._pb.retry_policy.minimum_backoff.seconds, 10) self.assertEqual(response._pb.retry_policy.maximum_backoff.seconds, 600) - def test_create_push_subscription(self): - """Test that creating a push subscription works properly.""" - project_name = os.environ["TEST_PROJECT_NAME"] - topic = Topic(name="my-topic", project_name=project_name) - subscription = Subscription(name="world", topic=topic, push_endpoint="https://example.com/endpoint") - - with patch("google.pubsub_v1.SubscriberClient.create_subscription", new=MockSubscriptionCreationResponse): - response = subscription.create(allow_existing=True) - - self.assertEqual(response._pb.ack_deadline_seconds, 600) - self.assertEqual(response._pb.expiration_policy.ttl.seconds, THIRTY_ONE_DAYS) - self.assertEqual(response._pb.message_retention_duration.seconds, 600) - self.assertTrue(response._pb.enable_message_ordering) - self.assertEqual(response._pb.retry_policy.minimum_backoff.seconds, 10) - self.assertEqual(response._pb.retry_policy.maximum_backoff.seconds, 600) - self.assertEqual(response._pb.push_config.push_endpoint, "https://example.com/endpoint") - def test_is_pull_subscription(self): """Test that `is_pull_subscription` is `True` for a pull subscription.""" self.assertTrue(self.subscription.is_pull_subscription) diff --git a/tests/test_cli.py b/tests/test_cli.py index 8b856bfa9..a3d19c0cf 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -9,10 +9,8 @@ from octue.cli import octue_cli from octue.cloud import storage -from octue.cloud.emulators._pub_sub import MockService, MockSubscription, MockTopic +from octue.cloud.emulators._pub_sub import MockService from octue.cloud.emulators.service import ServicePatcher -from octue.cloud.events import OCTUE_SERVICES_TOPIC_NAME -from octue.cloud.pub_sub import Topic from octue.configuration import AppConfiguration, ServiceConfiguration from octue.resources import Dataset from octue.utils.patches import MultiPatcher @@ -415,75 +413,3 @@ def test_get_diagnostics_with_datasets(self): {"kind": "result", "output_values": [1, 2, 3, 4, 5]}, ], ) - - -class TestDeployCommand(BaseTestCase): - def test_deploy_command_group(self): - """Test that the `create-push-subscription` command is a subcommand of the `deploy` command.""" - result = CliRunner().invoke(octue_cli, ["deploy", "--help"]) - self.assertIn("create-push-subscription ", result.output) - - def test_create_push_subscription(self): - """Test that a push subscription can be created using the `octue deploy create-push-subscription` command and - that its expiry time is correct. - """ - for expiration_time_option, expected_expiration_time in ( - ([], None), - (["--expiration-time="], None), - (["--expiration-time=100"], 100), - ): - with self.subTest(expiration_time_option=expiration_time_option): - with patch("octue.cloud.pub_sub.Topic", new=MockTopic): - with patch("octue.cloud.pub_sub.Subscription") as subscription: - result = CliRunner().invoke( - octue_cli, - [ - "deploy", - "create-push-subscription", - "my-project", - "octue", - "example-service", - "https://example.com/endpoint", - *expiration_time_option, - "--revision-tag=3.5.0", - ], - ) - - self.assertIsNone(result.exception) - self.assertEqual(result.exit_code, 0) - self.assertEqual(subscription.call_args.kwargs["name"], "octue.example-service.3-5-0") - self.assertEqual(subscription.call_args.kwargs["push_endpoint"], "https://example.com/endpoint") - self.assertEqual(subscription.call_args.kwargs["expiration_time"], expected_expiration_time) - self.assertEqual(result.output, "Subscription for 'octue/example-service:3.5.0' created.\n") - - def test_create_push_subscription_when_already_exists(self): - """Test attempting to create a push subscription for a service revision when one already exists for it.""" - sruid = "octue.example-service.3-5-0" - push_endpoint = "https://example.com/endpoint" - - with patch("octue.cloud.pub_sub.Topic", new=MockTopic): - with patch("octue.cloud.pub_sub.Subscription", new=MockSubscription): - subscription = MockSubscription( - name=sruid, - topic=Topic(name=OCTUE_SERVICES_TOPIC_NAME, project_name="my-project"), - push_endpoint=push_endpoint, - ) - - subscription.create() - - result = CliRunner().invoke( - octue_cli, - [ - "deploy", - "create-push-subscription", - "my-project", - "octue", - "example-service", - push_endpoint, - "--revision-tag=3.5.0", - ], - ) - - self.assertIsNone(result.exception) - self.assertEqual(result.exit_code, 0) - self.assertEqual(result.output, "Subscription for 'octue/example-service:3.5.0' already exists.\n") From c8ba8bd6b8291e21dcbbef3e433897c8ff737fe0 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Wed, 5 Feb 2025 14:18:25 +0000 Subject: [PATCH 060/125] ENH: Remove cloud run runtime warning --- octue/cloud/pub_sub/service.py | 16 ++------------ tests/cloud/pub_sub/test_service.py | 33 ----------------------------- 2 files changed, 2 insertions(+), 47 deletions(-) diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index 5e7527d0b..4ede725fe 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -4,8 +4,6 @@ import importlib.metadata import json import logging -import os -import time import uuid from google.api_core import retry @@ -233,12 +231,11 @@ def answer(self, question, heartbeat_interval=120, timeout=30): try: self._send_delivery_acknowledgment(**routing_metadata) - start_time = time.perf_counter() heartbeater = RepeatingTimer( interval=heartbeat_interval, function=self._send_heartbeat_and_check_runtime, - kwargs={"start_time": start_time, **routing_metadata}, + kwargs=routing_metadata, ) heartbeater.daemon = True @@ -680,12 +677,9 @@ def _send_heartbeat_and_check_runtime( parent, originator, retry_count, - start_time, - runtime_timeout_warning_time=3480, # This is 58 minutes in seconds. timeout=30, ): - """Send a heartbeat to the parent, indicating that the service is alive. If it's running on Cloud Run and it's - been running for longer than the runtime timeout warning time, log a warning that it will be stopped soon. + """Send a heartbeat to the parent, indicating that the service is alive. :param str question_uuid: the UUID of the question this event relates to :param str|None parent_question_uuid: the UUID of the question that triggered this question @@ -711,12 +705,6 @@ def _send_heartbeat_and_check_runtime( timeout=timeout, ) - if ( - os.environ.get("COMPUTE_PROVIDER") == "GOOGLE_CLOUD_RUN" - and time.perf_counter() - start_time > runtime_timeout_warning_time - ): - logger.warning("This analysis will reach the maximum runtime and be stopped soon.") - logger.debug("Heartbeat sent by %r.", self) def _send_monitor_message( diff --git a/tests/cloud/pub_sub/test_service.py b/tests/cloud/pub_sub/test_service.py index 5e57dc3ec..7fd57d257 100644 --- a/tests/cloud/pub_sub/test_service.py +++ b/tests/cloud/pub_sub/test_service.py @@ -5,7 +5,6 @@ import random import tempfile import time -from unittest import mock from unittest.mock import patch import google.api_core.exceptions @@ -758,38 +757,6 @@ def run_function(*args, **kwargs): delta=datetime.timedelta(0.05), ) - def test_runtime_timeout_warning_logged_if_running_on_cloud_run(self): - """Test that a warning is logged when the runtime timeout warning time is reached if the service is running on - Cloud Run. - """ - - def run_function(*args, **kwargs): - time.sleep(0.3) - return MockAnalysis() - - child = MockService(backend=BACKEND, run_function=lambda *args, **kwargs: run_function()) - parent = MockService(backend=BACKEND, children={child.id: child}) - child.serve() - - # Trigger the heartbeat check straight away. - with patch( - "octue.cloud.emulators._pub_sub.MockService.answer", - functools.partial(child.answer, heartbeat_interval=0.1), - ): - with patch( - "octue.cloud.pub_sub.service.Service._send_heartbeat_and_check_runtime", - functools.partial(child._send_heartbeat_and_check_runtime, runtime_timeout_warning_time=0), - ): - with mock.patch.dict(os.environ, COMPUTE_PROVIDER="GOOGLE_CLOUD_RUN"): - with self.assertLogs(level=logging.WARNING) as logging_context: - subscription, _ = parent.ask(service_id=child.id, input_values={}) - parent.wait_for_answer(subscription) - - self.assertIn( - "This analysis will reach the maximum runtime and be stopped soon.", - logging_context.output[0], - ) - def test_send_monitor_messages_periodically(self): """Test that monitor messages are sent periodically if set up in the run function and that the periodic monitor message thread doesn't stop the result from being received (i.e. message sending is thread-safe). From 9076d5b0f57e71c989871acb909f8e7fee763d12 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Wed, 5 Feb 2025 14:20:33 +0000 Subject: [PATCH 061/125] ENH: Add `GOOGLE_KUEUE` to compute providers and remove old ones --- octue/definitions.py | 2 +- tests/cloud/test_service_id.py | 10 +++++----- tests/test_log_handlers.py | 10 +++++----- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/octue/definitions.py b/octue/definitions.py index f16992a3d..90ac05990 100644 --- a/octue/definitions.py +++ b/octue/definitions.py @@ -16,4 +16,4 @@ # TODO this should probably be defined in twined RUN_STRANDS = ("input_values", "input_manifest", "credentials", "children") -GOOGLE_COMPUTE_PROVIDERS = {"GOOGLE_CLOUD_FUNCTION", "GOOGLE_CLOUD_RUN", "GOOGLE_DATAFLOW"} +GOOGLE_COMPUTE_PROVIDERS = {"GOOGLE_CLOUD_FUNCTION", "GOOGLE_KUEUE"} diff --git a/tests/cloud/test_service_id.py b/tests/cloud/test_service_id.py index 606e76dfa..18588f2b5 100644 --- a/tests/cloud/test_service_id.py +++ b/tests/cloud/test_service_id.py @@ -6,7 +6,6 @@ import requests -import octue.exceptions from octue.cloud.service_id import ( convert_service_id_to_pub_sub_form, get_default_sruid, @@ -17,6 +16,7 @@ validate_sruid, ) from octue.configuration import ServiceConfiguration +import octue.exceptions from octue.exceptions import InvalidServiceID from tests import MOCK_SERVICE_REVISION_TAG @@ -94,8 +94,8 @@ def test_convert_service_id_to_pub_sub_form(self): class TestGetSRUIDFromPubSubResourceName(unittest.TestCase): def test_get_sruid_from_pub_sub_resource_name(self): """Test that an SRUID can be extracted from a Pub/Sub resource name.""" - sruid = get_sruid_from_pub_sub_resource_name("octue.example-service-cloud-run.0-3-2") - self.assertEqual(sruid, "octue/example-service-cloud-run:0.3.2") + sruid = get_sruid_from_pub_sub_resource_name("octue.example-service.0-3-2") + self.assertEqual(sruid, "octue/example-service:0.3.2") class TestValidateSRUID(unittest.TestCase): @@ -117,7 +117,7 @@ def test_error_raised_if_service_id_invalid(self): "MY-ORG/my-service:1.9.4", "my-org/MY-SERVICE:1.9.4", "my-org/MY-SERVICE:@", - f"my-org/my-service:{'1'*129}", + f"my-org/my-service:{'1' * 129}", "/my-service", "/my-service:", ): @@ -165,7 +165,7 @@ def test_error_raised_if_sruid_components_invalid(self): ("MY-ORG", "my-service", "1.9.4"), ("my-org", "MY-SERVICE", "1.9.4"), ("my-org", "my-service", "@"), - ("my-org", "my-service", f"{'1'*129}"), + ("my-org", "my-service", f"{'1' * 129}"), ): with self.subTest(namespace=namespace, name=name, revision_tag=revision_tag): with self.assertRaises(InvalidServiceID): diff --git a/tests/test_log_handlers.py b/tests/test_log_handlers.py index d54fc69a9..0d1426bc7 100644 --- a/tests/test_log_handlers.py +++ b/tests/test_log_handlers.py @@ -17,11 +17,11 @@ class TestLogging(BaseTestCase): - def test_log_record_attributes_without_timestamp_used_if_compute_provider_is_google_cloud_run(self): + def test_log_record_attributes_without_timestamp_used_if_compute_provider_is_google_kueue(self): """Test that the formatter without a timestamp is used for logging if the `COMPUTE_PROVIDER` environment - variable is present and equal to "GOOGLE_CLOUD_RUN", and `USE_OCTUE_LOG_HANDLER` is equal to "1". + variable is present and equal to "GOOGLE_KUEUE", and `USE_OCTUE_LOG_HANDLER` is equal to "1". """ - with mock.patch.dict(os.environ, USE_OCTUE_LOG_HANDLER="1", COMPUTE_PROVIDER="GOOGLE_CLOUD_RUN"): + with mock.patch.dict(os.environ, USE_OCTUE_LOG_HANDLER="1", COMPUTE_PROVIDER="GOOGLE_KUEUE"): with mock.patch("octue.log_handlers.create_octue_formatter") as create_octue_formatter: importlib.reload(sys.modules["octue"]) @@ -32,9 +32,9 @@ def test_log_record_attributes_without_timestamp_used_if_compute_provider_is_goo include_thread_name=False, ) - def test_log_record_attributes_with_timestamp_used_if_compute_provider_is_not_google_cloud_run(self): + def test_log_record_attributes_with_timestamp_used_if_compute_provider_is_not_google_kueue(self): """Test that the formatter without a timestamp is used for logging if the `COMPUTE_PROVIDER` environment - variable is present and not equal to "GOOGLE_CLOUD_RUN", and `USE_OCTUE_LOG_HANDLER` is equal to "1". + variable is present and not equal to "GOOGLE_KUEUE", and `USE_OCTUE_LOG_HANDLER` is equal to "1". """ with mock.patch.dict(os.environ, USE_OCTUE_LOG_HANDLER="1", COMPUTE_PROVIDER="BLAH"): with mock.patch("octue.log_handlers.create_octue_formatter") as create_octue_formatter: From e5769d7c5a9d4bcc7b9ab7e78872152c16de77d7 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Wed, 5 Feb 2025 14:21:21 +0000 Subject: [PATCH 062/125] REF: Remove unused global variable --- octue/cloud/pub_sub/service.py | 1 - 1 file changed, 1 deletion(-) diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index 4ede725fe..d1bac4f22 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -38,7 +38,6 @@ DEFAULT_NAMESPACE = "default" ANSWERS_NAMESPACE = "answers" -OCTUE_SERVICE_REGISTRY_ENDPOINT = "services.registry.octue.com" # Switch message batching off by setting `max_messages` to 1. This minimises latency and is recommended for # microservices publishing single messages in a request-response sequence. From b5b50e65c8a95e98a700e4c987f5bbb02278b99a Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Wed, 5 Feb 2025 14:26:31 +0000 Subject: [PATCH 063/125] ENH: Remove option to parse events from Cloud Run --- octue/cloud/pub_sub/events.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/octue/cloud/pub_sub/events.py b/octue/cloud/pub_sub/events.py index b754dce3b..578108921 100644 --- a/octue/cloud/pub_sub/events.py +++ b/octue/cloud/pub_sub/events.py @@ -1,4 +1,3 @@ -import base64 from datetime import datetime, timedelta from functools import cached_property import json @@ -20,10 +19,9 @@ def extract_event_and_attributes_from_pub_sub_message(message): - """Extract an Octue service event and its attributes from a Google Pub/Sub message in either direct Pub/Sub format - or in the Google Cloud Run format. + """Extract an Octue service event and its attributes from a Google Pub/Sub message. - :param dict|google.cloud.pubsub_v1.subscriber.message.Message message: the message in Google Cloud Run format or Google Pub/Sub format + :param dict|google.cloud.pubsub_v1.subscriber.message.Message message: the message in dictionary format or direct Google Pub/Sub format :return (any, dict): the extracted event and its attributes """ # Cast attributes to a dictionary to avoid defaultdict-like behaviour from Pub/Sub message attributes container. @@ -59,14 +57,9 @@ def extract_event_and_attributes_from_pub_sub_message(message): if isinstance(message, dict) and "event" in message: event = message["event"] - # Extract question from Cloud Run or Pub/Sub format. + # Extract event directly from Pub/Sub. else: - try: - # Parse event directly from Pub/Sub or Dataflow. - event = json.loads(message.data.decode(), cls=OctueJSONDecoder) - except Exception: - # Parse event from Google Cloud Run. - event = json.loads(base64.b64decode(message["data"]).decode("utf-8").strip(), cls=OctueJSONDecoder) + event = json.loads(message.data.decode(), cls=OctueJSONDecoder) return event, attributes From 1c09aa767ff5bdda4493e46a9073a0f3d1d2e2fe Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Wed, 5 Feb 2025 14:26:53 +0000 Subject: [PATCH 064/125] REF: Remove other mentions of cloud run skipci --- octue/cloud/pub_sub/service.py | 4 ++-- octue/cloud/storage/client.py | 9 ++++----- octue/log_handlers.py | 5 ++--- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index d1bac4f22..0fb0c5d1e 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -745,9 +745,9 @@ def _send_monitor_message( logger.debug("Monitor message sent by %r.", self) def _parse_question(self, question): - """Parse a question in the Google Cloud Run or Google Pub/Sub format. + """Parse a question in dictionary format or direct Google Pub/Sub format. - :param dict|google.cloud.pubsub_v1.subscriber.message.Message question: the question to parse in Google Cloud Run or Google Pub/Sub format + :param dict|google.cloud.pubsub_v1.subscriber.message.Message question: the question to parse in dictionary format or direct Google Pub/Sub format :return (dict, str, str, str, bool, str, str, str, str, int): the question's event and its attributes (question UUID, parent question UUID, originator question UUID, whether to forward logs, the Octue SDK version of the parent, whether to save diagnostics, the SRUID of the parent that asked the question, the SRUID of the service revision that triggered all ancestor questions of this question, and the retry count) """ logger.info("%r received a question.", self) diff --git a/octue/cloud/storage/client.py b/octue/cloud/storage/client.py index 5e31e1d87..9f2b9e974 100644 --- a/octue/cloud/storage/client.py +++ b/octue/cloud/storage/client.py @@ -6,10 +6,10 @@ import os import warnings -import google.api_core.exceptions -import google.auth.exceptions from google import auth +import google.api_core.exceptions from google.auth import compute_engine +import google.auth.exceptions from google.auth.transport import requests as google_requests from google.cloud.storage import Client from google.cloud.storage.constants import _DEFAULT_TIMEOUT @@ -24,7 +24,6 @@ from octue.utils.decoders import OctueJSONDecoder from octue.utils.encoders import OctueJSONEncoder - logger = logging.getLogger(__name__) @@ -320,8 +319,8 @@ def generate_signed_url(self, cloud_path, expiration=datetime.timedelta(days=7)) blob = self._blob(cloud_path) try: - # Use compute engine credentials if running on e.g. Google Cloud Run, performing a refresh request to get - # the access token of the credentials (otherwise it's `None`). + # Use compute engine credentials if running in Google Cloud, performing a refresh request to get the access + # token of the credentials (otherwise it's `None`). credentials, _ = google.auth.default() request = google_requests.Request() credentials.refresh(request) diff --git a/octue/log_handlers.py b/octue/log_handlers.py index 1a50e1e44..3c0fb2665 100644 --- a/octue/log_handlers.py +++ b/octue/log_handlers.py @@ -5,7 +5,6 @@ from octue.definitions import GOOGLE_COMPUTE_PROVIDERS - if os.environ.get("COMPUTE_PROVIDER", "UNKNOWN") in GOOGLE_COMPUTE_PROVIDERS: # Google Cloud logs don't support colour currently - provide a no-operation function. colourise = lambda string, text_colour=None, background_colour=None: string @@ -189,8 +188,8 @@ def get_remote_handler( def get_log_record_attributes_for_environment(): - """Get the correct log record attributes for the environment. If the environment is Google Cloud Run, get log record - attributes not including the timestamp in the log context to avoid the date appearing twice in the Google Cloud Run + """Get the correct log record attributes for the environment. If the environment is in Google Cloud, get log record + attributes not including the timestamp in the log context to avoid the date appearing twice in the Google Cloud logs (Google adds its own timestamp to log messages). Otherwise, get log record attributes including the timestamp. :return list: From 3626b6ae06a7d9f40d82d81db28262d16f9fcaaa Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Wed, 5 Feb 2025 14:51:37 +0000 Subject: [PATCH 065/125] OPS: Remove terraform config --- terraform/.terraform.lock.hcl | 22 ----- terraform/artifact_registry.tf | 6 -- terraform/bigquery.tf | 100 ---------------------- terraform/functions.tf | 41 --------- terraform/iam.tf | 136 ------------------------------ terraform/iam_service_accounts.tf | 22 ----- terraform/main.tf | 100 ---------------------- terraform/pub_sub.tf | 3 - terraform/variables.tf | 39 --------- 9 files changed, 469 deletions(-) delete mode 100644 terraform/.terraform.lock.hcl delete mode 100644 terraform/artifact_registry.tf delete mode 100644 terraform/bigquery.tf delete mode 100644 terraform/functions.tf delete mode 100644 terraform/iam.tf delete mode 100644 terraform/iam_service_accounts.tf delete mode 100644 terraform/main.tf delete mode 100644 terraform/pub_sub.tf delete mode 100644 terraform/variables.tf diff --git a/terraform/.terraform.lock.hcl b/terraform/.terraform.lock.hcl deleted file mode 100644 index 9f6af285e..000000000 --- a/terraform/.terraform.lock.hcl +++ /dev/null @@ -1,22 +0,0 @@ -# This file is maintained automatically by "terraform init". -# Manual edits may be lost in future updates. - -provider "registry.terraform.io/hashicorp/google" { - version = "4.53.1" - constraints = "4.53.1" - hashes = [ - "h1:2I17Y3hcJ0TQ/yiUB8fRhB5FE3Ula+vGZecjGmq+NdU=", - "zh:29e289c3026d369e8b06b0dbe7f35db4aaa08439e801ddfc3349bd28fbf93635", - "zh:3e43a212acd2710f9cebd24677ff67c7aedffd958914c06eb7ca147556ee95f6", - "zh:53120dee6a3a29ac9559e23630a92350ba2ad20b1f8410be2fa2956d60769e2c", - "zh:63e7fd9d5da42db8421e53fc98e1bd91ad02e348b304255cc1e4bf7d55d84d06", - "zh:8084389a193262ddfe3f0367ef6660bc292799b0cbcc4196b8148d23c827b6c7", - "zh:86e5809b4a042a0161afcdc4c84a1082ea9461557affcd482ff740c5e252187f", - "zh:99ff6325e2bf07430aa50637078b33904a9f08b6a11d1370a133eb9902a855ba", - "zh:cbdd1415abd7034ff8e4a5173e1d0e13d2364185752df277bbfd34cc8242ba23", - "zh:d42ed1eaa3019d23af86f253ee38204d3b5d020c1daee50be0d8bbe4d9170eaf", - "zh:ee8b3371858b083f10dfb6c31db7933d7dfb3a8b6e6de6669f1fe07f9c407a1e", - "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", - "zh:f745d0071e8acdc13995c8f9ecb34d2309303134ae361b17f3ed31399d20a165", - ] -} diff --git a/terraform/artifact_registry.tf b/terraform/artifact_registry.tf deleted file mode 100644 index 87f8e28ea..000000000 --- a/terraform/artifact_registry.tf +++ /dev/null @@ -1,6 +0,0 @@ -resource "google_artifact_registry_repository" "artifact_registry_repository" { - location = var.region - repository_id = "${var.service_namespace}" - description = "Docker image repository" - format = "DOCKER" -} diff --git a/terraform/bigquery.tf b/terraform/bigquery.tf deleted file mode 100644 index bd8bfa630..000000000 --- a/terraform/bigquery.tf +++ /dev/null @@ -1,100 +0,0 @@ -resource "google_bigquery_dataset" "test_dataset" { - dataset_id = "octue_sdk_python_test_dataset" - description = "A dataset for testing storing events for the Octue SDK." - location = "EU" - - labels = { - env = "default" - } -} - -resource "google_bigquery_table" "test_table" { - dataset_id = google_bigquery_dataset.test_dataset.dataset_id - table_id = "service-events" - clustering = ["sender", "question_uuid"] - - schema = < Settings > Users -# and Permissions, with "Owner" level permission. - -resource "google_service_account" "dev_cortadocodes_service_account" { - account_id = "dev-cortadocodes" - description = "Allow cortadocodes to access developer-specific resources" - display_name = "dev-cortadocodes" - project = var.project -} - - -resource "google_service_account" "github_actions_service_account" { - account_id = "github-actions" - description = "Allow GitHub Actions to test the SDK." - display_name = "github-actions" - project = var.project -} diff --git a/terraform/main.tf b/terraform/main.tf deleted file mode 100644 index e858236dd..000000000 --- a/terraform/main.tf +++ /dev/null @@ -1,100 +0,0 @@ -terraform { - required_providers { - google = { - source = "hashicorp/google" - version = "4.53.1" - } - } - cloud { - organization = "octue" - workspaces { - name = "octue-sdk-python" - } - } -} - - -resource "google_project_service" "pub_sub" { - project = var.project - service = "pubsub.googleapis.com" - - timeouts { - create = "30m" - update = "40m" - } -} - - -resource "google_project_service" "cloud_resource_manager" { - project = var.project - service = "cloudresourcemanager.googleapis.com" - - timeouts { - create = "30m" - update = "40m" - } -} - - -resource "google_project_service" "iam" { - project = var.project - service = "iam.googleapis.com" - - timeouts { - create = "30m" - update = "40m" - } -} - - -resource "google_project_service" "artifact_registry" { - project = var.project - service = "artifactregistry.googleapis.com" - - timeouts { - create = "30m" - update = "40m" - } -} - - -resource "google_project_service" "cloud_run" { - project = var.project - service = "run.googleapis.com" - - timeouts { - create = "30m" - update = "40m" - } -} - - -resource "google_project_service" "cloud_functions" { - project = var.project - service = "cloudfunctions.googleapis.com" -} - - -resource "google_project_service" "eventarc" { - project = var.project - service = "eventarc.googleapis.com" -} - - -resource "google_project_service" "cloud_build" { - project = var.project - service = "cloudbuild.googleapis.com" -} - - -resource "google_project_service" "bigquery" { - project = var.project - service = "bigquery.googleapis.com" -} - - -provider "google" { - credentials = file(var.credentials_file) - project = var.project - region = var.region -} diff --git a/terraform/pub_sub.tf b/terraform/pub_sub.tf deleted file mode 100644 index ce436afec..000000000 --- a/terraform/pub_sub.tf +++ /dev/null @@ -1,3 +0,0 @@ -resource "google_pubsub_topic" "services_topic" { - name = "octue.services" -} diff --git a/terraform/variables.tf b/terraform/variables.tf deleted file mode 100644 index ccdfb938c..000000000 --- a/terraform/variables.tf +++ /dev/null @@ -1,39 +0,0 @@ -variable "organization" { - type = string - default = "octue" -} - -variable "project" { - type = string - default = "octue-sdk-python" -} - -variable "project_number" { - type = string - default = "437801218871" -} - -variable "region" { - type = string - default = "europe-west1" -} - -variable "github_organisation" { - type = string - default = "octue" -} - -variable "credentials_file" { - type = string - default = "gcp-credentials.json" -} - -variable "service_namespace" { - type = string - default = "octue" -} - -variable "service_name" { - type = string - default = "example-service-cloud-run" -} From 8a17a14f7830f132c06a1db5d6439873e4e1ed8d Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Wed, 5 Feb 2025 14:53:15 +0000 Subject: [PATCH 066/125] REF: Remove unused dockerfile and workflow skipci --- .github/workflows/build-docker-image.yml | 35 ------------------------ Dockerfile | 17 ------------ 2 files changed, 52 deletions(-) delete mode 100644 .github/workflows/build-docker-image.yml delete mode 100644 Dockerfile diff --git a/.github/workflows/build-docker-image.yml b/.github/workflows/build-docker-image.yml deleted file mode 100644 index 76dbeb9da..000000000 --- a/.github/workflows/build-docker-image.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: build-docker-image - -on: - workflow_dispatch: - inputs: - tag: - description: 'Tag for Docker image' - required: true - ref: - description: 'Branch, tag, or commit SHA to build from' - required: true - default: main - -jobs: - build-and-push: - runs-on: ubuntu-latest - timeout-minutes: 300 - steps: - - name: Checkout - uses: actions/checkout@v3 - with: - ref: ${{ github.event.inputs.ref }} - - - name: Log in to DockerHub - uses: docker/login-action@v1 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Build and push - uses: docker/build-push-action@v2.10.0 - with: - context: . - push: true - tags: octue/octue-sdk-python:${{ github.event.inputs.tag }} diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 1a1db8307..000000000 --- a/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -FROM python:3.8.12-slim - -RUN apt-get update && apt-get install -y git curl - -ENV POETRY_HOME=/etc/poetry -RUN curl -sSL https://install.python-poetry.org | python3 - -ENV PATH="$POETRY_HOME/bin:$PATH" -RUN poetry config virtualenvs.create false - -# Install python dependencies. Note that poetry installs any root packages by default, but this is not available at this -# stage of caching dependencies. So we do a dependency-only install here to cache the dependencies, then a full poetry -# install post-create to install the root package, which will change more rapidly than dependencies. -COPY pyproject.toml poetry.lock ./ -RUN poetry install --no-ansi --no-interaction --only=main --no-root -v - -COPY . . -RUN poetry install --no-ansi --no-interaction --only=main -v From e9fd3d0c011345476b673b024e45f1e04d801612 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Wed, 5 Feb 2025 15:34:33 +0000 Subject: [PATCH 067/125] REF: Split pub/sub event/attributes extraction into two functions --- octue/cloud/pub_sub/events.py | 34 +++++++++++++++++++++------------- octue/cloud/pub_sub/service.py | 5 +++-- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/octue/cloud/pub_sub/events.py b/octue/cloud/pub_sub/events.py index 578108921..7876905b0 100644 --- a/octue/cloud/pub_sub/events.py +++ b/octue/cloud/pub_sub/events.py @@ -18,11 +18,25 @@ MAX_SIMULTANEOUS_MESSAGES_PULL = 50 -def extract_event_and_attributes_from_pub_sub_message(message): - """Extract an Octue service event and its attributes from a Google Pub/Sub message. +def extract_event(message): + """Extract a Twined service event from a dictionary or Pub/Sub message. :param dict|google.cloud.pubsub_v1.subscriber.message.Message message: the message in dictionary format or direct Google Pub/Sub format - :return (any, dict): the extracted event and its attributes + :return dict: the extracted event + """ + # Support already-extracted questions (e.g. from the `octue question ask local` CLI command). + if isinstance(message, dict) and "event" in message: + return message["event"] + + # Extract event directly from Pub/Sub. + return json.loads(message.data.decode(), cls=OctueJSONDecoder) + + +def extract_and_convert_attributes(message): + """Extract a Twined service event's attributes and convert them to the expected form. + + :param dict|google.cloud.pubsub_v1.subscriber.message.Message message: the message in dictionary format or direct Google Pub/Sub format + :return dict: the extracted and converted attributes """ # Cast attributes to a dictionary to avoid defaultdict-like behaviour from Pub/Sub message attributes container. attributes = dict(get_nested_attribute(message, "attributes")) @@ -53,15 +67,7 @@ def extract_event_and_attributes_from_pub_sub_message(message): if cpus: attributes["cpus"] = int(cpus) - # Support already-extracted questions (e.g. from the `octue question ask local` CLI command). - if isinstance(message, dict) and "event" in message: - event = message["event"] - - # Extract event directly from Pub/Sub. - else: - event = json.loads(message.data.decode(), cls=OctueJSONDecoder) - - return event, attributes + return attributes class GoogleCloudPubSubEventHandler(AbstractEventHandler): @@ -277,4 +283,6 @@ def _extract_event_and_attributes(self, container): :param dict container: a Pub/Sub message :return (any, dict): the event and its attributes """ - return extract_event_and_attributes_from_pub_sub_message(container.message) + event = extract_event(container.message) + attributes = extract_and_convert_attributes(container.message) + return event, attributes diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index 0fb0c5d1e..e990330e5 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -16,7 +16,7 @@ from octue.cloud.events.utils import make_attributes from octue.cloud.events.validation import raise_if_event_is_invalid from octue.cloud.pub_sub import Subscription, Topic -from octue.cloud.pub_sub.events import GoogleCloudPubSubEventHandler, extract_event_and_attributes_from_pub_sub_message +from octue.cloud.pub_sub.events import GoogleCloudPubSubEventHandler, extract_and_convert_attributes, extract_event from octue.cloud.pub_sub.logging import GoogleCloudPubSubHandler from octue.cloud.service_id import ( convert_service_id_to_pub_sub_form, @@ -757,7 +757,8 @@ def _parse_question(self, question): question.ack() logger.info("Question acknowledged on Pub/Sub.") - event, attributes = extract_event_and_attributes_from_pub_sub_message(question) + event = extract_event(question) + attributes = extract_and_convert_attributes(question) logger.info("Extracted question event and attributes.") raise_if_event_is_invalid( From 1f8ee6140f1348b934be74c9c55e5ba23bedf02e Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Wed, 5 Feb 2025 15:40:39 +0000 Subject: [PATCH 068/125] REF: Move attributes extraction into `events` subpackage skipci --- octue/cloud/events/extraction.py | 39 +++++++++++++++++++++++++++++++ octue/cloud/pub_sub/events.py | 40 +------------------------------- octue/cloud/pub_sub/service.py | 3 ++- 3 files changed, 42 insertions(+), 40 deletions(-) create mode 100644 octue/cloud/events/extraction.py diff --git a/octue/cloud/events/extraction.py b/octue/cloud/events/extraction.py new file mode 100644 index 000000000..c1aaa02e6 --- /dev/null +++ b/octue/cloud/events/extraction.py @@ -0,0 +1,39 @@ +from octue.utils.objects import get_nested_attribute + + +def extract_and_convert_attributes(container): + """Extract a Twined service event's attributes and convert them to the expected form. + + :param dict|google.cloud.pubsub_v1.subscriber.message.Message container: the event container in dictionary format or direct Google Pub/Sub format + :return dict: the extracted and converted attributes + """ + # Cast attributes to a dictionary to avoid defaultdict-like behaviour from Pub/Sub message attributes container. + attributes = dict(get_nested_attribute(container, "attributes")) + + # Deserialise the `parent_question_uuid`, `forward_logs`, and `retry_count`, fields if they're present + # (don't assume they are before validation). + if attributes.get("parent_question_uuid") == "null": + attributes["parent_question_uuid"] = None + + retry_count = attributes.get("retry_count") + + if retry_count: + attributes["retry_count"] = int(retry_count) + else: + attributes["retry_count"] = None + + # Question events have some extra optional attributes. + if attributes.get("sender_type") == "PARENT": + forward_logs = attributes.get("forward_logs") + + if forward_logs: + attributes["forward_logs"] = bool(int(forward_logs)) + else: + attributes["forward_logs"] = None + + cpus = attributes.get("cpus") + + if cpus: + attributes["cpus"] = int(cpus) + + return attributes diff --git a/octue/cloud/pub_sub/events.py b/octue/cloud/pub_sub/events.py index 7876905b0..54c599c67 100644 --- a/octue/cloud/pub_sub/events.py +++ b/octue/cloud/pub_sub/events.py @@ -7,10 +7,10 @@ from google.api_core import retry from google.cloud.pubsub_v1 import SubscriberClient +from octue.cloud.events.extraction import extract_and_convert_attributes from octue.cloud.events.handler import AbstractEventHandler from octue.cloud.events.validation import SERVICE_COMMUNICATION_SCHEMA from octue.utils.decoders import OctueJSONDecoder -from octue.utils.objects import get_nested_attribute from octue.utils.threads import RepeatingTimer logger = logging.getLogger(__name__) @@ -32,44 +32,6 @@ def extract_event(message): return json.loads(message.data.decode(), cls=OctueJSONDecoder) -def extract_and_convert_attributes(message): - """Extract a Twined service event's attributes and convert them to the expected form. - - :param dict|google.cloud.pubsub_v1.subscriber.message.Message message: the message in dictionary format or direct Google Pub/Sub format - :return dict: the extracted and converted attributes - """ - # Cast attributes to a dictionary to avoid defaultdict-like behaviour from Pub/Sub message attributes container. - attributes = dict(get_nested_attribute(message, "attributes")) - - # Deserialise the `parent_question_uuid`, `forward_logs`, and `retry_count`, fields if they're present - # (don't assume they are before validation). - if attributes.get("parent_question_uuid") == "null": - attributes["parent_question_uuid"] = None - - retry_count = attributes.get("retry_count") - - if retry_count: - attributes["retry_count"] = int(retry_count) - else: - attributes["retry_count"] = None - - # Question events have some extra optional attributes. - if attributes.get("sender_type") == "PARENT": - forward_logs = attributes.get("forward_logs") - - if forward_logs: - attributes["forward_logs"] = bool(int(forward_logs)) - else: - attributes["forward_logs"] = None - - cpus = attributes.get("cpus") - - if cpus: - attributes["cpus"] = int(cpus) - - return attributes - - class GoogleCloudPubSubEventHandler(AbstractEventHandler): """A synchronous handler for events received as Google Pub/Sub messages from a pull subscription. diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index e990330e5..cc7363bc1 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -13,10 +13,11 @@ from octue.cloud import LOCAL_SDK_VERSION from octue.cloud.events import OCTUE_SERVICES_TOPIC_NAME +from octue.cloud.events.extraction import extract_and_convert_attributes from octue.cloud.events.utils import make_attributes from octue.cloud.events.validation import raise_if_event_is_invalid from octue.cloud.pub_sub import Subscription, Topic -from octue.cloud.pub_sub.events import GoogleCloudPubSubEventHandler, extract_and_convert_attributes, extract_event +from octue.cloud.pub_sub.events import GoogleCloudPubSubEventHandler, extract_event from octue.cloud.pub_sub.logging import GoogleCloudPubSubHandler from octue.cloud.service_id import ( convert_service_id_to_pub_sub_form, From c564ae3e2ffec784103cde577622cf07e4124588 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Wed, 5 Feb 2025 15:46:34 +0000 Subject: [PATCH 069/125] CHO: Deprecate `octue get-diagnostics` CLI command --- octue/cli.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/octue/cli.py b/octue/cli.py index d83312bf0..520adc690 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -543,6 +543,28 @@ def diagnostics(cloud_path, local_path, download_datasets): logger.info("Downloaded diagnostics from %r to %r.", cloud_path, local_path) +@octue_cli.command(deprecated=True) +@click.argument( + "cloud_path", + type=str, +) +@click.option( + "--local-path", + type=click.Path(file_okay=False), + default=".", + help="The path to a directory to store the directory of diagnostics data in. Defaults to the current working " + "directory.", +) +@click.option( + "--download-datasets", + is_flag=True, + help="If provided, download any datasets from the diagnostics and update their paths in the configuration and " + "input manifests to the new local paths.", +) +def get_diagnostics(cloud_path, local_path, download_datasets): + diagnostics(cloud_path, local_path, download_datasets) + + @octue_cli.command(deprecated=True) @click.option( "-c", From 31fc7bb182fa742de420ef1acbb36232c54b2001 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Wed, 5 Feb 2025 17:54:09 +0000 Subject: [PATCH 070/125] ENH: Add log message explaining waiting for question acceptance skipci --- octue/resources/child.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/octue/resources/child.py b/octue/resources/child.py index 00e17de0f..5ae6e9dc2 100644 --- a/octue/resources/child.py +++ b/octue/resources/child.py @@ -139,6 +139,8 @@ def ask( if push_endpoint or asynchronous: return subscription, question_uuid + logger.info("Waiting for question to be accepted...") + try: answer = self._service.wait_for_answer( subscription=subscription, From 9eca502c529e5c85a11ec1e1b98fa98aeb519bf2 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Wed, 5 Feb 2025 18:21:12 +0000 Subject: [PATCH 071/125] FIX: Only add resource request attributes if non-None skipci --- octue/cloud/events/utils.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/octue/cloud/events/utils.py b/octue/cloud/events/utils.py index d6da8dffa..bc200bccf 100644 --- a/octue/cloud/events/utils.py +++ b/octue/cloud/events/utils.py @@ -74,9 +74,6 @@ def make_attributes( "sender_sdk_version": LOCAL_SDK_VERSION, "recipient": recipient, "retry_count": int(retry_count), - "cpus": cpus, - "memory": memory, - "ephemeral_storage": ephemeral_storage, } if sender_type == "PARENT": @@ -88,5 +85,6 @@ def make_attributes( attributes["forward_logs"] = bool(forward_logs) attributes["save_diagnostics"] = save_diagnostics + attributes.update(make_minimal_dictionary(cpus=cpus, memory=memory, ephemeral_storage=ephemeral_storage)) return attributes From a4e7e0e35a4ee4cc26a60b4185885901db00b7f0 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Wed, 5 Feb 2025 19:28:29 +0000 Subject: [PATCH 072/125] REF: Rename function --- octue/cloud/pub_sub/service.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index cc7363bc1..30408c5ef 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -234,7 +234,7 @@ def answer(self, question, heartbeat_interval=120, timeout=30): heartbeater = RepeatingTimer( interval=heartbeat_interval, - function=self._send_heartbeat_and_check_runtime, + function=self._send_heartbeat, kwargs=routing_metadata, ) @@ -669,7 +669,7 @@ def _send_delivery_acknowledgment( logger.info("%r acknowledged receipt of question %r.", self, question_uuid) - def _send_heartbeat_and_check_runtime( + def _send_heartbeat( self, question_uuid, parent_question_uuid, @@ -688,7 +688,6 @@ def _send_heartbeat_and_check_runtime( :param str originator: the SRUID of the service revision that triggered all ancestor questions of this question :param int retry_count: the retry count of the question (this is zero if it's the first attempt at the question) :param int|float start_time: the `time.perf_counter` time that the analysis was started [s] - :param int|float runtime_timeout_warning_time: the amount of time after which to warn that the runtime timeout is approaching [s] :param float timeout: time in seconds after which to give up sending :return None: """ From f4c1c2b5ad9fb864fb389cf0b30f2c3ac8b22c27 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Wed, 5 Feb 2025 19:32:45 +0000 Subject: [PATCH 073/125] FEA: Add `cancel` method to `Service` and `Child` --- octue/cloud/pub_sub/service.py | 25 +++++++++++++++++++++++++ octue/resources/child.py | 20 ++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index 30408c5ef..2433df085 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -451,6 +451,31 @@ def wait_for_answer( finally: subscription.delete() + def cancel( + self, + question_uuid, + parent_question_uuid, + originator_question_uuid, + parent, + originator, + retry_count, + timeout=30, + ): + self._emit_event( + {"kind": "cancellation"}, + question_uuid=question_uuid, + parent_question_uuid=parent_question_uuid, + originator_question_uuid=originator_question_uuid, + parent=parent, + originator=originator, + recipient=parent, + retry_count=retry_count, + attributes={"sender_type": PARENT_SENDER_TYPE}, + timeout=timeout, + ) + + logger.info("%r requested cancellation of question %r.", self, question_uuid) + def send_exception( self, question_uuid, diff --git a/octue/resources/child.py b/octue/resources/child.py index 5ae6e9dc2..707292cd2 100644 --- a/octue/resources/child.py +++ b/octue/resources/child.py @@ -236,3 +236,23 @@ def ask_multiple( # Convert dictionary to list in asking order. return [answer[1] for answer in sorted(answers.items(), key=lambda item: item[0])] + + def cancel( + self, + question_uuid, + parent_question_uuid, + originator_question_uuid, + parent, + originator, + retry_count, + timeout=30, + ): + self._service.cancel( + question_uuid, + parent_question_uuid, + originator_question_uuid, + parent, + originator, + retry_count, + timeout=timeout, + ) From a514d4cb780848aed84d04b12fc7293b7a1f052d Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Wed, 5 Feb 2025 19:57:25 +0000 Subject: [PATCH 074/125] WIP: Update event schema locally skipci --- octue/cloud/events/validation.py | 285 ++++++++++++++++++++++++++++++- 1 file changed, 283 insertions(+), 2 deletions(-) diff --git a/octue/cloud/events/validation.py b/octue/cloud/events/validation.py index 41df484db..e1e37e558 100644 --- a/octue/cloud/events/validation.py +++ b/octue/cloud/events/validation.py @@ -4,7 +4,6 @@ from octue.compatibility import warn_if_incompatible - VALID_EVENT_KINDS = { "question", "delivery_acknowledgement", @@ -18,8 +17,290 @@ SERVICE_COMMUNICATION_SCHEMA_VERSION = "0.14.1" SERVICE_COMMUNICATION_SCHEMA_INFO_URL = "https://strands.octue.com/octue/service-communication" +# SERVICE_COMMUNICATION_SCHEMA = { +# "$ref": f"https://jsonschema.registry.octue.com/octue/service-communication/{SERVICE_COMMUNICATION_SCHEMA_VERSION}.json" +# } + + SERVICE_COMMUNICATION_SCHEMA = { - "$ref": f"https://jsonschema.registry.octue.com/octue/service-communication/{SERVICE_COMMUNICATION_SCHEMA_VERSION}.json" + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Octue services communication", + "description": "A schema describing the events Octue services can emit and consume.", + "type": "object", + "properties": { + "attributes": { + "title": "Event attributes", + "description": "Metadata for routing the event, adding context, and guiding the receiver's behaviour.", + "type": "object", + "oneOf": [ + { + "title": "Attributes for an event from a parent service", + "properties": { + "datetime": { + "type": "string", + "format": "date-time", + "description": "The UTC datetime the event was emitted at in ISO8601 format.", + }, + "uuid": { + "type": "string", + "format": "uuid", + "description": "A universally unique identifier for this event.", + }, + "question_uuid": { + "type": "string", + "description": "The UUID of the question the event is related to.", + }, + "parent_question_uuid": { + "oneOf": [ + { + "type": "string", + "description": "The UUID of the question that triggered this question.", + }, + {"type": "null", "description": "If this is the originating question."}, + ] + }, + "originator_question_uuid": { + "type": "string", + "description": "The UUID of the ultimate question that triggered this tree of questions.", + }, + "forward_logs": {"oneOf": [{"type": "boolean"}, {"enum": ["0", "1"]}]}, + "save_diagnostics": { + "enum": ["SAVE_DIAGNOSTICS_OFF", "SAVE_DIAGNOSTICS_ON_CRASH", "SAVE_DIAGNOSTICS_ON"] + }, + "parent": { + "type": "string", + "description": "The service revision unique identifier (SRUID) of the parent that asked the question this event is related to.", + "examples": ["octue:test-service:1.2.0"], + }, + "originator": { + "type": "string", + "description": "The service revision unique identifier (SRUID) of the service revision that triggered the tree of questions this event is related to.", + "examples": ["octue:test-service:1.2.0"], + }, + "sender": { + "type": "string", + "description": "The service revision unique identifier (SRUID) of the service revision emitting the event.", + "examples": ["octue:test-service:1.2.0"], + }, + "sender_type": { + "type": "string", + "pattern": "^PARENT$", + "description": "An indicator that the sender is a parent.", + }, + "sender_sdk_version": { + "type": "string", + "description": "The version of Octue SDK the sender is running.", + }, + "recipient": { + "type": "string", + "description": "The service revision unique identifier (SRUID) of the service revision this event is meant for.", + "examples": ["octue:test-service:1.2.0"], + }, + "retry_count": { + "type": "integer", + "description": "The retry count for the question. All events related to the retry of a given question will have the same retry count. A question that is being asked for the first time will have a retry count of 0.", + "minimum": 0, + }, + }, + "required": [ + "datetime", + "uuid", + "question_uuid", + "parent_question_uuid", + "originator_question_uuid", + "forward_logs", + "save_diagnostics", + "parent", + "originator", + "sender", + "sender_type", + "sender_sdk_version", + "recipient", + "retry_count", + ], + }, + { + "title": "Attributes for an event from a child service", + "properties": { + "datetime": { + "type": "string", + "format": "date-time", + "description": "The UTC datetime the event was emitted at in ISO8601 format.", + }, + "uuid": { + "type": "string", + "format": "uuid", + "description": "A universally unique identifier for this event.", + }, + "question_uuid": { + "type": "string", + "description": "The UUID of the question the event is related to.", + }, + "parent_question_uuid": { + "oneOf": [ + { + "type": "string", + "description": "The UUID of the question that triggered this question.", + }, + {"type": "null", "description": "If this is the originating question."}, + ] + }, + "originator_question_uuid": { + "type": "string", + "description": "The UUID of the ultimate question that triggered this tree of questions.", + }, + "parent": { + "type": "string", + "description": "The service revision unique identifier (SRUID) of the parent that asked the question this event is related to.", + "examples": ["octue:test-service:1.2.0"], + }, + "originator": { + "type": "string", + "description": "The service revision unique identifier (SRUID) of the service revision that triggered the tree of questions this event is related to.", + "examples": ["octue:test-service:1.2.0"], + }, + "sender": { + "type": "string", + "description": "The service revision unique identifier (SRUID) of the service revision emitting the event.", + "examples": ["octue:test-service:1.2.0"], + }, + "sender_type": { + "type": "string", + "pattern": "^CHILD$", + "description": "An indicator that the sender is a child.", + }, + "sender_sdk_version": { + "type": "string", + "description": "The version of Octue SDK the sender is running.", + }, + "recipient": { + "type": "string", + "description": "The service revision unique identifier (SRUID) of the service revision this event is meant for.", + "examples": ["octue:test-service:1.2.0"], + }, + "retry_count": { + "type": "integer", + "description": "The retry count for the question. All events related to the retry of a given question will have the same retry count. A question that is being asked for the first time will have a retry count of 0.", + "minimum": 0, + }, + }, + "required": [ + "datetime", + "uuid", + "question_uuid", + "parent_question_uuid", + "originator_question_uuid", + "parent", + "originator", + "sender", + "sender_type", + "sender_sdk_version", + "recipient", + "retry_count", + ], + }, + ], + }, + "event": { + "title": "Event data", + "description": "An Octue service event/message (e.g. heartbeat, log record, result).", + "type": "object", + "oneOf": [ + { + "title": "Delivery acknowledgement", + "description": "An acknowledgement of successful receipt of a question. This type of message can only be sent by a child to a parent as part of the child's response to a question.", + "type": "object", + "properties": {"kind": {"type": "string", "pattern": "^delivery_acknowledgement$"}}, + "required": ["kind"], + }, + { + "title": "Heartbeat", + "type": "object", + "description": "A message sent at regular intervals to let the parent know the child is still processing its question and that it should keep waiting for further messages. This type of message can only be sent by a child to a parent as part of the child's response to a question.", + "properties": {"kind": {"type": "string", "pattern": "^heartbeat$"}}, + "required": ["kind"], + }, + { + "title": "Monitor message", + "type": "object", + "description": "An interim result or update sent during the processing of a question. This type of message can only be sent by a child to a parent as part of the child's response to a question.", + "properties": { + "kind": {"type": "string", "pattern": "^monitor_message$"}, + "data": { + "description": "This schema is set in the child's twine (see https://twined.readthedocs.io/en/latest/anatomy_monitors.html)." + }, + }, + "required": ["kind", "data"], + }, + { + "title": "Log record", + "description": "A log record generated during the processing of a question. This type of message can only be sent by a child to a parent as part of the child's response to a question.", + "type": "object", + "properties": { + "kind": {"type": "string", "pattern": "^log_record$"}, + "log_record": {"type": "object"}, + }, + "required": ["kind", "log_record"], + }, + { + "title": "Exception", + "description": "An unhandled error raised during the processing of a question, marking its premature end. This type of message can only be sent by a child to a parent as part of the child's response to a question.", + "type": "object", + "properties": { + "kind": {"type": "string", "pattern": "^exception$"}, + "exception_message": {"type": "string"}, + "exception_type": {"type": "string"}, + "exception_traceback": {"type": "array", "items": {"type": "string"}}, + }, + "required": ["kind", "exception_message", "exception_type", "exception_traceback"], + }, + { + "title": "Result", + "description": "The final result of processing a question. This type of message can only and must be sent by a child to a parent to complete the child's response to a question.", + "type": "object", + "properties": { + "kind": {"type": "string", "pattern": "^result$"}, + "output_values": { + "description": "This schema is set in the child's twine (see https://twined.readthedocs.io/en/latest/anatomy_values.html)." + }, + "output_manifest": { + "description": "See schema information here: https://strands.octue.com/octue/manifest", + "$ref": "https://jsonschema.registry.octue.com/octue/manifest/0.1.0.json", + }, + }, + "required": ["kind"], + }, + { + "title": "Question", + "description": "A question for a child to process. This type of message can only be sent by a parent to a child to trigger the child to process a question.", + "properties": { + "kind": {"type": "string", "pattern": "^question$"}, + "input_values": { + "description": "This schema is set in the child's twine (see https://twined.readthedocs.io/en/latest/anatomy_values.html)." + }, + "input_manifest": { + "description": "See schema information here: https://strands.octue.com/octue/manifest", + "$ref": "https://jsonschema.registry.octue.com/octue/manifest/0.1.0.json", + }, + "children": { + "description": "See schema information here: https://strands.octue.com/octue/children", + "$ref": "https://jsonschema.registry.octue.com/octue/children/0.1.0.json", + }, + }, + "required": ["kind"], + }, + { + "title": "Cancellation", + "description": "A cancellation of a question. This type of message can only be sent by a parent.", + "properties": { + "kind": {"type": "string", "pattern": "^cancellation$"}, + }, + "required": ["kind"], + }, + ], + }, + }, + "required": ["attributes", "event"], } From cf911805bd15c10d5c1876efea19e51e84cce009 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Thu, 6 Feb 2025 11:25:03 +0000 Subject: [PATCH 075/125] ENH: Simplify question cancellation --- octue/cloud/pub_sub/service.py | 36 ++++++++++++++++++---------------- octue/resources/child.py | 21 ++------------------ 2 files changed, 21 insertions(+), 36 deletions(-) diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index 2433df085..0acc0da6d 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -17,6 +17,7 @@ from octue.cloud.events.utils import make_attributes from octue.cloud.events.validation import raise_if_event_is_invalid from octue.cloud.pub_sub import Subscription, Topic +from octue.cloud.pub_sub.bigquery import get_events from octue.cloud.pub_sub.events import GoogleCloudPubSubEventHandler, extract_event from octue.cloud.pub_sub.logging import GoogleCloudPubSubHandler from octue.cloud.service_id import ( @@ -451,30 +452,31 @@ def wait_for_answer( finally: subscription.delete() - def cancel( - self, - question_uuid, - parent_question_uuid, - originator_question_uuid, - parent, - originator, - retry_count, - timeout=30, - ): + def cancel(self, question_uuid, event_store_table_id, timeout=30): + questions = get_events(table_id=event_store_table_id, question_uuid=question_uuid, kinds=["question"]) + + if len(questions) == 0: + raise ValueError("No question found with question UUID %r.", question_uuid) + + if len(questions) > 1: + raise ValueError("Multiple questions found with same question UUID %r.", question_uuid) + + question_attributes = questions[0] + self._emit_event( {"kind": "cancellation"}, question_uuid=question_uuid, - parent_question_uuid=parent_question_uuid, - originator_question_uuid=originator_question_uuid, - parent=parent, - originator=originator, - recipient=parent, - retry_count=retry_count, + parent_question_uuid=question_attributes["parent_question_uuid"], + originator_question_uuid=question_attributes["originator_question_uuid"], + parent=question_attributes["parent"], + originator=question_attributes["originator"], + recipient=question_attributes["recipient"], + retry_count=question_attributes["retry_count"], attributes={"sender_type": PARENT_SENDER_TYPE}, timeout=timeout, ) - logger.info("%r requested cancellation of question %r.", self, question_uuid) + logger.info("Cancellation of question %r requested.", self, question_uuid) def send_exception( self, diff --git a/octue/resources/child.py b/octue/resources/child.py index 707292cd2..feb1b3ad0 100644 --- a/octue/resources/child.py +++ b/octue/resources/child.py @@ -237,22 +237,5 @@ def ask_multiple( # Convert dictionary to list in asking order. return [answer[1] for answer in sorted(answers.items(), key=lambda item: item[0])] - def cancel( - self, - question_uuid, - parent_question_uuid, - originator_question_uuid, - parent, - originator, - retry_count, - timeout=30, - ): - self._service.cancel( - question_uuid, - parent_question_uuid, - originator_question_uuid, - parent, - originator, - retry_count, - timeout=timeout, - ) + def cancel(self, question_uuid, event_store_table_id, timeout=30): + self._service.cancel(question_uuid=question_uuid, event_store_table_id=event_store_table_id, timeout=timeout) From ac1ba023398e57afde29c88cd646f5991afb003b Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Thu, 6 Feb 2025 11:27:00 +0000 Subject: [PATCH 076/125] FEA: Add `octue question cancel` CLI command --- octue/cli.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/octue/cli.py b/octue/cli.py index 520adc690..85b866a02 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -543,6 +543,43 @@ def diagnostics(cloud_path, local_path, download_datasets): logger.info("Downloaded diagnostics from %r to %r.", cloud_path, local_path) +@question.command() +@click.argument("question_uuid", type=str) +@click.option( + "-p", + "--project-name", + type=str, + default=None, + help="If asking a remote question, the name of the Google Cloud project the service is deployed in. If not " + "provided, the project name is detected from the local Google application credentials if present.", +) +@click.option( + "-c", + "--service-config", + type=click.Path(dir_okay=False), + default=None, + help="An optional path to an `octue.yaml` file defining service registries to use. If not provided, the " + "`OCTUE_SERVICE_CONFIGURATION_PATH` environment variable is used if present, otherwise the local path `octue.yaml` " + "is used.", +) +def cancel(question_uuid, project_name, service_config): + """Cancel a question running on a Twined service. + + QUESTION_UUID: The question UUID of a running question + """ + service_configuration = ServiceConfiguration.from_file(path=service_config) + + if not project_name: + _, project_name = auth.default() + + child = Child( + id=question["attributes"]["recipient"], + backend={"name": "GCPPubSubBackend", "project_name": project_name}, + ) + + child.cancel(question_uuid=question_uuid, event_store_table_id=service_configuration.event_store_table_id) + + @octue_cli.command(deprecated=True) @click.argument( "cloud_path", From a108f0e28adadb2786834512951b787845cc6cda Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Thu, 6 Feb 2025 11:29:28 +0000 Subject: [PATCH 077/125] ENH: Avoid trying to cancel question if it's already finished skipci --- octue/cloud/pub_sub/service.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index 0acc0da6d..67468d5a5 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -461,6 +461,15 @@ def cancel(self, question_uuid, event_store_table_id, timeout=30): if len(questions) > 1: raise ValueError("Multiple questions found with same question UUID %r.", question_uuid) + question_finished = get_events( + table_id=event_store_table_id, + question_uuid=question_uuid, + kinds=["result", "exception"], + ) + + if question_finished: + raise ValueError("Question %r has already finished.", question_uuid) + question_attributes = questions[0] self._emit_event( From 0912281fcc83b6121806ad9bc1f15937e283158d Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Thu, 6 Feb 2025 11:33:07 +0000 Subject: [PATCH 078/125] FIX: Avoid passing invalid ID to child in `octue question cancel` skipci --- octue/cli.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/octue/cli.py b/octue/cli.py index 85b866a02..f1cacefcb 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -572,11 +572,7 @@ def cancel(question_uuid, project_name, service_config): if not project_name: _, project_name = auth.default() - child = Child( - id=question["attributes"]["recipient"], - backend={"name": "GCPPubSubBackend", "project_name": project_name}, - ) - + child = Child(id=None, backend={"name": "GCPPubSubBackend", "project_name": project_name}) child.cancel(question_uuid=question_uuid, event_store_table_id=service_configuration.event_store_table_id) From 2c330fa6899e5cb59129950500858cfa8fdada82 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Thu, 6 Feb 2025 11:34:14 +0000 Subject: [PATCH 079/125] FIX: Add missing key skipci --- octue/cloud/pub_sub/service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index 67468d5a5..2cc60be9d 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -470,7 +470,7 @@ def cancel(self, question_uuid, event_store_table_id, timeout=30): if question_finished: raise ValueError("Question %r has already finished.", question_uuid) - question_attributes = questions[0] + question_attributes = questions[0]["attributes"] self._emit_event( {"kind": "cancellation"}, From 7b96f433e44757fc6177d10546e6f3ce5e74fff2 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Thu, 6 Feb 2025 11:37:22 +0000 Subject: [PATCH 080/125] FIX: Remove restriction on attributes for `PARENT` sender type skipci --- octue/cloud/events/utils.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/octue/cloud/events/utils.py b/octue/cloud/events/utils.py index bc200bccf..933ea54d2 100644 --- a/octue/cloud/events/utils.py +++ b/octue/cloud/events/utils.py @@ -77,14 +77,16 @@ def make_attributes( } if sender_type == "PARENT": - if forward_logs is None or save_diagnostics is None: - raise ValueError( - "`forward_logs` and `save_diagnostics` must be present in the attributes if the sender type is " - "'PARENT'." - ) + if forward_logs: + attributes["forward_logs"] = bool(forward_logs) - attributes["forward_logs"] = bool(forward_logs) - attributes["save_diagnostics"] = save_diagnostics - attributes.update(make_minimal_dictionary(cpus=cpus, memory=memory, ephemeral_storage=ephemeral_storage)) + attributes.update( + make_minimal_dictionary( + save_diagnostics=save_diagnostics, + cpus=cpus, + memory=memory, + ephemeral_storage=ephemeral_storage, + ) + ) return attributes From 2d3647abffdb35b70239b0e2860e789d8c47fd4b Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Thu, 6 Feb 2025 11:38:46 +0000 Subject: [PATCH 081/125] FIX: Fix log message skipci --- octue/cloud/pub_sub/service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index 2cc60be9d..5e38507da 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -485,7 +485,7 @@ def cancel(self, question_uuid, event_store_table_id, timeout=30): timeout=timeout, ) - logger.info("Cancellation of question %r requested.", self, question_uuid) + logger.info("Cancellation of question %r requested.", question_uuid) def send_exception( self, From cdc0c0cc1db7d494b7ec728b2bb48dcb2881a751 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Thu, 6 Feb 2025 11:41:02 +0000 Subject: [PATCH 082/125] FIX: Fix string interpolations in log messages skipci --- octue/cloud/pub_sub/service.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index 5e38507da..56522af95 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -456,10 +456,10 @@ def cancel(self, question_uuid, event_store_table_id, timeout=30): questions = get_events(table_id=event_store_table_id, question_uuid=question_uuid, kinds=["question"]) if len(questions) == 0: - raise ValueError("No question found with question UUID %r.", question_uuid) + raise ValueError(f"No question found with question UUID {question_uuid!r}.") if len(questions) > 1: - raise ValueError("Multiple questions found with same question UUID %r.", question_uuid) + raise ValueError(f"Multiple questions found with same question UUID {question_uuid!r}.") question_finished = get_events( table_id=event_store_table_id, @@ -468,7 +468,7 @@ def cancel(self, question_uuid, event_store_table_id, timeout=30): ) if question_finished: - raise ValueError("Question %r has already finished.", question_uuid) + raise ValueError(f"Question {question_uuid!r} has already finished.") question_attributes = questions[0]["attributes"] From 9a86bce51fddc9061be06d4b2edf6d8a8d5ecdc6 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Thu, 6 Feb 2025 12:12:32 +0000 Subject: [PATCH 083/125] FIX: Wait for event emission future by default skipci --- octue/cloud/pub_sub/logging.py | 2 +- octue/cloud/pub_sub/service.py | 17 ++++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/octue/cloud/pub_sub/logging.py b/octue/cloud/pub_sub/logging.py index 0da172017..cbe344e78 100644 --- a/octue/cloud/pub_sub/logging.py +++ b/octue/cloud/pub_sub/logging.py @@ -1,7 +1,6 @@ import logging import re - ANSI_ESCAPE_SEQUENCES_PATTERN = r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])" @@ -66,6 +65,7 @@ def emit(self, record): originator_question_uuid=self.originator_question_uuid, # The sender type is repeated here as a string to avoid a circular import. attributes={"sender_type": "CHILD"}, + wait=False, ) except Exception: # noqa diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index 56522af95..87fc14cf0 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -270,7 +270,7 @@ def answer(self, question, heartbeat_interval=120, timeout=30): if analysis.output_manifest is not None: result["output_manifest"] = analysis.output_manifest.to_primitive() - future = self._emit_event( + self._emit_event( event=result, recipient=parent, attributes={"sender_type": CHILD_SENDER_TYPE}, @@ -278,9 +278,6 @@ def answer(self, question, heartbeat_interval=120, timeout=30): **routing_metadata, ) - # Await successful publishing of the result. - future.result() - heartbeater.cancel() logger.info("%r answered question %r.", self, question_uuid) @@ -540,6 +537,7 @@ def _emit_event( recipient, retry_count, attributes=None, + wait=True, timeout=30, ): """Emit a JSON-serialised event as a Pub/Sub message to the services topic with optional message attributes. @@ -566,6 +564,7 @@ def _emit_event( :param str recipient: the SRUID of the service the event is intended for :param int retry_count: the retry count of the question (this is zero if it's the first attempt at the question) :param dict|None attributes: key-value pairs to attach to the event - the values must be strings or bytes + :param bool wait: if `True`, wait for the result of the publishing future before continuing execution (this is important if the python process ends promptly after the event is emitted instead of being part of a prolonged stream as the publishing may not complete and the event won't actually be emitted) :param int|float timeout: the timeout for sending the event in seconds :return google.cloud.pubsub_v1.publisher.futures.Future: """ @@ -603,6 +602,9 @@ def _emit_event( **converted_attributes, ) + if wait: + future.result() + return future def _send_question( @@ -645,7 +647,7 @@ def _send_question( input_manifest.use_signed_urls_for_datasets() question["input_manifest"] = input_manifest.to_primitive() - future = self._emit_event( + self._emit_event( event=question, question_uuid=question_uuid, parent_question_uuid=parent_question_uuid, @@ -665,8 +667,6 @@ def _send_question( timeout=timeout, ) - # Await successful publishing of the question. - future.result() logger.info("%r asked a question %r to service %r.", self, question_uuid, recipient) def _send_delivery_acknowledgment( @@ -701,6 +701,7 @@ def _send_delivery_acknowledgment( recipient=parent, retry_count=retry_count, attributes={"sender_type": CHILD_SENDER_TYPE}, + wait=False, ) logger.info("%r acknowledged receipt of question %r.", self, question_uuid) @@ -738,6 +739,7 @@ def _send_heartbeat( retry_count=retry_count, attributes={"sender_type": CHILD_SENDER_TYPE}, timeout=timeout, + wait=False, ) logger.debug("Heartbeat sent by %r.", self) @@ -776,6 +778,7 @@ def _send_monitor_message( retry_count=retry_count, timeout=timeout, attributes={"sender_type": CHILD_SENDER_TYPE}, + wait=False, ) logger.debug("Monitor message sent by %r.", self) From 0a49780a4b4d029ad244f5c2dae1801064dbb5ed Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Tue, 11 Feb 2025 15:47:34 +0000 Subject: [PATCH 084/125] REF: Factor out getting local `octue` version skipci --- octue/__init__.py | 4 ---- octue/cli.py | 6 ++---- octue/cloud/__init__.py | 8 +------- octue/cloud/emulators/_pub_sub.py | 4 ++-- octue/cloud/events/handler.py | 11 +++-------- octue/cloud/events/utils.py | 2 +- octue/cloud/pub_sub/service.py | 5 ++--- octue/definitions.py | 3 +++ octue/mixins/metadata.py | 4 ++-- 9 files changed, 16 insertions(+), 31 deletions(-) diff --git a/octue/__init__.py b/octue/__init__.py index 2e9ae9f5c..60da2ca73 100644 --- a/octue/__init__.py +++ b/octue/__init__.py @@ -4,16 +4,12 @@ from .log_handlers import apply_log_handler, should_use_octue_log_handler from .runner import Runner - logger = logging.getLogger(__name__) __all__ = ("Runner",) - - REPOSITORY_ROOT = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) - if should_use_octue_log_handler(): apply_log_handler( logger_name=None, # Apply to the root logger. diff --git a/octue/cli.py b/octue/cli.py index f1cacefcb..dc154ffc9 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -1,7 +1,5 @@ import copy import functools -import importlib.metadata -import importlib.util import json import logging import os @@ -20,7 +18,7 @@ from octue.cloud.service_id import create_sruid, get_sruid_parts from octue.cloud.storage import GoogleCloudStorageClient from octue.configuration import ServiceConfiguration, load_service_and_app_configuration -from octue.definitions import MANIFEST_FILENAME, VALUES_FILENAME +from octue.definitions import LOCAL_SDK_VERSION, MANIFEST_FILENAME, VALUES_FILENAME from octue.exceptions import ServiceAlreadyExists from octue.log_handlers import apply_log_handler, get_remote_handler from octue.resources import Child, Manifest, service_backends @@ -56,7 +54,7 @@ show_default=True, help="Forces a reset of analysis cache and outputs [For future use, currently not implemented]", ) -@click.version_option(version=importlib.metadata.version("octue")) +@click.version_option(version=LOCAL_SDK_VERSION) def octue_cli(id, logger_uri, log_level, force_reset): """The CLI for the Octue SDK. Use it to start an Octue data service or digital twin locally or run an analysis on one locally. diff --git a/octue/cloud/__init__.py b/octue/cloud/__init__.py index 96fc05939..13c386b10 100644 --- a/octue/cloud/__init__.py +++ b/octue/cloud/__init__.py @@ -1,12 +1,6 @@ -import importlib.metadata - import octue.exceptions -import twined.exceptions from octue.utils.exceptions import create_exceptions_mapping - - -LOCAL_SDK_VERSION = importlib.metadata.version("octue") - +import twined.exceptions EXCEPTIONS_MAPPING = create_exceptions_mapping( globals()["__builtins__"], diff --git a/octue/cloud/emulators/_pub_sub.py b/octue/cloud/emulators/_pub_sub.py index c9c01089c..a4a088dab 100644 --- a/octue/cloud/emulators/_pub_sub.py +++ b/octue/cloud/emulators/_pub_sub.py @@ -1,5 +1,4 @@ from collections import defaultdict -import importlib.metadata import json import logging @@ -7,6 +6,7 @@ from octue.cloud.pub_sub import Subscription, Topic from octue.cloud.pub_sub.service import PARENT_SENDER_TYPE, Service +from octue.definitions import LOCAL_SDK_VERSION from octue.resources import Manifest from octue.utils.dictionaries import make_minimal_dictionary from octue.utils.encoders import OctueJSONEncoder @@ -341,7 +341,7 @@ def ask( memory=None, ephemeral_storage=None, timeout=86400, - parent_sdk_version=importlib.metadata.version("octue"), + parent_sdk_version=LOCAL_SDK_VERSION, ): """Put the question into the messages register, register the existence of the corresponding response topic, add the response to the register, and return a MockFuture containing the answer subscription path. diff --git a/octue/cloud/events/handler.py b/octue/cloud/events/handler.py index a8fe55b8f..45942d343 100644 --- a/octue/cloud/events/handler.py +++ b/octue/cloud/events/handler.py @@ -1,18 +1,16 @@ import abc -import importlib.metadata +from datetime import datetime import logging import os import re import time -from datetime import datetime from octue.cloud import EXCEPTIONS_MAPPING from octue.cloud.events.validation import SERVICE_COMMUNICATION_SCHEMA, is_event_valid -from octue.definitions import GOOGLE_COMPUTE_PROVIDERS +from octue.definitions import GOOGLE_COMPUTE_PROVIDERS, LOCAL_SDK_VERSION from octue.log_handlers import COLOUR_PALETTE from octue.resources.manifest import Manifest - logger = logging.getLogger(__name__) @@ -23,9 +21,6 @@ from octue.utils.colour import colourise -PARENT_SDK_VERSION = importlib.metadata.version("octue") - - class AbstractEventHandler: """An abstract event handler for Octue service events that: - Provide handlers for the Octue service event kinds (see https://strands.octue.com/octue/service-communication) @@ -123,7 +118,7 @@ def _extract_and_validate_event(self, container): event=event, attributes=attributes, recipient=recipient, - parent_sdk_version=PARENT_SDK_VERSION, + parent_sdk_version=LOCAL_SDK_VERSION, child_sdk_version=child_sdk_version, schema=self.schema, ): diff --git a/octue/cloud/events/utils.py b/octue/cloud/events/utils.py index 933ea54d2..85c4ae15f 100644 --- a/octue/cloud/events/utils.py +++ b/octue/cloud/events/utils.py @@ -1,7 +1,7 @@ import datetime import uuid -from octue.cloud import LOCAL_SDK_VERSION +from octue.definitions import LOCAL_SDK_VERSION from octue.utils.dictionaries import make_minimal_dictionary diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index 87fc14cf0..878216ceb 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -1,7 +1,6 @@ import concurrent.futures import copy import functools -import importlib.metadata import json import logging import uuid @@ -11,7 +10,6 @@ from google.cloud import pubsub_v1 import jsonschema -from octue.cloud import LOCAL_SDK_VERSION from octue.cloud.events import OCTUE_SERVICES_TOPIC_NAME from octue.cloud.events.extraction import extract_and_convert_attributes from octue.cloud.events.utils import make_attributes @@ -29,6 +27,7 @@ validate_sruid, ) from octue.compatibility import warn_if_incompatible +from octue.definitions import LOCAL_SDK_VERSION import octue.exceptions from octue.utils.dictionaries import make_minimal_dictionary from octue.utils.encoders import OctueJSONEncoder @@ -806,7 +805,7 @@ def _parse_question(self, question): recipient=self.id, # Don't assume the presence of specific attributes before validation. parent_sdk_version=attributes.get("sender_sdk_version"), - child_sdk_version=importlib.metadata.version("octue"), + child_sdk_version=LOCAL_SDK_VERSION, ) logger.info("%r parsed question %r successfully.", self, attributes["question_uuid"]) diff --git a/octue/definitions.py b/octue/definitions.py index 90ac05990..08a6291e9 100644 --- a/octue/definitions.py +++ b/octue/definitions.py @@ -1,3 +1,5 @@ +import importlib.metadata + VALUES_FILENAME = "values.json" MANIFEST_FILENAME = "manifest.json" @@ -17,3 +19,4 @@ RUN_STRANDS = ("input_values", "input_manifest", "credentials", "children") GOOGLE_COMPUTE_PROVIDERS = {"GOOGLE_CLOUD_FUNCTION", "GOOGLE_KUEUE"} +LOCAL_SDK_VERSION = importlib.metadata.version("octue") diff --git a/octue/mixins/metadata.py b/octue/mixins/metadata.py index 721613c4d..5adaf034d 100644 --- a/octue/mixins/metadata.py +++ b/octue/mixins/metadata.py @@ -1,6 +1,6 @@ -import importlib.metadata from abc import abstractmethod +from octue.definitions import LOCAL_SDK_VERSION from octue.mixins.hashable import Hashable @@ -36,7 +36,7 @@ def metadata(self, include_id=True, include_sdk_version=True, **kwargs): metadata = {name: getattr(self, name) for name in self._METADATA_ATTRIBUTES} if include_sdk_version: - metadata["sdk_version"] = importlib.metadata.version("octue") + metadata["sdk_version"] = LOCAL_SDK_VERSION if not include_id and "id" in metadata: del metadata["id"] From a42c781a6f9771f45ab69444d67214d7737a07ee Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Tue, 11 Feb 2025 15:48:43 +0000 Subject: [PATCH 085/125] FIX: Fix incorrect path change in dockerfiles skipci --- octue/cloud/deployment/dockerfiles/Dockerfile-python310 | 2 +- octue/cloud/deployment/dockerfiles/Dockerfile-python311 | 2 +- octue/cloud/deployment/dockerfiles/Dockerfile-python39 | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/octue/cloud/deployment/dockerfiles/Dockerfile-python310 b/octue/cloud/deployment/dockerfiles/Dockerfile-python310 index b5ec4fc20..a3ca69a0b 100644 --- a/octue/cloud/deployment/dockerfiles/Dockerfile-python310 +++ b/octue/cloud/deployment/dockerfiles/Dockerfile-python310 @@ -27,7 +27,7 @@ RUN if [ -f "pyproject.toml" ]; then poetry install \ fi # Copy local code to the application root directory. -COPY ../google/cloud_run . +COPY . . # Install local packages if using poetry. Otherwise, install everything if using `setup.py` or `requirements.txt`. RUN if [ -f "pyproject.toml" ]; then poetry install --only main; \ diff --git a/octue/cloud/deployment/dockerfiles/Dockerfile-python311 b/octue/cloud/deployment/dockerfiles/Dockerfile-python311 index bbe2367ad..03d99cd73 100644 --- a/octue/cloud/deployment/dockerfiles/Dockerfile-python311 +++ b/octue/cloud/deployment/dockerfiles/Dockerfile-python311 @@ -27,7 +27,7 @@ RUN if [ -f "pyproject.toml" ]; then poetry install \ fi # Copy local code to the application root directory. -COPY ../google/cloud_run . +COPY . . # Install local packages if using poetry. Otherwise, install everything if using `setup.py` or `requirements.txt`. RUN if [ -f "pyproject.toml" ]; then poetry install --only main; \ diff --git a/octue/cloud/deployment/dockerfiles/Dockerfile-python39 b/octue/cloud/deployment/dockerfiles/Dockerfile-python39 index 683573489..a76513322 100644 --- a/octue/cloud/deployment/dockerfiles/Dockerfile-python39 +++ b/octue/cloud/deployment/dockerfiles/Dockerfile-python39 @@ -27,7 +27,7 @@ RUN if [ -f "pyproject.toml" ]; then poetry install \ fi # Copy local code to the application root directory. -COPY ../google/cloud_run . +COPY . . # Install local packages if using poetry. Otherwise, install everything if using `setup.py` or `requirements.txt`. RUN if [ -f "pyproject.toml" ]; then poetry install --only main; \ From 27c712d126aaa735bcef89f21ae2551bfc0d5e92 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Tue, 11 Feb 2025 15:54:13 +0000 Subject: [PATCH 086/125] REF: Move CRC32C warning catch below imports --- octue/cloud/storage/client.py | 8 ++++---- octue/resources/datafile.py | 9 ++++----- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/octue/cloud/storage/client.py b/octue/cloud/storage/client.py index 9f2b9e974..a4064df2b 100644 --- a/octue/cloud/storage/client.py +++ b/octue/cloud/storage/client.py @@ -15,15 +15,15 @@ from google.cloud.storage.constants import _DEFAULT_TIMEOUT from google.cloud.storage.retry import DEFAULT_RETRY -with warnings.catch_warnings(): - warnings.simplefilter("ignore") - from google_crc32c import Checksum - from octue.cloud import storage from octue.exceptions import CloudStorageBucketNotFound from octue.utils.decoders import OctueJSONDecoder from octue.utils.encoders import OctueJSONEncoder +with warnings.catch_warnings(): + warnings.simplefilter("ignore") + from google_crc32c import Checksum + logger = logging.getLogger(__name__) diff --git a/octue/resources/datafile.py b/octue/resources/datafile.py index f139bcdc9..8c0f3eeae 100644 --- a/octue/resources/datafile.py +++ b/octue/resources/datafile.py @@ -11,11 +11,6 @@ import requests -with warnings.catch_warnings(): - warnings.simplefilter("ignore") - from google_crc32c import Checksum - - # The `h5py` package is only needed if dealing with HDF5 files. It's only available if the `hdf5` extra is provided # during installation of `octue`. try: @@ -31,6 +26,10 @@ from octue.utils.decoders import OctueJSONDecoder from octue.utils.metadata import METADATA_FILENAME, UpdateLocalMetadata, load_local_metadata_file +with warnings.catch_warnings(): + warnings.simplefilter("ignore") + from google_crc32c import Checksum + logger = logging.getLogger(__name__) From 02c0e8ebc887ac73f06a1e92b8679c448bbb8aa5 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Tue, 11 Feb 2025 15:59:46 +0000 Subject: [PATCH 087/125] TST: Fix test --- tests/mixins/test_identifiable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/mixins/test_identifiable.py b/tests/mixins/test_identifiable.py index 21bcbbc48..a649dd51b 100644 --- a/tests/mixins/test_identifiable.py +++ b/tests/mixins/test_identifiable.py @@ -78,5 +78,5 @@ class Inherit(Identifiable): # Make test work across python versions. self.assertTrue( - ("object has no setter" in e.exception.args[0]) or ("object has no setter" in e.exception.args[0]) + ("can't set attribute" in e.exception.args[0]) or ("object has no setter" in e.exception.args[0]) ) From c3bafa4c92930f3cec9c55b193f9499212945164 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Tue, 11 Feb 2025 16:58:59 +0000 Subject: [PATCH 088/125] DOC: Document resource parameters in `ask` methods --- octue/cloud/pub_sub/service.py | 6 +++--- octue/resources/child.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index 878216ceb..a75f55c7a 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -327,9 +327,9 @@ def ask( :param str|None push_endpoint: if answers to the question should be pushed to an endpoint, provide its URL here (the returned subscription will be a push subscription); if not, leave this as `None` :param bool asynchronous: if `True` and not using a push endpoint, don't create an answer subscription :param int retry_count: the retry count of the question (this is zero if it's the first attempt at the question) - :param int|None cpus: - :param str|None memory: - :param str|None ephemeral_storage: + :param int|None cpus: the number of CPUs to request for the question; defaults to the number set by the child service + :param str|None memory: the amount of memory to request for the question e.g. "256Mi" or "1GiB"; defaults to the amount set by the child service + :param str|None ephemeral_storage: the amount of ephemeral storage to request for the question e.g. "256Mi" or "1GiB"; defaults to the amount set by the child service :param float|None timeout: time in seconds to keep retrying sending the question :return (octue.cloud.pub_sub.subscription.Subscription|None, str): the answer subscription (if the question is synchronous or a push endpoint was used) and question UUID """ diff --git a/octue/resources/child.py b/octue/resources/child.py index feb1b3ad0..65cd400a3 100644 --- a/octue/resources/child.py +++ b/octue/resources/child.py @@ -99,9 +99,9 @@ def ask( :param str|None push_endpoint: if answers to the question should be pushed to an endpoint, provide its URL here (the returned subscription will be a push subscription); if not, leave this as `None` :param bool asynchronous: if `True`, don't wait for an answer or create an answer subscription (the result and other events can be retrieved from the event store later) :param int retry_count: the retry count of the question (this is zero if it's the first attempt at the question) - :param int|None cpus: - :param str|None memory: - :param str|None ephemeral_storage: + :param int|None cpus: the number of CPUs to request for the question; defaults to the number set by the child service + :param str|None memory: the amount of memory to request for the question e.g. "256Mi" or "1GiB"; defaults to the amount set by the child service + :param str|None ephemeral_storage: the amount of ephemeral storage to request for the question e.g. "256Mi" or "1GiB"; defaults to the amount set by the child service :param bool raise_errors: if `True` and the question fails, raise the error; if False, return the error in place of the answer :param int max_retries: if `raise_errors=False` and the question fails, retry the question up to this number of times :param list(type)|None prevent_retries_when: if `raise_errors=False` and the question fails, prevent retrying the question if it fails with an exception type in this list From 38b38c43d2397acbe792e16ad545a5067d0929bb Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Tue, 11 Feb 2025 17:05:22 +0000 Subject: [PATCH 089/125] DOC: Document `cancel` methods --- octue/cloud/pub_sub/service.py | 8 ++++++++ octue/resources/child.py | 9 ++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index a75f55c7a..c350930a4 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -449,6 +449,14 @@ def wait_for_answer( subscription.delete() def cancel(self, question_uuid, event_store_table_id, timeout=30): + """Request cancellation of a running question. + + :param str question_uuid: the question UUID of the question to cancel + :param str event_store_table_id: the full ID of the Google BigQuery table used as the event store e.g. "your-project.your-dataset.your-table" + :param float timeout: time to wait for the cancellation to send before raising a timeout error [s] + :raise ValueError: if no question or more than one question is found for the given question UUID, or if the question has already finished + :return None: + """ questions = get_events(table_id=event_store_table_id, question_uuid=question_uuid, kinds=["question"]) if len(questions) == 0: diff --git a/octue/resources/child.py b/octue/resources/child.py index 65cd400a3..91e0148fc 100644 --- a/octue/resources/child.py +++ b/octue/resources/child.py @@ -106,7 +106,7 @@ def ask( :param int max_retries: if `raise_errors=False` and the question fails, retry the question up to this number of times :param list(type)|None prevent_retries_when: if `raise_errors=False` and the question fails, prevent retrying the question if it fails with an exception type in this list :param bool log_errors: if `True`, `raise_errors=False`, and the question fails after its final retry, log the error - :param float timeout: time in seconds to wait for an answer before raising a timeout error + :param float timeout: time to wait for an answer before raising a timeout error [s] :param float|int maximum_heartbeat_interval: the maximum amount of time (in seconds) allowed between child heartbeats before an error is raised :raise TimeoutError: if the timeout is exceeded while waiting for an answer :raise Exception: if the question raises an error and `raise_errors=True` @@ -238,4 +238,11 @@ def ask_multiple( return [answer[1] for answer in sorted(answers.items(), key=lambda item: item[0])] def cancel(self, question_uuid, event_store_table_id, timeout=30): + """Request cancellation of a running question. + + :param str question_uuid: the question UUID of the question to cancel + :param str event_store_table_id: the full ID of the Google BigQuery table used as the event store e.g. "your-project.your-dataset.your-table" + :param float timeout: time to wait for the cancellation to send before raising a timeout error [s] + :return None: + """ self._service.cancel(question_uuid=question_uuid, event_store_table_id=event_store_table_id, timeout=timeout) From 973c11c0a001d317fdddc5e1f71a30b3a2933e29 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Tue, 11 Feb 2025 17:07:02 +0000 Subject: [PATCH 090/125] ENH: Log warning instead of raising if question already finished skipci --- octue/cloud/pub_sub/service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index c350930a4..900332244 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -454,7 +454,7 @@ def cancel(self, question_uuid, event_store_table_id, timeout=30): :param str question_uuid: the question UUID of the question to cancel :param str event_store_table_id: the full ID of the Google BigQuery table used as the event store e.g. "your-project.your-dataset.your-table" :param float timeout: time to wait for the cancellation to send before raising a timeout error [s] - :raise ValueError: if no question or more than one question is found for the given question UUID, or if the question has already finished + :raise ValueError: if no question or more than one question is found for the given question UUID :return None: """ questions = get_events(table_id=event_store_table_id, question_uuid=question_uuid, kinds=["question"]) @@ -472,7 +472,7 @@ def cancel(self, question_uuid, event_store_table_id, timeout=30): ) if question_finished: - raise ValueError(f"Question {question_uuid!r} has already finished.") + logger.warning("Cannot cancel question %r - it has already finished.", question_uuid) question_attributes = questions[0]["attributes"] From da00d8cda3bba34117d0ddb533ef16799d54814d Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Tue, 11 Feb 2025 17:09:14 +0000 Subject: [PATCH 091/125] DOC: Fix docstrings skipci --- octue/cloud/pub_sub/service.py | 4 ++-- octue/resources/child.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index 900332244..88137717c 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -328,8 +328,8 @@ def ask( :param bool asynchronous: if `True` and not using a push endpoint, don't create an answer subscription :param int retry_count: the retry count of the question (this is zero if it's the first attempt at the question) :param int|None cpus: the number of CPUs to request for the question; defaults to the number set by the child service - :param str|None memory: the amount of memory to request for the question e.g. "256Mi" or "1GiB"; defaults to the amount set by the child service - :param str|None ephemeral_storage: the amount of ephemeral storage to request for the question e.g. "256Mi" or "1GiB"; defaults to the amount set by the child service + :param str|None memory: the amount of memory to request for the question e.g. "256Mi" or "1Gi"; defaults to the amount set by the child service + :param str|None ephemeral_storage: the amount of ephemeral storage to request for the question e.g. "256Mi" or "1Gi"; defaults to the amount set by the child service :param float|None timeout: time in seconds to keep retrying sending the question :return (octue.cloud.pub_sub.subscription.Subscription|None, str): the answer subscription (if the question is synchronous or a push endpoint was used) and question UUID """ diff --git a/octue/resources/child.py b/octue/resources/child.py index 91e0148fc..cbc0e70cc 100644 --- a/octue/resources/child.py +++ b/octue/resources/child.py @@ -100,8 +100,8 @@ def ask( :param bool asynchronous: if `True`, don't wait for an answer or create an answer subscription (the result and other events can be retrieved from the event store later) :param int retry_count: the retry count of the question (this is zero if it's the first attempt at the question) :param int|None cpus: the number of CPUs to request for the question; defaults to the number set by the child service - :param str|None memory: the amount of memory to request for the question e.g. "256Mi" or "1GiB"; defaults to the amount set by the child service - :param str|None ephemeral_storage: the amount of ephemeral storage to request for the question e.g. "256Mi" or "1GiB"; defaults to the amount set by the child service + :param str|None memory: the amount of memory to request for the question e.g. "256Mi" or "1Gi"; defaults to the amount set by the child service + :param str|None ephemeral_storage: the amount of ephemeral storage to request for the question e.g. "256Mi" or "1Gi"; defaults to the amount set by the child service :param bool raise_errors: if `True` and the question fails, raise the error; if False, return the error in place of the answer :param int max_retries: if `raise_errors=False` and the question fails, retry the question up to this number of times :param list(type)|None prevent_retries_when: if `raise_errors=False` and the question fails, prevent retrying the question if it fails with an exception type in this list From 97e9bfac017835abedef636c5141ba43308784c6 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Tue, 11 Feb 2025 17:18:15 +0000 Subject: [PATCH 092/125] REF: Rename function --- octue/cloud/events/extraction.py | 4 ++-- octue/cloud/pub_sub/events.py | 4 ++-- octue/cloud/pub_sub/service.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/octue/cloud/events/extraction.py b/octue/cloud/events/extraction.py index c1aaa02e6..964d8a145 100644 --- a/octue/cloud/events/extraction.py +++ b/octue/cloud/events/extraction.py @@ -1,8 +1,8 @@ from octue.utils.objects import get_nested_attribute -def extract_and_convert_attributes(container): - """Extract a Twined service event's attributes and convert them to the expected form. +def extract_and_deserialise_attributes(container): + """Extract a Twined service event's attributes and deserialise them to the expected form. :param dict|google.cloud.pubsub_v1.subscriber.message.Message container: the event container in dictionary format or direct Google Pub/Sub format :return dict: the extracted and converted attributes diff --git a/octue/cloud/pub_sub/events.py b/octue/cloud/pub_sub/events.py index 54c599c67..bbae0fa59 100644 --- a/octue/cloud/pub_sub/events.py +++ b/octue/cloud/pub_sub/events.py @@ -7,7 +7,7 @@ from google.api_core import retry from google.cloud.pubsub_v1 import SubscriberClient -from octue.cloud.events.extraction import extract_and_convert_attributes +from octue.cloud.events.extraction import extract_and_deserialise_attributes from octue.cloud.events.handler import AbstractEventHandler from octue.cloud.events.validation import SERVICE_COMMUNICATION_SCHEMA from octue.utils.decoders import OctueJSONDecoder @@ -246,5 +246,5 @@ def _extract_event_and_attributes(self, container): :return (any, dict): the event and its attributes """ event = extract_event(container.message) - attributes = extract_and_convert_attributes(container.message) + attributes = extract_and_deserialise_attributes(container.message) return event, attributes diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index 88137717c..48cc26e0a 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -11,7 +11,7 @@ import jsonschema from octue.cloud.events import OCTUE_SERVICES_TOPIC_NAME -from octue.cloud.events.extraction import extract_and_convert_attributes +from octue.cloud.events.extraction import extract_and_deserialise_attributes from octue.cloud.events.utils import make_attributes from octue.cloud.events.validation import raise_if_event_is_invalid from octue.cloud.pub_sub import Subscription, Topic @@ -804,7 +804,7 @@ def _parse_question(self, question): logger.info("Question acknowledged on Pub/Sub.") event = extract_event(question) - attributes = extract_and_convert_attributes(question) + attributes = extract_and_deserialise_attributes(question) logger.info("Extracted question event and attributes.") raise_if_event_is_invalid( From 29ac6fbac63ec5f34fbe8c5a82c8df23861d4594 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Tue, 11 Feb 2025 17:23:37 +0000 Subject: [PATCH 093/125] DOC: Update docstrings in `answer_pub_sub_question` --- octue/cloud/pub_sub/answer_question.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/octue/cloud/pub_sub/answer_question.py b/octue/cloud/pub_sub/answer_question.py index 23a324617..c891a4287 100644 --- a/octue/cloud/pub_sub/answer_question.py +++ b/octue/cloud/pub_sub/answer_question.py @@ -11,12 +11,12 @@ def answer_pub_sub_question(question, project_name, service_configuration=None, app_configuration=None): - """Answer a question sent to an app deployed in Google Cloud. + """Answer a question received by a service via Pub/Sub. - :param dict question: - :param str project_name: - :param service_configuration: - :param app_configuration: + :param dict question: a question event and its attributes + :param str project_name: the name of the project the service is running on + :param octue.configuration.ServiceConfiguration|None service_configuration: + :param octue.configuration.AppConfiguration|None app_configuration: :return None: """ if not service_configuration: From 8bfee3faf12ce06a555236e8025d0c7ac37b484f Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Tue, 11 Feb 2025 17:26:27 +0000 Subject: [PATCH 094/125] REF: Rename `answer_pub_sub_question` to `answer_question` skipci --- octue/cli.py | 4 ++-- .../cloud/{pub_sub => events}/answer_question.py | 4 ++-- .../{pub_sub => events}/test_answer_question.py | 16 ++++++++-------- 3 files changed, 12 insertions(+), 12 deletions(-) rename octue/cloud/{pub_sub => events}/answer_question.py (92%) rename tests/cloud/{pub_sub => events}/test_answer_question.py (91%) diff --git a/octue/cli.py b/octue/cli.py index dc154ffc9..5afe17d80 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -9,10 +9,10 @@ from google import auth from octue.cloud import storage +from octue.cloud.events.answer_question import answer_question from octue.cloud.events.replayer import EventReplayer from octue.cloud.events.utils import make_question_event from octue.cloud.events.validation import VALID_EVENT_KINDS -from octue.cloud.pub_sub.answer_question import answer_pub_sub_question from octue.cloud.pub_sub.bigquery import get_events from octue.cloud.pub_sub.service import Service from octue.cloud.service_id import create_sruid, get_sruid_parts @@ -239,7 +239,7 @@ def local(input_values, input_manifest, attributes, service_config): _, project_name = auth.default() backend = service_backends.get_backend()(project_name=project_name) - answer_pub_sub_question(question=question, project_name=backend.project_name, service_configuration=service_config) + answer_question(question=question, project_name=backend.project_name, service_configuration=service_config) @question.group() diff --git a/octue/cloud/pub_sub/answer_question.py b/octue/cloud/events/answer_question.py similarity index 92% rename from octue/cloud/pub_sub/answer_question.py rename to octue/cloud/events/answer_question.py index c891a4287..9ef2e7285 100644 --- a/octue/cloud/pub_sub/answer_question.py +++ b/octue/cloud/events/answer_question.py @@ -10,8 +10,8 @@ logger = logging.getLogger(__name__) -def answer_pub_sub_question(question, project_name, service_configuration=None, app_configuration=None): - """Answer a question received by a service via Pub/Sub. +def answer_question(question, project_name, service_configuration=None, app_configuration=None): + """Answer a question received by a service. :param dict question: a question event and its attributes :param str project_name: the name of the project the service is running on diff --git a/tests/cloud/pub_sub/test_answer_question.py b/tests/cloud/events/test_answer_question.py similarity index 91% rename from tests/cloud/pub_sub/test_answer_question.py rename to tests/cloud/events/test_answer_question.py index cf1a79a2b..eb20dd9be 100644 --- a/tests/cloud/pub_sub/test_answer_question.py +++ b/tests/cloud/events/test_answer_question.py @@ -6,12 +6,12 @@ import yaml from octue.cloud.emulators._pub_sub import MockTopic -from octue.cloud.pub_sub.answer_question import answer_pub_sub_question +from octue.cloud.events.answer_question import answer_question from octue.utils.patches import MultiPatcher from tests.mocks import MockOpen -class TestAnswerPubSubQuestion(TestCase): +class TestAnswerQuestion(TestCase): def test_with_no_app_configuration_file(self): """Test that the `answer_question` function uses the default service and app configuration values when the minimal service configuration is provided with no path to an app configuration file. @@ -25,12 +25,12 @@ def test_with_no_app_configuration_file(self): ), ), patch("octue.cloud.pub_sub.service.Topic", new=MockTopic), - patch("octue.cloud.pub_sub.answer_question.Service"), + patch("octue.cloud.events.answer_question.Service"), patch.dict(os.environ, {"OCTUE_SERVICE_REVISION_TAG": "blah"}), ] ): - with patch("octue.cloud.pub_sub.answer_question.Runner.from_configuration") as mock_constructor: - answer_pub_sub_question( + with patch("octue.cloud.events.answer_question.Runner.from_configuration") as mock_constructor: + answer_question( question={ "data": {}, "attributes": { @@ -83,16 +83,16 @@ class MockOpenForConfigurationFiles(MockOpen): "app_configuration.json": json.dumps({"configuration_values": {"hello": "configuration"}}), } - with patch("octue.cloud.pub_sub.answer_question.Runner.from_configuration") as mock_constructor: + with patch("octue.cloud.events.answer_question.Runner.from_configuration") as mock_constructor: with MultiPatcher( patches=[ patch("octue.configuration.open", mock.mock_open(mock=MockOpenForConfigurationFiles)), patch("octue.cloud.pub_sub.service.Topic", new=MockTopic), - patch("octue.cloud.pub_sub.answer_question.Service"), + patch("octue.cloud.events.answer_question.Service"), patch.dict(os.environ, {"OCTUE_SERVICE_REVISION_TAG": "blah"}), ] ): - answer_pub_sub_question( + answer_question( question={ "data": {}, "attributes": { From 5578fdfa3c479737cc6d4e61db4fb47f67e44bcd Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Tue, 11 Feb 2025 17:45:23 +0000 Subject: [PATCH 095/125] ENH: Remove outdated `octue run` CLI command BREAKING CHANGE: Use the new `octue question ask local` CLI command instead. --- octue/cli.py | 99 ---------------------------------------------------- 1 file changed, 99 deletions(-) diff --git a/octue/cli.py b/octue/cli.py index 5afe17d80..5ca1b4e6e 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -24,7 +24,6 @@ from octue.resources import Child, Manifest, service_backends from octue.runner import Runner from octue.utils.decoders import OctueJSONDecoder -from octue.utils.encoders import OctueJSONEncoder logger = logging.getLogger(__name__) @@ -596,104 +595,6 @@ def get_diagnostics(cloud_path, local_path, download_datasets): diagnostics(cloud_path, local_path, download_datasets) -@octue_cli.command(deprecated=True) -@click.option( - "-c", - "--service-config", - type=click.Path(dir_okay=False), - default=None, - help="The path to an `octue.yaml` file defining the service to run. If not provided, the " - "`OCTUE_SERVICE_CONFIGURATION_PATH` environment variable is used if present, otherwise the local path `octue.yaml` " - "is used.", -) -@click.option( - "--input-dir", - type=click.Path(file_okay=False, exists=True), - default=".", - show_default=True, - help="The path to a directory containing the input values (in a file called 'values.json') and/or input manifest " - "(in a file called 'manifest.json').", -) -@click.option( - "-o", - "--output-file", - type=click.Path(dir_okay=False), - default=None, - show_default=True, - help="The path to a JSON file to store the output values in, if required.", -) -@click.option( - "--output-manifest-file", - type=click.Path(dir_okay=False), - default=None, - help="The path to a JSON file to store the output manifest in. The default is 'output_manifest_.json'.", -) -@click.option( - "--monitor-messages-file", - type=click.Path(dir_okay=False), - default=None, - show_default=True, - help="The path to a JSON file in which to store any monitor messages received. Monitor messages will be ignored " - "if this option isn't provided.", -) -def run(service_config, input_dir, output_file, output_manifest_file, monitor_messages_file): - """Run an analysis on the given input data using an Octue service or digital twin locally. The output values are - printed to `stdout`. If an output manifest is produced, it will be saved locally (see the `--output-manifest-file` - option). - """ - service_configuration, app_configuration = load_service_and_app_configuration(service_config) - - input_values_path = os.path.join(input_dir, VALUES_FILENAME) - input_manifest_path = os.path.join(input_dir, MANIFEST_FILENAME) - - input_values = None - input_manifest = None - - if os.path.exists(input_values_path): - input_values = input_values_path - - if os.path.exists(input_manifest_path): - input_manifest = input_manifest_path - - runner = Runner.from_configuration(service_configuration=service_configuration, app_configuration=app_configuration) - - if monitor_messages_file: - if not os.path.exists(os.path.dirname(monitor_messages_file)): - os.makedirs(os.path.dirname(monitor_messages_file)) - - monitor_message_handler = lambda message: _add_monitor_message_to_file(monitor_messages_file, message) - - else: - monitor_message_handler = None - - analysis = runner.run( - analysis_id=global_cli_context["analysis_id"], - input_values=input_values, - input_manifest=input_manifest, - analysis_log_level=global_cli_context["log_level"], - analysis_log_handler=global_cli_context["log_handler"], - handle_monitor_message=monitor_message_handler, - ) - - click.echo(json.dumps(analysis.output_values, cls=OctueJSONEncoder)) - - if analysis.output_values and output_file: - if not os.path.exists(os.path.dirname(output_file)): - os.makedirs(os.path.dirname(output_file)) - - with open(output_file, "w") as f: - json.dump(analysis.output_values, f, cls=OctueJSONEncoder, indent=4) - - if analysis.output_manifest: - if not os.path.exists(os.path.dirname(output_manifest_file)): - os.makedirs(os.path.dirname(output_manifest_file)) - - with open(output_manifest_file or f"output_manifest_{analysis.id}.json", "w") as f: - json.dump(analysis.output_manifest.to_primitive(), f, cls=OctueJSONEncoder, indent=4) - - return 0 - - @octue_cli.command() @click.option( "-c", From 5a7a010ab58e45e32c4365decfb8d67bd8e1337c Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Tue, 11 Feb 2025 17:49:21 +0000 Subject: [PATCH 096/125] ENH: Return result from `Service.answer` and `octue question ask local` --- octue/cli.py | 3 ++- octue/cloud/events/answer_question.py | 4 ++-- octue/cloud/pub_sub/service.py | 3 ++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/octue/cli.py b/octue/cli.py index 5ca1b4e6e..a4a0e78c9 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -238,7 +238,8 @@ def local(input_values, input_manifest, attributes, service_config): _, project_name = auth.default() backend = service_backends.get_backend()(project_name=project_name) - answer_question(question=question, project_name=backend.project_name, service_configuration=service_config) + answer = answer_question(question=question, project_name=backend.project_name, service_configuration=service_config) + click.echo(answer) @question.group() diff --git a/octue/cloud/events/answer_question.py b/octue/cloud/events/answer_question.py index 9ef2e7285..571e3262e 100644 --- a/octue/cloud/events/answer_question.py +++ b/octue/cloud/events/answer_question.py @@ -17,7 +17,7 @@ def answer_question(question, project_name, service_configuration=None, app_conf :param str project_name: the name of the project the service is running on :param octue.configuration.ServiceConfiguration|None service_configuration: :param octue.configuration.AppConfiguration|None app_configuration: - :return None: + :return dict: the result event """ if not service_configuration: service_configuration, app_configuration = load_service_and_app_configuration() @@ -42,7 +42,7 @@ def answer_question(question, project_name, service_configuration=None, app_conf ) service.run_function = runner.run - service.answer(question) + return service.answer(question) except BaseException as error: # noqa service.send_exception( diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index 48cc26e0a..5e774b0a8 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -200,7 +200,7 @@ def answer(self, question, heartbeat_interval=120, timeout=30): :param int|float heartbeat_interval: the time interval, in seconds, at which to send heartbeats :param float|None timeout: time in seconds to keep retrying sending of the answer once it has been calculated :raise Exception: if any exception arises during running analysis and sending its results - :return None: + :return dict: the result event """ try: ( @@ -279,6 +279,7 @@ def answer(self, question, heartbeat_interval=120, timeout=30): heartbeater.cancel() logger.info("%r answered question %r.", self, question_uuid) + return result except BaseException as error: # noqa if heartbeater is not None: From 6e89bfcb1112e7394be8fc6098ed2d94b1576a5a Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Tue, 11 Feb 2025 18:10:01 +0000 Subject: [PATCH 097/125] REF: Allow `create_sruid` to create default SRUIDs --- octue/cloud/pub_sub/service.py | 3 +-- octue/cloud/service_id.py | 14 ++++++++------ tests/cloud/test_service_id.py | 30 ++++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index 5e774b0a8..836bd9bce 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -37,7 +37,6 @@ logger = logging.getLogger(__name__) -DEFAULT_NAMESPACE = "default" ANSWERS_NAMESPACE = "answers" # Switch message batching off by setting `max_messages` to 1. This minimises latency and is recommended for @@ -68,7 +67,7 @@ class Service: def __init__(self, backend, service_id=None, run_function=None, service_registries=None): if service_id is None: - self.id = create_sruid(namespace=DEFAULT_NAMESPACE, name=str(uuid.uuid4())) + self.id = create_sruid() # Raise an error if the service ID is some kind of falsey object that isn't `None`. elif not service_id: diff --git a/octue/cloud/service_id.py b/octue/cloud/service_id.py index dd041a38a..b94c45df6 100644 --- a/octue/cloud/service_id.py +++ b/octue/cloud/service_id.py @@ -1,13 +1,13 @@ import logging import os import re +import uuid import coolname import requests import octue.exceptions - logger = logging.getLogger(__name__) SERVICE_NAMESPACE_AND_NAME_PATTERN = r"([a-z0-9])+(-([a-z0-9])+)*" @@ -19,6 +19,8 @@ SRUID_PATTERN = rf"^{SERVICE_NAMESPACE_AND_NAME_PATTERN}\/{SERVICE_NAMESPACE_AND_NAME_PATTERN}:{REVISION_TAG_PATTERN}$" COMPILED_SRUID_PATTERN = re.compile(SRUID_PATTERN) +DEFAULT_NAMESPACE = "default" + def get_sruid_parts(service_configuration): """Get the namespace, name, and revision tag for the service from either the service environment variables or the @@ -61,16 +63,16 @@ def get_sruid_parts(service_configuration): return service_namespace, service_name, service_revision_tag -def create_sruid(namespace, name, revision_tag=None): - """Create and validate a service revision unique identifier (SRUID) from a namespace, name, and revision tag. If no - revision tag is given, a "cool name" revision tag is generated. +def create_sruid(namespace=DEFAULT_NAMESPACE, name=None, revision_tag=None): + """Create and validate a service revision unique identifier (SRUID) from a namespace, name, and revision tag. :param str namespace: the name of the group to which the service belongs - :param str name: the name of the service - :param str|None revision_tag: a tag that uniquely identifies a particular revision of the service + :param str|None name: the name of the service; if not provided, a UUID is generated + :param str|None revision_tag: a tag that uniquely identifies a particular revision of the service; if not provided, a "cool name" is generated :raise octue.exceptions.InvalidServiceID: if any of the namespace, name, or revision tag are invalid :return str: the valid SRUID comprising the namespace, name, and revision tag """ + name = name or str(uuid.uuid4()) revision_tag = revision_tag or coolname.generate_slug(2) validate_sruid(namespace=namespace, name=name, revision_tag=revision_tag) return f"{namespace}/{name}:{revision_tag}" diff --git a/tests/cloud/test_service_id.py b/tests/cloud/test_service_id.py index 18588f2b5..1aaf64688 100644 --- a/tests/cloud/test_service_id.py +++ b/tests/cloud/test_service_id.py @@ -7,7 +7,9 @@ import requests from octue.cloud.service_id import ( + DEFAULT_NAMESPACE, convert_service_id_to_pub_sub_form, + create_sruid, get_default_sruid, get_sruid_from_pub_sub_resource_name, get_sruid_parts, @@ -74,6 +76,34 @@ def test_with_revision_tag_environment_variable(self): self.assertEqual(revision_tag, "this-is-a-tag") +class TestCreateSRUID(unittest.TestCase): + def test_error_raised_for_invalid_arguments(self): + """Test that an error is raised when trying to create an SRUID from invalid components.""" + for namespace, name, revision_tag in ( + ("MY-NAMESPACE", "my-name", "my-tag"), + ("my-namespace", "MY-NAME", "my-tag"), + ("my-namespace", "my-name", "@"), + ): + with self.subTest(namespace=namespace, name=name, revision_tag=revision_tag): + with self.assertRaises(InvalidServiceID): + create_sruid(namespace, name, revision_tag) + + def test_default(self): + """Test that a valid SRUID is created when no arguments are provided and that a different SRUID is created each + time. + """ + sruid = create_sruid() + validate_sruid(sruid) + self.assertTrue(sruid.startswith(DEFAULT_NAMESPACE)) + self.assertNotEqual(sruid, create_sruid()) + + def test_with_arguments(self): + """Test that a valid SRUID is created from valid arguments.""" + sruid = create_sruid("my-namespace", "my-name", "my-tag") + validate_sruid(sruid) + self.assertEqual(sruid, "my-namespace/my-name:my-tag") + + class TestConvertServiceIDToPubSubForm(unittest.TestCase): def test_convert_service_id_to_pub_sub_form(self): """Test that service IDs containing organisations, revision tags, and the services namespace are all converted From de2b27294c8b2802c3389b516cf0e466c157fa1b Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Tue, 11 Feb 2025 18:13:14 +0000 Subject: [PATCH 098/125] REF: Simplify `octue question ask local` --- octue/cli.py | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/octue/cli.py b/octue/cli.py index a4a0e78c9..27f73b454 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -199,9 +199,8 @@ def local(input_values, input_manifest, attributes, service_config): This command is similar to running `octue service start` and asking the resulting local service revision a question via Pub/Sub. Instead of starting a local Pub/Sub service revision, however, no Pub/Sub subscription or subscriber is - created; the question is instead passed directly and to local the service revision without Pub/Sub being involved. - Everything after this runs the same, though, with any events emitted by the service revision emitted via Pub/Sub as - usual. + created; the question is instead passed directly to local the service revision without Pub/Sub being involved. + Everything after this runs the same, though, with the service revision emitting any events via Pub/Sub as usual. """ if input_values: input_values = json.loads(input_values, cls=OctueJSONDecoder) @@ -213,20 +212,17 @@ def local(input_values, input_manifest, attributes, service_config): if attributes: attributes = json.loads(attributes, cls=OctueJSONDecoder) - parent_sruid = None - child_sruid = None + question = make_question_event(input_values=input_values, input_manifest=input_manifest, attributes=attributes) else: - parent_sruid = "local/local:local" - service_namespace, service_name, service_revision_tag = get_sruid_parts(service_configuration) - child_sruid = create_sruid(namespace=service_namespace, name=service_name, revision_tag=service_revision_tag) - - question = make_question_event( - input_values=input_values, - input_manifest=input_manifest, - parent_sruid=parent_sruid, - child_sruid=child_sruid, - attributes=attributes, - ) + namespace, name, revision_tag = get_sruid_parts(service_configuration) + child_sruid = create_sruid(namespace=namespace, name=name, revision_tag=revision_tag) + + question = make_question_event( + input_values=input_values, + input_manifest=input_manifest, + parent_sruid=create_sruid(), + child_sruid=child_sruid, + ) backend_configuration_values = (app_configuration.configuration_values or {}).get("backend") From d3c944478a0585d82ced63f94124f5760b600311 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Tue, 11 Feb 2025 18:17:03 +0000 Subject: [PATCH 099/125] DOC: Improve documentation of `octue question ask remote` --- octue/cli.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/octue/cli.py b/octue/cli.py index 27f73b454..aefc0366e 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -103,14 +103,14 @@ def ask(): "--project-name", type=str, default=None, - help="If asking a remote question, the name of the Google Cloud project the service is deployed in. If not " - "provided, the project name is detected from the local Google application credentials if present.", + help="The name of the Google Cloud project the service is deployed in. If not provided, the project name is " + "detected from the local Google application credentials if present.", ) @click.option( "--asynchronous", is_flag=True, - help="If provided, ask the question and detach (the result and other events can be retrieved from the event store " - "later).", + help="If provided, ask the question and detach. The result and other events can be retrieved from the event store " + "later.", ) @click.option( "-c", @@ -124,9 +124,9 @@ def ask(): def remote(sruid, input_values, input_manifest, project_name, asynchronous, service_config): """Ask a question to a remote Octue Twined service. - SRUID should be a valid service revision unique identifier for an existing Octue Twined service + SRUID should be a valid service revision unique identifier for an existing Octue Twined service e.g. - e.g. octue question ask octue/example-service:1.0.3 + octue question ask remote your-org/example-service:1.2.0 """ try: service_configuration = ServiceConfiguration.from_file(service_config) From 3a3160a63513033275f11e738a4df28b8421958f Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Tue, 11 Feb 2025 18:36:06 +0000 Subject: [PATCH 100/125] REF: Tidy up `octue question events get` --- octue/cli.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/octue/cli.py b/octue/cli.py index aefc0366e..21108e272 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -240,7 +240,7 @@ def local(input_values, input_manifest, attributes, service_config): @question.group() def events(): - """Get and replay events from questions to Octue Twined services.""" + """Get and replay events from past and current questions.""" @events.command() @@ -253,12 +253,14 @@ def events(): @click.option( "--parent-question-uuid", type=str, - help="The UUID of a parent question to get the sub-question events for", + default=None, + help="The UUID of a parent question to get the sub-question events for.", ) @click.option( "--originator-question-uuid", type=str, - help="The UUID of an originator question get the full tree of events for", + default=None, + help="The UUID of an originator question get the full tree of events for.", ) @click.option( "-k", @@ -266,8 +268,7 @@ def events(): type=str, default=None, help="The kinds of event to get as a comma-separated list e.g. 'question,result'. If not provided, all event kinds " - "are returned. The valid kinds are " - f"{VALID_EVENT_KINDS!r}.", + f"are returned. The valid kinds are {VALID_EVENT_KINDS!r}.", ) @click.option( "-e", @@ -275,8 +276,7 @@ def events(): type=str, default=None, help="The kinds of event to exclude as a comma-separated list e.g. 'question,result'. If not provided, all event " - "kinds are returned. The valid kinds are " - f"{VALID_EVENT_KINDS!r}.", + f"kinds are returned. The valid kinds are {VALID_EVENT_KINDS!r}.", ) @click.option( "--include-backend-metadata", @@ -310,13 +310,11 @@ def get( limit, service_config, ): - """Get the raw events emitted during a question as JSON. One of the following must be set: - - --question-uuid + """Get the events emitted during a question as JSON. One of the following must be set: - --parent-question-uuid - - --originator-question-uuid + --question-uuid\n + --parent-question-uuid\n + --originator-question-uuid\n """ if kinds: kinds = kinds.split(",") From 9baf194ea68534c7af5c5291b69876f842318473 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Tue, 11 Feb 2025 18:39:13 +0000 Subject: [PATCH 101/125] REF: Tidy up `octue question events replay` --- octue/cli.py | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/octue/cli.py b/octue/cli.py index 21108e272..3d9db3d0e 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -348,12 +348,12 @@ def get( @click.option( "--parent-question-uuid", type=str, - help="The UUID of a parent question to get the sub-question events for", + help="The UUID of a parent question to get the sub-question events for.", ) @click.option( "--originator-question-uuid", type=str, - help="The UUID of an originator question get the full tree of events for", + help="The UUID of an originator question get the full tree of events for.", ) @click.option( "-k", @@ -361,8 +361,7 @@ def get( type=str, default=None, help="The kinds of event to get as a comma-separated list e.g. 'question,result'. If not provided, all event kinds " - "are returned. The valid kinds are " - f"{VALID_EVENT_KINDS!r}.", + f"are returned. The valid kinds are {VALID_EVENT_KINDS!r}.", ) @click.option( "-e", @@ -370,8 +369,7 @@ def get( type=str, default=None, help="The kinds of event to exclude as a comma-separated list e.g. 'question,result'. If not provided, all event " - "kinds are returned. The valid kinds are " - f"{VALID_EVENT_KINDS!r}.", + f"kinds are returned. The valid kinds are {VALID_EVENT_KINDS!r}.", ) @click.option( "-l", @@ -391,10 +389,10 @@ def get( "is used.", ) @click.option( - "--include-service-metadata-in-logs", + "--include-service-metadata", is_flag=True, help="Include the SRUIDs and question UUIDs of the service revisions involved in the question at the start of each " - "log message.", + "log message. This is useful when a child asks its own sub-questions.", ) @click.option( "--exclude-logs-containing", @@ -406,7 +404,8 @@ def get( "-r", "--only-handle-result", is_flag=True, - help="Skip non-result events and only handle the 'result' event if present.", + help="Skip all events apart from the 'result' event (if there is one). If providing this option, the " + "`--include-kinds` and `--exclude-kinds` options are ignored. This option can speed up event handling.", ) @click.option( "--validate-events", @@ -421,18 +420,17 @@ def replay( exclude_kinds, limit, service_config, - include_service_metadata_in_logs, + include_service_metadata, exclude_logs_containing, only_handle_result, validate_events, ): - """Replay a question's events, returning the result as JSON if there is one. One of the following must be set: - - --question-uuid + """Replay a question's events, returning the result as JSON at the end if there is one. One of the following must be + set: - --parent-question-uuid - - --originator-question-uuid + --question-uuid\n + --parent-question-uuid\n + --originator-question-uuid\n """ if kinds: kinds = kinds.split(",") @@ -453,7 +451,7 @@ def replay( ) replayer = EventReplayer( - include_service_metadata_in_logs=include_service_metadata_in_logs, + include_service_metadata_in_logs=include_service_metadata, exclude_logs_containing=exclude_logs_containing, only_handle_result=only_handle_result, validate_events=validate_events, From a1e364b611e194b319e18fb749010a39b7f6033a Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Tue, 11 Feb 2025 18:39:46 +0000 Subject: [PATCH 102/125] ENH: Avoid getting non-result events from event store if unnecessary --- octue/cli.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/octue/cli.py b/octue/cli.py index 3d9db3d0e..09f334c9b 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -432,11 +432,15 @@ def replay( --parent-question-uuid\n --originator-question-uuid\n """ - if kinds: - kinds = kinds.split(",") + if only_handle_result: + kinds = ["result"] + exclude_kinds = None + else: + if kinds: + kinds = kinds.split(",") - if exclude_kinds: - exclude_kinds = exclude_kinds.split(",") + if exclude_kinds: + exclude_kinds = exclude_kinds.split(",") service_configuration = ServiceConfiguration.from_file(path=service_config) From ea32f31db84638cd3688c902ac8ef67d4fb44b4b Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Tue, 11 Feb 2025 18:40:31 +0000 Subject: [PATCH 103/125] ENH: Warn if no result is returned by `octue question events replay` --- octue/cli.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/octue/cli.py b/octue/cli.py index 09f334c9b..283d579b4 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -465,6 +465,9 @@ def replay( if result: click.echo(result) + return + + logger.warning("No result was found for this question.") @question.command() From 9013444bfd8b75e6983cd0251578643300e950d2 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Tue, 11 Feb 2025 18:41:00 +0000 Subject: [PATCH 104/125] DOC: Improve docstring skipci --- octue/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/octue/cli.py b/octue/cli.py index 283d579b4..02bef7a97 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -560,7 +560,7 @@ def diagnostics(cloud_path, local_path, download_datasets): "is used.", ) def cancel(question_uuid, project_name, service_config): - """Cancel a question running on a Twined service. + """Cancel a question running on an Octue Twined service. QUESTION_UUID: The question UUID of a running question """ From 8b6c8d75b0d09f84dee9913331f0576b123cffbb Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Wed, 12 Feb 2025 12:07:45 +0000 Subject: [PATCH 105/125] FIX: Pass service/app config correctly in `octue question ask local` --- octue/cli.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/octue/cli.py b/octue/cli.py index 02bef7a97..8c1086e83 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -234,7 +234,13 @@ def local(input_values, input_manifest, attributes, service_config): _, project_name = auth.default() backend = service_backends.get_backend()(project_name=project_name) - answer = answer_question(question=question, project_name=backend.project_name, service_configuration=service_config) + answer = answer_question( + question=question, + project_name=backend.project_name, + service_configuration=service_configuration, + app_configuration=app_configuration, + ) + click.echo(answer) From ffbcc553dabaca9458ffa9f7d129434370f70dde Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Wed, 12 Feb 2025 12:11:06 +0000 Subject: [PATCH 106/125] ENH: Make service/app config mandatory in `answer_question` --- octue/cloud/events/answer_question.py | 10 ++-- tests/cloud/events/test_answer_question.py | 62 ++++------------------ 2 files changed, 13 insertions(+), 59 deletions(-) diff --git a/octue/cloud/events/answer_question.py b/octue/cloud/events/answer_question.py index 571e3262e..18ea83630 100644 --- a/octue/cloud/events/answer_question.py +++ b/octue/cloud/events/answer_question.py @@ -2,7 +2,6 @@ from octue.cloud.pub_sub.service import Service from octue.cloud.service_id import create_sruid, get_sruid_parts -from octue.configuration import load_service_and_app_configuration from octue.resources.service_backends import GCPPubSubBackend from octue.runner import Runner from octue.utils.objects import get_nested_attribute @@ -10,18 +9,15 @@ logger = logging.getLogger(__name__) -def answer_question(question, project_name, service_configuration=None, app_configuration=None): +def answer_question(question, project_name, service_configuration, app_configuration): """Answer a question received by a service. :param dict question: a question event and its attributes :param str project_name: the name of the project the service is running on - :param octue.configuration.ServiceConfiguration|None service_configuration: - :param octue.configuration.AppConfiguration|None app_configuration: + :param octue.configuration.ServiceConfiguration service_configuration: + :param octue.configuration.AppConfiguration app_configuration: :return dict: the result event """ - if not service_configuration: - service_configuration, app_configuration = load_service_and_app_configuration() - service_namespace, service_name, service_revision_tag = get_sruid_parts(service_configuration) service_sruid = create_sruid( diff --git a/tests/cloud/events/test_answer_question.py b/tests/cloud/events/test_answer_question.py index eb20dd9be..dd0e52c31 100644 --- a/tests/cloud/events/test_answer_question.py +++ b/tests/cloud/events/test_answer_question.py @@ -7,63 +7,14 @@ from octue.cloud.emulators._pub_sub import MockTopic from octue.cloud.events.answer_question import answer_question +from octue.configuration import load_service_and_app_configuration from octue.utils.patches import MultiPatcher from tests.mocks import MockOpen class TestAnswerQuestion(TestCase): - def test_with_no_app_configuration_file(self): - """Test that the `answer_question` function uses the default service and app configuration values when the - minimal service configuration is provided with no path to an app configuration file. - """ - with MultiPatcher( - patches=[ - patch( - "octue.configuration.open", - mock.mock_open( - read_data=yaml.dump({"services": [{"name": "test-service", "namespace": "testing"}]}) - ), - ), - patch("octue.cloud.pub_sub.service.Topic", new=MockTopic), - patch("octue.cloud.events.answer_question.Service"), - patch.dict(os.environ, {"OCTUE_SERVICE_REVISION_TAG": "blah"}), - ] - ): - with patch("octue.cloud.events.answer_question.Runner.from_configuration") as mock_constructor: - answer_question( - question={ - "data": {}, - "attributes": { - "question_uuid": "8c859f87-b594-4297-883f-cd1c7718ef29", - "parent_question_uuid": "8c859f87-b594-4297-883f-cd1c7718ef29", - "originator_question_uuid": "8c859f87-b594-4297-883f-cd1c7718ef29", - "parent": "some/originator:service", - "originator": "some/originator:service", - "retry_count": 0, - }, - }, - project_name="a-project-name", - ) - - self.assertTrue( - mock_constructor.call_args.kwargs["service_configuration"].app_source_path.endswith("octue-sdk-python") - ) - self.assertTrue(mock_constructor.call_args.kwargs["service_configuration"].twine_path.endswith("twine.json")) - self.assertIsNone(mock_constructor.call_args.kwargs["service_configuration"].diagnostics_cloud_path) - self.assertIsNone(mock_constructor.call_args.kwargs["service_configuration"].service_registries) - - self.assertIsNone(mock_constructor.call_args.kwargs["app_configuration"].configuration_values) - self.assertIsNone(mock_constructor.call_args.kwargs["app_configuration"].configuration_manifest) - self.assertIsNone(mock_constructor.call_args.kwargs["app_configuration"].children) - self.assertIsNone(mock_constructor.call_args.kwargs["app_configuration"].output_location) - - self.assertEqual(mock_constructor.call_args.kwargs["project_name"], "a-project-name") - self.assertEqual(mock_constructor.call_args.kwargs["service_id"], "testing/test-service:blah") - - def test_with_service_configuration_file_and_app_configuration_file(self): - """Test that the `answer_question` function uses the values in the service and app configuration files if they - are provided. - """ + def test_answer_question(self): + """Test that the `answer_question` function uses the values in the service and app configurations correctly.""" class MockOpenForConfigurationFiles(MockOpen): path_to_contents_mapping = { @@ -92,6 +43,8 @@ class MockOpenForConfigurationFiles(MockOpen): patch.dict(os.environ, {"OCTUE_SERVICE_REVISION_TAG": "blah"}), ] ): + service_config, app_config = load_service_and_app_configuration() + answer_question( question={ "data": {}, @@ -105,20 +58,25 @@ class MockOpenForConfigurationFiles(MockOpen): }, }, project_name="a-project-name", + service_configuration=service_config, + app_configuration=app_config, ) self.assertTrue( mock_constructor.call_args.kwargs["service_configuration"].app_source_path.endswith("path/to/app_dir") ) + self.assertTrue( mock_constructor.call_args.kwargs["service_configuration"].twine_path.endswith("path/to/twine.json") ) + self.assertIsNone(mock_constructor.call_args.kwargs["service_configuration"].diagnostics_cloud_path) self.assertIsNone(mock_constructor.call_args.kwargs["service_configuration"].service_registries) self.assertEqual( mock_constructor.call_args.kwargs["app_configuration"].configuration_values, {"hello": "configuration"} ) + self.assertIsNone(mock_constructor.call_args.kwargs["app_configuration"].configuration_manifest) self.assertIsNone(mock_constructor.call_args.kwargs["app_configuration"].children) self.assertIsNone(mock_constructor.call_args.kwargs["app_configuration"].output_location) From d2f4b4f0b18a62d53d259ec5e542e31b72837dbf Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Wed, 12 Feb 2025 12:20:39 +0000 Subject: [PATCH 107/125] DOC: Remove outdated docstring param --- octue/cloud/pub_sub/service.py | 1 - 1 file changed, 1 deletion(-) diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index 836bd9bce..53f62446d 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -731,7 +731,6 @@ def _send_heartbeat( :param str parent: the SRUID of the parent that asked the question this event is related to :param str originator: the SRUID of the service revision that triggered all ancestor questions of this question :param int retry_count: the retry count of the question (this is zero if it's the first attempt at the question) - :param int|float start_time: the `time.perf_counter` time that the analysis was started [s] :param float timeout: time in seconds after which to give up sending :return None: """ From 0f923bedf8e0e886ad95881eeed5c3bd2424c76f Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Wed, 12 Feb 2025 12:55:14 +0000 Subject: [PATCH 108/125] FIX: Pass CLI JSON output to stdout as valid JSON --- octue/cli.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/octue/cli.py b/octue/cli.py index 8c1086e83..66c0c2c23 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -159,7 +159,7 @@ def remote(sruid, input_values, input_manifest, project_name, asynchronous, serv click.echo(question_uuid) return - click.echo(answer) + click.echo(json.dumps(answer)) @ask.command() @@ -241,7 +241,7 @@ def local(input_values, input_manifest, attributes, service_config): app_configuration=app_configuration, ) - click.echo(answer) + click.echo(json.dumps(answer)) @question.group() @@ -341,7 +341,7 @@ def get( limit=limit, ) - click.echo(events) + click.echo(json.dumps(events)) @events.command() @@ -470,7 +470,7 @@ def replay( result = replayer.handle_events(events) if result: - click.echo(result) + click.echo(json.dumps(result)) return logger.warning("No result was found for this question.") From adf2b2efa473aa7ed6879b35c002dfb5f4dd25a2 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Wed, 12 Feb 2025 13:08:23 +0000 Subject: [PATCH 109/125] TST: Test `octue question ask remote` command skipci --- .../input/values.json | 3 - tests/test_cli.py | 167 ++++++++---------- 2 files changed, 76 insertions(+), 94 deletions(-) delete mode 100644 tests/data/data_dir_with_no_manifests/input/values.json diff --git a/tests/data/data_dir_with_no_manifests/input/values.json b/tests/data/data_dir_with_no_manifests/input/values.json deleted file mode 100644 index 58275dbd0..000000000 --- a/tests/data/data_dir_with_no_manifests/input/values.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "height": 3 -} diff --git a/tests/test_cli.py b/tests/test_cli.py index a3d19c0cf..be6f75812 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -35,128 +35,113 @@ def test_help(self): self.assertEqual(help_result.output, h_result.output) -class TestRunCommand(BaseTestCase): +class TestQuestionAskRemoteCommand(BaseTestCase): MOCK_CONFIGURATIONS = ( - ServiceConfiguration( - name="test-app", - namespace="testing", - app_source_path=os.path.join(TESTS_DIR, "test_app_modules", "app_module"), - twine_path=TWINE_FILE_PATH, - app_configuration_path="blah.json", - ), + ServiceConfiguration(name="test-app", namespace="testing"), AppConfiguration(configuration_values={"n_iterations": 5}), ) - def test_run(self): - """Test that the `run` CLI command runs the given service and outputs the output values.""" - with mock.patch("octue.cli.load_service_and_app_configuration", return_value=self.MOCK_CONFIGURATIONS): - result = CliRunner().invoke( - octue_cli, - [ - "run", - f"--input-dir={os.path.join(TESTS_DIR, 'data', 'data_dir_with_no_manifests', 'input')}", - ], - ) - - self.assertIn(json.dumps({"width": 3}), result.output) + SRUID = "my-org/my-service:1.0.0" + QUESTION_UUID = "81f35b28-068b-4314-9eeb-e55e60d0fe8a" - def test_run_with_output_values_file(self): - """Test that the `run` CLI command runs the given service and stores the output values in a file if the `-o` - option is given. - """ - with tempfile.NamedTemporaryFile(delete=False) as temporary_file: - with mock.patch("octue.cli.load_service_and_app_configuration", return_value=self.MOCK_CONFIGURATIONS): + def test_with_input_values(self): + """Test that the `octue question ask remote` CLI command works with just input values.""" + with mock.patch("octue.cli.ServiceConfiguration.from_file", return_value=self.MOCK_CONFIGURATIONS[0]): + with mock.patch("octue.cli.Child.ask", return_value=({"some": "data"}, self.QUESTION_UUID)) as mock_ask: result = CliRunner().invoke( octue_cli, [ - "run", - f"--input-dir={os.path.join(TESTS_DIR, 'data', 'data_dir_with_no_manifests', 'input')}", - "-o", - temporary_file.name, + "question", + "ask", + "remote", + self.SRUID, + '--input-values={"height": 3}', ], ) - with open(temporary_file.name) as f: - self.assertEqual(json.load(f), {"width": 3}) - - self.assertIn(json.dumps({"width": 3}), result.output) - - def test_run_with_output_manifest(self): - """Test that the `run` CLI command runs the given service and stores the output manifest in a file.""" - with tempfile.NamedTemporaryFile("w", delete=False, suffix=".json") as temporary_twine: - temporary_twine.write( - json.dumps({"input_values_schema": {}, "output_manifest": {"datasets": {}}, "output_values_schema": {}}) - ) + mock_ask.assert_called_with(input_values={"height": 3}, input_manifest=None, asynchronous=False) + self.assertIn(json.dumps({"some": "data"}), result.output) - mock_configurations = ( - ServiceConfiguration( - name="test-app", - namespace="testing", - app_source_path=os.path.join(TESTS_DIR, "test_app_modules", "app_module_with_output_manifest"), - twine_path=temporary_twine.name, - ), - AppConfiguration(), - ) + def test_with_input_manifest(self): + """Test that the `octue question ask remote` CLI command works with just an input manifest.""" + input_manifest = self.create_valid_manifest() - with tempfile.NamedTemporaryFile(delete=False) as temporary_manifest: - with mock.patch("octue.cli.load_service_and_app_configuration", return_value=mock_configurations): + with mock.patch("octue.cli.ServiceConfiguration.from_file", return_value=self.MOCK_CONFIGURATIONS[0]): + with mock.patch("octue.cli.Child.ask", return_value=({"some": "data"}, self.QUESTION_UUID)) as mock_ask: result = CliRunner().invoke( octue_cli, [ - "run", - f"--input-dir={os.path.join(TESTS_DIR, 'data', 'data_dir_with_no_manifests', 'input')}", - f"--output-manifest-file={temporary_manifest.name}", + "question", + "ask", + "remote", + self.SRUID, + f"--input-manifest={input_manifest.serialise()}", ], ) - with open(temporary_manifest.name) as f: - self.assertIn("datasets", json.load(f)) - - self.assertIn(json.dumps({"width": 3}), result.output) - - def test_run_with_monitor_messages_sent_to_file(self): - """Test that, when the `--monitor-messages-file` is provided, any monitor messages are written to it.""" - mock_configurations = ( - ServiceConfiguration( - name="test-app", - namespace="testing", - app_source_path=os.path.join(TESTS_DIR, "test_app_modules", "app_with_monitor_message"), - twine_path=TWINE_FILE_PATH, - app_configuration_path="blah.json", - ), - AppConfiguration(configuration_values={"n_iterations": 5}), - ) + self.assertEqual(mock_ask.call_args.kwargs["input_manifest"].id, input_manifest.id) + self.assertIn(json.dumps({"some": "data"}), result.output) - with tempfile.NamedTemporaryFile(delete=False) as monitor_messages_file: - with mock.patch("octue.cli.load_service_and_app_configuration", return_value=mock_configurations): + def test_with_input_values_and_manifest(self): + """Test that the `octue question ask remote` CLI command works with input values and input manifest.""" + input_values = {"height": 3} + input_manifest = self.create_valid_manifest() + + with mock.patch("octue.cli.ServiceConfiguration.from_file", return_value=self.MOCK_CONFIGURATIONS[0]): + with mock.patch("octue.cli.Child.ask", return_value=({"some": "data"}, self.QUESTION_UUID)) as mock_ask: result = CliRunner().invoke( octue_cli, [ - "run", - f"--input-dir={os.path.join(TESTS_DIR, 'data', 'data_dir_with_no_manifests', 'input')}", - f"--monitor-messages-file={monitor_messages_file.name}", + "question", + "ask", + "remote", + self.SRUID, + f"--input-values={json.dumps(input_values)}", + f"--input-manifest={input_manifest.serialise()}", ], ) - with open(monitor_messages_file.name) as f: - self.assertEqual(json.load(f), [{"status": "hello"}]) - - self.assertIn(json.dumps({"width": 3}), result.output) + self.assertEqual(mock_ask.call_args.kwargs["input_values"], input_values) + self.assertEqual(mock_ask.call_args.kwargs["input_manifest"].id, input_manifest.id) + self.assertIn(json.dumps({"some": "data"}), result.output) - def test_remote_logger_uri_can_be_set(self): - """Test that remote logger URI can be set via the CLI and that this is logged locally.""" - with mock.patch("octue.cli.load_service_and_app_configuration", return_value=self.MOCK_CONFIGURATIONS): - with mock.patch("logging.StreamHandler.emit") as mock_local_logger_emit: - CliRunner().invoke( + def test_asynchronous(self): + """Test that the `octue question ask remote` CLI command works with the `--asynchronous` option and returns the + question UUID. + """ + with mock.patch("octue.cli.ServiceConfiguration.from_file", return_value=self.MOCK_CONFIGURATIONS[0]): + with mock.patch("octue.cli.Child.ask", return_value=({"some": "data"}, self.QUESTION_UUID)) as mock_ask: + result = CliRunner().invoke( octue_cli, [ - "--logger-uri=wss://0.0.0.1:3000", - "run", - f"--input-dir={os.path.join(TESTS_DIR, 'data', 'data_dir_with_no_manifests', 'input')}", + "question", + "ask", + "remote", + self.SRUID, + '--input-values={"height": 3}', + "--asynchronous", ], ) - mock_local_logger_emit.assert_called() + mock_ask.assert_called_with(input_values={"height": 3}, input_manifest=None, asynchronous=True) + self.assertIn(self.QUESTION_UUID, result.output) + + def test_with_no_service_configuration(self): + """Test that the command works when no service configuration is provided.""" + with mock.patch("octue.cli.Child.ask", return_value=({"some": "data"}, self.QUESTION_UUID)) as mock_ask: + result = CliRunner().invoke( + octue_cli, + [ + "question", + "ask", + "remote", + self.SRUID, + '--input-values={"height": 3}', + ], + ) + + mock_ask.assert_called_with(input_values={"height": 3}, input_manifest=None, asynchronous=False) + self.assertIn(json.dumps({"some": "data"}), result.output) class TestStartCommand(BaseTestCase): @@ -244,7 +229,7 @@ def test_start_command_with_revision_tag_override_when_revision_tag_environment_ self.assertEqual(result.exit_code, 0) -class TestGetDiagnosticsCommand(BaseTestCase): +class TestQuestionDiagnosticsCommand(BaseTestCase): DIAGNOSTICS_CLOUD_PATH = storage.path.generate_gs_path(TEST_BUCKET_NAME, "diagnostics") ANALYSIS_ID = "dc1f09ca-7037-484f-a394-8bd04866f924" From 7c1f9a647dd836c4b936fa78f88289716b0a9030 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Wed, 12 Feb 2025 13:57:41 +0000 Subject: [PATCH 110/125] REF: Simplify making an originator question --- octue/cli.py | 6 +++--- octue/cloud/events/utils.py | 33 +++++++++++++++++---------------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/octue/cli.py b/octue/cli.py index 66c0c2c23..84a04f6e4 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -215,13 +215,13 @@ def local(input_values, input_manifest, attributes, service_config): question = make_question_event(input_values=input_values, input_manifest=input_manifest, attributes=attributes) else: namespace, name, revision_tag = get_sruid_parts(service_configuration) - child_sruid = create_sruid(namespace=namespace, name=name, revision_tag=revision_tag) + recipient = create_sruid(namespace=namespace, name=name, revision_tag=revision_tag) question = make_question_event( input_values=input_values, input_manifest=input_manifest, - parent_sruid=create_sruid(), - child_sruid=child_sruid, + sender=create_sruid(), + recipient=recipient, ) backend_configuration_values = (app_configuration.configuration_values or {}).get("backend") diff --git a/octue/cloud/events/utils.py b/octue/cloud/events/utils.py index 85c4ae15f..3b95981f1 100644 --- a/octue/cloud/events/utils.py +++ b/octue/cloud/events/utils.py @@ -8,8 +8,8 @@ def make_question_event( input_values, input_manifest, - parent_sruid=None, - child_sruid=None, + sender=None, + recipient=None, question_uuid=None, attributes=None, ): @@ -17,10 +17,10 @@ def make_question_event( :param dict input_values: :param octue.resources.manifest.Manifest input_manifest: - :param str parent_sruid: - :param str child_sruid: - :param str question_uuid: - :param dict attributes: + :param str|None sender: + :param str|None recipient: + :param str|None question_uuid: + :param dict|None attributes: :return dict: """ if not attributes: @@ -28,12 +28,8 @@ def make_question_event( attributes = make_attributes( question_uuid=question_uuid, - parent_question_uuid=question_uuid, - originator_question_uuid=question_uuid, - parent=parent_sruid, - originator=parent_sruid, - sender=parent_sruid, - recipient=child_sruid, + sender=sender, + recipient=recipient, forward_logs=True, save_diagnostics="SAVE_DIAGNOSTICS_ON", sender_type="PARENT", @@ -46,14 +42,14 @@ def make_question_event( def make_attributes( - parent_question_uuid, - originator_question_uuid, - parent, - originator, sender, sender_type, recipient, question_uuid=None, + parent_question_uuid=None, + originator_question_uuid=None, + parent=None, + originator=None, retry_count=0, forward_logs=None, save_diagnostics=None, @@ -61,6 +57,11 @@ def make_attributes( memory=None, ephemeral_storage=None, ): + # If the originator isn't provided, assume that this service revision is the originator. + originator_question_uuid = originator_question_uuid or question_uuid + parent = parent or sender + originator = originator or sender + attributes = { "uuid": str(uuid.uuid4()), "datetime": datetime.datetime.now(tz=datetime.timezone.utc).isoformat(), From 6019f36aba1874ef4de23bffd9f59e51b49c44e6 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Wed, 12 Feb 2025 14:15:02 +0000 Subject: [PATCH 111/125] TST: Test `octue question ask local` command skipci --- tests/test_cli.py | 195 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 186 insertions(+), 9 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index be6f75812..7e486362c 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -43,11 +43,12 @@ class TestQuestionAskRemoteCommand(BaseTestCase): SRUID = "my-org/my-service:1.0.0" QUESTION_UUID = "81f35b28-068b-4314-9eeb-e55e60d0fe8a" + RESULT = {"output_values": {"some": "data"}} def test_with_input_values(self): """Test that the `octue question ask remote` CLI command works with just input values.""" with mock.patch("octue.cli.ServiceConfiguration.from_file", return_value=self.MOCK_CONFIGURATIONS[0]): - with mock.patch("octue.cli.Child.ask", return_value=({"some": "data"}, self.QUESTION_UUID)) as mock_ask: + with mock.patch("octue.cli.Child.ask", return_value=(self.RESULT, self.QUESTION_UUID)) as mock_ask: result = CliRunner().invoke( octue_cli, [ @@ -60,14 +61,14 @@ def test_with_input_values(self): ) mock_ask.assert_called_with(input_values={"height": 3}, input_manifest=None, asynchronous=False) - self.assertIn(json.dumps({"some": "data"}), result.output) + self.assertIn(json.dumps(self.RESULT), result.output) def test_with_input_manifest(self): """Test that the `octue question ask remote` CLI command works with just an input manifest.""" input_manifest = self.create_valid_manifest() with mock.patch("octue.cli.ServiceConfiguration.from_file", return_value=self.MOCK_CONFIGURATIONS[0]): - with mock.patch("octue.cli.Child.ask", return_value=({"some": "data"}, self.QUESTION_UUID)) as mock_ask: + with mock.patch("octue.cli.Child.ask", return_value=(self.RESULT, self.QUESTION_UUID)) as mock_ask: result = CliRunner().invoke( octue_cli, [ @@ -80,7 +81,7 @@ def test_with_input_manifest(self): ) self.assertEqual(mock_ask.call_args.kwargs["input_manifest"].id, input_manifest.id) - self.assertIn(json.dumps({"some": "data"}), result.output) + self.assertIn(json.dumps(self.RESULT), result.output) def test_with_input_values_and_manifest(self): """Test that the `octue question ask remote` CLI command works with input values and input manifest.""" @@ -88,7 +89,7 @@ def test_with_input_values_and_manifest(self): input_manifest = self.create_valid_manifest() with mock.patch("octue.cli.ServiceConfiguration.from_file", return_value=self.MOCK_CONFIGURATIONS[0]): - with mock.patch("octue.cli.Child.ask", return_value=({"some": "data"}, self.QUESTION_UUID)) as mock_ask: + with mock.patch("octue.cli.Child.ask", return_value=(self.RESULT, self.QUESTION_UUID)) as mock_ask: result = CliRunner().invoke( octue_cli, [ @@ -103,14 +104,14 @@ def test_with_input_values_and_manifest(self): self.assertEqual(mock_ask.call_args.kwargs["input_values"], input_values) self.assertEqual(mock_ask.call_args.kwargs["input_manifest"].id, input_manifest.id) - self.assertIn(json.dumps({"some": "data"}), result.output) + self.assertIn(json.dumps(self.RESULT), result.output) def test_asynchronous(self): """Test that the `octue question ask remote` CLI command works with the `--asynchronous` option and returns the question UUID. """ with mock.patch("octue.cli.ServiceConfiguration.from_file", return_value=self.MOCK_CONFIGURATIONS[0]): - with mock.patch("octue.cli.Child.ask", return_value=({"some": "data"}, self.QUESTION_UUID)) as mock_ask: + with mock.patch("octue.cli.Child.ask", return_value=(self.RESULT, self.QUESTION_UUID)) as mock_ask: result = CliRunner().invoke( octue_cli, [ @@ -128,7 +129,7 @@ def test_asynchronous(self): def test_with_no_service_configuration(self): """Test that the command works when no service configuration is provided.""" - with mock.patch("octue.cli.Child.ask", return_value=({"some": "data"}, self.QUESTION_UUID)) as mock_ask: + with mock.patch("octue.cli.Child.ask", return_value=(self.RESULT, self.QUESTION_UUID)) as mock_ask: result = CliRunner().invoke( octue_cli, [ @@ -141,7 +142,183 @@ def test_with_no_service_configuration(self): ) mock_ask.assert_called_with(input_values={"height": 3}, input_manifest=None, asynchronous=False) - self.assertIn(json.dumps({"some": "data"}), result.output) + self.assertIn(json.dumps(self.RESULT), result.output) + + +class TestQuestionAskLocalCommand(BaseTestCase): + MOCK_CONFIGURATIONS = ( + ServiceConfiguration(name="test-app", namespace="testing"), + AppConfiguration(configuration_values={"n_iterations": 5}), + ) + + RESULT = {"output_values": {"some": "data"}} + + def test_with_input_values(self): + """Test that the `octue question ask local` CLI command works with just input values and sends an originator + question. + """ + with mock.patch("octue.cli.load_service_and_app_configuration", return_value=self.MOCK_CONFIGURATIONS): + with mock.patch("octue.cli.answer_question", return_value=self.RESULT) as mock_answer_question: + result = CliRunner().invoke( + octue_cli, + [ + "question", + "ask", + "local", + '--input-values={"height": 3}', + ], + ) + + # Check service and app configuration. + mock_answer_question_kwargs = mock_answer_question.call_args.kwargs + self.assertEqual(mock_answer_question_kwargs["service_configuration"].namespace, "testing") + self.assertEqual(mock_answer_question_kwargs["service_configuration"].name, "test-app") + self.assertEqual(mock_answer_question_kwargs["app_configuration"].configuration_values, {"n_iterations": 5}) + + # Check question event. + question = mock_answer_question_kwargs["question"] + self.assertEqual(question["event"], {"kind": "question", "input_values": {"height": 3}}) + + # Check question attributes. + self.assertTrue(question["attributes"]["recipient"].startswith("testing/test-app")) + # The question should be an originator question. + self.assertEqual(question["attributes"]["question_uuid"], question["attributes"]["originator_question_uuid"]) + self.assertEqual(question["attributes"]["parent"], question["attributes"]["originator"]) + self.assertEqual(question["attributes"]["parent"], question["attributes"]["sender"]) + self.assertIsNone(question["attributes"]["parent_question_uuid"]) + + # Check the result is in the output. + self.assertIn(json.dumps(self.RESULT), result.output) + + def test_with_input_manifest(self): + """Test that the `octue question ask local` CLI command works with just an input manifest and sends an + originator question. + """ + input_manifest = self.create_valid_manifest() + with mock.patch("octue.cli.load_service_and_app_configuration", return_value=self.MOCK_CONFIGURATIONS): + with mock.patch("octue.cli.answer_question", return_value=self.RESULT) as mock_answer_question: + result = CliRunner().invoke( + octue_cli, + [ + "question", + "ask", + "local", + f"--input-manifest={input_manifest.serialise()}", + ], + ) + + # Check service and app configuration. + mock_answer_question_kwargs = mock_answer_question.call_args.kwargs + self.assertEqual(mock_answer_question_kwargs["service_configuration"].namespace, "testing") + self.assertEqual(mock_answer_question_kwargs["service_configuration"].name, "test-app") + self.assertEqual(mock_answer_question_kwargs["app_configuration"].configuration_values, {"n_iterations": 5}) + + # Check question event. + question = mock_answer_question_kwargs["question"] + self.assertEqual(question["event"]["input_manifest"].id, input_manifest.id) + + # Check question attributes. + self.assertTrue(question["attributes"]["recipient"].startswith("testing/test-app")) + # The question should be an originator question. + self.assertEqual(question["attributes"]["question_uuid"], question["attributes"]["originator_question_uuid"]) + self.assertEqual(question["attributes"]["parent"], question["attributes"]["originator"]) + self.assertEqual(question["attributes"]["parent"], question["attributes"]["sender"]) + self.assertIsNone(question["attributes"]["parent_question_uuid"]) + + # Check the result is in the output. + self.assertIn(json.dumps(self.RESULT), result.output) + + def test_with_input_values_and_manifest(self): + """Test that the `octue question ask local` CLI command works with input values and input manifest and sends an + originator question.""" + input_values = {"height": 3} + input_manifest = self.create_valid_manifest() + + with mock.patch("octue.cli.load_service_and_app_configuration", return_value=self.MOCK_CONFIGURATIONS): + with mock.patch("octue.cli.answer_question", return_value=self.RESULT) as mock_answer_question: + result = CliRunner().invoke( + octue_cli, + [ + "question", + "ask", + "local", + f"--input-values={json.dumps(input_values)}", + f"--input-manifest={input_manifest.serialise()}", + ], + ) + + # Check service and app configuration. + mock_answer_question_kwargs = mock_answer_question.call_args.kwargs + self.assertEqual(mock_answer_question_kwargs["service_configuration"].namespace, "testing") + self.assertEqual(mock_answer_question_kwargs["service_configuration"].name, "test-app") + self.assertEqual(mock_answer_question_kwargs["app_configuration"].configuration_values, {"n_iterations": 5}) + + # Check question event. + question = mock_answer_question_kwargs["question"] + self.assertEqual(question["event"]["input_values"], input_values) + self.assertEqual(question["event"]["input_manifest"].id, input_manifest.id) + + # Check question attributes. + self.assertTrue(question["attributes"]["recipient"].startswith("testing/test-app")) + # The question should be an originator question. + self.assertEqual(question["attributes"]["question_uuid"], question["attributes"]["originator_question_uuid"]) + self.assertEqual(question["attributes"]["parent"], question["attributes"]["originator"]) + self.assertEqual(question["attributes"]["parent"], question["attributes"]["sender"]) + self.assertIsNone(question["attributes"]["parent_question_uuid"]) + + # Check the result is in the output. + self.assertIn(json.dumps(self.RESULT), result.output) + + def test_with_attributes(self): + """Test that the `octue question ask remote` CLI command can be passed question attributes which are passed + along to the answering `Service` instance. + """ + original_attributes = { + "datetime": "2024-04-11T10:46:48.236064", + "uuid": "a9de11b1-e88f-43fa-b3a4-40a590c3443f", + "question_uuid": "d45c7e99-d610-413b-8130-dd6eef46dda6", + "parent_question_uuid": "5776ad74-52a6-46f7-a526-90421d91b8b2", + "originator_question_uuid": "86dc55b2-4282-42bd-92d0-bd4991ae7356", + "parent": "octue/the-parent:1.0.0", + "originator": "octue/the-originator:1.0.5", + "sender": "octue/the-sender:2.0.0", + "sender_type": "CHILD", + "sender_sdk_version": "0.51.0", + "recipient": "octue/the-recipient:0.3.2", + "retry_count": 1, + "forward_logs": True, + "save_diagnostics": "SAVE_DIAGNOSTICS_OFF", + "cpus": 3, + "memory": "10Gi", + "ephemeral_storage": "500Mi", + } + + with mock.patch("octue.cli.load_service_and_app_configuration", return_value=self.MOCK_CONFIGURATIONS): + with mock.patch("octue.cli.answer_question", return_value=self.RESULT) as mock_answer_question: + result = CliRunner().invoke( + octue_cli, + [ + "question", + "ask", + "local", + '--input-values={"height": 3}', + f"--attributes={json.dumps(original_attributes)}", + ], + ) + + # Check service and app configuration. + mock_answer_question_kwargs = mock_answer_question.call_args.kwargs + self.assertEqual(mock_answer_question_kwargs["service_configuration"].namespace, "testing") + self.assertEqual(mock_answer_question_kwargs["service_configuration"].name, "test-app") + self.assertEqual(mock_answer_question_kwargs["app_configuration"].configuration_values, {"n_iterations": 5}) + + # Check question event and attributes. + question = mock_answer_question_kwargs["question"] + self.assertEqual(question["event"], {"kind": "question", "input_values": {"height": 3}}) + self.assertEqual(question["attributes"], original_attributes) + + # Check the result is in the output. + self.assertIn(json.dumps(self.RESULT), result.output) class TestStartCommand(BaseTestCase): From 6c8c6594624a164517f6f22e2c11a252787023c3 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Wed, 12 Feb 2025 15:44:21 +0000 Subject: [PATCH 112/125] FIX: Use encoder when dumping to JSON in CLI --- octue/cli.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/octue/cli.py b/octue/cli.py index 84a04f6e4..4d9f46833 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -24,6 +24,7 @@ from octue.resources import Child, Manifest, service_backends from octue.runner import Runner from octue.utils.decoders import OctueJSONDecoder +from octue.utils.encoders import OctueJSONEncoder logger = logging.getLogger(__name__) @@ -159,7 +160,7 @@ def remote(sruid, input_values, input_manifest, project_name, asynchronous, serv click.echo(question_uuid) return - click.echo(json.dumps(answer)) + click.echo(json.dumps(answer, cls=OctueJSONEncoder)) @ask.command() @@ -241,7 +242,7 @@ def local(input_values, input_manifest, attributes, service_config): app_configuration=app_configuration, ) - click.echo(json.dumps(answer)) + click.echo(json.dumps(answer, cls=OctueJSONEncoder)) @question.group() @@ -341,7 +342,7 @@ def get( limit=limit, ) - click.echo(json.dumps(events)) + click.echo(json.dumps(events, cls=OctueJSONEncoder)) @events.command() @@ -470,7 +471,7 @@ def replay( result = replayer.handle_events(events) if result: - click.echo(json.dumps(result)) + click.echo(json.dumps(result, cls=OctueJSONEncoder)) return logger.warning("No result was found for this question.") From 4109684f266a04f498ecacb10b04f169981c92bb Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Wed, 12 Feb 2025 15:44:46 +0000 Subject: [PATCH 113/125] FIX: Send output manifests to `stdout` in serialised form --- octue/cli.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/octue/cli.py b/octue/cli.py index 4d9f46833..8914c11a9 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -160,6 +160,11 @@ def remote(sruid, input_values, input_manifest, project_name, asynchronous, serv click.echo(question_uuid) return + output_manifest = answer.get("output_manifest") + + if output_manifest: + answer["output_manifest"] = output_manifest.to_primitive() + click.echo(json.dumps(answer, cls=OctueJSONEncoder)) From 6bccdd9f58266f66345beaaf13dd2c752dc920b0 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Wed, 12 Feb 2025 15:47:43 +0000 Subject: [PATCH 114/125] TST: Test output manifests are passed to `stdout` properly from CLI --- tests/test_cli.py | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 7e486362c..c04a36ff8 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -106,6 +106,27 @@ def test_with_input_values_and_manifest(self): self.assertEqual(mock_ask.call_args.kwargs["input_manifest"].id, input_manifest.id) self.assertIn(json.dumps(self.RESULT), result.output) + def test_with_output_manifest(self): + """Test that the `octue question ask remote` CLI command returns output manifests in a useful form.""" + result = {"output_values": {"some": "data"}, "output_manifest": self.create_valid_manifest()} + + with mock.patch("octue.cli.ServiceConfiguration.from_file", return_value=self.MOCK_CONFIGURATIONS[0]): + with mock.patch("octue.cli.Child.ask", return_value=(result, self.QUESTION_UUID)): + result = CliRunner().invoke( + octue_cli, + [ + "question", + "ask", + "remote", + self.SRUID, + f"--input-values={json.dumps({'height': 3})}", + ], + ) + + output = json.loads(result.output) + self.assertEqual(output["output_values"], {"some": "data"}) + self.assertEqual(len(output["output_manifest"]["datasets"]), 2) + def test_asynchronous(self): """Test that the `octue question ask remote` CLI command works with the `--asynchronous` option and returns the question UUID. @@ -230,7 +251,8 @@ def test_with_input_manifest(self): def test_with_input_values_and_manifest(self): """Test that the `octue question ask local` CLI command works with input values and input manifest and sends an - originator question.""" + originator question. + """ input_values = {"height": 3} input_manifest = self.create_valid_manifest() @@ -269,6 +291,26 @@ def test_with_input_values_and_manifest(self): # Check the result is in the output. self.assertIn(json.dumps(self.RESULT), result.output) + def test_with_output_manifest(self): + """Test that the `octue question ask local` CLI command returns output manifests in a useful form.""" + result = {"output_values": {"some": "data"}, "output_manifest": self.create_valid_manifest()} + + with mock.patch("octue.cli.load_service_and_app_configuration", return_value=self.MOCK_CONFIGURATIONS): + with mock.patch("octue.cli.answer_question", return_value=result): + result = CliRunner().invoke( + octue_cli, + [ + "question", + "ask", + "local", + f"--input-values={json.dumps({'height': 3})}", + ], + ) + + output = json.loads(result.output) + self.assertEqual(output["output_values"], {"some": "data"}) + self.assertEqual(len(output["output_manifest"]["datasets"]), 2) + def test_with_attributes(self): """Test that the `octue question ask remote` CLI command can be passed question attributes which are passed along to the answering `Service` instance. From 235c295e690a4034b19d6ddd242a8d339ab78157 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Wed, 12 Feb 2025 15:51:55 +0000 Subject: [PATCH 115/125] TST: Factor out repeated fixture --- tests/test_cli.py | 74 ++++++++++++++++++++++------------------------- 1 file changed, 34 insertions(+), 40 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index c04a36ff8..de8ea6dfd 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -19,6 +19,13 @@ TWINE_FILE_PATH = os.path.join(TESTS_DIR, "data", "twines", "valid_schema_twine.json") +MOCK_CONFIGURATIONS = ( + ServiceConfiguration(name="test-app", namespace="testing"), + AppConfiguration(configuration_values={"n_iterations": 5}), +) + +RESULT = {"output_values": {"some": "data"}} + class TestCLI(BaseTestCase): def test_version(self): @@ -36,19 +43,13 @@ def test_help(self): class TestQuestionAskRemoteCommand(BaseTestCase): - MOCK_CONFIGURATIONS = ( - ServiceConfiguration(name="test-app", namespace="testing"), - AppConfiguration(configuration_values={"n_iterations": 5}), - ) - SRUID = "my-org/my-service:1.0.0" QUESTION_UUID = "81f35b28-068b-4314-9eeb-e55e60d0fe8a" - RESULT = {"output_values": {"some": "data"}} def test_with_input_values(self): """Test that the `octue question ask remote` CLI command works with just input values.""" - with mock.patch("octue.cli.ServiceConfiguration.from_file", return_value=self.MOCK_CONFIGURATIONS[0]): - with mock.patch("octue.cli.Child.ask", return_value=(self.RESULT, self.QUESTION_UUID)) as mock_ask: + with mock.patch("octue.cli.ServiceConfiguration.from_file", return_value=MOCK_CONFIGURATIONS[0]): + with mock.patch("octue.cli.Child.ask", return_value=(RESULT, self.QUESTION_UUID)) as mock_ask: result = CliRunner().invoke( octue_cli, [ @@ -61,14 +62,14 @@ def test_with_input_values(self): ) mock_ask.assert_called_with(input_values={"height": 3}, input_manifest=None, asynchronous=False) - self.assertIn(json.dumps(self.RESULT), result.output) + self.assertIn(json.dumps(RESULT), result.output) def test_with_input_manifest(self): """Test that the `octue question ask remote` CLI command works with just an input manifest.""" input_manifest = self.create_valid_manifest() - with mock.patch("octue.cli.ServiceConfiguration.from_file", return_value=self.MOCK_CONFIGURATIONS[0]): - with mock.patch("octue.cli.Child.ask", return_value=(self.RESULT, self.QUESTION_UUID)) as mock_ask: + with mock.patch("octue.cli.ServiceConfiguration.from_file", return_value=MOCK_CONFIGURATIONS[0]): + with mock.patch("octue.cli.Child.ask", return_value=(RESULT, self.QUESTION_UUID)) as mock_ask: result = CliRunner().invoke( octue_cli, [ @@ -81,15 +82,15 @@ def test_with_input_manifest(self): ) self.assertEqual(mock_ask.call_args.kwargs["input_manifest"].id, input_manifest.id) - self.assertIn(json.dumps(self.RESULT), result.output) + self.assertIn(json.dumps(RESULT), result.output) def test_with_input_values_and_manifest(self): """Test that the `octue question ask remote` CLI command works with input values and input manifest.""" input_values = {"height": 3} input_manifest = self.create_valid_manifest() - with mock.patch("octue.cli.ServiceConfiguration.from_file", return_value=self.MOCK_CONFIGURATIONS[0]): - with mock.patch("octue.cli.Child.ask", return_value=(self.RESULT, self.QUESTION_UUID)) as mock_ask: + with mock.patch("octue.cli.ServiceConfiguration.from_file", return_value=MOCK_CONFIGURATIONS[0]): + with mock.patch("octue.cli.Child.ask", return_value=(RESULT, self.QUESTION_UUID)) as mock_ask: result = CliRunner().invoke( octue_cli, [ @@ -104,13 +105,13 @@ def test_with_input_values_and_manifest(self): self.assertEqual(mock_ask.call_args.kwargs["input_values"], input_values) self.assertEqual(mock_ask.call_args.kwargs["input_manifest"].id, input_manifest.id) - self.assertIn(json.dumps(self.RESULT), result.output) + self.assertIn(json.dumps(RESULT), result.output) def test_with_output_manifest(self): """Test that the `octue question ask remote` CLI command returns output manifests in a useful form.""" result = {"output_values": {"some": "data"}, "output_manifest": self.create_valid_manifest()} - with mock.patch("octue.cli.ServiceConfiguration.from_file", return_value=self.MOCK_CONFIGURATIONS[0]): + with mock.patch("octue.cli.ServiceConfiguration.from_file", return_value=MOCK_CONFIGURATIONS[0]): with mock.patch("octue.cli.Child.ask", return_value=(result, self.QUESTION_UUID)): result = CliRunner().invoke( octue_cli, @@ -131,8 +132,8 @@ def test_asynchronous(self): """Test that the `octue question ask remote` CLI command works with the `--asynchronous` option and returns the question UUID. """ - with mock.patch("octue.cli.ServiceConfiguration.from_file", return_value=self.MOCK_CONFIGURATIONS[0]): - with mock.patch("octue.cli.Child.ask", return_value=(self.RESULT, self.QUESTION_UUID)) as mock_ask: + with mock.patch("octue.cli.ServiceConfiguration.from_file", return_value=MOCK_CONFIGURATIONS[0]): + with mock.patch("octue.cli.Child.ask", return_value=(RESULT, self.QUESTION_UUID)) as mock_ask: result = CliRunner().invoke( octue_cli, [ @@ -150,7 +151,7 @@ def test_asynchronous(self): def test_with_no_service_configuration(self): """Test that the command works when no service configuration is provided.""" - with mock.patch("octue.cli.Child.ask", return_value=(self.RESULT, self.QUESTION_UUID)) as mock_ask: + with mock.patch("octue.cli.Child.ask", return_value=(RESULT, self.QUESTION_UUID)) as mock_ask: result = CliRunner().invoke( octue_cli, [ @@ -163,23 +164,16 @@ def test_with_no_service_configuration(self): ) mock_ask.assert_called_with(input_values={"height": 3}, input_manifest=None, asynchronous=False) - self.assertIn(json.dumps(self.RESULT), result.output) + self.assertIn(json.dumps(RESULT), result.output) class TestQuestionAskLocalCommand(BaseTestCase): - MOCK_CONFIGURATIONS = ( - ServiceConfiguration(name="test-app", namespace="testing"), - AppConfiguration(configuration_values={"n_iterations": 5}), - ) - - RESULT = {"output_values": {"some": "data"}} - def test_with_input_values(self): """Test that the `octue question ask local` CLI command works with just input values and sends an originator question. """ - with mock.patch("octue.cli.load_service_and_app_configuration", return_value=self.MOCK_CONFIGURATIONS): - with mock.patch("octue.cli.answer_question", return_value=self.RESULT) as mock_answer_question: + with mock.patch("octue.cli.load_service_and_app_configuration", return_value=MOCK_CONFIGURATIONS): + with mock.patch("octue.cli.answer_question", return_value=RESULT) as mock_answer_question: result = CliRunner().invoke( octue_cli, [ @@ -209,15 +203,15 @@ def test_with_input_values(self): self.assertIsNone(question["attributes"]["parent_question_uuid"]) # Check the result is in the output. - self.assertIn(json.dumps(self.RESULT), result.output) + self.assertIn(json.dumps(RESULT), result.output) def test_with_input_manifest(self): """Test that the `octue question ask local` CLI command works with just an input manifest and sends an originator question. """ input_manifest = self.create_valid_manifest() - with mock.patch("octue.cli.load_service_and_app_configuration", return_value=self.MOCK_CONFIGURATIONS): - with mock.patch("octue.cli.answer_question", return_value=self.RESULT) as mock_answer_question: + with mock.patch("octue.cli.load_service_and_app_configuration", return_value=MOCK_CONFIGURATIONS): + with mock.patch("octue.cli.answer_question", return_value=RESULT) as mock_answer_question: result = CliRunner().invoke( octue_cli, [ @@ -247,7 +241,7 @@ def test_with_input_manifest(self): self.assertIsNone(question["attributes"]["parent_question_uuid"]) # Check the result is in the output. - self.assertIn(json.dumps(self.RESULT), result.output) + self.assertIn(json.dumps(RESULT), result.output) def test_with_input_values_and_manifest(self): """Test that the `octue question ask local` CLI command works with input values and input manifest and sends an @@ -256,8 +250,8 @@ def test_with_input_values_and_manifest(self): input_values = {"height": 3} input_manifest = self.create_valid_manifest() - with mock.patch("octue.cli.load_service_and_app_configuration", return_value=self.MOCK_CONFIGURATIONS): - with mock.patch("octue.cli.answer_question", return_value=self.RESULT) as mock_answer_question: + with mock.patch("octue.cli.load_service_and_app_configuration", return_value=MOCK_CONFIGURATIONS): + with mock.patch("octue.cli.answer_question", return_value=RESULT) as mock_answer_question: result = CliRunner().invoke( octue_cli, [ @@ -289,13 +283,13 @@ def test_with_input_values_and_manifest(self): self.assertIsNone(question["attributes"]["parent_question_uuid"]) # Check the result is in the output. - self.assertIn(json.dumps(self.RESULT), result.output) + self.assertIn(json.dumps(RESULT), result.output) def test_with_output_manifest(self): """Test that the `octue question ask local` CLI command returns output manifests in a useful form.""" result = {"output_values": {"some": "data"}, "output_manifest": self.create_valid_manifest()} - with mock.patch("octue.cli.load_service_and_app_configuration", return_value=self.MOCK_CONFIGURATIONS): + with mock.patch("octue.cli.load_service_and_app_configuration", return_value=MOCK_CONFIGURATIONS): with mock.patch("octue.cli.answer_question", return_value=result): result = CliRunner().invoke( octue_cli, @@ -335,8 +329,8 @@ def test_with_attributes(self): "ephemeral_storage": "500Mi", } - with mock.patch("octue.cli.load_service_and_app_configuration", return_value=self.MOCK_CONFIGURATIONS): - with mock.patch("octue.cli.answer_question", return_value=self.RESULT) as mock_answer_question: + with mock.patch("octue.cli.load_service_and_app_configuration", return_value=MOCK_CONFIGURATIONS): + with mock.patch("octue.cli.answer_question", return_value=RESULT) as mock_answer_question: result = CliRunner().invoke( octue_cli, [ @@ -360,7 +354,7 @@ def test_with_attributes(self): self.assertEqual(question["attributes"], original_attributes) # Check the result is in the output. - self.assertIn(json.dumps(self.RESULT), result.output) + self.assertIn(json.dumps(RESULT), result.output) class TestStartCommand(BaseTestCase): From 8ed47282b62089639712967f1bbd7124a41dcb49 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Wed, 12 Feb 2025 16:10:13 +0000 Subject: [PATCH 116/125] ENH: Log warning if no events found in `octue question events get` --- octue/cli.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/octue/cli.py b/octue/cli.py index 8914c11a9..eccc81421 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -347,6 +347,9 @@ def get( limit=limit, ) + if not events: + logger.warning("No events were found for this question.") + click.echo(json.dumps(events, cls=OctueJSONEncoder)) From d403ed4fc3b565f3edd8999c3ebb812adfde2297 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Wed, 12 Feb 2025 16:10:54 +0000 Subject: [PATCH 117/125] TST: Test `octue question events get` CLI command skipci --- tests/test_cli.py | 111 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/tests/test_cli.py b/tests/test_cli.py index de8ea6dfd..c460e88b7 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -357,6 +357,117 @@ def test_with_attributes(self): self.assertIn(json.dumps(RESULT), result.output) +class TestQuestionEventsGetCommand(BaseTestCase): + QUESTION_UUID = "3ffc192c-7db0-4941-9b66-328a9fc02b62" + + with open(os.path.join(TESTS_DIR, "data", "events.json")) as f: + EVENTS = json.load(f) + + def test_warning_logged_if_no_events_found(self): + """Test that a warning is logged if no events are found for a question.""" + with mock.patch("octue.cli.ServiceConfiguration.from_file", return_value=MOCK_CONFIGURATIONS[0]): + with mock.patch("octue.cli.get_events", return_value=[]): + with self.assertLogs(level=logging.WARNING) as logging_context: + result = CliRunner().invoke( + octue_cli, + [ + "question", + "events", + "get", + "--question-uuid", + self.QUESTION_UUID, + ], + ) + + self.assertIn("No events were found for this question.", logging_context.output[0]) + self.assertTrue(result.output.endswith("[]\n")) + + def test_with_question_uuid_types(self): + """Test that each of the question UUID types is valid as an argument by themselves.""" + for question_uuid_arg in ( + "--question-uuid", + "--parent-question-uuid", + "--originator-question-uuid", + ): + with self.subTest(question_uuid_arg=question_uuid_arg): + with mock.patch("octue.cli.ServiceConfiguration.from_file", return_value=MOCK_CONFIGURATIONS[0]): + with mock.patch("octue.cli.get_events", return_value=self.EVENTS): + result = CliRunner().invoke( + octue_cli, + [ + "question", + "events", + "get", + question_uuid_arg, + self.QUESTION_UUID, + ], + ) + + self.assertEqual(json.loads(result.output), self.EVENTS) + + def test_with_kinds(self): + """Test that the `--kinds` option is respected.""" + with mock.patch("octue.cli.ServiceConfiguration.from_file", return_value=MOCK_CONFIGURATIONS[0]): + with mock.patch("octue.cli.get_events", return_value=self.EVENTS[-2:-1]) as mock_get_events: + result = CliRunner().invoke( + octue_cli, + [ + "question", + "events", + "get", + "--question-uuid", + self.QUESTION_UUID, + "--kinds", + "result", + ], + ) + + self.assertEqual(mock_get_events.call_args.kwargs["kinds"], ["result"]) + self.assertIsNone(mock_get_events.call_args.kwargs["exclude_kinds"]) + self.assertEqual(json.loads(result.output), self.EVENTS[-2:-1]) + + def test_with_exclude_kinds(self): + """Test that the `--exclude-kinds` option is respected.""" + with mock.patch("octue.cli.ServiceConfiguration.from_file", return_value=MOCK_CONFIGURATIONS[0]): + with mock.patch("octue.cli.get_events", return_value=self.EVENTS[:1]) as mock_get_events: + result = CliRunner().invoke( + octue_cli, + [ + "question", + "events", + "get", + "--question-uuid", + self.QUESTION_UUID, + "--exclude-kinds", + "log_record,result,heartbeat", + ], + ) + + self.assertIsNone(mock_get_events.call_args.kwargs["kinds"]) + self.assertEqual(mock_get_events.call_args.kwargs["exclude_kinds"], ["log_record", "result", "heartbeat"]) + self.assertEqual(json.loads(result.output), self.EVENTS[:1]) + + def test_with_limit(self): + """Test that the `--limit` option is respected.""" + with mock.patch("octue.cli.ServiceConfiguration.from_file", return_value=MOCK_CONFIGURATIONS[0]): + with mock.patch("octue.cli.get_events", return_value=self.EVENTS[:1]) as mock_get_events: + result = CliRunner().invoke( + octue_cli, + [ + "question", + "events", + "get", + "--question-uuid", + self.QUESTION_UUID, + "--limit", + "1", + ], + ) + + self.assertEqual(mock_get_events.call_args.kwargs["limit"], 1) + self.assertEqual(json.loads(result.output), self.EVENTS[:1]) + + class TestStartCommand(BaseTestCase): @classmethod def setUpClass(cls): From 2337d09c4f042a9022e66d46ce4432fd38f91001 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Wed, 12 Feb 2025 16:47:32 +0000 Subject: [PATCH 118/125] ENH: Log warning if no events found in `octue question events replay` --- octue/cli.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/octue/cli.py b/octue/cli.py index eccc81421..5e4120429 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -469,6 +469,11 @@ def replay( limit=limit, ) + if not events: + logger.warning("No events were found for this question.") + click.echo(None) + return + replayer = EventReplayer( include_service_metadata_in_logs=include_service_metadata, exclude_logs_containing=exclude_logs_containing, @@ -478,11 +483,10 @@ def replay( result = replayer.handle_events(events) - if result: - click.echo(json.dumps(result, cls=OctueJSONEncoder)) - return + if not result: + logger.warning("No result was found for this question.") - logger.warning("No result was found for this question.") + click.echo(json.dumps(result, cls=OctueJSONEncoder)) @question.command() From cf5838a8d71d40636ba06a3dfb0ea123a5f2f674 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Wed, 12 Feb 2025 16:59:14 +0000 Subject: [PATCH 119/125] REF: Move no events/result warnings into underlying functions --- octue/cli.py | 8 -------- octue/cloud/events/replayer.py | 3 ++- octue/cloud/pub_sub/bigquery.py | 4 ++++ tests/cloud/events/test_replayer.py | 9 ++++++--- tests/cloud/pub_sub/test_bigquery.py | 9 ++++++--- tests/test_cli.py | 24 +++++++++++------------- 6 files changed, 29 insertions(+), 28 deletions(-) diff --git a/octue/cli.py b/octue/cli.py index 5e4120429..fdd5bca52 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -347,9 +347,6 @@ def get( limit=limit, ) - if not events: - logger.warning("No events were found for this question.") - click.echo(json.dumps(events, cls=OctueJSONEncoder)) @@ -470,7 +467,6 @@ def replay( ) if not events: - logger.warning("No events were found for this question.") click.echo(None) return @@ -482,10 +478,6 @@ def replay( ) result = replayer.handle_events(events) - - if not result: - logger.warning("No result was found for this question.") - click.echo(json.dumps(result, cls=OctueJSONEncoder)) diff --git a/octue/cloud/events/replayer.py b/octue/cloud/events/replayer.py index 8336bf2c7..1ee2bfc47 100644 --- a/octue/cloud/events/replayer.py +++ b/octue/cloud/events/replayer.py @@ -3,7 +3,6 @@ from octue.cloud.events.handler import AbstractEventHandler from octue.cloud.events.validation import SERVICE_COMMUNICATION_SCHEMA - logger = logging.getLogger(__name__) @@ -78,6 +77,8 @@ def handle_events(self, events): if result: return result + logger.warning("No result was found for this question.") + def _extract_event_and_attributes(self, container): """Extract an event and its attributes from the event container. diff --git a/octue/cloud/pub_sub/bigquery.py b/octue/cloud/pub_sub/bigquery.py index 8aa809b33..a6ef64ce2 100644 --- a/octue/cloud/pub_sub/bigquery.py +++ b/octue/cloud/pub_sub/bigquery.py @@ -1,7 +1,10 @@ +import logging + from google.cloud.bigquery import Client, QueryJobConfig, ScalarQueryParameter from octue.cloud.events.validation import VALID_EVENT_KINDS +logger = logging.getLogger(__name__) DEFAULT_FIELDS = ( "`originator_question_uuid`", @@ -120,6 +123,7 @@ def get_events( result = query_job.result() if result.total_rows == 0: + logger.warning("No events were found for this question.") return [] events = [_deserialise_event(event) for event in result] diff --git a/tests/cloud/events/test_replayer.py b/tests/cloud/events/test_replayer.py index 0fae6e6ab..b19cbf6d8 100644 --- a/tests/cloud/events/test_replayer.py +++ b/tests/cloud/events/test_replayer.py @@ -7,7 +7,6 @@ from octue.cloud.events.replayer import EventReplayer from tests import TEST_BUCKET_NAME, TESTS_DIR - with open(os.path.join(TESTS_DIR, "data", "events.json")) as f: EVENTS = json.load(f) @@ -31,7 +30,10 @@ class TestEventReplayer(unittest.TestCase): def test_with_no_events(self): """Test that `None` is returned if no events are passed in.""" - result = EventReplayer().handle_events(events=[]) + with self.assertLogs(level=logging.WARNING) as logging_context: + result = EventReplayer().handle_events(events=[]) + + self.assertIn("No result was found for this question.", logging_context.output[0]) self.assertIsNone(result) def test_with_no_valid_events(self): @@ -65,8 +67,9 @@ def test_no_result_event(self): with self.assertLogs() as logging_context: result = EventReplayer().handle_events(events=[event]) - self.assertIsNone(result) self.assertIn("question was delivered", logging_context.output[0]) + self.assertIn("No result was found for this question.", logging_context.output[1]) + self.assertIsNone(result) def test_with_events_including_result_event(self): """Test that stored events can be replayed and the outputs extracted from the "result" event.""" diff --git a/tests/cloud/pub_sub/test_bigquery.py b/tests/cloud/pub_sub/test_bigquery.py index 0a999fb5d..a60841639 100644 --- a/tests/cloud/pub_sub/test_bigquery.py +++ b/tests/cloud/pub_sub/test_bigquery.py @@ -1,3 +1,4 @@ +import logging from unittest import TestCase from unittest.mock import MagicMock, patch @@ -51,11 +52,13 @@ def test_error_raised_if_kinds_invalid(self): with self.assertRaises(ValueError): get_events(table_id="blah", question_uuid="blah", kinds=invalid_kinds) - def test_no_events_found(self): - """Test that an empty list is returned if no events are found for the question UUID.""" + def test_warning_logged_if_no_events_found(self): + """Test that an empty list is returned and a warning is logged if no events are found for the question UUID.""" with patch("octue.cloud.pub_sub.bigquery.Client", MockEmptyBigQueryClient): - events = get_events(table_id="blah", question_uuid="blah") + with self.assertLogs(level=logging.WARNING) as logging_context: + events = get_events(table_id="blah", question_uuid="blah") + self.assertIn("No events were found for this question.", logging_context.output[0]) self.assertEqual(events, []) def test_without_tail(self): diff --git a/tests/test_cli.py b/tests/test_cli.py index c460e88b7..667e65e9b 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -367,19 +367,17 @@ def test_warning_logged_if_no_events_found(self): """Test that a warning is logged if no events are found for a question.""" with mock.patch("octue.cli.ServiceConfiguration.from_file", return_value=MOCK_CONFIGURATIONS[0]): with mock.patch("octue.cli.get_events", return_value=[]): - with self.assertLogs(level=logging.WARNING) as logging_context: - result = CliRunner().invoke( - octue_cli, - [ - "question", - "events", - "get", - "--question-uuid", - self.QUESTION_UUID, - ], - ) - - self.assertIn("No events were found for this question.", logging_context.output[0]) + result = CliRunner().invoke( + octue_cli, + [ + "question", + "events", + "get", + "--question-uuid", + self.QUESTION_UUID, + ], + ) + self.assertTrue(result.output.endswith("[]\n")) def test_with_question_uuid_types(self): From 4dc099bb3f27b1ba16b59834845eacd670da184d Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Wed, 12 Feb 2025 17:30:06 +0000 Subject: [PATCH 120/125] ENH: Remove redundant `--only-handle-result` option from `replay` command --- octue/cli.py | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/octue/cli.py b/octue/cli.py index fdd5bca52..7b834deb4 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -412,13 +412,6 @@ def get( default=None, help="Skip handling log messages containing this string.", ) -@click.option( - "-r", - "--only-handle-result", - is_flag=True, - help="Skip all events apart from the 'result' event (if there is one). If providing this option, the " - "`--include-kinds` and `--exclude-kinds` options are ignored. This option can speed up event handling.", -) @click.option( "--validate-events", is_flag=True, @@ -434,7 +427,6 @@ def replay( service_config, include_service_metadata, exclude_logs_containing, - only_handle_result, validate_events, ): """Replay a question's events, returning the result as JSON at the end if there is one. One of the following must be @@ -444,15 +436,11 @@ def replay( --parent-question-uuid\n --originator-question-uuid\n """ - if only_handle_result: - kinds = ["result"] - exclude_kinds = None - else: - if kinds: - kinds = kinds.split(",") + if kinds: + kinds = kinds.split(",") - if exclude_kinds: - exclude_kinds = exclude_kinds.split(",") + if exclude_kinds: + exclude_kinds = exclude_kinds.split(",") service_configuration = ServiceConfiguration.from_file(path=service_config) @@ -473,7 +461,6 @@ def replay( replayer = EventReplayer( include_service_metadata_in_logs=include_service_metadata, exclude_logs_containing=exclude_logs_containing, - only_handle_result=only_handle_result, validate_events=validate_events, ) From 2d26da3f604b0a5126136845b138b06ac49c6553 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Wed, 12 Feb 2025 17:30:34 +0000 Subject: [PATCH 121/125] ENH: Return no result from `replay` command if no events/result --- octue/cli.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/octue/cli.py b/octue/cli.py index 7b834deb4..3aadeed3a 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -455,7 +455,6 @@ def replay( ) if not events: - click.echo(None) return replayer = EventReplayer( @@ -465,6 +464,10 @@ def replay( ) result = replayer.handle_events(events) + + if not result: + return + click.echo(json.dumps(result, cls=OctueJSONEncoder)) From 808d987637f4bb0a1c9c1135fce78a52541dc517 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Wed, 12 Feb 2025 17:35:25 +0000 Subject: [PATCH 122/125] TST: Test `octue question events replay` CLI command skipci --- tests/test_cli.py | 133 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) diff --git a/tests/test_cli.py b/tests/test_cli.py index 667e65e9b..ffee8266a 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -466,6 +466,139 @@ def test_with_limit(self): self.assertEqual(json.loads(result.output), self.EVENTS[:1]) +class TestQuestionEventsReplayCommand(BaseTestCase): + QUESTION_UUID = "3ffc192c-7db0-4941-9b66-328a9fc02b62" + + with open(os.path.join(TESTS_DIR, "data", "events.json")) as f: + EVENTS = json.load(f) + + maxDiff = None + + def test_with_no_events_found(self): + """Test that an empty list is passed to `stdout` if no events are found for a question.""" + with mock.patch("octue.cli.ServiceConfiguration.from_file", return_value=MOCK_CONFIGURATIONS[0]): + with mock.patch("octue.cli.get_events", return_value=[]): + result = CliRunner().invoke( + octue_cli, + [ + "question", + "events", + "replay", + "--question-uuid", + self.QUESTION_UUID, + ], + ) + + self.assertEqual(result.output, "") + + def test_with_question_uuid_types_and_validate_events(self): + """Test that each of the question UUID types is valid as an argument by themselves and that replaying still + works with the `--validate-events` option. + """ + for options in ( + ["--question-uuid"], + ["--parent-question-uuid"], + ["--originator-question-uuid"], + ["--validate-events", "--question-uuid"], + ): + with self.subTest(question_uuid_arg=options): + with mock.patch("octue.cli.ServiceConfiguration.from_file", return_value=MOCK_CONFIGURATIONS[0]): + with mock.patch("octue.cli.get_events", return_value=self.EVENTS): + with self.assertLogs() as logging_context: + result = CliRunner().invoke( + octue_cli, + [ + "question", + "events", + "replay", + *options, + self.QUESTION_UUID, + ], + ) + + replayed_log_messages = "\n".join(logging_context.output) + log_record_events = [event for event in self.EVENTS if event["event"]["kind"] == "log_record"] + + # Check logs messages were replayed. + for event in log_record_events: + self.assertIn(event["event"]["log_record"]["msg"], replayed_log_messages) + + # Check result is correct. + self.assertEqual( + json.loads(result.output.splitlines()[-1])["output_values"], + self.EVENTS[-2]["event"]["output_values"], + ) + + def test_with_kinds(self): + """Test that the `--kinds` option is respected.""" + with mock.patch("octue.cli.ServiceConfiguration.from_file", return_value=MOCK_CONFIGURATIONS[0]): + with mock.patch("octue.cli.get_events", return_value=self.EVENTS[-2:-1]) as mock_get_events: + result = CliRunner().invoke( + octue_cli, + [ + "question", + "events", + "replay", + "--question-uuid", + self.QUESTION_UUID, + "--kinds", + "result", + ], + ) + + # Check result is correct. + self.assertEqual( + json.loads(result.output.splitlines()[-1])["output_values"], + self.EVENTS[-2]["event"]["output_values"], + ) + + self.assertEqual(mock_get_events.call_args.kwargs["kinds"], ["result"]) + self.assertIsNone(mock_get_events.call_args.kwargs["exclude_kinds"]) + + def test_with_exclude_kinds(self): + """Test that the `--exclude-kinds` option is respected.""" + with mock.patch("octue.cli.ServiceConfiguration.from_file", return_value=MOCK_CONFIGURATIONS[0]): + with mock.patch("octue.cli.get_events", return_value=self.EVENTS[:1]) as mock_get_events: + result = CliRunner().invoke( + octue_cli, + [ + "question", + "events", + "replay", + "--question-uuid", + self.QUESTION_UUID, + "--exclude-kinds", + "log_record,result,heartbeat", + ], + ) + + # Check result isn't replayed. + self.assertTrue(result.output.splitlines()[-1].endswith("No result was found for this question.")) + self.assertIsNone(mock_get_events.call_args.kwargs["kinds"]) + self.assertEqual(mock_get_events.call_args.kwargs["exclude_kinds"], ["log_record", "result", "heartbeat"]) + + def test_with_limit(self): + """Test that the `--limit` option is respected.""" + with mock.patch("octue.cli.ServiceConfiguration.from_file", return_value=MOCK_CONFIGURATIONS[0]): + with mock.patch("octue.cli.get_events", return_value=self.EVENTS[:1]) as mock_get_events: + result = CliRunner().invoke( + octue_cli, + [ + "question", + "events", + "replay", + "--question-uuid", + self.QUESTION_UUID, + "--limit", + "1", + ], + ) + + # Check result isn't replayed. + self.assertTrue(result.output.splitlines()[-1].endswith("No result was found for this question.")) + self.assertEqual(mock_get_events.call_args.kwargs["limit"], 1) + + class TestStartCommand(BaseTestCase): @classmethod def setUpClass(cls): From a83a734a63f343b473ca70b9b4299377313bbbfb Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Wed, 12 Feb 2025 17:40:57 +0000 Subject: [PATCH 123/125] TST: Test `octue get-diagnostics` is deprecated --- tests/test_cli.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/test_cli.py b/tests/test_cli.py index ffee8266a..5d5f50e56 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -705,6 +705,25 @@ def setUpClass(cls): diagnostics.upload(storage.path.join(cls.DIAGNOSTICS_CLOUD_PATH, cls.ANALYSIS_ID)) + def test_old_get_diagnostics_command_deprecated(self): + """Test that the old `octue get-diagnostics` command is deprecated and just calls the new one.""" + cloud_path = storage.path.join(self.DIAGNOSTICS_CLOUD_PATH, self.ANALYSIS_ID) + local_path = "some-local-path" + + with patch("octue.cli.diagnostics") as mock_diagnostics: + result = CliRunner().invoke( + octue_cli, + [ + "get-diagnostics", + cloud_path, + "--local-path", + local_path, + ], + ) + + mock_diagnostics.assert_called_with(cloud_path, local_path, False) + self.assertEqual(result.output, "DeprecationWarning: The command 'get-diagnostics' is deprecated.\n") + def test_warning_logged_if_no_diagnostics_found(self): """Test that a warning about there being no diagnostics is logged if the diagnostics cloud path is empty.""" with tempfile.TemporaryDirectory() as temporary_directory: From 3bb11f5258db84caf5cd4e699fb863b52e07a7d2 Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Wed, 12 Feb 2025 17:44:14 +0000 Subject: [PATCH 124/125] ENH: Remove cloud run specific commands in dockerfiles --- .../deployment/dockerfiles/Dockerfile-python310 | 14 ++------------ .../deployment/dockerfiles/Dockerfile-python311 | 14 ++------------ .../deployment/dockerfiles/Dockerfile-python39 | 14 ++------------ 3 files changed, 6 insertions(+), 36 deletions(-) diff --git a/octue/cloud/deployment/dockerfiles/Dockerfile-python310 b/octue/cloud/deployment/dockerfiles/Dockerfile-python310 index a3ca69a0b..693fc1b4d 100644 --- a/octue/cloud/deployment/dockerfiles/Dockerfile-python310 +++ b/octue/cloud/deployment/dockerfiles/Dockerfile-python310 @@ -34,16 +34,6 @@ RUN if [ -f "pyproject.toml" ]; then poetry install --only main; \ elif [ -f "setup.py" ]; then pip install --upgrade pip && pip install -e .; \ elif [ -f "requirements.txt" ]; then pip install --upgrade pip && pip install -r requirements.txt; fi -EXPOSE $PORT - ENV USE_OCTUE_LOG_HANDLER=1 -ENV COMPUTE_PROVIDER=GOOGLE_CLOUD_RUN - -ARG GUNICORN_WORKERS=1 -ENV GUNICORN_WORKERS=$GUNICORN_WORKERS - -ARG GUNICORN_THREADS=8 -ENV GUNICORN_THREADS=$GUNICORN_THREADS - -# Timeout is set to 0 to disable the timeouts of the workers to allow Cloud Run to handle instance scaling. -CMD exec gunicorn --bind :$PORT --workers $GUNICORN_WORKERS --threads $GUNICORN_THREADS --timeout 0 octue.cloud.deployment.google.cloud_run.flask_app:app +ENV COMPUTE_PROVIDER=GOOGLE_KUEUE +CMD ["octue", "question", "ask", "local"] diff --git a/octue/cloud/deployment/dockerfiles/Dockerfile-python311 b/octue/cloud/deployment/dockerfiles/Dockerfile-python311 index 03d99cd73..37e36d875 100644 --- a/octue/cloud/deployment/dockerfiles/Dockerfile-python311 +++ b/octue/cloud/deployment/dockerfiles/Dockerfile-python311 @@ -34,16 +34,6 @@ RUN if [ -f "pyproject.toml" ]; then poetry install --only main; \ elif [ -f "setup.py" ]; then pip install --upgrade pip && pip install -e .; \ elif [ -f "requirements.txt" ]; then pip install --upgrade pip && pip install -r requirements.txt; fi -EXPOSE $PORT - ENV USE_OCTUE_LOG_HANDLER=1 -ENV COMPUTE_PROVIDER=GOOGLE_CLOUD_RUN - -ARG GUNICORN_WORKERS=1 -ENV GUNICORN_WORKERS=$GUNICORN_WORKERS - -ARG GUNICORN_THREADS=8 -ENV GUNICORN_THREADS=$GUNICORN_THREADS - -# Timeout is set to 0 to disable the timeouts of the workers to allow Cloud Run to handle instance scaling. -CMD exec gunicorn --bind :$PORT --workers $GUNICORN_WORKERS --threads $GUNICORN_THREADS --timeout 0 octue.cloud.deployment.google.cloud_run.flask_app:app +ENV COMPUTE_PROVIDER=GOOGLE_KUEUE +CMD ["octue", "question", "ask", "local"] diff --git a/octue/cloud/deployment/dockerfiles/Dockerfile-python39 b/octue/cloud/deployment/dockerfiles/Dockerfile-python39 index a76513322..889053bce 100644 --- a/octue/cloud/deployment/dockerfiles/Dockerfile-python39 +++ b/octue/cloud/deployment/dockerfiles/Dockerfile-python39 @@ -34,16 +34,6 @@ RUN if [ -f "pyproject.toml" ]; then poetry install --only main; \ elif [ -f "setup.py" ]; then pip install --upgrade pip && pip install -e .; \ elif [ -f "requirements.txt" ]; then pip install --upgrade pip && pip install -r requirements.txt; fi -EXPOSE $PORT - ENV USE_OCTUE_LOG_HANDLER=1 -ENV COMPUTE_PROVIDER=GOOGLE_CLOUD_RUN - -ARG GUNICORN_WORKERS=1 -ENV GUNICORN_WORKERS=$GUNICORN_WORKERS - -ARG GUNICORN_THREADS=8 -ENV GUNICORN_THREADS=$GUNICORN_THREADS - -# Timeout is set to 0 to disable the timeouts of the workers to allow Cloud Run to handle instance scaling. -CMD exec gunicorn --bind :$PORT --workers $GUNICORN_WORKERS --threads $GUNICORN_THREADS --timeout 0 octue.cloud.deployment.google.cloud_run.flask_app:app +ENV COMPUTE_PROVIDER=GOOGLE_KUEUE +CMD ["octue", "question", "ask", "local"] From 4fa4886f1869376b44f22bee669d0d74afb1eacd Mon Sep 17 00:00:00 2001 From: cortadocodes Date: Wed, 12 Feb 2025 17:44:33 +0000 Subject: [PATCH 125/125] ENH: Delete unsupported python3.9 dockerfile skipci --- .../dockerfiles/Dockerfile-python39 | 39 ------------------- 1 file changed, 39 deletions(-) delete mode 100644 octue/cloud/deployment/dockerfiles/Dockerfile-python39 diff --git a/octue/cloud/deployment/dockerfiles/Dockerfile-python39 b/octue/cloud/deployment/dockerfiles/Dockerfile-python39 deleted file mode 100644 index 889053bce..000000000 --- a/octue/cloud/deployment/dockerfiles/Dockerfile-python39 +++ /dev/null @@ -1,39 +0,0 @@ -FROM windpioneers/gdal-python:little-gecko-gdal-2.4.1-python-3.9-slim - -# Ensure print statements and log messages appear promptly in Cloud Logging. -ENV PYTHONUNBUFFERED=True - -ENV PROJECT_ROOT=/workspace -WORKDIR $PROJECT_ROOT - -RUN apt-get update -y && apt-get install -y --fix-missing build-essential && rm -rf /var/lib/apt/lists/* - -# Install poetry. -ENV POETRY_HOME=/root/.poetry -ENV PATH="$POETRY_HOME/bin:$PATH" -RUN curl -sSL https://install.python-poetry.org | python3 - && poetry config virtualenvs.create false; - -# Copy in the dependencies file(s) for caching. One or more of `requirements.txt`, `setup.py`, and `pyproject.toml and -# `poetry.lock` must be present. -COPY pyproject.tom[l] poetry.loc[k] setup.p[y] requirements.tx[t] ./ - -# If `pyproject.toml` is present, install the dependencies only to utilise layer caching for quick rebuilds. -RUN if [ -f "pyproject.toml" ]; then poetry install \ - --no-ansi \ - --no-interaction \ - --no-cache \ - --no-root \ - --only main; \ - fi - -# Copy local code to the application root directory. -COPY . . - -# Install local packages if using poetry. Otherwise, install everything if using `setup.py` or `requirements.txt`. -RUN if [ -f "pyproject.toml" ]; then poetry install --only main; \ - elif [ -f "setup.py" ]; then pip install --upgrade pip && pip install -e .; \ - elif [ -f "requirements.txt" ]; then pip install --upgrade pip && pip install -r requirements.txt; fi - -ENV USE_OCTUE_LOG_HANDLER=1 -ENV COMPUTE_PROVIDER=GOOGLE_KUEUE -CMD ["octue", "question", "ask", "local"]