diff --git a/poetry.lock b/poetry.lock index 1a143bf..8e020dc 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. [[package]] name = "allure-pytest" @@ -81,14 +81,26 @@ trio = ["trio (>=0.31.0) ; python_version < \"3.10\"", "trio (>=0.32.0) ; python [[package]] name = "attrs" -version = "25.4.0" +version = "26.1.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.9" groups = ["integration"] files = [ - {file = "attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373"}, - {file = "attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11"}, + {file = "attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309"}, + {file = "attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32"}, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.7" +groups = ["integration"] +files = [ + {file = "certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa"}, + {file = "certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7"}, ] [[package]] @@ -191,14 +203,14 @@ pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} [[package]] name = "charmlibs-interfaces-tls-certificates" -version = "1.8.0" +version = "1.8.1" description = "The charmlibs.interfaces.tls_certificates package." optional = false python-versions = ">=3.10" groups = ["main", "integration"] files = [ - {file = "charmlibs_interfaces_tls_certificates-1.8.0-py3-none-any.whl", hash = "sha256:c2d1d0f1fd9dfefcbdb078c810190e18710e04de4faf58139a9290689ad944fc"}, - {file = "charmlibs_interfaces_tls_certificates-1.8.0.tar.gz", hash = "sha256:ed29003ecd0b83e6b036936ba4c23bb9eb623a89c66096438b2c017cef7d8e3a"}, + {file = "charmlibs_interfaces_tls_certificates-1.8.1-py3-none-any.whl", hash = "sha256:8e8fe047e02515d76f57a1d019056d72ce8c859c2ffb39a1e379cfc11fc048e6"}, + {file = "charmlibs_interfaces_tls_certificates-1.8.1.tar.gz", hash = "sha256:f2bfabf3a3b4c18034941771733177b30e4742c06d7742d4bb30da6ead953f43"}, ] [package.dependencies] @@ -236,16 +248,155 @@ files = [ [package.dependencies] opentelemetry-api = "*" +[[package]] +name = "charset-normalizer" +version = "3.4.6" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7" +groups = ["integration"] +files = [ + {file = "charset_normalizer-3.4.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2e1d8ca8611099001949d1cdfaefc510cf0f212484fe7c565f735b68c78c3c95"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e25369dc110d58ddf29b949377a93e0716d72a24f62bad72b2b39f155949c1fd"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:259695e2ccc253feb2a016303543d691825e920917e31f894ca1a687982b1de4"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dda86aba335c902b6149a02a55b38e96287157e609200811837678214ba2b1db"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51fb3c322c81d20567019778cb5a4a6f2dc1c200b886bc0d636238e364848c89"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:4482481cb0572180b6fd976a4d5c72a30263e98564da68b86ec91f0fe35e8565"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39f5068d35621da2881271e5c3205125cc456f54e9030d3f723288c873a71bf9"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8bea55c4eef25b0b19a0337dc4e3f9a15b00d569c77211fa8cde38684f234fb7"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f0cdaecd4c953bfae0b6bb64910aaaca5a424ad9c72d85cb88417bb9814f7550"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:150b8ce8e830eb7ccb029ec9ca36022f756986aaaa7956aad6d9ec90089338c0"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e68c14b04827dd76dcbd1aeea9e604e3e4b78322d8faf2f8132c7138efa340a8"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:3778fd7d7cd04ae8f54651f4a7a0bd6e39a0cf20f801720a4c21d80e9b7ad6b0"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dad6e0f2e481fffdcf776d10ebee25e0ef89f16d691f1e5dee4b586375fdc64b"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-win32.whl", hash = "sha256:74a2e659c7ecbc73562e2a15e05039f1e22c75b7c7618b4b574a3ea9118d1557"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-win_amd64.whl", hash = "sha256:aa9cccf4a44b9b62d8ba8b4dd06c649ba683e4bf04eea606d2e94cfc2d6ff4d6"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-win_arm64.whl", hash = "sha256:e985a16ff513596f217cee86c21371b8cd011c0f6f056d0920aa2d926c544058"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:82060f995ab5003a2d6e0f4ad29065b7672b6593c8c63559beefe5b443242c3e"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60c74963d8350241a79cb8feea80e54d518f72c26db618862a8f53e5023deaf9"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6e4333fb15c83f7d1482a76d45a0818897b3d33f00efd215528ff7c51b8e35d"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bc72863f4d9aba2e8fd9085e63548a324ba706d2ea2c83b260da08a59b9482de"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9cc4fc6c196d6a8b76629a70ddfcd4635a6898756e2d9cac5565cf0654605d73"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:0c173ce3a681f309f31b87125fecec7a5d1347261ea11ebbb856fa6006b23c8c"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c907cdc8109f6c619e6254212e794d6548373cc40e1ec75e6e3823d9135d29cc"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:404a1e552cf5b675a87f0651f8b79f5f1e6fd100ee88dc612f89aa16abd4486f"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e3c701e954abf6fc03a49f7c579cc80c2c6cc52525340ca3186c41d3f33482ef"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7a6967aaf043bceabab5412ed6bd6bd26603dae84d5cb75bf8d9a74a4959d398"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5feb91325bbceade6afab43eb3b508c63ee53579fe896c77137ded51c6b6958e"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f820f24b09e3e779fe84c3c456cb4108a7aa639b0d1f02c28046e11bfcd088ed"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b35b200d6a71b9839a46b9b7fff66b6638bb52fc9658aa58796b0326595d3021"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-win32.whl", hash = "sha256:9ca4c0b502ab399ef89248a2c84c54954f77a070f28e546a85e91da627d1301e"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:a9e68c9d88823b274cf1e72f28cb5dc89c990edf430b0bfd3e2fb0785bfeabf4"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:97d0235baafca5f2b09cf332cc275f021e694e8362c6bb9c96fc9a0eb74fc316"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-win32.whl", hash = "sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-win32.whl", hash = "sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-win32.whl", hash = "sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-win32.whl", hash = "sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:659a1e1b500fac8f2779dd9e1570464e012f43e580371470b45277a27baa7532"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f61aa92e4aad0be58eb6eb4e0c21acf32cf8065f4b2cae5665da756c4ceef982"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f50498891691e0864dc3da965f340fada0771f6142a378083dc4608f4ea513e2"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bf625105bb9eef28a56a943fec8c8a98aeb80e7d7db99bd3c388137e6eb2d237"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2bd9d128ef93637a5d7a6af25363cf5dec3fa21cf80e68055aad627f280e8afa"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux_2_31_armv7l.whl", hash = "sha256:d08ec48f0a1c48d75d0356cea971921848fb620fdeba805b28f937e90691209f"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ed80ff870ca6de33f4d953fda4d55654b9a2b340ff39ab32fa3adbcd718f264"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f98059e4fcd3e3e4e2d632b7cf81c2faae96c43c60b569e9c621468082f1d104"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:ab30e5e3e706e3063bc6de96b118688cb10396b70bb9864a430f67df98c61ecc"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:d5f5d1e9def3405f60e3ca8232d56f35c98fb7bf581efcc60051ebf53cb8b611"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:461598cd852bfa5a61b09cae2b1c02e2efcd166ee5516e243d540ac24bfa68a7"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:71be7e0e01753a89cf024abf7ecb6bca2c81738ead80d43004d9b5e3f1244e64"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:df01808ee470038c3f8dc4f48620df7225c49c2d6639e38f96e6d6ac6e6f7b0e"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-win32.whl", hash = "sha256:69dd852c2f0ad631b8b60cfbe25a28c0058a894de5abb566619c205ce0550eae"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-win_amd64.whl", hash = "sha256:517ad0e93394ac532745129ceabdf2696b609ec9f87863d337140317ebce1c14"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:31215157227939b4fb3d740cd23fe27be0439afef67b785a1eb78a3ae69cba9e"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecbbd45615a6885fe3240eb9db73b9e62518b611850fdf8ab08bd56de7ad2b17"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c45a03a4c69820a399f1dda9e1d8fbf3562eda46e7720458180302021b08f778"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e8aeb10fcbe92767f0fa69ad5a72deca50d0dca07fbde97848997d778a50c9fe"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54fae94be3d75f3e573c9a1b5402dc593de19377013c9a0e4285e3d402dd3a2a"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:2f7fdd9b6e6c529d6a2501a2d36b240109e78a8ceaef5687cfcfa2bbe671d297"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1d02209e06550bdaef34af58e041ad71b88e624f5d825519da3a3308e22687"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8bc5f0687d796c05b1e28ab0d38a50e6309906ee09375dd3aff6a9c09dd6e8f4"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ee4ec14bc1680d6b0afab9aea2ef27e26d2024f18b24a2d7155a52b60da7e833"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:d1a2ee9c1499fc8f86f4521f27a973c914b211ffa87322f4ee33bb35392da2c5"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:48696db7f18afb80a068821504296eb0787d9ce239b91ca15059d1d3eaacf13b"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4f41da960b196ea355357285ad1316a00099f22d0929fe168343b99b254729c9"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:802168e03fba8bbc5ce0d866d589e4b1ca751d06edee69f7f3a19c5a9fe6b597"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-win32.whl", hash = "sha256:8761ac29b6c81574724322a554605608a9960769ea83d2c73e396f3df896ad54"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-win_amd64.whl", hash = "sha256:1cf0a70018692f85172348fe06d3a4b63f94ecb055e13a00c644d368eb82e5b8"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-win_arm64.whl", hash = "sha256:3516bbb8d42169de9e61b8520cbeeeb716f12f4ecfe3fd30a9919aa16c806ca8"}, + {file = "charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69"}, + {file = "charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6"}, +] + [[package]] name = "codespell" -version = "2.4.1" +version = "2.4.2" description = "Fix common misspellings in text files" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["lint"] files = [ - {file = "codespell-2.4.1-py3-none-any.whl", hash = "sha256:3dadafa67df7e4a3dbf51e0d7315061b80d265f9552ebd699b3dd6834b47e425"}, - {file = "codespell-2.4.1.tar.gz", hash = "sha256:299fcdcb09d23e81e35a671bbe746d5ad7e8385972e65dbb833a2eaac33c01e5"}, + {file = "codespell-2.4.2-py3-none-any.whl", hash = "sha256:97e0c1060cf46bd1d5db89a936c98db8c2b804e1fdd4b5c645e82a1ec6b1f886"}, + {file = "codespell-2.4.2.tar.gz", hash = "sha256:3c33be9ae34543807f088aeb4832dfad8cb2dae38da61cac0a7045dd376cfdf3"}, ] [package.extras] @@ -269,118 +420,118 @@ files = [ [[package]] name = "coverage" -version = "7.13.4" +version = "7.13.5" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.10" groups = ["unit"] files = [ - {file = "coverage-7.13.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fc31c787a84f8cd6027eba44010517020e0d18487064cd3d8968941856d1415"}, - {file = "coverage-7.13.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a32ebc02a1805adf637fc8dec324b5cdacd2e493515424f70ee33799573d661b"}, - {file = "coverage-7.13.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e24f9156097ff9dc286f2f913df3a7f63c0e333dcafa3c196f2c18b4175ca09a"}, - {file = "coverage-7.13.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8041b6c5bfdc03257666e9881d33b1abc88daccaf73f7b6340fb7946655cd10f"}, - {file = "coverage-7.13.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a09cfa6a5862bc2fc6ca7c3def5b2926194a56b8ab78ffcf617d28911123012"}, - {file = "coverage-7.13.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:296f8b0af861d3970c2a4d8c91d48eb4dd4771bcef9baedec6a9b515d7de3def"}, - {file = "coverage-7.13.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e101609bcbbfb04605ea1027b10dc3735c094d12d40826a60f897b98b1c30256"}, - {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aa3feb8db2e87ff5e6d00d7e1480ae241876286691265657b500886c98f38bda"}, - {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4fc7fa81bbaf5a02801b65346c8b3e657f1d93763e58c0abdf7c992addd81a92"}, - {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:33901f604424145c6e9c2398684b92e176c0b12df77d52db81c20abd48c3794c"}, - {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:bb28c0f2cf2782508a40cec377935829d5fcc3ad9a3681375af4e84eb34b6b58"}, - {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d107aff57a83222ddbd8d9ee705ede2af2cc926608b57abed8ef96b50b7e8f9"}, - {file = "coverage-7.13.4-cp310-cp310-win32.whl", hash = "sha256:a6f94a7d00eb18f1b6d403c91a88fd58cfc92d4b16080dfdb774afc8294469bf"}, - {file = "coverage-7.13.4-cp310-cp310-win_amd64.whl", hash = "sha256:2cb0f1e000ebc419632bbe04366a8990b6e32c4e0b51543a6484ffe15eaeda95"}, - {file = "coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053"}, - {file = "coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11"}, - {file = "coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa"}, - {file = "coverage-7.13.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7"}, - {file = "coverage-7.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00"}, - {file = "coverage-7.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef"}, - {file = "coverage-7.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903"}, - {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f"}, - {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299"}, - {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505"}, - {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6"}, - {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9"}, - {file = "coverage-7.13.4-cp311-cp311-win32.whl", hash = "sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9"}, - {file = "coverage-7.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f"}, - {file = "coverage-7.13.4-cp311-cp311-win_arm64.whl", hash = "sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f"}, - {file = "coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459"}, - {file = "coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3"}, - {file = "coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634"}, - {file = "coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3"}, - {file = "coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa"}, - {file = "coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3"}, - {file = "coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a"}, - {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7"}, - {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc"}, - {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47"}, - {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985"}, - {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0"}, - {file = "coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246"}, - {file = "coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126"}, - {file = "coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d"}, - {file = "coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9"}, - {file = "coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac"}, - {file = "coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea"}, - {file = "coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b"}, - {file = "coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525"}, - {file = "coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242"}, - {file = "coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148"}, - {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a"}, - {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23"}, - {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80"}, - {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea"}, - {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a"}, - {file = "coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d"}, - {file = "coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd"}, - {file = "coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af"}, - {file = "coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d"}, - {file = "coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12"}, - {file = "coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b"}, - {file = "coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9"}, - {file = "coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092"}, - {file = "coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9"}, - {file = "coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26"}, - {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2"}, - {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940"}, - {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c"}, - {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0"}, - {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b"}, - {file = "coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9"}, - {file = "coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd"}, - {file = "coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997"}, - {file = "coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601"}, - {file = "coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689"}, - {file = "coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c"}, - {file = "coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129"}, - {file = "coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552"}, - {file = "coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a"}, - {file = "coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356"}, - {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71"}, - {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5"}, - {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98"}, - {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5"}, - {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0"}, - {file = "coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb"}, - {file = "coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505"}, - {file = "coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2"}, - {file = "coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056"}, - {file = "coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc"}, - {file = "coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9"}, - {file = "coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf"}, - {file = "coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55"}, - {file = "coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72"}, - {file = "coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a"}, - {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6"}, - {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3"}, - {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750"}, - {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39"}, - {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0"}, - {file = "coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea"}, - {file = "coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932"}, - {file = "coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b"}, - {file = "coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0"}, - {file = "coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91"}, + {file = "coverage-7.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0723d2c96324561b9aa76fb982406e11d93cdb388a7a7da2b16e04719cf7ca5"}, + {file = "coverage-7.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52f444e86475992506b32d4e5ca55c24fc88d73bcbda0e9745095b28ef4dc0cf"}, + {file = "coverage-7.13.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:704de6328e3d612a8f6c07000a878ff38181ec3263d5a11da1db294fa6a9bdf8"}, + {file = "coverage-7.13.5-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a1a6d79a14e1ec1832cabc833898636ad5f3754a678ef8bb4908515208bf84f4"}, + {file = "coverage-7.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79060214983769c7ba3f0cee10b54c97609dca4d478fa1aa32b914480fd5738d"}, + {file = "coverage-7.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:356e76b46783a98c2a2fe81ec79df4883a1e62895ea952968fb253c114e7f930"}, + {file = "coverage-7.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0cef0cdec915d11254a7f549c1170afecce708d30610c6abdded1f74e581666d"}, + {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dc022073d063b25a402454e5712ef9e007113e3a676b96c5f29b2bda29352f40"}, + {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9b74db26dfea4f4e50d48a4602207cd1e78be33182bc9cbf22da94f332f99878"}, + {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ad146744ca4fd09b50c482650e3c1b1f4dfa1d4792e0a04a369c7f23336f0400"}, + {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c555b48be1853fe3997c11c4bd521cdd9a9612352de01fa4508f16ec341e6fe0"}, + {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7034b5c56a58ae5e85f23949d52c14aca2cfc6848a31764995b7de88f13a1ea0"}, + {file = "coverage-7.13.5-cp310-cp310-win32.whl", hash = "sha256:eb7fdf1ef130660e7415e0253a01a7d5a88c9c4d158bcf75cbbd922fd65a5b58"}, + {file = "coverage-7.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:3e1bb5f6c78feeb1be3475789b14a0f0a5b47d505bfc7267126ccbd50289999e"}, + {file = "coverage-7.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66a80c616f80181f4d643b0f9e709d97bcea413ecd9631e1dedc7401c8e6695d"}, + {file = "coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587"}, + {file = "coverage-7.13.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0672854dc733c342fa3e957e0605256d2bf5934feeac328da9e0b5449634a642"}, + {file = "coverage-7.13.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec10e2a42b41c923c2209b846126c6582db5e43a33157e9870ba9fb70dc7854b"}, + {file = "coverage-7.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be3d4bbad9d4b037791794ddeedd7d64a56f5933a2c1373e18e9e568b9141686"}, + {file = "coverage-7.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d2afbc5cc54d286bfb54541aa50b64cdb07a718227168c87b9e2fb8f25e1743"}, + {file = "coverage-7.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ad050321264c49c2fa67bb599100456fc51d004b82534f379d16445da40fb75"}, + {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7300c8a6d13335b29bb76d7651c66af6bd8658517c43499f110ddc6717bfc209"}, + {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eb07647a5738b89baab047f14edd18ded523de60f3b30e75c2acc826f79c839a"}, + {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9adb6688e3b53adffefd4a52d72cbd8b02602bfb8f74dcd862337182fd4d1a4e"}, + {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c8d4bc913dd70b93488d6c496c77f3aff5ea99a07e36a18f865bca55adef8bd"}, + {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e3c426ffc4cd952f54ee9ffbdd10345709ecc78a3ecfd796a57236bfad0b9b8"}, + {file = "coverage-7.13.5-cp311-cp311-win32.whl", hash = "sha256:259b69bb83ad9894c4b25be2528139eecba9a82646ebdda2d9db1ba28424a6bf"}, + {file = "coverage-7.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:258354455f4e86e3e9d0d17571d522e13b4e1e19bf0f8596bcf9476d61e7d8a9"}, + {file = "coverage-7.13.5-cp311-cp311-win_arm64.whl", hash = "sha256:bff95879c33ec8da99fc9b6fe345ddb5be6414b41d6d1ad1c8f188d26f36e028"}, + {file = "coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01"}, + {file = "coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422"}, + {file = "coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f"}, + {file = "coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5"}, + {file = "coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376"}, + {file = "coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256"}, + {file = "coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c"}, + {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5"}, + {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09"}, + {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9"}, + {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf"}, + {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c"}, + {file = "coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf"}, + {file = "coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810"}, + {file = "coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de"}, + {file = "coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1"}, + {file = "coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3"}, + {file = "coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26"}, + {file = "coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3"}, + {file = "coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b"}, + {file = "coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a"}, + {file = "coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969"}, + {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161"}, + {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15"}, + {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1"}, + {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6"}, + {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17"}, + {file = "coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85"}, + {file = "coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b"}, + {file = "coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664"}, + {file = "coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d"}, + {file = "coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0"}, + {file = "coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806"}, + {file = "coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3"}, + {file = "coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9"}, + {file = "coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd"}, + {file = "coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606"}, + {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e"}, + {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0"}, + {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87"}, + {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479"}, + {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2"}, + {file = "coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a"}, + {file = "coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819"}, + {file = "coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911"}, + {file = "coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f"}, + {file = "coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e"}, + {file = "coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a"}, + {file = "coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510"}, + {file = "coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247"}, + {file = "coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6"}, + {file = "coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0"}, + {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882"}, + {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740"}, + {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16"}, + {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0"}, + {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0"}, + {file = "coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc"}, + {file = "coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633"}, + {file = "coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8"}, + {file = "coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b"}, + {file = "coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c"}, + {file = "coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9"}, + {file = "coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29"}, + {file = "coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607"}, + {file = "coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90"}, + {file = "coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3"}, + {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab"}, + {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562"}, + {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2"}, + {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea"}, + {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a"}, + {file = "coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215"}, + {file = "coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43"}, + {file = "coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45"}, + {file = "coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61"}, + {file = "coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179"}, ] [package.extras] @@ -460,24 +611,36 @@ test-randomorder = ["pytest-randomly"] [[package]] name = "data-platform-helpers" -version = "0.1.7" +version = "1.0.1" description = "" optional = false python-versions = "<4.0,>=3.10" groups = ["main", "integration"] files = [ - {file = "data_platform_helpers-0.1.7-py3-none-any.whl", hash = "sha256:cf01caef414a4c07911ecf2c009c19b8f69cae154b540dd578ddbe2af8fd3b98"}, - {file = "data_platform_helpers-0.1.7.tar.gz", hash = "sha256:04445f3f4309730bb1c569a04464caeb27390b0d6e07829d09dacd8882fcfcd1"}, + {file = "data_platform_helpers-1.0.1-py3-none-any.whl", hash = "sha256:21ef9c3cc62c19648a1ca742e962d1a3ed361dd97bf60f4c20e3d16228df74d4"}, + {file = "data_platform_helpers-1.0.1.tar.gz", hash = "sha256:eb1d8350dc4a6c3670237dfaec3c53433ff42c757cfba3bed92e35c2779a7447"}, ] [package.dependencies] -ops = ">=2.15.0" -pydantic = ">=2.0" +ops = ">=3,<4" +pydantic = ">=2.11,<3" rich = "*" [package.extras] -all = ["pytest_operator (==0.36.0)"] -tests = ["pytest_operator (==0.36.0)"] +all = ["pytest-operator"] +tests = ["pytest-operator"] + +[[package]] +name = "durationpy" +version = "0.10" +description = "Module for converting between datetime.timedelta and Go's Duration strings." +optional = false +python-versions = "*" +groups = ["integration"] +files = [ + {file = "durationpy-0.10-py3-none-any.whl", hash = "sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286"}, + {file = "durationpy-0.10.tar.gz", hash = "sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba"}, +] [[package]] name = "idna" @@ -545,6 +708,33 @@ files = [ [package.dependencies] PyYAML = "==6.*" +[[package]] +name = "kubernetes" +version = "35.0.0" +description = "Kubernetes python client" +optional = false +python-versions = ">=3.6" +groups = ["integration"] +files = [ + {file = "kubernetes-35.0.0-py2.py3-none-any.whl", hash = "sha256:39e2b33b46e5834ef6c3985ebfe2047ab39135d41de51ce7641a7ca5b372a13d"}, + {file = "kubernetes-35.0.0.tar.gz", hash = "sha256:3d00d344944239821458b9efd484d6df9f011da367ecb155dadf9513f05f09ee"}, +] + +[package.dependencies] +certifi = ">=14.05.14" +durationpy = ">=0.7" +python-dateutil = ">=2.5.3" +pyyaml = ">=5.4.1" +requests = "*" +requests-oauthlib = "*" +six = ">=1.9.0" +urllib3 = ">=1.24.2,<2.6.0 || >2.6.0" +websocket-client = ">=0.32.0,<0.40.0 || >0.40.0,<0.41.dev0 || >=0.43.dev0" + +[package.extras] +adal = ["adal (>=1.0.2)"] +google-auth = ["google-auth (>=1.0.1)"] + [[package]] name = "markdown-it-py" version = "4.0.0" @@ -581,16 +771,33 @@ files = [ {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] +[[package]] +name = "oauthlib" +version = "3.3.1" +description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" +optional = false +python-versions = ">=3.8" +groups = ["integration"] +files = [ + {file = "oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1"}, + {file = "oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9"}, +] + +[package.extras] +rsa = ["cryptography (>=3.0.0)"] +signals = ["blinker (>=1.4.0)"] +signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] + [[package]] name = "opentelemetry-api" -version = "1.39.1" +version = "1.40.0" description = "OpenTelemetry Python API" optional = false python-versions = ">=3.9" groups = ["main", "charm-libs", "integration", "unit"] files = [ - {file = "opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950"}, - {file = "opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c"}, + {file = "opentelemetry_api-1.40.0-py3-none-any.whl", hash = "sha256:82dd69331ae74b06f6a874704be0cfaa49a1650e1537d4a813b86ecef7d0ecf9"}, + {file = "opentelemetry_api-1.40.0.tar.gz", hash = "sha256:159be641c0b04d11e9ecd576906462773eb97ae1b657730f0ecf64d32071569f"}, ] [package.dependencies] @@ -680,22 +887,20 @@ testing = ["coverage", "pytest", "pytest-benchmark"] [[package]] name = "protobuf" -version = "6.33.5" +version = "7.34.1" description = "" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["integration"] files = [ - {file = "protobuf-6.33.5-cp310-abi3-win32.whl", hash = "sha256:d71b040839446bac0f4d162e758bea99c8251161dae9d0983a3b88dee345153b"}, - {file = "protobuf-6.33.5-cp310-abi3-win_amd64.whl", hash = "sha256:3093804752167bcab3998bec9f1048baae6e29505adaf1afd14a37bddede533c"}, - {file = "protobuf-6.33.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a5cb85982d95d906df1e2210e58f8e4f1e3cdc088e52c921a041f9c9a0386de5"}, - {file = "protobuf-6.33.5-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190"}, - {file = "protobuf-6.33.5-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd"}, - {file = "protobuf-6.33.5-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0"}, - {file = "protobuf-6.33.5-cp39-cp39-win32.whl", hash = "sha256:a3157e62729aafb8df6da2c03aa5c0937c7266c626ce11a278b6eb7963c4e37c"}, - {file = "protobuf-6.33.5-cp39-cp39-win_amd64.whl", hash = "sha256:8f04fa32763dcdb4973d537d6b54e615cc61108c7cb38fe59310c3192d29510a"}, - {file = "protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02"}, - {file = "protobuf-6.33.5.tar.gz", hash = "sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c"}, + {file = "protobuf-7.34.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:d8b2cc79c4d8f62b293ad9b11ec3aebce9af481fa73e64556969f7345ebf9fc7"}, + {file = "protobuf-7.34.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:5185e0e948d07abe94bb76ec9b8416b604cfe5da6f871d67aad30cbf24c3110b"}, + {file = "protobuf-7.34.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:403b093a6e28a960372b44e5eb081775c9b056e816a8029c61231743d63f881a"}, + {file = "protobuf-7.34.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:8ff40ce8cd688f7265326b38d5a1bed9bfdf5e6723d49961432f83e21d5713e4"}, + {file = "protobuf-7.34.1-cp310-abi3-win32.whl", hash = "sha256:34b84ce27680df7cca9f231043ada0daa55d0c44a2ddfaa58ec1d0d89d8bf60a"}, + {file = "protobuf-7.34.1-cp310-abi3-win_amd64.whl", hash = "sha256:e97b55646e6ce5cbb0954a8c28cd39a5869b59090dfaa7df4598a7fba869468c"}, + {file = "protobuf-7.34.1-py3-none-any.whl", hash = "sha256:bb3812cd53aefea2b028ef42bd780f5b96407247f20c6ef7c679807e9d188f11"}, + {file = "protobuf-7.34.1.tar.gz", hash = "sha256:9ce42245e704cc5027be797c1db1eb93184d44d1cdd71811fb2d9b25ad541280"}, ] [[package]] @@ -1040,6 +1245,47 @@ files = [ {file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"}, ] +[[package]] +name = "requests" +version = "2.32.5" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.9" +groups = ["integration"] +files = [ + {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, + {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset_normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +description = "OAuthlib authentication support for Requests." +optional = false +python-versions = ">=3.4" +groups = ["integration"] +files = [ + {file = "requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9"}, + {file = "requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36"}, +] + +[package.dependencies] +oauthlib = ">=3.0.0" +requests = ">=2.0.0" + +[package.extras] +rsa = ["oauthlib[signedtoken] (>=3.0.0)"] + [[package]] name = "rich" version = "14.3.3" @@ -1061,30 +1307,30 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" -version = "0.15.2" +version = "0.15.7" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" groups = ["format", "lint"] files = [ - {file = "ruff-0.15.2-py3-none-linux_armv6l.whl", hash = "sha256:120691a6fdae2f16d65435648160f5b81a9625288f75544dc40637436b5d3c0d"}, - {file = "ruff-0.15.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a89056d831256099658b6bba4037ac6dd06f49d194199215befe2bb10457ea5e"}, - {file = "ruff-0.15.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e36dee3a64be0ebd23c86ffa3aa3fd3ac9a712ff295e192243f814a830b6bd87"}, - {file = "ruff-0.15.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9fb47b6d9764677f8c0a193c0943ce9a05d6763523f132325af8a858eadc2b9"}, - {file = "ruff-0.15.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f376990f9d0d6442ea9014b19621d8f2aaf2b8e39fdbfc79220b7f0c596c9b80"}, - {file = "ruff-0.15.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2dcc987551952d73cbf5c88d9fdee815618d497e4df86cd4c4824cc59d5dd75f"}, - {file = "ruff-0.15.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42a47fd785cbe8c01b9ff45031af875d101b040ad8f4de7bbb716487c74c9a77"}, - {file = "ruff-0.15.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbe9f49354866e575b4c6943856989f966421870e85cd2ac94dccb0a9dcb2fea"}, - {file = "ruff-0.15.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7a672c82b5f9887576087d97be5ce439f04bbaf548ee987b92d3a7dede41d3a"}, - {file = "ruff-0.15.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ecc64f46f7019e2bcc3cdc05d4a7da958b629a5ab7033195e11a438403d956"}, - {file = "ruff-0.15.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:8dcf243b15b561c655c1ef2f2b0050e5d50db37fe90115507f6ff37d865dc8b4"}, - {file = "ruff-0.15.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dab6941c862c05739774677c6273166d2510d254dac0695c0e3f5efa1b5585de"}, - {file = "ruff-0.15.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b9164f57fc36058e9a6806eb92af185b0697c9fe4c7c52caa431c6554521e5c"}, - {file = "ruff-0.15.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:80d24fcae24d42659db7e335b9e1531697a7102c19185b8dc4a028b952865fd8"}, - {file = "ruff-0.15.2-py3-none-win32.whl", hash = "sha256:fd5ff9e5f519a7e1bd99cbe8daa324010a74f5e2ebc97c6242c08f26f3714f6f"}, - {file = "ruff-0.15.2-py3-none-win_amd64.whl", hash = "sha256:d20014e3dfa400f3ff84830dfb5755ece2de45ab62ecea4af6b7262d0fb4f7c5"}, - {file = "ruff-0.15.2-py3-none-win_arm64.whl", hash = "sha256:cabddc5822acdc8f7b5527b36ceac55cc51eec7b1946e60181de8fe83ca8876e"}, - {file = "ruff-0.15.2.tar.gz", hash = "sha256:14b965afee0969e68bb871eba625343b8673375f457af4abe98553e8bbb98342"}, + {file = "ruff-0.15.7-py3-none-linux_armv6l.whl", hash = "sha256:a81cc5b6910fb7dfc7c32d20652e50fa05963f6e13ead3c5915c41ac5d16668e"}, + {file = "ruff-0.15.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:722d165bd52403f3bdabc0ce9e41fc47070ac56d7a91b4e0d097b516a53a3477"}, + {file = "ruff-0.15.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7fbc2448094262552146cbe1b9643a92f66559d3761f1ad0656d4991491af49e"}, + {file = "ruff-0.15.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b39329b60eba44156d138275323cc726bbfbddcec3063da57caa8a8b1d50adf"}, + {file = "ruff-0.15.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87768c151808505f2bfc93ae44e5f9e7c8518943e5074f76ac21558ef5627c85"}, + {file = "ruff-0.15.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb0511670002c6c529ec66c0e30641c976c8963de26a113f3a30456b702468b0"}, + {file = "ruff-0.15.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0d19644f801849229db8345180a71bee5407b429dd217f853ec515e968a6912"}, + {file = "ruff-0.15.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4806d8e09ef5e84eb19ba833d0442f7e300b23fe3f0981cae159a248a10f0036"}, + {file = "ruff-0.15.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dce0896488562f09a27b9c91b1f58a097457143931f3c4d519690dea54e624c5"}, + {file = "ruff-0.15.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1852ce241d2bc89e5dc823e03cff4ce73d816b5c6cdadd27dbfe7b03217d2a12"}, + {file = "ruff-0.15.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5f3e4b221fb4bd293f79912fc5e93a9063ebd6d0dcbd528f91b89172a9b8436c"}, + {file = "ruff-0.15.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b15e48602c9c1d9bdc504b472e90b90c97dc7d46c7028011ae67f3861ceba7b4"}, + {file = "ruff-0.15.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b4705e0e85cedc74b0a23cf6a179dbb3df184cb227761979cc76c0440b5ab0d"}, + {file = "ruff-0.15.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:112c1fa316a558bb34319282c1200a8bf0495f1b735aeb78bfcb2991e6087580"}, + {file = "ruff-0.15.7-py3-none-win32.whl", hash = "sha256:6d39e2d3505b082323352f733599f28169d12e891f7dd407f2d4f54b4c2886de"}, + {file = "ruff-0.15.7-py3-none-win_amd64.whl", hash = "sha256:4d53d712ddebcd7dace1bc395367aec12c057aacfe9adbb6d832302575f4d3a1"}, + {file = "ruff-0.15.7-py3-none-win_arm64.whl", hash = "sha256:18e8d73f1c3fdf27931497972250340f92e8c861722161a9caeb89a58ead6ed2"}, + {file = "ruff-0.15.7.tar.gz", hash = "sha256:04f1ae61fc20fe0b148617c324d9d009b5f63412c0b16474f3d5f1a1a665f7ac"}, ] [[package]] @@ -1169,6 +1415,24 @@ files = [ [package.dependencies] typing-extensions = ">=4.12.0" +[[package]] +name = "urllib3" +version = "2.6.3" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.9" +groups = ["integration"] +files = [ + {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, + {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, +] + +[package.extras] +brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] + [[package]] name = "valkey-glide" version = "0.0.0" @@ -1231,4 +1495,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = "^3.12" -content-hash = "9085ffa9d77cbcb0ab1d8858dfdae18db329647e3036be7792c3abb94403dc95" +content-hash = "56113586f9c112fad2330adf05308bbed41a7240ecc26800a1164c314e883f82" diff --git a/pyproject.toml b/pyproject.toml index 4a04e78..6268e22 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,6 +55,7 @@ python-dateutil = "*" tenacity = "^9.1.2" # https://github.com/valkey-io/valkey-glide/pull/5124 not yet released valkey-glide = { git = "https://github.com/skourta/valkey-glide", subdirectory = "python/glide-async", branch = "add-build-rs-to-async-client" } +kubernetes = "^35.0.0" [tool.coverage.run] branch = true diff --git a/src/common/locks.py b/src/common/locks.py index 02e87e8..52721e2 100644 --- a/src/common/locks.py +++ b/src/common/locks.py @@ -4,6 +4,7 @@ """Collection of locks for cluster operations.""" import logging +import time from abc import abstractmethod from typing import TYPE_CHECKING, Protocol, override @@ -52,10 +53,16 @@ class DataBagLock(Lockable): unit_request_lock_atr_name: str member_with_lock_atr_name: str + lock_timestamp: str = "databaglock_timestamp" def __init__(self, state: "ClusterState") -> None: self.state = state + def __init_subclass__(cls) -> None: + """Initialize subclass attributes.""" + super().__init_subclass__() + cls.lock_timestamp = cls.__name__.lower() + "_timestamp" + @property def units_requesting_lock(self) -> list[str]: """Get the list of units requesting the start lock.""" @@ -68,6 +75,8 @@ def units_requesting_lock(self) -> list[str]: @property def next_unit_to_give_lock(self) -> str | None: """Get the next unit to give the start lock to.""" + if self.state.unit_server.model[self.unit_request_lock_atr_name]: + return self.state.unit_server.unit_name return self.units_requesting_lock[0] if self.units_requesting_lock else None @property @@ -98,11 +107,13 @@ def is_held_by_this_unit(self) -> bool: def request_lock(self) -> bool: """Request the lock for the local unit.""" - self.state.unit_server.update( - { - self.unit_request_lock_atr_name: True, - } - ) + if not self.state.unit_server.model[self.unit_request_lock_atr_name]: + self.state.unit_server.update( + { + self.unit_request_lock_atr_name: True, + self.lock_timestamp: time.time(), + } + ) if self.state.unit_server.unit.is_leader(): logger.info( "Leader unit requesting %s lock. Triggering lock request processing.", @@ -114,11 +125,13 @@ def request_lock(self) -> bool: def release_lock(self) -> bool: """Release the lock from the local unit.""" - self.state.unit_server.update( - { - self.unit_request_lock_atr_name: False, - } - ) + if self.state.unit_server.model[self.unit_request_lock_atr_name]: + self.state.unit_server.update( + { + self.unit_request_lock_atr_name: False, + self.lock_timestamp: time.time(), + } + ) if self.state.unit_server.unit.is_leader(): logger.info( "Leader unit releasing %s lock. Triggering lock request processing.", @@ -157,6 +170,24 @@ def is_lock_free_to_give(self) -> bool: not self.state.cluster.model.start_member or not starting_unit or starting_unit.is_started + or not starting_unit.model.request_start_lock + ) + + +class RestartLock(DataBagLock): + """Lock for restart operations.""" + + unit_request_lock_atr_name = "request_restart_lock" + member_with_lock_atr_name = "restart_member" + + @property + def is_lock_free_to_give(self) -> bool: + """Check if the unit with the restart lock has completed its operation.""" + restarting_unit = self.unit_with_lock + return ( + not self.state.cluster.model.restart_member + or not restarting_unit + or not restarting_unit.model.request_restart_lock ) diff --git a/src/core/models.py b/src/core/models.py index a1dc042..5299081 100644 --- a/src/core/models.py +++ b/src/core/models.py @@ -54,6 +54,7 @@ class PeerAppModel(PeerModel): charmed_sentinel_peers_password: InternalUsersSecret = Field(default="") charmed_sentinel_operator_password: InternalUsersSecret = Field(default="") start_member: str = Field(default="") + restart_member: str = Field(default="") internal_ca_certificate: InternalCertificatesSecret = Field(default="") internal_ca_private_key: InternalCertificatesSecret = Field(default="") tls_client_private_key: ExtraSecretStr = Field(default=None) @@ -67,6 +68,7 @@ class PeerUnitModel(PeerModel): hostname: str = Field(default="") private_ip: str = Field(default="") request_start_lock: bool = Field(default=False) + request_restart_lock: bool = Field(default=False) scale_down_state: str = Field(default="") tls_client_state: str = Field(default="") client_cert_ready: bool = Field(default=False) diff --git a/src/events/base_events.py b/src/events/base_events.py index e63e6c2..9f34558 100644 --- a/src/events/base_events.py +++ b/src/events/base_events.py @@ -19,7 +19,7 @@ ValkeyServicesFailedToStartError, ValkeyWorkloadCommandError, ) -from common.locks import ScaleDownLock, StartLock +from common.locks import RestartLock, ScaleDownLock, StartLock from literals import ( CLIENT_PORT, DATA_STORAGE, @@ -41,6 +41,29 @@ logger = logging.getLogger(__name__) +class RestartWorkloadEvent(ops.EventBase): + """Event for restarting the workload when certain events happen, e.g. IP change.""" + + def __init__( + self, handle: ops.Handle, restart_valkey: bool = True, restart_sentinel: bool = True + ): + super().__init__(handle) + self.restart_valkey = restart_valkey + self.restart_sentinel = restart_sentinel + + def snapshot(self) -> dict[str, str]: + """Save the state of the event.""" + return { + "restart_valkey": str(self.restart_valkey), + "restart_sentinel": str(self.restart_sentinel), + } + + def restore(self, snapshot: dict[str, str]) -> None: + """Restore the state of the event.""" + self.restart_valkey = snapshot.get("restart_valkey", "True") == "True" + self.restart_sentinel = snapshot.get("restart_sentinel", "True") == "True" + + class UnitFullyStarted(ops.EventBase): """Event that signals that the unit's has fully started. @@ -66,6 +89,7 @@ class BaseEvents(ops.Object): """Handle all base events.""" unit_fully_started = ops.EventSource(UnitFullyStarted) + restart_workload = ops.EventSource(RestartWorkloadEvent) def __init__(self, charm: "ValkeyCharm"): super().__init__(charm, key="base_events") @@ -81,6 +105,7 @@ def __init__(self, charm: "ValkeyCharm"): self.framework.observe(self.charm.on.config_changed, self._on_config_changed) self.framework.observe(self.charm.on.secret_changed, self._on_secret_changed) self.framework.observe(self.unit_fully_started, self._on_unit_fully_started) + self.framework.observe(self.restart_workload, self._on_restart_workload) self.framework.observe( self.charm.on[DATA_STORAGE].storage_detaching, self._on_storage_detaching ) @@ -230,7 +255,7 @@ def _on_peer_relation_changed(self, event: ops.RelationChangedEvent) -> None: if not self.charm.unit.is_leader(): return - for lock in [StartLock(self.charm.state)]: + for lock in [StartLock(self.charm.state), RestartLock(self.charm.state)]: lock.process() def _on_update_status(self, event: ops.UpdateStatusEvent) -> None: @@ -287,12 +312,15 @@ def _on_leader_elected(self, event: ops.LeaderElectedEvent) -> None: def _on_config_changed(self, event: ops.ConfigChangedEvent) -> None: """Handle the config_changed event.""" - self.charm.state.unit_server.update( - { - "hostname": self.charm.state.hostname, - "private_ip": self.charm.state.bind_address, - } - ) + # on k8s we use hostnames so we do not have to reconfigure on ip change + if ( + self.charm.state.unit_server.model.private_ip + and self.charm.state.bind_address != self.charm.state.unit_server.model.private_ip + and self.charm.state.substrate == Substrate.VM + ): + self.charm.config_manager.configure_services( + self.charm.sentinel_manager.get_primary_ip() + ) if not self.charm.unit.is_leader(): return @@ -524,3 +552,62 @@ def _set_state_for_going_away(self) -> None: ) self.charm.state.unit_server.update({"scale_down_state": ScaleDownState.GOING_AWAY}) + + def _on_restart_workload(self, event: RestartWorkloadEvent) -> None: + """Handle the restart_workload event.""" + logger.info( + "Restarting workload Event. Restart Valkey: %s, Restart Sentinel: %s", + event.restart_valkey, + event.restart_sentinel, + ) + restart_lock = RestartLock(self.charm.state) + restart_lock.request_lock() + if not restart_lock.is_held_by_this_unit: + logger.info("Waiting for lock to restart workload") + event.defer() + return + + try: + if event.restart_valkey: + self.charm.workload.restart(self.charm.workload.valkey_service) + if event.restart_sentinel: + self.charm.sentinel_manager.restart_service() + + if event.restart_valkey and not self.charm.cluster_manager.is_healthy( + check_replica_sync=False + ): + self.charm.status.set_running_status( + ClusterStatuses.VALKEY_UNHEALTHY_RESTART.value, + scope="unit", + component_name=self.charm.cluster_manager.name, + statuses_state=self.charm.state.statuses, + ) + event.defer() + return + + self.charm.state.statuses.delete( + ClusterStatuses.VALKEY_UNHEALTHY_RESTART.value, + scope="unit", + component=self.charm.cluster_manager.name, + ) + + if event.restart_sentinel and not self.charm.sentinel_manager.is_healthy(): + self.charm.status.set_running_status( + ClusterStatuses.SENTINEL_UNHEALTHY_RESTART.value, + scope="unit", + component_name=self.charm.cluster_manager.name, + statuses_state=self.charm.state.statuses, + ) + event.defer() + return + + self.charm.state.statuses.delete( + ClusterStatuses.SENTINEL_UNHEALTHY_RESTART.value, + scope="unit", + component=self.charm.cluster_manager.name, + ) + except ValkeyServicesFailedToStartError as e: + logger.error(e) + event.defer() + finally: + restart_lock.release_lock() diff --git a/src/events/tls.py b/src/events/tls.py index dc830bf..49d9801 100644 --- a/src/events/tls.py +++ b/src/events/tls.py @@ -25,6 +25,7 @@ CLIENT_TLS_RELATION_NAME, PEER_RELATION, TLS_CLIENT_PRIVATE_KEY_CONFIG, + Substrate, TLSCARotationState, TLSState, ) @@ -303,21 +304,6 @@ def _on_secret_changed(self, event: ops.SecretChangedEvent) -> None: if self.charm.state.client_tls_relation: self.refresh_tls_certificates_event.emit() - def _on_config_changed(self, event: ops.ConfigChangedEvent) -> None: - """Handle TLS related config changes.""" - if not (secret_id := self.charm.config.get(TLS_CLIENT_PRIVATE_KEY_CONFIG)): - return - - if not (private_key := self.charm.tls_manager.read_and_validate_private_key(secret_id)): - logger.error("Invalid private key provided, cannot update TLS certificates.") - return - - if self.charm.unit.is_leader(): - self.charm.state.cluster.update({"tls_client_private_key": private_key.raw}) - - if self.charm.state.client_tls_relation: - self.refresh_tls_certificates_event.emit() - def _enable_client_tls(self) -> None: """Check preconditions and enable TLS if possible.""" if not all(server.model.client_cert_ready for server in self.charm.state.servers): @@ -335,6 +321,39 @@ def _enable_client_tls(self) -> None: self.charm.cluster_manager.reload_tls_settings(tls_config) self.charm.sentinel_manager.restart_service() + def _on_config_changed(self, event: ops.ConfigChangedEvent) -> None: + """Handle the `config-changed` event.""" + if ( + (secret_id := self.charm.config.get(TLS_CLIENT_PRIVATE_KEY_CONFIG)) + and (private_key := self.charm.tls_manager.read_and_validate_private_key(secret_id)) + and self.charm.unit.is_leader() + ): + self.charm.state.cluster.update({"tls_client_private_key": private_key.raw}) + if self.charm.state.client_tls_relation: + self.refresh_tls_certificates_event.emit() + + if ( + self.charm.state.unit_server.model.private_ip + and self.charm.state.bind_address != self.charm.state.unit_server.model.private_ip + ): + if self.charm.tls_manager.certificate_sans_require_update(): + if self.charm.state.client_tls_relation: + self.charm.tls_events.refresh_tls_certificates_event.emit() + event.defer() + return + + self.charm.tls_manager.create_and_store_self_signed_certificate() + + self.charm.state.unit_server.update( + { + "hostname": self.charm.state.hostname, + "private_ip": self.charm.state.bind_address, + } + ) + # only restart on VM because on k8s the hostname is stable and does not change with IP changes + if self.charm.state.substrate == Substrate.VM: + self.charm.base_events.restart_workload.emit() + def _orchestrate_ca_rotation(self) -> None: """Orchestrate the workflow when a TLS CA rotation has been initiated.""" match self.charm.state.unit_server.tls_ca_rotation_state: diff --git a/src/managers/config.py b/src/managers/config.py index 9290c1d..1b5960f 100644 --- a/src/managers/config.py +++ b/src/managers/config.py @@ -78,7 +78,6 @@ def get_config_properties(self, primary_endpoint: str) -> dict[str, str]: config_properties["aclfile"] = self.workload.acl_file.as_posix() config_properties["dir"] = self.workload.working_dir.as_posix() - # bind to all interfaces config_properties["bind"] = self.state.endpoint # replica related config @@ -93,7 +92,7 @@ def get_config_properties(self, primary_endpoint: str) -> dict[str, str]: def _generate_replica_config(self, primary_endpoint: str) -> dict[str, str]: """Generate the config properties related to replica configuration based on the current cluster state.""" - local_unit_endpoint = self.state.unit_server.get_endpoint(self.state.substrate) + local_unit_endpoint = self.state.endpoint replica_config = { "primaryuser": CharmUsers.VALKEY_REPLICA.value, "primaryauth": self.state.cluster.internal_users_credentials.get( diff --git a/src/managers/tls.py b/src/managers/tls.py index a838bce..14f752d 100644 --- a/src/managers/tls.py +++ b/src/managers/tls.py @@ -304,6 +304,53 @@ def start_ca_rotation_if_required( ) return True + def get_current_sans(self) -> dict[str, set[str]]: + """Get the current SANs for a unit's cert.""" + cert_file = self.workload.tls_paths.client_cert + + sans_ip = set() + sans_dns = set() + if not ( + san_lines := self.workload.exec( + [ + "openssl", + "x509", + "-ext", + "subjectAltName", + "-noout", + "-in", + cert_file.as_posix(), + ] + )[0].splitlines() + ): + return {"sans_ip": sans_ip, "sans_dns": sans_dns} + + for line in san_lines: + for sans in line.split(", "): + san_type, san_value = sans.split(":") + + if san_type.strip() == "DNS": + sans_dns.add(san_value) + if san_type.strip() == "IP Address": + sans_ip.add(san_value) + + return {"sans_ip": sans_ip, "sans_dns": sans_dns} + + def certificate_sans_require_update(self) -> bool: + """Check current certificate sans and determine if certificate requires update. + + Returns: + bool: True if certificate sans have changed, False if they are still the same. + """ + current_sans = self.get_current_sans() + new_sans_ip = self.build_sans_ip() + new_sans_dns = self.build_sans_dns() + + if new_sans_ip ^ current_sans["sans_ip"] or new_sans_dns ^ current_sans["sans_dns"]: + return True + + return False + def get_statuses(self, scope: Scope, recompute: bool = False) -> list[StatusObject]: # noqa: C901 """Compute the TLS statuses.""" status_list: list[StatusObject] = [] diff --git a/src/statuses.py b/src/statuses.py index 2913f5a..a924f8f 100644 --- a/src/statuses.py +++ b/src/statuses.py @@ -34,6 +34,18 @@ class ClusterStatuses(Enum): running="async", ) + VALKEY_UNHEALTHY_RESTART = StatusObject( + status="maintenance", + message="Valkey unhealthy after restart", + running="async", + ) + + SENTINEL_UNHEALTHY_RESTART = StatusObject( + status="maintenance", + message="Sentinel unhealthy after restart", + running="async", + ) + class StartStatuses(Enum): """Collection of possible statuses related to the service start.""" diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 36269e4..d6366ea 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -31,6 +31,14 @@ def c_writes_runner(juju: jubilant.Juju, c_writes: ContinuousWrites): logger.info(c_writes.clear()) +@pytest.fixture(scope="function") +async def c_writes_async_clean(c_writes: ContinuousWrites): + """Clear continuous write operations at the end of the test.""" + yield + logger.info("Clearing continuous writes after test completion") + logger.info(await c_writes.async_clear()) + + @pytest.fixture(scope="session") def substrate(request) -> Substrate: """Substrate that we are testing.""" diff --git a/tests/integration/continuous_writes.py b/tests/integration/continuous_writes.py index 3cc44bc..b30c7da 100644 --- a/tests/integration/continuous_writes.py +++ b/tests/integration/continuous_writes.py @@ -15,11 +15,13 @@ import jubilant from glide import ( + AdvancedGlideClientConfiguration, BackoffStrategy, GlideClient, GlideClientConfiguration, NodeAddress, ServerCredentials, + TlsAdvancedConfiguration, ) from tenacity import ( retry, @@ -28,7 +30,7 @@ wait_random, ) -from literals import CharmUsers +from literals import CLIENT_PORT, TLS_PORT, CharmUsers from tests.integration.helpers import get_data_bag, get_password logger = logging.getLogger(__name__) @@ -58,7 +60,12 @@ class ContinuousWrites: VALKEY_PORT = 6379 def __init__( - self, juju: jubilant.Juju, app: str, initial_count: int = 0, in_between_sleep: float = 1.0 + self, + juju: jubilant.Juju, + app: str, + initial_count: int = 0, + in_between_sleep: float = 1.0, + tls_enabled: bool = False, ): self._juju = juju self._app = app @@ -69,29 +76,55 @@ def __init__( self._initial_count = initial_count self._in_between_sleep = in_between_sleep self._mp_ctx = multiprocessing.get_context("spawn") + self.tls_enabled = tls_enabled def _get_config(self) -> SimpleNamespace: """Fetch current cluster configuration from Juju.""" return SimpleNamespace( endpoints=get_active_hostnames(self._juju, self._app), valkey_password=get_password(self._juju, user=CharmUsers.VALKEY_ADMIN), + tls_enabled=self.tls_enabled, ) async def _create_glide_client(self, config: Optional[SimpleNamespace] = None) -> GlideClient: """Asynchronously create and return a configured GlideClient.""" conf = config or self._get_config() - addresses = [NodeAddress(host, self.VALKEY_PORT) for host in conf.endpoints.split(",")] + addresses = [ + NodeAddress(host, TLS_PORT if conf.tls_enabled else CLIENT_PORT) + for host in conf.endpoints.split(",") + ] credentials = ServerCredentials( username=CharmUsers.VALKEY_ADMIN.value, password=conf.valkey_password ) + tls_cert = tls_key = tls_ca_cert = None + if conf.tls_enabled: + # Read locally stored certificate files + with open("client.pem", "rb") as f: + tls_cert = f.read() + with open("client.key", "rb") as f: + tls_key = f.read() + with open("client_ca.pem", "rb") as f: + tls_ca_cert = f.read() + logger.info( + "TLS is enabled. Loaded client certificate, key, and CA cert for Glide client configuration." + ) + + tls_config = TlsAdvancedConfiguration( + client_cert_pem=tls_cert if conf.tls_enabled else None, + client_key_pem=tls_key if conf.tls_enabled else None, + root_pem_cacerts=tls_ca_cert if conf.tls_enabled else None, + ) + glide_config = GlideClientConfiguration( addresses=addresses, client_name="continuous_writes_client", request_timeout=500, credentials=credentials, reconnect_strategy=BackoffStrategy(num_of_retries=1, factor=50, exponent_base=2), + use_tls=True if conf.tls_enabled else False, + advanced_config=AdvancedGlideClientConfiguration(tls_config=tls_config), ) return await GlideClient.create(glide_config) @@ -132,7 +165,10 @@ def clear(self) -> SimpleNamespace | None: if not self._is_stopped: result = self.stop() - asyncio.run(self._async_delete()) + try: + asyncio.run(self._async_delete()) + except Exception as e: + logger.warning("Failed to clear continuous writes data from Valkey: %s", e) last_written_file = Path(self.LAST_WRITTEN_VAL_PATH) if last_written_file.exists(): @@ -146,7 +182,10 @@ async def async_clear(self) -> SimpleNamespace | None: if not self._is_stopped: result = await self.async_stop() - await self._async_delete() + try: + await self._async_delete() + except Exception as e: + logger.warning("Failed to clear continuous writes data from Valkey: %s", e) last_written_file = Path(self.LAST_WRITTEN_VAL_PATH) if last_written_file.exists(): @@ -243,20 +282,40 @@ async def _async_run( async def _make_client(conf: SimpleNamespace) -> GlideClient: addresses = [ - NodeAddress(host, ContinuousWrites.VALKEY_PORT) + NodeAddress(host, TLS_PORT if conf.tls_enabled else CLIENT_PORT) for host in conf.endpoints.split(",") ] + credentials = ServerCredentials( - username=CharmUsers.VALKEY_ADMIN.value, - password=conf.valkey_password, + username=CharmUsers.VALKEY_ADMIN.value, password=conf.valkey_password + ) + + tls_cert = tls_key = tls_ca_cert = None + if conf.tls_enabled: + # Read locally stored certificate files + with open("client.pem", "rb") as f: + tls_cert = f.read() + with open("client.key", "rb") as f: + tls_key = f.read() + with open("client_ca.pem", "rb") as f: + tls_ca_cert = f.read() + + tls_config = TlsAdvancedConfiguration( + client_cert_pem=tls_cert if conf.tls_enabled else None, + client_key_pem=tls_key if conf.tls_enabled else None, + root_pem_cacerts=tls_ca_cert if conf.tls_enabled else None, ) + glide_config = GlideClientConfiguration( addresses=addresses, - client_name="continuous_writes_worker", + client_name="continuous_writes_client", request_timeout=500, credentials=credentials, reconnect_strategy=BackoffStrategy(num_of_retries=1, factor=50, exponent_base=2), + use_tls=True if conf.tls_enabled else False, + advanced_config=AdvancedGlideClientConfiguration(tls_config=tls_config), ) + return await GlideClient.create(glide_config) @asynccontextmanager diff --git a/tests/integration/cw_helpers.py b/tests/integration/cw_helpers.py index 0756328..be32214 100644 --- a/tests/integration/cw_helpers.py +++ b/tests/integration/cw_helpers.py @@ -50,12 +50,14 @@ async def assert_continuous_writes_increasing( hostnames: list[str], username: str, password: str, + tls_enabled: bool = False, ) -> None: """Assert that the continuous writes are increasing.""" async with create_valkey_client( hostnames, username=username, password=password, + tls_enabled=tls_enabled, ) as client: writes_count = await client.llen(KEY) await asyncio.sleep(10) diff --git a/tests/integration/ha/conftest.py b/tests/integration/ha/conftest.py new file mode 100644 index 0000000..172ef56 --- /dev/null +++ b/tests/integration/ha/conftest.py @@ -0,0 +1,24 @@ +# Copyright 2026 Canonical Ltd. +# See LICENSE file for licensing details. + + +from collections.abc import Generator +from typing import Any + +import jubilant +import pytest + +from literals import Substrate + +from .helpers.helpers import deploy_chaos_mesh, destroy_chaos_mesh + + +@pytest.fixture(scope="module") +def chaos_mesh(juju: jubilant.Juju, substrate: Substrate) -> Generator[None, Any, Any]: + assert juju.model, "Juju model is not set. Ensure that the test is running with a Juju model." + if substrate == Substrate.K8S: + deploy_chaos_mesh(juju.model) + yield + destroy_chaos_mesh(juju.model) + else: + yield diff --git a/tests/integration/ha/helpers/chaos_network_loss.yml b/tests/integration/ha/helpers/chaos_network_loss.yml new file mode 100644 index 0000000..bde55f7 --- /dev/null +++ b/tests/integration/ha/helpers/chaos_network_loss.yml @@ -0,0 +1,18 @@ +apiVersion: chaos-mesh.org/v1alpha1 +kind: NetworkChaos +# Directive for chaosmesh to simulate a network loss for a pod for the network cut HA test. +# Namespace and pod ID are templated and populated by the test. +metadata: + name: network-loss-primary + namespace: $namespace +spec: + action: loss + mode: one + selector: + pods: + $namespace: + - $pod + loss: + loss: "100" + correlation: "100" + duration: "60m" diff --git a/tests/integration/ha/helpers/deploy_chaos_mesh.sh b/tests/integration/ha/helpers/deploy_chaos_mesh.sh new file mode 100755 index 0000000..05792fa --- /dev/null +++ b/tests/integration/ha/helpers/deploy_chaos_mesh.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +# Utility script to install chaosmesh in the K8S cluster, so test can use it to simulate +# infrastructure failures +# source: https://github.com/canonical/mongo-single-kernel-library/blob/8/edge/tests/integration/helpers/scripts/deploy_chaos_mesh.sh + +chaos_mesh_ns=$1 +chaos_mesh_version="2.4.1" + +if [ -z "${chaos_mesh_ns}" ]; then + exit 1 +fi + +deploy_chaos_mesh() { + if [ "$(microk8s.helm repo list | grep -c 'chaos-mesh')" != "1" ]; then + echo "adding chaos-mesh microk8s.helm repo" + microk8s.helm repo add chaos-mesh https://charts.chaos-mesh.org + fi + + echo "installing chaos-mesh" + microk8s.helm install chaos-mesh chaos-mesh/chaos-mesh --namespace="${chaos_mesh_ns}" --set chaosDaemon.runtime=containerd --set chaosDaemon.socketPath=/var/snap/microk8s/common/run/containerd.sock --set dashboard.create=false --version "${chaos_mesh_version}" --set clusterScoped=false --set controllerManager.targetNamespace="${chaos_mesh_ns}" + sleep 10 +} + +echo "namespace=${chaos_mesh_ns}" +chmod 0700 ~/.kube/config +deploy_chaos_mesh diff --git a/tests/integration/ha/helpers/destroy_chaos_mesh.sh b/tests/integration/ha/helpers/destroy_chaos_mesh.sh new file mode 100755 index 0000000..3b08194 --- /dev/null +++ b/tests/integration/ha/helpers/destroy_chaos_mesh.sh @@ -0,0 +1,53 @@ +#!/bin/bash + +# Utility script to removing chaosmesh from the K8S cluster, to clean up test artefacts +# source: https://github.com/canonical/mongo-single-kernel-library/blob/8/edge/tests/integration/helpers/scripts/destroy_chaos_mesh.sh + +chaos_mesh_ns=$1 + +if [ -z "${chaos_mesh_ns}" ]; then + exit 1 +fi + +destroy_chaos_mesh() { + echo "deleting api-resources" + for i in $(kubectl api-resources | grep chaos-mesh | awk '{print $1}'); do timeout 30 kubectl delete "${i}" --all --all-namespaces || :; done + + if [ "$(kubectl -n "${chaos_mesh_ns}" get mutatingwebhookconfiguration | grep -c 'chaos-mesh-mutation')" = "1" ]; then + echo "deleting chaos-mesh-mutation" + timeout 30 kubectl -n "${chaos_mesh_ns}" delete mutatingwebhookconfiguration chaos-mesh-mutation || : + fi + + if [ "$(kubectl -n "${chaos_mesh_ns}" get validatingwebhookconfiguration | grep -c 'chaos-mesh-validation-auth')" = "1" ]; then + echo "deleting chaos-mesh-validation-auth" + timeout 30 kubectl -n "${chaos_mesh_ns}" delete validatingwebhookconfiguration chaos-mesh-validation-auth || : + fi + + if [ "$(kubectl -n "${chaos_mesh_ns}" get validatingwebhookconfiguration | grep -c 'chaos-mesh-validation')" = "1" ]; then + echo 'deleting chaos-mesh-validation' + timeout 30 kubectl -n "${chaos_mesh_ns}" delete validatingwebhookconfiguration chaos-mesh-validation || : + fi + + if [ "$(kubectl get clusterrolebinding | grep 'chaos-mesh' | awk '{print $1}' | wc -l)" != "0" ]; then + echo "deleting clusterrolebindings" + timeout 30 kubectl delete clusterrolebinding "$(kubectl get clusterrolebinding | grep 'chaos-mesh' | awk '{print $1}')" || : + fi + + if [ "$(kubectl get clusterrole | grep 'chaos-mesh' | awk '{print $1}' | wc -l)" != "0" ]; then + echo "deleting clusterroles" + timeout 30 kubectl delete clusterrole "$(kubectl get clusterrole | grep 'chaos-mesh' | awk '{print $1}')" || : + fi + + if [ "$(kubectl get crd | grep 'chaos-mesh.org' | awk '{print $1}' | wc -l)" != "0" ]; then + echo "deleting crds" + timeout 30 kubectl delete crd "$(kubectl get crd | grep 'chaos-mesh.org' | awk '{print $1}')" || : + fi + + if [ -n "${chaos_mesh_ns}" ] && [ "$(microk8s.helm repo list --namespace "${chaos_mesh_ns}" | grep -c 'chaos-mesh')" = "1" ]; then + echo "uninstalling chaos-mesh microk8s.helm repo" + microk8s.helm uninstall chaos-mesh --namespace "${chaos_mesh_ns}" || : + fi +} + +echo "Destroying chaos mesh in ${chaos_mesh_ns}" +destroy_chaos_mesh diff --git a/tests/integration/ha/helpers/helpers.py b/tests/integration/ha/helpers/helpers.py new file mode 100644 index 0000000..581ae15 --- /dev/null +++ b/tests/integration/ha/helpers/helpers.py @@ -0,0 +1,431 @@ +#!/usr/bin/env python3 +# Copyright 2026 Canonical Ltd. +# See LICENSE file for licensing details. + +"""High availability helpers.""" + +import os +import string +import subprocess +import tempfile +import time +from logging import getLogger + +import jubilant +import urllib3 +import yaml +from kubernetes import client, config +from kubernetes.client.rest import ApiException +from tenacity import RetryError, Retrying, stop_after_attempt, wait_fixed + +from literals import Substrate +from tests.integration.helpers import APP_NAME, get_sentinels + +logger = getLogger(__name__) + + +def lxd_cut_network_from_unit_with_ip_change(machine_name: str) -> None: + """Cut network from a lxc container in a way the changes the IP.""" + # apply a mask (device type `none`) + cut_network_command = f"lxc config device add {machine_name} eth0 none" + subprocess.check_call(cut_network_command.split()) + + time.sleep(5) + + +def lxd_cut_network_from_unit_without_ip_change(machine_name: str) -> None: + """Cut network from a lxc container (without causing the change of the unit IP address).""" + override_command = f"lxc config device override {machine_name} eth0" + try: + subprocess.check_call(override_command.split()) + except subprocess.CalledProcessError: + # Ignore if the interface was already overridden. + pass + + limit_set_command = f"lxc config device set {machine_name} eth0 limits.egress=0kbit" + subprocess.check_call(limit_set_command.split()) + limit_set_command = f"lxc config device set {machine_name} eth0 limits.ingress=1kbit" + subprocess.check_call(limit_set_command.split()) + limit_set_command = f"lxc config device set {machine_name} eth0 limits.priority=10" + subprocess.check_call(limit_set_command.split()) + + +def k8s_cut_network_from_unit_without_ip_change(model_name: str, machine_name: str) -> None: + """Cut network from a k8s pod without causing the change of the unit IP address.""" + # Apply a NetworkChaos file to use chaos-mesh to simulate a network cut. + with tempfile.NamedTemporaryFile(dir=".") as temp_file: + # Generates a manifest for chaosmesh to simulate network failure for a pod + with open( + "tests/integration/ha/helpers/chaos_network_loss.yml" + ) as chaos_network_loss_file: + logger.info( + f"Calling network loss on ns={model_name} and pod={machine_name.replace('/', '-')}" + ) + template = string.Template(chaos_network_loss_file.read()) + chaos_network_loss = template.substitute( + namespace=model_name, + pod=machine_name.replace("/", "-"), + ) + + temp_file.write(str.encode(chaos_network_loss)) + temp_file.flush() + + # Apply the generated manifest, chaosmesh would then make the pod inaccessible + env = os.environ + env["KUBECONFIG"] = os.path.expanduser("~/.kube/config") + try: + command_result = subprocess.check_output( + " ".join(["microk8s", "kubectl", "apply", "-f", temp_file.name]), + shell=True, + env=env, + stderr=subprocess.STDOUT, + ) + except subprocess.CalledProcessError as err: + logger.error( + f"Failed to apply network isolation: [{err.returncode}] {err.stderr=}, {err.stdout=}" + ) + raise + logger.info("Result of isolating unit from cluster is '%s'", command_result) + + +def cut_network_from_unit( + substrate: Substrate, model_name: str, machine_name: str, change_ip: bool = False +) -> None: + """Cut network from a lxc container. + + Args: + juju: Juju client + substrate: The substrate the test is running on + model_name: The juju model name (only applicable for k8s) + machine_name: lxc container hostname or k8s pod name + change_ip: Whether to change the IP address of the unit on the network cut (only applicable for VMs) + """ + if substrate == Substrate.VM: + if change_ip: + lxd_cut_network_from_unit_with_ip_change(machine_name) + else: + lxd_cut_network_from_unit_without_ip_change(machine_name) + else: + k8s_cut_network_from_unit_without_ip_change(model_name, machine_name) + + +def restore_network_to_unit( + substrate: Substrate, model_name: str, machine_name: str, change_ip: bool = False +) -> None: + """Restore network from a lxc container. + + Args: + substrate: The substrate the test is running on + model_name: The juju model name (only applicable for k8s) + machine_name: lxc container hostname or k8s pod name + change_ip: Whether the network cut changed the IP address of the unit (only applicable for VMs) + """ + if substrate == Substrate.VM: + if change_ip: + # remove mask from eth0 + restore_network_command = f"lxc config device remove {machine_name} eth0" + subprocess.check_call(restore_network_command.split()) + return + limit_set_command = f"lxc config device set {machine_name} eth0 limits.egress=" + subprocess.check_call(limit_set_command.split()) + limit_set_command = f"lxc config device set {machine_name} eth0 limits.ingress=" + subprocess.check_call(limit_set_command.split()) + limit_set_command = f"lxc config device set {machine_name} eth0 limits.priority=" + subprocess.check_call(limit_set_command.split()) + else: + env = os.environ + env["KUBECONFIG"] = os.path.expanduser("~/.kube/config") + subprocess.check_output( + f"microk8s kubectl -n {model_name} delete networkchaos network-loss-primary", + shell=True, + env=env, + ) + + +def deploy_chaos_mesh(namespace: str) -> None: + """Deploy chaos mesh to the provided namespace. + + Chaos mesh can them be used by the tests to simulate a variety of failures. + + Args: + namespace: The namespace to deploy chaos mesh to + """ + env = os.environ + env["KUBECONFIG"] = os.path.expanduser("~/.kube/config") + + subprocess.check_output( + " ".join( + [ + "tests/integration/ha/helpers/deploy_chaos_mesh.sh", + namespace, + ] + ), + shell=True, + env=env, + ) + + +def destroy_chaos_mesh(namespace: str) -> None: + """Destroy chaos mesh on a provided namespace. + + Cleans up the test K8S from test related dependencies. + + Args: + namespace: The namespace to deploy chaos mesh to + """ + env = os.environ + env["KUBECONFIG"] = os.path.expanduser("~/.kube/config") + + subprocess.check_output( + f"tests/integration/ha/helpers/destroy_chaos_mesh.sh {namespace}", + shell=True, + env=env, + ) + + +def get_unit_name_from_primary_ip( + juju: jubilant.Juju, primary_ip: str, substrate: Substrate +) -> str: + """Get the container name from the primary endpoint. + + Args: + juju: Juju client + primary_ip: The primary endpoint IP address to get the corresponding container name for + substrate: The substrate the test is running on + + Returns: + The container name corresponding to the primary endpoint. + """ + for unit_name, unit in juju.status().apps[APP_NAME].units.items(): + try: + if ( + juju.exec("unit-get private-address", unit=unit_name, wait=5).stdout.strip() + == primary_ip + ): + return unit_name + except TimeoutError as e: + logger.warning(f"Failed to get private address for {unit_name}: {e}") + raise ValueError(f"No unit found with IP address {primary_ip}") + + +def is_unit_reachable_k8s(namespace: str, source_pod_name: str, to_host: str) -> bool: + """Test network reachability to a unit in k8s by creating a temporary pod with the same labels as the source pod and trying to ping the destination IP.""" + # --------------------------------------------------------- + # 1. Setup Client and Bypass SSL (for local/testing clusters) + # --------------------------------------------------------- + config.load_kube_config() + + configuration = client.Configuration.get_default_copy() + configuration.verify_ssl = False + client.Configuration.set_default(configuration) + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + v1 = client.CoreV1Api() + + # --------------------------------------------------------- + # 2. Fetch Labels from the Source Pod + # --------------------------------------------------------- + try: + source_pod = v1.read_namespaced_pod(name=source_pod_name, namespace=namespace) + source_labels = source_pod.metadata.labels or {} + logger.info(f"Fetched labels from {source_pod_name}: {source_labels}") + except ApiException as e: + logger.error(f"Failed to read source pod {source_pod_name}: {e}") + return False + + # --------------------------------------------------------- + # 3. Define the Temporary Test Pod + # --------------------------------------------------------- + temp_pod_name = f"netshoot-test-{int(time.time())}" + + pod_manifest = client.V1Pod( + metadata=client.V1ObjectMeta( + name=temp_pod_name, + namespace=namespace, + labels=source_labels, # <--- Injecting the source pod's labels here + ), + spec=client.V1PodSpec( + restart_policy="Never", + containers=[ + client.V1Container( + name="netshoot", + image="nicolaka/netshoot", + # Ping five times (-c 5), wait up to 2 seconds for a response (-W 2) + command=["ping", "-c", "5", "-W", "2", to_host], + ) + ], + ), + ) + + # --------------------------------------------------------- + # 4. Execute and Wait for Results + # --------------------------------------------------------- + try: + logger.info(f"Creating test pod '{temp_pod_name}' to ping {to_host}...") + v1.create_namespaced_pod(namespace=namespace, body=pod_manifest) + + # Poll the pod status until it completes + phase = None + for attempt in Retrying(stop=stop_after_attempt(30), wait=wait_fixed(2)): + with attempt: + pod_status = v1.read_namespaced_pod(name=temp_pod_name, namespace=namespace) + phase = pod_status.status.phase + + if phase not in ["Succeeded", "Failed"]: + logger.info( + f"Pod '{temp_pod_name}' is in phase '{phase}'. Waiting for completion..." + ) + raise ValueError("Pod not completed yet") + + # Optional: Fetch the actual ping output logs for debugging + logs = v1.read_namespaced_pod_log(name=temp_pod_name, namespace=namespace) + logger.info(f"Ping Output:\n{logs.strip()}") + + # If phase is Succeeded, the ping command returned exit code 0 + is_reachable = phase == "Succeeded" + + if is_reachable: + logger.info(f"Success: {to_host} is reachable from {source_pod_name}.") + else: + logger.error(f"Failure: {to_host} is NOT reachable from {source_pod_name}.") + + return is_reachable + + except ApiException as e: + logger.error(f"Exception during pod creation/execution: {e}") + return False + + # --------------------------------------------------------- + # 5. Clean Up (Always runs, even if errors occur above) + # --------------------------------------------------------- + finally: + logger.info(f"Cleaning up pod '{temp_pod_name}'...") + try: + v1.delete_namespaced_pod(name=temp_pod_name, namespace=namespace) + except ApiException as e: + logger.error(f"Failed to delete temporary pod {temp_pod_name}: {e}") + + +def is_unit_reachable_lxd(from_host: str, to_host: str, number_of_retries: int = 10) -> bool: + """Test network reachability between LXD hosts.""" + try: + for attempt in Retrying(stop=stop_after_attempt(number_of_retries), wait=wait_fixed(10)): + with attempt: + ping = subprocess.call( + f"lxc exec {from_host} -- ping -c 5 -W 2 {to_host}".split(), + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + if ping == 0: + return True + else: + raise ValueError + except RetryError: + return False + return False + + +def is_unit_reachable( + juju: jubilant.Juju, + from_host: str, + to_host: str, + substrate: Substrate, + number_of_retries: int = 10, +) -> bool: + """Test network reachability to a unit based on the substrate.""" + assert juju.model, "Juju client must be connected to a model before checking unit reachability" + match substrate: + case Substrate.K8S: + return is_unit_reachable_k8s(juju.model, from_host, to_host) + case Substrate.VM: + return is_unit_reachable_lxd(from_host, to_host, number_of_retries=number_of_retries) + + +def hostname_from_unit(juju: jubilant.Juju, unit_name: str) -> str: + """Get the machine hostname from a specific unit. + + Args: + juju: An instance of Jubilant's Juju class on which to run Juju commands + unit_name: The name of the unit to get the machine + + Returns: + The hostname of the machine. + """ + task_result = juju.exec(command="hostname", unit=unit_name) + + return task_result.stdout.strip() + + +def get_sans_from_certificate(certificate_path: str) -> dict[str, set[str]]: + """Get the SANs for a unit's cert.""" + sans_ip = set() + sans_dns = set() + if not ( + san_lines := subprocess.run( + [ + "openssl", + "x509", + "-ext", + "subjectAltName", + "-noout", + "-in", + certificate_path, + ], + capture_output=True, + text=True, + ).stdout.splitlines() + ): + return {"sans_ip": sans_ip, "sans_dns": sans_dns} + + for line in san_lines: + for sans in line.split(", "): + san_type, san_value = sans.split(":") + + if san_type.strip() == "DNS": + sans_dns.add(san_value) + if san_type.strip() == "IP Address": + sans_ip.add(san_value) + + return {"sans_ip": sans_ip, "sans_dns": sans_dns} + + +def lxd_get_controller_hostname(juju: jubilant.Juju) -> str: + """Return controller machine hostname.""" + raw_model = juju.cli("show-model", juju.model, include_model=False) + raw_controller = juju.cli("show-controller", include_model=False) + + model_details = yaml.safe_load(raw_model) + controller_details = yaml.safe_load(raw_controller) + controller_name = model_details[juju.model]["controller-name"] + + return [ + machine.get("instance-id") + for machine in controller_details[controller_name]["controller-machines"].values() + ][0] + + +def endpoint_in_sentinels( + juju: jubilant.Juju, + endpoint: str, + hostname: str, + status: str = "", + tls_enabled: bool = False, +) -> bool: + """Check if the provided endpoint is present in the sentinels list of any of the provided hostnames.""" + endpoint_sentinel = [ + sentinel + for sentinel in get_sentinels(juju, primary_ip=hostname, tls_enabled=tls_enabled) + if endpoint in sentinel["ip"] + ] + if not endpoint_sentinel: + logger.error( + f"Endpoint {endpoint} not found in sentinels list of {hostname}. Sentinels list: {get_sentinels(juju, primary_ip=hostname, tls_enabled=tls_enabled)}" + ) + return False + if status and status not in endpoint_sentinel[0]["flags"]: + logger.error( + f"Endpoint {endpoint} found in sentinels list of {hostname} but with unexpected status. Expected status: {status}, Sentinels list: {get_sentinels(juju, primary_ip=hostname, tls_enabled=tls_enabled)}" + ) + return False + + return True diff --git a/tests/integration/ha/test_network_cut.py b/tests/integration/ha/test_network_cut.py new file mode 100644 index 0000000..2fd655f --- /dev/null +++ b/tests/integration/ha/test_network_cut.py @@ -0,0 +1,279 @@ +#!/usr/bin/env python3 +# Copyright 2026 Canonical Ltd. +# See LICENSE file for licensing details. +import logging + +import jubilant +import pytest +from tenacity import Retrying, stop_after_attempt, wait_fixed + +from literals import Substrate +from tests.integration.cw_helpers import ( + assert_continuous_writes_increasing, +) +from tests.integration.ha.helpers.helpers import ( + cut_network_from_unit, + endpoint_in_sentinels, + get_sans_from_certificate, + get_unit_name_from_primary_ip, + hostname_from_unit, + is_unit_reachable, + lxd_get_controller_hostname, + restore_network_to_unit, +) +from tests.integration.helpers import ( + APP_NAME, + IMAGE_RESOURCE, + TLS_CHANNEL, + TLS_NAME, + CharmUsers, + are_apps_active_and_agents_idle, + download_client_certificate_from_unit, + get_cluster_hostnames, + get_ip_from_unit, + get_number_connected_replicas, + get_password, + get_primary_ip, +) + +logger = logging.getLogger(__name__) + +NUM_UNITS = 3 + + +@pytest.mark.parametrize("tls_enabled", [False, True], ids=["tls_off", "tls_on"]) +def test_build_and_deploy( + tls_enabled: bool, charm: str, juju: jubilant.Juju, substrate: Substrate +) -> None: + """Build the charm-under-test and deploy it with three units.""" + juju.deploy( + charm, + resources=IMAGE_RESOURCE if substrate == Substrate.K8S else None, + num_units=NUM_UNITS, + trust=True, + ) + + if tls_enabled: + juju.deploy(TLS_NAME, channel=TLS_CHANNEL) + juju.integrate(f"{APP_NAME}:client-certificates", TLS_NAME) + + juju.wait( + lambda status: are_apps_active_and_agents_idle(status, APP_NAME, idle_period=30), + timeout=600, + ) + + assert len(juju.status().apps[APP_NAME].units) == NUM_UNITS, ( + f"Unexpected number of units after initial deploy: expected {NUM_UNITS}, got {len(juju.status().apps[APP_NAME].units)}" + ) + + +@pytest.mark.parametrize("tls_enabled", [False, True], ids=["tls_off", "tls_on"]) +@pytest.mark.parametrize("change_ip", [True, False], ids=["change_ip", "no_change_ip"]) +async def test_network_cut_primary( # noqa: C901 + tls_enabled: bool, + change_ip: bool, + juju: jubilant.Juju, + substrate: Substrate, + chaos_mesh, + c_writes, + c_writes_async_clean, +) -> None: + """Cut the network to the primary unit and verify that a new primary is elected.""" + if change_ip and substrate == Substrate.K8S: + pytest.skip("Changing IP is not applicable for k8s substrate.") + + download_client_certificate_from_unit(juju, APP_NAME) + hostnames = get_cluster_hostnames(juju, APP_NAME) + + c_writes.tls_enabled = tls_enabled + await c_writes.async_clear() + c_writes.start() + + # Get the current primary unit + old_primary_endpoint = get_primary_ip(juju, APP_NAME, tls_enabled=tls_enabled) + assert old_primary_endpoint, "Failed to get primary endpoint from Juju status." + + # Cut the network to the primary unit + logger.info("Cutting network to primary unit at %s", old_primary_endpoint) + primary_unit_name = get_unit_name_from_primary_ip(juju, old_primary_endpoint, substrate) + + download_client_certificate_from_unit(juju, APP_NAME, unit_name=primary_unit_name) + + primary_hostname = hostname_from_unit(juju, primary_unit_name) + machine_name = primary_hostname + if substrate == Substrate.K8S: + primary_hostname = f"{primary_hostname}.{APP_NAME}-endpoints" + + logger.info("Identified container name for primary unit: %s", primary_hostname) + cut_network_from_unit(substrate, juju.model, machine_name, change_ip=change_ip) + + # on K8s the controller is on a different namespace + if substrate == Substrate.VM: + controller_hostname = lxd_get_controller_hostname(juju) + assert not is_unit_reachable( + juju, controller_hostname, primary_hostname, substrate, number_of_retries=3 + ), ( + f"Controller {controller_hostname} can still reach the primary unit {primary_hostname} after network cut." + ) + + for unit in juju.status().apps[APP_NAME].units: + if unit == primary_unit_name: + continue + assert not is_unit_reachable( + juju, + hostname_from_unit(juju, unit), + primary_hostname, + substrate, + number_of_retries=3, + ), f"Unit {unit} can still reach the primary unit {primary_hostname} after network cut." + + logger.info( + "Network successfully cut to primary unit %s at %s. Verifying new primary election...", + primary_unit_name, + old_primary_endpoint, + ) + + new_primary_endpoint = None + for attempt in Retrying(stop=stop_after_attempt(10), wait=wait_fixed(10)): + with attempt: + try: + new_primary_endpoint = get_primary_ip(juju, APP_NAME, tls_enabled=tls_enabled) + break + except ValueError as e: + logger.warning(f"Error getting primary IP after network cut: {e}") + logger.info("Waiting for new primary to be elected...") + + assert new_primary_endpoint and new_primary_endpoint != old_primary_endpoint, ( + "Primary IP did not change after cutting network to the primary unit." + ) + logger.info( + "New primary IP after network cut: %s vs old primary IP: %s", + new_primary_endpoint, + old_primary_endpoint, + ) + + # check replica number that it is down to NUM_UNITS - 2 + number_of_replicas = await get_number_connected_replicas( + hostnames=hostnames, + username=CharmUsers.VALKEY_ADMIN.value, + password=get_password(juju, user=CharmUsers.VALKEY_ADMIN), + tls_enabled=tls_enabled, + ) + assert number_of_replicas == NUM_UNITS - 2, ( + f"Expected {NUM_UNITS - 2} connected replicas, got {number_of_replicas}." + ) + + logger.info( + "Verified that a new primary has been elected and is reachable at %s. Verifying that old primary endpoint is marked as down in sentinels of other units...", + new_primary_endpoint, + ) + for hostname in hostnames: + if hostname == old_primary_endpoint: + continue + assert endpoint_in_sentinels( + juju, old_primary_endpoint, hostname, status="s_down", tls_enabled=tls_enabled + ), ( + f"The old primary endpoint should be marked as down in sentinels list of hostname {hostname} after network cut." + ) + logger.info( + "Verified that old primary endpoint %s is marked as down in sentinels of hostname %s after network cut.", + old_primary_endpoint, + hostname, + ) + + await assert_continuous_writes_increasing( + hostnames=hostnames, + username=CharmUsers.VALKEY_ADMIN.value, + password=get_password(juju, user=CharmUsers.VALKEY_ADMIN), + tls_enabled=tls_enabled, + ) + + # restore network to the original primary unit + logger.info("Restoring network to original primary unit at %s", primary_hostname) + restore_network_to_unit(substrate, juju.model, machine_name, change_ip=change_ip) + juju.wait( + lambda status: are_apps_active_and_agents_idle( + status, APP_NAME, unit_count=NUM_UNITS, idle_period=30 + ) + ) + c_writes.update() + + logger.info( + "Network restored to original primary unit %s. Verifying that all units can reach the original primary unit at %s...", + primary_unit_name, + primary_hostname, + ) + for unit in juju.status().apps[APP_NAME].units: + if unit == primary_unit_name: + continue + assert is_unit_reachable( + juju, hostname_from_unit(juju, unit), primary_hostname, substrate + ), ( + f"Unit {unit} cannot reach the original primary unit {primary_hostname} after network restoration." + ) + logger.info( + "Unit %s can reach the original primary unit %s after network restoration.", + unit, + primary_hostname, + ) + + download_client_certificate_from_unit(juju, APP_NAME, unit_name=primary_unit_name) + new_unit_ip = get_ip_from_unit(juju, primary_unit_name) + # read ip from cert and check if is a different ip than before if change_ip is True + certificate_sans = get_sans_from_certificate("./client.pem") + if change_ip: + assert old_primary_endpoint not in certificate_sans["sans_ip"], ( + "The old IP should not be in SANs of client certificate after network cut and IP change." + ) + assert new_unit_ip in certificate_sans["sans_ip"], ( + "The new IP should be in SANs of client certificate after network cut and IP change." + ) + + hostnames = get_cluster_hostnames(juju, APP_NAME) + # check replica number that it is back to NUM_UNITS - 1 + number_of_replicas = await get_number_connected_replicas( + hostnames=hostnames, + username=CharmUsers.VALKEY_ADMIN.value, + password=get_password(juju, user=CharmUsers.VALKEY_ADMIN), + tls_enabled=tls_enabled, + ) + assert number_of_replicas == NUM_UNITS - 1, ( + f"Expected {NUM_UNITS - 1} connected replicas after network restoration, got {number_of_replicas}." + ) + + for hostname in hostnames: + if hostname == new_unit_ip: + continue + if change_ip: + assert not endpoint_in_sentinels( + juju, old_primary_endpoint, hostname, tls_enabled=tls_enabled + ), ( + f"The old primary endpoint should not be present in sentinels list of hostname {hostname} after network cut and IP change." + ) + assert endpoint_in_sentinels(juju, new_unit_ip, hostname, tls_enabled=tls_enabled), ( + f"The new primary IP should be present in sentinels list of hostname {hostname} after network cut and IP change." + ) + logger.info( + "Verified that old primary endpoint %s is not in sentinels and new primary IP %s is in sentinels of hostname %s after network restoration with IP change.", + old_primary_endpoint, + new_unit_ip, + hostname, + ) + else: + assert endpoint_in_sentinels( + juju, old_primary_endpoint, hostname, tls_enabled=tls_enabled + ), ( + f"The old primary endpoint should be present in sentinels list of hostname {hostname} after network cut and no IP change." + ) + logger.info( + "Verified that old primary endpoint %s is in sentinels of hostname %s after network restoration with no IP change.", + old_primary_endpoint, + hostname, + ) + + await assert_continuous_writes_increasing( + hostnames=hostnames, + username=CharmUsers.VALKEY_ADMIN.value, + password=get_password(juju, user=CharmUsers.VALKEY_ADMIN), + tls_enabled=tls_enabled, + ) diff --git a/tests/integration/helpers.py b/tests/integration/helpers.py index 4b3bf70..1f2c104 100644 --- a/tests/integration/helpers.py +++ b/tests/integration/helpers.py @@ -33,6 +33,8 @@ INTERNAL_USERS_PASSWORD_CONFIG, INTERNAL_USERS_SECRET_LABEL_SUFFIX, PEER_RELATION, + SENTINEL_PORT, + SENTINEL_TLS_PORT, TLS_PORT, CharmUsers, Substrate, @@ -237,13 +239,14 @@ def get_cluster_hostnames(juju: jubilant.Juju, app_name: str) -> list[str]: Returns: A list of hostnames for all units in the Valkey application. """ - status = juju.status() - model_info = juju.show_model() - - if model_info.type == "kubernetes": - return [unit.address for unit in status.get_units(app_name).values()] - - return [unit.public_address for unit in status.get_units(app_name).values()] + # returns the real ip addresses even if they are not updated on juju's status + ips = [] + for unit in juju.status().get_units(app_name): + try: + ips.append(juju.exec("unit-get private-address", unit=unit, wait=5).stdout.strip()) + except TimeoutError as e: + logger.warning(f"Failed to get private address for {unit}: {e}") + return ips def get_secret_by_label(juju: jubilant.Juju, label: str) -> dict[str, str]: @@ -352,9 +355,11 @@ def fast_forward(juju: jubilant.Juju): juju.model_config({"update-status-hook-interval": old}) -def download_client_certificate_from_unit(juju: jubilant.Juju, app_name: str = APP_NAME) -> None: +def download_client_certificate_from_unit( + juju: jubilant.Juju, app_name: str = APP_NAME, unit_name: str | None = None +) -> None: """Copy the client certificate files from a unit to the host's filesystem.""" - unit = next(iter(juju.status().get_units(app_name))) + unit = unit_name or next(iter(juju.status().get_units(app_name))) model_info = juju.show_model() if model_info.type == "kubernetes": @@ -369,26 +374,29 @@ def download_client_certificate_from_unit(juju: jubilant.Juju, app_name: str = A juju.scp(f"{unit}:{tls_path}/ca_certs/{TLS_CA_FILE}", TLS_CA_FILE) -def get_primary_ip(juju: jubilant.Juju, app: str) -> str: +def get_primary_ip(juju: jubilant.Juju, app: str, tls_enabled: bool = False) -> str: """Get the primary node of the Valkey cluster. Returns: The IP address of the primary node. """ hostnames = get_cluster_hostnames(juju, app) - replication_info = exec_valkey_cli( - hostnames[0], - username=CharmUsers.VALKEY_ADMIN.value, - password=get_password(juju), - command="info replication", - ).stdout - # if master then we return the hostname - if "role:master" in replication_info: - return hostnames[0] - # extract ip - if not (match := re.search(r"master_host:([^\s]+)", replication_info)): - raise ValueError("Could not find master_host in replication info") - return match.group(1) + for hostname in hostnames: + try: + replication_info = exec_valkey_cli( + hostname, + username=CharmUsers.VALKEY_ADMIN.value, + password=get_password(juju), + command="info replication", + tls_enabled=tls_enabled, + ).stdout + # if master then we return the hostname + if "role:master" in replication_info: + return hostname + except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e: + logger.warning(f"Error executing Valkey CLI on {hostname}: {e}") + + raise ValueError("No primary node found in the cluster") def get_password(juju: jubilant.Juju, user: CharmUsers = CharmUsers.VALKEY_ADMIN) -> str: @@ -467,12 +475,29 @@ async def seed_valkey(juju: jubilant.Juju, target_gb: float = 1.0) -> None: def exec_valkey_cli( - hostname: str, username: str, password: str, command: str + hostname: str, + username: str, + password: str, + command: str, + tls_enabled: bool = False, + json: bool = False, + sentinel: bool = False, ) -> valkey_cli_result: """Execute a Valkey CLI command and returns the output as a string.""" - command = f"valkey-cli --no-auth-warning -h {hostname} -p {CLIENT_PORT} --user {username} --pass {password} {command}" + port = TLS_PORT if tls_enabled else CLIENT_PORT + if sentinel: + port = SENTINEL_TLS_PORT if tls_enabled else SENTINEL_PORT + pre_command = f"valkey-cli --no-auth-warning -h {hostname} -p {port} --user {username} --pass {password} {'--json' if json else ''}" + if tls_enabled: + pre_command += " --tls --cert client.pem --key client.key --cacert client_ca.pem" + exec_command = f"{pre_command} {command}" result = subprocess.run( - command.split(), check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True + exec_command.split(), + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + timeout=10, ) return valkey_cli_result( stdout=result.stdout.strip(), stderr=result.stderr.strip(), returncode=result.returncode @@ -568,6 +593,7 @@ async def get_number_connected_replicas( hostnames: list[str], username: str, password: str, + tls_enabled: bool = False, ) -> int: """Get the number of connected replicas in the Valkey cluster. @@ -575,12 +601,16 @@ async def get_number_connected_replicas( hostnames: List of hostnames of the Valkey cluster nodes. username: The username for authentication. password: The password for authentication. + tls_enabled: Whether TLS certificates are needed. Returns: The number of connected replicas. """ async with create_valkey_client( - hostnames=hostnames, username=username, password=password + hostnames=hostnames, + username=username, + password=password, + tls_enabled=tls_enabled, ) as client: info = (await client.info([InfoSection.REPLICATION])).decode() search_result = re.search(r"connected_slaves:([\d+])", info) @@ -694,3 +724,23 @@ def existing_app(juju: jubilant.Juju) -> str | None: return app_name return None + + +def get_ip_from_unit(juju: jubilant.Juju, unit_name: str) -> str: + """Get the IP address of a unit based on the substrate type.""" + return juju.exec("unit-get", "private-address", unit=unit_name).stdout.strip() + + +def get_sentinels(juju: jubilant.Juju, primary_ip: str, tls_enabled: bool = False) -> list[dict]: + """Get the list of sentinels from the data bag.""" + return json.loads( + exec_valkey_cli( + primary_ip, + username=CharmUsers.SENTINEL_CHARM_ADMIN.value, + password=get_password(juju, user=CharmUsers.SENTINEL_CHARM_ADMIN), + tls_enabled=tls_enabled, + command="sentinel sentinels primary", + json=True, + sentinel=True, + ).stdout + ) diff --git a/tests/spread/k8s/test_network_cut_tls_off.py/task.yaml b/tests/spread/k8s/test_network_cut_tls_off.py/task.yaml new file mode 100644 index 0000000..38a1fb2 --- /dev/null +++ b/tests/spread/k8s/test_network_cut_tls_off.py/task.yaml @@ -0,0 +1,7 @@ +summary: test_network_cut.py +environment: + TEST_MODULE: ha/test_network_cut.py +execute: | + tox run -e integration -- "tests/integration/$TEST_MODULE" --substrate k8s -k "tls_off" --alluredir="$SPREAD_TASK/allure-results" +artifacts: + - allure-results diff --git a/tests/spread/k8s/test_network_cut_tls_on.py/task.yaml b/tests/spread/k8s/test_network_cut_tls_on.py/task.yaml new file mode 100644 index 0000000..c21ff70 --- /dev/null +++ b/tests/spread/k8s/test_network_cut_tls_on.py/task.yaml @@ -0,0 +1,7 @@ +summary: test_network_cut.py +environment: + TEST_MODULE: ha/test_network_cut.py +execute: | + tox run -e integration -- "tests/integration/$TEST_MODULE" --substrate k8s -k "tls_on" --alluredir="$SPREAD_TASK/allure-results" +artifacts: + - allure-results diff --git a/tests/spread/vm/test_network_cut_tls_off.py/task.yaml b/tests/spread/vm/test_network_cut_tls_off.py/task.yaml new file mode 100644 index 0000000..16fff46 --- /dev/null +++ b/tests/spread/vm/test_network_cut_tls_off.py/task.yaml @@ -0,0 +1,7 @@ +summary: test_network_cut.py +environment: + TEST_MODULE: ha/test_network_cut.py +execute: | + tox run -e integration -- "tests/integration/$TEST_MODULE" --substrate vm -k "tls_off" --alluredir="$SPREAD_TASK/allure-results" +artifacts: + - allure-results diff --git a/tests/spread/vm/test_network_cut_tls_on.py/task.yaml b/tests/spread/vm/test_network_cut_tls_on.py/task.yaml new file mode 100644 index 0000000..dcf3558 --- /dev/null +++ b/tests/spread/vm/test_network_cut_tls_on.py/task.yaml @@ -0,0 +1,7 @@ +summary: test_network_cut.py +environment: + TEST_MODULE: ha/test_network_cut.py +execute: | + tox run -e integration -- "tests/integration/$TEST_MODULE" --substrate vm -k "tls_on" --alluredir="$SPREAD_TASK/allure-results" +artifacts: + - allure-results diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py index f83aa99..44cf96e 100644 --- a/tests/unit/test_charm.py +++ b/tests/unit/test_charm.py @@ -465,40 +465,11 @@ def test_config_changed_non_leader_unit(cloud_spec): config={INTERNAL_USERS_PASSWORD_CONFIG: password_secret.id}, model=testing.Model(name="my-vm-model", type="lxd", cloud_spec=cloud_spec), ) - with ( - patch("events.base_events.BaseEvents._update_internal_users_password") as mock_update, - ): + with patch("events.base_events.BaseEvents._update_internal_users_password") as mock_update: ctx.run(ctx.on.config_changed(), state_in) mock_update.assert_not_called() -def test_config_changed_leader_unit_valkey_update_fails(cloud_spec): - ctx = testing.Context(ValkeyCharm, app_trusted=True) - relation = testing.PeerRelation( - id=1, endpoint=PEER_RELATION, local_unit_data={"start-state": "started"} - ) - container = testing.Container(name=CONTAINER, can_connect=True) - - password_secret = testing.Secret( - tracked_content={user.value: "secure-password" for user in CharmUsers}, - remote_grants=APP_NAME, - ) - state_in = testing.State( - leader=True, - relations={relation}, - containers={container}, - secrets={password_secret}, - config={INTERNAL_USERS_PASSWORD_CONFIG: password_secret.id}, - model=testing.Model(name="my-vm-model", type="lxd", cloud_spec=cloud_spec), - ) - with ( - patch("workload_k8s.ValkeyK8sWorkload.write_file"), - patch("core.models.RelationState.update") as mock_update, - ): - ctx.run(ctx.on.config_changed(), state_in) - mock_update.assert_called_once() - - def test_config_changed_leader_unit(cloud_spec): ctx = testing.Context(ValkeyCharm, app_trusted=True) relation = testing.PeerRelation( @@ -567,6 +538,53 @@ def test_config_changed_leader_unit_wrong_username(cloud_spec): mock_set_acl_file.assert_not_called() +def test_config_changed_ip_change_no_tls_relation(cloud_spec_vm): + ctx = testing.Context(ValkeyCharm, app_trusted=True) + relation = testing.PeerRelation( + id=1, + endpoint=PEER_RELATION, + local_unit_data={"start-state": "started", "private-ip": "127.0.1.1"}, + ) + container = testing.Container(name=CONTAINER, can_connect=True) + + password_secret = testing.Secret( + tracked_content={user.value: "secure-password" for user in CharmUsers}, + remote_grants=APP_NAME, + ) + state_in = testing.State( + leader=True, + relations={relation}, + containers={container}, + secrets={password_secret}, + config={INTERNAL_USERS_PASSWORD_CONFIG: password_secret.id}, + model=testing.Model(name="my-vm-model", type="lxd", cloud_spec=cloud_spec_vm), + ) + with ( + patch("managers.config.ConfigManager.configure_services"), + patch("managers.sentinel.SentinelManager.get_primary_ip", return_value="127.1.1.2"), + patch("managers.sentinel.SentinelManager.restart_service") as mock_restart_sentinel, + patch( + "workload_vm.ValkeyVmWorkload.exec", + return_value=("DNS:www.example.com, IP Address:127.1.1.1",), + ), + patch("workload_vm.ValkeyVmWorkload.restart") as mock_workload_restart, + patch("managers.tls.TLSManager.build_sans_ip", return_value=frozenset({"127.0.1.1"})), + patch( + "managers.tls.TLSManager.build_sans_dns", return_value=frozenset({"www.example.com"}) + ), + patch("events.base_events.BaseEvents._update_internal_users_password"), + patch( + "managers.tls.TLSManager.create_and_store_self_signed_certificate" + ) as mock_create_certificate, + patch("managers.cluster.ClusterManager.is_healthy", return_value=True), + patch("managers.sentinel.SentinelManager.is_healthy", return_value=True), + ): + ctx.run(ctx.on.config_changed(), state_in) + mock_create_certificate.assert_called_once() + mock_restart_sentinel.assert_called_once() + mock_workload_restart.assert_called_once() + + def test_change_password_secret_changed_non_leader_unit(cloud_spec): ctx = testing.Context(ValkeyCharm, app_trusted=True) relation = testing.PeerRelation(