From d48ee86c07a64023aed168b12b063b1279745643 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Thu, 9 Jan 2025 15:40:21 +0000 Subject: [PATCH 001/216] 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 <marcus@octue.com>", "Thomas Clark <support@octue.com>"] From a74f75213d79e65d9c23ee78f953f566652a0a96 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Thu, 9 Jan 2025 15:42:25 +0000 Subject: [PATCH 002/216] 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 <cortado.codes@protonmail.com> Date: Thu, 9 Jan 2025 15:43:01 +0000 Subject: [PATCH 003/216] 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 <cortado.codes@protonmail.com> Date: Thu, 9 Jan 2025 15:44:02 +0000 Subject: [PATCH 004/216] 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 <cortado.codes@protonmail.com> Date: Thu, 9 Jan 2025 15:44:26 +0000 Subject: [PATCH 005/216] 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 <cortado.codes@protonmail.com> Date: Thu, 9 Jan 2025 17:46:33 +0000 Subject: [PATCH 006/216] 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 <cortado.codes@protonmail.com> Date: Mon, 13 Jan 2025 09:54:20 +0000 Subject: [PATCH 007/216] 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 <marcus@octue.com>", "Thomas Clark <support@octue.com>"] From 96b145eae1f761465d3ea3b9c42ae55e9de80f9d Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Mon, 13 Jan 2025 10:23:55 +0000 Subject: [PATCH 008/216] 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 <cortado.codes@protonmail.com> Date: Mon, 13 Jan 2025 10:37:28 +0000 Subject: [PATCH 009/216] 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 <cortado.codes@protonmail.com> Date: Mon, 13 Jan 2025 10:51:06 +0000 Subject: [PATCH 010/216] 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 <cortado.codes@protonmail.com> Date: Mon, 13 Jan 2025 10:52:10 +0000 Subject: [PATCH 011/216] 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 <cortado.codes@protonmail.com> Date: Mon, 13 Jan 2025 10:56:02 +0000 Subject: [PATCH 012/216] 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 <cortado.codes@protonmail.com> Date: Mon, 13 Jan 2025 10:57:47 +0000 Subject: [PATCH 013/216] 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 <cortado.codes@protonmail.com> Date: Mon, 13 Jan 2025 11:00:58 +0000 Subject: [PATCH 014/216] 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 <cortado.codes@protonmail.com> Date: Mon, 13 Jan 2025 11:56:55 +0000 Subject: [PATCH 015/216] 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 <cortado.codes@protonmail.com> Date: Mon, 13 Jan 2025 13:07:33 +0000 Subject: [PATCH 016/216] 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 <cortado.codes@protonmail.com> Date: Mon, 13 Jan 2025 13:08:04 +0000 Subject: [PATCH 017/216] 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 <cortado.codes@protonmail.com> Date: Mon, 13 Jan 2025 13:09:24 +0000 Subject: [PATCH 018/216] 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 <cortado.codes@protonmail.com> Date: Mon, 13 Jan 2025 14:39:46 +0000 Subject: [PATCH 019/216] 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 <cortado.codes@protonmail.com> Date: Mon, 13 Jan 2025 15:18:23 +0000 Subject: [PATCH 020/216] 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 <cortado.codes@protonmail.com> Date: Mon, 13 Jan 2025 15:30:46 +0000 Subject: [PATCH 021/216] 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 <cortado.codes@protonmail.com> Date: Mon, 13 Jan 2025 15:34:32 +0000 Subject: [PATCH 022/216] 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 <cortado.codes@protonmail.com> Date: Mon, 13 Jan 2025 15:39:57 +0000 Subject: [PATCH 023/216] 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 <cortado.codes@protonmail.com> Date: Mon, 13 Jan 2025 15:43:57 +0000 Subject: [PATCH 024/216] 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 <cortado.codes@protonmail.com> Date: Mon, 13 Jan 2025 15:46:26 +0000 Subject: [PATCH 025/216] 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 <cortado.codes@protonmail.com> Date: Mon, 13 Jan 2025 15:48:41 +0000 Subject: [PATCH 026/216] 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 <cortado.codes@protonmail.com> Date: Mon, 13 Jan 2025 15:50:06 +0000 Subject: [PATCH 027/216] 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 <cortado.codes@protonmail.com> Date: Mon, 13 Jan 2025 17:08:59 +0000 Subject: [PATCH 028/216] 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 <cortado.codes@protonmail.com> Date: Mon, 13 Jan 2025 17:16:10 +0000 Subject: [PATCH 029/216] 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 <cortado.codes@protonmail.com> Date: Mon, 13 Jan 2025 17:23:17 +0000 Subject: [PATCH 030/216] 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 <cortado.codes@protonmail.com> Date: Tue, 14 Jan 2025 13:07:55 +0000 Subject: [PATCH 031/216] 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 <cortado.codes@protonmail.com> Date: Tue, 14 Jan 2025 13:15:48 +0000 Subject: [PATCH 032/216] 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 <cortado.codes@protonmail.com> Date: Thu, 23 Jan 2025 12:41:47 +0000 Subject: [PATCH 033/216] 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 <cortado.codes@protonmail.com> Date: Thu, 23 Jan 2025 12:42:17 +0000 Subject: [PATCH 034/216] 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 <marcus@octue.com>", "Thomas Clark <support@octue.com>"] From 0d7c8c2290c7d6c5b26896dd85e46eaac405706a Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Thu, 23 Jan 2025 12:47:26 +0000 Subject: [PATCH 035/216] 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://<diagnostics-cloud-path>/%s " + "Question %r failed. Run 'octue question diagnostics gs://<diagnostics-cloud-path>/%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 <cortado.codes@protonmail.com> Date: Mon, 27 Jan 2025 17:17:02 +0000 Subject: [PATCH 036/216] 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 <cortado.codes@protonmail.com> Date: Mon, 27 Jan 2025 17:27:02 +0000 Subject: [PATCH 037/216] 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 <cortado.codes@protonmail.com> Date: Mon, 27 Jan 2025 17:38:43 +0000 Subject: [PATCH 038/216] 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 <cortado.codes@protonmail.com> Date: Mon, 27 Jan 2025 17:53:31 +0000 Subject: [PATCH 039/216] 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 <cortado.codes@protonmail.com> Date: Mon, 27 Jan 2025 17:59:21 +0000 Subject: [PATCH 040/216] 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 <cortado.codes@protonmail.com> Date: Mon, 27 Jan 2025 18:15:26 +0000 Subject: [PATCH 041/216] 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 <cortado.codes@protonmail.com> Date: Mon, 27 Jan 2025 18:19:39 +0000 Subject: [PATCH 042/216] 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 <cortado.codes@protonmail.com> Date: Mon, 27 Jan 2025 18:24:48 +0000 Subject: [PATCH 043/216] 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 <cortado.codes@protonmail.com> Date: Tue, 28 Jan 2025 16:09:41 +0000 Subject: [PATCH 044/216] 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 <cortado.codes@protonmail.com> Date: Tue, 28 Jan 2025 16:27:21 +0000 Subject: [PATCH 045/216] 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 <cortado.codes@protonmail.com> Date: Tue, 28 Jan 2025 16:45:50 +0000 Subject: [PATCH 046/216] 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 <cortado.codes@protonmail.com> Date: Mon, 3 Feb 2025 12:11:47 +0000 Subject: [PATCH 047/216] 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 <cortado.codes@protonmail.com> Date: Mon, 3 Feb 2025 12:24:44 +0000 Subject: [PATCH 048/216] 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 <cortado.codes@protonmail.com> Date: Mon, 3 Feb 2025 12:33:11 +0000 Subject: [PATCH 049/216] 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 <cortado.codes@protonmail.com> Date: Tue, 4 Feb 2025 11:41:20 +0000 Subject: [PATCH 050/216] 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 <cortado.codes@protonmail.com> Date: Tue, 4 Feb 2025 12:50:54 +0000 Subject: [PATCH 051/216] 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 <cortado.codes@protonmail.com> Date: Tue, 4 Feb 2025 16:29:52 +0000 Subject: [PATCH 052/216] 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 <cortado.codes@protonmail.com> Date: Tue, 4 Feb 2025 16:32:23 +0000 Subject: [PATCH 053/216] 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 <cortado.codes@protonmail.com> Date: Wed, 5 Feb 2025 13:56:52 +0000 Subject: [PATCH 054/216] 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 <cortado.codes@protonmail.com> Date: Wed, 5 Feb 2025 14:06:07 +0000 Subject: [PATCH 055/216] 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 <cortado.codes@protonmail.com> Date: Wed, 5 Feb 2025 14:06:25 +0000 Subject: [PATCH 056/216] 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 <cortado.codes@protonmail.com> Date: Wed, 5 Feb 2025 14:11:21 +0000 Subject: [PATCH 057/216] 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 <cortado.codes@protonmail.com> Date: Wed, 5 Feb 2025 14:12:14 +0000 Subject: [PATCH 058/216] 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 <cortado.codes@protonmail.com> Date: Wed, 5 Feb 2025 14:16:26 +0000 Subject: [PATCH 059/216] 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 <cortado.codes@protonmail.com> Date: Wed, 5 Feb 2025 14:18:25 +0000 Subject: [PATCH 060/216] 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 <cortado.codes@protonmail.com> Date: Wed, 5 Feb 2025 14:20:33 +0000 Subject: [PATCH 061/216] 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 <cortado.codes@protonmail.com> Date: Wed, 5 Feb 2025 14:21:21 +0000 Subject: [PATCH 062/216] 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 <cortado.codes@protonmail.com> Date: Wed, 5 Feb 2025 14:26:31 +0000 Subject: [PATCH 063/216] 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 <cortado.codes@protonmail.com> Date: Wed, 5 Feb 2025 14:26:53 +0000 Subject: [PATCH 064/216] 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 <cortado.codes@protonmail.com> Date: Wed, 5 Feb 2025 14:51:37 +0000 Subject: [PATCH 065/216] 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 = <<EOF -[ - { - "name": "datetime", - "type": "TIMESTAMP", - "mode": "REQUIRED" - }, - { - "name": "uuid", - "type": "STRING", - "mode": "REQUIRED" - }, - { - "name": "kind", - "type": "STRING", - "mode": "REQUIRED" - }, - { - "name": "event", - "type": "JSON", - "mode": "REQUIRED" - }, - { - "name": "other_attributes", - "type": "JSON", - "mode": "REQUIRED" - }, - { - "name": "originator", - "type": "STRING", - "mode": "REQUIRED" - }, - { - "name": "parent", - "type": "STRING", - "mode": "REQUIRED" - }, - { - "name": "sender", - "type": "STRING", - "mode": "REQUIRED" - }, - { - "name": "sender_type", - "type": "STRING", - "mode": "REQUIRED" - }, - { - "name": "sender_sdk_version", - "type": "STRING", - "mode": "REQUIRED" - }, - { - "name": "recipient", - "type": "STRING", - "mode": "REQUIRED" - }, - { - "name": "originator_question_uuid", - "type": "STRING", - "mode": "REQUIRED" - }, - { - "name": "parent_question_uuid", - "type": "STRING", - "mode": "NULLABLE" - }, - { - "name": "question_uuid", - "type": "STRING", - "mode": "REQUIRED" - }, - { - "name": "backend", - "type": "STRING", - "mode": "REQUIRED" - }, - { - "name": "backend_metadata", - "type": "JSON", - "mode": "REQUIRED" - } -] -EOF -} diff --git a/terraform/functions.tf b/terraform/functions.tf deleted file mode 100644 index 7fca55d58..000000000 --- a/terraform/functions.tf +++ /dev/null @@ -1,41 +0,0 @@ -resource "google_cloudfunctions2_function" "event_handler" { - name = "event-handler" - location = var.region - description = "A function for handling events from Octue services." - - build_config { - runtime = "python312" - entry_point = "store_pub_sub_event_in_bigquery" - source { - storage_source { - bucket = "twined-gcp" - object = "event_handler/0.6.1.zip" - } - } - } - - service_config { - max_instance_count = 100 - available_memory = "256M" - timeout_seconds = 60 - environment_variables = { - BIGQUERY_EVENTS_TABLE = "${google_bigquery_dataset.test_dataset.dataset_id}.${google_bigquery_table.test_table.table_id}" - } - } - - event_trigger { - trigger_region = var.region - event_type = "google.cloud.pubsub.topic.v1.messagePublished" - pubsub_topic = google_pubsub_topic.services_topic.id - retry_policy = "RETRY_POLICY_RETRY" - } - -} - - -resource "google_cloud_run_service_iam_member" "function_invoker" { - location = google_cloudfunctions2_function.event_handler.location - service = google_cloudfunctions2_function.event_handler.name - role = "roles/run.invoker" - member = "allUsers" -} diff --git a/terraform/iam.tf b/terraform/iam.tf deleted file mode 100644 index 1887878b5..000000000 --- a/terraform/iam.tf +++ /dev/null @@ -1,136 +0,0 @@ -resource "google_project_iam_binding" "iam_serviceaccountuser" { - project = var.project - role = "roles/iam.serviceAccountUser" - members = [ - "serviceAccount:${google_service_account.dev_cortadocodes_service_account.email}", - "serviceAccount:${google_service_account.github_actions_service_account.email}", - ] -} - - -resource "google_project_iam_binding" "pubsub_editor" { - project = var.project - role = "roles/pubsub.editor" - members = [ - "serviceAccount:${google_service_account.dev_cortadocodes_service_account.email}", - "serviceAccount:${google_service_account.github_actions_service_account.email}", - ] -} - - -# Allows the GHA to call "namespaces get" for Cloud Run to determine the resulting run URLs of the services. -# This should also allow a service to get its own name by using: -# https://stackoverflow.com/questions/65628822/google-cloud-run-can-a-service-know-its-own-url/65634104#65634104 -resource "google_project_iam_binding" "run_developer" { - project = var.project - role = "roles/run.developer" - members = [ - "serviceAccount:${google_service_account.github_actions_service_account.email}", - ] -} - - -resource "google_project_iam_binding" "artifactregistry_writer" { - project = var.project - role = "roles/artifactregistry.writer" - members = [ - "serviceAccount:${google_service_account.github_actions_service_account.email}", - ] -} - - -resource "google_project_iam_binding" "storage_objectadmin" { - project = var.project - role = "roles/storage.objectAdmin" - members = [ - "serviceAccount:${google_service_account.github_actions_service_account.email}", - ] -} - - -resource "google_project_iam_binding" "errorreporting_writer" { - project = var.project - role = "roles/errorreporting.writer" - members = [ - "serviceAccount:${google_service_account.dev_cortadocodes_service_account.email}", - "serviceAccount:${google_service_account.github_actions_service_account.email}", - ] -} - - -resource "google_project_iam_binding" "bigquery_dataeditor" { - project = var.project - role = "roles/bigquery.dataEditor" - members = [ - "serviceAccount:service-${var.project_number}@gcp-sa-pubsub.iam.gserviceaccount.com", - ] -} - - -resource "google_project_iam_binding" "bigquery_dataviewer" { - project = var.project - role = "roles/bigquery.dataViewer" - members = [ - "serviceAccount:${google_service_account.dev_cortadocodes_service_account.email}", - ] -} - - -resource "google_project_iam_binding" "bigquery_jobuser" { - project = var.project - role = "roles/bigquery.jobUser" - members = [ - "serviceAccount:${google_service_account.dev_cortadocodes_service_account.email}", - ] -} - - -resource "google_project_iam_binding" "bigquery_readsessionuser" { - project = var.project - role = "roles/bigquery.readSessionUser" - members = [ - "serviceAccount:${google_service_account.dev_cortadocodes_service_account.email}", - ] -} - - -resource "google_iam_workload_identity_pool" "github_actions_pool" { - display_name = "github-actions-pool" - project = var.project - workload_identity_pool_id = "github-actions-pool" -} - - -resource "google_iam_workload_identity_pool_provider" "github_actions_provider" { - attribute_mapping = { - "attribute.actor" = "assertion.actor" - "attribute.repository" = "assertion.repository" - "attribute.repository_owner" = "assertion.repository_owner" - "google.subject" = "assertion.sub" - } - display_name = "Github Actions Provider" - project = var.project_number - workload_identity_pool_id = "github-actions-pool" - workload_identity_pool_provider_id = "github-actions-provider" - - oidc { - allowed_audiences = [] - issuer_uri = "https://token.actions.githubusercontent.com" - } -} - -data "google_iam_policy" "github_actions_workload_identity_pool_policy" { - binding { - role = "roles/iam.workloadIdentityUser" - members = [ - "principalSet://iam.googleapis.com/projects/${var.project_number}/locations/global/workloadIdentityPools/${google_iam_workload_identity_pool.github_actions_pool.workload_identity_pool_id}/attribute.repository_owner/${var.github_organisation}" - ] - } -} - - -# Allow a machine under Workload Identity Federation to act as the given service account. -resource "google_service_account_iam_policy" "github_actions_workload_identity_service_account_policy" { - service_account_id = google_service_account.github_actions_service_account.name - policy_data = data.google_iam_policy.github_actions_workload_identity_pool_policy.policy_data -} diff --git a/terraform/iam_service_accounts.tf b/terraform/iam_service_accounts.tf deleted file mode 100644 index fa611b09c..000000000 --- a/terraform/iam_service_accounts.tf +++ /dev/null @@ -1,22 +0,0 @@ -# You need to start with a service account called "terraform" which has both the 'editor' and 'owner' basic permissions. -# This allows it to assign permissions to resources per https://cloud.google.com/iam/docs/understanding-roles -# -# To create domain-named storage buckets using terraform, you first have to verify ownership of the root domain, or -# "property", (eg octue.com) using the google search console. Once verified, you need to add the service account with -# which terraform acts ( eg terraform@octue-sdk-python.iam.gserviceaccount.com ) to Google Search Console > 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 <cortado.codes@protonmail.com> Date: Wed, 5 Feb 2025 14:53:15 +0000 Subject: [PATCH 066/216] 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 <cortado.codes@protonmail.com> Date: Wed, 5 Feb 2025 15:34:33 +0000 Subject: [PATCH 067/216] 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 <cortado.codes@protonmail.com> Date: Wed, 5 Feb 2025 15:40:39 +0000 Subject: [PATCH 068/216] 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 <cortado.codes@protonmail.com> Date: Wed, 5 Feb 2025 15:46:34 +0000 Subject: [PATCH 069/216] 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 <cortado.codes@protonmail.com> Date: Wed, 5 Feb 2025 17:54:09 +0000 Subject: [PATCH 070/216] 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 <cortado.codes@protonmail.com> Date: Wed, 5 Feb 2025 18:21:12 +0000 Subject: [PATCH 071/216] 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 <cortado.codes@protonmail.com> Date: Wed, 5 Feb 2025 19:28:29 +0000 Subject: [PATCH 072/216] 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 <cortado.codes@protonmail.com> Date: Wed, 5 Feb 2025 19:32:45 +0000 Subject: [PATCH 073/216] 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 <cortado.codes@protonmail.com> Date: Wed, 5 Feb 2025 19:57:25 +0000 Subject: [PATCH 074/216] 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 <cortado.codes@protonmail.com> Date: Thu, 6 Feb 2025 11:25:03 +0000 Subject: [PATCH 075/216] 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 <cortado.codes@protonmail.com> Date: Thu, 6 Feb 2025 11:27:00 +0000 Subject: [PATCH 076/216] 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 <cortado.codes@protonmail.com> Date: Thu, 6 Feb 2025 11:29:28 +0000 Subject: [PATCH 077/216] 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 <cortado.codes@protonmail.com> Date: Thu, 6 Feb 2025 11:33:07 +0000 Subject: [PATCH 078/216] 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 <cortado.codes@protonmail.com> Date: Thu, 6 Feb 2025 11:34:14 +0000 Subject: [PATCH 079/216] 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 <cortado.codes@protonmail.com> Date: Thu, 6 Feb 2025 11:37:22 +0000 Subject: [PATCH 080/216] 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 <cortado.codes@protonmail.com> Date: Thu, 6 Feb 2025 11:38:46 +0000 Subject: [PATCH 081/216] 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 <cortado.codes@protonmail.com> Date: Thu, 6 Feb 2025 11:41:02 +0000 Subject: [PATCH 082/216] 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 <cortado.codes@protonmail.com> Date: Thu, 6 Feb 2025 12:12:32 +0000 Subject: [PATCH 083/216] 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 74f11d8f11597d347bbe91930c859405af08352c Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Tue, 11 Feb 2025 15:21:59 +0000 Subject: [PATCH 084/216] REF: Factor out attributes into `EventAttributes` class skipci --- octue/cloud/emulators/_pub_sub.py | 45 ++-- octue/cloud/events/attributes.py | 95 +++++++++ octue/cloud/events/utils.py | 54 +---- octue/cloud/events/validation.py | 20 +- octue/cloud/pub_sub/logging.py | 44 +--- octue/cloud/pub_sub/service.py | 308 +++++----------------------- tests/cloud/pub_sub/test_events.py | 113 +++------- tests/cloud/pub_sub/test_logging.py | 39 ++-- 8 files changed, 226 insertions(+), 492 deletions(-) create mode 100644 octue/cloud/events/attributes.py diff --git a/octue/cloud/emulators/_pub_sub.py b/octue/cloud/emulators/_pub_sub.py index c9c01089c..dc9edc8ee 100644 --- a/octue/cloud/emulators/_pub_sub.py +++ b/octue/cloud/emulators/_pub_sub.py @@ -405,31 +405,28 @@ def ask( # If the originator isn't provided, assume that this service revision is the originator. originator = originator or self.id + attributes = make_minimal_dictionary( + datetime="2024-04-11T10:46:48.236064", + uuid="a9de11b1-e88f-43fa-b3a4-40a590c3443f", + question_uuid=question_uuid, + parent_question_uuid=parent_question_uuid, + originator_question_uuid=originator_question_uuid, + forward_logs=subscribe_to_logs, + save_diagnostics=save_diagnostics, + parent=self.id, + originator=originator, + sender=self.id, + sender_type=PARENT_SENDER_TYPE, + sender_sdk_version=parent_sdk_version, + recipient=service_id, + retry_count=retry_count, + cpus=cpus, + memory=memory, + ephemeral_storage=ephemeral_storage, + ) + try: - self.children[service_id].answer( - MockMessage.from_primitive( - data=question, - attributes={ - "datetime": "2024-04-11T10:46:48.236064", - "uuid": "a9de11b1-e88f-43fa-b3a4-40a590c3443f", - "question_uuid": question_uuid, - "parent_question_uuid": parent_question_uuid, - "originator_question_uuid": originator_question_uuid, - "forward_logs": subscribe_to_logs, - "save_diagnostics": save_diagnostics, - "parent": self.id, - "originator": originator, - "sender": self.id, - "sender_type": PARENT_SENDER_TYPE, - "sender_sdk_version": parent_sdk_version, - "recipient": service_id, - "retry_count": retry_count, - "cpus": cpus, - "memory": memory, - "ephemeral_storage": ephemeral_storage, - }, - ) - ) + self.children[service_id].answer(MockMessage.from_primitive(data=question, attributes=attributes)) except Exception as e: # noqa logger.exception(e) diff --git a/octue/cloud/events/attributes.py b/octue/cloud/events/attributes.py new file mode 100644 index 000000000..180a82a76 --- /dev/null +++ b/octue/cloud/events/attributes.py @@ -0,0 +1,95 @@ +import datetime as dt +import json +import uuid as uuid_library + +from octue.cloud import LOCAL_SDK_VERSION +from octue.utils.dictionaries import make_minimal_dictionary + +SENDER_TYPE_OPPOSITES = {"CHILD": "PARENT", "PARENT": "CHILD"} + + +class EventAttributes: + def __init__( + self, + originator_question_uuid, + parent, + originator, + sender, + sender_type, + recipient, + uuid=None, + datetime=None, + question_uuid=None, + parent_question_uuid=None, + sender_sdk_version=None, + retry_count=0, + forward_logs=True, + save_diagnostics=True, + cpus=None, + memory=None, + ephemeral_storage=None, + ): + # Attributes for all event kinds. + self.uuid = uuid or str(uuid_library.uuid4()) + self.datetime = datetime or dt.datetime.now(tz=dt.timezone.utc).isoformat() + self.question_uuid = question_uuid or str(uuid_library.uuid4()) + self.parent_question_uuid = parent_question_uuid + self.originator_question_uuid = originator_question_uuid + self.parent = parent + self.originator = originator + self.sender = sender + self.sender_type = sender_type + self.sender_sdk_version = sender_sdk_version or LOCAL_SDK_VERSION + self.recipient = recipient + self.retry_count = int(retry_count) + + # Question event attributes. + self.forward_logs = bool(forward_logs) + self.save_diagnostics = save_diagnostics + self.cpus = cpus + self.memory = memory + self.ephemeral_storage = ephemeral_storage + + def make_response_attributes(self): + attributes = self.to_dict() + attributes["sender"] = self.recipient + attributes["sender_type"] = SENDER_TYPE_OPPOSITES[self.sender_type] + attributes["sender_sdk_version"] = LOCAL_SDK_VERSION + attributes["recipient"] = self.sender + return EventAttributes(**attributes) + + def to_dict(self): + return make_minimal_dictionary( + uuid=self.uuid, + datetime=self.datetime, + question_uuid=self.question_uuid, + parent_question_uuid=self.parent_question_uuid, + originator_question_uuid=self.originator_question_uuid, + parent=self.parent, + originator=self.originator, + sender=self.sender, + sender_type=self.sender_type, + sender_sdk_version=self.sender_sdk_version, + recipient=self.recipient, + retry_count=self.retry_count, + forward_logs=self.forward_logs, + save_diagnostics=self.save_diagnostics, + cpus=self.cpus, + memory=self.memory, + ephemeral_storage=self.ephemeral_storage, + ) + + def to_serialised_attributes(self): + serialised_attributes = {} + + for key, value in self.to_dict().items(): + if isinstance(value, bool): + value = str(int(value)) + elif isinstance(value, (int, float)): + value = str(value) + elif value is None: + value = json.dumps(value) + + serialised_attributes[key] = value + + return serialised_attributes diff --git a/octue/cloud/events/utils.py b/octue/cloud/events/utils.py index 933ea54d2..f7c45433c 100644 --- a/octue/cloud/events/utils.py +++ b/octue/cloud/events/utils.py @@ -1,7 +1,6 @@ -import datetime import uuid -from octue.cloud import LOCAL_SDK_VERSION +from octue.cloud.events.attributes import EventAttributes from octue.utils.dictionaries import make_minimal_dictionary @@ -26,7 +25,7 @@ def make_question_event( if not attributes: question_uuid = question_uuid or str(uuid.uuid4()) - attributes = make_attributes( + attributes = EventAttributes( question_uuid=question_uuid, parent_question_uuid=question_uuid, originator_question_uuid=question_uuid, @@ -41,52 +40,5 @@ def make_question_event( return { "event": make_minimal_dictionary(input_values=input_values, input_manifest=input_manifest, kind="question"), - "attributes": attributes, + "attributes": attributes.to_dict(), } - - -def make_attributes( - parent_question_uuid, - originator_question_uuid, - parent, - originator, - sender, - sender_type, - recipient, - question_uuid=None, - retry_count=0, - forward_logs=None, - save_diagnostics=None, - cpus=None, - memory=None, - ephemeral_storage=None, -): - attributes = { - "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_type": sender_type, - "sender_sdk_version": LOCAL_SDK_VERSION, - "recipient": recipient, - "retry_count": int(retry_count), - } - - if sender_type == "PARENT": - if forward_logs: - attributes["forward_logs"] = bool(forward_logs) - - attributes.update( - make_minimal_dictionary( - save_diagnostics=save_diagnostics, - cpus=cpus, - memory=memory, - ephemeral_storage=ephemeral_storage, - ) - ) - - return attributes diff --git a/octue/cloud/events/validation.py b/octue/cloud/events/validation.py index e1e37e558..a99546ffa 100644 --- a/octue/cloud/events/validation.py +++ b/octue/cloud/events/validation.py @@ -51,13 +51,8 @@ "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."}, - ] + "type": "string", + "description": "If this isn't the originating question, the UUID of the question that triggered this question. If it is, don't provide this.", }, "originator_question_uuid": { "type": "string", @@ -106,7 +101,6 @@ "datetime", "uuid", "question_uuid", - "parent_question_uuid", "originator_question_uuid", "forward_logs", "save_diagnostics", @@ -137,13 +131,8 @@ "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."}, - ] + "type": "string", + "description": "If this isn't the originating question, the UUID of the question that triggered this question. If it is, don't provide this.", }, "originator_question_uuid": { "type": "string", @@ -188,7 +177,6 @@ "datetime", "uuid", "question_uuid", - "parent_question_uuid", "originator_question_uuid", "parent", "originator", diff --git a/octue/cloud/pub_sub/logging.py b/octue/cloud/pub_sub/logging.py index cbe344e78..0e4b134dc 100644 --- a/octue/cloud/pub_sub/logging.py +++ b/octue/cloud/pub_sub/logging.py @@ -8,39 +8,14 @@ class GoogleCloudPubSubHandler(logging.Handler): """A log handler that publishes log records to a Google Cloud Pub/Sub topic. :param callable event_emitter: the `_emit_event` method of the service that instantiated this instance - :param str question_uuid: the UUID of the question to handle log records for - :param str|None parent_question_uuid: the UUID of the question these log records are related to - :param str|None originator_question_uuid: the UUID of the question that triggered all ancestor questions of this question - :param str parent: the SRUID of the parent that asked the question these log records are related to - :param str originator: the SRUID of the service revision that triggered the tree of questions these log records are related to - :param str recipient: the SRUID of the service to send these log records to - :param int retry_count: the retry count of the question (this is zero if it's the first attempt at the question) + :param octue.cloud.events.attributes.EventAttributes attributes: :param float timeout: timeout in seconds for attempting to publish each log record :return None: """ - def __init__( - self, - event_emitter, - question_uuid, - parent_question_uuid, - originator_question_uuid, - parent, - originator, - recipient, - retry_count, - timeout=60, - *args, - **kwargs, - ): + def __init__(self, event_emitter, attributes, timeout=60, *args, **kwargs): super().__init__(*args, **kwargs) - self.question_uuid = question_uuid - self.parent_question_uuid = parent_question_uuid - self.originator_question_uuid = originator_question_uuid - self.parent = parent - self.originator = originator - self.recipient = recipient - self.retry_count = retry_count + self.attributes = attributes self.timeout = timeout self._emit_event = event_emitter @@ -56,22 +31,15 @@ def emit(self, record): "kind": "log_record", "log_record": self._convert_log_record_to_primitives(record), }, - parent=self.parent, - originator=self.originator, - recipient=self.recipient, - retry_count=self.retry_count, - question_uuid=self.question_uuid, - parent_question_uuid=self.parent_question_uuid, - 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"}, + attributes=self.attributes, wait=False, ) except Exception: # noqa self.handleError(record) - def _convert_log_record_to_primitives(self, log_record): + @staticmethod + def _convert_log_record_to_primitives(log_record): """Convert a log record to JSON-serialisable primitives by interpolating the args into the message, and removing the exception info, which is potentially not JSON-serialisable. This is similar to the approach in `logging.handlers.SocketHandler.makePickle`. Also strip any ANSI escape sequences from the message. diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index 87fc14cf0..392d20a8a 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -13,8 +13,8 @@ from octue.cloud import LOCAL_SDK_VERSION from octue.cloud.events import OCTUE_SERVICES_TOPIC_NAME +from octue.cloud.events.attributes import EventAttributes 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.bigquery import get_events @@ -62,7 +62,6 @@ class Service: :param octue.resources.service_backends.ServiceBackend backend: the object representing the type of backend the service uses :param str|None service_id: a unique ID to give to the service (any string); a UUID is generated if none is given :param callable|None run_function: the function the service should run when it is called - :param str|None name: an optional name to use for the service to override its ID in its string representation :param iter(dict)|None service_registries: the names and endpoints of the registries used to resolve service revisions when asking questions; these should be in priority order (highest priority first) :return None: """ @@ -204,65 +203,45 @@ def answer(self, question, heartbeat_interval=120, timeout=30): :return None: """ try: - ( - question, - question_uuid, - parent_question_uuid, - originator_question_uuid, - forward_logs, - parent_sdk_version, - save_diagnostics, - parent, - originator, - retry_count, - ) = self._parse_question(question) + question, question_attributes = self._parse_question(question) except jsonschema.ValidationError: return heartbeater = None - - routing_metadata = { - "question_uuid": question_uuid, - "parent_question_uuid": parent_question_uuid, - "originator_question_uuid": originator_question_uuid, - "parent": parent, - "originator": originator, - "retry_count": retry_count, - } + response_attributes = question_attributes.make_response_attributes() try: - self._send_delivery_acknowledgment(**routing_metadata) + self._send_delivery_acknowledgment(response_attributes) heartbeater = RepeatingTimer( interval=heartbeat_interval, function=self._send_heartbeat, - kwargs=routing_metadata, + kwargs={"attributes": response_attributes}, ) heartbeater.daemon = True heartbeater.start() - if forward_logs: + if question_attributes.forward_logs: analysis_log_handler = GoogleCloudPubSubHandler( event_emitter=self._emit_event, - recipient=parent, - **routing_metadata, + attributes=response_attributes, ) else: analysis_log_handler = None - handle_monitor_message = functools.partial(self._send_monitor_message, **routing_metadata) + handle_monitor_message = functools.partial(self._send_monitor_message, attributes=response_attributes) analysis = self.run_function( - analysis_id=question_uuid, + analysis_id=question_attributes.question_uuid, input_values=question.get("input_values"), input_manifest=question.get("input_manifest"), children=question.get("children"), analysis_log_handler=analysis_log_handler, handle_monitor_message=handle_monitor_message, - save_diagnostics=save_diagnostics, - originator_question_uuid=originator_question_uuid, - originator=originator, + save_diagnostics=question_attributes.save_diagnostics, + originator_question_uuid=question_attributes.originator_question_uuid, + originator=question_attributes.originator, ) result = make_minimal_dictionary(kind="result", output_values=analysis.output_values) @@ -270,23 +249,20 @@ 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( - event=result, - recipient=parent, - attributes={"sender_type": CHILD_SENDER_TYPE}, - timeout=timeout, - **routing_metadata, - ) - + self._emit_event(event=result, attributes=response_attributes, timeout=timeout) heartbeater.cancel() - logger.info("%r answered question %r.", self, question_uuid) + logger.info("%r answered question %r.", self, question_attributes.question_uuid) except BaseException as error: # noqa if heartbeater is not None: heartbeater.cancel() - warn_if_incompatible(child_sdk_version=LOCAL_SDK_VERSION, parent_sdk_version=parent_sdk_version) - self.send_exception(timeout=timeout, **routing_metadata) + warn_if_incompatible( + child_sdk_version=LOCAL_SDK_VERSION, + parent_sdk_version=question_attributes.sender_sdk_version, + ) + + self.send_exception(attributes=response_attributes, timeout=timeout) raise error def ask( @@ -467,41 +443,14 @@ def cancel(self, question_uuid, event_store_table_id, timeout=30): if question_finished: raise ValueError(f"Question {question_uuid!r} has already finished.") - question_attributes = questions[0]["attributes"] - - self._emit_event( - {"kind": "cancellation"}, - question_uuid=question_uuid, - 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, - ) - + question_attributes = EventAttributes(**questions[0]["attributes"]) + self._emit_event({"kind": "cancellation"}, attributes=question_attributes, timeout=timeout) logger.info("Cancellation of question %r requested.", question_uuid) - def send_exception( - self, - question_uuid, - parent_question_uuid, - originator_question_uuid, - parent, - originator, - retry_count, - timeout=30, - ): + def send_exception(self, attributes, timeout=30): """Serialise and send the exception being handled to the parent. - :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 - :param str|None originator_question_uuid: the UUID of the question that triggered all ancestor questions of this question - :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 octue.cloud.events.attributes.EventAttributes attributes: :param float|None timeout: time in seconds to keep retrying sending of the exception :return None: """ @@ -515,31 +464,11 @@ def send_exception( "exception_message": exception_message, "exception_traceback": exception["traceback"], }, - 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": CHILD_SENDER_TYPE}, + attributes=attributes, timeout=timeout, ) - def _emit_event( - self, - event, - question_uuid, - parent_question_uuid, - originator_question_uuid, - parent, - originator, - recipient, - retry_count, - attributes=None, - wait=True, - timeout=30, - ): + def _emit_event(self, event, attributes, wait=True, timeout=30): """Emit a JSON-serialised event as a Pub/Sub message to the services topic with optional message attributes. Extra attributes can be added to an event via the `attributes` argument but the following attributes are always included: @@ -555,51 +484,17 @@ def _emit_event( - `retry_count` - `datetime` - :param dict event: JSON-serialisable data to emit as an event - :param str question_uuid: - :param str|None parent_question_uuid: the UUID of the question that triggered this question - :param str|None originator_question_uuid: the UUID of the question that triggered all ancestor questions of this question - :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 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 octue.cloud.events.attributes.EventAttributes attributes: :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: """ - attributes = attributes or {} - - 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 = {} - - for key, value in attributes.items(): - if isinstance(value, bool): - value = str(int(value)) - elif isinstance(value, (int, float)): - value = str(value) - elif value is None: - value = json.dumps(value) - - converted_attributes[key] = value - future = self.publisher.publish( topic=self.services_topic.path, data=json.dumps(event, cls=OctueJSONEncoder).encode(), - ordering_key=question_uuid, + ordering_key=attributes.question_uuid, retry=retry.Retry(deadline=timeout), - **converted_attributes, + **attributes.to_serialised_attributes(), ) if wait: @@ -647,147 +542,62 @@ def _send_question( input_manifest.use_signed_urls_for_datasets() question["input_manifest"] = input_manifest.to_primitive() - self._emit_event( - event=question, + question_attributes = EventAttributes( question_uuid=question_uuid, parent_question_uuid=parent_question_uuid, originator_question_uuid=originator_question_uuid, parent=self.id, originator=originator, + sender=self.id, + sender_type=PARENT_SENDER_TYPE, recipient=recipient, retry_count=retry_count, - attributes={ - "forward_logs": forward_logs, - "save_diagnostics": save_diagnostics, - "sender_type": PARENT_SENDER_TYPE, - "cpus": cpus, - "memory": memory, - "ephemeral_storage": ephemeral_storage, - }, - timeout=timeout, + forward_logs=forward_logs, + save_diagnostics=save_diagnostics, + cpus=cpus, + memory=memory, + ephemeral_storage=ephemeral_storage, ) + self._emit_event(event=question, attributes=question_attributes, timeout=timeout) logger.info("%r asked a question %r to service %r.", self, question_uuid, recipient) - def _send_delivery_acknowledgment( - self, - question_uuid, - parent_question_uuid, - originator_question_uuid, - parent, - originator, - retry_count, - timeout=30, - ): + def _send_delivery_acknowledgment(self, attributes, timeout=30): """Send an acknowledgement of question receipt to the parent. - :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 - :param str|None originator_question_uuid: the UUID of the question that triggered all ancestor questions of this question - :param str parent: the SRUID of the service 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 octue.cloud.events.attributes.EventAttributes attributes: :param float timeout: time in seconds after which to give up sending :return None: """ - self._emit_event( - {"kind": "delivery_acknowledgement"}, - question_uuid=question_uuid, - parent_question_uuid=parent_question_uuid, - originator_question_uuid=originator_question_uuid, - timeout=timeout, - parent=parent, - originator=originator, - 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) + self._emit_event({"kind": "delivery_acknowledgement"}, attributes=attributes, timeout=timeout, wait=False) + logger.info("%r acknowledged receipt of question %r.", self, attributes.question_uuid) - def _send_heartbeat( - self, - question_uuid, - parent_question_uuid, - originator_question_uuid, - parent, - originator, - retry_count, - timeout=30, - ): + def _send_heartbeat(self, attributes, timeout=30): """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 - :param str|None originator_question_uuid: the UUID of the question that triggered all ancestor questions of this question - :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 octue.cloud.events.attributes.EventAttributes attributes: :param float timeout: time in seconds after which to give up sending :return None: """ - self._emit_event( - {"kind": "heartbeat"}, - 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": CHILD_SENDER_TYPE}, - timeout=timeout, - wait=False, - ) - + self._emit_event({"kind": "heartbeat"}, attributes=attributes, timeout=timeout, wait=False) logger.debug("Heartbeat sent by %r.", self) - def _send_monitor_message( - self, - data, - question_uuid, - parent_question_uuid, - originator_question_uuid, - parent, - originator, - retry_count, - timeout=30, - ): + def _send_monitor_message(self, data, attributes, timeout=30): """Send a monitor message to the parent. :param any data: the data to send as a monitor message - :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 - :param str|None originator_question_uuid: the UUID of the question that triggered all ancestor questions of this question - :param str parent: the SRUID of the service 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 octue.cloud.events.attributes.EventAttributes attributes: :param float timeout: time in seconds to retry sending the message :return None: """ - self._emit_event( - {"kind": "monitor_message", "data": data}, - 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, - timeout=timeout, - attributes={"sender_type": CHILD_SENDER_TYPE}, - wait=False, - ) - + self._emit_event({"kind": "monitor_message", "data": data}, attributes=attributes, timeout=timeout, wait=False) logger.debug("Monitor message sent by %r.", self) def _parse_question(self, question): """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 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) + :return (dict, octue.cloud.events.attributes.EventAttributes): the question's event and its attributes """ logger.info("%r received a question.", self) @@ -810,19 +620,9 @@ def _parse_question(self, question): ) logger.info("%r parsed question %r successfully.", self, attributes["question_uuid"]) + attributes = EventAttributes(**attributes) - if attributes["retry_count"] > 0: - logger.warning("This is retry %d for question %r.", attributes["retry_count"], attributes["question_uuid"]) - - return ( - event, - attributes["question_uuid"], - attributes["parent_question_uuid"], - attributes["originator_question_uuid"], - attributes["forward_logs"], - attributes["sender_sdk_version"], - attributes["save_diagnostics"], - attributes["parent"], - attributes["originator"], - attributes["retry_count"], - ) + if attributes.retry_count > 0: + logger.warning("This is retry %d for question %r.", attributes.retry_count, attributes.question_uuid) + + return event, attributes diff --git a/tests/cloud/pub_sub/test_events.py b/tests/cloud/pub_sub/test_events.py index 614f58831..34c93d821 100644 --- a/tests/cloud/pub_sub/test_events.py +++ b/tests/cloud/pub_sub/test_events.py @@ -1,10 +1,11 @@ import datetime import os -import uuid from unittest.mock import patch +import uuid from octue.cloud.emulators._pub_sub import MESSAGES, MockMessage, MockService, MockSubscription from octue.cloud.emulators.service import ServicePatcher +from octue.cloud.events.attributes import EventAttributes from octue.cloud.pub_sub.events import GoogleCloudPubSubEventHandler from octue.resources.service_backends import GCPPubSubBackend from tests import TEST_PROJECT_NAME @@ -30,6 +31,16 @@ def setUpClass(cls): backend=GCPPubSubBackend(project_name=TEST_PROJECT_NAME), ) + cls.attributes = EventAttributes( + question_uuid=cls.question_uuid, + originator_question_uuid=cls.question_uuid, + parent=cls.parent.id, + originator=cls.parent.id, + sender=cls.parent.id, + sender_type="CHILD", + recipient=cls.parent.id, + ) + @classmethod def tearDownClass(cls): """Stop the services patcher. @@ -68,36 +79,14 @@ def test_handle_events(self): child = MockService(backend=GCPPubSubBackend(project_name=TEST_PROJECT_NAME)) events = [ - { - "event": {"kind": "test", "order": 0}, - "attributes": {"sender_type": "CHILD"}, - }, - { - "event": {"kind": "test", "order": 1}, - "attributes": {"sender_type": "CHILD"}, - }, - { - "event": {"kind": "test", "order": 2}, - "attributes": {"sender_type": "CHILD"}, - }, - { - "event": {"kind": "finish-test", "order": 3}, - "attributes": {"sender_type": "CHILD"}, - }, + {"event": {"kind": "test", "order": 0}}, + {"event": {"kind": "test", "order": 1}}, + {"event": {"kind": "test", "order": 2}}, + {"event": {"kind": "finish-test", "order": 3}}, ] for event in events: - child._emit_event( - event=event["event"], - question_uuid=self.question_uuid, - parent_question_uuid=None, - originator_question_uuid=self.question_uuid, - attributes=event["attributes"], - parent=self.parent.id, - originator=self.parent.id, - recipient=self.parent.id, - retry_count=0, - ) + child._emit_event(event=event["event"], attributes=self.attributes) result = event_handler.handle_events() self.assertEqual(result, "This is the result.") @@ -126,32 +115,13 @@ def test_no_timeout(self): child = MockService(backend=GCPPubSubBackend(project_name=TEST_PROJECT_NAME)) events = [ - { - "event": {"kind": "test", "order": 0}, - "attributes": {"sender_type": "CHILD"}, - }, - { - "event": {"kind": "test", "order": 1}, - "attributes": {"sender_type": "CHILD"}, - }, - { - "event": {"kind": "finish-test", "order": 2}, - "attributes": {"sender_type": "CHILD"}, - }, + {"event": {"kind": "test", "order": 0}}, + {"event": {"kind": "test", "order": 1}}, + {"event": {"kind": "finish-test", "order": 2}}, ] for event in events: - child._emit_event( - event=event["event"], - question_uuid=self.question_uuid, - parent_question_uuid=None, - originator_question_uuid=self.question_uuid, - attributes=event["attributes"], - parent=self.parent.id, - originator=self.parent.id, - recipient=self.parent.id, - retry_count=0, - ) + child._emit_event(event=event["event"], attributes=self.attributes) result = event_handler.handle_events(timeout=None) @@ -167,27 +137,14 @@ def test_delivery_acknowledgement(self): child = MockService(backend=GCPPubSubBackend(project_name=TEST_PROJECT_NAME)) events = [ - { - "event": {"kind": "delivery_acknowledgement", "order": 0}, - "attributes": {"sender_type": "CHILD"}, - }, - { - "event": {"kind": "result", "order": 1}, - "attributes": {"sender_type": "CHILD"}, - }, + {"event": {"kind": "delivery_acknowledgement", "order": 0}}, + {"event": {"kind": "result", "order": 1}}, ] for event in events: child._emit_event( event=event["event"], - question_uuid=self.question_uuid, - parent_question_uuid=None, - originator_question_uuid=self.question_uuid, - attributes=event["attributes"], - parent=self.parent.id, - originator=self.parent.id, - recipient=self.parent.id, - retry_count=0, + attributes=self.attributes, ) result = event_handler.handle_events() @@ -220,28 +177,12 @@ def test_error_not_raised_if_heartbeat_has_been_received_in_maximum_allowed_inte event_handler._last_heartbeat = datetime.datetime.now() events = [ - { - "event": {"kind": "delivery_acknowledgement", "order": 0}, - "attributes": {"sender_type": "CHILD"}, - }, - { - "event": {"kind": "result", "order": 1}, - "attributes": {"sender_type": "CHILD"}, - }, + {"event": {"kind": "delivery_acknowledgement", "order": 0}}, + {"event": {"kind": "result", "order": 1}}, ] for event in events: - child._emit_event( - event=event["event"], - question_uuid=self.question_uuid, - parent_question_uuid=None, - originator_question_uuid=self.question_uuid, - attributes=event["attributes"], - parent=self.parent.id, - originator=self.parent.id, - recipient=self.parent.id, - retry_count=0, - ) + child._emit_event(event=event["event"], attributes=self.attributes) with patch( "octue.cloud.pub_sub.events.GoogleCloudPubSubEventHandler._time_since_last_heartbeat", diff --git a/tests/cloud/pub_sub/test_logging.py b/tests/cloud/pub_sub/test_logging.py index 6e3ecfad7..36518bf94 100644 --- a/tests/cloud/pub_sub/test_logging.py +++ b/tests/cloud/pub_sub/test_logging.py @@ -5,10 +5,23 @@ from octue.cloud.emulators._pub_sub import MESSAGES, MockService from octue.cloud.emulators.service import ServicePatcher +from octue.cloud.events.attributes import EventAttributes from octue.cloud.pub_sub.logging import GoogleCloudPubSubHandler from octue.resources.service_backends import GCPPubSubBackend from tests.base import BaseTestCase +QUESTION_UUID = "96d69278-44ac-4631-aeea-c90fb08a1b2b" + +ATTRIBUTES = EventAttributes( + question_uuid=QUESTION_UUID, + originator_question_uuid=QUESTION_UUID, + parent="another/service:1.0.0", + originator="another/service:1.0.0", + sender="another/service:1.0.0", + sender_type="CHILD", + recipient="another/service:1.0.0", +) + class NonJSONSerialisable: def __repr__(self): @@ -36,23 +49,13 @@ def tearDownClass(cls): def test_emit(self): """Test the log message is published when `GoogleCloudPubSubHandler.emit` is called.""" - question_uuid = "96d69278-44ac-4631-aeea-c90fb08a1b2b" log_record = makeLogRecord({"msg": "Starting analysis."}) service = MockService(backend=GCPPubSubBackend(project_name="blah")) - GoogleCloudPubSubHandler( - event_emitter=service._emit_event, - question_uuid=question_uuid, - parent_question_uuid=None, - originator_question_uuid=question_uuid, - parent="another/service:1.0.0", - originator="another/service:1.0.0", - recipient="another/service:1.0.0", - retry_count=0, - ).emit(log_record) + GoogleCloudPubSubHandler(event_emitter=service._emit_event, attributes=ATTRIBUTES).emit(log_record) self.assertEqual( - json.loads(MESSAGES[question_uuid][0].data.decode())["log_record"]["msg"], + json.loads(MESSAGES[QUESTION_UUID][0].data.decode())["log_record"]["msg"], "Starting analysis.", ) @@ -60,7 +63,6 @@ def test_emit_with_non_json_serialisable_args(self): """Test that non-JSON-serialisable arguments to log messages are converted to their string representation before being serialised and published to the Pub/Sub topic. """ - question_uuid = "96d69278-44ac-4631-aeea-c90fb08a1b2b" non_json_serialisable_thing = NonJSONSerialisable() # Check that it can't be serialised to JSON. @@ -74,16 +76,7 @@ def test_emit_with_non_json_serialisable_args(self): service = MockService(backend=GCPPubSubBackend(project_name="blah")) with patch("octue.cloud.emulators._pub_sub.MockPublisher.publish") as mock_publish: - GoogleCloudPubSubHandler( - event_emitter=service._emit_event, - question_uuid=question_uuid, - parent_question_uuid=None, - originator_question_uuid=question_uuid, - parent="another/service:1.0.0", - originator="another/service:1.0.0", - recipient="another/service:1.0.0", - retry_count=0, - ).emit(record) + GoogleCloudPubSubHandler(event_emitter=service._emit_event, attributes=ATTRIBUTES).emit(record) self.assertEqual( json.loads(mock_publish.call_args.kwargs["data"].decode())["log_record"]["msg"], From 0a49780a4b4d029ad244f5c2dae1801064dbb5ed Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Tue, 11 Feb 2025 15:47:34 +0000 Subject: [PATCH 085/216] 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 <cortado.codes@protonmail.com> Date: Tue, 11 Feb 2025 15:48:43 +0000 Subject: [PATCH 086/216] 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 <cortado.codes@protonmail.com> Date: Tue, 11 Feb 2025 15:54:13 +0000 Subject: [PATCH 087/216] 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 <cortado.codes@protonmail.com> Date: Tue, 11 Feb 2025 15:59:46 +0000 Subject: [PATCH 088/216] 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 <cortado.codes@protonmail.com> Date: Tue, 11 Feb 2025 16:58:59 +0000 Subject: [PATCH 089/216] 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 <cortado.codes@protonmail.com> Date: Tue, 11 Feb 2025 17:05:22 +0000 Subject: [PATCH 090/216] 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 <cortado.codes@protonmail.com> Date: Tue, 11 Feb 2025 17:07:02 +0000 Subject: [PATCH 091/216] 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 <cortado.codes@protonmail.com> Date: Tue, 11 Feb 2025 17:09:14 +0000 Subject: [PATCH 092/216] 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 <cortado.codes@protonmail.com> Date: Tue, 11 Feb 2025 17:18:15 +0000 Subject: [PATCH 093/216] 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 <cortado.codes@protonmail.com> Date: Tue, 11 Feb 2025 17:23:37 +0000 Subject: [PATCH 094/216] 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 <cortado.codes@protonmail.com> Date: Tue, 11 Feb 2025 17:26:27 +0000 Subject: [PATCH 095/216] 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 <cortado.codes@protonmail.com> Date: Tue, 11 Feb 2025 17:45:23 +0000 Subject: [PATCH 096/216] 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_<analysis_id>.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 <cortado.codes@protonmail.com> Date: Tue, 11 Feb 2025 17:49:21 +0000 Subject: [PATCH 097/216] 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 <cortado.codes@protonmail.com> Date: Tue, 11 Feb 2025 18:10:01 +0000 Subject: [PATCH 098/216] 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 <cortado.codes@protonmail.com> Date: Tue, 11 Feb 2025 18:13:14 +0000 Subject: [PATCH 099/216] 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 <cortado.codes@protonmail.com> Date: Tue, 11 Feb 2025 18:17:03 +0000 Subject: [PATCH 100/216] 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 <cortado.codes@protonmail.com> Date: Tue, 11 Feb 2025 18:36:06 +0000 Subject: [PATCH 101/216] 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 <cortado.codes@protonmail.com> Date: Tue, 11 Feb 2025 18:39:13 +0000 Subject: [PATCH 102/216] 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 <cortado.codes@protonmail.com> Date: Tue, 11 Feb 2025 18:39:46 +0000 Subject: [PATCH 103/216] 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 <cortado.codes@protonmail.com> Date: Tue, 11 Feb 2025 18:40:31 +0000 Subject: [PATCH 104/216] 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 <cortado.codes@protonmail.com> Date: Tue, 11 Feb 2025 18:41:00 +0000 Subject: [PATCH 105/216] 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 <cortado.codes@protonmail.com> Date: Wed, 12 Feb 2025 12:07:45 +0000 Subject: [PATCH 106/216] 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 <cortado.codes@protonmail.com> Date: Wed, 12 Feb 2025 12:11:06 +0000 Subject: [PATCH 107/216] 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 <cortado.codes@protonmail.com> Date: Wed, 12 Feb 2025 12:20:39 +0000 Subject: [PATCH 108/216] 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 <cortado.codes@protonmail.com> Date: Wed, 12 Feb 2025 12:55:14 +0000 Subject: [PATCH 109/216] 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 <cortado.codes@protonmail.com> Date: Wed, 12 Feb 2025 13:08:23 +0000 Subject: [PATCH 110/216] 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 <cortado.codes@protonmail.com> Date: Wed, 12 Feb 2025 13:57:41 +0000 Subject: [PATCH 111/216] 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 <cortado.codes@protonmail.com> Date: Wed, 12 Feb 2025 14:15:02 +0000 Subject: [PATCH 112/216] 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 <cortado.codes@protonmail.com> Date: Wed, 12 Feb 2025 15:44:21 +0000 Subject: [PATCH 113/216] 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 <cortado.codes@protonmail.com> Date: Wed, 12 Feb 2025 15:44:46 +0000 Subject: [PATCH 114/216] 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 <cortado.codes@protonmail.com> Date: Wed, 12 Feb 2025 15:47:43 +0000 Subject: [PATCH 115/216] 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 <cortado.codes@protonmail.com> Date: Wed, 12 Feb 2025 15:51:55 +0000 Subject: [PATCH 116/216] 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 <cortado.codes@protonmail.com> Date: Wed, 12 Feb 2025 16:10:13 +0000 Subject: [PATCH 117/216] 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 <cortado.codes@protonmail.com> Date: Wed, 12 Feb 2025 16:10:54 +0000 Subject: [PATCH 118/216] 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 <cortado.codes@protonmail.com> Date: Wed, 12 Feb 2025 16:47:32 +0000 Subject: [PATCH 119/216] 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 <cortado.codes@protonmail.com> Date: Wed, 12 Feb 2025 16:59:14 +0000 Subject: [PATCH 120/216] 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 <cortado.codes@protonmail.com> Date: Wed, 12 Feb 2025 17:30:06 +0000 Subject: [PATCH 121/216] 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 <cortado.codes@protonmail.com> Date: Wed, 12 Feb 2025 17:30:34 +0000 Subject: [PATCH 122/216] 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 <cortado.codes@protonmail.com> Date: Wed, 12 Feb 2025 17:35:25 +0000 Subject: [PATCH 123/216] 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 <cortado.codes@protonmail.com> Date: Wed, 12 Feb 2025 17:40:57 +0000 Subject: [PATCH 124/216] 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 <cortado.codes@protonmail.com> Date: Wed, 12 Feb 2025 17:44:14 +0000 Subject: [PATCH 125/216] 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 <cortado.codes@protonmail.com> Date: Wed, 12 Feb 2025 17:44:33 +0000 Subject: [PATCH 126/216] 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"] From f0b689fbb3b86dbb1d619a21b9a347123ed12305 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Mon, 17 Feb 2025 10:45:16 +0000 Subject: [PATCH 127/216] WIP: Temporarily hard-code WIF details into CI workflow --- .github/workflows/python-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index 6596a5315..1e0f859c5 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -53,7 +53,7 @@ jobs: with: # NOTE: If setting create_credentials_file=true, .dockerignore file must include `gha-creds-*.json` to avoid baking these credentials into build create_credentials_file: true - workload_identity_provider: "projects/437801218871/locations/global/workloadIdentityPools/github-actions-pool/providers/github-actions-provider" + workload_identity_provider: "projects/437801218871/locations/global/workloadIdentityPools/dev-github-actions-pool/providers/dev-github-actions-provider" service_account: "github-actions@octue-sdk-python.iam.gserviceaccount.com" - name: Run tests From f72470d3cd0408789b8390005c3054a1c5e69614 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Mon, 17 Feb 2025 14:53:53 +0000 Subject: [PATCH 128/216] FIX: Avoid deserialising input manifest in `octue question ask local` skipci --- octue/cli.py | 3 --- tests/test_cli.py | 14 +++++++++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/octue/cli.py b/octue/cli.py index 3aadeed3a..bc23e3b90 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -211,9 +211,6 @@ def local(input_values, input_manifest, attributes, service_config): if input_values: input_values = json.loads(input_values, cls=OctueJSONDecoder) - if input_manifest: - input_manifest = Manifest.deserialise(input_manifest, from_string=True) - service_configuration, app_configuration = load_service_and_app_configuration(service_config) if attributes: diff --git a/tests/test_cli.py b/tests/test_cli.py index 5d5f50e56..1491dbc78 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -12,7 +12,7 @@ from octue.cloud.emulators._pub_sub import MockService from octue.cloud.emulators.service import ServicePatcher from octue.configuration import AppConfiguration, ServiceConfiguration -from octue.resources import Dataset +from octue.resources import Dataset, Manifest from octue.utils.patches import MultiPatcher from tests import MOCK_SERVICE_REVISION_TAG, TEST_BUCKET_NAME, TESTS_DIR from tests.base import BaseTestCase @@ -230,7 +230,11 @@ def test_with_input_manifest(self): # Check question event. question = mock_answer_question_kwargs["question"] - self.assertEqual(question["event"]["input_manifest"].id, input_manifest.id) + + self.assertEqual( + Manifest.deserialise(question["event"]["input_manifest"], from_string=True).id, + input_manifest.id, + ) # Check question attributes. self.assertTrue(question["attributes"]["recipient"].startswith("testing/test-app")) @@ -272,7 +276,11 @@ def test_with_input_values_and_manifest(self): # 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) + + self.assertEqual( + Manifest.deserialise(question["event"]["input_manifest"], from_string=True).id, + input_manifest.id, + ) # Check question attributes. self.assertTrue(question["attributes"]["recipient"].startswith("testing/test-app")) From cd02dd15f6b8693f3d41c8468e05bf8da932393d Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Mon, 17 Feb 2025 15:03:50 +0000 Subject: [PATCH 129/216] FIX: Deserialise input manifest from JSON in `octue question ask local` skipci --- octue/cli.py | 3 +++ tests/test_cli.py | 12 ++---------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/octue/cli.py b/octue/cli.py index bc23e3b90..c366b035a 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -211,6 +211,9 @@ def local(input_values, input_manifest, attributes, service_config): if input_values: input_values = json.loads(input_values, cls=OctueJSONDecoder) + if input_manifest: + input_manifest = json.loads(input_manifest, cls=OctueJSONDecoder) + service_configuration, app_configuration = load_service_and_app_configuration(service_config) if attributes: diff --git a/tests/test_cli.py b/tests/test_cli.py index 1491dbc78..102963856 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -230,11 +230,7 @@ def test_with_input_manifest(self): # Check question event. question = mock_answer_question_kwargs["question"] - - self.assertEqual( - Manifest.deserialise(question["event"]["input_manifest"], from_string=True).id, - input_manifest.id, - ) + self.assertEqual(Manifest.deserialise(question["event"]["input_manifest"]).id, input_manifest.id) # Check question attributes. self.assertTrue(question["attributes"]["recipient"].startswith("testing/test-app")) @@ -276,11 +272,7 @@ def test_with_input_values_and_manifest(self): # Check question event. question = mock_answer_question_kwargs["question"] self.assertEqual(question["event"]["input_values"], input_values) - - self.assertEqual( - Manifest.deserialise(question["event"]["input_manifest"], from_string=True).id, - input_manifest.id, - ) + self.assertEqual(Manifest.deserialise(question["event"]["input_manifest"]).id, input_manifest.id) # Check question attributes. self.assertTrue(question["attributes"]["recipient"].startswith("testing/test-app")) From 1394334dacafab7bc2b8cb0cbbf5e408356bfdba Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Tue, 25 Feb 2025 15:29:08 +0000 Subject: [PATCH 130/216] FIX: Remove `GOOGLE_KUEUE` from log timestamp exclusion skipci --- octue/definitions.py | 2 +- tests/test_log_handlers.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/octue/definitions.py b/octue/definitions.py index 08a6291e9..8781d90c1 100644 --- a/octue/definitions.py +++ b/octue/definitions.py @@ -18,5 +18,5 @@ # TODO this should probably be defined in twined RUN_STRANDS = ("input_values", "input_manifest", "credentials", "children") -GOOGLE_COMPUTE_PROVIDERS = {"GOOGLE_CLOUD_FUNCTION", "GOOGLE_KUEUE"} +GOOGLE_COMPUTE_PROVIDERS = {"GOOGLE_CLOUD_FUNCTION"} LOCAL_SDK_VERSION = importlib.metadata.version("octue") diff --git a/tests/test_log_handlers.py b/tests/test_log_handlers.py index 0d1426bc7..ff0902d45 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_kueue(self): + def test_log_record_attributes_without_timestamp_used_if_compute_provider_is_google_cloud_function(self): """Test that the formatter without a timestamp is used for logging if the `COMPUTE_PROVIDER` environment - variable is present and equal to "GOOGLE_KUEUE", and `USE_OCTUE_LOG_HANDLER` is equal to "1". + variable is present and equal to "GOOGLE_CLOUD_FUNCTION", and `USE_OCTUE_LOG_HANDLER` is equal to "1". """ - with mock.patch.dict(os.environ, USE_OCTUE_LOG_HANDLER="1", COMPUTE_PROVIDER="GOOGLE_KUEUE"): + with mock.patch.dict(os.environ, USE_OCTUE_LOG_HANDLER="1", COMPUTE_PROVIDER="GOOGLE_CLOUD_FUNCTION"): 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_kueue(self): + def test_log_record_attributes_with_timestamp_used_if_compute_provider_is_not_google_cloud_function(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_KUEUE", and `USE_OCTUE_LOG_HANDLER` is equal to "1". + variable is present and not equal to "GOOGLE_CLOUD_FUNCTION", 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 5013eae1c537d97f8872d4d33c3ce02b11943173 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Wed, 26 Feb 2025 11:52:39 +0000 Subject: [PATCH 131/216] CHO: Ignore GCP credentials files --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index dfd8963eb..2ba81f335 100644 --- a/.gitignore +++ b/.gitignore @@ -98,7 +98,7 @@ ENV/ # See: https://github.com/google-github-actions/auth/issues/123 google_credentials.json gha-creds-*.json -gcp-creds-*.json +gcp-cred* terraform/gcp-credentials.json .terraform From 313bf5c0d9e54fc8cdfc7650fc5acdf9e94b5d79 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Wed, 26 Feb 2025 11:53:02 +0000 Subject: [PATCH 132/216] TST: Replace cloud run deployment test with kueue equivalent --- ...n_deployment.py => test_kueue_deployment.py} | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) rename tests/cloud/deployment/{test_cloud_run_deployment.py => test_kueue_deployment.py} (78%) diff --git a/tests/cloud/deployment/test_cloud_run_deployment.py b/tests/cloud/deployment/test_kueue_deployment.py similarity index 78% rename from tests/cloud/deployment/test_cloud_run_deployment.py rename to tests/cloud/deployment/test_kueue_deployment.py index d7b0919af..0970187fa 100644 --- a/tests/cloud/deployment/test_cloud_run_deployment.py +++ b/tests/cloud/deployment/test_kueue_deployment.py @@ -9,15 +9,14 @@ from octue.resources import Child import twined.exceptions -EXAMPLE_SERVICE_SRUID = "octue/example-service:0.5.0" +EXAMPLE_SERVICE_SRUID = "octue/example-service-kueue:0.1.0" @unittest.skipUnless( - condition=os.getenv("RUN_CLOUD_RUN_DEPLOYMENT_TEST", "0").lower() == "1", - reason="'RUN_CLOUD_RUN_DEPLOYMENT_TEST' environment variable is False or not present.", + condition=os.getenv("RUN_DEPLOYMENT_TEST", "0").lower() == "1", + reason="'RUN_DEPLOYMENT_TEST' environment variable is False or not present.", ) -class TestCloudRunDeployment(TestCase): - # This is the service ID of the example service deployed to Google Cloud Run. +class TestKueueDeployment(TestCase): child = Child( id=EXAMPLE_SERVICE_SRUID, backend={"name": "GCPPubSubBackend", "project_name": os.environ["TEST_PROJECT_NAME"]}, @@ -29,8 +28,8 @@ def test_forwards_exceptions_to_parent(self): self.child.ask(input_values={"invalid_input_data": "hello"}) def test_synchronous_question(self): - """Test that the Google Cloud Run example deployment works, providing a service that can be asked questions and - send responses. + """Test that the Kueue example deployment works, providing a service that can be asked questions and send + responses. """ answer, _ = self.child.ask(input_values={"n_iterations": 3}) @@ -47,9 +46,9 @@ def test_asynchronous_question(self): self.assertIsNone(answer) # Wait for question to complete. - time.sleep(15) + time.sleep(60) - events = get_events(table_id="octue_sdk_python_test_dataset.service-events", question_uuid=question_uuid) + events = get_events(table_id="octue_twined.service-events", question_uuid=question_uuid) self.assertTrue( is_event_valid( From 4ea98a04fb1fc049862c8f4d084b28fdc692a11d Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Wed, 26 Feb 2025 11:53:35 +0000 Subject: [PATCH 133/216] OPS: Move `pydocstyle` config inside `pyproject.toml` --- pyproject.toml | 3 +++ setup.cfg | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) delete mode 100644 setup.cfg diff --git a/pyproject.toml b/pyproject.toml index 2ae836891..5c41a6912 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,6 +77,9 @@ known-first-party = ["octue", "app", "fractal", "test", "tests", "twined"] section-order = ["future", "standard-library", "third-party", "first-party", "local-folder"] force-sort-within-sections = true +[tool.pydocstyle] +ignore = ["D100", "D101", "D102", "D103", "D104", "D105", "D107", "D203", "D205", "D213", "D400", "D415"] + [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index d3f6f4e77..000000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[pydocstyle] -ignore = D100, D101, D102, D103, D104, D105, D107, D203, D205, D213, D400, D415 From 7ec39432884c807351cdf6eeb663364bf0bf5108 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Wed, 26 Feb 2025 12:02:50 +0000 Subject: [PATCH 134/216] OPS: Use `pydocstyle` within `ruff` in pre-commit checks --- .pre-commit-config.yaml | 5 ----- pyproject.toml | 15 ++++++++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e3dfeb7f0..b1980e11c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,11 +31,6 @@ repos: hooks: - id: prettier - - repo: https://github.com/pycqa/pydocstyle - rev: 6.1.1 - hooks: - - id: pydocstyle - - repo: https://github.com/thclark/pre-commit-sphinx rev: 0.0.3 hooks: diff --git a/pyproject.toml b/pyproject.toml index 5c41a6912..28ca8d08e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,18 +67,23 @@ ruff = "^0.6.9" [tool.ruff] line-length = 120 -# Enable pycodestyle (`E`) and Pyflakes (`F`) codes. -lint.select = ["E", "F"] + +[tool.ruff.lint] +# Enable pydocstyle (`D`), pycodestyle (`E`), and Pyflakes (`F`) codes. +select = ["D", "E", "F"] # Ignore E501 line-too-long - see https://docs.astral.sh/ruff/faq/#is-the-ruff-linter-compatible-with-black for why -lint.ignore = ["F405", "E501", "E203", "E731", "N818"] +ignore = [ + "D100", "D101", "D102", "D103", "D104", "D105", "D107", "D203", "D205", "D213", "D400", "D415", + "E501", "E203", "E731", + "F405", + "N818", +] [tool.ruff.lint.isort] known-first-party = ["octue", "app", "fractal", "test", "tests", "twined"] section-order = ["future", "standard-library", "third-party", "first-party", "local-folder"] force-sort-within-sections = true -[tool.pydocstyle] -ignore = ["D100", "D101", "D102", "D103", "D104", "D105", "D107", "D203", "D205", "D213", "D400", "D415"] [build-system] requires = ["poetry-core>=1.0.0"] From fd6289df8eacd132c990bed6a5f4691982bb328f Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Wed, 26 Feb 2025 12:08:16 +0000 Subject: [PATCH 135/216] DOC: Update docstring 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 53f62446d..1db0eb69f 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -315,7 +315,7 @@ def ask( :param str service_id: the ID of the child to ask the question to :param any|None input_values: any input values for the question - :param octue.resources.manifest.Manifest|None input_manifest: an input manifest of any datasets needed for the question + :param dict|octue.resources.manifest.Manifest|None input_manifest: an input manifest of any datasets needed for the question :param list(dict)|None children: a list of children for the child to use instead of its default children (if it uses children). These should be in the same format as in an app's app configuration file and have the same keys. :param bool subscribe_to_logs: if `True`, subscribe to the child's logs and handle them with the local log handlers :param bool allow_local_files: if `True`, allow the input manifest to contain references to local files - this should only be set to `True` if the child will be able to access these local files From 16233a4e3a965d94e0aa25cfcf13c41887ea2029 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Wed, 26 Feb 2025 12:42:50 +0000 Subject: [PATCH 136/216] ENH: Set better defaults in `EventAttributes` --- octue/cloud/events/attributes.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/octue/cloud/events/attributes.py b/octue/cloud/events/attributes.py index 180a82a76..8d273be42 100644 --- a/octue/cloud/events/attributes.py +++ b/octue/cloud/events/attributes.py @@ -2,7 +2,7 @@ import json import uuid as uuid_library -from octue.cloud import LOCAL_SDK_VERSION +from octue.definitions import LOCAL_SDK_VERSION from octue.utils.dictionaries import make_minimal_dictionary SENDER_TYPE_OPPOSITES = {"CHILD": "PARENT", "PARENT": "CHILD"} @@ -11,9 +11,6 @@ class EventAttributes: def __init__( self, - originator_question_uuid, - parent, - originator, sender, sender_type, recipient, @@ -21,6 +18,9 @@ def __init__( datetime=None, question_uuid=None, parent_question_uuid=None, + originator_question_uuid=None, + parent=None, + originator=None, sender_sdk_version=None, retry_count=0, forward_logs=True, @@ -34,9 +34,9 @@ def __init__( self.datetime = datetime or dt.datetime.now(tz=dt.timezone.utc).isoformat() self.question_uuid = question_uuid or str(uuid_library.uuid4()) self.parent_question_uuid = parent_question_uuid - self.originator_question_uuid = originator_question_uuid - self.parent = parent - self.originator = originator + self.originator_question_uuid = originator_question_uuid or question_uuid + self.parent = parent or sender + self.originator = originator or sender self.sender = sender self.sender_type = sender_type self.sender_sdk_version = sender_sdk_version or LOCAL_SDK_VERSION From 22b7e98199ef5b90cef7de8704f47ec8c3e64eeb Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Wed, 26 Feb 2025 12:55:08 +0000 Subject: [PATCH 137/216] FIX: Avoid calling `to_dict` method on dictionary --- 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 f71c5ecb5..e77b78054 100644 --- a/octue/cloud/events/utils.py +++ b/octue/cloud/events/utils.py @@ -32,9 +32,9 @@ def make_question_event( forward_logs=True, save_diagnostics="SAVE_DIAGNOSTICS_ON", sender_type="PARENT", - ) + ).to_dict() return { "event": make_minimal_dictionary(input_values=input_values, input_manifest=input_manifest, kind="question"), - "attributes": attributes.to_dict(), + "attributes": attributes, } From 5a501ae6c076f15ea4bd33432a8d79658fe90a49 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Wed, 26 Feb 2025 12:58:45 +0000 Subject: [PATCH 138/216] TST: Fix tests skipci --- tests/test_cli.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 102963856..799fbcc17 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -200,7 +200,7 @@ def test_with_input_values(self): 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"]) + self.assertNotIn("parent_question_uuid", question["attributes"]) # Check the result is in the output. self.assertIn(json.dumps(RESULT), result.output) @@ -238,7 +238,7 @@ def test_with_input_manifest(self): 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"]) + self.assertNotIn("parent_question_uuid", question["attributes"]) # Check the result is in the output. self.assertIn(json.dumps(RESULT), result.output) @@ -280,7 +280,7 @@ def test_with_input_values_and_manifest(self): 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"]) + self.assertNotIn("parent_question_uuid", question["attributes"]) # Check the result is in the output. self.assertIn(json.dumps(RESULT), result.output) From 32fec936854c5f44549b6370a7df1d15868e0e49 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Wed, 26 Feb 2025 16:52:28 +0000 Subject: [PATCH 139/216] FIX: Fix question attributes defaults --- octue/cloud/events/attributes.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/octue/cloud/events/attributes.py b/octue/cloud/events/attributes.py index 8d273be42..08202aa72 100644 --- a/octue/cloud/events/attributes.py +++ b/octue/cloud/events/attributes.py @@ -23,8 +23,8 @@ def __init__( originator=None, sender_sdk_version=None, retry_count=0, - forward_logs=True, - save_diagnostics=True, + forward_logs=None, + save_diagnostics=None, cpus=None, memory=None, ephemeral_storage=None, @@ -43,9 +43,12 @@ def __init__( self.recipient = recipient self.retry_count = int(retry_count) - # Question event attributes. - self.forward_logs = bool(forward_logs) - self.save_diagnostics = save_diagnostics + # Question event attributes with non-`None` defaults. + if sender_type == "PARENT": + self.forward_logs = forward_logs or True + self.save_diagnostics = save_diagnostics or "SAVE_DIAGNOSTICS_ON_CRASH" + + # Question event attributes with `None` defaults. self.cpus = cpus self.memory = memory self.ephemeral_storage = ephemeral_storage From 59dd51e9506ca29eb5b9804fabf1a99aadd167b5 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Wed, 26 Feb 2025 16:55:02 +0000 Subject: [PATCH 140/216] REF: Simplify attribute defaults --- octue/cloud/events/attributes.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/octue/cloud/events/attributes.py b/octue/cloud/events/attributes.py index 08202aa72..1fb5cbf37 100644 --- a/octue/cloud/events/attributes.py +++ b/octue/cloud/events/attributes.py @@ -21,7 +21,7 @@ def __init__( originator_question_uuid=None, parent=None, originator=None, - sender_sdk_version=None, + sender_sdk_version=LOCAL_SDK_VERSION, retry_count=0, forward_logs=None, save_diagnostics=None, @@ -39,9 +39,9 @@ def __init__( self.originator = originator or sender self.sender = sender self.sender_type = sender_type - self.sender_sdk_version = sender_sdk_version or LOCAL_SDK_VERSION + self.sender_sdk_version = sender_sdk_version self.recipient = recipient - self.retry_count = int(retry_count) + self.retry_count = retry_count # Question event attributes with non-`None` defaults. if sender_type == "PARENT": From cc6659250c1e08d1e1d0d662af352138be3396c0 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Wed, 26 Feb 2025 17:02:06 +0000 Subject: [PATCH 141/216] FIX: Remove question attributes from `make_response_attributes` --- octue/cloud/events/attributes.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/octue/cloud/events/attributes.py b/octue/cloud/events/attributes.py index 1fb5cbf37..4986c172e 100644 --- a/octue/cloud/events/attributes.py +++ b/octue/cloud/events/attributes.py @@ -59,6 +59,10 @@ def make_response_attributes(self): attributes["sender_type"] = SENDER_TYPE_OPPOSITES[self.sender_type] attributes["sender_sdk_version"] = LOCAL_SDK_VERSION attributes["recipient"] = self.sender + + for attr in ("forward_logs", "save_diagnostics", "cpus", "memory", "ephemeral_storage"): + del attributes[attr] + return EventAttributes(**attributes) def to_dict(self): From 9e6eb795ed4685b7c34c4d60dd82734eca1ce823 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Wed, 26 Feb 2025 17:36:46 +0000 Subject: [PATCH 142/216] DOC: Add missing param to docstring --- 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 6ec067814..1d55a925b 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -491,6 +491,7 @@ def _emit_event(self, event, attributes, wait=True, timeout=30): - `retry_count` - `datetime` + :param dict event: JSON-serialisable data to emit as an event :param octue.cloud.events.attributes.EventAttributes attributes: :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 From bc9f19882720775b3d3bd547f124e484a84c611c Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Wed, 26 Feb 2025 17:38:03 +0000 Subject: [PATCH 143/216] FIX: Remove default question attributes from `EventAttributes` --- octue/cloud/events/attributes.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/octue/cloud/events/attributes.py b/octue/cloud/events/attributes.py index 4986c172e..af09ccee0 100644 --- a/octue/cloud/events/attributes.py +++ b/octue/cloud/events/attributes.py @@ -34,21 +34,18 @@ def __init__( self.datetime = datetime or dt.datetime.now(tz=dt.timezone.utc).isoformat() self.question_uuid = question_uuid or str(uuid_library.uuid4()) self.parent_question_uuid = parent_question_uuid - self.originator_question_uuid = originator_question_uuid or question_uuid - self.parent = parent or sender - self.originator = originator or sender + self.originator_question_uuid = originator_question_uuid or self.question_uuid self.sender = sender + self.parent = parent or self.sender + self.originator = originator or self.sender self.sender_type = sender_type self.sender_sdk_version = sender_sdk_version self.recipient = recipient self.retry_count = retry_count - # Question event attributes with non-`None` defaults. - if sender_type == "PARENT": - self.forward_logs = forward_logs or True - self.save_diagnostics = save_diagnostics or "SAVE_DIAGNOSTICS_ON_CRASH" - - # Question event attributes with `None` defaults. + # Question event attributes. + self.forward_logs = forward_logs + self.save_diagnostics = save_diagnostics self.cpus = cpus self.memory = memory self.ephemeral_storage = ephemeral_storage From 7dd70409fdad3693d82911c160e2c7587f9740ce Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Wed, 26 Feb 2025 17:39:04 +0000 Subject: [PATCH 144/216] FIX: Only remove attributes if they're present --- octue/cloud/events/attributes.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/octue/cloud/events/attributes.py b/octue/cloud/events/attributes.py index af09ccee0..f978f0123 100644 --- a/octue/cloud/events/attributes.py +++ b/octue/cloud/events/attributes.py @@ -58,7 +58,8 @@ def make_response_attributes(self): attributes["recipient"] = self.sender for attr in ("forward_logs", "save_diagnostics", "cpus", "memory", "ephemeral_storage"): - del attributes[attr] + if attr in attributes: + del attributes[attr] return EventAttributes(**attributes) From 31ced10a4416fef85254cf492d7cf82d8ae52703 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Wed, 26 Feb 2025 17:39:34 +0000 Subject: [PATCH 145/216] FIX: Fix `save_diagnostics` default in `make_question_event` skipci --- octue/cloud/events/utils.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/octue/cloud/events/utils.py b/octue/cloud/events/utils.py index e77b78054..a89c17993 100644 --- a/octue/cloud/events/utils.py +++ b/octue/cloud/events/utils.py @@ -1,5 +1,3 @@ -import uuid - from octue.cloud.events.attributes import EventAttributes from octue.utils.dictionaries import make_minimal_dictionary @@ -23,14 +21,12 @@ def make_question_event( :return dict: """ if not attributes: - question_uuid = question_uuid or str(uuid.uuid4()) - attributes = EventAttributes( question_uuid=question_uuid, sender=sender, recipient=recipient, forward_logs=True, - save_diagnostics="SAVE_DIAGNOSTICS_ON", + save_diagnostics="SAVE_DIAGNOSTICS_ON_CRASH", sender_type="PARENT", ).to_dict() From eda2ec3c4683943360eb877324df22c278ea8f2b Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Wed, 26 Feb 2025 17:43:07 +0000 Subject: [PATCH 146/216] DOC: Add `attributes` param to docstrings --- octue/cloud/pub_sub/logging.py | 2 +- octue/cloud/pub_sub/service.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/octue/cloud/pub_sub/logging.py b/octue/cloud/pub_sub/logging.py index 0e4b134dc..94fd72e47 100644 --- a/octue/cloud/pub_sub/logging.py +++ b/octue/cloud/pub_sub/logging.py @@ -8,7 +8,7 @@ class GoogleCloudPubSubHandler(logging.Handler): """A log handler that publishes log records to a Google Cloud Pub/Sub topic. :param callable event_emitter: the `_emit_event` method of the service that instantiated this instance - :param octue.cloud.events.attributes.EventAttributes attributes: + :param octue.cloud.events.attributes.EventAttributes attributes: the attributes to use for the log record event :param float timeout: timeout in seconds for attempting to publish each log record :return None: """ diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index 1d55a925b..a3973f03a 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -457,7 +457,7 @@ def cancel(self, question_uuid, event_store_table_id, timeout=30): def send_exception(self, attributes, timeout=30): """Serialise and send the exception being handled to the parent. - :param octue.cloud.events.attributes.EventAttributes attributes: + :param octue.cloud.events.attributes.EventAttributes attributes: the attributes to use for the exception event :param float|None timeout: time in seconds to keep retrying sending of the exception :return None: """ @@ -492,7 +492,7 @@ def _emit_event(self, event, attributes, wait=True, timeout=30): - `datetime` :param dict event: JSON-serialisable data to emit as an event - :param octue.cloud.events.attributes.EventAttributes attributes: + :param octue.cloud.events.attributes.EventAttributes attributes: the attributes to use for the event :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: @@ -573,7 +573,7 @@ def _send_question( def _send_delivery_acknowledgment(self, attributes, timeout=30): """Send an acknowledgement of question receipt to the parent. - :param octue.cloud.events.attributes.EventAttributes attributes: + :param octue.cloud.events.attributes.EventAttributes attributes: the attributes to use for the delivery acknowledgement event :param float timeout: time in seconds after which to give up sending :return None: """ @@ -583,7 +583,7 @@ def _send_delivery_acknowledgment(self, attributes, timeout=30): def _send_heartbeat(self, attributes, timeout=30): """Send a heartbeat to the parent, indicating that the service is alive. - :param octue.cloud.events.attributes.EventAttributes attributes: + :param octue.cloud.events.attributes.EventAttributes attributes: the attributes to use for the heartbeat event :param float timeout: time in seconds after which to give up sending :return None: """ @@ -594,7 +594,7 @@ def _send_monitor_message(self, data, attributes, timeout=30): """Send a monitor message to the parent. :param any data: the data to send as a monitor message - :param octue.cloud.events.attributes.EventAttributes attributes: + :param octue.cloud.events.attributes.EventAttributes attributes: the attributes to use for the monitor message event :param float timeout: time in seconds to retry sending the message :return None: """ From 7adfe38469deefdb356cf9cf929f492191f2b35e Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Wed, 26 Feb 2025 17:49:42 +0000 Subject: [PATCH 147/216] REF: Add default args and document `make_question_event` skipci --- octue/cloud/events/utils.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/octue/cloud/events/utils.py b/octue/cloud/events/utils.py index a89c17993..0b3eb0ff4 100644 --- a/octue/cloud/events/utils.py +++ b/octue/cloud/events/utils.py @@ -3,8 +3,8 @@ def make_question_event( - input_values, - input_manifest, + input_values=None, + input_manifest=None, sender=None, recipient=None, question_uuid=None, @@ -12,13 +12,13 @@ def make_question_event( ): """Make a question event. If the `attributes` argument isn't provided, the question will be an originator question. - :param dict input_values: - :param octue.resources.manifest.Manifest input_manifest: - :param str|None sender: - :param str|None recipient: - :param str|None question_uuid: - :param dict|None attributes: - :return dict: + :param dict|None input_values: any input values for the question + :param dict|None input_manifest: an input manifest of any datasets needed for the question as a python primitive + :param str|None sender: the service revision unique identifier (SRUID) of the service revision sending the question + :param str|None recipient: the service revision unique identifier (SRUID) of the service revision the question is for + :param str|None question_uuid: the UUID to use for the question; if `None`, a UUID is generated + :param dict|None attributes: the attributes to use for the question event; if none are provided, the question will be an originator question + :return dict: the question event and its attributes """ if not attributes: attributes = EventAttributes( From 2b5707379b90b8ad42b44a552e63911dedd1b70b Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Wed, 26 Feb 2025 17:58:49 +0000 Subject: [PATCH 148/216] REF: Factor out emitting result event skipci --- octue/cloud/pub_sub/service.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index a3973f03a..569f37fc5 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -242,12 +242,7 @@ def answer(self, question, heartbeat_interval=120, timeout=30): originator=question_attributes.originator, ) - result = make_minimal_dictionary(kind="result", output_values=analysis.output_values) - - if analysis.output_manifest is not None: - result["output_manifest"] = analysis.output_manifest.to_primitive() - - self._emit_event(event=result, attributes=response_attributes, timeout=timeout) + result = self._send_result(analysis, response_attributes) heartbeater.cancel() logger.info("%r answered question %r.", self, question_attributes.question_uuid) return result @@ -601,6 +596,22 @@ def _send_monitor_message(self, data, attributes, timeout=30): self._emit_event({"kind": "monitor_message", "data": data}, attributes=attributes, timeout=timeout, wait=False) logger.debug("Monitor message sent by %r.", self) + def _send_result(self, analysis, attributes, timeout=30): + """Send the result to the parent. + + :param octue.resources.analysis.Analysis analysis: the analysis object containing the output values and/or output manifest + :param octue.cloud.events.attributes.EventAttributes attributes: the attributes to use for the monitor message event + :param float timeout: time in seconds to retry sending the message + :return dict: the result + """ + result = make_minimal_dictionary(kind="result", output_values=analysis.output_values) + + if analysis.output_manifest is not None: + result["output_manifest"] = analysis.output_manifest.to_primitive() + + self._emit_event(event=result, attributes=attributes, timeout=timeout) + return result + def _parse_question(self, question): """Parse a question in dictionary format or direct Google Pub/Sub format. From 5d6285fe85d300a52a025445dec65e20a3f7ff83 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Wed, 26 Feb 2025 18:02:07 +0000 Subject: [PATCH 149/216] DOC: Fix docstring 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 569f37fc5..96b53afd9 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -600,7 +600,7 @@ def _send_result(self, analysis, attributes, timeout=30): """Send the result to the parent. :param octue.resources.analysis.Analysis analysis: the analysis object containing the output values and/or output manifest - :param octue.cloud.events.attributes.EventAttributes attributes: the attributes to use for the monitor message event + :param octue.cloud.events.attributes.EventAttributes attributes: the attributes to use for the result event :param float timeout: time in seconds to retry sending the message :return dict: the result """ From 8188e7ca163bba1832784283493f01aea5a9f31f Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Thu, 27 Feb 2025 12:02:07 +0000 Subject: [PATCH 150/216] REF: Expect `datetime` attr as `datetime` object in `EventAttributes` --- octue/cloud/events/attributes.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/octue/cloud/events/attributes.py b/octue/cloud/events/attributes.py index f978f0123..5a7997670 100644 --- a/octue/cloud/events/attributes.py +++ b/octue/cloud/events/attributes.py @@ -31,7 +31,7 @@ def __init__( ): # Attributes for all event kinds. self.uuid = uuid or str(uuid_library.uuid4()) - self.datetime = datetime or dt.datetime.now(tz=dt.timezone.utc).isoformat() + self.datetime = datetime or dt.datetime.now(tz=dt.timezone.utc) self.question_uuid = question_uuid or str(uuid_library.uuid4()) self.parent_question_uuid = parent_question_uuid self.originator_question_uuid = originator_question_uuid or self.question_uuid @@ -92,6 +92,8 @@ def to_serialised_attributes(self): value = str(int(value)) elif isinstance(value, (int, float)): value = str(value) + elif isinstance(value, dt.datetime): + value = value.isoformat() elif value is None: value = json.dumps(value) From bd33a9319e9d7b53ad96266f6f9d182e75c7a38e Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Thu, 27 Feb 2025 12:32:02 +0000 Subject: [PATCH 151/216] REF: Rename `EventAttributes` methods and add docstrings skipci --- octue/cloud/events/attributes.py | 48 +++++++++++++++++++++++++++++--- octue/cloud/events/utils.py | 2 +- octue/cloud/pub_sub/service.py | 2 +- 3 files changed, 46 insertions(+), 6 deletions(-) diff --git a/octue/cloud/events/attributes.py b/octue/cloud/events/attributes.py index 5a7997670..3c8f45dea 100644 --- a/octue/cloud/events/attributes.py +++ b/octue/cloud/events/attributes.py @@ -9,6 +9,28 @@ class EventAttributes: + """A data structure for holding and working with attributes for a single Octue Twined event. + + :param str sender: the unique identifier (SRUID) of the service revision sending the question + :param str sender_type: the type of sender for this event; must be one of {"PARENT", "CHILD"} + :param str recipient: the SRUID of the service revision the question is for + :param str|None uuid: the UUID of the event; if `None`, a UUID is generated + :param datetime.datetime|None datetime: the datetime the event was created at; defaults to the current datetime in UTC + :param str|None question_uuid: the UUID of the question; if `None`, a UUID is generated + :param str|None parent_question_uuid: the UUID of the question that triggered this question; this should be `None` if this event relates to the first question in a question tree + :param str|None originator_question_uuid: the UUID of the question that triggered all ancestor questions of this question; if `None`, the event's related question is assumed to be the originator question and `question_uuid` is used + :param str|None parent: the SRUID of the service revision that asked the question this event is related to + :param str|None originator: the SRUID of the service revision that triggered all ancestor questions of this question; if `None`, the `sender` is used + :param str sender_sdk_version: the semantic version of Octue SDK the sender is running; defaults to the version in the environment + :param int retry_count: the retry count of the question this event is related to (this is zero if it's the first attempt at the question) + :param bool|None forward_logs: if this isn't a `question` event, this should be `None`; otherwise, it should be a boolean indicating whether the parent requested the child to forward its logs to it + :param str|None save_diagnostics: if this isn't a `question` event, this should be `None`; otherwise, it must be one of {"SAVE_DIAGNOSTICS_OFF", "SAVE_DIAGNOSTICS_ON_CRASH", "SAVE_DIAGNOSTICS_ON"} + :param int|None cpus: if this isn't a `question` event, this should be `None`; otherwise, it can be `None` or the number of CPUs requested for the question + :param str|None memory: if this isn't a `question` event, this should be `None`; otherwise, it can be `None` or the amount of memory requested for the question e.g. "256Mi" or "1Gi" + :param str|None ephemeral_storage: if this isn't a `question` event, this should be `None`; otherwise, it can be `None` or the amount of ephemeral storage requested for the question e.g. "256Mi" or "1Gi" + :return None: + """ + def __init__( self, sender, @@ -50,8 +72,14 @@ def __init__( self.memory = memory self.ephemeral_storage = ephemeral_storage - def make_response_attributes(self): - attributes = self.to_dict() + def make_opposite_attributes(self): + """Create the attributes for an event of the opposite sender type to this event (parent -> child or child + -> parent). For example, if these attributes are for a question event, create the attributes for a response + event such as a result or log record. + + :return octue.cloud.events.attributes.EventAttributes: the event attributes for an event with the opposite sender type + """ + attributes = self.to_minimal_dict() attributes["sender"] = self.recipient attributes["sender_type"] = SENDER_TYPE_OPPOSITES[self.sender_type] attributes["sender_sdk_version"] = LOCAL_SDK_VERSION @@ -63,7 +91,13 @@ def make_response_attributes(self): return EventAttributes(**attributes) - def to_dict(self): + def to_minimal_dict(self): + """Convert the attributes to a minimal dictionary containing only the attributes that have a non-`None` value. + Using a minimal dictionary means the smallest possible data structure is used so `None` values don't, + for example, need to be redundantly encoded and transmitted when part of a JSON payload for a Pub/Sub message. + + :return dict: the non-`None` attributes + """ return make_minimal_dictionary( uuid=self.uuid, datetime=self.datetime, @@ -85,9 +119,15 @@ def to_dict(self): ) def to_serialised_attributes(self): + """Convert the attributes to their serialised forms. This is required for e.g. sending the attributes as message + attributes on a Pub/Sub message. A minimal dictionary is produced containing only the attributes that have a + non-`None` value. + + :return dict: the attribute names of the non-`None` attributes mapped to their serialised values + """ serialised_attributes = {} - for key, value in self.to_dict().items(): + for key, value in self.to_minimal_dict().items(): if isinstance(value, bool): value = str(int(value)) elif isinstance(value, (int, float)): diff --git a/octue/cloud/events/utils.py b/octue/cloud/events/utils.py index 0b3eb0ff4..0b3bc9997 100644 --- a/octue/cloud/events/utils.py +++ b/octue/cloud/events/utils.py @@ -28,7 +28,7 @@ def make_question_event( forward_logs=True, save_diagnostics="SAVE_DIAGNOSTICS_ON_CRASH", sender_type="PARENT", - ).to_dict() + ).to_minimal_dict() return { "event": make_minimal_dictionary(input_values=input_values, input_manifest=input_manifest, kind="question"), diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index 96b53afd9..35ff60b9c 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -206,7 +206,7 @@ def answer(self, question, heartbeat_interval=120, timeout=30): return heartbeater = None - response_attributes = question_attributes.make_response_attributes() + response_attributes = question_attributes.make_opposite_attributes() try: self._send_delivery_acknowledgment(response_attributes) From b40bfb0ddce083692ffea3815b67d5e1022dfff8 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Thu, 27 Feb 2025 12:55:30 +0000 Subject: [PATCH 152/216] TST: Test `EventAttributes` class --- tests/cloud/events/test_attributes.py | 147 ++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 tests/cloud/events/test_attributes.py diff --git a/tests/cloud/events/test_attributes.py b/tests/cloud/events/test_attributes.py new file mode 100644 index 000000000..3dc8f4944 --- /dev/null +++ b/tests/cloud/events/test_attributes.py @@ -0,0 +1,147 @@ +import unittest + +from octue.cloud.events.attributes import EventAttributes + +QUESTION_UUID = "50760303-ee89-4752-81cc-aadd05f81752" +SENDER = "my-org/my-parent:1.0.0" +SENDER_TYPE = "PARENT" +RECIPIENT = "my-org/my-child:2.0.0" + + +class TestEventAttributes(unittest.TestCase): + def test_defaults(self): + """Test that the defaults are correct.""" + attributes = EventAttributes( + sender=SENDER, + sender_type=SENDER_TYPE, + recipient=RECIPIENT, + question_uuid=QUESTION_UUID, + ) + + attributes_dict = attributes.__dict__ + self.assertTrue(attributes_dict.pop("uuid")) + self.assertTrue(attributes_dict.pop("datetime")) + self.assertTrue(attributes_dict.pop("sender_sdk_version")) + + self.assertEqual( + attributes_dict, + { + "sender": SENDER, + "sender_type": SENDER_TYPE, + "recipient": RECIPIENT, + "question_uuid": QUESTION_UUID, + "parent_question_uuid": None, + "originator_question_uuid": QUESTION_UUID, + "parent": SENDER, + "originator": SENDER, + "retry_count": 0, + "forward_logs": None, + "save_diagnostics": None, + "cpus": None, + "memory": None, + "ephemeral_storage": None, + }, + ) + + def test_make_opposite_attributes(self): + """Test that the sender and recipient are reversed when making opposite attributes from a set of attributes.""" + attributes = EventAttributes( + sender=SENDER, + sender_type=SENDER_TYPE, + recipient=RECIPIENT, + question_uuid=QUESTION_UUID, + ) + + opposite_attributes = attributes.make_opposite_attributes() + + opposite_attributes_dict = opposite_attributes.__dict__ + self.assertTrue(opposite_attributes_dict.pop("uuid")) + self.assertTrue(opposite_attributes_dict.pop("datetime")) + self.assertTrue(opposite_attributes_dict.pop("sender_sdk_version")) + + self.assertEqual( + opposite_attributes_dict, + { + "sender": RECIPIENT, + "sender_type": "CHILD", + "recipient": SENDER, + "question_uuid": QUESTION_UUID, + "parent_question_uuid": None, + "originator_question_uuid": QUESTION_UUID, + "parent": SENDER, + "originator": SENDER, + "retry_count": 0, + "forward_logs": None, + "save_diagnostics": None, + "cpus": None, + "memory": None, + "ephemeral_storage": None, + }, + ) + + def test_to_minimal_dict(self): + """Test that non-`None` attributes are excluded when making a minimal dictionary from attributes.""" + attributes = EventAttributes( + sender=SENDER, + sender_type=SENDER_TYPE, + recipient=RECIPIENT, + question_uuid=QUESTION_UUID, + ) + + attributes_dict = attributes.to_minimal_dict() + self.assertTrue(attributes_dict.pop("uuid")) + self.assertTrue(attributes_dict.pop("datetime")) + self.assertTrue(attributes_dict.pop("sender_sdk_version")) + + self.assertEqual( + attributes_dict, + { + "sender": SENDER, + "sender_type": SENDER_TYPE, + "recipient": RECIPIENT, + "question_uuid": QUESTION_UUID, + "originator_question_uuid": QUESTION_UUID, + "parent": SENDER, + "originator": SENDER, + "retry_count": 0, + }, + ) + + def test_to_serialised_attributes(self): + """Test that attributes are serialised correctly.""" + attributes = EventAttributes( + sender=SENDER, + sender_type=SENDER_TYPE, + recipient=RECIPIENT, + question_uuid=QUESTION_UUID, + forward_logs=True, + save_diagnostics="SAVE_DIAGNOSTICS_ON", + cpus=1, + memory="2Gi", + ephemeral_storage="256Mi", + ) + + serialised_attributes = attributes.to_serialised_attributes() + + self.assertTrue(serialised_attributes.pop("uuid")) + self.assertTrue(serialised_attributes.pop("sender_sdk_version")) + self.assertTrue(isinstance(serialised_attributes.pop("datetime"), str)) + + self.assertEqual( + serialised_attributes, + { + "sender": SENDER, + "sender_type": SENDER_TYPE, + "recipient": RECIPIENT, + "question_uuid": QUESTION_UUID, + "originator_question_uuid": QUESTION_UUID, + "parent": SENDER, + "originator": SENDER, + "retry_count": "0", + "forward_logs": "1", + "save_diagnostics": "SAVE_DIAGNOSTICS_ON", + "cpus": "1", + "memory": "2Gi", + "ephemeral_storage": "256Mi", + }, + ) From 3cbfa9a86489e52e3b5174163cd4ea8c96d98da0 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Thu, 27 Feb 2025 13:01:30 +0000 Subject: [PATCH 153/216] REF: Make sender/recipient swap clearer --- octue/cloud/events/attributes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/octue/cloud/events/attributes.py b/octue/cloud/events/attributes.py index 3c8f45dea..44d620159 100644 --- a/octue/cloud/events/attributes.py +++ b/octue/cloud/events/attributes.py @@ -81,9 +81,9 @@ def make_opposite_attributes(self): """ attributes = self.to_minimal_dict() attributes["sender"] = self.recipient + attributes["recipient"] = self.sender attributes["sender_type"] = SENDER_TYPE_OPPOSITES[self.sender_type] attributes["sender_sdk_version"] = LOCAL_SDK_VERSION - attributes["recipient"] = self.sender for attr in ("forward_logs", "save_diagnostics", "cpus", "memory", "ephemeral_storage"): if attr in attributes: From 16e4e24faf4daaee0e6086701ebffb67dfde0a6d Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Thu, 27 Feb 2025 13:04:58 +0000 Subject: [PATCH 154/216] ENH: Remove need to (de)serialise `None` attribute values --- octue/cloud/events/attributes.py | 3 --- octue/cloud/events/extraction.py | 9 +++------ 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/octue/cloud/events/attributes.py b/octue/cloud/events/attributes.py index 44d620159..5487859f5 100644 --- a/octue/cloud/events/attributes.py +++ b/octue/cloud/events/attributes.py @@ -1,5 +1,4 @@ import datetime as dt -import json import uuid as uuid_library from octue.definitions import LOCAL_SDK_VERSION @@ -134,8 +133,6 @@ def to_serialised_attributes(self): value = str(value) elif isinstance(value, dt.datetime): value = value.isoformat() - elif value is None: - value = json.dumps(value) serialised_attributes[key] = value diff --git a/octue/cloud/events/extraction.py b/octue/cloud/events/extraction.py index 964d8a145..473fafe8d 100644 --- a/octue/cloud/events/extraction.py +++ b/octue/cloud/events/extraction.py @@ -10,11 +10,7 @@ def extract_and_deserialise_attributes(container): # 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 - + # Deserialise the `retry_count`, attribute if it's present (don't assume it is before validation). retry_count = attributes.get("retry_count") if retry_count: @@ -22,7 +18,8 @@ def extract_and_deserialise_attributes(container): else: attributes["retry_count"] = None - # Question events have some extra optional attributes. + # Question events have some extra optional attributes that also need deserialising if they're present (don't assume + # they are before validation). if attributes.get("sender_type") == "PARENT": forward_logs = attributes.get("forward_logs") From dd3ce1d993eef84b71550e46c856b64fae7f600b Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Thu, 27 Feb 2025 14:41:48 +0000 Subject: [PATCH 155/216] REF: Use `EventAttributes` in `MockService` --- octue/cloud/emulators/_pub_sub.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/octue/cloud/emulators/_pub_sub.py b/octue/cloud/emulators/_pub_sub.py index 4728b6727..b91595128 100644 --- a/octue/cloud/emulators/_pub_sub.py +++ b/octue/cloud/emulators/_pub_sub.py @@ -4,6 +4,7 @@ import google.api_core +from octue.cloud.events.attributes import EventAttributes 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 @@ -405,9 +406,7 @@ def ask( # If the originator isn't provided, assume that this service revision is the originator. originator = originator or self.id - attributes = make_minimal_dictionary( - datetime="2024-04-11T10:46:48.236064", - uuid="a9de11b1-e88f-43fa-b3a4-40a590c3443f", + attributes = EventAttributes( question_uuid=question_uuid, parent_question_uuid=parent_question_uuid, originator_question_uuid=originator_question_uuid, @@ -426,7 +425,9 @@ def ask( ) try: - self.children[service_id].answer(MockMessage.from_primitive(data=question, attributes=attributes)) + self.children[service_id].answer( + MockMessage.from_primitive(data=question, attributes=attributes.to_serialised_attributes()) + ) except Exception as e: # noqa logger.exception(e) From 571a4b67534010297290c39e412982859031eba7 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Thu, 27 Feb 2025 14:48:40 +0000 Subject: [PATCH 156/216] DOC: Improve docstring skipci --- octue/cloud/events/attributes.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/octue/cloud/events/attributes.py b/octue/cloud/events/attributes.py index 5487859f5..9c8e3e8d9 100644 --- a/octue/cloud/events/attributes.py +++ b/octue/cloud/events/attributes.py @@ -8,7 +8,9 @@ class EventAttributes: - """A data structure for holding and working with attributes for a single Octue Twined event. + """A data structure for holding and working with attributes for a single Octue Twined event. If originator and + parent information aren't provided, the attributes will correspond to an event of any kind related to an originator + question. :param str sender: the unique identifier (SRUID) of the service revision sending the question :param str sender_type: the type of sender for this event; must be one of {"PARENT", "CHILD"} From 45c37ea00adcb3f7801496a77961748094c35c6c Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Thu, 27 Feb 2025 14:54:35 +0000 Subject: [PATCH 157/216] REV: Revert "WIP: Temporarily hard-code WIF details into CI workflow" This reverts commit f0b689fbb3b86dbb1d619a21b9a347123ed12305. --- .github/workflows/python-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index 1e0f859c5..6596a5315 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -53,7 +53,7 @@ jobs: with: # NOTE: If setting create_credentials_file=true, .dockerignore file must include `gha-creds-*.json` to avoid baking these credentials into build create_credentials_file: true - workload_identity_provider: "projects/437801218871/locations/global/workloadIdentityPools/dev-github-actions-pool/providers/dev-github-actions-provider" + workload_identity_provider: "projects/437801218871/locations/global/workloadIdentityPools/github-actions-pool/providers/github-actions-provider" service_account: "github-actions@octue-sdk-python.iam.gserviceaccount.com" - name: Run tests From a35c2bf1b5d6d88837a516680a5cff39958f7919 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Thu, 27 Feb 2025 15:21:10 +0000 Subject: [PATCH 158/216] WIP: Comment out question cancellation methods for now skipci --- octue/cli.py | 62 +++++++++++++++++----------------- octue/cloud/pub_sub/service.py | 59 ++++++++++++++++---------------- octue/resources/child.py | 18 +++++----- 3 files changed, 69 insertions(+), 70 deletions(-) diff --git a/octue/cli.py b/octue/cli.py index c366b035a..692d2c737 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -541,37 +541,37 @@ 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 an Octue 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=None, backend={"name": "GCPPubSubBackend", "project_name": project_name}) - child.cancel(question_uuid=question_uuid, event_store_table_id=service_configuration.event_store_table_id) +# @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 an Octue 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=None, 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) diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index 35ff60b9c..13f041678 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -15,7 +15,6 @@ from octue.cloud.events.extraction import extract_and_deserialise_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 ( @@ -419,35 +418,35 @@ def wait_for_answer( finally: 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 - :return None: - """ - questions = get_events(table_id=event_store_table_id, question_uuid=question_uuid, kinds=["question"]) - - if len(questions) == 0: - raise ValueError(f"No question found with question UUID {question_uuid!r}.") - - if len(questions) > 1: - raise ValueError(f"Multiple questions found with same question UUID {question_uuid!r}.") - - question_finished = get_events( - table_id=event_store_table_id, - question_uuid=question_uuid, - kinds=["result", "exception"], - ) - - if question_finished: - logger.warning("Cannot cancel question %r - it has already finished.", question_uuid) - - question_attributes = EventAttributes(**questions[0]["attributes"]) - self._emit_event({"kind": "cancellation"}, attributes=question_attributes, timeout=timeout) - logger.info("Cancellation of question %r requested.", question_uuid) + # 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 + # :return None: + # """ + # questions = get_events(table_id=event_store_table_id, question_uuid=question_uuid, kinds=["question"]) + # + # if len(questions) == 0: + # raise ValueError(f"No question found with question UUID {question_uuid!r}.") + # + # if len(questions) > 1: + # raise ValueError(f"Multiple questions found with same question UUID {question_uuid!r}.") + # + # question_finished = get_events( + # table_id=event_store_table_id, + # question_uuid=question_uuid, + # kinds=["result", "exception"], + # ) + # + # if question_finished: + # logger.warning("Cannot cancel question %r - it has already finished.", question_uuid) + # + # question_attributes = EventAttributes(**questions[0]["attributes"]) + # self._emit_event({"kind": "cancellation"}, attributes=question_attributes, timeout=timeout) + # logger.info("Cancellation of question %r requested.", question_uuid) def send_exception(self, attributes, timeout=30): """Serialise and send the exception being handled to the parent. diff --git a/octue/resources/child.py b/octue/resources/child.py index cbc0e70cc..24554dd16 100644 --- a/octue/resources/child.py +++ b/octue/resources/child.py @@ -237,12 +237,12 @@ 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, 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) + # 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 1d35b50e7087cc6dc3d24d6c6093ed17702539a1 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Thu, 27 Feb 2025 17:29:43 +0000 Subject: [PATCH 159/216] TST: Use new project for example service --- tests/cloud/deployment/test_kueue_deployment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/cloud/deployment/test_kueue_deployment.py b/tests/cloud/deployment/test_kueue_deployment.py index 0970187fa..7a3da5aa0 100644 --- a/tests/cloud/deployment/test_kueue_deployment.py +++ b/tests/cloud/deployment/test_kueue_deployment.py @@ -19,7 +19,7 @@ class TestKueueDeployment(TestCase): child = Child( id=EXAMPLE_SERVICE_SRUID, - backend={"name": "GCPPubSubBackend", "project_name": os.environ["TEST_PROJECT_NAME"]}, + backend={"name": "GCPPubSubBackend", "project_name": "octue-twined-services"}, ) def test_forwards_exceptions_to_parent(self): From 3ea2289503cf1ab78a2d2a7bae55b70f10b48806 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Thu, 27 Feb 2025 17:30:00 +0000 Subject: [PATCH 160/216] TST: Wait 30s longer for deployment test question skipci --- tests/cloud/deployment/test_kueue_deployment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/cloud/deployment/test_kueue_deployment.py b/tests/cloud/deployment/test_kueue_deployment.py index 7a3da5aa0..a41a9f8c4 100644 --- a/tests/cloud/deployment/test_kueue_deployment.py +++ b/tests/cloud/deployment/test_kueue_deployment.py @@ -46,7 +46,7 @@ def test_asynchronous_question(self): self.assertIsNone(answer) # Wait for question to complete. - time.sleep(60) + time.sleep(90) events = get_events(table_id="octue_twined.service-events", question_uuid=question_uuid) From b2b00fde824c7ef33f753fc4309e08ae65534e68 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Mon, 3 Mar 2025 14:38:35 +0000 Subject: [PATCH 161/216] ENH: Increase max heartbeat interval by 1 minute skipci --- octue/cloud/pub_sub/events.py | 2 +- octue/cloud/pub_sub/service.py | 2 +- octue/resources/child.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/octue/cloud/pub_sub/events.py b/octue/cloud/pub_sub/events.py index bbae0fa59..3c46adb41 100644 --- a/octue/cloud/pub_sub/events.py +++ b/octue/cloud/pub_sub/events.py @@ -109,7 +109,7 @@ def _time_since_last_heartbeat(self): return datetime.now() - self._last_heartbeat - def handle_events(self, timeout=60, maximum_heartbeat_interval=300): + def handle_events(self, timeout=60, maximum_heartbeat_interval=360): """Pull events from the subscription and handle them in the order they were sent until a "result" event is handled, then return the handled result. diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index 13f041678..621c33eb9 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -384,7 +384,7 @@ def wait_for_answer( handle_monitor_message=None, record_events=True, timeout=60, - maximum_heartbeat_interval=300, + maximum_heartbeat_interval=360, ): """Wait for an answer to a question on the given subscription, deleting the subscription and its topic once the answer is received. diff --git a/octue/resources/child.py b/octue/resources/child.py index 24554dd16..6e15e8616 100644 --- a/octue/resources/child.py +++ b/octue/resources/child.py @@ -76,7 +76,7 @@ def ask( prevent_retries_when=None, log_errors=True, timeout=86400, - maximum_heartbeat_interval=300, + maximum_heartbeat_interval=360, ): """Ask the child either: - A synchronous (ask-and-wait) question and wait for it to return an output. Questions are synchronous if From fb3f9a706834ade9b534fc3b4de25f15ef67b19a Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Mon, 3 Mar 2025 15:06:26 +0000 Subject: [PATCH 162/216] TST: Move deployment test to example repo --- tests/cloud/deployment/__init__.py | 0 .../cloud/deployment/test_kueue_deployment.py | 70 ------------------- 2 files changed, 70 deletions(-) delete mode 100644 tests/cloud/deployment/__init__.py delete mode 100644 tests/cloud/deployment/test_kueue_deployment.py diff --git a/tests/cloud/deployment/__init__.py b/tests/cloud/deployment/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/cloud/deployment/test_kueue_deployment.py b/tests/cloud/deployment/test_kueue_deployment.py deleted file mode 100644 index a41a9f8c4..000000000 --- a/tests/cloud/deployment/test_kueue_deployment.py +++ /dev/null @@ -1,70 +0,0 @@ -import os -import time -import unittest -from unittest import TestCase - -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-kueue:0.1.0" - - -@unittest.skipUnless( - condition=os.getenv("RUN_DEPLOYMENT_TEST", "0").lower() == "1", - reason="'RUN_DEPLOYMENT_TEST' environment variable is False or not present.", -) -class TestKueueDeployment(TestCase): - child = Child( - id=EXAMPLE_SERVICE_SRUID, - backend={"name": "GCPPubSubBackend", "project_name": "octue-twined-services"}, - ) - - def test_forwards_exceptions_to_parent(self): - """Test that exceptions raised in the (remote) responding service are forwarded to and raised by the asker.""" - with self.assertRaises(twined.exceptions.InvalidValuesContents): - self.child.ask(input_values={"invalid_input_data": "hello"}) - - def test_synchronous_question(self): - """Test that the Kueue example deployment works, providing a service that can be asked questions and send - responses. - """ - answer, _ = self.child.ask(input_values={"n_iterations": 3}) - - # Check the output values. - self.assertEqual(answer["output_values"], [1, 2, 3, 4, 5]) - - # Check that the output dataset and its files can be accessed. - with answer["output_manifest"].datasets["example_dataset"].files.one() as (datafile, f): - self.assertEqual(f.read(), "This is some example service output.") - - def test_asynchronous_question(self): - """Test asking an asynchronous question and retrieving the resulting events from the event store.""" - answer, question_uuid = self.child.ask(input_values={"n_iterations": 3}, asynchronous=True) - self.assertIsNone(answer) - - # Wait for question to complete. - time.sleep(90) - - events = get_events(table_id="octue_twined.service-events", question_uuid=question_uuid) - - self.assertTrue( - is_event_valid( - event=events[0]["event"], - attributes=events[0]["attributes"], - recipient=None, - parent_sdk_version=None, - child_sdk_version=None, - ) - ) - - replayer = EventReplayer() - answer = replayer.handle_events(events) - - # Check the output values. - self.assertEqual(list(answer["output_values"]), [1, 2, 3, 4, 5]) - - with answer["output_manifest"].datasets["example_dataset"].files.one() as (datafile, f): - self.assertEqual(f.read(), "This is some example service output.") From 2411edaad7d0239d71849aff15de158e16677cc4 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Mon, 3 Mar 2025 16:01:43 +0000 Subject: [PATCH 163/216] OPS: Add terraform config for testing --- terraform/.terraform.lock.hcl | 41 ++++++++++++++++++++++++++++ terraform/main.tf | 50 +++++++++++++++++++++++++++++++++++ terraform/outputs.tf | 10 +++++++ terraform/variables.tf | 28 ++++++++++++++++++++ 4 files changed, 129 insertions(+) create mode 100644 terraform/.terraform.lock.hcl create mode 100644 terraform/main.tf create mode 100644 terraform/outputs.tf create mode 100644 terraform/variables.tf diff --git a/terraform/.terraform.lock.hcl b/terraform/.terraform.lock.hcl new file mode 100644 index 000000000..6bfb5f24a --- /dev/null +++ b/terraform/.terraform.lock.hcl @@ -0,0 +1,41 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/google" { + version = "6.23.0" + constraints = "~> 6.12" + hashes = [ + "h1:Gr39ABNw+A6lwP2gPG+yCzGmU5T97iI5qT0XLCd3Dh4=", + "zh:032dd78eff887a673a1067008a8e47a69983bbcea9f41832320470247a76863a", + "zh:1af89f75142cf9c54499a466c8dc7055e2bbf02771a6b8c8cd57eb13dce9a800", + "zh:3696a8e72c6cef80fec3c3574fc8519f0410f23f6dc3e3540d2f03345c140d38", + "zh:58a15c71ae128ff64117c1c6b9ccf8ab2ac3e8f9c2c52957d8327f93495f62b1", + "zh:70ba2909611e8d1cc8009567e50e195c4269e6582d6a6fa0bce0d4e6313ab8d5", + "zh:8f8489d1eb8c189d59dc85e519e51ab4c4b1940e4d72450ae130ba752028fa01", + "zh:99c8c4e8dc67a7ab597d46ed566f64c4409761276f34bd863457a37254620fa6", + "zh:9b24d53440e8d7e06020e7b3aeca0dde4f1a3ef997d7da05ff3acb918896fcc2", + "zh:b3667fd6057997dbf0bd0179ddf686c272d4ab4fc7da6a03fdf2fad31ad4ecb5", + "zh:cc6df6d2291a337a5f434b7335c693164c6987604b9690d6b953b796d8eaa08a", + "zh:d871f39c3c5b63995793c9a70f107e52ca699d211c770a6e7ebc398ba59bcdc7", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/hashicorp/time" { + version = "0.12.1" + hashes = [ + "h1:JzYsPugN8Fb7C4NlfLoFu7BBPuRVT2/fCOdCaxshveI=", + "zh:090023137df8effe8804e81c65f636dadf8f9d35b79c3afff282d39367ba44b2", + "zh:26f1e458358ba55f6558613f1427dcfa6ae2be5119b722d0b3adb27cd001efea", + "zh:272ccc73a03384b72b964918c7afeb22c2e6be22460d92b150aaf28f29a7d511", + "zh:438b8c74f5ed62fe921bd1078abe628a6675e44912933100ea4fa26863e340e9", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:85c8bd8eefc4afc33445de2ee7fbf33a7807bc34eb3734b8eefa4e98e4cddf38", + "zh:98bbe309c9ff5b2352de6a047e0ec6c7e3764b4ed3dfd370839c4be2fbfff869", + "zh:9c7bf8c56da1b124e0e2f3210a1915e778bab2be924481af684695b52672891e", + "zh:d2200f7f6ab8ecb8373cda796b864ad4867f5c255cff9d3b032f666e4c78f625", + "zh:d8c7926feaddfdc08d5ebb41b03445166df8c125417b28d64712dccd9feef136", + "zh:e2412a192fc340c61b373d6c20c9d805d7d3dee6c720c34db23c2a8ff0abd71b", + "zh:e6ac6bba391afe728a099df344dbd6481425b06d61697522017b8f7a59957d44", + ] +} diff --git a/terraform/main.tf b/terraform/main.tf new file mode 100644 index 000000000..99773cd03 --- /dev/null +++ b/terraform/main.tf @@ -0,0 +1,50 @@ +terraform { + required_version = ">= 1.8.0" + + required_providers { + google = { + source = "hashicorp/google" + version = "~>6.12" + } + } + + cloud { + organization = "octue" + workspaces { + project = "octue-twined" + tags = ["testing"] + } + } +} + + +provider "google" { + project = var.google_cloud_project_id + region = var.google_cloud_region +} + + +data "google_client_config" "default" {} + + +module "octue_twined_core" { + source = "git::github.com/octue/terraform-octue-twined-core.git?ref=create-initial-module" + google_cloud_project_id = var.google_cloud_project_id + google_cloud_region = var.google_cloud_region + github_organisation = var.github_organisation + developer_service_account_names = var.developer_service_account_names + deletion_protection = var.deletion_protection +} + + +resource "google_project_service" "pub_sub" { + service = "pubsub.googleapis.com" + disable_dependent_services = true + project = var.google_cloud_project_id +} + + +resource "google_pubsub_topic" "services_topic" { + name = "main.octue.services" + depends_on = [google_project_service.pub_sub] +} diff --git a/terraform/outputs.tf b/terraform/outputs.tf new file mode 100644 index 000000000..812e0da72 --- /dev/null +++ b/terraform/outputs.tf @@ -0,0 +1,10 @@ +output "event_store" { + description = "The full ID of the BigQuery table acting as the Octue Twined services event store." + value = module.octue_twined_core.bigquery_events_table_id +} + + +output "storage_bucket_url" { + description = "The `gs://` URL of the storage bucket used to store service inputs, outputs, and diagnostics." + value = module.octue_twined_core.storage_bucket_url +} diff --git a/terraform/variables.tf b/terraform/variables.tf new file mode 100644 index 000000000..a571da61d --- /dev/null +++ b/terraform/variables.tf @@ -0,0 +1,28 @@ +variable "google_cloud_project_id" { + type = string + default = "octue-sdk-python" +} + + +variable "google_cloud_region" { + type = string + default = "europe-west9" +} + + +variable "github_organisation" { + type = string + default = "octue" +} + + +variable "developer_service_account_names" { + type = set(string) + default = ["cortadocodes", "thclark"] +} + + +variable "deletion_protection" { + type = bool + default = false +} From ac802cc63da8c55732a6562ad62762121ebf1faf Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Mon, 3 Mar 2025 16:02:01 +0000 Subject: [PATCH 164/216] ENH: Only allow boolean for `forward_logs` attribute in schema --- octue/cloud/events/validation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/octue/cloud/events/validation.py b/octue/cloud/events/validation.py index a99546ffa..2e34802a7 100644 --- a/octue/cloud/events/validation.py +++ b/octue/cloud/events/validation.py @@ -58,7 +58,7 @@ "type": "string", "description": "The UUID of the ultimate question that triggered this tree of questions.", }, - "forward_logs": {"oneOf": [{"type": "boolean"}, {"enum": ["0", "1"]}]}, + "forward_logs": {"type": "boolean"}, "save_diagnostics": { "enum": ["SAVE_DIAGNOSTICS_OFF", "SAVE_DIAGNOSTICS_ON_CRASH", "SAVE_DIAGNOSTICS_ON"] }, From 5fff971bec7dd159d24e99fa63e55f80ddeca9de Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Mon, 3 Mar 2025 16:20:36 +0000 Subject: [PATCH 165/216] OPS: Use updated terraform module input skipci --- terraform/main.tf | 2 +- terraform/variables.tf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/terraform/main.tf b/terraform/main.tf index 99773cd03..ee5bb93a4 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -32,7 +32,7 @@ module "octue_twined_core" { google_cloud_project_id = var.google_cloud_project_id google_cloud_region = var.google_cloud_region github_organisation = var.github_organisation - developer_service_account_names = var.developer_service_account_names + maintainer_service_account_names = var.maintainer_service_account_names deletion_protection = var.deletion_protection } diff --git a/terraform/variables.tf b/terraform/variables.tf index a571da61d..fc90001d0 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -16,7 +16,7 @@ variable "github_organisation" { } -variable "developer_service_account_names" { +variable "maintainer_service_account_names" { type = set(string) default = ["cortadocodes", "thclark"] } From db88914e8ba5d5b8a76e6577ad4dc8c5ed2f9397 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Tue, 4 Mar 2025 11:19:44 +0000 Subject: [PATCH 166/216] ENH: Raise error if service registry request fails --- octue/cloud/service_id.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/octue/cloud/service_id.py b/octue/cloud/service_id.py index b94c45df6..5863437f3 100644 --- a/octue/cloud/service_id.py +++ b/octue/cloud/service_id.py @@ -258,7 +258,7 @@ def get_default_sruid(namespace, name, service_registries): service_id = f"{namespace}/{name}" for registry in service_registries: - response = requests.get(f"{registry['endpoint']}/{service_id}") + response = _make_service_registry_request(registry, namespace, name) if response.ok: revision_tag = response.json()["revision_tag"] @@ -282,7 +282,7 @@ def raise_if_revision_not_registered(sruid, service_registries): namespace, name, revision_tag = split_service_id(sruid, require_revision_tag=True) for registry in service_registries: - response = requests.get(f"{registry['endpoint']}/{namespace}/{name}?revision_tag={revision_tag}") + response = _make_service_registry_request(registry, namespace, name, revision_tag) if response.ok: logger.info("Found service revision %r in %r registry.", sruid, registry["name"]) @@ -291,3 +291,21 @@ def raise_if_revision_not_registered(sruid, service_registries): raise octue.exceptions.ServiceNotFound( f"Service revision {sruid!r} was not found in any of the specified service registries: {service_registries!r}" ) + + +def _make_service_registry_request(registry, namespace, name, revision_tag=None): + """Make a request to a service registry about a service. + + :param dict registry: a dictionary with the keys "endpoint" and "name" + :param str namespace: the namespace of the service + :param str name: the name of the service + :param str|None revision_tag: the revision tag for a revision of the service + :raise requests.exceptions.HTTPError: if the request fails with a status code other than 404 + :return requests.Response: the response from the service registry + """ + response = requests.get(f"{registry['endpoint']}/{namespace}/{name}", params={"revision_tag": revision_tag}) + + if response.status_code != 404: + response.raise_for_status() + + return response From 5b5f75684fd9515cf1dd67872012e10100fce71f Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Tue, 4 Mar 2025 11:21:39 +0000 Subject: [PATCH 167/216] FIX: Only declare service revision found if response code is 200 --- octue/cloud/service_id.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/octue/cloud/service_id.py b/octue/cloud/service_id.py index 5863437f3..64bb98a09 100644 --- a/octue/cloud/service_id.py +++ b/octue/cloud/service_id.py @@ -260,7 +260,7 @@ def get_default_sruid(namespace, name, service_registries): for registry in service_registries: response = _make_service_registry_request(registry, namespace, name) - if response.ok: + if response.status_code == 200: revision_tag = response.json()["revision_tag"] logger.info("Found service revision '%s:%s' in %r registry.", service_id, revision_tag, registry["name"]) return create_sruid(namespace=namespace, name=name, revision_tag=revision_tag) @@ -284,7 +284,7 @@ def raise_if_revision_not_registered(sruid, service_registries): for registry in service_registries: response = _make_service_registry_request(registry, namespace, name, revision_tag) - if response.ok: + if response.status_code == 200: logger.info("Found service revision %r in %r registry.", sruid, registry["name"]) return From 6e6432cac48b5f621fc3856688634c9549dd33b7 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Tue, 4 Mar 2025 11:24:57 +0000 Subject: [PATCH 168/216] TST: Test response to service registry request failure --- tests/cloud/test_service_id.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/cloud/test_service_id.py b/tests/cloud/test_service_id.py index 1aaf64688..4480ad6c6 100644 --- a/tests/cloud/test_service_id.py +++ b/tests/cloud/test_service_id.py @@ -235,6 +235,19 @@ def test_split_service_id(self): class TestGetLatestSRUID(unittest.TestCase): SERVICE_REGISTRIES = [{"name": "Octue Registry", "endpoint": "https://blah.com/services"}] + def test_error_raised_if_request_fails(self): + """Test that an error is raised if the request to the service registry fails.""" + mock_response = requests.Response() + mock_response.status_code = 403 + + with patch("requests.get", return_value=mock_response): + with self.assertRaises(requests.HTTPError): + get_default_sruid( + namespace="my-org", + name="my-service", + service_registries=self.SERVICE_REGISTRIES, + ) + def test_error_raised_if_revision_not_found(self): """Test that an error is raised if no revision is found for the service in the given registries.""" mock_response = requests.Response() @@ -286,6 +299,18 @@ def test_get_latest_sruid_when_not_in_first_registry(self): class TestRaiseIfRevisionNotRegistered(unittest.TestCase): SERVICE_REGISTRIES = [{"name": "Octue Registry", "endpoint": "https://blah.com/services"}] + def test_error_raised_if_request_fails(self): + """Test that an error is raised if the request to the service registry fails.""" + mock_response = requests.Response() + mock_response.status_code = 403 + + with patch("requests.get", return_value=mock_response): + with self.assertRaises(requests.HTTPError): + raise_if_revision_not_registered( + sruid="my-org/my-service:1.0.0", + service_registries=self.SERVICE_REGISTRIES, + ) + def test_error_raised_if_revision_not_found(self): """Test that an error is raised if no revision is found for the service in the given registries.""" mock_response = requests.Response() From 643320b39db4c3cfd5ee83efae393a2d6e8cb138 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Tue, 4 Mar 2025 11:27:55 +0000 Subject: [PATCH 169/216] ENH: Make log message more specific --- octue/cloud/service_id.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/octue/cloud/service_id.py b/octue/cloud/service_id.py index 64bb98a09..50417b1e2 100644 --- a/octue/cloud/service_id.py +++ b/octue/cloud/service_id.py @@ -262,7 +262,14 @@ def get_default_sruid(namespace, name, service_registries): if response.status_code == 200: revision_tag = response.json()["revision_tag"] - logger.info("Found service revision '%s:%s' in %r registry.", service_id, revision_tag, registry["name"]) + + logger.info( + "Found default service revision '%s:%s' in %r registry.", + service_id, + revision_tag, + registry["name"], + ) + return create_sruid(namespace=namespace, name=name, revision_tag=revision_tag) raise octue.exceptions.ServiceNotFound( From 6213f0c524897921d2d18a7422bd35c51f2a9b31 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Tue, 4 Mar 2025 11:35:14 +0000 Subject: [PATCH 170/216] ENH: Make requests to service registries authenticated --- octue/cloud/service_id.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/octue/cloud/service_id.py b/octue/cloud/service_id.py index 50417b1e2..0d21640fb 100644 --- a/octue/cloud/service_id.py +++ b/octue/cloud/service_id.py @@ -4,6 +4,8 @@ import uuid import coolname +import google.auth.transport.requests +import google.oauth2.id_token import requests import octue.exceptions @@ -301,7 +303,7 @@ def raise_if_revision_not_registered(sruid, service_registries): def _make_service_registry_request(registry, namespace, name, revision_tag=None): - """Make a request to a service registry about a service. + """Make an authenticated request to a service registry about a service. :param dict registry: a dictionary with the keys "endpoint" and "name" :param str namespace: the namespace of the service @@ -310,7 +312,13 @@ def _make_service_registry_request(registry, namespace, name, revision_tag=None) :raise requests.exceptions.HTTPError: if the request fails with a status code other than 404 :return requests.Response: the response from the service registry """ - response = requests.get(f"{registry['endpoint']}/{namespace}/{name}", params={"revision_tag": revision_tag}) + id_token = google.oauth2.id_token.fetch_id_token(google.auth.transport.requests.Request(), registry["endpoint"]) + + response = requests.get( + f"{registry['endpoint']}/{namespace}/{name}", + params={"revision_tag": revision_tag}, + headers={"Authorization": f"Bearer {id_token}"}, + ) if response.status_code != 404: response.raise_for_status() From 9fdb7cc0eb0c4cee70b52989fb4472a7bcf05039 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Wed, 5 Mar 2025 16:25:19 +0000 Subject: [PATCH 171/216] OPS: Use version `0.1.1` of `terraform-octue-twined-core` --- terraform/main.tf | 4 ++-- terraform/outputs.tf | 6 +++--- terraform/variables.tf | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/terraform/main.tf b/terraform/main.tf index ee5bb93a4..2e2a41694 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -28,10 +28,10 @@ data "google_client_config" "default" {} module "octue_twined_core" { - source = "git::github.com/octue/terraform-octue-twined-core.git?ref=create-initial-module" + source = "git::github.com/octue/terraform-octue-twined-core.git?ref=0.1.1" google_cloud_project_id = var.google_cloud_project_id google_cloud_region = var.google_cloud_region - github_organisation = var.github_organisation + github_account = var.github_account maintainer_service_account_names = var.maintainer_service_account_names deletion_protection = var.deletion_protection } diff --git a/terraform/outputs.tf b/terraform/outputs.tf index 812e0da72..1683d9c23 100644 --- a/terraform/outputs.tf +++ b/terraform/outputs.tf @@ -1,10 +1,10 @@ -output "event_store" { +output "event_store_id" { + value = module.octue_twined_core.event_store_id description = "The full ID of the BigQuery table acting as the Octue Twined services event store." - value = module.octue_twined_core.bigquery_events_table_id } output "storage_bucket_url" { - description = "The `gs://` URL of the storage bucket used to store service inputs, outputs, and diagnostics." value = module.octue_twined_core.storage_bucket_url + description = "The `gs://` URL of the storage bucket used to store service inputs, outputs, and diagnostics." } diff --git a/terraform/variables.tf b/terraform/variables.tf index fc90001d0..1141c9510 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -10,7 +10,7 @@ variable "google_cloud_region" { } -variable "github_organisation" { +variable "github_account" { type = string default = "octue" } From bdb1cccedd5304f593df0ceb3ab66d636b2d9b4f Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Wed, 5 Mar 2025 16:33:24 +0000 Subject: [PATCH 172/216] ENH: Disallow additional properties in service comms schema --- octue/cloud/events/validation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/octue/cloud/events/validation.py b/octue/cloud/events/validation.py index 2e34802a7..a4c5acf82 100644 --- a/octue/cloud/events/validation.py +++ b/octue/cloud/events/validation.py @@ -27,6 +27,7 @@ "title": "Octue services communication", "description": "A schema describing the events Octue services can emit and consume.", "type": "object", + "additionalProperties": False, "properties": { "attributes": { "title": "Event attributes", From 022a9c8ba4c5b9fe77c82ac96792ded3540c557c Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Wed, 5 Mar 2025 16:38:42 +0000 Subject: [PATCH 173/216] CHO: Update version compatibility data --- docs/source/inter_service_compatibility.rst | 208 ++++++++++---------- octue/metadata/version_compatibilities.json | 203 ++++++++++++++----- 2 files changed, 258 insertions(+), 153 deletions(-) diff --git a/docs/source/inter_service_compatibility.rst b/docs/source/inter_service_compatibility.rst index df6280926..2a4066f19 100644 --- a/docs/source/inter_service_compatibility.rst +++ b/docs/source/inter_service_compatibility.rst @@ -18,106 +18,108 @@ the parent, just that a child is able to accept a question. - ``0`` = incompatible - ``1`` = compatible -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| | 0.61.2 | 0.61.1 | 0.61.0 | 0.60.2 | 0.60.1 | 0.60.0 | 0.59.1 | 0.59.0 | 0.58.0 | 0.57.2 | 0.57.1 | 0.57.0 | 0.56.0 | 0.55.0 | 0.54.0 | 0.53.0 | 0.52.2 | 0.52.1 | 0.52.0 | 0.51.0 | 0.50.1 | 0.50.0 | 0.49.2 | 0.49.1 | 0.49.0 | 0.48.0 | 0.47.2 | 0.47.1 | 0.47.0 | 0.46.3 | 0.46.2 | 0.46.1 | 0.46.0 | 0.45.0 | 0.44.0 | 0.43.7 | 0.43.6 | 0.43.5 | 0.43.4 | 0.43.3 | 0.43.2 | 0.43.1 | 0.43.0 | 0.42.1 | 0.42.0 | 0.41.1 | 0.41.0 | 0.40.2 | 0.40.1 | 0.40.0 | -+========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+ -| 0.61.2 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.61.1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.61.0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.60.2 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.60.1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.60.0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.59.1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.59.0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.58.0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.57.2 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.57.1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.57.0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.56.0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.55.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.54.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.53.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.52.2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.52.1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.52.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.51.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.50.1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.50.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.49.2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.49.1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.49.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.48.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.47.2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.47.1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.47.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.46.3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.46.2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.46.1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.46.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.45.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.44.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.43.7 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.43.6 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.43.5 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.43.4 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.43.3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.43.2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.43.1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.43.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.42.1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.42.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.41.1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.41.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.40.2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.40.1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ -| 0.40.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | -+--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| | 0.62.0 | 0.61.2 | 0.61.1 | 0.61.0 | 0.60.2 | 0.60.1 | 0.60.0 | 0.59.1 | 0.59.0 | 0.58.0 | 0.57.2 | 0.57.1 | 0.57.0 | 0.56.0 | 0.55.0 | 0.54.0 | 0.53.0 | 0.52.2 | 0.52.1 | 0.52.0 | 0.51.0 | 0.50.1 | 0.50.0 | 0.49.2 | 0.49.1 | 0.49.0 | 0.48.0 | 0.47.2 | 0.47.1 | 0.47.0 | 0.46.3 | 0.46.2 | 0.46.1 | 0.46.0 | 0.45.0 | 0.44.0 | 0.43.7 | 0.43.6 | 0.43.5 | 0.43.4 | 0.43.3 | 0.43.2 | 0.43.1 | 0.43.0 | 0.42.1 | 0.42.0 | 0.41.1 | 0.41.0 | 0.40.2 | 0.40.1 | 0.40.0 | ++========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+==========+ +| 0.62.0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.61.2 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.61.1 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.61.0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.60.2 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.60.1 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.60.0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.59.1 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.59.0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.58.0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.57.2 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.57.1 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.57.0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.56.0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.55.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.54.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.53.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.52.2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.52.1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.52.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.51.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.50.1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.50.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.49.2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.49.1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.49.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.48.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.47.2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.47.1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.47.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.46.3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.46.2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.46.1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.46.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.45.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.44.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.43.7 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.43.6 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.43.5 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.43.4 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.43.3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.43.2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.43.1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.43.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.42.1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.42.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.41.1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.41.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.40.2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.40.1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ +| 0.40.0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ++--------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------+ diff --git a/octue/metadata/version_compatibilities.json b/octue/metadata/version_compatibilities.json index e10582ca2..55c3c1574 100644 --- a/octue/metadata/version_compatibilities.json +++ b/octue/metadata/version_compatibilities.json @@ -49,7 +49,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.40.1": { "0.40.1": true, @@ -101,7 +102,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.40.2": { "0.41.0": true, @@ -153,7 +155,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.41.0": { "0.41.0": true, @@ -205,7 +208,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.41.1": { "0.41.1": true, @@ -257,7 +261,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.42.0": { "0.42.0": true, @@ -309,7 +314,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.42.1": { "0.43.2": true, @@ -361,7 +367,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.43.0": { "0.43.2": true, @@ -413,7 +420,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.43.1": { "0.43.2": true, @@ -465,7 +473,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.43.2": { "0.43.2": true, @@ -517,7 +526,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.43.3": { "0.43.3": true, @@ -569,7 +579,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.43.4": { "0.43.4": true, @@ -621,7 +632,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.43.5": { "0.43.5": true, @@ -673,7 +685,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.43.6": { "0.43.6": true, @@ -725,7 +738,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.43.7": { "0.43.7": true, @@ -777,7 +791,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.44.0": { "0.44.0": true, @@ -829,7 +844,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.45.0": { "0.45.0": true, @@ -881,7 +897,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.46.0": { "0.46.0": true, @@ -933,7 +950,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.46.1": { "0.46.1": true, @@ -985,7 +1003,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.46.2": { "0.46.2": true, @@ -1037,7 +1056,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.46.3": { "0.46.3": true, @@ -1089,7 +1109,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.47.0": { "0.47.0": true, @@ -1141,7 +1162,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.47.1": { "0.47.1": true, @@ -1193,7 +1215,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.47.2": { "0.47.2": true, @@ -1245,7 +1268,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.48.0": { "0.48.0": true, @@ -1297,7 +1321,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.49.0": { "0.49.1": true, @@ -1349,7 +1374,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.49.1": { "0.49.1": true, @@ -1401,7 +1427,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.49.2": { "0.49.2": true, @@ -1453,7 +1480,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.50.0": { "0.50.0": true, @@ -1505,7 +1533,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.50.1": { "0.51.0": false, @@ -1557,7 +1586,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.51.0": { "0.51.0": true, @@ -1609,7 +1639,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.52.0": { "0.51.0": true, @@ -1661,7 +1692,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.52.1": { "0.51.0": true, @@ -1713,7 +1745,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.52.2": { "0.51.0": true, @@ -1765,7 +1798,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.53.0": { "0.51.0": false, @@ -1817,7 +1851,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.54.0": { "0.51.0": false, @@ -1869,7 +1904,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.55.0": { "0.51.0": false, @@ -1921,7 +1957,8 @@ "0.60.2": false, "0.61.0": false, "0.61.1": false, - "0.61.2": false + "0.61.2": false, + "0.62.0": false }, "0.56.0": { "0.51.0": false, @@ -1973,7 +2010,8 @@ "0.60.2": true, "0.61.0": true, "0.61.1": true, - "0.61.2": true + "0.61.2": true, + "0.62.0": false }, "0.57.0": { "0.51.0": false, @@ -2025,7 +2063,8 @@ "0.60.2": true, "0.61.0": true, "0.61.1": true, - "0.61.2": true + "0.61.2": true, + "0.62.0": false }, "0.57.1": { "0.51.0": false, @@ -2077,7 +2116,8 @@ "0.60.2": true, "0.61.0": true, "0.61.1": true, - "0.61.2": true + "0.61.2": true, + "0.62.0": false }, "0.57.2": { "0.51.0": false, @@ -2129,7 +2169,8 @@ "0.60.2": true, "0.61.0": true, "0.61.1": true, - "0.61.2": true + "0.61.2": true, + "0.62.0": false }, "0.58.0": { "0.51.0": false, @@ -2181,7 +2222,8 @@ "0.60.2": true, "0.61.0": true, "0.61.1": true, - "0.61.2": true + "0.61.2": true, + "0.62.0": false }, "0.59.0": { "0.51.0": false, @@ -2233,7 +2275,8 @@ "0.60.2": true, "0.61.0": true, "0.61.1": true, - "0.61.2": true + "0.61.2": true, + "0.62.0": false }, "0.59.1": { "0.51.0": false, @@ -2285,7 +2328,8 @@ "0.60.2": true, "0.61.0": true, "0.61.1": true, - "0.61.2": true + "0.61.2": true, + "0.62.0": false }, "0.60.0": { "0.51.0": false, @@ -2337,7 +2381,8 @@ "0.60.2": true, "0.61.0": true, "0.61.1": true, - "0.61.2": true + "0.61.2": true, + "0.62.0": false }, "0.60.1": { "0.51.0": false, @@ -2389,7 +2434,8 @@ "0.60.2": true, "0.61.0": true, "0.61.1": true, - "0.61.2": true + "0.61.2": true, + "0.62.0": false }, "0.60.2": { "0.51.0": false, @@ -2441,7 +2487,8 @@ "0.60.2": true, "0.61.0": true, "0.61.1": true, - "0.61.2": true + "0.61.2": true, + "0.62.0": false }, "0.61.0": { "0.51.0": false, @@ -2493,7 +2540,8 @@ "0.60.2": true, "0.61.0": true, "0.61.1": true, - "0.61.2": true + "0.61.2": true, + "0.62.0": false }, "0.61.1": { "0.51.0": false, @@ -2545,7 +2593,8 @@ "0.60.2": true, "0.61.0": true, "0.61.1": true, - "0.61.2": true + "0.61.2": true, + "0.62.0": false }, "0.61.2": { "0.51.0": false, @@ -2597,6 +2646,60 @@ "0.60.2": true, "0.61.0": true, "0.61.1": true, - "0.61.2": true + "0.61.2": true, + "0.62.0": false + }, + "0.62.0": { + "0.51.0": false, + "0.50.1": false, + "0.50.0": false, + "0.49.2": false, + "0.49.1": false, + "0.49.0": false, + "0.48.0": false, + "0.47.2": false, + "0.47.1": false, + "0.47.0": false, + "0.46.3": false, + "0.46.2": false, + "0.46.1": false, + "0.46.0": false, + "0.45.0": false, + "0.44.0": false, + "0.43.7": false, + "0.43.6": false, + "0.43.5": false, + "0.43.4": false, + "0.43.3": false, + "0.43.2": false, + "0.43.1": false, + "0.43.0": false, + "0.42.1": false, + "0.42.0": false, + "0.41.1": false, + "0.41.0": false, + "0.40.2": false, + "0.40.1": false, + "0.40.0": false, + "0.52.0": false, + "0.52.1": false, + "0.52.2": false, + "0.53.0": false, + "0.54.0": false, + "0.55.0": false, + "0.56.0": false, + "0.57.0": false, + "0.57.1": false, + "0.57.2": false, + "0.58.0": false, + "0.59.0": false, + "0.59.1": false, + "0.60.0": false, + "0.60.1": false, + "0.60.2": false, + "0.61.0": false, + "0.61.1": false, + "0.61.2": false, + "0.62.0": true } } From 28aa7b23587e83e1876c9eef93507aa3fe3c4240 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Wed, 5 Mar 2025 16:39:39 +0000 Subject: [PATCH 174/216] DOC: Remove outdated explanation about service compatibility --- docs/source/inter_service_compatibility.rst | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/docs/source/inter_service_compatibility.rst b/docs/source/inter_service_compatibility.rst index 2a4066f19..03150a3cb 100644 --- a/docs/source/inter_service_compatibility.rst +++ b/docs/source/inter_service_compatibility.rst @@ -3,15 +3,9 @@ Inter-service compatibility =========================== Octue services acting as parents and children communicate with each other according to the `services communication -schema <https://strands.octue.com/octue/service-communication>`_. Up until version ``0.51.0``, services running nearly -all versions of ``octue`` could communicate with each other compatibly. To allow a significant infrastructure upgrade, -version ``0.51.0`` introduced a number of breaking changes to the standard meaning services running versions ``0.51.0`` -to ``0.52.1`` are only able to communicate with other services running versions in the same range. The same applies to -services running versions ``>=0.53.0``. - -The table below shows which ``octue`` versions parents can run (rows) to send questions compatible with versions -children are running (columns). Note that this table does not display whether children's responses are compatible with -the parent, just that a child is able to accept a question. +schema <https://strands.octue.com/octue/service-communication>`_. The table below shows which ``octue`` versions parents +can run (rows) to send questions compatible with versions children are running (columns). Note that this table does not +display whether children's responses are compatible with the parent, just that a child is able to accept a question. **Key** From e103beccf657b629939c160eec0e4459d09c962c Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Wed, 5 Mar 2025 16:55:05 +0000 Subject: [PATCH 175/216] REF: Remove as-yet-unused `cancellation` event from schema --- octue/cloud/events/validation.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/octue/cloud/events/validation.py b/octue/cloud/events/validation.py index a4c5acf82..92e91e487 100644 --- a/octue/cloud/events/validation.py +++ b/octue/cloud/events/validation.py @@ -278,14 +278,6 @@ }, "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"], - }, ], }, }, From f2cbfcbcf79023d558e0794ce65d7e0530c9e209 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Wed, 5 Mar 2025 17:08:06 +0000 Subject: [PATCH 176/216] ENH: Add new resource attributes to schema skipci --- octue/cloud/events/validation.py | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/octue/cloud/events/validation.py b/octue/cloud/events/validation.py index 92e91e487..364d4fcea 100644 --- a/octue/cloud/events/validation.py +++ b/octue/cloud/events/validation.py @@ -59,10 +59,6 @@ "type": "string", "description": "The UUID of the ultimate question that triggered this tree of questions.", }, - "forward_logs": {"type": "boolean"}, - "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.", @@ -97,14 +93,36 @@ "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, }, + "forward_logs": { + "type": "boolean", + "description": "If `True`, forward any logs from the child to the parent and handle them with the local log handlers", + }, + "save_diagnostics": { + "type": "string", + "enum": ["SAVE_DIAGNOSTICS_OFF", "SAVE_DIAGNOSTICS_ON_CRASH", "SAVE_DIAGNOSTICS_ON"], + "description": "If turned on, allow the input values and manifest (and its datasets) to be saved either all the time or just if the analysis fails", + }, + "cpus": { + "type": "integer", + "minimum": 1, + "description": "The number of CPUs to request for the question; defaults to the number set by the child service", + }, + "memory": { + "type": "string", + "description": "The amount of memory to request for the question; defaults to the amount set by the child service", + "examples": ["256Mi", "1Gi"], + }, + "ephemeral_storage": { + "type": "string", + "description": "The amount of ephemeral storage to request for the question; defaults to the amount set by the child service", + "examples": ["256Mi", "1Gi"], + }, }, "required": [ "datetime", "uuid", "question_uuid", "originator_question_uuid", - "forward_logs", - "save_diagnostics", "parent", "originator", "sender", @@ -112,6 +130,8 @@ "sender_sdk_version", "recipient", "retry_count", + "forward_logs", + "save_diagnostics", ], }, { From 9146a60821b1bd7e3a831af579a534c25109e8b6 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Thu, 6 Mar 2025 11:30:07 +0000 Subject: [PATCH 177/216] ENH: Use version `0.15.0` of service communication schema --- octue/cloud/events/validation.py | 288 +------------------------------ 1 file changed, 2 insertions(+), 286 deletions(-) diff --git a/octue/cloud/events/validation.py b/octue/cloud/events/validation.py index 364d4fcea..5b2a2c918 100644 --- a/octue/cloud/events/validation.py +++ b/octue/cloud/events/validation.py @@ -14,297 +14,13 @@ "result", } -SERVICE_COMMUNICATION_SCHEMA_VERSION = "0.14.1" +SERVICE_COMMUNICATION_SCHEMA_VERSION = "0.15.0" 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 = { - "$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", - "additionalProperties": False, - "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": { - "type": "string", - "description": "If this isn't the originating question, the UUID of the question that triggered this question. If it is, don't provide this.", - }, - "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": "^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, - }, - "forward_logs": { - "type": "boolean", - "description": "If `True`, forward any logs from the child to the parent and handle them with the local log handlers", - }, - "save_diagnostics": { - "type": "string", - "enum": ["SAVE_DIAGNOSTICS_OFF", "SAVE_DIAGNOSTICS_ON_CRASH", "SAVE_DIAGNOSTICS_ON"], - "description": "If turned on, allow the input values and manifest (and its datasets) to be saved either all the time or just if the analysis fails", - }, - "cpus": { - "type": "integer", - "minimum": 1, - "description": "The number of CPUs to request for the question; defaults to the number set by the child service", - }, - "memory": { - "type": "string", - "description": "The amount of memory to request for the question; defaults to the amount set by the child service", - "examples": ["256Mi", "1Gi"], - }, - "ephemeral_storage": { - "type": "string", - "description": "The amount of ephemeral storage to request for the question; defaults to the amount set by the child service", - "examples": ["256Mi", "1Gi"], - }, - }, - "required": [ - "datetime", - "uuid", - "question_uuid", - "originator_question_uuid", - "parent", - "originator", - "sender", - "sender_type", - "sender_sdk_version", - "recipient", - "retry_count", - "forward_logs", - "save_diagnostics", - ], - }, - { - "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": { - "type": "string", - "description": "If this isn't the originating question, the UUID of the question that triggered this question. If it is, don't provide this.", - }, - "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", - "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"], - }, - ], - }, - }, - "required": ["attributes", "event"], + "$ref": f"https://jsonschema.registry.octue.com/octue/service-communication/{SERVICE_COMMUNICATION_SCHEMA_VERSION}.json" } - # Instantiate a JSON schema validator to cache the service communication schema. This avoids downloading it from the # registry every time a message is validated against it. jsonschema.Draft202012Validator.check_schema(SERVICE_COMMUNICATION_SCHEMA) From b6969d8c76707e8e279baf6f79d4409663f177b5 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Thu, 6 Mar 2025 12:24:01 +0000 Subject: [PATCH 178/216] TST: Patch ID token request for service registry requests --- octue/cloud/service_id.py | 11 ++++++++++- tests/cloud/pub_sub/test_service.py | 16 +++++++++------- tests/cloud/test_service_id.py | 20 +++++++++++++++++++- 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/octue/cloud/service_id.py b/octue/cloud/service_id.py index 0d21640fb..45ea32d71 100644 --- a/octue/cloud/service_id.py +++ b/octue/cloud/service_id.py @@ -312,7 +312,7 @@ def _make_service_registry_request(registry, namespace, name, revision_tag=None) :raise requests.exceptions.HTTPError: if the request fails with a status code other than 404 :return requests.Response: the response from the service registry """ - id_token = google.oauth2.id_token.fetch_id_token(google.auth.transport.requests.Request(), registry["endpoint"]) + id_token = _get_google_cloud_id_token(registry) response = requests.get( f"{registry['endpoint']}/{namespace}/{name}", @@ -324,3 +324,12 @@ def _make_service_registry_request(registry, namespace, name, revision_tag=None) response.raise_for_status() return response + + +def _get_google_cloud_id_token(registry): + """Get an ID token for Google Cloud. + + :param dict registry: a dictionary with the keys "endpoint" and "name" + :return str: an ID token for Google Cloud + """ + return google.oauth2.id_token.fetch_id_token(google.auth.transport.requests.Request(), registry["endpoint"]) diff --git a/tests/cloud/pub_sub/test_service.py b/tests/cloud/pub_sub/test_service.py index 7fd57d257..8bf7af164 100644 --- a/tests/cloud/pub_sub/test_service.py +++ b/tests/cloud/pub_sub/test_service.py @@ -133,11 +133,12 @@ def test_ask_unregistered_service_revision_when_service_registries_specified_res mock_response.status_code = 404 with patch("requests.get", return_value=mock_response): - with self.assertRaises(exceptions.ServiceNotFound): - service.ask( - service_id=f"my-org/unregistered-service:{MOCK_SERVICE_REVISION_TAG}", - input_values=[1, 2, 3, 4], - ) + with patch("octue.cloud.service_id._get_google_cloud_id_token", return_value="some-token"): + with self.assertRaises(exceptions.ServiceNotFound): + service.ask( + service_id=f"my-org/unregistered-service:{MOCK_SERVICE_REVISION_TAG}", + input_values=[1, 2, 3, 4], + ) def test_ask_unregistered_service_with_no_revision_tag_when_service_registries_specified_results_in_error(self): """Test that an error is raised when attempting to ask a question to an unregistered service without including @@ -152,8 +153,9 @@ def test_ask_unregistered_service_with_no_revision_tag_when_service_registries_s mock_response.status_code = 404 with patch("requests.get", return_value=mock_response): - with self.assertRaises(exceptions.ServiceNotFound): - service.ask(service_id="my-org/unregistered-service", input_values=[1, 2, 3, 4]) + with patch("octue.cloud.service_id._get_google_cloud_id_token", return_value="some-token"): + with self.assertRaises(exceptions.ServiceNotFound): + service.ask(service_id="my-org/unregistered-service", input_values=[1, 2, 3, 4]) def test_ask_service_with_no_revision_tag_when_service_registries_not_specified_results_in_error(self): """Test that an error is raised when attempting to ask a question to a service without including a revision tag diff --git a/tests/cloud/test_service_id.py b/tests/cloud/test_service_id.py index 4480ad6c6..dcf95654b 100644 --- a/tests/cloud/test_service_id.py +++ b/tests/cloud/test_service_id.py @@ -232,9 +232,18 @@ def test_split_service_id(self): self.assertIsNone(revision_tag) -class TestGetLatestSRUID(unittest.TestCase): +class TestGetDefaultSRUID(unittest.TestCase): SERVICE_REGISTRIES = [{"name": "Octue Registry", "endpoint": "https://blah.com/services"}] + @classmethod + def setUpClass(cls): + cls.id_token_patch = patch("octue.cloud.service_id._get_google_cloud_id_token", return_value="some-token") + cls.id_token_patch.start() + + @classmethod + def tearDownClass(cls): + cls.id_token_patch.stop() + def test_error_raised_if_request_fails(self): """Test that an error is raised if the request to the service registry fails.""" mock_response = requests.Response() @@ -299,6 +308,15 @@ def test_get_latest_sruid_when_not_in_first_registry(self): class TestRaiseIfRevisionNotRegistered(unittest.TestCase): SERVICE_REGISTRIES = [{"name": "Octue Registry", "endpoint": "https://blah.com/services"}] + @classmethod + def setUpClass(cls): + cls.id_token_patch = patch("octue.cloud.service_id._get_google_cloud_id_token", return_value="some-token") + cls.id_token_patch.start() + + @classmethod + def tearDownClass(cls): + cls.id_token_patch.stop() + def test_error_raised_if_request_fails(self): """Test that an error is raised if the request to the service registry fails.""" mock_response = requests.Response() From fa97144c8056ec8a66d397014025c87970aaff72 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Thu, 6 Mar 2025 14:52:22 +0000 Subject: [PATCH 179/216] FIX: Ensure event UUID and datetime is unique for each event --- octue/cloud/events/attributes.py | 9 +++++++++ octue/cloud/pub_sub/service.py | 4 +++- tests/cloud/events/test_attributes.py | 16 ++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/octue/cloud/events/attributes.py b/octue/cloud/events/attributes.py index 9c8e3e8d9..9633ad5c7 100644 --- a/octue/cloud/events/attributes.py +++ b/octue/cloud/events/attributes.py @@ -73,6 +73,15 @@ def __init__( self.memory = memory self.ephemeral_storage = ephemeral_storage + def refresh(self): + """Set a new UUID and datetime. This avoids having to create a new instance for every single event (for which + all other attributes are the same). + + :return None: + """ + self.uuid = str(uuid_library.uuid4()) + self.datetime = dt.datetime.now(tz=dt.timezone.utc) + def make_opposite_attributes(self): """Create the attributes for an event of the opposite sender type to this event (parent -> child or child -> parent). For example, if these attributes are for a question event, create the attributes for a response diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index 621c33eb9..a4f029ef8 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -486,11 +486,13 @@ def _emit_event(self, event, attributes, wait=True, timeout=30): - `datetime` :param dict event: JSON-serialisable data to emit as an event - :param octue.cloud.events.attributes.EventAttributes attributes: the attributes to use for the event + :param octue.cloud.events.attributes.EventAttributes attributes: the attributes to use for the event :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: """ + attributes.refresh() + future = self.publisher.publish( topic=self.services_topic.path, data=json.dumps(event, cls=OctueJSONEncoder).encode(), diff --git a/tests/cloud/events/test_attributes.py b/tests/cloud/events/test_attributes.py index 3dc8f4944..9774dc240 100644 --- a/tests/cloud/events/test_attributes.py +++ b/tests/cloud/events/test_attributes.py @@ -43,6 +43,22 @@ def test_defaults(self): }, ) + def test_refresh(self): + """Test that refreshing the attributes changes the UUID and datetime.""" + attributes = EventAttributes( + sender=SENDER, + sender_type=SENDER_TYPE, + recipient=RECIPIENT, + question_uuid=QUESTION_UUID, + ) + + original_uuid = attributes.uuid + original_datetime = attributes.datetime + + attributes.refresh() + self.assertNotEqual(attributes.uuid, original_uuid) + self.assertNotEqual(attributes.datetime, original_datetime) + def test_make_opposite_attributes(self): """Test that the sender and recipient are reversed when making opposite attributes from a set of attributes.""" attributes = EventAttributes( From d2f97569fa00724976f3e1c5c0af43460ee75889 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Thu, 6 Mar 2025 15:03:23 +0000 Subject: [PATCH 180/216] DOC: Replace "Octue service" with "Twined service" in docs --- docs/source/asking_questions.rst | 6 +++--- docs/source/authentication.rst | 2 +- docs/source/creating_apps.rst | 2 +- docs/source/creating_services.rst | 10 ++++----- docs/source/datafile.rst | 2 +- docs/source/dataset.rst | 2 +- docs/source/deploying_services.rst | 10 ++++----- docs/source/index.rst | 10 ++++----- docs/source/inter_service_compatibility.rst | 2 +- docs/source/logging.rst | 2 +- docs/source/manifest.rst | 8 +++---- docs/source/running_services_locally.rst | 2 +- docs/source/services.rst | 23 ++++++++++----------- docs/source/updating_services.rst | 4 ++-- 14 files changed, 42 insertions(+), 43 deletions(-) diff --git a/docs/source/asking_questions.rst b/docs/source/asking_questions.rst index 4c00e45c2..38d055b28 100644 --- a/docs/source/asking_questions.rst +++ b/docs/source/asking_questions.rst @@ -219,7 +219,7 @@ This method uses multithreading, allowing all the questions to be asked at once Asking a question within a service ================================== -If you have :doc:`created your own Octue service <creating_services>` and want to ask children questions, you can do +If you have :doc:`created your own Twined service <creating_services>` and want to ask children questions, you can do this more easily than above. Children are accessible from the ``analysis`` object by the keys you give them in the :ref:`app configuration <app_configuration>` file. For example, you can ask an ``elevation`` service a question like this: @@ -353,11 +353,11 @@ Using a service registry When asking a question, you can optionally specify one or more `service registries <https://django-twined.readthedocs.io/en/latest/>`_ to resolve SRUIDs against. This is analogous to specifying a different ``pip`` index for resolving package names when using ``pip install``. If you don't specify any registries, the -default Octue service registry is used. +default Twined service registry is used. Specifying service registries can be useful if: -- You have your own private services that aren't on the default Octue service registry +- You have your own private services that aren't on the default Twined service registry - You want services from one service registry with the same name as in another service registry to be prioritised Specifying service registries diff --git a/docs/source/authentication.rst b/docs/source/authentication.rst index aadedfe14..9177af92f 100644 --- a/docs/source/authentication.rst +++ b/docs/source/authentication.rst @@ -4,7 +4,7 @@ Authentication You need authentication while using ``octue`` to: - Access data from Google Cloud Storage -- Use, run, or deploy Octue services +- Use, run, or deploy Twined services Authentication can be provided by using one of: diff --git a/docs/source/creating_apps.rst b/docs/source/creating_apps.rst index e68e00040..ba7dcdd72 100644 --- a/docs/source/creating_apps.rst +++ b/docs/source/creating_apps.rst @@ -58,7 +58,7 @@ Accessing inputs and storing outputs ------------------------------------ Your app must access configuration and input data from and store output data on the :mod:`analysis <octue.resources.analysis.Analysis>` parameter (for function-based apps) or attribute (for class-based apps). This allows standardised -configuration/input/output validation against the twine and interoperability of all Octue services while leaving you +configuration/input/output validation against the twine and interoperability of all Twined services while leaving you freedom to do any kind of computation. To access the data, use the following attributes on the ``analysis`` parameter/attribute: diff --git a/docs/source/creating_services.rst b/docs/source/creating_services.rst index 3661745ec..87932e9b0 100644 --- a/docs/source/creating_services.rst +++ b/docs/source/creating_services.rst @@ -9,9 +9,9 @@ return answers. They can run locally on any machine or be deployed to the cloud. - The language of the entrypoint must by ``python3`` (you can call processes using other languages within this though) -Anatomy of an Octue service +Anatomy of an Twined service =========================== -An Octue service is defined by the following files (located in the repository root by default). +An Twined service is defined by the following files (located in the repository root by default). app.py ------ @@ -71,7 +71,7 @@ App configuration file (optional) ---- - If your app needs any configuration, asks questions to any other Octue services, or produces output + If your app needs any configuration, asks questions to any other Twined services, or produces output datafiles/datasets, you will need to provide an app configuration. Currently, this must take the form of a JSON file. It can contain the following keys: @@ -93,7 +93,7 @@ Dockerfile (optional) ---- - Octue services run in a Docker container if they are deployed. They can also run this way locally. The SDK + Twined services run in a Docker container if they are deployed. They can also run this way locally. The SDK provides a default ``Dockerfile`` for these purposes that will work for most cases: - For deploying to `Google Cloud Run <https://github.com/octue/octue-sdk-python/blob/main/octue/cloud/deployment/google/cloud_run/Dockerfile>`_ @@ -149,7 +149,7 @@ Template apps We've created some template apps for you to look at and play around with. We recommend going through them in this order: 1. The `fractal app template <https://github.com/octue/octue-sdk-python/tree/main/octue/templates/template-fractal>`_ - - introduces a basic Octue service that returns output values to its parent. + introduces a basic Twined service that returns output values to its parent. 2. The `using-manifests app template <https://github.com/octue/octue-sdk-python/tree/main/octue/templates/template-using-manifests>`_ - introduces using a manifest of output datasets to return output files to its parent. 3. The `child-services app template <https://github.com/octue/octue-sdk-python/tree/main/octue/templates/template-child-services>`_ - diff --git a/docs/source/datafile.rst b/docs/source/datafile.rst index d33213441..6e408f784 100644 --- a/docs/source/datafile.rst +++ b/docs/source/datafile.rst @@ -21,7 +21,7 @@ Datafile Use a datafile to work with a file if you want to: - Read/write to local and cloud files in the same way - - Include it in a :doc:`dataset <dataset>` that can be sent to an Octue service for processing + - Include it in a :doc:`dataset <dataset>` that can be sent to an Twined service for processing - Add metadata to it for future sorting and filtering Key features diff --git a/docs/source/dataset.rst b/docs/source/dataset.rst index 1ad79450b..f040d847d 100644 --- a/docs/source/dataset.rst +++ b/docs/source/dataset.rst @@ -23,7 +23,7 @@ Dataset - Group together a set of files that naturally relate to each other e.g. a timeseries that's been split into multiple files. - Add metadata to it for future sorting and filtering - - Include it in a :doc:`manifest <manifest>` with other datasets and send them to an Octue service for processing + - Include it in a :doc:`manifest <manifest>` with other datasets and send them to an Twined service for processing Key features diff --git a/docs/source/deploying_services.rst b/docs/source/deploying_services.rst index fef8ee3fa..2bb5bb88d 100644 --- a/docs/source/deploying_services.rst +++ b/docs/source/deploying_services.rst @@ -3,8 +3,8 @@ ====================================== Deploying services (developer's guide) ====================================== -This is a guide for developers that want to deploy Octue services themselves - it is not needed if Octue manages your -services for you or if you are only asking questions to existing Octue services. +This is a guide for developers that want to deploy Twined services themselves - it is not needed if Octue manages your +services for you or if you are only asking questions to existing Twined services. .. attention:: @@ -19,11 +19,11 @@ services for you or if you are only asking questions to existing Octue services. What deployment enables ----------------------- -Deploying an Octue service to Google Cloud Run means it: +Deploying an Twined service to Google Cloud Run means it: * Is deployed as a docker container -* Is ready to be asked questions by any other Octue service that has the correct permissions (you can control this) -* Can ask questions to any other Octue service for which it has the correct permissions +* Is ready to be asked questions by any other Twined service that has the correct permissions (you can control this) +* Can ask questions to any other Twined service for which it has the correct permissions * Will automatically build and redeploy upon the conditions you provide (e.g. pushes or merges into ``main``) * Will automatically start and run when Pub/Sub messages are received from the topic you created. The Pub/Sub messages can be sent from anywhere in the world, but the container will only run in the region you chose (you can diff --git a/docs/source/index.rst b/docs/source/index.rst index dcb2e6b79..f61e2e025 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -2,16 +2,16 @@ Octue SDK (Python) ================== -The python SDK for `Octue <https://octue.com>`_ data services, digital twins, and applications - get faster data +The python SDK for `Octue <https://octue.com>`_ Twined scientific data services and digital twins - get faster data groundwork so you have more time for the science! .. _service_definition: .. admonition:: Definition - Octue service - An Octue data service, digital twin, or application that can be asked questions, process them, and return - answers. Octue services can communicate with each other with minimal extra setup. + Octue Twined service + A data service or digital twin built on the ``octue`` SDK that can be asked questions, process them, and return + answers. Twined services can communicate with each other with minimal extra setup. Key features @@ -40,7 +40,7 @@ Key features **Create, run, and deploy your apps as services** - No need to change your app - just wrap it -- Use the ``octue`` CLI to run your service locally or deploy it to Google Cloud Run +- Use the ``octue`` CLI to run your service locally or deploy it to Google Kubernetes Engine (GKE) - Create JSON-schema interfaces to explicitly define the form of configuration, input, and output data - Ask other services questions as part of your app (i.e. build trees of services) - Automatically display readable, colourised logs, or use your own log handler diff --git a/docs/source/inter_service_compatibility.rst b/docs/source/inter_service_compatibility.rst index 03150a3cb..2170f4db4 100644 --- a/docs/source/inter_service_compatibility.rst +++ b/docs/source/inter_service_compatibility.rst @@ -2,7 +2,7 @@ Inter-service compatibility =========================== -Octue services acting as parents and children communicate with each other according to the `services communication +Twined services acting as parents and children communicate with each other according to the `services communication schema <https://strands.octue.com/octue/service-communication>`_. The table below shows which ``octue`` versions parents can run (rows) to send questions compatible with versions children are running (columns). Note that this table does not display whether children's responses are compatible with the parent, just that a child is able to accept a question. diff --git a/docs/source/logging.rst b/docs/source/logging.rst index 43a437a78..6a717a489 100644 --- a/docs/source/logging.rst +++ b/docs/source/logging.rst @@ -29,7 +29,7 @@ This is followed by the actual log message on the right: Colourised services ------------------- -Another advantage to using the Octue log handler is that each Octue service is coloured according to its position in the +Another advantage to using the Octue log handler is that each Twined service is coloured according to its position in the tree, making it much easier to read log messages from multiple levels of children. .. image:: images/coloured_logs.png diff --git a/docs/source/manifest.rst b/docs/source/manifest.rst index 0b299343e..30f4a4b01 100644 --- a/docs/source/manifest.rst +++ b/docs/source/manifest.rst @@ -8,11 +8,11 @@ Manifest :mod:`Manifest <octue.resources.manifest.Manifest>` A set of related cloud and/or local :doc:`datasets <dataset>`, metadata, and helper methods. Typically produced - by or needed for processing by an Octue service. + by or needed for processing by an Twined service. .. tip:: - Use a manifest to send :doc:`datasets <dataset>` to an Octue service as a question (for processing) - the service + Use a manifest to send :doc:`datasets <dataset>` to an Twined service as a question (for processing) - the service will send an output manifest back with its answer if the answer includes output datasets. @@ -38,7 +38,7 @@ Make a clear grouping of datasets needed for a particular analysis. Send datasets to a service -------------------------- -Get an Octue service to analyse data for you as part of a larger analysis. +Get an Twined service to analyse data for you as part of a larger analysis. .. code-block:: python @@ -56,7 +56,7 @@ See :doc:`here <asking_questions>` for more information. Receive datasets from a service ------------------------------- -Access output datasets from an Octue service from the cloud when you're ready. +Access output datasets from an Twined service from the cloud when you're ready. .. code-block:: python diff --git a/docs/source/running_services_locally.rst b/docs/source/running_services_locally.rst index ff42f4c29..2dcc997d0 100644 --- a/docs/source/running_services_locally.rst +++ b/docs/source/running_services_locally.rst @@ -8,7 +8,7 @@ Services can be operated locally (e.g. for testing or ad-hoc data processing). Y - Via the CLI - By using the ``octue`` library in a python script -- Start your service as a child, allowing it to answer any number of questions from any other Octue service: +- Start your service as a child, allowing it to answer any number of questions from any other Twined service: - Via the CLI diff --git a/docs/source/services.rst b/docs/source/services.rst index 2a71acd72..d1752305b 100644 --- a/docs/source/services.rst +++ b/docs/source/services.rst @@ -1,26 +1,25 @@ .. _services: -============== -Octue services -============== +===================== +Octue Twined services +===================== -There's a growing range of live :ref:`services <service_definition>` in the Octue ecosystem that you can ask questions -to and get answers from. Currently, all of them are related to wind energy. Here's a quick glossary of terms before we -tell you more: +There's a growing range of live :ref:`services <service_definition>` in the Octue ecosystem that you can query. +Currently, all of them are related to wind energy. Here's a quick glossary of terms before we tell you more: .. admonition:: Definitions - Octue service + Twined service See :ref:`here <service_definition>`. Child - An Octue service that can be asked a question. This name reflects the tree structure of services (specifically, + A Twined service that can be asked a question. This name reflects the tree structure of services (specifically, `a DAG <https://en.wikipedia.org/wiki/Directed_acyclic_graph>`_) formed by the service asking the question (the parent), the child it asks the question to, any children that the child asks questions to as part of forming its answer, and so on. Parent - An Octue service that asks a question to another Octue service (a child). + An Twined service that asks a question to another Twined service (a child). Asking a question Sending data (input values and/or an input manifest) to a child for processing/analysis. @@ -28,7 +27,7 @@ tell you more: Receiving an answer Receiving data (output values and/or an output manifest) from a child you asked a question to. - Octue ecosystem + Twined ecosystem The set of services running the Octue SDK as their backend. These services guarantee: - Defined input/output JSON schemas and validation @@ -48,7 +47,7 @@ They look like ``namespace/name:tag`` where the tag is often a semantic version .. admonition:: Definitions Service revision - A specific instance of an Octue service that can be individually addressed. The revision could correspond to a + A specific instance of an Twined service that can be individually addressed. The revision could correspond to a version of the service, a dynamic development branch for it, or a deliberate duplication or variation of it. .. _sruid_definition: @@ -93,7 +92,7 @@ They look like ``namespace/name:tag`` where the tag is often a semantic version Service communication standard ============================== -Octue services communicate according to the service communication standard. The JSON schema defining this can be found +Twined services communicate according to the service communication standard. The JSON schema defining this can be found `here <https://strands.octue.com/octue/service-communication>`_. Messages received by services are validated against it and invalid messages are rejected. The schema is in beta, so (rare) breaking changes are reflected in the minor version number. diff --git a/docs/source/updating_services.rst b/docs/source/updating_services.rst index e4867e2ab..5d3f38c51 100644 --- a/docs/source/updating_services.rst +++ b/docs/source/updating_services.rst @@ -1,9 +1,9 @@ .. _updating_services: -Updating an Octue service +Updating an Twined service ========================= -This page describes how to update an existing, deployed Octue service - in other words, how to deploy a new Octue +This page describes how to update an existing, deployed Twined service - in other words, how to deploy a new Octue service revision. We assume that: From 31d805c6e31f0ea62ccd76ca93c778ad5d9b17d0 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Thu, 6 Mar 2025 15:15:13 +0000 Subject: [PATCH 181/216] FIX: Remove extra `'` from log message --- octue/cloud/events/handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/octue/cloud/events/handler.py b/octue/cloud/events/handler.py index 45942d343..e2ab00661 100644 --- a/octue/cloud/events/handler.py +++ b/octue/cloud/events/handler.py @@ -155,7 +155,7 @@ def _handle_delivery_acknowledgement(self, event, attributes): :param dict attributes: the event's attributes :return None: """ - logger.info("%r's question was delivered at %s.", attributes["recipient"], attributes["datetime"]) + logger.info("%rs question was delivered at %s.", attributes["recipient"], attributes["datetime"]) def _handle_heartbeat(self, event, attributes): """Record the time the heartbeat was received. From 0b3670f1cd7225100734ee0c23322dd05f027494 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Thu, 6 Mar 2025 15:21:35 +0000 Subject: [PATCH 182/216] REF: Simplify allowing service configuration to not exist --- octue/cli.py | 7 ++++--- octue/configuration.py | 15 +++++++++++---- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/octue/cli.py b/octue/cli.py index 692d2c737..e76a1aa2b 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -129,10 +129,11 @@ def remote(sruid, input_values, input_manifest, project_name, asynchronous, serv octue question ask remote your-org/example-service:1.2.0 """ - try: - service_configuration = ServiceConfiguration.from_file(service_config) + service_configuration = ServiceConfiguration.from_file(service_config, allow_not_found=True) + + if service_configuration: service_registries = service_configuration.service_registries - except FileNotFoundError: + else: service_registries = None if input_values: diff --git a/octue/configuration.py b/octue/configuration.py index 150ef3922..ed49c8a21 100644 --- a/octue/configuration.py +++ b/octue/configuration.py @@ -76,16 +76,23 @@ def __init__( logger.warning(f"The following keyword arguments were not used by {type(self).__name__}: {kwargs!r}.") @classmethod - def from_file(cls, path=None): + def from_file(cls, path=None, allow_not_found=False): """Load a service configuration from a YAML file. :param str|None path: the path to the service configuration YAML file; if not provided, the `OCTUE_SERVICE_CONFIGURATION_PATH` environment variable is used if present, otherwise the local path `octue.yaml` is used - :return ServiceConfiguration: the service configuration loaded from the file + :param bool allow_not_found: if `True`, return `None` if a service configuration file isn't found + :return ServiceConfiguration|None: the service configuration loaded from the file """ path = path or os.environ.get("OCTUE_SERVICE_CONFIGURATION_PATH", DEFAULT_SERVICE_CONFIGURATION_PATH) - with open(path) as f: - raw_service_configuration = yaml.load(f, Loader=yaml.SafeLoader) + try: + with open(path) as f: + raw_service_configuration = yaml.load(f, Loader=yaml.SafeLoader) + except FileNotFoundError as error: + if allow_not_found: + return None + else: + raise error absolute_path = os.path.abspath(path) logger.info("Service configuration loaded from %r.", absolute_path) From 479ff2ede739960b76810a1533a0e52df6ef06cc Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Thu, 6 Mar 2025 15:24:49 +0000 Subject: [PATCH 183/216] ENH: Set default table ID in `get_events` --- octue/cloud/pub_sub/bigquery.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/octue/cloud/pub_sub/bigquery.py b/octue/cloud/pub_sub/bigquery.py index a6ef64ce2..c9af946c8 100644 --- a/octue/cloud/pub_sub/bigquery.py +++ b/octue/cloud/pub_sub/bigquery.py @@ -27,7 +27,7 @@ def get_events( - table_id, + table_id="octue_twined.service-events", question_uuid=None, parent_question_uuid=None, originator_question_uuid=None, @@ -47,7 +47,7 @@ def get_events( When the limit is smaller than the total number of events, the default behaviour is to return the "tail" of the event stream for the question (the most recent n events for the question). - :param str table_id: the full ID of the Google BigQuery table used as the event store e.g. "your-project.your-dataset.your-table" + :param str table_id: the full ID of the Google BigQuery table used as the event store e.g. "your-dataset.your-table" :param str|None question_uuid: the UUID of a question to get events for :param str|None parent_question_uuid: the UUID of a parent question to get the sub-question events for :param str|None originator_question_uuid: the UUID of an originator question get the full tree of events for From 57f64e677dca435e6d7c3b307835ae0f2e9be9ba Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Thu, 6 Mar 2025 15:26:43 +0000 Subject: [PATCH 184/216] ENH: Remove requirement for service config for `events get` command --- octue/cli.py | 11 ++++++++--- octue/cloud/pub_sub/bigquery.py | 4 +++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/octue/cli.py b/octue/cli.py index e76a1aa2b..b94ca8a32 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -13,7 +13,7 @@ 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.bigquery import get_events +from octue.cloud.pub_sub.bigquery import get_events, DEFAULT_EVENT_STORE_TABLE_ID 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 @@ -335,10 +335,15 @@ def get( if exclude_kinds: exclude_kinds = exclude_kinds.split(",") - service_configuration = ServiceConfiguration.from_file(path=service_config) + service_configuration = ServiceConfiguration.from_file(path=service_config, allow_not_found=True) + + if service_configuration: + event_store_table_id = service_configuration.event_store_table_id + else: + event_store_table_id = DEFAULT_EVENT_STORE_TABLE_ID events = get_events( - table_id=service_configuration.event_store_table_id, + table_id=event_store_table_id, question_uuid=question_uuid, parent_question_uuid=parent_question_uuid, originator_question_uuid=originator_question_uuid, diff --git a/octue/cloud/pub_sub/bigquery.py b/octue/cloud/pub_sub/bigquery.py index c9af946c8..79f769738 100644 --- a/octue/cloud/pub_sub/bigquery.py +++ b/octue/cloud/pub_sub/bigquery.py @@ -23,11 +23,13 @@ "`other_attributes`", ) +DEFAULT_EVENT_STORE_TABLE_ID = "octue_twined.service-events" + BACKEND_METADATA_FIELDS = ("`backend`", "`backend_metadata`") def get_events( - table_id="octue_twined.service-events", + table_id=DEFAULT_EVENT_STORE_TABLE_ID, question_uuid=None, parent_question_uuid=None, originator_question_uuid=None, From e7ea523280c22c71de171147c0e59354fe4c9a8b Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Thu, 6 Mar 2025 15:28:28 +0000 Subject: [PATCH 185/216] ENH: Remove requirement for service config for `events replay` command skipci --- octue/cli.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/octue/cli.py b/octue/cli.py index b94ca8a32..8914c35e8 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -448,10 +448,15 @@ def replay( if exclude_kinds: exclude_kinds = exclude_kinds.split(",") - service_configuration = ServiceConfiguration.from_file(path=service_config) + service_configuration = ServiceConfiguration.from_file(path=service_config, allow_not_found=True) + + if service_configuration: + event_store_table_id = service_configuration.event_store_table_id + else: + event_store_table_id = DEFAULT_EVENT_STORE_TABLE_ID events = get_events( - table_id=service_configuration.event_store_table_id, + table_id=event_store_table_id, question_uuid=question_uuid, parent_question_uuid=parent_question_uuid, originator_question_uuid=originator_question_uuid, From 2330239bf6c9eaa319b75da6b5f2b9aadc18ba27 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Thu, 6 Mar 2025 15:59:06 +0000 Subject: [PATCH 186/216] DOC: Update "asking questions" documentation --- docs/source/asking_questions.rst | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/docs/source/asking_questions.rst b/docs/source/asking_questions.rst index 38d055b28..c88ff9305 100644 --- a/docs/source/asking_questions.rst +++ b/docs/source/asking_questions.rst @@ -63,14 +63,18 @@ You can also set the following options when you call :mod:`Child.ask <octue.reso - ``subscribe_to_logs`` - if true, the child will forward its logs to you - ``allow_local_files`` - if true, local files/datasets are allowed in any input manifest you supply - ``handle_monitor_message`` - if provided a function, it will be called on any monitor messages from the child -- ``record_events`` – if ``True``, events received from the parent while it processes the question are saved to the ``Child.received_events`` property +- ``record_events`` – if true, events received from the parent while it processes the question are saved to the ``Child.received_events`` property - ``save_diagnostics`` – must be one of {"SAVE_DIAGNOSTICS_OFF", "SAVE_DIAGNOSTICS_ON_CRASH", "SAVE_DIAGNOSTICS_ON"}; if turned on, allow the input values and manifest (and its datasets) to be saved by the child either all the time or just if the analysis fails - ``question_uuid`` - if provided, the question will use this UUID instead of a generated one - ``push_endpoint`` - if provided, the result and other events produced during the processing of the question will be pushed to this HTTP endpoint (a URL) -- ``asynchronous`` - if ``True``, don't wait for an answer to the question (the result and other events can be :ref:`retrieved from the event store later <retrieving_asynchronous_answers>`) +- ``asynchronous`` - if true, don't wait for an answer to the question (the result and other events can be :ref:`retrieved from the event store later <retrieving_asynchronous_answers>`) +- ``cpus`` - the number of CPUs to request for the question; defaults to the number set by the child service +- ``memory`` - the amount of memory to request for the question e.g. "256Mi" or "1Gi"; defaults to the amount set by the child service +- ``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 - ``timeout`` - how long in seconds to wait for an answer (``None`` by default - i.e. don't time out) If the question fails: + - If ``raise_errors=False``, the unraised error is returned - If ``raise_errors=False`` and ``max_retries > 0``, the question is retried up to this number of times - If ``raise_errors=False``, ``max_retries > 0``, and ``prevent_retries_when`` is a list of exception types, the question is retried unless the error type is in the list @@ -101,14 +105,11 @@ access the event store and run: from octue.cloud.pub_sub.bigquery import get_events - events = get_events( - table_id="your-project.your-dataset.your-table", - question_uuid="53353901-0b47-44e7-9da3-a3ed59990a71", - ) + events = get_events(question_uuid="53353901-0b47-44e7-9da3-a3ed59990a71") **Options** - +- ``table_id`` - If you're not using the standard deployment, you can specify a different table here - ``question_uuid`` - Retrieve events from this specific question - ``parent_question_uuid`` - Retrieve events from questions triggered by the same parent question (this doesn't include the parent question's events) - ``originator_question_uuid`` - Retrieve events for the entire tree of questions triggered by an originator question (a question asked manually through ``Child.ask``; this does include the originator question's events) @@ -351,20 +352,17 @@ whole tree of children, grandchildren, and so on, please `upvote this issue. Using a service registry ======================== When asking a question, you can optionally specify one or more `service registries -<https://django-twined.readthedocs.io/en/latest/>`_ to resolve SRUIDs against. This is analogous to specifying a -different ``pip`` index for resolving package names when using ``pip install``. If you don't specify any registries, the -default Twined service registry is used. - -Specifying service registries can be useful if: +<https://django-twined.readthedocs.io/en/latest/>`_ to resolve SRUIDs against. This checks if the service revision +exists (good for catching typos in SRUIDs) and raises an error if it doesn't. Service registries can also get the +default revision of a service if you don't provide a revision tag. Asking a question if without specifying a registry +will bypass these checks. -- You have your own private services that aren't on the default Twined service registry -- You want services from one service registry with the same name as in another service registry to be prioritised Specifying service registries ----------------------------- You can specify service registries in two ways: -1. Globally for all questions asked inside a service. In the service configuration (``octue.yaml`` file): +1. For all questions asked inside a service. In the service configuration (``octue.yaml`` file): .. code-block:: yaml From 3bf7e54ca51ed29d75ed9ae2615d66932d0041b2 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Thu, 6 Mar 2025 16:11:50 +0000 Subject: [PATCH 187/216] DOC: Update "creating services" documentation --- docs/source/creating_services.rst | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/docs/source/creating_services.rst b/docs/source/creating_services.rst index 87932e9b0..385a4ce07 100644 --- a/docs/source/creating_services.rst +++ b/docs/source/creating_services.rst @@ -52,7 +52,6 @@ octue.yaml - ``app_source_path: <path>`` - if your ``app.py`` file is not in the repository root - ``app_configuration_path: <path>`` - if your app needs an app configuration file that isn't in the repository root - - ``dockerfile_path: <path>`` - if your app needs a ``Dockerfile`` that isn't in the repository root All paths should be relative to the repository root. Other valid entries can be found in the :mod:`ServiceConfiguration <octue.configuration.ServiceConfiguration>` constructor. @@ -96,7 +95,7 @@ Dockerfile (optional) Twined services run in a Docker container if they are deployed. They can also run this way locally. The SDK provides a default ``Dockerfile`` for these purposes that will work for most cases: - - For deploying to `Google Cloud Run <https://github.com/octue/octue-sdk-python/blob/main/octue/cloud/deployment/google/cloud_run/Dockerfile>`_ + - For deploying to `Kubernetes <https://github.com/octue/octue-sdk-python/blob/main/octue/cloud/deployment/dockerfiles/Dockerfile-python311>`_ However, you may need to write and provide your own ``Dockerfile`` if your app requires: @@ -106,10 +105,11 @@ Dockerfile (optional) Here are two examples of a custom ``Dockerfile`` that use different base images: - - `A TurbSim service <https://github.com/aerosense-ai/turbsim-service/blob/main/Dockerfile>`_ - - `An OpenFAST service <https://github.com/aerosense-ai/openfast-service/blob/main/Dockerfile>`_ + - `A TurbSim service <https://github.com/octue/turbsim-service/blob/main/Dockerfile>`_ + - `An OpenFAST service <https://github.com/octue/openfast-service/blob/main/Dockerfile>`_ - If you do provide one, you must specify its path in ``octue.yaml`` under the ``dockerfile_path`` key. + If you do provide one, you must provide its path relative to your repository to the `build-twined-services` + GitHub Actions `workflow <https://github.com/octue/workflows/blob/main/.github/workflows/build-twined-service.yml>`_. As always, if you need help with this, feel free to drop us a message or raise an issue! @@ -160,14 +160,11 @@ Deploying services automatically ================================ Automated deployment with Octue means: -- Your service runs in Google Cloud, ready to accept questions from and return answers to other services. +- Your service runs in Google Kubernetes Engine (GKE), ready to accept questions from and return answers to other services. - You don't need to do anything to update your deployed service with new code changes - the service simply gets rebuilt and re-deployed each time you push a commit to your ``main`` branch, or merge a pull request into it (other branches and deployment strategies are available, but this is the default). - Serverless is the default - your service only runs when questions from other services are sent to it, meaning there - is no cost to having it deployed but not in use. + are minimal costs to having it deployed but not in use. -To enable automated deployments, contact us so we can create a Google Cloud Build trigger linked to your git repository. -This requires no work from you apart from authorising the connection to GitHub (or another git provider). - -If you want to deploy services yourself, see :doc:`here <deploying_services>`. +If you'd like help deploying services, contact us. To do it yourself, see :doc:`here <deploying_services>`. From 87d91e32f44d790349809792ee8cf44a933f9dc4 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Thu, 6 Mar 2025 16:28:21 +0000 Subject: [PATCH 188/216] DOC: Update "updating services" documentation --- docs/source/updating_services.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/source/updating_services.rst b/docs/source/updating_services.rst index 5d3f38c51..dc7c09db3 100644 --- a/docs/source/updating_services.rst +++ b/docs/source/updating_services.rst @@ -3,15 +3,15 @@ Updating an Twined service ========================= -This page describes how to update an existing, deployed Twined service - in other words, how to deploy a new Octue +This page describes how to update an existing, deployed Twined service - in other words, how to deploy a new Twined service revision. We assume that: - Your service's repository is on GitHub and you have push access to it -- Octue's `standard deployment GitHub Actions workflow <https://github.com/octue/workflows/blob/main/.github/workflows/deploy-cloud-run-service.yml>`_ - is set up in the repository and being used to deploy the service to Google Cloud Run on merge of a pull request into - the ``main`` branch (see an example `here <https://github.com/octue/example-service-cloud-run/blob/main/.github/workflows/cd.yaml>`_) +- The `standard Twined deployment GitHub Actions workflow <https://github.com/octue/workflows/blob/main/.github/workflows/build-twined-service.yml>`_ + is set up in the repository and being used to build and push the service image to the artifact registry on merge of a + pull request into the ``main`` branch (see an example `here <https://github.com/octue/example-service-kueue/blob/main/.github/workflows/cd.yml>`_) - A release workflow is set up that will tag and release the new service revision on GitHub (see an example `here <https://github.com/octue/example-service-cloud-run/blob/main/.github/workflows/release.yml>`_) From b20fe07048a8ed5bbbde6d0b618f0de755530ef6 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Thu, 6 Mar 2025 16:34:26 +0000 Subject: [PATCH 189/216] DOC: Update docstring --- octue/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/octue/cli.py b/octue/cli.py index 8914c35e8..03e5621e6 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -204,7 +204,7 @@ def remote(sruid, input_values, input_manifest, project_name, asynchronous, serv 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 + This command is similar to running `octue 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 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. From 0508e484fe35d72567fb88985661a96a69e59220 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Thu, 6 Mar 2025 16:34:58 +0000 Subject: [PATCH 190/216] DOC: Update "running services locally" documentation skipci --- docs/source/running_services_locally.rst | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/docs/source/running_services_locally.rst b/docs/source/running_services_locally.rst index 2dcc997d0..88dc5f94f 100644 --- a/docs/source/running_services_locally.rst +++ b/docs/source/running_services_locally.rst @@ -5,7 +5,7 @@ Services can be operated locally (e.g. for testing or ad-hoc data processing). Y - Run your service once (i.e. run one analysis): - - Via the CLI + - Via the ``octue`` CLI - By using the ``octue`` library in a python script - Start your service as a child, allowing it to answer any number of questions from any other Twined service: @@ -19,22 +19,14 @@ Running a service once Via the CLI ----------- 1. Ensure you've created a valid :ref:`octue.yaml <octue_yaml>` file for your service -2. If your service requires inputs, create an input directory with the following structure - - .. code-block:: shell - input_directory - |--- values.json (if input values are required) - |--- manifest.json (if an input manifest is required) - -3. Run: +2. Run: .. code-block:: shell - octue run --input-dir=my_input_directory + octue question ask local --input-values='{"some": "input"}' -Any output values will be printed to ``stdout`` and any output datasets will be referenced in an output manifest file -named ``output_manifest_<analysis_id>.json``. +The output values and/or manifest will be printed to ``stdout`` but are also stored in the event store. Via a python script ------------------- From ad56feb85d43870f2436192c196f2c1b5f68f1a0 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Thu, 6 Mar 2025 16:41:34 +0000 Subject: [PATCH 191/216] DOC: Fix "an Twined" typo skipci --- docs/source/creating_services.rst | 4 ++-- docs/source/datafile.rst | 2 +- docs/source/dataset.rst | 2 +- docs/source/deploying_services.rst | 2 +- docs/source/manifest.rst | 8 ++++---- docs/source/services.rst | 4 ++-- docs/source/updating_services.rst | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/source/creating_services.rst b/docs/source/creating_services.rst index 385a4ce07..34470d81a 100644 --- a/docs/source/creating_services.rst +++ b/docs/source/creating_services.rst @@ -9,9 +9,9 @@ return answers. They can run locally on any machine or be deployed to the cloud. - The language of the entrypoint must by ``python3`` (you can call processes using other languages within this though) -Anatomy of an Twined service +Anatomy of a Twined service =========================== -An Twined service is defined by the following files (located in the repository root by default). +A Twined service is defined by the following files (located in the repository root by default). app.py ------ diff --git a/docs/source/datafile.rst b/docs/source/datafile.rst index 6e408f784..32c405987 100644 --- a/docs/source/datafile.rst +++ b/docs/source/datafile.rst @@ -21,7 +21,7 @@ Datafile Use a datafile to work with a file if you want to: - Read/write to local and cloud files in the same way - - Include it in a :doc:`dataset <dataset>` that can be sent to an Twined service for processing + - Include it in a :doc:`dataset <dataset>` that can be sent to a Twined service for processing - Add metadata to it for future sorting and filtering Key features diff --git a/docs/source/dataset.rst b/docs/source/dataset.rst index f040d847d..f7f4a686b 100644 --- a/docs/source/dataset.rst +++ b/docs/source/dataset.rst @@ -23,7 +23,7 @@ Dataset - Group together a set of files that naturally relate to each other e.g. a timeseries that's been split into multiple files. - Add metadata to it for future sorting and filtering - - Include it in a :doc:`manifest <manifest>` with other datasets and send them to an Twined service for processing + - Include it in a :doc:`manifest <manifest>` with other datasets and send them to a Twined service for processing Key features diff --git a/docs/source/deploying_services.rst b/docs/source/deploying_services.rst index 2bb5bb88d..bffb69f4f 100644 --- a/docs/source/deploying_services.rst +++ b/docs/source/deploying_services.rst @@ -19,7 +19,7 @@ services for you or if you are only asking questions to existing Twined services What deployment enables ----------------------- -Deploying an Twined service to Google Cloud Run means it: +Deploying a Twined service to Google Cloud Run means it: * Is deployed as a docker container * Is ready to be asked questions by any other Twined service that has the correct permissions (you can control this) diff --git a/docs/source/manifest.rst b/docs/source/manifest.rst index 30f4a4b01..95ca60368 100644 --- a/docs/source/manifest.rst +++ b/docs/source/manifest.rst @@ -8,11 +8,11 @@ Manifest :mod:`Manifest <octue.resources.manifest.Manifest>` A set of related cloud and/or local :doc:`datasets <dataset>`, metadata, and helper methods. Typically produced - by or needed for processing by an Twined service. + by or needed for processing by a Twined service. .. tip:: - Use a manifest to send :doc:`datasets <dataset>` to an Twined service as a question (for processing) - the service + Use a manifest to send :doc:`datasets <dataset>` to a Twined service as a question (for processing) - the service will send an output manifest back with its answer if the answer includes output datasets. @@ -38,7 +38,7 @@ Make a clear grouping of datasets needed for a particular analysis. Send datasets to a service -------------------------- -Get an Twined service to analyse data for you as part of a larger analysis. +Get a Twined service to analyse data for you as part of a larger analysis. .. code-block:: python @@ -56,7 +56,7 @@ See :doc:`here <asking_questions>` for more information. Receive datasets from a service ------------------------------- -Access output datasets from an Twined service from the cloud when you're ready. +Access output datasets from a Twined service from the cloud when you're ready. .. code-block:: python diff --git a/docs/source/services.rst b/docs/source/services.rst index d1752305b..08d8011f4 100644 --- a/docs/source/services.rst +++ b/docs/source/services.rst @@ -19,7 +19,7 @@ Currently, all of them are related to wind energy. Here's a quick glossary of te its answer, and so on. Parent - An Twined service that asks a question to another Twined service (a child). + A Twined service that asks a question to another Twined service (a child). Asking a question Sending data (input values and/or an input manifest) to a child for processing/analysis. @@ -47,7 +47,7 @@ They look like ``namespace/name:tag`` where the tag is often a semantic version .. admonition:: Definitions Service revision - A specific instance of an Twined service that can be individually addressed. The revision could correspond to a + A specific instance of a Twined service that can be individually addressed. The revision could correspond to a version of the service, a dynamic development branch for it, or a deliberate duplication or variation of it. .. _sruid_definition: diff --git a/docs/source/updating_services.rst b/docs/source/updating_services.rst index dc7c09db3..443e41fe3 100644 --- a/docs/source/updating_services.rst +++ b/docs/source/updating_services.rst @@ -1,6 +1,6 @@ .. _updating_services: -Updating an Twined service +Updating a Twined service ========================= This page describes how to update an existing, deployed Twined service - in other words, how to deploy a new Twined From e0c85ce23dd4620532005284b4513c62c52c5461 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Mon, 10 Mar 2025 12:39:42 +0000 Subject: [PATCH 192/216] DOC: Rewrite "deploying services" documentation skipci --- docs/source/deploying_services.rst | 144 +++++++++++------------------ 1 file changed, 55 insertions(+), 89 deletions(-) diff --git a/docs/source/deploying_services.rst b/docs/source/deploying_services.rst index bffb69f4f..a87b310e2 100644 --- a/docs/source/deploying_services.rst +++ b/docs/source/deploying_services.rst @@ -6,93 +6,59 @@ Deploying services (developer's guide) This is a guide for developers that want to deploy Twined services themselves - it is not needed if Octue manages your services for you or if you are only asking questions to existing Twined services. -.. attention:: - - The ``octue deploy`` CLI command can be used to deploy services automatically, but it: - - - Is in alpha so may not work as intended - - Requires the ``gcloud`` CLI tool with ``Google Cloud SDK 367.0.0`` and ``beta 2021.12.10`` to be available - - Requires the correct permissions via the ``gcloud`` tool logged into a Google user account and/or with an - appropriate service account available - - For now, we recommend `contacting us <https://www.octue.com/contact>`_ to help set up deployments for you. - What deployment enables ------------------------ -Deploying a Twined service to Google Cloud Run means it: - -* Is deployed as a docker container -* Is ready to be asked questions by any other Twined service that has the correct permissions (you can control this) -* Can ask questions to any other Twined service for which it has the correct permissions -* Will automatically build and redeploy upon the conditions you provide (e.g. pushes or merges into ``main``) -* Will automatically start and run when Pub/Sub messages are received from the topic you created. The Pub/Sub - messages can be sent from anywhere in the world, but the container will only run in the region you chose (you can - create multiple Cloud Run services in different regions for the same repository if this is a problem). -* Will automatically stop shortly after finishing the analyses asked for in the Pub/Sub message (although - you can set a minimum container count so one is always running to minimise cold starts). - -How to deploy -------------- -1. Ensuring you are in the desired project, go to the `Google Cloud Run <https://console.cloud.google.com/run>`_ page - and create a new service - -.. image:: images/deploying_services_advanced/create_service.png - -2. Give your service a unique name - -.. image:: images/deploying_services_advanced/service_name_and_region.png - -3. Choose a `low-carbon region <https://cloud.google.com/sustainability/region-carbon#data>`_ that supports Eventarc - triggers and is in a convenient geographic location for you (e.g. physically close to you for low latency or in a - region compatible with your data protection requirements). - -.. image:: images/deploying_services_advanced/low_carbon_regions.png - -3. Click "Next". When changes are made to the source code, we want them to be deployed automatically. So, we need to - connect the repository to GCP to enable this. Select "Continuously deploy new revisions from a source repository" and - then "Set up with cloud build". - -.. image:: images/deploying_services_advanced/set_up_with_cloud_build.png - -4. Choose your source code repository provider and the repository containing the code you'd like to deploy. You'll have - to give the provider permission to access the repository. If your provider isn't GitHub, BitBucket, or Google Cloud - Source Repositories (GCSR), you'll need to mirror the repository to GCSR before completing this step as Google Cloud - Build only supports these three providers currently. - -.. image:: images/deploying_services_advanced/choose_repository.png - -5. Click "Next", enter a regular expression for the branches you want to automatically deploy from (``main`` by default). - As the service will run in a docker container, select "Dockerfile" and click "Save". - -.. image:: images/deploying_services_advanced/choose_dockerfile.png - -6. Click "Next". If you want your service to be private, select "Allow internal traffic only" and "Require - authentication". This stops anyone without permission from using the service. - -.. image:: images/deploying_services_advanced/set_traffic.png - -7. The service needs a trigger to start up and respond to. We'll be using Google Pub/Sub. Click "Add eventarc trigger", - choose "Cloud Pub/Sub topic" as the trigger event, click on the menu called "Select a Cloud Pub/Sub topic", then - click "Create a topic". Any services that want to ask your service a question will publish their question to this - topic. - -.. image:: images/deploying_services_advanced/create_trigger.png - -8. The topic ID should be in the form ``octue.services.my-organisation.my-service``. Click "Create topic". - -9. Under "Invocation settings", click on the "Service account" menu and then "Create new service account". - -.. image:: images/deploying_services_advanced/create_service_account.png - -10. Make a new service account with a related name e.g. "my-service", then click "Create". Add the - "octue-service-user" and "Cloud Run Invoker" roles to the service account. Contact us if the "octue-service-user" - role is not available. - -.. image:: images/deploying_services_advanced/add_roles_to_service_account.png - -11. Click "Save" and then "Create". - -.. image:: images/deploying_services_advanced/save_and_create.png - -12. You can now view your service in the list of `Cloud Run services <https://console.cloud.google.com/run>`_ and view - its build trigger in the list of `Cloud Build triggers <https://console.cloud.google.com/cloud-build>`_. +======================= +Deploying a Twined service means the service: + +* Is a docker image that is spun up and down in a Kubernetes cluster on demand +* Is ready at any time to answer questions from users and other Twined services in the service network +* Can ask questions to any other Twined service in the service network +* Will automatically spin down after it has finished answering a question +* Will automatically build and redeploy after a relevant code change (e.g. on push or merge into ``main``) + +Prerequisites +============= +Twined services are currently deployable to Google Cloud Platform (GCP). You must have "owner" level access to the GCP +project you're deploying to and billing must be set up for it. + +Deploying step-by-step +====================== +The main part of deployment is deploying the service network infrastructure. Once this is done, services can be easily +added as necessary. + +There are three steps to a deployment: + +1. Deploy the core infrastructure (e.g. storage bucket, event store, service accounts and roles) +2. Deploy the Kubernetes cluster and partner cloud functions +3. Build and push service docker images to the artifact registry + +Deploy core infrastructure +-------------------------- + +- Deploy the ``terraform-octue-twined-core`` Terraform module +- This only needs to be done once per service network +- Follow the instructions `here <https://github.com/octue/terraform-octue-twined-core>`_ + +Deploy Kubernetes cluster +------------------------- + +- Deploy the ``terraform-octue-twined-cluster`` Terraform module +- This only needs to be done once per service network +- Follow the instructions `here <https://github.com/octue/terraform-octue-twined-cluster>`_ + +Build and push service docker images +------------------------------------ +Your service is available if its docker image is in the service network's artifact registry repository. We recommend +pushing a new image for each merge into the ``main`` branch, corresponding to a new service revision. + +- Add the `build-push-twined-service <https://github.com/octue/workflows/blob/main/.github/workflows/build-twined-service.yml>`_ + GitHub Actions workflow to your service's GitHub repository +- This needs to be done for every service you want to deploy +- Follow the instructions `here <https://github.com/octue/workflows#deploying-a-kuberneteskueue-octue-twined-service-revision>`_ +- A live example can be `found here <https://github.com/octue/example-service-kueue/blob/main/.github/workflows/release.yml>`_ + including automated pre-deployment testing and release of the code on GitHub + +What next? +========== +:doc:`Ask your service some questions <asking_questions>`! It will be available in the service network as +``<namespace>/<name>:<version>`` (e.g. ``octue/example-service-kueue:0.1.1``). From 08f2a3af3ffac39a64dff5f957abff5d500a0674 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Mon, 10 Mar 2025 15:12:18 +0000 Subject: [PATCH 193/216] DOC: Fix workflow name in docs --- docs/source/deploying_services.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/deploying_services.rst b/docs/source/deploying_services.rst index a87b310e2..44139ff93 100644 --- a/docs/source/deploying_services.rst +++ b/docs/source/deploying_services.rst @@ -51,7 +51,7 @@ Build and push service docker images Your service is available if its docker image is in the service network's artifact registry repository. We recommend pushing a new image for each merge into the ``main`` branch, corresponding to a new service revision. -- Add the `build-push-twined-service <https://github.com/octue/workflows/blob/main/.github/workflows/build-twined-service.yml>`_ +- Add the `build-twined-service <https://github.com/octue/workflows/blob/main/.github/workflows/build-twined-service.yml>`_ GitHub Actions workflow to your service's GitHub repository - This needs to be done for every service you want to deploy - Follow the instructions `here <https://github.com/octue/workflows#deploying-a-kuberneteskueue-octue-twined-service-revision>`_ From 4db55558777f27203dfce192f9a61f1994c389ed Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Mon, 10 Mar 2025 15:17:11 +0000 Subject: [PATCH 194/216] DOC: Update "troubleshooting services" docs --- docs/source/troubleshooting_services.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/troubleshooting_services.rst b/docs/source/troubleshooting_services.rst index 3e77d9aab..5f4c80cac 100644 --- a/docs/source/troubleshooting_services.rst +++ b/docs/source/troubleshooting_services.rst @@ -27,15 +27,15 @@ message. A user with credentials to access this path can use the ``octue`` CLI t .. code-block:: shell - octue get-diagnostics <cloud-path> + octue question diagnostics <cloud-path> More information on the command: .. code-block:: - >>> octue get-diagnostics -h + >>> octue question diagnostics -h - Usage: octue get-diagnostics [OPTIONS] CLOUD_PATH + Usage: octue question diagnostics [OPTIONS] CLOUD_PATH Download diagnostics for an analysis from the given directory in Google Cloud Storage. The cloud path should end in the analysis ID. From 9007fdeea213d7d407d88e9db4fcb98761634cb6 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Mon, 10 Mar 2025 15:36:54 +0000 Subject: [PATCH 195/216] DOC: Update "authentication" documentation skipci --- docs/source/authentication.rst | 39 ++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/docs/source/authentication.rst b/docs/source/authentication.rst index 9177af92f..8c12f1450 100644 --- a/docs/source/authentication.rst +++ b/docs/source/authentication.rst @@ -6,40 +6,43 @@ You need authentication while using ``octue`` to: - Access data from Google Cloud Storage - Use, run, or deploy Twined services -Authentication can be provided by using one of: - -- A service account -- Application Default Credentials +Authentication is provided by a GCP service account. Creating a service account ========================== -1. Create a service account (see Google's `getting started guide <https://cloud.google.com/docs/authentication/getting-started>`__) -2. Make sure your service account has access to any buckets you need, Google Pub/Sub, and Google Cloud Run if your - service is deployed on it (`see here <https://cloud.google.com/storage/docs/access-control/using-iam-permissions>`_) +By setting up your Twined service network with the :doc:`Twined Terraform modules <deploying_services>`, a set of +maintainer service accounts have already been created with the required permissions. Using a service account ======================= Locally ------- -1. Create and download a key for your service account - it will be called ``your-project-XXXXX.json``. +1. Access your service accounts `here <https://console.cloud.google.com/iam-admin/serviceaccounts>`_, making sure the + correct project is selected + +2. Click on the relevant service account, go to the "Keys" tab, and create (download) a JSON key for it - it will be + called ``<project-name>-XXXXX.json``. .. DANGER:: It's best not to store this in your project to prevent accidentally committing it or building it into a docker - image layer. Instead, bind mount it into your docker image from somewhere else on your local system. + image layer. Instead, keep it somewhere else on your local system with any other service account keys you already + have. - If you must keep within your project, it's good practice to name the file ``gha-greds-<whatever>.json`` and make - sure that ``gha-creds-*`` is in your ``.gitignore`` and ``.dockerignore`` files. + If you must keep within your project, it's good practice to name the file ``gcp-credentials.json`` and make + sure that ``gcp-cred*`` is in your ``.gitignore`` and ``.dockerignore`` files. -2. If you're developing in a container (like a VSCode ``.devcontainer``), mount the file into the container. You can +2. If you're developing in a container (like a VSCode ``devcontainer``), mount the file into the container. You can make gcloud available too - check out `this tutorial <https://medium.com/datamindedbe/application-default-credentials-477879e31cb5>`_. -3. Set the ``GOOGLE_APPLICATION_CREDENTIALS`` environment variable to the path of the key file. +3. Set the ``GOOGLE_APPLICATION_CREDENTIALS`` environment variable to the absolute path of the key file. If using a + ``devcontainer``, make sure this is the path inside the container and not the path on your local machine. -On GCP infrastructure ---------------------- -- Credentials are provided when running code on GCP infrastructure (e.g. Google Cloud Run) -- ``octue`` uses these when when running on these platforms -- You should ensure the correct service account is being used by the deployed instance +On GCP infrastructure / deployed services +----------------------------------------- +- Credentials are automatically provided when running code or services on GCP infrastructure, including the Kubernetes + cluster +- ``octue`` uses these when when running on these platforms, so there's no need to upload a service account key or + include one in service docker images From 01502ba8a197dd53b57ec6b82da80a0d37485ceb Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Mon, 10 Mar 2025 15:54:40 +0000 Subject: [PATCH 196/216] DOC: Remove boilerplate bibliography skipci --- docs/source/bibliography.rst | 128 ----------------------------------- docs/source/index.rst | 1 - 2 files changed, 129 deletions(-) delete mode 100644 docs/source/bibliography.rst diff --git a/docs/source/bibliography.rst b/docs/source/bibliography.rst deleted file mode 100644 index ab2a29f71..000000000 --- a/docs/source/bibliography.rst +++ /dev/null @@ -1,128 +0,0 @@ -.. _sec-bibliography: - -============ -Bibliography -============ - -.. [Agarwal] S. Agarwal, N. Snavely, S. M. Seitz and R. Szeliski, - **Bundle Adjustment in the Large**, *Proceedings of the European - Conference on Computer Vision*, pp. 29--42, 2010. - -.. [Bjorck] A. Bjorck, **Numerical Methods for Least Squares - Problems**, SIAM, 1996 - -.. [Brown] D. C. Brown, **A solution to the general problem of - multiple station analytical stereo triangulation**, Technical - Report 43, Patrick Airforce Base, Florida, 1958. - -.. [ByrdNocedal] R. H. Byrd, J. Nocedal, R. B. Schanbel, - **Representations of Quasi-Newton Matrices and their use in Limited - Memory Methods**, *Mathematical Programming* 63(4):129–-156, 1994. - -.. [ByrdSchnabel] R.H. Byrd, R.B. Schnabel, and G.A. Shultz, **Approximate - solution of the trust region problem by minimization over - two dimensional subspaces**, *Mathematical programming*, - 40(1):247–263, 1988. - -.. [Chen] Y. Chen, T. A. Davis, W. W. Hager, and - S. Rajamanickam, **Algorithm 887: CHOLMOD, Supernodal Sparse - Cholesky Factorization and Update/Downdate**, *TOMS*, 35(3), 2008. - -.. [Conn] A.R. Conn, N.I.M. Gould, and P.L. Toint, **Trust region - methods**, *Society for Industrial Mathematics*, 2000. - -.. [GolubPereyra] G.H. Golub and V. Pereyra, **The differentiation of - pseudo-inverses and nonlinear least squares problems whose - variables separate**, *SIAM Journal on numerical analysis*, - 10(2):413–432, 1973. - -.. [HartleyZisserman] R.I. Hartley & A. Zisserman, **Multiview - Geometry in Computer Vision**, Cambridge University Press, 2004. - -.. [KanataniMorris] K. Kanatani and D. D. Morris, **Gauges and gauge - transformations for uncertainty description of geometric structure - with indeterminacy**, *IEEE Transactions on Information Theory* - 47(5):2017-2028, 2001. - -.. [Keys] R. G. Keys, **Cubic convolution interpolation for digital - image processing**, *IEEE Trans. on Acoustics, Speech, and Signal - Processing*, 29(6), 1981. - -.. [KushalAgarwal] A. Kushal and S. Agarwal, **Visibility based - preconditioning for bundle adjustment**, *In Proceedings of the - IEEE Conference on Computer Vision and Pattern Recognition*, 2012. - -.. [Kanzow] C. Kanzow, N. Yamashita and M. Fukushima, - **Levenberg–Marquardt methods with strong local convergence - properties for solving nonlinear equations with convex - constraints**, *Journal of Computational and Applied Mathematics*, - 177(2):375–397, 2005. - -.. [Levenberg] K. Levenberg, **A method for the solution of certain - nonlinear problems in least squares**, *Quart. Appl. Math*, - 2(2):164–168, 1944. - -.. [LiSaad] Na Li and Y. Saad, **MIQR: A multilevel incomplete qr - preconditioner for large sparse least squares problems**, *SIAM - Journal on Matrix Analysis and Applications*, 28(2):524–550, 2007. - -.. [Madsen] K. Madsen, H.B. Nielsen, and O. Tingleff, **Methods for - nonlinear least squares problems**, 2004. - -.. [Mandel] J. Mandel, **On block diagonal and Schur complement - preconditioning**, *Numer. Math.*, 58(1):79–93, 1990. - -.. [Marquardt] D.W. Marquardt, **An algorithm for least squares - estimation of nonlinear parameters**, *J. SIAM*, 11(2):431–441, - 1963. - -.. [Mathew] T.P.A. Mathew, **Domain decomposition methods for the - numerical solution of partial differential equations**, Springer - Verlag, 2008. - -.. [NashSofer] S.G. Nash and A. Sofer, **Assessing a search direction - within a truncated newton method**, *Operations Research Letters*, - 9(4):219–221, 1990. - -.. [Nocedal] J. Nocedal, **Updating Quasi-Newton Matrices with Limited - Storage**, *Mathematics of Computation*, 35(151): 773--782, 1980. - -.. [NocedalWright] J. Nocedal & S. Wright, **Numerical Optimization**, - Springer, 2004. - -.. [Oren] S. S. Oren, **Self-scaling Variable Metric (SSVM) Algorithms - Part II: Implementation and Experiments**, Management Science, - 20(5), 863-874, 1974. - -.. [Ridders] C. J. F. Ridders, **Accurate computation of F'(x) and - F'(x) F"(x)**, Advances in Engineering Software 4(2), 75-76, 1978. - -.. [RuheWedin] A. Ruhe and P.Å. Wedin, **Algorithms for separable - nonlinear least squares problems**, Siam Review, 22(3):318–337, - 1980. - -.. [Saad] Y. Saad, **Iterative methods for sparse linear - systems**, SIAM, 2003. - -.. [Stigler] S. M. Stigler, **Gauss and the invention of least - squares**, *The Annals of Statistics*, 9(3):465-474, 1981. - -.. [TenenbaumDirector] J. Tenenbaum & B. Director, **How Gauss - Determined the Orbit of Ceres**. - -.. [TrefethenBau] L.N. Trefethen and D. Bau, **Numerical Linear - Algebra**, SIAM, 1997. - -.. [Triggs] B. Triggs, P. F. Mclauchlan, R. I. Hartley & - A. W. Fitzgibbon, **Bundle Adjustment: A Modern Synthesis**, - Proceedings of the International Workshop on Vision Algorithms: - Theory and Practice, pp. 298-372, 1999. - -.. [Wiberg] T. Wiberg, **Computation of principal components when data - are missing**, In Proc. *Second Symp. Computational Statistics*, - pages 229–236, 1976. - -.. [WrightHolt] S. J. Wright and J. N. Holt, **An Inexact - Levenberg Marquardt Method for Large Sparse Nonlinear Least - Squares**, *Journal of the Australian Mathematical Society Series - B*, 26(4):387–403, 1985. diff --git a/docs/source/index.rst b/docs/source/index.rst index f61e2e025..c5dffb873 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -96,4 +96,3 @@ We use `GitHub Issues <https://github.com/octue/octue-sdk-python/issues>`_ [#]_ api license version_history - bibliography From 10ae13c522820d74086285504feb105a5c58ac0d Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Mon, 10 Mar 2025 16:52:22 +0000 Subject: [PATCH 197/216] DOC: Improve docstring --- octue/configuration.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/octue/configuration.py b/octue/configuration.py index ed49c8a21..50d83f35f 100644 --- a/octue/configuration.py +++ b/octue/configuration.py @@ -4,7 +4,6 @@ import yaml - logger = logging.getLogger(__name__) @@ -80,7 +79,7 @@ def from_file(cls, path=None, allow_not_found=False): """Load a service configuration from a YAML file. :param str|None path: the path to the service configuration YAML file; if not provided, the `OCTUE_SERVICE_CONFIGURATION_PATH` environment variable is used if present, otherwise the local path `octue.yaml` is used - :param bool allow_not_found: if `True`, return `None` if a service configuration file isn't found + :param bool allow_not_found: if `True`, return `None` instead of raising an error if a service configuration file isn't found :return ServiceConfiguration|None: the service configuration loaded from the file """ path = path or os.environ.get("OCTUE_SERVICE_CONFIGURATION_PATH", DEFAULT_SERVICE_CONFIGURATION_PATH) From cdf1b1aeddaad15d8856968690d307c4ee579df3 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Mon, 10 Mar 2025 16:57:57 +0000 Subject: [PATCH 198/216] DOC: Fix list in docs skipci --- docs/source/asking_questions.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/asking_questions.rst b/docs/source/asking_questions.rst index c88ff9305..1f579c4be 100644 --- a/docs/source/asking_questions.rst +++ b/docs/source/asking_questions.rst @@ -109,6 +109,7 @@ access the event store and run: **Options** + - ``table_id`` - If you're not using the standard deployment, you can specify a different table here - ``question_uuid`` - Retrieve events from this specific question - ``parent_question_uuid`` - Retrieve events from questions triggered by the same parent question (this doesn't include the parent question's events) From 856109117dc116ac25b552545397d3c3ef163441 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Mon, 10 Mar 2025 17:10:02 +0000 Subject: [PATCH 199/216] DOC: Update example service links skipci --- docs/source/testing_services.rst | 2 +- docs/source/updating_services.rst | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/source/testing_services.rst b/docs/source/testing_services.rst index 491067cc8..6c43f3b21 100644 --- a/docs/source/testing_services.rst +++ b/docs/source/testing_services.rst @@ -4,7 +4,7 @@ Testing services ================ We recommend writing automated tests for your service so anyone who wants to use it can have confidence in its quality -and reliability at a glance. `Here's an example test <https://github.com/octue/example-service-cloud-run/blob/main/tests/test_app.py>`_ +and reliability at a glance. `Here's an example test <https://github.com/octue/example-service-kueue/blob/main/tests/test_app.py>`_ for our example service. diff --git a/docs/source/updating_services.rst b/docs/source/updating_services.rst index 443e41fe3..ead5eebdb 100644 --- a/docs/source/updating_services.rst +++ b/docs/source/updating_services.rst @@ -9,11 +9,11 @@ service revision. We assume that: - Your service's repository is on GitHub and you have push access to it -- The `standard Twined deployment GitHub Actions workflow <https://github.com/octue/workflows/blob/main/.github/workflows/build-twined-service.yml>`_ +- The `standard Twined service deployment GitHub Actions workflow <https://github.com/octue/workflows/blob/main/.github/workflows/build-twined-service.yml>`_ is set up in the repository and being used to build and push the service image to the artifact registry on merge of a - pull request into the ``main`` branch (see an example `here <https://github.com/octue/example-service-kueue/blob/main/.github/workflows/cd.yml>`_) + pull request into the ``main`` branch (see an example `here <https://github.com/octue/example-service-kueue/blob/main/.github/workflows/release.yml>`_) - A release workflow is set up that will tag and release the new service revision on GitHub (see an example - `here <https://github.com/octue/example-service-cloud-run/blob/main/.github/workflows/release.yml>`_) + `here <https://github.com/octue/example-service-kueue/blob/main/.github/workflows/release.yml>`_) Instructions ------------- From 2f45dead1d678a408ab7f33b55a34127f760e877 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Mon, 10 Mar 2025 17:27:31 +0000 Subject: [PATCH 200/216] DOC: Improve "deploying services" documentation skipci --- docs/source/deploying_services.rst | 40 ++++++++++++++++-------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/docs/source/deploying_services.rst b/docs/source/deploying_services.rst index 44139ff93..4c1cd5531 100644 --- a/docs/source/deploying_services.rst +++ b/docs/source/deploying_services.rst @@ -16,6 +16,9 @@ Deploying a Twined service means the service: * Will automatically spin down after it has finished answering a question * Will automatically build and redeploy after a relevant code change (e.g. on push or merge into ``main``) +The main part of the deployment process is deploying the service network infrastructure. Once this is done, services can +be easily added as necessary. + Prerequisites ============= Twined services are currently deployable to Google Cloud Platform (GCP). You must have "owner" level access to the GCP @@ -23,40 +26,39 @@ project you're deploying to and billing must be set up for it. Deploying step-by-step ====================== -The main part of deployment is deploying the service network infrastructure. Once this is done, services can be easily -added as necessary. - There are three steps to a deployment: 1. Deploy the core infrastructure (e.g. storage bucket, event store, service accounts and roles) 2. Deploy the Kubernetes cluster and partner cloud functions 3. Build and push service docker images to the artifact registry -Deploy core infrastructure --------------------------- +1. Deploy core infrastructure +----------------------------- -- Deploy the ``terraform-octue-twined-core`` Terraform module +- Follow `the instructions <https://github.com/octue/terraform-octue-twined-core>`_ to deploy the resources in the + ``terraform-octue-twined-core`` Terraform module - This only needs to be done once per service network -- Follow the instructions `here <https://github.com/octue/terraform-octue-twined-core>`_ -Deploy Kubernetes cluster -------------------------- +2. Deploy Kubernetes cluster +---------------------------- -- Deploy the ``terraform-octue-twined-cluster`` Terraform module +- Follow the `instructions <https://github.com/octue/terraform-octue-twined-cluster>`_ to deploy the resources in the + ``terraform-octue-twined-cluster`` Terraform module - This only needs to be done once per service network -- Follow the instructions `here <https://github.com/octue/terraform-octue-twined-cluster>`_ -Build and push service docker images ------------------------------------- +3. Build and push service docker images +--------------------------------------- Your service is available if its docker image is in the service network's artifact registry repository. We recommend -pushing a new image for each merge into the ``main`` branch, corresponding to a new service revision. +pushing a new image for each merge into the ``main`` branch, corresponding to a new service revision. This can be done +automatically: -- Add the `build-twined-service <https://github.com/octue/workflows/blob/main/.github/workflows/build-twined-service.yml>`_ - GitHub Actions workflow to your service's GitHub repository -- This needs to be done for every service you want to deploy -- Follow the instructions `here <https://github.com/octue/workflows#deploying-a-kuberneteskueue-octue-twined-service-revision>`_ +- Follow the `instructions <https://github.com/octue/workflows#deploying-a-kuberneteskueue-octue-twined-service-revision>`_ + to add the `build-twined-service <https://github.com/octue/workflows/blob/main/.github/workflows/build-twined-service.yml>`_ + GitHub Actions workflow to your service's GitHub repository. Set its trigger to merge or push to ``main`` (see example + below) +- This needs to be done **once for every service** you want to deploy - A live example can be `found here <https://github.com/octue/example-service-kueue/blob/main/.github/workflows/release.yml>`_ - including automated pre-deployment testing and release of the code on GitHub + including automated pre-deployment testing and creation of a GitHub release What next? ========== From a32d27271303c94e329d27fc342366fd7f0e7d6f Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Thu, 13 Mar 2025 12:19:20 +0000 Subject: [PATCH 201/216] DOC: Split deployment docs into service and network deployment --- docs/source/deploying_services.rst | 63 +++++++++++++++--------------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/docs/source/deploying_services.rst b/docs/source/deploying_services.rst index 4c1cd5531..bbf913d02 100644 --- a/docs/source/deploying_services.rst +++ b/docs/source/deploying_services.rst @@ -6,8 +6,8 @@ Deploying services (developer's guide) This is a guide for developers that want to deploy Twined services themselves - it is not needed if Octue manages your services for you or if you are only asking questions to existing Twined services. -What deployment enables -======================= +What is deployment? +=================== Deploying a Twined service means the service: * Is a docker image that is spun up and down in a Kubernetes cluster on demand @@ -16,51 +16,52 @@ Deploying a Twined service means the service: * Will automatically spin down after it has finished answering a question * Will automatically build and redeploy after a relevant code change (e.g. on push or merge into ``main``) -The main part of the deployment process is deploying the service network infrastructure. Once this is done, services can -be easily added as necessary. +We can split deployment into service deployment and infrastructure deployment. + +Deploying a service +=================== +Assuming the service network infrastructure already exists, a service can be deployed by building and pushing its docker +image to the service network's Artifact Registry repository. We recommend pushing a new image for each release of the +code e.g. on merge into the ``main`` branch. Each new image is the deployment of a new service revision. This can be +done automatically: + +- Follow the `instructions <https://github.com/octue/workflows#deploying-a-kuberneteskueue-octue-twined-service-revision>`_ + to add the `build-twined-service <https://github.com/octue/workflows/blob/main/.github/workflows/build-twined-service.yml>`_ + GitHub Actions workflow to your service's GitHub repository. Set its trigger to merge or push to ``main`` (see example + below) +- This needs to be done **once for every service** you want to deploy +- A live example can be `found here <https://github.com/octue/example-service-kueue/blob/main/.github/workflows/release.yml>`_ + including automated pre-deployment testing and creation of a GitHub release + +You can now :doc:`ask your service some questions <asking_questions>`! It will be available in the service network as +``<namespace>/<name>:<version>`` (e.g. ``octue/example-service-kueue:0.1.1``). + + +Deploying the infrastructure +============================ Prerequisites -============= +------------- Twined services are currently deployable to Google Cloud Platform (GCP). You must have "owner" level access to the GCP project you're deploying to and billing must be set up for it. Deploying step-by-step -====================== -There are three steps to a deployment: +---------------------- +There are two steps to deploying the infrastructure: -1. Deploy the core infrastructure (e.g. storage bucket, event store, service accounts and roles) -2. Deploy the Kubernetes cluster and partner cloud functions -3. Build and push service docker images to the artifact registry +1. Deploy the core infrastructure (storage bucket, event store, IAM service accounts and roles) +2. Deploy the Kubernetes cluster, event handler, service registry, and Pub/Sub topic 1. Deploy core infrastructure ------------------------------ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Follow `the instructions <https://github.com/octue/terraform-octue-twined-core>`_ to deploy the resources in the ``terraform-octue-twined-core`` Terraform module - This only needs to be done once per service network 2. Deploy Kubernetes cluster ----------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Follow the `instructions <https://github.com/octue/terraform-octue-twined-cluster>`_ to deploy the resources in the ``terraform-octue-twined-cluster`` Terraform module - This only needs to be done once per service network - -3. Build and push service docker images ---------------------------------------- -Your service is available if its docker image is in the service network's artifact registry repository. We recommend -pushing a new image for each merge into the ``main`` branch, corresponding to a new service revision. This can be done -automatically: - -- Follow the `instructions <https://github.com/octue/workflows#deploying-a-kuberneteskueue-octue-twined-service-revision>`_ - to add the `build-twined-service <https://github.com/octue/workflows/blob/main/.github/workflows/build-twined-service.yml>`_ - GitHub Actions workflow to your service's GitHub repository. Set its trigger to merge or push to ``main`` (see example - below) -- This needs to be done **once for every service** you want to deploy -- A live example can be `found here <https://github.com/octue/example-service-kueue/blob/main/.github/workflows/release.yml>`_ - including automated pre-deployment testing and creation of a GitHub release - -What next? -========== -:doc:`Ask your service some questions <asking_questions>`! It will be available in the service network as -``<namespace>/<name>:<version>`` (e.g. ``octue/example-service-kueue:0.1.1``). From 26fe59289d17ab3b43cbc32faafcff151d7f3d2d Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Thu, 13 Mar 2025 12:20:47 +0000 Subject: [PATCH 202/216] DOC: Update scope of existing services mentioned in docs --- docs/source/services.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/services.rst b/docs/source/services.rst index 08d8011f4..54fecd3d2 100644 --- a/docs/source/services.rst +++ b/docs/source/services.rst @@ -4,8 +4,8 @@ Octue Twined services ===================== -There's a growing range of live :ref:`services <service_definition>` in the Octue ecosystem that you can query. -Currently, all of them are related to wind energy. Here's a quick glossary of terms before we tell you more: +There's a growing range of live :ref:`services <service_definition>` in the Octue ecosystem that you can query, mostly +related to wind energy and other renewables. Here's a quick glossary of terms before we tell you more: .. admonition:: Definitions From 71b6a072de342668e4efc2e84230894f0b1e04e4 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Thu, 13 Mar 2025 12:27:13 +0000 Subject: [PATCH 203/216] REF: Move service registry code into `octue.cloud.registry` module --- octue/cloud/pub_sub/service.py | 3 +- octue/cloud/registry.py | 95 ++++++++++++++++++++++++ octue/cloud/service_id.py | 92 ----------------------- tests/cloud/test_registry.py | 130 +++++++++++++++++++++++++++++++++ tests/cloud/test_service_id.py | 128 -------------------------------- 5 files changed, 226 insertions(+), 222 deletions(-) create mode 100644 octue/cloud/registry.py create mode 100644 tests/cloud/test_registry.py diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index a4f029ef8..688068391 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -17,11 +17,10 @@ from octue.cloud.pub_sub import Subscription, Topic from octue.cloud.pub_sub.events import GoogleCloudPubSubEventHandler, extract_event from octue.cloud.pub_sub.logging import GoogleCloudPubSubHandler +from octue.cloud.registry import get_default_sruid, raise_if_revision_not_registered from octue.cloud.service_id import ( convert_service_id_to_pub_sub_form, create_sruid, - get_default_sruid, - raise_if_revision_not_registered, split_service_id, validate_sruid, ) diff --git a/octue/cloud/registry.py b/octue/cloud/registry.py new file mode 100644 index 000000000..9f3d5b00a --- /dev/null +++ b/octue/cloud/registry.py @@ -0,0 +1,95 @@ +import google.auth +import google.oauth2 +import requests + +from octue.cloud.service_id import create_sruid, logger, split_service_id +import octue.exceptions + + +def get_default_sruid(namespace, name, service_registries): + """Get the SRUID of the default revision of the service `<namespace>/<name>` if it exists in one of the specified + service registries. The registries should be provided in priority order so that, if more than one registry contains + a matching service, the revision that's returned is taken from the highest priority (first) registry. + + :param str namespace: the namespace of the service + :param str name: the name of the service + :param iter(dict) service_registries: the registries to look for the service in; the registries should be in priority order in case more than one has a service with the given namespace and name + :raise octue.exceptions.ServiceNotFound: if a revision can't be found for the service in the service registries + :return str: the SRUID of the default revision of the service + """ + service_id = f"{namespace}/{name}" + + for registry in service_registries: + response = _make_service_registry_request(registry, namespace, name) + + if response.status_code == 200: + revision_tag = response.json()["revision_tag"] + + logger.info( + "Found default service revision '%s:%s' in %r registry.", + service_id, + revision_tag, + registry["name"], + ) + + return create_sruid(namespace=namespace, name=name, revision_tag=revision_tag) + + raise octue.exceptions.ServiceNotFound( + f"No revisions for the service {service_id!r} were found in any of the specified service registries: " + f"{service_registries!r}" + ) + + +def raise_if_revision_not_registered(sruid, service_registries): + """Raise an error if the service revision isn't registered in the given service registries. + + :param str sruid: the SRUID of the service revision + :param iter(dict) service_registries: the registries to look for the service revision in + :raise octue.exceptions.ServiceNotFound: if the service revision isn't registered in any of the service registries + :return None: + """ + namespace, name, revision_tag = split_service_id(sruid, require_revision_tag=True) + + for registry in service_registries: + response = _make_service_registry_request(registry, namespace, name, revision_tag) + + if response.status_code == 200: + logger.info("Found service revision %r in %r registry.", sruid, registry["name"]) + return + + raise octue.exceptions.ServiceNotFound( + f"Service revision {sruid!r} was not found in any of the specified service registries: {service_registries!r}" + ) + + +def _make_service_registry_request(registry, namespace, name, revision_tag=None): + """Make an authenticated request to a service registry about a service. + + :param dict registry: a dictionary with the keys "endpoint" and "name" + :param str namespace: the namespace of the service + :param str name: the name of the service + :param str|None revision_tag: the revision tag for a revision of the service + :raise requests.exceptions.HTTPError: if the request fails with a status code other than 404 + :return requests.Response: the response from the service registry + """ + id_token = _get_google_cloud_id_token(registry) + + response = requests.get( + f"{registry['endpoint']}/{namespace}/{name}", + params={"revision_tag": revision_tag}, + headers={"Authorization": f"Bearer {id_token}"}, + ) + + if response.status_code != 404: + response.raise_for_status() + + return response + + +def _get_google_cloud_id_token(registry): + """Get an ID token for Google Cloud. + + :param dict registry: a dictionary with the keys "endpoint" and "name" + :return str: an ID token for Google Cloud + """ + return google.oauth2.id_token.fetch_id_token(google.auth.transport.requests.Request(), registry["endpoint"]) diff --git a/octue/cloud/service_id.py b/octue/cloud/service_id.py index 45ea32d71..967940b75 100644 --- a/octue/cloud/service_id.py +++ b/octue/cloud/service_id.py @@ -4,9 +4,6 @@ import uuid import coolname -import google.auth.transport.requests -import google.oauth2.id_token -import requests import octue.exceptions @@ -244,92 +241,3 @@ def split_service_id(service_id, require_revision_tag=False): validate_sruid(namespace=namespace, name=name, revision_tag=revision_tag) return namespace, name, revision_tag - - -def get_default_sruid(namespace, name, service_registries): - """Get the SRUID of the default revision of the service `<namespace>/<name>` if it exists in one of the specified - service registries. The registries should be provided in priority order so that, if more than one registry contains - a matching service, the revision that's returned is taken from the highest priority (first) registry. - - :param str namespace: the namespace of the service - :param str name: the name of the service - :param iter(dict) service_registries: the registries to look for the service in; the registries should be in priority order in case more than one has a service with the given namespace and name - :raise octue.exceptions.ServiceNotFound: if a revision can't be found for the service in the service registries - :return str: the SRUID of the default revision of the service - """ - service_id = f"{namespace}/{name}" - - for registry in service_registries: - response = _make_service_registry_request(registry, namespace, name) - - if response.status_code == 200: - revision_tag = response.json()["revision_tag"] - - logger.info( - "Found default service revision '%s:%s' in %r registry.", - service_id, - revision_tag, - registry["name"], - ) - - return create_sruid(namespace=namespace, name=name, revision_tag=revision_tag) - - raise octue.exceptions.ServiceNotFound( - f"No revisions for the service {service_id!r} were found in any of the specified service registries: " - f"{service_registries!r}" - ) - - -def raise_if_revision_not_registered(sruid, service_registries): - """Raise an error if the service revision isn't registered in the given service registries. - - :param str sruid: the SRUID of the service revision - :param iter(dict) service_registries: the registries to look for the service revision in - :raise octue.exceptions.ServiceNotFound: if the service revision isn't registered in any of the service registries - :return None: - """ - namespace, name, revision_tag = split_service_id(sruid, require_revision_tag=True) - - for registry in service_registries: - response = _make_service_registry_request(registry, namespace, name, revision_tag) - - if response.status_code == 200: - logger.info("Found service revision %r in %r registry.", sruid, registry["name"]) - return - - raise octue.exceptions.ServiceNotFound( - f"Service revision {sruid!r} was not found in any of the specified service registries: {service_registries!r}" - ) - - -def _make_service_registry_request(registry, namespace, name, revision_tag=None): - """Make an authenticated request to a service registry about a service. - - :param dict registry: a dictionary with the keys "endpoint" and "name" - :param str namespace: the namespace of the service - :param str name: the name of the service - :param str|None revision_tag: the revision tag for a revision of the service - :raise requests.exceptions.HTTPError: if the request fails with a status code other than 404 - :return requests.Response: the response from the service registry - """ - id_token = _get_google_cloud_id_token(registry) - - response = requests.get( - f"{registry['endpoint']}/{namespace}/{name}", - params={"revision_tag": revision_tag}, - headers={"Authorization": f"Bearer {id_token}"}, - ) - - if response.status_code != 404: - response.raise_for_status() - - return response - - -def _get_google_cloud_id_token(registry): - """Get an ID token for Google Cloud. - - :param dict registry: a dictionary with the keys "endpoint" and "name" - :return str: an ID token for Google Cloud - """ - return google.oauth2.id_token.fetch_id_token(google.auth.transport.requests.Request(), registry["endpoint"]) diff --git a/tests/cloud/test_registry.py b/tests/cloud/test_registry.py new file mode 100644 index 000000000..8e08989fd --- /dev/null +++ b/tests/cloud/test_registry.py @@ -0,0 +1,130 @@ +import json +import unittest +from unittest.mock import patch + +import requests + +from octue.cloud.registry import get_default_sruid, raise_if_revision_not_registered +import octue.exceptions + + +class TestGetDefaultSRUID(unittest.TestCase): + SERVICE_REGISTRIES = [{"name": "Octue Registry", "endpoint": "https://blah.com/services"}] + + @classmethod + def setUpClass(cls): + cls.id_token_patch = patch("octue.cloud.registry._get_google_cloud_id_token", return_value="some-token") + cls.id_token_patch.start() + + @classmethod + def tearDownClass(cls): + cls.id_token_patch.stop() + + def test_error_raised_if_request_fails(self): + """Test that an error is raised if the request to the service registry fails.""" + mock_response = requests.Response() + mock_response.status_code = 403 + + with patch("requests.get", return_value=mock_response): + with self.assertRaises(requests.HTTPError): + get_default_sruid( + namespace="my-org", + name="my-service", + service_registries=self.SERVICE_REGISTRIES, + ) + + def test_error_raised_if_revision_not_found(self): + """Test that an error is raised if no revision is found for the service in the given registries.""" + mock_response = requests.Response() + mock_response.status_code = 404 + + with patch("requests.get", return_value=mock_response): + with self.assertRaises(octue.exceptions.ServiceNotFound): + get_default_sruid( + namespace="my-org", + name="my-service", + service_registries=self.SERVICE_REGISTRIES, + ) + + def test_get_latest_sruid(self): + """Test that the latest SRUID for a service can be found.""" + mock_response = requests.Response() + mock_response.status_code = 200 + mock_response._content = json.dumps({"revision_tag": "1.3.9"}).encode() + + with patch("requests.get", return_value=mock_response): + latest_sruid = get_default_sruid( + namespace="my-org", + name="my-service", + service_registries=self.SERVICE_REGISTRIES, + ) + + self.assertEqual(latest_sruid, "my-org/my-service:1.3.9") + + def test_get_latest_sruid_when_not_in_first_registry(self): + """Test that the latest SRUID for a service can be found when the service isn't in the first registry.""" + mock_failure_response = requests.Response() + mock_failure_response.status_code = 404 + + mock_success_response = requests.Response() + mock_success_response.status_code = 200 + mock_success_response._content = json.dumps({"revision_tag": "1.3.9"}).encode() + + with patch("requests.get", side_effect=[mock_failure_response, mock_success_response]): + latest_sruid = get_default_sruid( + namespace="my-org", + name="my-service", + service_registries=self.SERVICE_REGISTRIES + + [{"name": "Another Registry", "endpoint": "cats.com/services"}], + ) + + self.assertEqual(latest_sruid, "my-org/my-service:1.3.9") + + +class TestRaiseIfRevisionNotRegistered(unittest.TestCase): + SERVICE_REGISTRIES = [{"name": "Octue Registry", "endpoint": "https://blah.com/services"}] + + @classmethod + def setUpClass(cls): + cls.id_token_patch = patch("octue.cloud.registry._get_google_cloud_id_token", return_value="some-token") + cls.id_token_patch.start() + + @classmethod + def tearDownClass(cls): + cls.id_token_patch.stop() + + def test_error_raised_if_request_fails(self): + """Test that an error is raised if the request to the service registry fails.""" + mock_response = requests.Response() + mock_response.status_code = 403 + + with patch("requests.get", return_value=mock_response): + with self.assertRaises(requests.HTTPError): + raise_if_revision_not_registered( + sruid="my-org/my-service:1.0.0", + service_registries=self.SERVICE_REGISTRIES, + ) + + def test_error_raised_if_revision_not_found(self): + """Test that an error is raised if no revision is found for the service in the given registries.""" + mock_response = requests.Response() + mock_response.status_code = 404 + + with patch("requests.get", return_value=mock_response): + with self.assertRaises(octue.exceptions.ServiceNotFound): + raise_if_revision_not_registered( + sruid="my-org/my-service:1.0.0", + service_registries=self.SERVICE_REGISTRIES, + ) + + def test_no_error_raised_if_service_revision_registered(self): + """Test that no error is raised if a revision is found for the service in the given registries.""" + mock_response = requests.Response() + mock_response.status_code = 200 + mock_response._content = json.dumps({"revision_tag": "1.0.0"}).encode() + + with patch("requests.get", return_value=mock_response): + raise_if_revision_not_registered( + sruid="my-org/my-service:1.0.0", + service_registries=self.SERVICE_REGISTRIES, + ) diff --git a/tests/cloud/test_service_id.py b/tests/cloud/test_service_id.py index dcf95654b..95d4097d8 100644 --- a/tests/cloud/test_service_id.py +++ b/tests/cloud/test_service_id.py @@ -1,24 +1,18 @@ -import json import logging import os import unittest from unittest.mock import patch -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, - raise_if_revision_not_registered, split_service_id, validate_sruid, ) from octue.configuration import ServiceConfiguration -import octue.exceptions from octue.exceptions import InvalidServiceID from tests import MOCK_SERVICE_REVISION_TAG @@ -230,125 +224,3 @@ def test_split_service_id(self): self.assertEqual(namespace, "octue") self.assertEqual(name, "my-service") self.assertIsNone(revision_tag) - - -class TestGetDefaultSRUID(unittest.TestCase): - SERVICE_REGISTRIES = [{"name": "Octue Registry", "endpoint": "https://blah.com/services"}] - - @classmethod - def setUpClass(cls): - cls.id_token_patch = patch("octue.cloud.service_id._get_google_cloud_id_token", return_value="some-token") - cls.id_token_patch.start() - - @classmethod - def tearDownClass(cls): - cls.id_token_patch.stop() - - def test_error_raised_if_request_fails(self): - """Test that an error is raised if the request to the service registry fails.""" - mock_response = requests.Response() - mock_response.status_code = 403 - - with patch("requests.get", return_value=mock_response): - with self.assertRaises(requests.HTTPError): - get_default_sruid( - namespace="my-org", - name="my-service", - service_registries=self.SERVICE_REGISTRIES, - ) - - def test_error_raised_if_revision_not_found(self): - """Test that an error is raised if no revision is found for the service in the given registries.""" - mock_response = requests.Response() - mock_response.status_code = 404 - - with patch("requests.get", return_value=mock_response): - with self.assertRaises(octue.exceptions.ServiceNotFound): - get_default_sruid( - namespace="my-org", - name="my-service", - service_registries=self.SERVICE_REGISTRIES, - ) - - def test_get_latest_sruid(self): - """Test that the latest SRUID for a service can be found.""" - mock_response = requests.Response() - mock_response.status_code = 200 - mock_response._content = json.dumps({"revision_tag": "1.3.9"}).encode() - - with patch("requests.get", return_value=mock_response): - latest_sruid = get_default_sruid( - namespace="my-org", - name="my-service", - service_registries=self.SERVICE_REGISTRIES, - ) - - self.assertEqual(latest_sruid, "my-org/my-service:1.3.9") - - def test_get_latest_sruid_when_not_in_first_registry(self): - """Test that the latest SRUID for a service can be found when the service isn't in the first registry.""" - mock_failure_response = requests.Response() - mock_failure_response.status_code = 404 - - mock_success_response = requests.Response() - mock_success_response.status_code = 200 - mock_success_response._content = json.dumps({"revision_tag": "1.3.9"}).encode() - - with patch("requests.get", side_effect=[mock_failure_response, mock_success_response]): - latest_sruid = get_default_sruid( - namespace="my-org", - name="my-service", - service_registries=self.SERVICE_REGISTRIES - + [{"name": "Another Registry", "endpoint": "cats.com/services"}], - ) - - self.assertEqual(latest_sruid, "my-org/my-service:1.3.9") - - -class TestRaiseIfRevisionNotRegistered(unittest.TestCase): - SERVICE_REGISTRIES = [{"name": "Octue Registry", "endpoint": "https://blah.com/services"}] - - @classmethod - def setUpClass(cls): - cls.id_token_patch = patch("octue.cloud.service_id._get_google_cloud_id_token", return_value="some-token") - cls.id_token_patch.start() - - @classmethod - def tearDownClass(cls): - cls.id_token_patch.stop() - - def test_error_raised_if_request_fails(self): - """Test that an error is raised if the request to the service registry fails.""" - mock_response = requests.Response() - mock_response.status_code = 403 - - with patch("requests.get", return_value=mock_response): - with self.assertRaises(requests.HTTPError): - raise_if_revision_not_registered( - sruid="my-org/my-service:1.0.0", - service_registries=self.SERVICE_REGISTRIES, - ) - - def test_error_raised_if_revision_not_found(self): - """Test that an error is raised if no revision is found for the service in the given registries.""" - mock_response = requests.Response() - mock_response.status_code = 404 - - with patch("requests.get", return_value=mock_response): - with self.assertRaises(octue.exceptions.ServiceNotFound): - raise_if_revision_not_registered( - sruid="my-org/my-service:1.0.0", - service_registries=self.SERVICE_REGISTRIES, - ) - - def test_no_error_raised_if_service_revision_registered(self): - """Test that no error is raised if a revision is found for the service in the given registries.""" - mock_response = requests.Response() - mock_response.status_code = 200 - mock_response._content = json.dumps({"revision_tag": "1.0.0"}).encode() - - with patch("requests.get", return_value=mock_response): - raise_if_revision_not_registered( - sruid="my-org/my-service:1.0.0", - service_registries=self.SERVICE_REGISTRIES, - ) From 398129d01aa9b01540746139cdffe431f869837d Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Thu, 13 Mar 2025 12:35:13 +0000 Subject: [PATCH 204/216] REF: Rename `EventAttributes.refresh` --- octue/cloud/events/attributes.py | 2 +- octue/cloud/pub_sub/service.py | 2 +- tests/cloud/events/test_attributes.py | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/octue/cloud/events/attributes.py b/octue/cloud/events/attributes.py index 9633ad5c7..8f2a7df9c 100644 --- a/octue/cloud/events/attributes.py +++ b/octue/cloud/events/attributes.py @@ -73,7 +73,7 @@ def __init__( self.memory = memory self.ephemeral_storage = ephemeral_storage - def refresh(self): + def reset_uuid_and_datetime(self): """Set a new UUID and datetime. This avoids having to create a new instance for every single event (for which all other attributes are the same). diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index 688068391..23b3dfe3a 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -490,7 +490,7 @@ def _emit_event(self, event, attributes, wait=True, timeout=30): :param int|float timeout: the timeout for sending the event in seconds :return google.cloud.pubsub_v1.publisher.futures.Future: """ - attributes.refresh() + attributes.reset_uuid_and_datetime() future = self.publisher.publish( topic=self.services_topic.path, diff --git a/tests/cloud/events/test_attributes.py b/tests/cloud/events/test_attributes.py index 9774dc240..6196c8ac8 100644 --- a/tests/cloud/events/test_attributes.py +++ b/tests/cloud/events/test_attributes.py @@ -43,8 +43,8 @@ def test_defaults(self): }, ) - def test_refresh(self): - """Test that refreshing the attributes changes the UUID and datetime.""" + def test_reset_uuid_and_datetime(self): + """Test that the `reset_uuid_and_datetime` method changes the UUID and datetime.""" attributes = EventAttributes( sender=SENDER, sender_type=SENDER_TYPE, @@ -55,7 +55,7 @@ def test_refresh(self): original_uuid = attributes.uuid original_datetime = attributes.datetime - attributes.refresh() + attributes.reset_uuid_and_datetime() self.assertNotEqual(attributes.uuid, original_uuid) self.assertNotEqual(attributes.datetime, original_datetime) From c802c4116429f2520a1ade58984dbb96b4ce4595 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Thu, 13 Mar 2025 12:45:00 +0000 Subject: [PATCH 205/216] REF: Simplify `EventAttributes.make_opposite_attributes` skipci --- octue/cloud/events/attributes.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/octue/cloud/events/attributes.py b/octue/cloud/events/attributes.py index 8f2a7df9c..4417e9d52 100644 --- a/octue/cloud/events/attributes.py +++ b/octue/cloud/events/attributes.py @@ -1,3 +1,4 @@ +import copy import datetime as dt import uuid as uuid_library @@ -89,17 +90,17 @@ def make_opposite_attributes(self): :return octue.cloud.events.attributes.EventAttributes: the event attributes for an event with the opposite sender type """ - attributes = self.to_minimal_dict() - attributes["sender"] = self.recipient - attributes["recipient"] = self.sender - attributes["sender_type"] = SENDER_TYPE_OPPOSITES[self.sender_type] - attributes["sender_sdk_version"] = LOCAL_SDK_VERSION + attributes = copy.copy(self) + attributes.sender = self.recipient + attributes.recipient = self.sender + attributes.sender_type = SENDER_TYPE_OPPOSITES[self.sender_type] + attributes.sender_sdk_version = LOCAL_SDK_VERSION + # Response attributes don't have these attributes set. for attr in ("forward_logs", "save_diagnostics", "cpus", "memory", "ephemeral_storage"): - if attr in attributes: - del attributes[attr] + setattr(self, attr, None) - return EventAttributes(**attributes) + return attributes def to_minimal_dict(self): """Convert the attributes to a minimal dictionary containing only the attributes that have a non-`None` value. From 10f044856deb03b855f2cd147bf5cd15031cb07a Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Thu, 13 Mar 2025 12:49:29 +0000 Subject: [PATCH 206/216] ENH: Remove overkill service exception handling --- octue/cloud/events/answer_question.py | 35 ++++++++------------------- 1 file changed, 10 insertions(+), 25 deletions(-) diff --git a/octue/cloud/events/answer_question.py b/octue/cloud/events/answer_question.py index 18ea83630..5be81ddbf 100644 --- a/octue/cloud/events/answer_question.py +++ b/octue/cloud/events/answer_question.py @@ -4,7 +4,6 @@ from octue.cloud.service_id import create_sruid, get_sruid_parts from octue.resources.service_backends import GCPPubSubBackend from octue.runner import Runner -from octue.utils.objects import get_nested_attribute logger = logging.getLogger(__name__) @@ -27,27 +26,13 @@ def answer_question(question, project_name, service_configuration, app_configura ) service = Service(service_id=service_sruid, backend=GCPPubSubBackend(project_name=project_name)) - question_uuid = get_nested_attribute(question, "attributes.question_uuid") - - try: - runner = Runner.from_configuration( - service_configuration=service_configuration, - app_configuration=app_configuration, - project_name=project_name, - service_id=service_sruid, - ) - - service.run_function = runner.run - return service.answer(question) - - except BaseException as error: # noqa - service.send_exception( - question_uuid=question_uuid, - parent_question_uuid=get_nested_attribute(question, "attributes.parent_question_uuid"), - originator_question_uuid=get_nested_attribute(question, "attributes.originator_question_uuid"), - parent=get_nested_attribute(question, "attributes.parent"), - originator=get_nested_attribute(question, "attributes.originator"), - retry_count=get_nested_attribute(question, "attributes.retry_count"), - ) - - logger.exception(error) + + runner = Runner.from_configuration( + service_configuration=service_configuration, + app_configuration=app_configuration, + project_name=project_name, + service_id=service_sruid, + ) + + service.run_function = runner.run + return service.answer(question) From be41e9d45318395fde4cd9e75d37b2947d57aebe Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Thu, 13 Mar 2025 13:29:33 +0000 Subject: [PATCH 207/216] REF: Split `EventAttributes` into question and response attributes --- octue/cloud/emulators/_pub_sub.py | 4 +- octue/cloud/events/attributes.py | 165 +++++++++++++++++++------- octue/cloud/events/utils.py | 4 +- octue/cloud/pub_sub/service.py | 8 +- tests/cloud/events/test_attributes.py | 162 ++++++++++++++++++++----- tests/cloud/pub_sub/test_events.py | 4 +- tests/cloud/pub_sub/test_logging.py | 4 +- tests/cloud/pub_sub/test_service.py | 4 +- 8 files changed, 267 insertions(+), 88 deletions(-) diff --git a/octue/cloud/emulators/_pub_sub.py b/octue/cloud/emulators/_pub_sub.py index b91595128..eb148d9a2 100644 --- a/octue/cloud/emulators/_pub_sub.py +++ b/octue/cloud/emulators/_pub_sub.py @@ -4,7 +4,7 @@ import google.api_core -from octue.cloud.events.attributes import EventAttributes +from octue.cloud.events.attributes import QuestionAttributes 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 @@ -406,7 +406,7 @@ def ask( # If the originator isn't provided, assume that this service revision is the originator. originator = originator or self.id - attributes = EventAttributes( + attributes = QuestionAttributes( question_uuid=question_uuid, parent_question_uuid=parent_question_uuid, originator_question_uuid=originator_question_uuid, diff --git a/octue/cloud/events/attributes.py b/octue/cloud/events/attributes.py index 4417e9d52..50cbbf905 100644 --- a/octue/cloud/events/attributes.py +++ b/octue/cloud/events/attributes.py @@ -1,12 +1,9 @@ -import copy import datetime as dt import uuid as uuid_library from octue.definitions import LOCAL_SDK_VERSION from octue.utils.dictionaries import make_minimal_dictionary -SENDER_TYPE_OPPOSITES = {"CHILD": "PARENT", "PARENT": "CHILD"} - class EventAttributes: """A data structure for holding and working with attributes for a single Octue Twined event. If originator and @@ -25,11 +22,6 @@ class EventAttributes: :param str|None originator: the SRUID of the service revision that triggered all ancestor questions of this question; if `None`, the `sender` is used :param str sender_sdk_version: the semantic version of Octue SDK the sender is running; defaults to the version in the environment :param int retry_count: the retry count of the question this event is related to (this is zero if it's the first attempt at the question) - :param bool|None forward_logs: if this isn't a `question` event, this should be `None`; otherwise, it should be a boolean indicating whether the parent requested the child to forward its logs to it - :param str|None save_diagnostics: if this isn't a `question` event, this should be `None`; otherwise, it must be one of {"SAVE_DIAGNOSTICS_OFF", "SAVE_DIAGNOSTICS_ON_CRASH", "SAVE_DIAGNOSTICS_ON"} - :param int|None cpus: if this isn't a `question` event, this should be `None`; otherwise, it can be `None` or the number of CPUs requested for the question - :param str|None memory: if this isn't a `question` event, this should be `None`; otherwise, it can be `None` or the amount of memory requested for the question e.g. "256Mi" or "1Gi" - :param str|None ephemeral_storage: if this isn't a `question` event, this should be `None`; otherwise, it can be `None` or the amount of ephemeral storage requested for the question e.g. "256Mi" or "1Gi" :return None: """ @@ -47,13 +39,7 @@ def __init__( originator=None, sender_sdk_version=LOCAL_SDK_VERSION, retry_count=0, - forward_logs=None, - save_diagnostics=None, - cpus=None, - memory=None, - ephemeral_storage=None, ): - # Attributes for all event kinds. self.uuid = uuid or str(uuid_library.uuid4()) self.datetime = datetime or dt.datetime.now(tz=dt.timezone.utc) self.question_uuid = question_uuid or str(uuid_library.uuid4()) @@ -67,13 +53,6 @@ def __init__( self.recipient = recipient self.retry_count = retry_count - # Question event attributes. - self.forward_logs = forward_logs - self.save_diagnostics = save_diagnostics - self.cpus = cpus - self.memory = memory - self.ephemeral_storage = ephemeral_storage - def reset_uuid_and_datetime(self): """Set a new UUID and datetime. This avoids having to create a new instance for every single event (for which all other attributes are the same). @@ -83,25 +62,6 @@ def reset_uuid_and_datetime(self): self.uuid = str(uuid_library.uuid4()) self.datetime = dt.datetime.now(tz=dt.timezone.utc) - def make_opposite_attributes(self): - """Create the attributes for an event of the opposite sender type to this event (parent -> child or child - -> parent). For example, if these attributes are for a question event, create the attributes for a response - event such as a result or log record. - - :return octue.cloud.events.attributes.EventAttributes: the event attributes for an event with the opposite sender type - """ - attributes = copy.copy(self) - attributes.sender = self.recipient - attributes.recipient = self.sender - attributes.sender_type = SENDER_TYPE_OPPOSITES[self.sender_type] - attributes.sender_sdk_version = LOCAL_SDK_VERSION - - # Response attributes don't have these attributes set. - for attr in ("forward_logs", "save_diagnostics", "cpus", "memory", "ephemeral_storage"): - setattr(self, attr, None) - - return attributes - def to_minimal_dict(self): """Convert the attributes to a minimal dictionary containing only the attributes that have a non-`None` value. Using a minimal dictionary means the smallest possible data structure is used so `None` values don't, @@ -122,11 +82,6 @@ def to_minimal_dict(self): sender_sdk_version=self.sender_sdk_version, recipient=self.recipient, retry_count=self.retry_count, - forward_logs=self.forward_logs, - save_diagnostics=self.save_diagnostics, - cpus=self.cpus, - memory=self.memory, - ephemeral_storage=self.ephemeral_storage, ) def to_serialised_attributes(self): @@ -149,3 +104,123 @@ def to_serialised_attributes(self): serialised_attributes[key] = value return serialised_attributes + + +class QuestionAttributes(EventAttributes): + """A data structure for holding and working with attributes for a single question event. If originator and parent + information aren't provided, the attributes will correspond to an event of any kind related to an originator + question. + + :param str sender: the unique identifier (SRUID) of the service revision sending the question + :param str sender_type: the type of sender for this event; must be one of {"PARENT", "CHILD"} + :param str recipient: the SRUID of the service revision the question is for + :param str|None uuid: the UUID of the event; if `None`, a UUID is generated + :param datetime.datetime|None datetime: the datetime the event was created at; defaults to the current datetime in UTC + :param str|None question_uuid: the UUID of the question; if `None`, a UUID is generated + :param str|None parent_question_uuid: the UUID of the question that triggered this question; this should be `None` if this event relates to the first question in a question tree + :param str|None originator_question_uuid: the UUID of the question that triggered all ancestor questions of this question; if `None`, the event's related question is assumed to be the originator question and `question_uuid` is used + :param str|None parent: the SRUID of the service revision that asked the question this event is related to + :param str|None originator: the SRUID of the service revision that triggered all ancestor questions of this question; if `None`, the `sender` is used + :param str sender_sdk_version: the semantic version of Octue SDK the sender is running; defaults to the version in the environment + :param int retry_count: the retry count of the question this event is related to (this is zero if it's the first attempt at the question) + :param bool|None forward_logs: if this isn't a `question` event, this should be `None`; otherwise, it should be a boolean indicating whether the parent requested the child to forward its logs to it + :param str|None save_diagnostics: if this isn't a `question` event, this should be `None`; otherwise, it must be one of {"SAVE_DIAGNOSTICS_OFF", "SAVE_DIAGNOSTICS_ON_CRASH", "SAVE_DIAGNOSTICS_ON"} + :param int|None cpus: if this isn't a `question` event, this should be `None`; otherwise, it can be `None` or the number of CPUs requested for the question + :param str|None memory: if this isn't a `question` event, this should be `None`; otherwise, it can be `None` or the amount of memory requested for the question e.g. "256Mi" or "1Gi" + :param str|None ephemeral_storage: if this isn't a `question` event, this should be `None`; otherwise, it can be `None` or the amount of ephemeral storage requested for the question e.g. "256Mi" or "1Gi" + :return None: + """ + + def __init__( + self, + sender, + sender_type, + recipient, + uuid=None, + datetime=None, + question_uuid=None, + parent_question_uuid=None, + originator_question_uuid=None, + parent=None, + originator=None, + sender_sdk_version=LOCAL_SDK_VERSION, + retry_count=0, + forward_logs=None, + save_diagnostics=None, + cpus=None, + memory=None, + ephemeral_storage=None, + ): + super().__init__( + sender, + sender_type, + recipient, + uuid, + datetime, + question_uuid, + parent_question_uuid, + originator_question_uuid, + parent, + originator, + sender_sdk_version, + retry_count, + ) + + self.forward_logs = forward_logs + self.save_diagnostics = save_diagnostics + self.cpus = cpus + self.memory = memory + self.ephemeral_storage = ephemeral_storage + + def to_minimal_dict(self): + return { + **super().to_minimal_dict(), + **make_minimal_dictionary( + forward_logs=self.forward_logs, + save_diagnostics=self.save_diagnostics, + cpus=self.cpus, + memory=self.memory, + ephemeral_storage=self.ephemeral_storage, + ), + } + + +class ResponseAttributes(EventAttributes): + """A data structure for holding and working with attributes for a single response event of any kind. If originator + and parent information aren't provided, the attributes will correspond to an event of any kind related to an + originator question. + + :param str sender: the unique identifier (SRUID) of the service revision sending the question + :param str sender_type: the type of sender for this event; must be one of {"PARENT", "CHILD"} + :param str recipient: the SRUID of the service revision the question is for + :param str|None uuid: the UUID of the event; if `None`, a UUID is generated + :param datetime.datetime|None datetime: the datetime the event was created at; defaults to the current datetime in UTC + :param str|None question_uuid: the UUID of the question; if `None`, a UUID is generated + :param str|None parent_question_uuid: the UUID of the question that triggered this question; this should be `None` if this event relates to the first question in a question tree + :param str|None originator_question_uuid: the UUID of the question that triggered all ancestor questions of this question; if `None`, the event's related question is assumed to be the originator question and `question_uuid` is used + :param str|None parent: the SRUID of the service revision that asked the question this event is related to + :param str|None originator: the SRUID of the service revision that triggered all ancestor questions of this question; if `None`, the `sender` is used + :param str sender_sdk_version: the semantic version of Octue SDK the sender is running; defaults to the version in the environment + :param int retry_count: the retry count of the question this event is related to (this is zero if it's the first attempt at the question) + :return None: + """ + + @classmethod + def from_question_attributes(cls, question_attributes): + """Create corresponding response attributes from a set of question attributes. + + :param octue.cloud.events.attributes.QuestionEventAttributes question_attributes: the question attributes to make the response attributes from + :return octue.cloud.events.attributes.ResponseEventAttributes: the event attributes for a response event of any kind + """ + return cls( + sender_type="CHILD", + sender=question_attributes.recipient, + recipient=question_attributes.sender, + question_uuid=question_attributes.question_uuid, + parent_question_uuid=question_attributes.parent_question_uuid, + originator_question_uuid=question_attributes.originator_question_uuid, + parent=question_attributes.parent, + originator=question_attributes.originator, + sender_sdk_version=question_attributes.sender_sdk_version, + retry_count=question_attributes.retry_count, + ) diff --git a/octue/cloud/events/utils.py b/octue/cloud/events/utils.py index 0b3bc9997..892306dc0 100644 --- a/octue/cloud/events/utils.py +++ b/octue/cloud/events/utils.py @@ -1,4 +1,4 @@ -from octue.cloud.events.attributes import EventAttributes +from octue.cloud.events.attributes import QuestionAttributes from octue.utils.dictionaries import make_minimal_dictionary @@ -21,7 +21,7 @@ def make_question_event( :return dict: the question event and its attributes """ if not attributes: - attributes = EventAttributes( + attributes = QuestionAttributes( question_uuid=question_uuid, sender=sender, recipient=recipient, diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index 23b3dfe3a..47cc9733f 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.attributes import EventAttributes +from octue.cloud.events.attributes import QuestionAttributes, ResponseAttributes from octue.cloud.events.extraction import extract_and_deserialise_attributes from octue.cloud.events.validation import raise_if_event_is_invalid from octue.cloud.pub_sub import Subscription, Topic @@ -204,7 +204,7 @@ def answer(self, question, heartbeat_interval=120, timeout=30): return heartbeater = None - response_attributes = question_attributes.make_opposite_attributes() + response_attributes = ResponseAttributes.from_question_attributes(question_attributes) try: self._send_delivery_acknowledgment(response_attributes) @@ -545,7 +545,7 @@ def _send_question( input_manifest.use_signed_urls_for_datasets() question["input_manifest"] = input_manifest.to_primitive() - question_attributes = EventAttributes( + question_attributes = QuestionAttributes( question_uuid=question_uuid, parent_question_uuid=parent_question_uuid, originator_question_uuid=originator_question_uuid, @@ -639,7 +639,7 @@ def _parse_question(self, question): ) logger.info("%r parsed question %r successfully.", self, attributes["question_uuid"]) - attributes = EventAttributes(**attributes) + attributes = QuestionAttributes(**attributes) if attributes.retry_count > 0: logger.warning("This is retry %d for question %r.", attributes.retry_count, attributes.question_uuid) diff --git a/tests/cloud/events/test_attributes.py b/tests/cloud/events/test_attributes.py index 6196c8ac8..d802e9cf7 100644 --- a/tests/cloud/events/test_attributes.py +++ b/tests/cloud/events/test_attributes.py @@ -1,6 +1,6 @@ import unittest -from octue.cloud.events.attributes import EventAttributes +from octue.cloud.events.attributes import QuestionAttributes, ResponseAttributes QUESTION_UUID = "50760303-ee89-4752-81cc-aadd05f81752" SENDER = "my-org/my-parent:1.0.0" @@ -8,10 +8,10 @@ RECIPIENT = "my-org/my-child:2.0.0" -class TestEventAttributes(unittest.TestCase): +class TestQuestionAttributes(unittest.TestCase): def test_defaults(self): """Test that the defaults are correct.""" - attributes = EventAttributes( + attributes = QuestionAttributes( sender=SENDER, sender_type=SENDER_TYPE, recipient=RECIPIENT, @@ -43,9 +43,76 @@ def test_defaults(self): }, ) + def test_to_minimal_dict(self): + """Test that non-`None` attributes are excluded when making a minimal dictionary from attributes.""" + attributes = QuestionAttributes( + sender=SENDER, + sender_type=SENDER_TYPE, + recipient=RECIPIENT, + question_uuid=QUESTION_UUID, + ) + + attributes_dict = attributes.to_minimal_dict() + self.assertTrue(attributes_dict.pop("uuid")) + self.assertTrue(attributes_dict.pop("datetime")) + self.assertTrue(attributes_dict.pop("sender_sdk_version")) + + self.assertEqual( + attributes_dict, + { + "sender": SENDER, + "sender_type": SENDER_TYPE, + "recipient": RECIPIENT, + "question_uuid": QUESTION_UUID, + "originator_question_uuid": QUESTION_UUID, + "parent": SENDER, + "originator": SENDER, + "retry_count": 0, + }, + ) + + def test_to_serialised_attributes(self): + """Test that attributes are serialised correctly.""" + attributes = QuestionAttributes( + sender=SENDER, + sender_type=SENDER_TYPE, + recipient=RECIPIENT, + question_uuid=QUESTION_UUID, + forward_logs=True, + save_diagnostics="SAVE_DIAGNOSTICS_ON", + cpus=1, + memory="2Gi", + ephemeral_storage="256Mi", + ) + + serialised_attributes = attributes.to_serialised_attributes() + + self.assertTrue(serialised_attributes.pop("uuid")) + self.assertTrue(serialised_attributes.pop("sender_sdk_version")) + self.assertTrue(isinstance(serialised_attributes.pop("datetime"), str)) + + self.assertEqual( + serialised_attributes, + { + "sender": SENDER, + "sender_type": SENDER_TYPE, + "recipient": RECIPIENT, + "question_uuid": QUESTION_UUID, + "originator_question_uuid": QUESTION_UUID, + "parent": SENDER, + "originator": SENDER, + "retry_count": "0", + "forward_logs": "1", + "save_diagnostics": "SAVE_DIAGNOSTICS_ON", + "cpus": "1", + "memory": "2Gi", + "ephemeral_storage": "256Mi", + }, + ) + def test_reset_uuid_and_datetime(self): """Test that the `reset_uuid_and_datetime` method changes the UUID and datetime.""" - attributes = EventAttributes( + attributes = QuestionAttributes( sender=SENDER, sender_type=SENDER_TYPE, recipient=RECIPIENT, @@ -59,28 +126,28 @@ def test_reset_uuid_and_datetime(self): self.assertNotEqual(attributes.uuid, original_uuid) self.assertNotEqual(attributes.datetime, original_datetime) - def test_make_opposite_attributes(self): - """Test that the sender and recipient are reversed when making opposite attributes from a set of attributes.""" - attributes = EventAttributes( + +class TestResponseAttributes(unittest.TestCase): + def test_defaults(self): + """Test that the defaults are correct.""" + attributes = QuestionAttributes( sender=SENDER, sender_type=SENDER_TYPE, recipient=RECIPIENT, question_uuid=QUESTION_UUID, ) - opposite_attributes = attributes.make_opposite_attributes() - - opposite_attributes_dict = opposite_attributes.__dict__ - self.assertTrue(opposite_attributes_dict.pop("uuid")) - self.assertTrue(opposite_attributes_dict.pop("datetime")) - self.assertTrue(opposite_attributes_dict.pop("sender_sdk_version")) + attributes_dict = attributes.__dict__ + self.assertTrue(attributes_dict.pop("uuid")) + self.assertTrue(attributes_dict.pop("datetime")) + self.assertTrue(attributes_dict.pop("sender_sdk_version")) self.assertEqual( - opposite_attributes_dict, + attributes_dict, { - "sender": RECIPIENT, - "sender_type": "CHILD", - "recipient": SENDER, + "sender": SENDER, + "sender_type": SENDER_TYPE, + "recipient": RECIPIENT, "question_uuid": QUESTION_UUID, "parent_question_uuid": None, "originator_question_uuid": QUESTION_UUID, @@ -97,7 +164,7 @@ def test_make_opposite_attributes(self): def test_to_minimal_dict(self): """Test that non-`None` attributes are excluded when making a minimal dictionary from attributes.""" - attributes = EventAttributes( + attributes = ResponseAttributes( sender=SENDER, sender_type=SENDER_TYPE, recipient=RECIPIENT, @@ -125,16 +192,11 @@ def test_to_minimal_dict(self): def test_to_serialised_attributes(self): """Test that attributes are serialised correctly.""" - attributes = EventAttributes( + attributes = ResponseAttributes( sender=SENDER, sender_type=SENDER_TYPE, recipient=RECIPIENT, question_uuid=QUESTION_UUID, - forward_logs=True, - save_diagnostics="SAVE_DIAGNOSTICS_ON", - cpus=1, - memory="2Gi", - ephemeral_storage="256Mi", ) serialised_attributes = attributes.to_serialised_attributes() @@ -154,10 +216,52 @@ def test_to_serialised_attributes(self): "parent": SENDER, "originator": SENDER, "retry_count": "0", - "forward_logs": "1", - "save_diagnostics": "SAVE_DIAGNOSTICS_ON", - "cpus": "1", - "memory": "2Gi", - "ephemeral_storage": "256Mi", + }, + ) + + def test_reset_uuid_and_datetime(self): + """Test that the `reset_uuid_and_datetime` method changes the UUID and datetime.""" + attributes = ResponseAttributes( + sender=SENDER, + sender_type=SENDER_TYPE, + recipient=RECIPIENT, + question_uuid=QUESTION_UUID, + ) + + original_uuid = attributes.uuid + original_datetime = attributes.datetime + + attributes.reset_uuid_and_datetime() + self.assertNotEqual(attributes.uuid, original_uuid) + self.assertNotEqual(attributes.datetime, original_datetime) + + def test_from_question_attributes(self): + """Test that the sender and recipient are reversed when making opposite attributes from a set of attributes.""" + question_attributes = QuestionAttributes( + sender=SENDER, + sender_type=SENDER_TYPE, + recipient=RECIPIENT, + question_uuid=QUESTION_UUID, + ) + + opposite_attributes = ResponseAttributes.from_question_attributes(question_attributes) + + opposite_attributes_dict = opposite_attributes.__dict__ + self.assertTrue(opposite_attributes_dict.pop("uuid")) + self.assertTrue(opposite_attributes_dict.pop("datetime")) + self.assertTrue(opposite_attributes_dict.pop("sender_sdk_version")) + + self.assertEqual( + opposite_attributes_dict, + { + "sender": RECIPIENT, + "sender_type": "CHILD", + "recipient": SENDER, + "question_uuid": QUESTION_UUID, + "parent_question_uuid": None, + "originator_question_uuid": QUESTION_UUID, + "parent": SENDER, + "originator": SENDER, + "retry_count": 0, }, ) diff --git a/tests/cloud/pub_sub/test_events.py b/tests/cloud/pub_sub/test_events.py index 34c93d821..8431248a8 100644 --- a/tests/cloud/pub_sub/test_events.py +++ b/tests/cloud/pub_sub/test_events.py @@ -5,7 +5,7 @@ from octue.cloud.emulators._pub_sub import MESSAGES, MockMessage, MockService, MockSubscription from octue.cloud.emulators.service import ServicePatcher -from octue.cloud.events.attributes import EventAttributes +from octue.cloud.events.attributes import ResponseAttributes from octue.cloud.pub_sub.events import GoogleCloudPubSubEventHandler from octue.resources.service_backends import GCPPubSubBackend from tests import TEST_PROJECT_NAME @@ -31,7 +31,7 @@ def setUpClass(cls): backend=GCPPubSubBackend(project_name=TEST_PROJECT_NAME), ) - cls.attributes = EventAttributes( + cls.attributes = ResponseAttributes( question_uuid=cls.question_uuid, originator_question_uuid=cls.question_uuid, parent=cls.parent.id, diff --git a/tests/cloud/pub_sub/test_logging.py b/tests/cloud/pub_sub/test_logging.py index 36518bf94..5a2aefc65 100644 --- a/tests/cloud/pub_sub/test_logging.py +++ b/tests/cloud/pub_sub/test_logging.py @@ -5,14 +5,14 @@ from octue.cloud.emulators._pub_sub import MESSAGES, MockService from octue.cloud.emulators.service import ServicePatcher -from octue.cloud.events.attributes import EventAttributes +from octue.cloud.events.attributes import ResponseAttributes from octue.cloud.pub_sub.logging import GoogleCloudPubSubHandler from octue.resources.service_backends import GCPPubSubBackend from tests.base import BaseTestCase QUESTION_UUID = "96d69278-44ac-4631-aeea-c90fb08a1b2b" -ATTRIBUTES = EventAttributes( +ATTRIBUTES = ResponseAttributes( question_uuid=QUESTION_UUID, originator_question_uuid=QUESTION_UUID, parent="another/service:1.0.0", diff --git a/tests/cloud/pub_sub/test_service.py b/tests/cloud/pub_sub/test_service.py index 8bf7af164..b84faa1ce 100644 --- a/tests/cloud/pub_sub/test_service.py +++ b/tests/cloud/pub_sub/test_service.py @@ -133,7 +133,7 @@ def test_ask_unregistered_service_revision_when_service_registries_specified_res mock_response.status_code = 404 with patch("requests.get", return_value=mock_response): - with patch("octue.cloud.service_id._get_google_cloud_id_token", return_value="some-token"): + with patch("octue.cloud.registry._get_google_cloud_id_token", return_value="some-token"): with self.assertRaises(exceptions.ServiceNotFound): service.ask( service_id=f"my-org/unregistered-service:{MOCK_SERVICE_REVISION_TAG}", @@ -153,7 +153,7 @@ def test_ask_unregistered_service_with_no_revision_tag_when_service_registries_s mock_response.status_code = 404 with patch("requests.get", return_value=mock_response): - with patch("octue.cloud.service_id._get_google_cloud_id_token", return_value="some-token"): + with patch("octue.cloud.registry._get_google_cloud_id_token", return_value="some-token"): with self.assertRaises(exceptions.ServiceNotFound): service.ask(service_id="my-org/unregistered-service", input_values=[1, 2, 3, 4]) From 9ecc28df6ce00c3ef6ab0eb3f6246838237aa731 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Thu, 13 Mar 2025 13:30:10 +0000 Subject: [PATCH 208/216] REF: Factor out question attributes deserialisation --- octue/cloud/events/extraction.py | 34 +++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/octue/cloud/events/extraction.py b/octue/cloud/events/extraction.py index 473fafe8d..ce54df301 100644 --- a/octue/cloud/events/extraction.py +++ b/octue/cloud/events/extraction.py @@ -2,7 +2,8 @@ def extract_and_deserialise_attributes(container): - """Extract a Twined service event's attributes and deserialise them to the expected form. + """Extract a Twined service event's attributes and deserialise them to the expected form. This function doesn't + assume the required attributes are present as validation hasn't happened yet. :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 @@ -10,7 +11,6 @@ def extract_and_deserialise_attributes(container): # 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 `retry_count`, attribute if it's present (don't assume it is before validation). retry_count = attributes.get("retry_count") if retry_count: @@ -18,19 +18,29 @@ def extract_and_deserialise_attributes(container): else: attributes["retry_count"] = None - # Question events have some extra optional attributes that also need deserialising if they're present (don't assume - # they are before validation). if attributes.get("sender_type") == "PARENT": - forward_logs = attributes.get("forward_logs") + attributes = _deserialise_question_attributes(attributes) - if forward_logs: - attributes["forward_logs"] = bool(int(forward_logs)) - else: - attributes["forward_logs"] = None + return attributes + + +def _deserialise_question_attributes(attributes): + """Extract and deserialise the attributes specific to a question event. This function doesn't assume these + attributes are present as validation hasn't happened yet. + + :param dict attributes: attributes for a question event + :return dict: the deserialised attributes + """ + 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") + cpus = attributes.get("cpus") - if cpus: - attributes["cpus"] = int(cpus) + if cpus: + attributes["cpus"] = int(cpus) return attributes From d744a08a8c17f0d389c464237db3343e6633f5c5 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Thu, 13 Mar 2025 13:31:14 +0000 Subject: [PATCH 209/216] DOC: Add missing docstring --- octue/cloud/events/attributes.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/octue/cloud/events/attributes.py b/octue/cloud/events/attributes.py index 50cbbf905..755c08fb0 100644 --- a/octue/cloud/events/attributes.py +++ b/octue/cloud/events/attributes.py @@ -173,6 +173,12 @@ def __init__( self.ephemeral_storage = ephemeral_storage def to_minimal_dict(self): + """Convert the attributes to a minimal dictionary containing only the attributes that have a non-`None` value. + Using a minimal dictionary means the smallest possible data structure is used so `None` values don't, + for example, need to be redundantly encoded and transmitted when part of a JSON payload for a Pub/Sub message. + + :return dict: the non-`None` attributes + """ return { **super().to_minimal_dict(), **make_minimal_dictionary( From 0b2f636395aa2ae9ecee1d8d68bf438aded333b1 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Thu, 13 Mar 2025 13:35:39 +0000 Subject: [PATCH 210/216] ENH: Remove unnecessary setting of missing attributes before validation skipci --- octue/cloud/events/extraction.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/octue/cloud/events/extraction.py b/octue/cloud/events/extraction.py index ce54df301..dca4890a6 100644 --- a/octue/cloud/events/extraction.py +++ b/octue/cloud/events/extraction.py @@ -10,13 +10,10 @@ def extract_and_deserialise_attributes(container): """ # Cast attributes to a dictionary to avoid defaultdict-like behaviour from Pub/Sub message attributes container. attributes = dict(get_nested_attribute(container, "attributes")) - retry_count = attributes.get("retry_count") if retry_count: attributes["retry_count"] = int(retry_count) - else: - attributes["retry_count"] = None if attributes.get("sender_type") == "PARENT": attributes = _deserialise_question_attributes(attributes) @@ -35,8 +32,6 @@ def _deserialise_question_attributes(attributes): if forward_logs: attributes["forward_logs"] = bool(int(forward_logs)) - else: - attributes["forward_logs"] = None cpus = attributes.get("cpus") From 6317b3af4ce0d96fdff86d1f2dc3c1cf05fe1538 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Thu, 13 Mar 2025 17:04:09 +0000 Subject: [PATCH 211/216] REF: Move attribute extraction into `EventAttributes` class --- octue/cloud/events/attributes.py | 73 +++++++++++++++++++++++++++++ octue/cloud/events/extraction.py | 41 ---------------- octue/cloud/events/handler.py | 40 +++++++--------- octue/cloud/events/replayer.py | 3 +- octue/cloud/events/validation.py | 6 +-- octue/cloud/pub_sub/events.py | 6 ++- octue/cloud/pub_sub/service.py | 12 ++--- tests/cloud/events/test_replayer.py | 38 ++++++++------- 8 files changed, 123 insertions(+), 96 deletions(-) delete mode 100644 octue/cloud/events/extraction.py diff --git a/octue/cloud/events/attributes.py b/octue/cloud/events/attributes.py index 755c08fb0..059fed713 100644 --- a/octue/cloud/events/attributes.py +++ b/octue/cloud/events/attributes.py @@ -53,6 +53,35 @@ def __init__( self.recipient = recipient self.retry_count = retry_count + @classmethod + def from_serialised_attributes(cls, serialised_attributes): + """Extract a Twined service event's attributes and deserialise them to the expected form. This function doesn't + assume the required attributes are present as validation hasn't happened yet. + + :param dict serialised_attributes: the event container in dictionary format or direct Google Pub/Sub format + :return octue.cloud.events.attributes.EventAttributes: the extracted and deserialised attributes + """ + serialised_attributes = dict(serialised_attributes) + retry_count = serialised_attributes.get("retry_count") + + if retry_count: + serialised_attributes["retry_count"] = int(retry_count) + + return cls( + uuid=serialised_attributes.get("uuid"), + datetime=serialised_attributes.get("datetime"), + question_uuid=serialised_attributes.get("question_uuid"), + parent_question_uuid=serialised_attributes.get("parent_question_uuid"), + originator_question_uuid=serialised_attributes.get("originator_question_uuid"), + sender=serialised_attributes.get("sender"), + parent=serialised_attributes.get("parent"), + originator=serialised_attributes.get("originator"), + sender_type=serialised_attributes.get("sender_type"), + sender_sdk_version=serialised_attributes.get("sender_sdk_version"), + recipient=serialised_attributes.get("recipient"), + retry_count=serialised_attributes.get("retry_count"), + ) + def reset_uuid_and_datetime(self): """Set a new UUID and datetime. This avoids having to create a new instance for every single event (for which all other attributes are the same). @@ -172,6 +201,50 @@ def __init__( self.memory = memory self.ephemeral_storage = ephemeral_storage + @classmethod + def from_serialised_attributes(cls, serialised_attributes): + """Extract and deserialise the attributes specific to a question event. This function doesn't assume these + attributes are present as validation hasn't happened yet. + + :param dict serialised_attributes: attributes for a question event + :return octue.cloud.events.attributes.QuestionAttributes: the deserialised attributes + """ + serialised_attributes = dict(serialised_attributes) + retry_count = serialised_attributes.get("retry_count") + + if retry_count: + serialised_attributes["retry_count"] = int(retry_count) + + forward_logs = serialised_attributes.get("forward_logs") + + if forward_logs: + serialised_attributes["forward_logs"] = bool(int(forward_logs)) + + cpus = serialised_attributes.get("cpus") + + if cpus: + serialised_attributes["cpus"] = int(cpus) + + return cls( + uuid=serialised_attributes.get("uuid"), + datetime=serialised_attributes.get("datetime"), + question_uuid=serialised_attributes.get("question_uuid"), + parent_question_uuid=serialised_attributes.get("parent_question_uuid"), + originator_question_uuid=serialised_attributes.get("originator_question_uuid"), + sender=serialised_attributes.get("sender"), + parent=serialised_attributes.get("parent"), + originator=serialised_attributes.get("originator"), + sender_type=serialised_attributes.get("sender_type"), + sender_sdk_version=serialised_attributes.get("sender_sdk_version"), + recipient=serialised_attributes.get("recipient"), + retry_count=serialised_attributes.get("retry_count"), + forward_logs=serialised_attributes.get("forward_logs"), + save_diagnostics=serialised_attributes.get("save_diagnostics"), + cpus=serialised_attributes.get("cpus"), + memory=serialised_attributes.get("memory"), + ephemeral_storage=serialised_attributes.get("ephemeral_storage"), + ) + def to_minimal_dict(self): """Convert the attributes to a minimal dictionary containing only the attributes that have a non-`None` value. Using a minimal dictionary means the smallest possible data structure is used so `None` values don't, diff --git a/octue/cloud/events/extraction.py b/octue/cloud/events/extraction.py deleted file mode 100644 index dca4890a6..000000000 --- a/octue/cloud/events/extraction.py +++ /dev/null @@ -1,41 +0,0 @@ -from octue.utils.objects import get_nested_attribute - - -def extract_and_deserialise_attributes(container): - """Extract a Twined service event's attributes and deserialise them to the expected form. This function doesn't - assume the required attributes are present as validation hasn't happened yet. - - :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")) - retry_count = attributes.get("retry_count") - - if retry_count: - attributes["retry_count"] = int(retry_count) - - if attributes.get("sender_type") == "PARENT": - attributes = _deserialise_question_attributes(attributes) - - return attributes - - -def _deserialise_question_attributes(attributes): - """Extract and deserialise the attributes specific to a question event. This function doesn't assume these - attributes are present as validation hasn't happened yet. - - :param dict attributes: attributes for a question event - :return dict: the deserialised attributes - """ - forward_logs = attributes.get("forward_logs") - - if forward_logs: - attributes["forward_logs"] = bool(int(forward_logs)) - - cpus = attributes.get("cpus") - - if cpus: - attributes["cpus"] = int(cpus) - - return attributes diff --git a/octue/cloud/events/handler.py b/octue/cloud/events/handler.py index e2ab00661..ecd78ce6d 100644 --- a/octue/cloud/events/handler.py +++ b/octue/cloud/events/handler.py @@ -106,30 +106,22 @@ def _extract_and_validate_event(self, container): """ try: event, attributes = self._extract_event_and_attributes(container) - except Exception: + except Exception as e: + logger.exception(e) event = None attributes = {} - # Don't assume the presence of specific attributes before validation. - recipient = attributes.get("recipient") - child_sdk_version = attributes.get("sender_sdk_version") - if self.validate_events and not is_event_valid( event=event, attributes=attributes, - recipient=recipient, + recipient=attributes.recipient, parent_sdk_version=LOCAL_SDK_VERSION, - child_sdk_version=child_sdk_version, + child_sdk_version=attributes.sender_sdk_version, schema=self.schema, ): return (None, None) - logger.debug( - "%r: Received an event related to question %r.", - attributes.get("recipient"), - attributes.get("question_uuid"), - ) - + logger.debug("%r: Received an event related to question %r.", attributes.recipient, attributes.question_uuid) return (event, attributes) def _handle_event(self, event, attributes): @@ -140,7 +132,7 @@ def _handle_event(self, event, attributes): :return dict|None: the output of the event (this should be `None` unless the event is a "result" event) """ if self.record_events: - self.handled_events.append({"event": event, "attributes": attributes}) + self.handled_events.append({"event": event, "attributes": attributes.to_minimal_dict()}) if self.only_handle_result and event["kind"] != "result": return @@ -155,7 +147,7 @@ def _handle_delivery_acknowledgement(self, event, attributes): :param dict attributes: the event's attributes :return None: """ - logger.info("%rs question was delivered at %s.", attributes["recipient"], attributes["datetime"]) + logger.info("%rs question was delivered at %s.", attributes.recipient, attributes.datetime) def _handle_heartbeat(self, event, attributes): """Record the time the heartbeat was received. @@ -168,9 +160,9 @@ def _handle_heartbeat(self, event, attributes): logger.info( "%r: Received a heartbeat from service %r for question %r.", - attributes["recipient"], - attributes["sender"], - attributes["question_uuid"], + attributes.recipient, + attributes.sender, + attributes.question_uuid, ) def _handle_monitor_message(self, event, attributes): @@ -182,9 +174,9 @@ def _handle_monitor_message(self, event, attributes): """ logger.debug( "%r: Received a monitor message from service %r for question %r.", - attributes["recipient"], - attributes["sender"], - attributes["question_uuid"], + attributes.recipient, + attributes.sender, + attributes.question_uuid, ) if self.handle_monitor_message is not None: @@ -212,7 +204,7 @@ def _handle_log_message(self, event, attributes): # Get information about the immediate child sending the event and colour it with the first colour in the # colour palette. immediate_child_analysis_section = colourise( - f"[{attributes['sender']} | {attributes['question_uuid']}]", + f"[{attributes.sender} | {attributes.question_uuid}]", text_colour=self._log_message_colours[0], ) @@ -241,7 +233,7 @@ def _handle_exception(self, event, attributes): exception_message = "\n\n".join( ( event["exception_message"], - f"The following traceback was captured from the remote service {attributes['sender']!r}:", + f"The following traceback was captured from the remote service {attributes.sender!r}:", "".join(event["exception_traceback"]), ) ) @@ -262,7 +254,7 @@ def _handle_result(self, event, attributes): :param dict attributes: the event's attributes :return dict: """ - logger.info("%r: Received an answer to question %r.", attributes["recipient"], attributes["question_uuid"]) + logger.info("%r: Received an answer to question %r.", attributes.recipient, attributes.question_uuid) if event.get("output_manifest"): output_manifest = Manifest.deserialise(event["output_manifest"]) diff --git a/octue/cloud/events/replayer.py b/octue/cloud/events/replayer.py index 1ee2bfc47..ab57f6003 100644 --- a/octue/cloud/events/replayer.py +++ b/octue/cloud/events/replayer.py @@ -1,5 +1,6 @@ import logging +from octue.cloud.events.attributes import ResponseAttributes from octue.cloud.events.handler import AbstractEventHandler from octue.cloud.events.validation import SERVICE_COMMUNICATION_SCHEMA @@ -85,7 +86,7 @@ def _extract_event_and_attributes(self, container): :param dict container: the container of the event :return (any, dict): the event and its attributes """ - return container["event"], container["attributes"] + return container.get("event", {}), ResponseAttributes(**container["attributes"]) def _handle_question(self, event, attributes): """Log that the question was sent. diff --git a/octue/cloud/events/validation.py b/octue/cloud/events/validation.py index 5b2a2c918..238e4f7d2 100644 --- a/octue/cloud/events/validation.py +++ b/octue/cloud/events/validation.py @@ -33,7 +33,7 @@ def is_event_valid(event, attributes, recipient, parent_sdk_version, child_sdk_v """Check if the event and its attributes are valid according to the Octue services communication schema. :param dict event: the event to validate - :param dict attributes: the attributes of the event to validate + :param octue.cloud.events.attributes.EventAttributes attributes: the attributes of the event to validate :param str recipient: the SRUID of the service revision receiving and validating the event :param str parent_sdk_version: the semantic version of Octue SDK running on the parent :param str child_sdk_version: the semantic version of Octue SDK running on the child @@ -59,7 +59,7 @@ def raise_if_event_is_invalid(event, attributes, recipient, parent_sdk_version, """Raise an error if the event or its attributes aren't valid according to the Octue services communication schema. :param dict event: the event to validate - :param dict attributes: the attributes of the event to validate + :param octue.cloud.events.attributes.EventAttributes attributes: the attributes of the event to validate :param str recipient: the SRUID of the service revision receiving and validating the event :param str parent_sdk_version: the semantic version of Octue SDK running on the parent :param str child_sdk_version: the semantic version of Octue SDK running on the child @@ -68,7 +68,7 @@ def raise_if_event_is_invalid(event, attributes, recipient, parent_sdk_version, :return None: """ # Transform attributes to a dictionary in the case they're a different kind of mapping. - data = {"event": event, "attributes": dict(attributes)} + data = {"event": event, "attributes": attributes.to_minimal_dict()} if schema is None: schema = SERVICE_COMMUNICATION_SCHEMA diff --git a/octue/cloud/pub_sub/events.py b/octue/cloud/pub_sub/events.py index 3c46adb41..004ca3fdd 100644 --- a/octue/cloud/pub_sub/events.py +++ b/octue/cloud/pub_sub/events.py @@ -7,10 +7,11 @@ from google.api_core import retry from google.cloud.pubsub_v1 import SubscriberClient -from octue.cloud.events.extraction import extract_and_deserialise_attributes +from octue.cloud.events.attributes import ResponseAttributes 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__) @@ -246,5 +247,6 @@ def _extract_event_and_attributes(self, container): :return (any, dict): the event and its attributes """ event = extract_event(container.message) - attributes = extract_and_deserialise_attributes(container.message) + attributes = get_nested_attribute(container.message, "attributes") + attributes = ResponseAttributes.from_serialised_attributes(attributes) return event, attributes diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index 47cc9733f..522b58ef0 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -12,7 +12,6 @@ from octue.cloud.events import OCTUE_SERVICES_TOPIC_NAME from octue.cloud.events.attributes import QuestionAttributes, ResponseAttributes -from octue.cloud.events.extraction import extract_and_deserialise_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 @@ -30,6 +29,7 @@ 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.objects import get_nested_attribute from octue.utils.threads import RepeatingTimer logger = logging.getLogger(__name__) @@ -616,7 +616,7 @@ def _parse_question(self, question): """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 dictionary format or direct Google Pub/Sub format - :return (dict, octue.cloud.events.attributes.EventAttributes): the question's event and its attributes + :return (dict, octue.cloud.events.attributes.QuestionAttributes): the question's event and its attributes """ logger.info("%r received a question.", self) @@ -626,20 +626,18 @@ def _parse_question(self, question): logger.info("Question acknowledged on Pub/Sub.") event = extract_event(question) - attributes = extract_and_deserialise_attributes(question) + attributes = QuestionAttributes.from_serialised_attributes(get_nested_attribute(question, "attributes")) logger.info("Extracted question event and attributes.") raise_if_event_is_invalid( event=copy.deepcopy(event), attributes=attributes, recipient=self.id, - # Don't assume the presence of specific attributes before validation. - parent_sdk_version=attributes.get("sender_sdk_version"), + parent_sdk_version=attributes.sender_sdk_version, child_sdk_version=LOCAL_SDK_VERSION, ) - logger.info("%r parsed question %r successfully.", self, attributes["question_uuid"]) - attributes = QuestionAttributes(**attributes) + logger.info("%r parsed question %r successfully.", self, attributes.question_uuid) if attributes.retry_count > 0: logger.warning("This is retry %d for question %r.", attributes.retry_count, attributes.question_uuid) diff --git a/tests/cloud/events/test_replayer.py b/tests/cloud/events/test_replayer.py index b19cbf6d8..030fc8c01 100644 --- a/tests/cloud/events/test_replayer.py +++ b/tests/cloud/events/test_replayer.py @@ -11,6 +11,22 @@ EVENTS = json.load(f) +ATTRIBUTES = { + "datetime": "2024-04-11T10:46:48.236064", + "uuid": "a9de11b1-e88f-43fa-b3a4-40a590c3443f", + "retry_count": 0, + "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/test-service:1.0.0", + "originator": "octue/test-service:1.0.0", + "sender": "octue/test-service:1.0.0", + "sender_type": "CHILD", + "sender_sdk_version": "0.51.0", + "recipient": "octue/another-service:3.2.1", +} + + EXPECTED_OUTPUT_MANIFEST = { "id": "a13713ae-f207-41c6-9e29-0a848ced6039", "name": None, @@ -39,30 +55,16 @@ def test_with_no_events(self): def test_with_no_valid_events(self): """Test that `None` is returned if no valid events are received.""" with self.assertLogs(level=logging.DEBUG) as logging_context: - result = EventReplayer(validate_events=True).handle_events(events=[{"invalid": "event"}]) + result = EventReplayer(validate_events=True).handle_events( + events=[{"invalid": "event", "attributes": ATTRIBUTES}] + ) self.assertIsNone(result) self.assertIn("received an event that doesn't conform", logging_context.output[1]) def test_no_result_event(self): """Test that `None` is returned if no result event is received.""" - event = { - "event": {"kind": "delivery_acknowledgement"}, - "attributes": { - "datetime": "2024-04-11T10:46:48.236064", - "uuid": "a9de11b1-e88f-43fa-b3a4-40a590c3443f", - "retry_count": 0, - "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/test-service:1.0.0", - "originator": "octue/test-service:1.0.0", - "sender": "octue/test-service:1.0.0", - "sender_type": "CHILD", - "sender_sdk_version": "0.51.0", - "recipient": "octue/another-service:3.2.1", - }, - } + event = {"event": {"kind": "delivery_acknowledgement"}, "attributes": ATTRIBUTES} with self.assertLogs() as logging_context: result = EventReplayer().handle_events(events=[event]) From ca09b789a05c7179af0343b562ca517417d6f2d5 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Thu, 13 Mar 2025 17:14:26 +0000 Subject: [PATCH 212/216] REF: Remove extraneous event validation arguments --- octue/cloud/events/handler.py | 4 +--- octue/cloud/events/validation.py | 20 +++++--------------- octue/cloud/pub_sub/service.py | 13 +++---------- octue/compatibility.py | 29 ++++++++++++++--------------- tests/test_compatibility.py | 16 ++++++++-------- 5 files changed, 31 insertions(+), 51 deletions(-) diff --git a/octue/cloud/events/handler.py b/octue/cloud/events/handler.py index ecd78ce6d..581461f3b 100644 --- a/octue/cloud/events/handler.py +++ b/octue/cloud/events/handler.py @@ -7,7 +7,7 @@ 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, LOCAL_SDK_VERSION +from octue.definitions import GOOGLE_COMPUTE_PROVIDERS from octue.log_handlers import COLOUR_PALETTE from octue.resources.manifest import Manifest @@ -115,8 +115,6 @@ def _extract_and_validate_event(self, container): event=event, attributes=attributes, recipient=attributes.recipient, - parent_sdk_version=LOCAL_SDK_VERSION, - child_sdk_version=attributes.sender_sdk_version, schema=self.schema, ): return (None, None) diff --git a/octue/cloud/events/validation.py b/octue/cloud/events/validation.py index 238e4f7d2..f625994a7 100644 --- a/octue/cloud/events/validation.py +++ b/octue/cloud/events/validation.py @@ -3,6 +3,7 @@ import jsonschema from octue.compatibility import warn_if_incompatible +from octue.definitions import LOCAL_SDK_VERSION VALID_EVENT_KINDS = { "question", @@ -29,40 +30,29 @@ logger = logging.getLogger(__name__) -def is_event_valid(event, attributes, recipient, parent_sdk_version, child_sdk_version, schema=None): +def is_event_valid(event, attributes, recipient, schema=None): """Check if the event and its attributes are valid according to the Octue services communication schema. :param dict event: the event to validate :param octue.cloud.events.attributes.EventAttributes attributes: the attributes of the event to validate :param str recipient: the SRUID of the service revision receiving and validating the event - :param str parent_sdk_version: the semantic version of Octue SDK running on the parent - :param str child_sdk_version: the semantic version of Octue SDK running on the child :param dict|None schema: the schema to validate the event and its attributes against; if `None`, this defaults to the service communication schema used in this version of Octue SDK :return bool: `True` if the event and its attributes are valid """ try: - raise_if_event_is_invalid( - event, - attributes, - recipient, - parent_sdk_version, - child_sdk_version, - schema=schema, - ) + raise_if_event_is_invalid(event, attributes, recipient, schema=schema) except jsonschema.ValidationError: return False return True -def raise_if_event_is_invalid(event, attributes, recipient, parent_sdk_version, child_sdk_version, schema=None): +def raise_if_event_is_invalid(event, attributes, recipient, schema=None): """Raise an error if the event or its attributes aren't valid according to the Octue services communication schema. :param dict event: the event to validate :param octue.cloud.events.attributes.EventAttributes attributes: the attributes of the event to validate :param str recipient: the SRUID of the service revision receiving and validating the event - :param str parent_sdk_version: the semantic version of Octue SDK running on the parent - :param str child_sdk_version: the semantic version of Octue SDK running on the child :param dict|None schema: the schema to validate the event and its attributes against; if `None`, this defaults to the service communication schema used in this version of Octue SDK :raise jsonschema.ValidationError: if the event or its attributes are invalid :return None: @@ -83,7 +73,7 @@ def raise_if_event_is_invalid(event, attributes, recipient, parent_sdk_version, jsonschema.validate(data, schema) except jsonschema.ValidationError as error: - warn_if_incompatible(parent_sdk_version=parent_sdk_version, child_sdk_version=child_sdk_version) + warn_if_incompatible(sender_sdk_version=attributes.sender_sdk_version, recipient_sdk_version=LOCAL_SDK_VERSION) logger.exception( "%r received an event that doesn't conform with version %s of the service communication schema (%s): %r.", diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index 522b58ef0..46a8dcb22 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -250,8 +250,8 @@ def answer(self, question, heartbeat_interval=120, timeout=30): heartbeater.cancel() warn_if_incompatible( - child_sdk_version=LOCAL_SDK_VERSION, - parent_sdk_version=question_attributes.sender_sdk_version, + recipient_sdk_version=LOCAL_SDK_VERSION, + sender_sdk_version=question_attributes.sender_sdk_version, ) self.send_exception(attributes=response_attributes, timeout=timeout) @@ -629,14 +629,7 @@ def _parse_question(self, question): attributes = QuestionAttributes.from_serialised_attributes(get_nested_attribute(question, "attributes")) logger.info("Extracted question event and attributes.") - raise_if_event_is_invalid( - event=copy.deepcopy(event), - attributes=attributes, - recipient=self.id, - parent_sdk_version=attributes.sender_sdk_version, - child_sdk_version=LOCAL_SDK_VERSION, - ) - + raise_if_event_is_invalid(event=copy.deepcopy(event), attributes=attributes, recipient=self.id) logger.info("%r parsed question %r successfully.", self, attributes.question_uuid) if attributes.retry_count > 0: diff --git a/octue/compatibility.py b/octue/compatibility.py index abd402470..8d1d9f823 100644 --- a/octue/compatibility.py +++ b/octue/compatibility.py @@ -2,7 +2,6 @@ import logging import os - logger = logging.getLogger(__name__) @@ -36,18 +35,18 @@ def is_compatible(parent_sdk_version, child_sdk_version): return VERSION_COMPATIBILITIES[parent_sdk_version][child_sdk_version] -def warn_if_incompatible(parent_sdk_version, child_sdk_version): - """Log a warning if the parent SDK version isn't compatible with the child SDK version, or if compatibility can't be - checked due to an absence of version information for one of them. +def warn_if_incompatible(sender_sdk_version, recipient_sdk_version): + """Log a warning if the sender's SDK version isn't compatible with the recipient's SDK version, or if compatibility + can't be checked due to an absence of version information for one of them. - :param str|None parent_sdk_version: the version of the Octue SDK running locally / on the local service - :param str|None child_sdk_version: the version of the Octue SDK running on the remote service + :param str|None sender_sdk_version: the version of the Octue SDK running on the sender + :param str|None recipient_sdk_version: the version of the Octue SDK running on the recipient :return None: """ - if not parent_sdk_version: - missing_service_version_information = "parent" - elif not child_sdk_version: - missing_service_version_information = "child" + if not sender_sdk_version: + missing_service_version_information = "sender" + elif not recipient_sdk_version: + missing_service_version_information = "recipient" else: missing_service_version_information = None @@ -59,10 +58,10 @@ def warn_if_incompatible(parent_sdk_version, child_sdk_version): ) return - if not is_compatible(parent_sdk_version, child_sdk_version): + if not is_compatible(sender_sdk_version, recipient_sdk_version): logger.warning( - "The parent's Octue SDK version %s is incompatible with the child's version %s. Please update either or " - "both to the latest version.", - parent_sdk_version, - child_sdk_version, + "The sender's Octue SDK version %s is incompatible with the recipient's version %s. Please update either " + "or both to the latest version.", + sender_sdk_version, + recipient_sdk_version, ) diff --git a/tests/test_compatibility.py b/tests/test_compatibility.py index 241f904c6..b6477d1cf 100644 --- a/tests/test_compatibility.py +++ b/tests/test_compatibility.py @@ -64,21 +64,21 @@ class TestWarnIfIncompatible(BaseTestCase): def test_warn_if_incompatible_with_missing_child_version_information(self): """Test that a warning is raised when calling `warn_if_incompatible` with missing child version information.""" with self.assertLogs(level=logging.WARNING) as logging_context: - warn_if_incompatible(parent_sdk_version="0.51.0", child_sdk_version=None) + warn_if_incompatible(sender_sdk_version="0.51.0", recipient_sdk_version=None) self.assertIn( - "The child couldn't be checked for compatibility with this service because its Octue SDK version wasn't " - "provided. Please update it to the latest Octue SDK version.", + "The recipient couldn't be checked for compatibility with this service because its Octue SDK version " + "wasn't provided. Please update it to the latest Octue SDK version.", logging_context.output[0], ) def test_warn_if_incompatible_with_missing_parent_version_information(self): """Test that a warning is raised when calling `warn_if_incompatible` with missing parent version information.""" with self.assertLogs(level=logging.WARNING) as logging_context: - warn_if_incompatible(parent_sdk_version=None, child_sdk_version="0.16.0") + warn_if_incompatible(sender_sdk_version=None, recipient_sdk_version="0.16.0") self.assertIn( - "The parent couldn't be checked for compatibility with this service because its Octue SDK version wasn't " + "The sender couldn't be checked for compatibility with this service because its Octue SDK version wasn't " "provided. Please update it to the latest Octue SDK version.", logging_context.output[0], ) @@ -86,10 +86,10 @@ def test_warn_if_incompatible_with_missing_parent_version_information(self): def test_warn_if_incompatible_with_incompatible_versions(self): """Test that a warning is raised if incompatible versions are detected.""" with self.assertLogs(level=logging.WARNING) as logging_context: - warn_if_incompatible(parent_sdk_version="0.50.0", child_sdk_version="0.51.0") + warn_if_incompatible(sender_sdk_version="0.50.0", recipient_sdk_version="0.51.0") self.assertIn( - "The parent's Octue SDK version 0.50.0 is incompatible with the child's version 0.51.0. Please update " + "The sender's Octue SDK version 0.50.0 is incompatible with the recipient's version 0.51.0. Please update " "either or both to the latest version.", logging_context.output[0], ) @@ -100,7 +100,7 @@ def test_warn_if_incompatible_with_compatible_versions(self): try: with self.assertLogs(level=logging.WARNING): - warn_if_incompatible(parent_sdk_version="0.40.0", child_sdk_version="0.40.0") + warn_if_incompatible(sender_sdk_version="0.40.0", recipient_sdk_version="0.40.0") except AssertionError: no_warnings = True From da6faa56c4528be14d36e8c7bf70ee3871cb2c50 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Thu, 13 Mar 2025 17:17:44 +0000 Subject: [PATCH 213/216] REF: Factor out default maximum heartbeat interval --- octue/cloud/pub_sub/events.py | 3 ++- octue/cloud/pub_sub/service.py | 4 ++-- octue/definitions.py | 1 + octue/resources/child.py | 3 ++- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/octue/cloud/pub_sub/events.py b/octue/cloud/pub_sub/events.py index 004ca3fdd..e33c8a3b1 100644 --- a/octue/cloud/pub_sub/events.py +++ b/octue/cloud/pub_sub/events.py @@ -10,6 +10,7 @@ from octue.cloud.events.attributes import ResponseAttributes from octue.cloud.events.handler import AbstractEventHandler from octue.cloud.events.validation import SERVICE_COMMUNICATION_SCHEMA +from octue.definitions import DEFAULT_MAXIMUM_HEARTBEAT_INTERVAL from octue.utils.decoders import OctueJSONDecoder from octue.utils.objects import get_nested_attribute from octue.utils.threads import RepeatingTimer @@ -110,7 +111,7 @@ def _time_since_last_heartbeat(self): return datetime.now() - self._last_heartbeat - def handle_events(self, timeout=60, maximum_heartbeat_interval=360): + def handle_events(self, timeout=60, maximum_heartbeat_interval=DEFAULT_MAXIMUM_HEARTBEAT_INTERVAL): """Pull events from the subscription and handle them in the order they were sent until a "result" event is handled, then return the handled result. diff --git a/octue/cloud/pub_sub/service.py b/octue/cloud/pub_sub/service.py index 46a8dcb22..da63e8470 100644 --- a/octue/cloud/pub_sub/service.py +++ b/octue/cloud/pub_sub/service.py @@ -24,7 +24,7 @@ validate_sruid, ) from octue.compatibility import warn_if_incompatible -from octue.definitions import LOCAL_SDK_VERSION +from octue.definitions import DEFAULT_MAXIMUM_HEARTBEAT_INTERVAL, LOCAL_SDK_VERSION import octue.exceptions from octue.utils.dictionaries import make_minimal_dictionary from octue.utils.encoders import OctueJSONEncoder @@ -383,7 +383,7 @@ def wait_for_answer( handle_monitor_message=None, record_events=True, timeout=60, - maximum_heartbeat_interval=360, + maximum_heartbeat_interval=DEFAULT_MAXIMUM_HEARTBEAT_INTERVAL, ): """Wait for an answer to a question on the given subscription, deleting the subscription and its topic once the answer is received. diff --git a/octue/definitions.py b/octue/definitions.py index 8781d90c1..87b81998f 100644 --- a/octue/definitions.py +++ b/octue/definitions.py @@ -20,3 +20,4 @@ GOOGLE_COMPUTE_PROVIDERS = {"GOOGLE_CLOUD_FUNCTION"} LOCAL_SDK_VERSION = importlib.metadata.version("octue") +DEFAULT_MAXIMUM_HEARTBEAT_INTERVAL = 360 diff --git a/octue/resources/child.py b/octue/resources/child.py index 6e15e8616..53bcea6bc 100644 --- a/octue/resources/child.py +++ b/octue/resources/child.py @@ -4,6 +4,7 @@ import os from octue.cloud.pub_sub.service import Service +from octue.definitions import DEFAULT_MAXIMUM_HEARTBEAT_INTERVAL from octue.resources import service_backends logger = logging.getLogger(__name__) @@ -76,7 +77,7 @@ def ask( prevent_retries_when=None, log_errors=True, timeout=86400, - maximum_heartbeat_interval=360, + maximum_heartbeat_interval=DEFAULT_MAXIMUM_HEARTBEAT_INTERVAL, ): """Ask the child either: - A synchronous (ask-and-wait) question and wait for it to return an output. Questions are synchronous if From a0a631783196b2b1541734251e63a8ef1884a73f Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Thu, 13 Mar 2025 17:27:55 +0000 Subject: [PATCH 214/216] REF: Rename `utils` module to `question` skipci --- octue/cli.py | 2 +- octue/cloud/events/{utils.py => question.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename octue/cloud/events/{utils.py => question.py} (100%) diff --git a/octue/cli.py b/octue/cli.py index 03e5621e6..00d06b9af 100644 --- a/octue/cli.py +++ b/octue/cli.py @@ -11,7 +11,7 @@ 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.question import make_question_event from octue.cloud.events.validation import VALID_EVENT_KINDS from octue.cloud.pub_sub.bigquery import get_events, DEFAULT_EVENT_STORE_TABLE_ID from octue.cloud.pub_sub.service import Service diff --git a/octue/cloud/events/utils.py b/octue/cloud/events/question.py similarity index 100% rename from octue/cloud/events/utils.py rename to octue/cloud/events/question.py From 85b27bd99006cef477e4716bcf6580d93e2d37ad Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Thu, 13 Mar 2025 17:38:02 +0000 Subject: [PATCH 215/216] FIX: Ensure only `google_crc32c` warning is caught --- octue/cloud/storage/client.py | 2 +- octue/mixins/hashable.py | 2 +- octue/resources/datafile.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/octue/cloud/storage/client.py b/octue/cloud/storage/client.py index a4064df2b..c45e5da45 100644 --- a/octue/cloud/storage/client.py +++ b/octue/cloud/storage/client.py @@ -21,7 +21,7 @@ from octue.utils.encoders import OctueJSONEncoder with warnings.catch_warnings(): - warnings.simplefilter("ignore") + warnings.filterwarnings("ignore", category=RuntimeWarning, module="google_crc32c") from google_crc32c import Checksum logger = logging.getLogger(__name__) diff --git a/octue/mixins/hashable.py b/octue/mixins/hashable.py index 1efdc6497..10ff7fc23 100644 --- a/octue/mixins/hashable.py +++ b/octue/mixins/hashable.py @@ -4,7 +4,7 @@ import warnings with warnings.catch_warnings(): - warnings.simplefilter("ignore") + warnings.filterwarnings("ignore", category=RuntimeWarning, module="google_crc32c") from google_crc32c import Checksum diff --git a/octue/resources/datafile.py b/octue/resources/datafile.py index 8c0f3eeae..bb02153e7 100644 --- a/octue/resources/datafile.py +++ b/octue/resources/datafile.py @@ -27,7 +27,7 @@ from octue.utils.metadata import METADATA_FILENAME, UpdateLocalMetadata, load_local_metadata_file with warnings.catch_warnings(): - warnings.simplefilter("ignore") + warnings.filterwarnings("ignore", category=RuntimeWarning, module="google_crc32c") from google_crc32c import Checksum From 15bbf72ec26abe4dd94bd0114f53161eae996475 Mon Sep 17 00:00:00 2001 From: cortadocodes <cortado.codes@protonmail.com> Date: Mon, 24 Mar 2025 09:53:00 +0000 Subject: [PATCH 216/216] DEP: Remove `gunicorn` and `Flask` dependencies skipci --- poetry.lock | 283 +++++++------------------------------------------ pyproject.toml | 2 - 2 files changed, 40 insertions(+), 245 deletions(-) diff --git a/poetry.lock b/poetry.lock index d46f5e880..c519c5a5c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.0.0 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. [[package]] name = "alabaster" @@ -7,7 +7,6 @@ description = "A light, configurable Sphinx theme" optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"}, {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, @@ -20,7 +19,6 @@ 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"}, @@ -43,7 +41,6 @@ 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"}, @@ -60,7 +57,6 @@ description = "A small Python module for determining appropriate platform-specif 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"}, @@ -73,19 +69,18 @@ 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"}, ] [package.extras] -benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] -tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] +tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""] [[package]] name = "autodocsumm" @@ -94,7 +89,6 @@ 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"}, @@ -110,7 +104,6 @@ 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"}, @@ -126,7 +119,6 @@ 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"}, @@ -142,19 +134,6 @@ charset-normalizer = ["charset-normalizer"] html5lib = ["html5lib"] lxml = ["lxml"] -[[package]] -name = "blinker" -version = "1.9.0" -description = "Fast, simple object-to-object and broadcast signaling" -optional = false -python-versions = ">=3.9" -groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" -files = [ - {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.2" @@ -162,7 +141,6 @@ 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.2-py3-none-any.whl", hash = "sha256:ebad2091bf12d0d200dfc2464330db638c5deb41d546f6d7aca079e87290f3b0"}, {file = "cachecontrol-0.14.2.tar.gz", hash = "sha256:7d47d19f866409b98ff6025b6a0fca8e4c791fb31abbd95f622093894ce903a2"}, @@ -185,7 +163,6 @@ 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"}, @@ -198,7 +175,6 @@ 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"}, @@ -211,7 +187,6 @@ 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,7 +199,6 @@ description = "The Real First Universal Charset Detector. Open, modern and activ optional = false 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"}, @@ -327,7 +301,6 @@ 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.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, @@ -347,7 +320,7 @@ 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\")"} +markers = {main = "platform_system == \"Windows\"", dev = "sys_platform == \"win32\" or platform_system == \"Windows\""} [[package]] name = "coolname" @@ -356,7 +329,6 @@ 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"}, @@ -369,7 +341,6 @@ 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"}, @@ -435,7 +406,6 @@ 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"}, @@ -446,7 +416,7 @@ more-itertools = "*" [package.extras] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -test = ["cssselect", "importlib-resources", "jaraco.test (>=5.1)", "lxml", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] +test = ["cssselect", "importlib-resources ; python_version < \"3.9\"", "jaraco.test (>=5.1)", "lxml ; python_version < \"3.11\"", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] [[package]] name = "dateparser" @@ -455,7 +425,6 @@ 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"}, @@ -468,7 +437,7 @@ regex = "<2019.02.19 || >2019.02.19,<2021.8.27 || >2021.8.27,<2022.3.15" tzlocal = "*" [package.extras] -calendars = ["convertdate", "convertdate", "hijri-converter"] +calendars = ["convertdate ; python_version < \"3.6\"", "convertdate ; python_version >= \"3.6\"", "hijri-converter ; python_version >= \"3.6\""] fasttext = ["fasttext"] langdetect = ["langdetect"] @@ -479,7 +448,6 @@ description = "Python @deprecated decorator to deprecate old python classes, fun 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"}, @@ -489,7 +457,7 @@ files = [ wrapt = ">=1.10,<2" [package.extras] -dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "jinja2 (>=3.0.3,<3.1.0)", "setuptools", "sphinx (<2)", "tox"] +dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "jinja2 (>=3.0.3,<3.1.0)", "setuptools ; python_version >= \"3.12\"", "sphinx (<2)", "tox"] [[package]] name = "dict2css" @@ -498,7 +466,6 @@ description = "A μ-library for constructing cascading style sheets from Python 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"}, @@ -515,7 +482,6 @@ 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"}, @@ -528,7 +494,6 @@ 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"}, @@ -541,7 +506,6 @@ 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"}, @@ -578,7 +542,6 @@ 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,31 +550,7 @@ files = [ [package.extras] docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"] testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] -typing = ["typing-extensions (>=4.12.2)"] - -[[package]] -name = "flask" -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"}, -] - -[package.dependencies] -blinker = ">=1.6.2" -click = ">=8.1.3" -itsdangerous = ">=2.1.2" -Jinja2 = ">=3.1.2" -Werkzeug = ">=2.3.7" - -[package.extras] -async = ["asgiref (>=3.2)"] -dotenv = ["python-dotenv"] +typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] [[package]] name = "fs" @@ -620,7 +559,6 @@ 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"}, @@ -632,7 +570,7 @@ setuptools = "*" six = ">=1.10,<2.0" [package.extras] -scandir = ["scandir (>=1.5,<2.0)"] +scandir = ["scandir (>=1.5,<2.0) ; python_version < \"3.5\""] [[package]] name = "gcp-storage-emulator" @@ -641,7 +579,6 @@ 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"}, @@ -658,7 +595,6 @@ 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"}, @@ -668,15 +604,15 @@ files = [ google-auth = ">=2.14.1,<3.0.dev0" googleapis-common-protos = ">=1.56.2,<2.0.dev0" grpcio = [ - {version = ">=1.33.2,<2.0dev", optional = true, markers = "python_version < \"3.11\" and extra == \"grpc\""}, + {version = ">=1.33.2,<2.0dev", optional = true, markers = "extra == \"grpc\""}, {version = ">=1.49.1,<2.0dev", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}, ] grpcio-status = [ - {version = ">=1.33.2,<2.0.dev0", optional = true, markers = "python_version < \"3.11\" and extra == \"grpc\""}, + {version = ">=1.33.2,<2.0.dev0", optional = true, markers = "extra == \"grpc\""}, {version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}, ] proto-plus = [ - {version = ">=1.22.3,<2.0.0dev", markers = "python_version < \"3.13\""}, + {version = ">=1.22.3,<2.0.0dev"}, {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" @@ -684,7 +620,7 @@ requests = ">=2.18.0,<3.0.0.dev0" [package.extras] async-rest = ["google-auth[aiohttp] (>=2.35.0,<3.0.dev0)"] -grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio (>=1.49.1,<2.0dev)", "grpcio-status (>=1.33.2,<2.0.dev0)", "grpcio-status (>=1.49.1,<2.0.dev0)"] +grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio (>=1.49.1,<2.0dev) ; python_version >= \"3.11\"", "grpcio-status (>=1.33.2,<2.0.dev0)", "grpcio-status (>=1.49.1,<2.0.dev0) ; python_version >= \"3.11\""] grpcgcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] @@ -695,7 +631,6 @@ 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"}, @@ -721,7 +656,6 @@ 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"}, @@ -737,14 +671,14 @@ python-dateutil = ">=2.7.3,<3.0dev" requests = ">=2.21.0,<3.0.0dev" [package.extras] -all = ["Shapely (>=1.8.4,<3.0.0dev)", "bigquery-magics (>=0.1.0)", "db-dtypes (>=0.3.0,<2.0.0dev)", "geopandas (>=0.9.0,<1.0dev)", "google-cloud-bigquery-storage (>=2.6.0,<3.0.0dev)", "grpcio (>=1.47.0,<2.0dev)", "grpcio (>=1.49.1,<2.0dev)", "importlib-metadata (>=1.0.0)", "ipykernel (>=6.0.0)", "ipywidgets (>=7.7.0)", "opentelemetry-api (>=1.1.0)", "opentelemetry-instrumentation (>=0.20b0)", "opentelemetry-sdk (>=1.1.0)", "pandas (>=1.1.0)", "proto-plus (>=1.22.3,<2.0.0dev)", "protobuf (>=3.20.2,!=4.21.0,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5,<6.0.0dev)", "pyarrow (>=3.0.0)", "tqdm (>=4.7.4,<5.0.0dev)"] +all = ["Shapely (>=1.8.4,<3.0.0dev)", "bigquery-magics (>=0.1.0)", "db-dtypes (>=0.3.0,<2.0.0dev)", "geopandas (>=0.9.0,<1.0dev)", "google-cloud-bigquery-storage (>=2.6.0,<3.0.0dev)", "grpcio (>=1.47.0,<2.0dev)", "grpcio (>=1.49.1,<2.0dev) ; python_version >= \"3.11\"", "importlib-metadata (>=1.0.0) ; python_version < \"3.8\"", "ipykernel (>=6.0.0)", "ipywidgets (>=7.7.0)", "opentelemetry-api (>=1.1.0)", "opentelemetry-instrumentation (>=0.20b0)", "opentelemetry-sdk (>=1.1.0)", "pandas (>=1.1.0)", "proto-plus (>=1.22.3,<2.0.0dev)", "protobuf (>=3.20.2,!=4.21.0,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5,<6.0.0dev)", "pyarrow (>=3.0.0)", "tqdm (>=4.7.4,<5.0.0dev)"] bigquery-v2 = ["proto-plus (>=1.22.3,<2.0.0dev)", "protobuf (>=3.20.2,!=4.21.0,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5,<6.0.0dev)"] -bqstorage = ["google-cloud-bigquery-storage (>=2.6.0,<3.0.0dev)", "grpcio (>=1.47.0,<2.0dev)", "grpcio (>=1.49.1,<2.0dev)", "pyarrow (>=3.0.0)"] +bqstorage = ["google-cloud-bigquery-storage (>=2.6.0,<3.0.0dev)", "grpcio (>=1.47.0,<2.0dev)", "grpcio (>=1.49.1,<2.0dev) ; python_version >= \"3.11\"", "pyarrow (>=3.0.0)"] geopandas = ["Shapely (>=1.8.4,<3.0.0dev)", "geopandas (>=0.9.0,<1.0dev)"] ipython = ["bigquery-magics (>=0.1.0)"] ipywidgets = ["ipykernel (>=6.0.0)", "ipywidgets (>=7.7.0)"] opentelemetry = ["opentelemetry-api (>=1.1.0)", "opentelemetry-instrumentation (>=0.20b0)", "opentelemetry-sdk (>=1.1.0)"] -pandas = ["db-dtypes (>=0.3.0,<2.0.0dev)", "importlib-metadata (>=1.0.0)", "pandas (>=1.1.0)", "pyarrow (>=3.0.0)"] +pandas = ["db-dtypes (>=0.3.0,<2.0.0dev)", "importlib-metadata (>=1.0.0) ; python_version < \"3.8\"", "pandas (>=1.1.0)", "pyarrow (>=3.0.0)"] tqdm = ["tqdm (>=4.7.4,<5.0.0dev)"] [[package]] @@ -754,7 +688,6 @@ 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"}, @@ -774,7 +707,6 @@ 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.2-py2.py3-none-any.whl", hash = "sha256:a919f84fdea683b0a02464e38dd32332edbcbc8e85da82070079a57791119fd6"}, {file = "google_cloud_pubsub-2.27.2.tar.gz", hash = "sha256:d92c156c7ddd0e5125008f977898198d7b1ae766026056497271bec4909647fe"}, @@ -789,8 +721,8 @@ 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.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.22.0,<2.0.0dev"}, + {version = ">=1.22.2,<2.0.0dev", markers = "python_version >= \"3.11\""}, {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" @@ -805,7 +737,6 @@ 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"}, @@ -816,7 +747,7 @@ 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.22.3,<2.0.0dev", markers = "python_version < \"3.13\""}, + {version = ">=1.22.3,<2.0.0dev"}, {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" @@ -828,7 +759,6 @@ 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"}, @@ -853,7 +783,6 @@ 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"}, @@ -910,7 +839,6 @@ 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"}, @@ -930,7 +858,6 @@ 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"}, @@ -950,7 +877,6 @@ 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.14.0-py2.py3-none-any.whl", hash = "sha256:fb4a084b30099ba3ab07d61d620a0d4429570b13ff53bd37bac75235f98b7da4"}, {file = "grpc_google_iam_v1-0.14.0.tar.gz", hash = "sha256:c66e07aa642e39bb37950f9e7f491f70dad150ac9801263b42b2814307c2df99"}, @@ -968,7 +894,6 @@ description = "HTTP/2-based RPC framework" optional = false python-versions = ">=3.8" 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"}, @@ -1037,7 +962,6 @@ 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.69.0-py3-none-any.whl", hash = "sha256:d6b2a3c9562c03a817c628d7ba9a925e209c228762d6d7677ae5c9401a542853"}, {file = "grpcio_status-1.69.0.tar.gz", hash = "sha256:595ef84e5178d6281caa732ccf68ff83259241608d26b0e9c40a5e66eee2a2d2"}, @@ -1048,29 +972,6 @@ googleapis-common-protos = ">=1.5.5" grpcio = ">=1.69.0" protobuf = ">=5.26.1,<6.0dev" -[[package]] -name = "gunicorn" -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"}, -] - -[package.dependencies] -packaging = "*" - -[package.extras] -eventlet = ["eventlet (>=0.24.1,!=0.36.0)"] -gevent = ["gevent (>=1.4.0)"] -setproctitle = ["setproctitle"] -testing = ["coverage", "eventlet", "gevent", "pytest", "pytest-cov"] -tornado = ["tornado (>=0.2)"] - [[package]] name = "h5py" version = "3.12.1" @@ -1078,7 +979,7 @@ description = "Read and write HDF5 files from Python" optional = true python-versions = ">=3.9" groups = ["main"] -markers = "(python_version <= \"3.11\" or python_version >= \"3.12\") and extra == \"hdf5\"" +markers = "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"}, @@ -1118,7 +1019,6 @@ 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"}, @@ -1129,10 +1029,10 @@ six = ">=1.9" webencodings = "*" [package.extras] -all = ["chardet (>=2.2)", "genshi", "lxml"] +all = ["chardet (>=2.2)", "genshi", "lxml ; platform_python_implementation == \"CPython\""] chardet = ["chardet (>=2.2)"] genshi = ["genshi"] -lxml = ["lxml"] +lxml = ["lxml ; platform_python_implementation == \"CPython\""] [[package]] name = "identify" @@ -1141,7 +1041,6 @@ description = "File identification library for Python" optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "identify-2.6.5-py2.py3-none-any.whl", hash = "sha256:14181a47091eb75b337af4c23078c9d09225cd4c48929f521f3bf16b09d02566"}, {file = "identify-2.6.5.tar.gz", hash = "sha256:c10b33f250e5bba374fae86fb57f3adcebf1161bce7cdf92031915fd480c13bc"}, @@ -1157,7 +1056,6 @@ 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"}, @@ -1173,7 +1071,6 @@ 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"}, @@ -1186,7 +1083,6 @@ 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"}, @@ -1196,12 +1092,12 @@ files = [ zipp = ">=3.20" [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] 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)"] perf = ["ipython"] -test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +test = ["flufl.flake8", "importlib-resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] type = ["pytest-mypy"] [[package]] @@ -1211,33 +1107,18 @@ 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"}, ] -[[package]] -name = "itsdangerous" -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"}, -] - [[package]] name = "jinja2" 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\"" +groups = ["dev"] files = [ {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"}, {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, @@ -1256,7 +1137,6 @@ 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"}, @@ -1279,7 +1159,6 @@ description = "The JSON Schema meta-schemas and vocabularies, exposed as a Regis optional = false python-versions = ">=3.9" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "jsonschema_specifications-2024.10.1-py3-none-any.whl", hash = "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf"}, {file = "jsonschema_specifications-2024.10.1.tar.gz", hash = "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272"}, @@ -1294,8 +1173,7 @@ version = "3.0.2" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.9" -groups = ["main", "dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +groups = ["dev"] 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"}, @@ -1367,7 +1245,6 @@ 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"}, @@ -1380,7 +1257,6 @@ 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"}, @@ -1455,7 +1331,6 @@ 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"}, @@ -1472,7 +1347,6 @@ 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"}, @@ -1485,7 +1359,6 @@ description = "Fundamental package for array computing in Python" optional = false 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"}, @@ -1551,7 +1424,6 @@ 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"}, @@ -1568,7 +1440,6 @@ 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"}, @@ -1586,7 +1457,6 @@ 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"}, @@ -1603,7 +1473,6 @@ 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"}, @@ -1616,7 +1485,6 @@ description = "Powerful data structures for data analysis, time series, and stat optional = false 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"}, @@ -1704,7 +1572,6 @@ description = "A small Python package for determining appropriate platform-speci 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"}, @@ -1722,7 +1589,6 @@ 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"}, @@ -1739,7 +1605,6 @@ description = "A framework for managing and maintaining multi-language pre-commi 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"}, @@ -1759,7 +1624,6 @@ 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"}, @@ -1778,7 +1642,6 @@ description = "" optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {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"}, @@ -1800,7 +1663,6 @@ description = "library with cross-python path, ini-parsing, io, code, log facili 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"}, @@ -1813,7 +1675,6 @@ description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs 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"}, @@ -1826,7 +1687,6 @@ 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"}, @@ -1842,7 +1702,6 @@ description = "The kitchen sink of Python utility libraries for doing \"stuff\" 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"}, @@ -1861,7 +1720,6 @@ 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.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, @@ -1877,7 +1735,6 @@ 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"}, @@ -1901,7 +1758,6 @@ 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"}, @@ -1917,7 +1773,6 @@ description = "Read key-value pairs from a .env file and set them as environment 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"}, @@ -1933,7 +1788,6 @@ 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"}, @@ -1946,7 +1800,6 @@ 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"}, @@ -2010,7 +1863,6 @@ 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"}, @@ -2027,7 +1879,6 @@ 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"}, @@ -2112,7 +1963,6 @@ 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"}, @@ -2135,7 +1985,6 @@ description = "Python bindings to Rust's persistent data structures (rpds)" optional = false 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"}, @@ -2249,7 +2098,6 @@ 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"}, @@ -2265,7 +2113,6 @@ description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip pres optional = false python-versions = ">=3.7" groups = ["dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "ruamel.yaml-0.18.10-py3-none-any.whl", hash = "sha256:30f22513ab2301b3d2b577adc121c6471f28734d3d9728581245f1e76468b4f1"}, {file = "ruamel.yaml-0.18.10.tar.gz", hash = "sha256:20c86ab29ac2153f80a428e1254a8adf686d3383df04490514ca3b79a362db58"}, @@ -2285,7 +2132,7 @@ description = "C version of reader, parser and emitter for ruamel.yaml derived f optional = false 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\")" +markers = "platform_python_implementation == \"CPython\" and python_version < \"3.13\"" 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"}, @@ -2337,7 +2184,6 @@ description = "An extremely fast Python linter and code formatter, written in Ru 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"}, @@ -2366,20 +2212,19 @@ description = "Easily download, build, install, upgrade, and uninstall Python pa optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {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.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)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] +core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "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) ; python_version < \"3.11\"", "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.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"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "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 ; sys_platform != \"cygwin\"", "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) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] [[package]] name = "six" @@ -2388,7 +2233,6 @@ 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"}, @@ -2401,7 +2245,6 @@ description = "This package provides 29 stemmers for 28 languages generated from 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"}, @@ -2414,7 +2257,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"}, @@ -2427,7 +2269,6 @@ description = "Python documentation generator" optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "sphinx-7.3.7-py3-none-any.whl", hash = "sha256:413f75440be4cacf328f580b4274ada4565fb2187d696a84970c23f77b64d8c3"}, {file = "sphinx-7.3.7.tar.gz", hash = "sha256:a4a7db75ed37531c05002d56ed6948d4c42f473a36f46e1382b0bd76ca9627bc"}, @@ -2464,7 +2305,6 @@ description = "Type hints (PEP 484) support for the Sphinx autodoc extension" optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {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"}, @@ -2485,7 +2325,6 @@ description = "Patches Jinja2 v3 to restore compatibility with earlier Sphinx ve 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"}, @@ -2503,7 +2342,6 @@ description = "Sphinx directive to add unselectable prompt" optional = false python-versions = ">=3.9,<4.0" groups = ["dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "sphinx_prompt-1.8.0-py3-none-any.whl", hash = "sha256:369ecc633f0711886f9b3a078c83264245be1adf46abeeb9b88b5519e4b51007"}, {file = "sphinx_prompt-1.8.0.tar.gz", hash = "sha256:47482f86fcec29662fdfd23e7c04ef03582714195d01f5d565403320084372ed"}, @@ -2521,7 +2359,6 @@ 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"}, @@ -2542,7 +2379,6 @@ 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"}, @@ -2564,7 +2400,6 @@ 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"}, @@ -2600,7 +2435,6 @@ description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"}, {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"}, @@ -2618,7 +2452,6 @@ description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"}, {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"}, @@ -2636,7 +2469,6 @@ description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML h optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"}, {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"}, @@ -2654,7 +2486,6 @@ 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"}, @@ -2670,7 +2501,6 @@ description = "A sphinx extension which renders display math in HTML via JavaScr 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"}, @@ -2686,7 +2516,6 @@ description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp d optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"}, {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"}, @@ -2704,7 +2533,6 @@ description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}, {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, @@ -2735,7 +2563,6 @@ 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"}, ] @@ -2747,7 +2574,6 @@ 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"}, @@ -2806,7 +2632,6 @@ 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"}, @@ -2824,7 +2649,7 @@ virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2, [package.extras] docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] -testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pathlib2 (>=2.3.3)", "psutil (>=5.6.1)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)"] +testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pathlib2 (>=2.3.3) ; python_version < \"3.4\"", "psutil (>=5.6.1) ; platform_python_implementation == \"cpython\"", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)"] [[package]] name = "twined" @@ -2833,7 +2658,6 @@ description = "A library to help digital twins and data services talk to one ano 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"}, @@ -2850,7 +2674,6 @@ 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"}, @@ -2863,7 +2686,6 @@ 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"}, @@ -2876,7 +2698,6 @@ 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"}, @@ -2895,14 +2716,13 @@ description = "HTTP library with thread-safe connection pooling, file post, and optional = false python-versions = ">=3.9" groups = ["main", "dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] @@ -2914,7 +2734,6 @@ 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.1-py3-none-any.whl", hash = "sha256:412773c85d4dab0409b83ec36f7a6499e72eaf08c80e81e9576bca61831c71cb"}, {file = "virtualenv-20.28.1.tar.gz", hash = "sha256:5d34ab240fdb5d21549b76f9e8ff3af28252f5499fb6d6f031adac4e5a8c5329"}, @@ -2927,7 +2746,7 @@ platformdirs = ">=3.9.1,<5" [package.extras] docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] [[package]] name = "webencodings" @@ -2936,31 +2755,11 @@ 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"}, ] -[[package]] -name = "werkzeug" -version = "3.1.3" -description = "The comprehensive WSGI web application library." -optional = false -python-versions = ">=3.9" -groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" -files = [ - {file = "werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e"}, - {file = "werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746"}, -] - -[package.dependencies] -MarkupSafe = ">=2.1.1" - -[package.extras] -watchdog = ["watchdog (>=2.3)"] - [[package]] name = "wrapt" version = "1.17.0" @@ -2968,7 +2767,6 @@ 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"}, @@ -3044,18 +2842,17 @@ description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.9" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] 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 = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +test = ["big-O", "importlib-resources ; python_version < \"3.9\"", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] type = ["pytest-mypy"] [extras] @@ -3064,4 +2861,4 @@ hdf5 = ["h5py"] [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "a40029a7ca7b9d45458ae24c22a9c1e6c15d1d9f35cfd2a81e4b142be244968f" +content-hash = "f5f9a73e20b613a5064ff065a5f314e9d228191730139f3ffdc5d476987dcd79" diff --git a/pyproject.toml b/pyproject.toml index 28ca8d08e..30f74ea70 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,13 +24,11 @@ classifiers = [ python = "^3.10" click = ">=7,<9" coolname = "^2" -Flask = "^2" google-auth = ">=1.27.0,<3" google-cloud-pubsub = "^2.5" google-cloud-secret-manager = "^2.20" google-cloud-storage = ">=1.35.1, <3" google-crc32c = "^1.1" -gunicorn = "^22" python-dateutil = "^2.8" pyyaml = "^6" h5py = { version = "^3.6", optional = true }