diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 371d05f9..09520ac8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,7 +23,7 @@ repos: args: [--branch, main, --branch, dev] - id: check-added-large-files - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.9.4 + rev: v0.9.6 hooks: - id: ruff args: [ --fix, --exit-non-zero-on-fix, "--ignore=C901" ] @@ -35,7 +35,7 @@ repos: pass_filenames: false - repo: https://github.com/PyCQA/bandit.git - rev: 1.8.2 + rev: 1.8.3 hooks: - id: bandit args: [-lll, --recursive, clumper] diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d6b4967..2a07ae1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Change log +## [v2.0.0-alpha1](https://github.com/simvue-io/client/releases/tag/v2.0.0a1) - 2025-02-19 +* Fixed `add_alerts` so that it now works with both IDs and names +* Improved alert and folder deduplication methods to rely on 409 responses from server upon creation +* Added `attach_to_run` option to create alerts methods so that alerts can be created without a run attached +* Improved merging of local staging file and _staged dict using `deepmerge` - fixes bugs with tags, alerts and metadata in offline mode +* Added `started`, `created` and `ended` timestamps to runs in offline mode +* Remove all erronous server calls in offline mode +* Fixed method to find simvue.toml config files, now just looks in cwd and home +* Added run notification option to `run.init` so that users can now get emails upon their runs completing +* Fixed artifact retrieval by run so that `category` parameter works correctly +* Fixed bug where file artifacts wouldn't be saved correctly in offline mode if sender runs in different location to script +* Fixed bug where DEBUG log messages were spamming to the console +* Fixed link to run dashboard printed to the console by removing `/api` +* Fixed bug where offline mode wouldn't work if no run name provided +* Fixed bug where errors would be thrown if a traceback was logged as an event when a run was already terminated +* Fixed hierarchical artifact retrieval to maintain directory structure +* Loosened Numpy requirement to >2.0.0 + ## [v2.0.0-alpha0](https://github.com/simvue-io/client/releases/tag/v2.0.0a0) - 2025-02-10 * Add support for defining Simvue run defaults using `tool.simvue` in a project `pyproject.toml` file. diff --git a/CITATION.cff b/CITATION.cff index b39358d6..d9026ae2 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -42,9 +42,9 @@ keywords: - alerting - simulation license: Apache-2.0 -commit: 5aaebe682d7ec2f80fefc3eb2a8a26f5bdca1e0c -version: 2.0.0a0 -date-released: '2025-02-11' +commit: 124b2993d91dbff2e475aa972916b11e7bd02fa4 +version: 2.0.0a1 +date-released: '2025-02-19' references: - title: mlco2/codecarbon version: v2.8.2 diff --git a/poetry.lock b/poetry.lock index aa295ce7..ed998f4c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -413,75 +413,76 @@ test-no-images = ["pytest", "pytest-cov", "pytest-rerunfailures", "pytest-xdist" [[package]] name = "coverage" -version = "7.6.10" +version = "7.6.12" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" groups = ["dev"] markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "coverage-7.6.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78"}, - {file = "coverage-7.6.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c"}, - {file = "coverage-7.6.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3b204c11e2b2d883946fe1d97f89403aa1811df28ce0447439178cc7463448a"}, - {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32ee6d8491fcfc82652a37109f69dee9a830e9379166cb73c16d8dc5c2915165"}, - {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675cefc4c06e3b4c876b85bfb7c59c5e2218167bbd4da5075cbe3b5790a28988"}, - {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f4f620668dbc6f5e909a0946a877310fb3d57aea8198bde792aae369ee1c23b5"}, - {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4eea95ef275de7abaef630c9b2c002ffbc01918b726a39f5a4353916ec72d2f3"}, - {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e2f0280519e42b0a17550072861e0bc8a80a0870de260f9796157d3fca2733c5"}, - {file = "coverage-7.6.10-cp310-cp310-win32.whl", hash = "sha256:bc67deb76bc3717f22e765ab3e07ee9c7a5e26b9019ca19a3b063d9f4b874244"}, - {file = "coverage-7.6.10-cp310-cp310-win_amd64.whl", hash = "sha256:0f460286cb94036455e703c66988851d970fdfd8acc2a1122ab7f4f904e4029e"}, - {file = "coverage-7.6.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3"}, - {file = "coverage-7.6.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43"}, - {file = "coverage-7.6.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132"}, - {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f"}, - {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994"}, - {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99"}, - {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd"}, - {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377"}, - {file = "coverage-7.6.10-cp311-cp311-win32.whl", hash = "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8"}, - {file = "coverage-7.6.10-cp311-cp311-win_amd64.whl", hash = "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609"}, - {file = "coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853"}, - {file = "coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078"}, - {file = "coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0"}, - {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50"}, - {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022"}, - {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b"}, - {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0"}, - {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852"}, - {file = "coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359"}, - {file = "coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247"}, - {file = "coverage-7.6.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9"}, - {file = "coverage-7.6.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b"}, - {file = "coverage-7.6.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690"}, - {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18"}, - {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c"}, - {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd"}, - {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e"}, - {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694"}, - {file = "coverage-7.6.10-cp313-cp313-win32.whl", hash = "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6"}, - {file = "coverage-7.6.10-cp313-cp313-win_amd64.whl", hash = "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e"}, - {file = "coverage-7.6.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe"}, - {file = "coverage-7.6.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273"}, - {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8"}, - {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098"}, - {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb"}, - {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0"}, - {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf"}, - {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2"}, - {file = "coverage-7.6.10-cp313-cp313t-win32.whl", hash = "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312"}, - {file = "coverage-7.6.10-cp313-cp313t-win_amd64.whl", hash = "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d"}, - {file = "coverage-7.6.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:656c82b8a0ead8bba147de9a89bda95064874c91a3ed43a00e687f23cc19d53a"}, - {file = "coverage-7.6.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccc2b70a7ed475c68ceb548bf69cec1e27305c1c2606a5eb7c3afff56a1b3b27"}, - {file = "coverage-7.6.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5e37dc41d57ceba70956fa2fc5b63c26dba863c946ace9705f8eca99daecdc4"}, - {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0aa9692b4fdd83a4647eeb7db46410ea1322b5ed94cd1715ef09d1d5922ba87f"}, - {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa744da1820678b475e4ba3dfd994c321c5b13381d1041fe9c608620e6676e25"}, - {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c0b1818063dc9e9d838c09e3a473c1422f517889436dd980f5d721899e66f315"}, - {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:59af35558ba08b758aec4d56182b222976330ef8d2feacbb93964f576a7e7a90"}, - {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7ed2f37cfce1ce101e6dffdfd1c99e729dd2ffc291d02d3e2d0af8b53d13840d"}, - {file = "coverage-7.6.10-cp39-cp39-win32.whl", hash = "sha256:4bcc276261505d82f0ad426870c3b12cb177752834a633e737ec5ee79bbdff18"}, - {file = "coverage-7.6.10-cp39-cp39-win_amd64.whl", hash = "sha256:457574f4599d2b00f7f637a0700a6422243b3565509457b2dbd3f50703e11f59"}, - {file = "coverage-7.6.10-pp39.pp310-none-any.whl", hash = "sha256:fd34e7b3405f0cc7ab03d54a334c17a9e802897580d964bd8c2001f4b9fd488f"}, - {file = "coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23"}, + {file = "coverage-7.6.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:704c8c8c6ce6569286ae9622e534b4f5b9759b6f2cd643f1c1a61f666d534fe8"}, + {file = "coverage-7.6.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ad7525bf0241e5502168ae9c643a2f6c219fa0a283001cee4cf23a9b7da75879"}, + {file = "coverage-7.6.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06097c7abfa611c91edb9e6920264e5be1d6ceb374efb4986f38b09eed4cb2fe"}, + {file = "coverage-7.6.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:220fa6c0ad7d9caef57f2c8771918324563ef0d8272c94974717c3909664e674"}, + {file = "coverage-7.6.12-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3688b99604a24492bcfe1c106278c45586eb819bf66a654d8a9a1433022fb2eb"}, + {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d1a987778b9c71da2fc8948e6f2656da6ef68f59298b7e9786849634c35d2c3c"}, + {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cec6b9ce3bd2b7853d4a4563801292bfee40b030c05a3d29555fd2a8ee9bd68c"}, + {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ace9048de91293e467b44bce0f0381345078389814ff6e18dbac8fdbf896360e"}, + {file = "coverage-7.6.12-cp310-cp310-win32.whl", hash = "sha256:ea31689f05043d520113e0552f039603c4dd71fa4c287b64cb3606140c66f425"}, + {file = "coverage-7.6.12-cp310-cp310-win_amd64.whl", hash = "sha256:676f92141e3c5492d2a1596d52287d0d963df21bf5e55c8b03075a60e1ddf8aa"}, + {file = "coverage-7.6.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e18aafdfb3e9ec0d261c942d35bd7c28d031c5855dadb491d2723ba54f4c3015"}, + {file = "coverage-7.6.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66fe626fd7aa5982cdebad23e49e78ef7dbb3e3c2a5960a2b53632f1f703ea45"}, + {file = "coverage-7.6.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ef01d70198431719af0b1f5dcbefc557d44a190e749004042927b2a3fed0702"}, + {file = "coverage-7.6.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e92ae5a289a4bc4c0aae710c0948d3c7892e20fd3588224ebe242039573bf0"}, + {file = "coverage-7.6.12-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e695df2c58ce526eeab11a2e915448d3eb76f75dffe338ea613c1201b33bab2f"}, + {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d74c08e9aaef995f8c4ef6d202dbd219c318450fe2a76da624f2ebb9c8ec5d9f"}, + {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e995b3b76ccedc27fe4f477b349b7d64597e53a43fc2961db9d3fbace085d69d"}, + {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b1f097878d74fe51e1ddd1be62d8e3682748875b461232cf4b52ddc6e6db0bba"}, + {file = "coverage-7.6.12-cp311-cp311-win32.whl", hash = "sha256:1f7ffa05da41754e20512202c866d0ebfc440bba3b0ed15133070e20bf5aeb5f"}, + {file = "coverage-7.6.12-cp311-cp311-win_amd64.whl", hash = "sha256:e216c5c45f89ef8971373fd1c5d8d1164b81f7f5f06bbf23c37e7908d19e8558"}, + {file = "coverage-7.6.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b172f8e030e8ef247b3104902cc671e20df80163b60a203653150d2fc204d1ad"}, + {file = "coverage-7.6.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:641dfe0ab73deb7069fb972d4d9725bf11c239c309ce694dd50b1473c0f641c3"}, + {file = "coverage-7.6.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e549f54ac5f301e8e04c569dfdb907f7be71b06b88b5063ce9d6953d2d58574"}, + {file = "coverage-7.6.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:959244a17184515f8c52dcb65fb662808767c0bd233c1d8a166e7cf74c9ea985"}, + {file = "coverage-7.6.12-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bda1c5f347550c359f841d6614fb8ca42ae5cb0b74d39f8a1e204815ebe25750"}, + {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ceeb90c3eda1f2d8c4c578c14167dbd8c674ecd7d38e45647543f19839dd6ea"}, + {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f16f44025c06792e0fb09571ae454bcc7a3ec75eeb3c36b025eccf501b1a4c3"}, + {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b076e625396e787448d27a411aefff867db2bffac8ed04e8f7056b07024eed5a"}, + {file = "coverage-7.6.12-cp312-cp312-win32.whl", hash = "sha256:00b2086892cf06c7c2d74983c9595dc511acca00665480b3ddff749ec4fb2a95"}, + {file = "coverage-7.6.12-cp312-cp312-win_amd64.whl", hash = "sha256:7ae6eabf519bc7871ce117fb18bf14e0e343eeb96c377667e3e5dd12095e0288"}, + {file = "coverage-7.6.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:488c27b3db0ebee97a830e6b5a3ea930c4a6e2c07f27a5e67e1b3532e76b9ef1"}, + {file = "coverage-7.6.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d1095bbee1851269f79fd8e0c9b5544e4c00c0c24965e66d8cba2eb5bb535fd"}, + {file = "coverage-7.6.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0533adc29adf6a69c1baa88c3d7dbcaadcffa21afbed3ca7a225a440e4744bf9"}, + {file = "coverage-7.6.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53c56358d470fa507a2b6e67a68fd002364d23c83741dbc4c2e0680d80ca227e"}, + {file = "coverage-7.6.12-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64cbb1a3027c79ca6310bf101014614f6e6e18c226474606cf725238cf5bc2d4"}, + {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79cac3390bfa9836bb795be377395f28410811c9066bc4eefd8015258a7578c6"}, + {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b148068e881faa26d878ff63e79650e208e95cf1c22bd3f77c3ca7b1d9821a3"}, + {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8bec2ac5da793c2685ce5319ca9bcf4eee683b8a1679051f8e6ec04c4f2fd7dc"}, + {file = "coverage-7.6.12-cp313-cp313-win32.whl", hash = "sha256:200e10beb6ddd7c3ded322a4186313d5ca9e63e33d8fab4faa67ef46d3460af3"}, + {file = "coverage-7.6.12-cp313-cp313-win_amd64.whl", hash = "sha256:2b996819ced9f7dbb812c701485d58f261bef08f9b85304d41219b1496b591ef"}, + {file = "coverage-7.6.12-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:299cf973a7abff87a30609879c10df0b3bfc33d021e1adabc29138a48888841e"}, + {file = "coverage-7.6.12-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4b467a8c56974bf06e543e69ad803c6865249d7a5ccf6980457ed2bc50312703"}, + {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2458f275944db8129f95d91aee32c828a408481ecde3b30af31d552c2ce284a0"}, + {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a9d8be07fb0832636a0f72b80d2a652fe665e80e720301fb22b191c3434d924"}, + {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d47376a4f445e9743f6c83291e60adb1b127607a3618e3185bbc8091f0467b"}, + {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b95574d06aa9d2bd6e5cc35a5bbe35696342c96760b69dc4287dbd5abd4ad51d"}, + {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ecea0c38c9079570163d663c0433a9af4094a60aafdca491c6a3d248c7432827"}, + {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2251fabcfee0a55a8578a9d29cecfee5f2de02f11530e7d5c5a05859aa85aee9"}, + {file = "coverage-7.6.12-cp313-cp313t-win32.whl", hash = "sha256:eb5507795caabd9b2ae3f1adc95f67b1104971c22c624bb354232d65c4fc90b3"}, + {file = "coverage-7.6.12-cp313-cp313t-win_amd64.whl", hash = "sha256:f60a297c3987c6c02ffb29effc70eadcbb412fe76947d394a1091a3615948e2f"}, + {file = "coverage-7.6.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e7575ab65ca8399c8c4f9a7d61bbd2d204c8b8e447aab9d355682205c9dd948d"}, + {file = "coverage-7.6.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8161d9fbc7e9fe2326de89cd0abb9f3599bccc1287db0aba285cb68d204ce929"}, + {file = "coverage-7.6.12-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a1e465f398c713f1b212400b4e79a09829cd42aebd360362cd89c5bdc44eb87"}, + {file = "coverage-7.6.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f25d8b92a4e31ff1bd873654ec367ae811b3a943583e05432ea29264782dc32c"}, + {file = "coverage-7.6.12-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a936309a65cc5ca80fa9f20a442ff9e2d06927ec9a4f54bcba9c14c066323f2"}, + {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aa6f302a3a0b5f240ee201297fff0bbfe2fa0d415a94aeb257d8b461032389bd"}, + {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f973643ef532d4f9be71dd88cf7588936685fdb576d93a79fe9f65bc337d9d73"}, + {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:78f5243bb6b1060aed6213d5107744c19f9571ec76d54c99cc15938eb69e0e86"}, + {file = "coverage-7.6.12-cp39-cp39-win32.whl", hash = "sha256:69e62c5034291c845fc4df7f8155e8544178b6c774f97a99e2734b05eb5bed31"}, + {file = "coverage-7.6.12-cp39-cp39-win_amd64.whl", hash = "sha256:b01a840ecc25dce235ae4c1b6a0daefb2a203dba0e6e980637ee9c2f6ee0df57"}, + {file = "coverage-7.6.12-pp39.pp310-none-any.whl", hash = "sha256:7e39e845c4d764208e7b8f6a21c541ade741e2c41afabdfa1caa28687a3c98cf"}, + {file = "coverage-7.6.12-py3-none-any.whl", hash = "sha256:eb8668cfbc279a536c633137deeb9435d2962caec279c3f8cf8b91fff6ff8953"}, + {file = "coverage-7.6.12.tar.gz", hash = "sha256:48cfc4641d95d34766ad41d9573cc0f22a48aa88d22657a1fe01dca0dbae4de2"}, ] [package.dependencies] @@ -492,42 +493,44 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "44.0.0" +version = "44.0.1" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = "!=3.9.0,!=3.9.1,>=3.7" groups = ["main"] markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "cryptography-44.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123"}, - {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092"}, - {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831c3c4d0774e488fdc83a1923b49b9957d33287de923d58ebd3cec47a0ae43f"}, - {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb"}, - {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b"}, - {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543"}, - {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:60eb32934076fa07e4316b7b2742fa52cbb190b42c2df2863dbc4230a0a9b385"}, - {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e"}, - {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e"}, - {file = "cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053"}, - {file = "cryptography-44.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:abc998e0c0eee3c8a1904221d3f67dcfa76422b23620173e28c11d3e626c21bd"}, - {file = "cryptography-44.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:660cb7312a08bc38be15b696462fa7cc7cd85c3ed9c576e81f4dc4d8b2b31591"}, - {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1923cb251c04be85eec9fda837661c67c1049063305d6be5721643c22dd4e2b7"}, - {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:404fdc66ee5f83a1388be54300ae978b2efd538018de18556dde92575e05defc"}, - {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289"}, - {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7"}, - {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c"}, - {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9abcc2e083cbe8dde89124a47e5e53ec38751f0d7dfd36801008f316a127d7ba"}, - {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64"}, - {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285"}, - {file = "cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417"}, - {file = "cryptography-44.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:708ee5f1bafe76d041b53a4f95eb28cdeb8d18da17e597d46d7833ee59b97ede"}, - {file = "cryptography-44.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:37d76e6863da3774cd9db5b409a9ecfd2c71c981c38788d3fcfaf177f447b731"}, - {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:f677e1268c4e23420c3acade68fac427fffcb8d19d7df95ed7ad17cdef8404f4"}, - {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f5e7cb1e5e56ca0933b4873c0220a78b773b24d40d186b6738080b73d3d0a756"}, - {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:8b3e6eae66cf54701ee7d9c83c30ac0a1e3fa17be486033000f2a73a12ab507c"}, - {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:be4ce505894d15d5c5037167ffb7f0ae90b7be6f2a98f9a5c3442395501c32fa"}, - {file = "cryptography-44.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:62901fb618f74d7d81bf408c8719e9ec14d863086efe4185afd07c352aee1d2c"}, - {file = "cryptography-44.0.0.tar.gz", hash = "sha256:cd4e834f340b4293430701e772ec543b0fbe6c2dea510a5286fe0acabe153a02"}, + {file = "cryptography-44.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf688f615c29bfe9dfc44312ca470989279f0e94bb9f631f85e3459af8efc009"}, + {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd7c7e2d71d908dc0f8d2027e1604102140d84b155e658c20e8ad1304317691f"}, + {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:887143b9ff6bad2b7570da75a7fe8bbf5f65276365ac259a5d2d5147a73775f2"}, + {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:322eb03ecc62784536bc173f1483e76747aafeb69c8728df48537eb431cd1911"}, + {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:21377472ca4ada2906bc313168c9dc7b1d7ca417b63c1c3011d0c74b7de9ae69"}, + {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:df978682c1504fc93b3209de21aeabf2375cb1571d4e61907b3e7a2540e83026"}, + {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:eb3889330f2a4a148abead555399ec9a32b13b7c8ba969b72d8e500eb7ef84cd"}, + {file = "cryptography-44.0.1-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:8e6a85a93d0642bd774460a86513c5d9d80b5c002ca9693e63f6e540f1815ed0"}, + {file = "cryptography-44.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6f76fdd6fd048576a04c5210d53aa04ca34d2ed63336d4abd306d0cbe298fddf"}, + {file = "cryptography-44.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6c8acf6f3d1f47acb2248ec3ea261171a671f3d9428e34ad0357148d492c7864"}, + {file = "cryptography-44.0.1-cp37-abi3-win32.whl", hash = "sha256:24979e9f2040c953a94bf3c6782e67795a4c260734e5264dceea65c8f4bae64a"}, + {file = "cryptography-44.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:fd0ee90072861e276b0ff08bd627abec29e32a53b2be44e41dbcdf87cbee2b00"}, + {file = "cryptography-44.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a2d8a7045e1ab9b9f803f0d9531ead85f90c5f2859e653b61497228b18452008"}, + {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8272f257cf1cbd3f2e120f14c68bff2b6bdfcc157fafdee84a1b795efd72862"}, + {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e8d181e90a777b63f3f0caa836844a1182f1f265687fac2115fcf245f5fbec3"}, + {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:436df4f203482f41aad60ed1813811ac4ab102765ecae7a2bbb1dbb66dcff5a7"}, + {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4f422e8c6a28cf8b7f883eb790695d6d45b0c385a2583073f3cec434cc705e1a"}, + {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:72198e2b5925155497a5a3e8c216c7fb3e64c16ccee11f0e7da272fa93b35c4c"}, + {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a46a89ad3e6176223b632056f321bc7de36b9f9b93b2cc1cccf935a3849dc62"}, + {file = "cryptography-44.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:53f23339864b617a3dfc2b0ac8d5c432625c80014c25caac9082314e9de56f41"}, + {file = "cryptography-44.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:888fcc3fce0c888785a4876ca55f9f43787f4c5c1cc1e2e0da71ad481ff82c5b"}, + {file = "cryptography-44.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:00918d859aa4e57db8299607086f793fa7813ae2ff5a4637e318a25ef82730f7"}, + {file = "cryptography-44.0.1-cp39-abi3-win32.whl", hash = "sha256:9b336599e2cb77b1008cb2ac264b290803ec5e8e89d618a5e978ff5eb6f715d9"}, + {file = "cryptography-44.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:e403f7f766ded778ecdb790da786b418a9f2394f36e8cc8b796cc056ab05f44f"}, + {file = "cryptography-44.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1f9a92144fa0c877117e9748c74501bea842f93d21ee00b0cf922846d9d0b183"}, + {file = "cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:610a83540765a8d8ce0f351ce42e26e53e1f774a6efb71eb1b41eb01d01c3d12"}, + {file = "cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5fed5cd6102bb4eb843e3315d2bf25fede494509bddadb81e03a859c1bc17b83"}, + {file = "cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:f4daefc971c2d1f82f03097dc6f216744a6cd2ac0f04c68fb935ea2ba2a0d420"}, + {file = "cryptography-44.0.1-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94f99f2b943b354a5b6307d7e8d19f5c423a794462bde2bf310c770ba052b1c4"}, + {file = "cryptography-44.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d9c5b9f698a83c8bd71e0f4d3f9f839ef244798e5ffe96febfa9714717db7af7"}, + {file = "cryptography-44.0.1.tar.gz", hash = "sha256:f51f5705ab27898afda1aaa430f34ad90dc117421057782022edf0600bec5f14"}, ] [package.dependencies] @@ -540,7 +543,7 @@ nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2)"] pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] sdist = ["build (>=1.0.0)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi (>=2024)", "cryptography-vectors (==44.0.0)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] +test = ["certifi (>=2024)", "cryptography-vectors (==44.0.1)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] test-randomorder = ["pytest-randomly"] [[package]] @@ -560,6 +563,22 @@ files = [ docs = ["ipython", "matplotlib", "numpydoc", "sphinx"] tests = ["pytest", "pytest-cov", "pytest-xdist"] +[[package]] +name = "deepmerge" +version = "2.0" +description = "A toolset for deeply merging Python dictionaries." +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "deepmerge-2.0-py3-none-any.whl", hash = "sha256:6de9ce507115cff0bed95ff0ce9ecc31088ef50cbdf09bc90a09349a318b3d00"}, + {file = "deepmerge-2.0.tar.gz", hash = "sha256:5c3d86081fbebd04dd5de03626a0607b809a98fb6ccba5770b62466fe940ff20"}, +] + +[package.extras] +dev = ["black", "build", "mypy", "pytest", "pyupgrade", "twine", "validate-pyproject[all]"] + [[package]] name = "dnspython" version = "2.7.0" @@ -683,63 +702,63 @@ files = [ [[package]] name = "fonttools" -version = "4.55.8" +version = "4.56.0" description = "Tools to manipulate font files" optional = true python-versions = ">=3.8" groups = ["main"] markers = "python_version <= \"3.11\" and extra == \"plot\" or python_version >= \"3.12\" and extra == \"plot\"" files = [ - {file = "fonttools-4.55.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d11600f5343092697d7434f3bf77a393c7ae74be206fe30e577b9a195fd53165"}, - {file = "fonttools-4.55.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c96f2506ce1a0beeaa9595f9a8b7446477eb133f40c0e41fc078744c28149f80"}, - {file = "fonttools-4.55.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b5f05ef72e846e9f49ccdd74b9da4309901a4248434c63c1ee9321adcb51d65"}, - {file = "fonttools-4.55.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba45b637da80a262b55b7657aec68da2ac54b8ae7891cd977a5dbe5fd26db429"}, - {file = "fonttools-4.55.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:edcffaeadba9a334c1c3866e275d7dd495465e7dbd296f688901bdbd71758113"}, - {file = "fonttools-4.55.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b9f9fce3c9b2196e162182ec5db8af8eb3acd0d76c2eafe9fdba5f370044e556"}, - {file = "fonttools-4.55.8-cp310-cp310-win32.whl", hash = "sha256:f089e8da0990cfe2d67e81d9cf581ff372b48dc5acf2782701844211cd1f0eb3"}, - {file = "fonttools-4.55.8-cp310-cp310-win_amd64.whl", hash = "sha256:01ea3901b0802fc5f9e854f5aeb5bc27770dd9dd24c28df8f74ba90f8b3f5915"}, - {file = "fonttools-4.55.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:95f5a1d4432b3cea6571f5ce4f4e9b25bf36efbd61c32f4f90130a690925d6ee"}, - {file = "fonttools-4.55.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d20f152de7625a0008ba1513f126daaaa0de3b4b9030aa72dd5c27294992260"}, - {file = "fonttools-4.55.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5a3ff5bb95fd5a3962b2754f8435e6d930c84fc9e9921c51e802dddf40acd56"}, - {file = "fonttools-4.55.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b99d4fd2b6d0a00c7336c8363fccc7a11eccef4b17393af75ca6e77cf93ff413"}, - {file = "fonttools-4.55.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d637e4d33e46619c79d1a6c725f74d71b574cd15fb5bbb9b6f3eba8f28363573"}, - {file = "fonttools-4.55.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0f38bfb6b7a39c4162c3eb0820a0bdf8e3bdd125cd54e10ba242397d15e32439"}, - {file = "fonttools-4.55.8-cp311-cp311-win32.whl", hash = "sha256:acfec948de41cd5e640d5c15d0200e8b8e7c5c6bb82afe1ca095cbc4af1188ee"}, - {file = "fonttools-4.55.8-cp311-cp311-win_amd64.whl", hash = "sha256:604c805b41241b4880e2dc86cf2d4754c06777371c8299799ac88d836cb18c3b"}, - {file = "fonttools-4.55.8-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:63403ee0f2fa4e1de28e539f8c24f2bdca1d8ecb503fa9ea2d231d9f1e729809"}, - {file = "fonttools-4.55.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:302e1003a760b222f711d5ba6d1ad7fd5f7f713eb872cd6a3eb44390bc9770af"}, - {file = "fonttools-4.55.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e72a7816ff8a759be9ca36ca46934f8ccf4383711ef597d9240306fe1878cb8d"}, - {file = "fonttools-4.55.8-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03c2b50b54e6e8b3564b232e57e8f58be217cf441cf0155745d9e44a76f9c30f"}, - {file = "fonttools-4.55.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a7230f7590f9570d26ee903b6a4540274494e200fae978df0d9325b7b9144529"}, - {file = "fonttools-4.55.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:466a78984f0572305c3c48377f4e3f7f4e909f1209f45ef8e7041d5c8a744a56"}, - {file = "fonttools-4.55.8-cp312-cp312-win32.whl", hash = "sha256:243cbfc0b7cb1c307af40e321f8343a48d0a080bc1f9466cf2b5468f776ef108"}, - {file = "fonttools-4.55.8-cp312-cp312-win_amd64.whl", hash = "sha256:a19059aa892676822c1f05cb5a67296ecdfeb267fe7c47d4758f3e8e942c2b2a"}, - {file = "fonttools-4.55.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:332883b6280b9d90d2ba7e9e81be77cf2ace696161e60cdcf40cfcd2b3ed06fa"}, - {file = "fonttools-4.55.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6b8d7c149d47b47de7ec81763396c8266e5ebe2e0b14aa9c3ccf29e52260ab2f"}, - {file = "fonttools-4.55.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dfae7c94987149bdaa0388e6c937566aa398fa0eec973b17952350a069cff4e"}, - {file = "fonttools-4.55.8-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0fe12f06169af2fdc642d26a8df53e40adc3beedbd6ffedb19f1c5397b63afd"}, - {file = "fonttools-4.55.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f971aa5f50c22dc4b63a891503624ae2c77330429b34ead32f23c2260c5618cd"}, - {file = "fonttools-4.55.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:708cb17b2590b7f6c6854999df0039ff1140dda9e6f56d67c3599ba6f968fab5"}, - {file = "fonttools-4.55.8-cp313-cp313-win32.whl", hash = "sha256:cfe9cf30f391a0f2875247a3e5e44d8dcb61596e5cf89b360cdffec8a80e9961"}, - {file = "fonttools-4.55.8-cp313-cp313-win_amd64.whl", hash = "sha256:1e10efc8ee10d6f1fe2931d41bccc90cd4b872f2ee4ff21f2231a2c293b2dbf8"}, - {file = "fonttools-4.55.8-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:9b6fcff4dc755b32faff955d989ee26394ddad3a90ea7d558db17a4633c8390c"}, - {file = "fonttools-4.55.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:02c41322e5bdcb484b61b776fcea150215c83619b39c96aa0b44d4fd87bb5574"}, - {file = "fonttools-4.55.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9164f44add0acec0f12fce682824c040dc52e483bfe3838c37142897150c8364"}, - {file = "fonttools-4.55.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2248ebfbcea0d0b3cb459d76a9f67f2eadc10ec0d07e9cadab8777d3f016bf2"}, - {file = "fonttools-4.55.8-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:3461347016c94cb42b36caa907e11565878c4c2c375604f3651d11dc06d1ab3e"}, - {file = "fonttools-4.55.8-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:67df1c3935838fb9e56f227d7f506c9043b149a4a3b667bef17929c7a1114d19"}, - {file = "fonttools-4.55.8-cp38-cp38-win32.whl", hash = "sha256:cb121d6dd34625cece32234a5fa0359475bb118838b6b4295ffdb13b935edb04"}, - {file = "fonttools-4.55.8-cp38-cp38-win_amd64.whl", hash = "sha256:285c1ac10c160fbdff6d05358230e66c4f98cbbf271f3ec7eb34e967771543e8"}, - {file = "fonttools-4.55.8-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8abd135e427d88e461a4833c03cf96cfb9028c78c15d58123291f22398e25492"}, - {file = "fonttools-4.55.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:65cb8f97eed7906dcf19bc2736b70c6239e9d7e77aad7c6110ba7239ae082e81"}, - {file = "fonttools-4.55.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:450c354c04a6e12a3db968e915fe05730f79ff3d39560947ef8ee6eaa2ab2212"}, - {file = "fonttools-4.55.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2232012a1502b2b8ab4c6bc1d3524bfe90238c0c1a50ac94a0a2085aa87a58a5"}, - {file = "fonttools-4.55.8-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d39f0c977639be0f9f5505d4c7c478236737f960c567a35f058649c056e41434"}, - {file = "fonttools-4.55.8-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:de78d6d0dbe32561ce059265437021f4746e56073c4799f0f1095828ae7232bd"}, - {file = "fonttools-4.55.8-cp39-cp39-win32.whl", hash = "sha256:bf4b5b3496ddfdd4e57112e77ec51f1ab388d35ac17322c1248addb2eb0d429a"}, - {file = "fonttools-4.55.8-cp39-cp39-win_amd64.whl", hash = "sha256:ccf8ae02918f431953d338db4d0a675a395faf82bab3a76025582cf32a2f3b7b"}, - {file = "fonttools-4.55.8-py3-none-any.whl", hash = "sha256:07636dae94f7fe88561f9da7a46b13d8e3f529f87fdb221b11d85f91eabceeb7"}, - {file = "fonttools-4.55.8.tar.gz", hash = "sha256:54d481d456dcd59af25d4a9c56b2c4c3f20e9620b261b84144e5950f33e8df17"}, + {file = "fonttools-4.56.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:331954d002dbf5e704c7f3756028e21db07097c19722569983ba4d74df014000"}, + {file = "fonttools-4.56.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8d1613abd5af2f93c05867b3a3759a56e8bf97eb79b1da76b2bc10892f96ff16"}, + {file = "fonttools-4.56.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:705837eae384fe21cee5e5746fd4f4b2f06f87544fa60f60740007e0aa600311"}, + {file = "fonttools-4.56.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc871904a53a9d4d908673c6faa15689874af1c7c5ac403a8e12d967ebd0c0dc"}, + {file = "fonttools-4.56.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:38b947de71748bab150259ee05a775e8a0635891568e9fdb3cdd7d0e0004e62f"}, + {file = "fonttools-4.56.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:86b2a1013ef7a64d2e94606632683f07712045ed86d937c11ef4dde97319c086"}, + {file = "fonttools-4.56.0-cp310-cp310-win32.whl", hash = "sha256:133bedb9a5c6376ad43e6518b7e2cd2f866a05b1998f14842631d5feb36b5786"}, + {file = "fonttools-4.56.0-cp310-cp310-win_amd64.whl", hash = "sha256:17f39313b649037f6c800209984a11fc256a6137cbe5487091c6c7187cae4685"}, + {file = "fonttools-4.56.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ef04bc7827adb7532be3d14462390dd71287644516af3f1e67f1e6ff9c6d6df"}, + {file = "fonttools-4.56.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ffda9b8cd9cb8b301cae2602ec62375b59e2e2108a117746f12215145e3f786c"}, + {file = "fonttools-4.56.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e993e8db36306cc3f1734edc8ea67906c55f98683d6fd34c3fc5593fdbba4c"}, + {file = "fonttools-4.56.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:003548eadd674175510773f73fb2060bb46adb77c94854af3e0cc5bc70260049"}, + {file = "fonttools-4.56.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd9825822e7bb243f285013e653f6741954d8147427aaa0324a862cdbf4cbf62"}, + {file = "fonttools-4.56.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b23d30a2c0b992fb1c4f8ac9bfde44b5586d23457759b6cf9a787f1a35179ee0"}, + {file = "fonttools-4.56.0-cp311-cp311-win32.whl", hash = "sha256:47b5e4680002ae1756d3ae3b6114e20aaee6cc5c69d1e5911f5ffffd3ee46c6b"}, + {file = "fonttools-4.56.0-cp311-cp311-win_amd64.whl", hash = "sha256:14a3e3e6b211660db54ca1ef7006401e4a694e53ffd4553ab9bc87ead01d0f05"}, + {file = "fonttools-4.56.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d6f195c14c01bd057bc9b4f70756b510e009c83c5ea67b25ced3e2c38e6ee6e9"}, + {file = "fonttools-4.56.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fa760e5fe8b50cbc2d71884a1eff2ed2b95a005f02dda2fa431560db0ddd927f"}, + {file = "fonttools-4.56.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d54a45d30251f1d729e69e5b675f9a08b7da413391a1227781e2a297fa37f6d2"}, + {file = "fonttools-4.56.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:661a8995d11e6e4914a44ca7d52d1286e2d9b154f685a4d1f69add8418961563"}, + {file = "fonttools-4.56.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9d94449ad0a5f2a8bf5d2f8d71d65088aee48adbe45f3c5f8e00e3ad861ed81a"}, + {file = "fonttools-4.56.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f59746f7953f69cc3290ce2f971ab01056e55ddd0fb8b792c31a8acd7fee2d28"}, + {file = "fonttools-4.56.0-cp312-cp312-win32.whl", hash = "sha256:bce60f9a977c9d3d51de475af3f3581d9b36952e1f8fc19a1f2254f1dda7ce9c"}, + {file = "fonttools-4.56.0-cp312-cp312-win_amd64.whl", hash = "sha256:300c310bb725b2bdb4f5fc7e148e190bd69f01925c7ab437b9c0ca3e1c7cd9ba"}, + {file = "fonttools-4.56.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f20e2c0dfab82983a90f3d00703ac0960412036153e5023eed2b4641d7d5e692"}, + {file = "fonttools-4.56.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f36a0868f47b7566237640c026c65a86d09a3d9ca5df1cd039e30a1da73098a0"}, + {file = "fonttools-4.56.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62b4c6802fa28e14dba010e75190e0e6228513573f1eeae57b11aa1a39b7e5b1"}, + {file = "fonttools-4.56.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a05d1f07eb0a7d755fbe01fee1fd255c3a4d3730130cf1bfefb682d18fd2fcea"}, + {file = "fonttools-4.56.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0073b62c3438cf0058488c002ea90489e8801d3a7af5ce5f7c05c105bee815c3"}, + {file = "fonttools-4.56.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e2cad98c94833465bcf28f51c248aaf07ca022efc6a3eba750ad9c1e0256d278"}, + {file = "fonttools-4.56.0-cp313-cp313-win32.whl", hash = "sha256:d0cb73ccf7f6d7ca8d0bc7ea8ac0a5b84969a41c56ac3ac3422a24df2680546f"}, + {file = "fonttools-4.56.0-cp313-cp313-win_amd64.whl", hash = "sha256:62cc1253827d1e500fde9dbe981219fea4eb000fd63402283472d38e7d8aa1c6"}, + {file = "fonttools-4.56.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3fd3fccb7b9adaaecfa79ad51b759f2123e1aba97f857936ce044d4f029abd71"}, + {file = "fonttools-4.56.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:193b86e9f769320bc98ffdb42accafb5d0c8c49bd62884f1c0702bc598b3f0a2"}, + {file = "fonttools-4.56.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e81c1cc80c1d8bf071356cc3e0e25071fbba1c75afc48d41b26048980b3c771"}, + {file = "fonttools-4.56.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9270505a19361e81eecdbc2c251ad1e1a9a9c2ad75fa022ccdee533f55535dc"}, + {file = "fonttools-4.56.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:53f5e9767978a4daf46f28e09dbeb7d010319924ae622f7b56174b777258e5ba"}, + {file = "fonttools-4.56.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:9da650cb29bc098b8cfd15ef09009c914b35c7986c8fa9f08b51108b7bc393b4"}, + {file = "fonttools-4.56.0-cp38-cp38-win32.whl", hash = "sha256:965d0209e6dbdb9416100123b6709cb13f5232e2d52d17ed37f9df0cc31e2b35"}, + {file = "fonttools-4.56.0-cp38-cp38-win_amd64.whl", hash = "sha256:654ac4583e2d7c62aebc6fc6a4c6736f078f50300e18aa105d87ce8925cfac31"}, + {file = "fonttools-4.56.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ca7962e8e5fc047cc4e59389959843aafbf7445b6c08c20d883e60ced46370a5"}, + {file = "fonttools-4.56.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1af375734018951c31c0737d04a9d5fd0a353a0253db5fbed2ccd44eac62d8c"}, + {file = "fonttools-4.56.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:442ad4122468d0e47d83bc59d0e91b474593a8c813839e1872e47c7a0cb53b10"}, + {file = "fonttools-4.56.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cf4f8d2a30b454ac682e12c61831dcb174950c406011418e739de592bbf8f76"}, + {file = "fonttools-4.56.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:96a4271f63a615bcb902b9f56de00ea225d6896052c49f20d0c91e9f43529a29"}, + {file = "fonttools-4.56.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6c1d38642ca2dddc7ae992ef5d026e5061a84f10ff2b906be5680ab089f55bb8"}, + {file = "fonttools-4.56.0-cp39-cp39-win32.whl", hash = "sha256:2d351275f73ebdd81dd5b09a8b8dac7a30f29a279d41e1c1192aedf1b6dced40"}, + {file = "fonttools-4.56.0-cp39-cp39-win_amd64.whl", hash = "sha256:d6ca96d1b61a707ba01a43318c9c40aaf11a5a568d1e61146fafa6ab20890793"}, + {file = "fonttools-4.56.0-py3-none-any.whl", hash = "sha256:1088182f68c303b50ca4dc0c82d42083d176cba37af1937e1a976a31149d4d14"}, + {file = "fonttools-4.56.0.tar.gz", hash = "sha256:a114d1567e1a1586b7e9e7fc2ff686ca542a82769a296cef131e4c4af51e58f4"}, ] [package.extras] @@ -1301,22 +1320,22 @@ files = [ [[package]] name = "narwhals" -version = "1.24.2" +version = "1.27.1" description = "Extremely lightweight compatibility layer between dataframe libraries" optional = true python-versions = ">=3.8" groups = ["main"] markers = "python_version <= \"3.11\" and extra == \"plot\" or python_version >= \"3.12\" and extra == \"plot\"" files = [ - {file = "narwhals-1.24.2-py3-none-any.whl", hash = "sha256:5d70dda4c032c86d0bda3f4d60a9d3f8dbd7d83deb8d23d68dd2f2eb3bc86417"}, - {file = "narwhals-1.24.2.tar.gz", hash = "sha256:c7bbb96ec8efb22ee384872e4a57cff591df653dc49067c314f88e2a54d8a76b"}, + {file = "narwhals-1.27.1-py3-none-any.whl", hash = "sha256:71e4a126007886e3dd9d71d0d5921ebd2e8c1f9be9c405fe11850ece2b066c59"}, + {file = "narwhals-1.27.1.tar.gz", hash = "sha256:68505d0cee1e6c00382ac8b65e922f8b694a11cbe482a057fa63139de8d0ea03"}, ] [package.extras] core = ["duckdb", "pandas", "polars", "pyarrow", "pyarrow-stubs"] cudf = ["cudf (>=24.10.0)"] dask = ["dask[dataframe] (>=2024.8)"] -dev = ["covdefaults", "hypothesis", "pre-commit", "pytest", "pytest-cov", "pytest-env", "pytest-randomly", "typing-extensions"] +dev = ["covdefaults", "hypothesis", "mypy (>=1.15.0,<1.16.0)", "pandas-stubs", "pre-commit", "pytest", "pytest-cov", "pytest-env", "pytest-randomly", "typing-extensions"] docs = ["black", "duckdb", "jinja2", "markdown-exec[ansi]", "mkdocs", "mkdocs-autorefs", "mkdocs-material", "mkdocstrings[python]", "pandas", "polars (>=1.0.0)", "pyarrow"] duckdb = ["duckdb (>=1.0)"] extra = ["scikit-learn"] @@ -1326,71 +1345,73 @@ pandas = ["pandas (>=0.25.3)"] polars = ["polars (>=0.20.3)"] pyarrow = ["pyarrow (>=11.0.0)"] pyspark = ["pyspark (>=3.5.0)"] +tests = ["covdefaults", "hypothesis", "pytest", "pytest-cov", "pytest-env", "pytest-randomly", "typing-extensions"] +typing = ["mypy (>=1.15.0,<1.16.0)", "pandas-stubs", "typing-extensions"] [[package]] name = "numpy" -version = "2.2.2" +version = "2.2.3" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.10" groups = ["main"] markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "numpy-2.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7079129b64cb78bdc8d611d1fd7e8002c0a2565da6a47c4df8062349fee90e3e"}, - {file = "numpy-2.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2ec6c689c61df613b783aeb21f945c4cbe6c51c28cb70aae8430577ab39f163e"}, - {file = "numpy-2.2.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:40c7ff5da22cd391944a28c6a9c638a5eef77fcf71d6e3a79e1d9d9e82752715"}, - {file = "numpy-2.2.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:995f9e8181723852ca458e22de5d9b7d3ba4da3f11cc1cb113f093b271d7965a"}, - {file = "numpy-2.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b78ea78450fd96a498f50ee096f69c75379af5138f7881a51355ab0e11286c97"}, - {file = "numpy-2.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3fbe72d347fbc59f94124125e73fc4976a06927ebc503ec5afbfb35f193cd957"}, - {file = "numpy-2.2.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8e6da5cffbbe571f93588f562ed130ea63ee206d12851b60819512dd3e1ba50d"}, - {file = "numpy-2.2.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:09d6a2032faf25e8d0cadde7fd6145118ac55d2740132c1d845f98721b5ebcfd"}, - {file = "numpy-2.2.2-cp310-cp310-win32.whl", hash = "sha256:159ff6ee4c4a36a23fe01b7c3d07bd8c14cc433d9720f977fcd52c13c0098160"}, - {file = "numpy-2.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:64bd6e1762cd7f0986a740fee4dff927b9ec2c5e4d9a28d056eb17d332158014"}, - {file = "numpy-2.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:642199e98af1bd2b6aeb8ecf726972d238c9877b0f6e8221ee5ab945ec8a2189"}, - {file = "numpy-2.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6d9fc9d812c81e6168b6d405bf00b8d6739a7f72ef22a9214c4241e0dc70b323"}, - {file = "numpy-2.2.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:c7d1fd447e33ee20c1f33f2c8e6634211124a9aabde3c617687d8b739aa69eac"}, - {file = "numpy-2.2.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:451e854cfae0febe723077bd0cf0a4302a5d84ff25f0bfece8f29206c7bed02e"}, - {file = "numpy-2.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd249bc894af67cbd8bad2c22e7cbcd46cf87ddfca1f1289d1e7e54868cc785c"}, - {file = "numpy-2.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02935e2c3c0c6cbe9c7955a8efa8908dd4221d7755644c59d1bba28b94fd334f"}, - {file = "numpy-2.2.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a972cec723e0563aa0823ee2ab1df0cb196ed0778f173b381c871a03719d4826"}, - {file = "numpy-2.2.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d6d6a0910c3b4368d89dde073e630882cdb266755565155bc33520283b2d9df8"}, - {file = "numpy-2.2.2-cp311-cp311-win32.whl", hash = "sha256:860fd59990c37c3ef913c3ae390b3929d005243acca1a86facb0773e2d8d9e50"}, - {file = "numpy-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:da1eeb460ecce8d5b8608826595c777728cdf28ce7b5a5a8c8ac8d949beadcf2"}, - {file = "numpy-2.2.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ac9bea18d6d58a995fac1b2cb4488e17eceeac413af014b1dd26170b766d8467"}, - {file = "numpy-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23ae9f0c2d889b7b2d88a3791f6c09e2ef827c2446f1c4a3e3e76328ee4afd9a"}, - {file = "numpy-2.2.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3074634ea4d6df66be04f6728ee1d173cfded75d002c75fac79503a880bf3825"}, - {file = "numpy-2.2.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:8ec0636d3f7d68520afc6ac2dc4b8341ddb725039de042faf0e311599f54eb37"}, - {file = "numpy-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ffbb1acd69fdf8e89dd60ef6182ca90a743620957afb7066385a7bbe88dc748"}, - {file = "numpy-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0349b025e15ea9d05c3d63f9657707a4e1d471128a3b1d876c095f328f8ff7f0"}, - {file = "numpy-2.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:463247edcee4a5537841d5350bc87fe8e92d7dd0e8c71c995d2c6eecb8208278"}, - {file = "numpy-2.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9dd47ff0cb2a656ad69c38da850df3454da88ee9a6fde0ba79acceee0e79daba"}, - {file = "numpy-2.2.2-cp312-cp312-win32.whl", hash = "sha256:4525b88c11906d5ab1b0ec1f290996c0020dd318af8b49acaa46f198b1ffc283"}, - {file = "numpy-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:5acea83b801e98541619af398cc0109ff48016955cc0818f478ee9ef1c5c3dcb"}, - {file = "numpy-2.2.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b208cfd4f5fe34e1535c08983a1a6803fdbc7a1e86cf13dd0c61de0b51a0aadc"}, - {file = "numpy-2.2.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d0bbe7dd86dca64854f4b6ce2ea5c60b51e36dfd597300057cf473d3615f2369"}, - {file = "numpy-2.2.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:22ea3bb552ade325530e72a0c557cdf2dea8914d3a5e1fecf58fa5dbcc6f43cd"}, - {file = "numpy-2.2.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:128c41c085cab8a85dc29e66ed88c05613dccf6bc28b3866cd16050a2f5448be"}, - {file = "numpy-2.2.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:250c16b277e3b809ac20d1f590716597481061b514223c7badb7a0f9993c7f84"}, - {file = "numpy-2.2.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0c8854b09bc4de7b041148d8550d3bd712b5c21ff6a8ed308085f190235d7ff"}, - {file = "numpy-2.2.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b6fb9c32a91ec32a689ec6410def76443e3c750e7cfc3fb2206b985ffb2b85f0"}, - {file = "numpy-2.2.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:57b4012e04cc12b78590a334907e01b3a85efb2107df2b8733ff1ed05fce71de"}, - {file = "numpy-2.2.2-cp313-cp313-win32.whl", hash = "sha256:4dbd80e453bd34bd003b16bd802fac70ad76bd463f81f0c518d1245b1c55e3d9"}, - {file = "numpy-2.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:5a8c863ceacae696aff37d1fd636121f1a512117652e5dfb86031c8d84836369"}, - {file = "numpy-2.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:b3482cb7b3325faa5f6bc179649406058253d91ceda359c104dac0ad320e1391"}, - {file = "numpy-2.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9491100aba630910489c1d0158034e1c9a6546f0b1340f716d522dc103788e39"}, - {file = "numpy-2.2.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:41184c416143defa34cc8eb9d070b0a5ba4f13a0fa96a709e20584638254b317"}, - {file = "numpy-2.2.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7dca87ca328f5ea7dafc907c5ec100d187911f94825f8700caac0b3f4c384b49"}, - {file = "numpy-2.2.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bc61b307655d1a7f9f4b043628b9f2b721e80839914ede634e3d485913e1fb2"}, - {file = "numpy-2.2.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fad446ad0bc886855ddf5909cbf8cb5d0faa637aaa6277fb4b19ade134ab3c7"}, - {file = "numpy-2.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:149d1113ac15005652e8d0d3f6fd599360e1a708a4f98e43c9c77834a28238cb"}, - {file = "numpy-2.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:106397dbbb1896f99e044efc90360d098b3335060375c26aa89c0d8a97c5f648"}, - {file = "numpy-2.2.2-cp313-cp313t-win32.whl", hash = "sha256:0eec19f8af947a61e968d5429f0bd92fec46d92b0008d0a6685b40d6adf8a4f4"}, - {file = "numpy-2.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:97b974d3ba0fb4612b77ed35d7627490e8e3dff56ab41454d9e8b23448940576"}, - {file = "numpy-2.2.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b0531f0b0e07643eb089df4c509d30d72c9ef40defa53e41363eca8a8cc61495"}, - {file = "numpy-2.2.2-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:e9e82dcb3f2ebbc8cb5ce1102d5f1c5ed236bf8a11730fb45ba82e2841ec21df"}, - {file = "numpy-2.2.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0d4142eb40ca6f94539e4db929410f2a46052a0fe7a2c1c59f6179c39938d2a"}, - {file = "numpy-2.2.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:356ca982c188acbfa6af0d694284d8cf20e95b1c3d0aefa8929376fea9146f60"}, - {file = "numpy-2.2.2.tar.gz", hash = "sha256:ed6906f61834d687738d25988ae117683705636936cc605be0bb208b23df4d8f"}, + {file = "numpy-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cbc6472e01952d3d1b2772b720428f8b90e2deea8344e854df22b0618e9cce71"}, + {file = "numpy-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdfe0c22692a30cd830c0755746473ae66c4a8f2e7bd508b35fb3b6a0813d787"}, + {file = "numpy-2.2.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:e37242f5324ffd9f7ba5acf96d774f9276aa62a966c0bad8dae692deebec7716"}, + {file = "numpy-2.2.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:95172a21038c9b423e68be78fd0be6e1b97674cde269b76fe269a5dfa6fadf0b"}, + {file = "numpy-2.2.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5b47c440210c5d1d67e1cf434124e0b5c395eee1f5806fdd89b553ed1acd0a3"}, + {file = "numpy-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0391ea3622f5c51a2e29708877d56e3d276827ac5447d7f45e9bc4ade8923c52"}, + {file = "numpy-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f6b3dfc7661f8842babd8ea07e9897fe3d9b69a1d7e5fbb743e4160f9387833b"}, + {file = "numpy-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1ad78ce7f18ce4e7df1b2ea4019b5817a2f6a8a16e34ff2775f646adce0a5027"}, + {file = "numpy-2.2.3-cp310-cp310-win32.whl", hash = "sha256:5ebeb7ef54a7be11044c33a17b2624abe4307a75893c001a4800857956b41094"}, + {file = "numpy-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:596140185c7fa113563c67c2e894eabe0daea18cf8e33851738c19f70ce86aeb"}, + {file = "numpy-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:16372619ee728ed67a2a606a614f56d3eabc5b86f8b615c79d01957062826ca8"}, + {file = "numpy-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5521a06a3148686d9269c53b09f7d399a5725c47bbb5b35747e1cb76326b714b"}, + {file = "numpy-2.2.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:7c8dde0ca2f77828815fd1aedfdf52e59071a5bae30dac3b4da2a335c672149a"}, + {file = "numpy-2.2.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:77974aba6c1bc26e3c205c2214f0d5b4305bdc719268b93e768ddb17e3fdd636"}, + {file = "numpy-2.2.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d42f9c36d06440e34226e8bd65ff065ca0963aeecada587b937011efa02cdc9d"}, + {file = "numpy-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2712c5179f40af9ddc8f6727f2bd910ea0eb50206daea75f58ddd9fa3f715bb"}, + {file = "numpy-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c8b0451d2ec95010d1db8ca733afc41f659f425b7f608af569711097fd6014e2"}, + {file = "numpy-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9b4a8148c57ecac25a16b0e11798cbe88edf5237b0df99973687dd866f05e1b"}, + {file = "numpy-2.2.3-cp311-cp311-win32.whl", hash = "sha256:1f45315b2dc58d8a3e7754fe4e38b6fce132dab284a92851e41b2b344f6441c5"}, + {file = "numpy-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f48ba6f6c13e5e49f3d3efb1b51c8193215c42ac82610a04624906a9270be6f"}, + {file = "numpy-2.2.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12c045f43b1d2915eca6b880a7f4a256f59d62df4f044788c8ba67709412128d"}, + {file = "numpy-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:87eed225fd415bbae787f93a457af7f5990b92a334e346f72070bf569b9c9c95"}, + {file = "numpy-2.2.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:712a64103d97c404e87d4d7c47fb0c7ff9acccc625ca2002848e0d53288b90ea"}, + {file = "numpy-2.2.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a5ae282abe60a2db0fd407072aff4599c279bcd6e9a2475500fc35b00a57c532"}, + {file = "numpy-2.2.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5266de33d4c3420973cf9ae3b98b54a2a6d53a559310e3236c4b2b06b9c07d4e"}, + {file = "numpy-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b787adbf04b0db1967798dba8da1af07e387908ed1553a0d6e74c084d1ceafe"}, + {file = "numpy-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:34c1b7e83f94f3b564b35f480f5652a47007dd91f7c839f404d03279cc8dd021"}, + {file = "numpy-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4d8335b5f1b6e2bce120d55fb17064b0262ff29b459e8493d1785c18ae2553b8"}, + {file = "numpy-2.2.3-cp312-cp312-win32.whl", hash = "sha256:4d9828d25fb246bedd31e04c9e75714a4087211ac348cb39c8c5f99dbb6683fe"}, + {file = "numpy-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:83807d445817326b4bcdaaaf8e8e9f1753da04341eceec705c001ff342002e5d"}, + {file = "numpy-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bfdb06b395385ea9b91bf55c1adf1b297c9fdb531552845ff1d3ea6e40d5aba"}, + {file = "numpy-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:23c9f4edbf4c065fddb10a4f6e8b6a244342d95966a48820c614891e5059bb50"}, + {file = "numpy-2.2.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:a0c03b6be48aaf92525cccf393265e02773be8fd9551a2f9adbe7db1fa2b60f1"}, + {file = "numpy-2.2.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:2376e317111daa0a6739e50f7ee2a6353f768489102308b0d98fcf4a04f7f3b5"}, + {file = "numpy-2.2.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8fb62fe3d206d72fe1cfe31c4a1106ad2b136fcc1606093aeab314f02930fdf2"}, + {file = "numpy-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52659ad2534427dffcc36aac76bebdd02b67e3b7a619ac67543bc9bfe6b7cdb1"}, + {file = "numpy-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b416af7d0ed3271cad0f0a0d0bee0911ed7eba23e66f8424d9f3dfcdcae1304"}, + {file = "numpy-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1402da8e0f435991983d0a9708b779f95a8c98c6b18a171b9f1be09005e64d9d"}, + {file = "numpy-2.2.3-cp313-cp313-win32.whl", hash = "sha256:136553f123ee2951bfcfbc264acd34a2fc2f29d7cdf610ce7daf672b6fbaa693"}, + {file = "numpy-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:5b732c8beef1d7bc2d9e476dbba20aaff6167bf205ad9aa8d30913859e82884b"}, + {file = "numpy-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:435e7a933b9fda8126130b046975a968cc2d833b505475e588339e09f7672890"}, + {file = "numpy-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7678556eeb0152cbd1522b684dcd215250885993dd00adb93679ec3c0e6e091c"}, + {file = "numpy-2.2.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2e8da03bd561504d9b20e7a12340870dfc206c64ea59b4cfee9fceb95070ee94"}, + {file = "numpy-2.2.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:c9aa4496fd0e17e3843399f533d62857cef5900facf93e735ef65aa4bbc90ef0"}, + {file = "numpy-2.2.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4ca91d61a4bf61b0f2228f24bbfa6a9facd5f8af03759fe2a655c50ae2c6610"}, + {file = "numpy-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:deaa09cd492e24fd9b15296844c0ad1b3c976da7907e1c1ed3a0ad21dded6f76"}, + {file = "numpy-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:246535e2f7496b7ac85deffe932896a3577be7af8fb7eebe7146444680297e9a"}, + {file = "numpy-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:daf43a3d1ea699402c5a850e5313680ac355b4adc9770cd5cfc2940e7861f1bf"}, + {file = "numpy-2.2.3-cp313-cp313t-win32.whl", hash = "sha256:cf802eef1f0134afb81fef94020351be4fe1d6681aadf9c5e862af6602af64ef"}, + {file = "numpy-2.2.3-cp313-cp313t-win_amd64.whl", hash = "sha256:aee2512827ceb6d7f517c8b85aa5d3923afe8fc7a57d028cffcd522f1c6fd082"}, + {file = "numpy-2.2.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3c2ec8a0f51d60f1e9c0c5ab116b7fc104b165ada3f6c58abf881cb2eb16044d"}, + {file = "numpy-2.2.3-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:ed2cf9ed4e8ebc3b754d398cba12f24359f018b416c380f577bbae112ca52fc9"}, + {file = "numpy-2.2.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39261798d208c3095ae4f7bc8eaeb3481ea8c6e03dc48028057d3cbdbdb8937e"}, + {file = "numpy-2.2.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:783145835458e60fa97afac25d511d00a1eca94d4a8f3ace9fe2043003c678e4"}, + {file = "numpy-2.2.3.tar.gz", hash = "sha256:dbdc15f0c81611925f382dfa97b3bd0bc2c1ce19d4fe50482cb0ddc12ba30020"}, ] [[package]] @@ -2289,31 +2310,31 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" -version = "0.9.5" +version = "0.9.6" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" groups = ["dev"] markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "ruff-0.9.5-py3-none-linux_armv6l.whl", hash = "sha256:d466d2abc05f39018d53f681fa1c0ffe9570e6d73cde1b65d23bb557c846f442"}, - {file = "ruff-0.9.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:38840dbcef63948657fa7605ca363194d2fe8c26ce8f9ae12eee7f098c85ac8a"}, - {file = "ruff-0.9.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d56ba06da53536b575fbd2b56517f6f95774ff7be0f62c80b9e67430391eeb36"}, - {file = "ruff-0.9.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f7cb2a01da08244c50b20ccfaeb5972e4228c3c3a1989d3ece2bc4b1f996001"}, - {file = "ruff-0.9.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:96d5c76358419bc63a671caac70c18732d4fd0341646ecd01641ddda5c39ca0b"}, - {file = "ruff-0.9.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:deb8304636ed394211f3a6d46c0e7d9535b016f53adaa8340139859b2359a070"}, - {file = "ruff-0.9.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:df455000bf59e62b3e8c7ba5ed88a4a2bc64896f900f311dc23ff2dc38156440"}, - {file = "ruff-0.9.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de92170dfa50c32a2b8206a647949590e752aca8100a0f6b8cefa02ae29dce80"}, - {file = "ruff-0.9.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d28532d73b1f3f627ba88e1456f50748b37f3a345d2be76e4c653bec6c3e393"}, - {file = "ruff-0.9.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c746d7d1df64f31d90503ece5cc34d7007c06751a7a3bbeee10e5f2463d52d2"}, - {file = "ruff-0.9.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:11417521d6f2d121fda376f0d2169fb529976c544d653d1d6044f4c5562516ee"}, - {file = "ruff-0.9.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b9d71c3879eb32de700f2f6fac3d46566f644a91d3130119a6378f9312a38e1"}, - {file = "ruff-0.9.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2e36c61145e70febcb78483903c43444c6b9d40f6d2f800b5552fec6e4a7bb9a"}, - {file = "ruff-0.9.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2f71d09aeba026c922aa7aa19a08d7bd27c867aedb2f74285a2639644c1c12f5"}, - {file = "ruff-0.9.5-py3-none-win32.whl", hash = "sha256:134f958d52aa6fdec3b294b8ebe2320a950d10c041473c4316d2e7d7c2544723"}, - {file = "ruff-0.9.5-py3-none-win_amd64.whl", hash = "sha256:78cc6067f6d80b6745b67498fb84e87d32c6fc34992b52bffefbdae3442967d6"}, - {file = "ruff-0.9.5-py3-none-win_arm64.whl", hash = "sha256:18a29f1a005bddb229e580795627d297dfa99f16b30c7039e73278cf6b5f9fa9"}, - {file = "ruff-0.9.5.tar.gz", hash = "sha256:11aecd7a633932875ab3cb05a484c99970b9d52606ce9ea912b690b02653d56c"}, + {file = "ruff-0.9.6-py3-none-linux_armv6l.whl", hash = "sha256:2f218f356dd2d995839f1941322ff021c72a492c470f0b26a34f844c29cdf5ba"}, + {file = "ruff-0.9.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b908ff4df65dad7b251c9968a2e4560836d8f5487c2f0cc238321ed951ea0504"}, + {file = "ruff-0.9.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b109c0ad2ececf42e75fa99dc4043ff72a357436bb171900714a9ea581ddef83"}, + {file = "ruff-0.9.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1de4367cca3dac99bcbd15c161404e849bb0bfd543664db39232648dc00112dc"}, + {file = "ruff-0.9.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac3ee4d7c2c92ddfdaedf0bf31b2b176fa7aa8950efc454628d477394d35638b"}, + {file = "ruff-0.9.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5dc1edd1775270e6aa2386119aea692039781429f0be1e0949ea5884e011aa8e"}, + {file = "ruff-0.9.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4a091729086dffa4bd070aa5dab7e39cc6b9d62eb2bef8f3d91172d30d599666"}, + {file = "ruff-0.9.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1bbc6808bf7b15796cef0815e1dfb796fbd383e7dbd4334709642649625e7c5"}, + {file = "ruff-0.9.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:589d1d9f25b5754ff230dce914a174a7c951a85a4e9270613a2b74231fdac2f5"}, + {file = "ruff-0.9.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc61dd5131742e21103fbbdcad683a8813be0e3c204472d520d9a5021ca8b217"}, + {file = "ruff-0.9.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5e2d9126161d0357e5c8f30b0bd6168d2c3872372f14481136d13de9937f79b6"}, + {file = "ruff-0.9.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:68660eab1a8e65babb5229a1f97b46e3120923757a68b5413d8561f8a85d4897"}, + {file = "ruff-0.9.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c4cae6c4cc7b9b4017c71114115db0445b00a16de3bcde0946273e8392856f08"}, + {file = "ruff-0.9.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:19f505b643228b417c1111a2a536424ddde0db4ef9023b9e04a46ed8a1cb4656"}, + {file = "ruff-0.9.6-py3-none-win32.whl", hash = "sha256:194d8402bceef1b31164909540a597e0d913c0e4952015a5b40e28c146121b5d"}, + {file = "ruff-0.9.6-py3-none-win_amd64.whl", hash = "sha256:03482d5c09d90d4ee3f40d97578423698ad895c87314c4de39ed2af945633caa"}, + {file = "ruff-0.9.6-py3-none-win_arm64.whl", hash = "sha256:0e2bb706a2be7ddfea4a4af918562fdc1bcb16df255e5fa595bbd800ce322a5a"}, + {file = "ruff-0.9.6.tar.gz", hash = "sha256:81761592f72b620ec8fa1068a6fd00e98a5ebee342a3642efd84454f3031dca9"}, ] [[package]] @@ -2636,4 +2657,4 @@ plot = ["matplotlib", "plotly"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<3.14" -content-hash = "b87307deab6d125136242de2adc36049337970d6abea23392c9fdf57761230a6" +content-hash = "b829fc59acd9d10a8a7ca42f30d94775233c526ebcbea192bafaa8e992cc5c05" diff --git a/pyproject.toml b/pyproject.toml index bbbfd633..d22de055 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "simvue" -version = "2.0.0a0" +version = "2.0.0a1" description = "Simulation tracking and monitoring" authors = [ {name = "Simvue Development Team", email = "info@simvue.io"} @@ -46,13 +46,14 @@ dependencies = [ "humanfriendly (>=10.0,<11.0)", "randomname (>=0.2.1,<0.3.0)", "codecarbon (>=2.8.3,<3.0.0)", - "numpy (>=2.2.2,<3.0.0)", + "numpy (>=2.0.0,<3.0.0)", "flatdict (>=4.0.1,<5.0.0)", "semver (>=3.0.4,<4.0.0)", "email-validator (>=2.2.0,<3.0.0)", "psutil (>=6.1.1,<7.0.0)", "tenacity (>=9.0.0,<10.0.0)", "typing-extensions (>=4.12.2,<5.0.0) ; python_version < \"3.11\"", + "deepmerge (>=2.0,<3.0)", ] [project.urls] diff --git a/simvue/api/objects/alert/events.py b/simvue/api/objects/alert/events.py index 0d38b63f..00558ffb 100644 --- a/simvue/api/objects/alert/events.py +++ b/simvue/api/objects/alert/events.py @@ -87,6 +87,7 @@ def new( _offline=offline, ) _alert._staging |= _alert_definition + _alert._params = {"deduplicate": True} return _alert diff --git a/simvue/api/objects/alert/metrics.py b/simvue/api/objects/alert/metrics.py index 2fb74f06..e9340873 100644 --- a/simvue/api/objects/alert/metrics.py +++ b/simvue/api/objects/alert/metrics.py @@ -105,6 +105,8 @@ def new( _offline=offline, ) _alert._staging |= _alert_definition + _alert._params = {"deduplicate": True} + return _alert @@ -194,6 +196,7 @@ def new( _offline=offline, ) _alert._staging |= _alert_definition + _alert._params = {"deduplicate": True} return _alert diff --git a/simvue/api/objects/alert/user.py b/simvue/api/objects/alert/user.py index 9ddcd6e1..7fdcee43 100644 --- a/simvue/api/objects/alert/user.py +++ b/simvue/api/objects/alert/user.py @@ -57,7 +57,7 @@ def new( whether this alert should be created locally, default is False """ - return UserAlert( + _alert = UserAlert( name=name, description=description, notification=notification, @@ -66,6 +66,8 @@ def new( _read_only=False, _offline=offline, ) + _alert._params = {"deduplicate": True} + return _alert @classmethod def get( diff --git a/simvue/api/objects/artifact/base.py b/simvue/api/objects/artifact/base.py index dac009f5..0b5af04d 100644 --- a/simvue/api/objects/artifact/base.py +++ b/simvue/api/objects/artifact/base.py @@ -97,6 +97,7 @@ def _upload(self, file: io.BytesIO) -> None: _response = sv_post( url=_url, headers={}, + params={}, is_json=False, files={"file": file}, data=self._init_data.get("fields"), diff --git a/simvue/api/objects/artifact/fetch.py b/simvue/api/objects/artifact/fetch.py index 88f582ce..1d571266 100644 --- a/simvue/api/objects/artifact/fetch.py +++ b/simvue/api/objects/artifact/fetch.py @@ -23,6 +23,62 @@ def __new__(cls, identifier: str | None = None, **kwargs): else: return ObjectArtifact(identifier=identifier, **kwargs) + @classmethod + def from_run( + cls, + run_id: str, + category: typing.Literal["input", "output", "code"] | None = None, + **kwargs, + ) -> typing.Generator[tuple[str, FileArtifact | ObjectArtifact], None, None]: + """Return artifacts associated with a given run. + + Parameters + ---------- + run_id : str + The ID of the run to retriece artifacts from + category : typing.Literal["input", "output", "code"] | None, optional + The category of artifacts to return, by default all artifacts are returned + + Returns + ------- + typing.Generator[tuple[str, FileArtifact | ObjectArtifact], None, None] + The artifacts + + Yields + ------ + Iterator[typing.Generator[tuple[str, FileArtifact | ObjectArtifact], None, None]] + identifier for artifact + the artifact itself as a class instance + + Raises + ------ + ObjectNotFoundError + Raised if artifacts could not be found for that run + """ + _temp = ArtifactBase(**kwargs) + _url = URL(_temp._user_config.server.url) / f"runs/{run_id}/artifacts" + _response = sv_get( + url=f"{_url}", params={"category": category}, headers=_temp._headers + ) + _json_response = get_json_from_response( + expected_type=list, + response=_response, + expected_status=[http.HTTPStatus.OK, http.HTTPStatus.NOT_FOUND], + scenario=f"Retrieval of artifacts for run '{run_id}'", + ) + + if _response.status_code == http.HTTPStatus.NOT_FOUND or not _json_response: + raise ObjectNotFoundError( + _temp._label, category, extra=f"for run '{run_id}'" + ) + + for _entry in _json_response: + _id = _entry.pop("id") + yield ( + _id, + Artifact(_local=True, _read_only=True, identifier=_id, **_entry), + ) + @classmethod def from_name( cls, run_id: str, name: str, **kwargs @@ -99,21 +155,9 @@ def get( if (_data := _json_response.get("data")) is None: raise RuntimeError(f"Expected key 'data' for retrieval of {_label}s") - _out_dict: dict[str, FileArtifact | ObjectArtifact] = {} - for _entry in _data: _id = _entry.pop("id") - if _entry["original_path"]: - yield ( - _id, - FileArtifact( - _local=True, _read_only=True, identifier=_id, **_entry - ), - ) - else: - yield ( - _id, - ObjectArtifact( - _local=True, _read_only=True, identifier=_id, **_entry - ), - ) + yield ( + _id, + Artifact(_local=True, _read_only=True, identifier=_id, **_entry), + ) diff --git a/simvue/api/objects/artifact/file.py b/simvue/api/objects/artifact/file.py index 7878d514..54f0a8cb 100644 --- a/simvue/api/objects/artifact/file.py +++ b/simvue/api/objects/artifact/file.py @@ -52,14 +52,15 @@ def new( if _mime_type not in get_mimetypes(): raise ValueError(f"Invalid MIME type '{mime_type}' specified") - file_path = pathlib.Path(file_path) - _file_size = file_path.stat().st_size - _file_orig_path = file_path.expanduser().absolute() - _file_checksum = calculate_sha256(f"{file_path}", is_file=True) - kwargs.pop("original_path", None) - kwargs.pop("size", None) - kwargs.pop("checksum", None) + if _file_orig_path := kwargs.pop("original_path", None): + _file_size = kwargs.pop("size") + _file_checksum = kwargs.pop("checksum") + else: + file_path = pathlib.Path(file_path) + _file_size = file_path.stat().st_size + _file_orig_path = file_path.expanduser().absolute() + _file_checksum = calculate_sha256(f"{file_path}", is_file=True) _artifact = FileArtifact( name=name, diff --git a/simvue/api/objects/base.py b/simvue/api/objects/base.py index 7f43904f..f6f61762 100644 --- a/simvue/api/objects/base.py +++ b/simvue/api/objects/base.py @@ -17,6 +17,7 @@ import msgpack import pydantic +from simvue.utilities import staging_merger from simvue.config.user import SimvueConfiguration from simvue.exception import ObjectNotFoundError from simvue.version import __version__ @@ -164,10 +165,16 @@ def __init__( ) ) - self._headers: dict[str, str] = { - "Authorization": f"Bearer {self._user_config.server.token.get_secret_value()}", - "User-Agent": _user_agent or f"Simvue Python client {__version__}", - } + self._headers: dict[str, str] = ( + { + "Authorization": f"Bearer {self._user_config.server.token.get_secret_value()}", + "User-Agent": _user_agent or f"Simvue Python client {__version__}", + } + if not self._offline + else {} + ) + + self._params: dict[str, str] = {} self._staging: dict[str, typing.Any] = {} @@ -412,6 +419,7 @@ def _post(self, is_json: bool = True, **kwargs) -> dict[str, typing.Any]: _response = sv_post( url=f"{self._base_url}", headers=self._headers | {"Content-Type": "application/msgpack"}, + params=self._params, data=kwargs, is_json=is_json, ) @@ -423,7 +431,7 @@ def _post(self, is_json: bool = True, **kwargs) -> dict[str, typing.Any]: _json_response = get_json_from_response( response=_response, - expected_status=[http.HTTPStatus.OK], + expected_status=[http.HTTPStatus.OK, http.HTTPStatus.CONFLICT], scenario=f"Creation of {self._label}", ) @@ -452,7 +460,7 @@ def _put(self, **kwargs) -> dict[str, typing.Any]: return get_json_from_response( response=_response, - expected_status=[http.HTTPStatus.OK], + expected_status=[http.HTTPStatus.OK, http.HTTPStatus.CONFLICT], scenario=f"Creation of {self._label} '{self._identifier}", ) @@ -524,7 +532,8 @@ def _cache(self) -> None: with self._local_staging_file.open() as in_f: _local_data = json.load(in_f) - _local_data |= self._staging + staging_merger.merge(_local_data, self._staging) + with self._local_staging_file.open("w", encoding="utf-8") as out_f: json.dump(_local_data, out_f, indent=2) diff --git a/simvue/api/objects/metrics.py b/simvue/api/objects/metrics.py index de5d3075..43b75561 100644 --- a/simvue/api/objects/metrics.py +++ b/simvue/api/objects/metrics.py @@ -37,7 +37,7 @@ def __init__( def new( cls, *, run: str, offline: bool = False, metrics: list[MetricSet], **kwargs ): - """Create a new Events entry on the Simvue server""" + """Create a new Metrics entry on the Simvue server""" return Metrics( run=run, metrics=[metric.model_dump() for metric in metrics], @@ -51,27 +51,23 @@ def get( cls, metrics: list[str], xaxis: typing.Literal["timestamp", "step", "time"], + runs: list[str], *, count: pydantic.PositiveInt | None = None, offset: pydantic.PositiveInt | None = None, **kwargs, ) -> typing.Generator[MetricSet, None, None]: _class_instance = cls(_read_only=True, _local=True) - if ( - _data := cls._get_all_objects( - count, - offset, - metrics=json.dumps(metrics), - xaxis=xaxis, - **kwargs, - ).get("data") - ) is None: - raise RuntimeError( - f"Expected key 'data' for retrieval of {_class_instance.__class__.__name__.lower()}s" - ) - - for _entry in _data: - yield MetricSet(**_entry) + _data = cls._get_all_objects( + count, + offset, + metrics=json.dumps(metrics), + runs=json.dumps(runs), + xaxis=xaxis, + **kwargs, + ) + # TODO: Temp fix, just return the dictionary. Not sure what format we really want this in... + return _data @pydantic.validate_call def span(self, run_ids: list[str]) -> dict[str, int | float]: diff --git a/simvue/api/objects/run.py b/simvue/api/objects/run.py index 9841ded5..803681ae 100644 --- a/simvue/api/objects/run.py +++ b/simvue/api/objects/run.py @@ -11,6 +11,7 @@ import typing import pydantic import datetime +import time try: from typing import Self @@ -214,23 +215,31 @@ def heartbeat_timeout(self, time_seconds: int | None) -> None: @property @staging_check - def notifications(self) -> typing.Literal["none", "email"]: - return self._get_attribute("notifications") + def notifications(self) -> typing.Literal["none", "all", "error", "lost"]: + return self._get_attribute("notifications")["state"] @notifications.setter @write_only @pydantic.validate_call - def notifications(self, notifications: typing.Literal["none", "email"]) -> None: - self._staging["notifications"] = notifications + def notifications( + self, notifications: typing.Literal["none", "all", "error", "lost"] + ) -> None: + self._staging["notifications"] = {"state": notifications} @property @staging_check - def alerts(self) -> typing.Generator[str, None, None]: - for alert in self.get_alert_details(): - yield alert["id"] + def alerts(self) -> list[str]: + if self._offline: + return self._get_attribute("alerts") + + return [alert["id"] for alert in self.get_alert_details()] def get_alert_details(self) -> typing.Generator[dict[str, typing.Any], None, None]: """Retrieve the full details of alerts for this run""" + if self._offline: + raise RuntimeError( + "Cannot get alert details from an offline run - use .alerts to access a list of IDs instead" + ) for alert in self._get_attribute("alerts"): yield alert["alert"] @@ -238,9 +247,7 @@ def get_alert_details(self) -> typing.Generator[dict[str, typing.Any], None, Non @write_only @pydantic.validate_call def alerts(self, alerts: list[str]) -> None: - self._staging["alerts"] = [ - alert for alert in alerts if alert not in self._staging.get("alerts", []) - ] + self._staging["alerts"] = list(set(self._staging.get("alerts", []) + alerts)) @property @staging_check @@ -251,6 +258,19 @@ def created(self) -> datetime.datetime | None: datetime.datetime.strptime(_created, DATETIME_FORMAT) if _created else None ) + @created.setter + @write_only + @pydantic.validate_call + def created(self, created: datetime.datetime) -> None: + self._staging["created"] = created.strftime(DATETIME_FORMAT) + + @property + @staging_check + def runtime(self) -> datetime.datetime | None: + """Retrieve created datetime for the run""" + _runtime: str | None = self._get_attribute("runtime") + return time.strptime(_runtime, "%H:%M:%S.%f") if _runtime else None + @property @staging_check def started(self) -> datetime.datetime | None: diff --git a/simvue/api/request.py b/simvue/api/request.py index 3ebdb86b..8dd6a8bd 100644 --- a/simvue/api/request.py +++ b/simvue/api/request.py @@ -64,6 +64,7 @@ def is_retryable_exception(exception: Exception) -> bool: def post( url: str, headers: dict[str, str], + params: dict[str, str], data: typing.Any, is_json: bool = True, files: dict[str, typing.Any] | None = None, @@ -76,6 +77,8 @@ def post( URL to post to headers : dict[str, str] headers for the post request + params : dict[str, str] + query parameters for the post request data : dict[str, typing.Any] data to post is_json : bool, optional @@ -95,7 +98,12 @@ def post( logging.debug(f"POST: {url}\n\tdata={data_sent}") response = requests.post( - url, headers=headers, data=data_sent, timeout=DEFAULT_API_TIMEOUT, files=files + url, + headers=headers, + params=params, + data=data_sent, + timeout=DEFAULT_API_TIMEOUT, + files=files, ) if response.status_code == http.HTTPStatus.UNPROCESSABLE_ENTITY: diff --git a/simvue/client.py b/simvue/client.py index 45f152c5..c69e6b3a 100644 --- a/simvue/client.py +++ b/simvue/client.py @@ -9,7 +9,6 @@ import contextlib import json import logging -import os import pathlib import typing import http @@ -45,12 +44,9 @@ def _download_artifact_to_file( artifact: Artifact, output_dir: pathlib.Path | None ) -> None: - try: - _file_name = os.path.basename(artifact.name) - except AttributeError: - _file_name = os.path.basename(artifact) - _output_file = (output_dir or pathlib.Path.cwd()).joinpath(_file_name) - + _output_file = (output_dir or pathlib.Path.cwd()).joinpath(artifact.name) + # If this is a hierarchical structure being downloaded, need to create directories + _output_file.parent.mkdir(parents=True, exist_ok=True) with _output_file.open("wb") as out_f: for content in artifact.download_content(): out_f.write(content) @@ -565,9 +561,6 @@ def get_artifacts_as_files( run_id: str, category: typing.Literal["input", "output", "code"] | None = None, output_dir: pydantic.DirectoryPath | None = None, - startswith: str | None = None, - contains: str | None = None, - endswith: str | None = None, ) -> None: """Retrieve artifacts from the given run as a set of files @@ -575,24 +568,20 @@ def get_artifacts_as_files( ---------- run_id : str the unique identifier for the run + category : typing.Literal["input", "output", "code"] | + the type of files to retrieve output_dir : str | None, optional location to download files to, the default of None will download them to the current working directory - startswith : str, optional - only download artifacts with this prefix in their name, by default None - contains : str, optional - only download artifacts containing this term in their name, by default None - endswith : str, optional - only download artifacts ending in this term in their name, by default None Raises ------ RuntimeError if there was a failure retrieving artifacts from the server """ - _artifacts: typing.Generator[tuple[str, Artifact], None, None] = Artifact.get( - runs=json.dumps([run_id]), category=category - ) # type: ignore + _artifacts: typing.Generator[tuple[str, Artifact], None, None] = ( + Artifact.from_run(run_id=run_id, category=category) + ) with ThreadPoolExecutor(CONCURRENT_DOWNLOADS) as executor: futures = [ diff --git a/simvue/config/parameters.py b/simvue/config/parameters.py index 472811f6..c6d65c93 100644 --- a/simvue/config/parameters.py +++ b/simvue/config/parameters.py @@ -21,19 +21,23 @@ class ServerSpecifications(pydantic.BaseModel): - url: pydantic.AnyHttpUrl - token: pydantic.SecretStr + url: pydantic.AnyHttpUrl | None + token: pydantic.SecretStr | None @pydantic.field_validator("url") @classmethod - def url_to_api_url(cls, v: typing.Any) -> str: + def url_to_api_url(cls, v: typing.Any) -> str | None: + if not v: + return if f"{v}".endswith("/api"): return f"{v}" _url = URL(f"{v}") / "api" return f"{_url}" @pydantic.field_validator("token") - def check_token(cls, v: typing.Any) -> str: + def check_token(cls, v: typing.Any) -> str | None: + if not v: + return value = v.get_secret_value() if not (expiry := get_expiry(value)): raise AssertionError("Failed to parse Simvue token - invalid token form") diff --git a/simvue/config/user.py b/simvue/config/user.py index 931d2fa5..22357ce5 100644 --- a/simvue/config/user.py +++ b/simvue/config/user.py @@ -212,10 +212,10 @@ def fetch( _run_mode = mode or _config_dict["run"].get("mode") or "online" - if not _server_url: + if not _server_url and _run_mode != "offline": raise RuntimeError("No server URL was specified") - if not _server_token: + if not _server_token and _run_mode != "offline": raise RuntimeError("No server token was specified") _config_dict["server"]["token"] = _server_token diff --git a/simvue/factory/dispatch/queued.py b/simvue/factory/dispatch/queued.py index 6ebf5ff3..ae5b094e 100644 --- a/simvue/factory/dispatch/queued.py +++ b/simvue/factory/dispatch/queued.py @@ -21,7 +21,6 @@ QUEUE_SIZE = 10000 logger = logging.getLogger(__name__) -logger.setLevel(logging.DEBUG) class QueuedDispatcher(threading.Thread, DispatcherBaseClass): diff --git a/simvue/run.py b/simvue/run.py index af7a5aa0..5bfda8b3 100644 --- a/simvue/run.py +++ b/simvue/run.py @@ -25,14 +25,13 @@ import typing import warnings import uuid - +import randomname import click import psutil -from simvue.api.objects.alert.base import AlertBase from simvue.api.objects.alert.fetch import Alert -from simvue.api.objects.folder import Folder, get_folder_from_path -from simvue.exception import ObjectNotFoundError, SimvueRunError +from simvue.api.objects.folder import Folder +from simvue.exception import SimvueRunError from simvue.utilities import prettify_pydantic @@ -185,9 +184,13 @@ def __init__( if self._user_config.metrics.resources_metrics_interval < 1 else self._user_config.metrics.resources_metrics_interval ) - self._headers: dict[str, str] = { - "Authorization": f"Bearer {self._user_config.server.token.get_secret_value()}" - } + self._headers: dict[str, str] = ( + { + "Authorization": f"Bearer {self._user_config.server.token.get_secret_value()}" + } + if mode != "offline" + else {} + ) self._sv_obj: RunObject | None = None self._pid: int | None = 0 self._shutdown_event: threading.Event | None = None @@ -249,19 +252,12 @@ def _handle_exception_throw( else f"An exception was thrown: {_exception_thrown}" ) - self.log_event(_event_msg) - self.set_status("terminated" if _is_terminated else "failed") - # If the dispatcher has already been aborted then this will # fail so just continue without the event with contextlib.suppress(RuntimeError): - self.log_event(f"{_exception_thrown}: {value}") + self.log_event(_event_msg) - if not traceback: - return - - with contextlib.suppress(RuntimeError): - self.log_event(f"Traceback: {traceback}") + self.set_status("terminated" if _is_terminated else "failed") def __exit__( self, @@ -419,7 +415,9 @@ def _create_dispatch_callback( if self._user_config.run.mode == "online" and not self._id: raise RuntimeError("Expected identifier for run") - if not self._user_config.server.url or not self._sv_obj: + if ( + self._user_config.run.mode != "offline" and not self._user_config.server.url + ) or not self._sv_obj: raise RuntimeError("Cannot commence dispatch, run not initialised") def _dispatch_callback( @@ -465,12 +463,19 @@ def _start(self, reconnect: bool = False) -> bool: logger.debug("Starting run") - if self._sv_obj: - self._sv_obj.status = self._status - self._sv_obj.commit() - self._start_time = time.time() + if self._sv_obj: + _changed = False + if self._sv_obj.status != "running": + self._sv_obj.status = self._status + _changed = True + if self._user_config.run.mode == "offline": + self._sv_obj.started = self._start_time + _changed = True + if _changed: + self._sv_obj.commit() + if self._pid == 0: self._pid = os.getpid() @@ -565,6 +570,7 @@ def init( folder: typing.Annotated[ str, pydantic.Field(None, pattern=FOLDER_REGEX) ] = None, + notification: typing.Literal["none", "all", "error", "lost"] = "none", running: bool = True, retention_period: str | None = None, timeout: int | None = 180, @@ -586,6 +592,9 @@ def init( description of the run, by default None folder : str, optional folder within which to store the run, by default "/" + notification: typing.Literal["none", "all", "error", "lost"], optional + whether to notify the user by email upon completion of the run if + the run is in the specified state, by default "none" running : bool, optional whether to set the status as running or created, the latter implying the run will be commenced at a later time. Default is True. @@ -621,13 +630,10 @@ def init( self._term_color = not no_color - try: - self._folder = get_folder_from_path(path=folder) - except ObjectNotFoundError: - self._folder = Folder.new( - path=folder, offline=self._user_config.run.mode == "offline" - ) - self._folder.commit() # type: ignore + self._folder = Folder.new( + path=folder, offline=self._user_config.run.mode == "offline" + ) + self._folder.commit() # type: ignore if isinstance(visibility, str) and visibility not in ("public", "tenant"): self._error( @@ -638,7 +644,9 @@ def init( self._error("invalid mode specified, must be online, offline or disabled") return False - if not self._user_config.server.token or not self._user_config.server.url: + if self._user_config.run.mode != "offline" and ( + not self._user_config.server.token or not self._user_config.server.url + ): self._error( "Unable to get URL and token from environment variables or config file" ) @@ -647,6 +655,8 @@ def init( if name and not re.match(r"^[a-zA-Z0-9\-\_\s\/\.:]+$", name): self._error("specified name is invalid") return False + elif not name and self._user_config.run.mode == "offline": + name = randomname.get_name() self._name = name @@ -686,6 +696,9 @@ def init( self._sv_obj.tags = tags self._sv_obj.metadata = (metadata or {}) | git_info(os.getcwd()) | environment() self._sv_obj.heartbeat_timeout = timeout + self._sv_obj.alerts = [] + self._sv_obj.created = time.time() + self._sv_obj.notifications = notification if self._status == "running": self._sv_obj.system = get_system() @@ -714,7 +727,7 @@ def init( fg="green" if self._term_color else None, ) click.secho( - f"[simvue] Monitor in the UI at {self._user_config.server.url}/dashboard/runs/run/{self._id}", + f"[simvue] Monitor in the UI at {self._user_config.server.url.rsplit('/api', 1)[0]}/dashboard/runs/run/{self._id}", bold=self._term_color, fg="green" if self._term_color else None, ) @@ -1459,7 +1472,7 @@ def set_status( ) -> bool: """Set run status - status to assign to this run + status to assign to this run once finished Parameters ---------- @@ -1479,6 +1492,7 @@ def set_status( if self._sv_obj: self._sv_obj.status = status + self._sv_obj.endtime = time.time() self._sv_obj.commit() return True @@ -1507,7 +1521,11 @@ def _tidy_run(self) -> None: self._dispatcher.purge() self._dispatcher.join() - if self._user_config.run.mode == "offline" and self._status != "created": + if ( + self._sv_obj + and self._user_config.run.mode == "offline" + and self._status != "created" + ): self._user_config.offline.cache.joinpath( "runs", f"{self._id}.closed" ).touch() @@ -1631,49 +1649,25 @@ def add_alerts( if names and not ids: try: if alerts := Alert.get(offline=self._user_config.run.mode == "offline"): - for alert in alerts: - if alert.name in names: - ids.append(alert.id) + ids += [id for id, alert in alerts if alert.name in names] + else: + self._error("No existing alerts") + return False except RuntimeError as e: self._error(f"{e.args[0]}") return False - else: - self._error("No existing alerts") - return False elif not names and not ids: self._error("Need to provide alert ids or alert names") return False # Avoid duplication - self._sv_obj.alerts = list(set(self._sv_obj.alerts + [ids])) - self._sv_obj.commit() - - return False - - def _attach_alert_to_run(self, alert: AlertBase) -> str | None: - # Check if the alert already exists - _alert_id: str | None = None - - for _, _existing_alert in Alert.get( - offline=self._user_config.run.mode == "offline" - ): - if _existing_alert.compare(alert): - _alert_id = _existing_alert.id - logger.info("Existing alert found with id: %s", _existing_alert.id) - break - - if not _alert_id: - alert.commit() - _alert_id = alert.id - - self._sv_obj.alerts = [_alert_id] - + _deduplicated = list(set(self._sv_obj.alerts + ids)) + self._sv_obj.alerts = _deduplicated self._sv_obj.commit() - return _alert_id + return True @skip_if_failed("_aborted", "_suppress_errors", None) - @check_run_initialised @pydantic.validate_call def create_metric_range_alert( self, @@ -1691,6 +1685,7 @@ def create_metric_range_alert( ] = "average", notification: typing.Literal["email", "none"] = "none", trigger_abort: bool = False, + attach_to_run: bool = True, ) -> str | None: """Creates a metric range alert with the specified name (if it doesn't exist) and applies it to the current run. If alert already exists it will @@ -1720,6 +1715,8 @@ def create_metric_range_alert( whether to notify on trigger, by default "none" trigger_abort : bool, optional whether this alert can trigger a run abort, default False + attach_to_run : bool, optional + whether to attach this alert to the current run, default True Returns ------- @@ -1741,10 +1738,12 @@ def create_metric_range_alert( offline=self._user_config.run.mode == "offline", ) _alert.abort = trigger_abort - return self._attach_alert_to_run(_alert) + _alert.commit() + if attach_to_run: + self.add_alerts(ids=[_alert.id]) + return _alert.id @skip_if_failed("_aborted", "_suppress_errors", None) - @check_run_initialised @pydantic.validate_call def create_metric_threshold_alert( self, @@ -1761,6 +1760,7 @@ def create_metric_threshold_alert( ] = "average", notification: typing.Literal["email", "none"] = "none", trigger_abort: bool = False, + attach_to_run: bool = True, ) -> str | None: """Creates a metric threshold alert with the specified name (if it doesn't exist) and applies it to the current run. If alert already exists it will @@ -1788,6 +1788,8 @@ def create_metric_threshold_alert( whether to notify on trigger, by default "none" trigger_abort : bool, optional whether this alert can trigger a run abort, default False + attach_to_run : bool, optional + whether to attach this alert to the current run, default True Returns ------- @@ -1807,11 +1809,14 @@ def create_metric_threshold_alert( notification=notification, offline=self._user_config.run.mode == "offline", ) + _alert.abort = trigger_abort - return self._attach_alert_to_run(_alert) + _alert.commit() + if attach_to_run: + self.add_alerts(ids=[_alert.id]) + return _alert.id @skip_if_failed("_aborted", "_suppress_errors", None) - @check_run_initialised @pydantic.validate_call def create_event_alert( self, @@ -1822,6 +1827,7 @@ def create_event_alert( frequency: pydantic.PositiveInt = 1, notification: typing.Literal["email", "none"] = "none", trigger_abort: bool = False, + attach_to_run: bool = True, ) -> str | None: """Creates an events alert with the specified name (if it doesn't exist) and applies it to the current run. If alert already exists it will @@ -1839,6 +1845,8 @@ def create_event_alert( whether to notify on trigger, by default "none" trigger_abort : bool, optional whether this alert can trigger a run abort + attach_to_run : bool, optional + whether to attach this alert to the current run, default True Returns ------- @@ -1855,10 +1863,12 @@ def create_event_alert( offline=self._user_config.run.mode == "offline", ) _alert.abort = trigger_abort - return self._attach_alert_to_run(_alert) + _alert.commit() + if attach_to_run: + self.add_alerts(ids=[_alert.id]) + return _alert.id @skip_if_failed("_aborted", "_suppress_errors", None) - @check_run_initialised @pydantic.validate_call def create_user_alert( self, @@ -1867,6 +1877,7 @@ def create_user_alert( description: str | None = None, notification: typing.Literal["email", "none"] = "none", trigger_abort: bool = False, + attach_to_run: bool = True, ) -> None: """Creates a user alert with the specified name (if it doesn't exist) and applies it to the current run. If alert already exists it will @@ -1882,6 +1893,8 @@ def create_user_alert( whether to notify on trigger, by default "none" trigger_abort : bool, optional whether this alert can trigger a run abort, default False + attach_to_run : bool, optional + whether to attach this alert to the current run, default True Returns ------- @@ -1896,7 +1909,10 @@ def create_user_alert( offline=self._user_config.run.mode == "offline", ) _alert.abort = trigger_abort - return self._attach_alert_to_run(_alert) + _alert.commit() + if attach_to_run: + self.add_alerts(ids=[_alert.id]) + return _alert.id @skip_if_failed("_aborted", "_suppress_errors", False) @check_run_initialised diff --git a/simvue/utilities.py b/simvue/utilities.py index 69a8ecd8..a5f40430 100644 --- a/simvue/utilities.py +++ b/simvue/utilities.py @@ -11,8 +11,8 @@ import os import pathlib import typing - import jwt +from deepmerge import Merger from datetime import timezone from simvue.models import DATETIME_FORMAT @@ -50,16 +50,17 @@ def find_first_instance_of_file( if isinstance(file_names, str): file_names = [file_names] - for root, _, files in os.walk(os.getcwd(), topdown=False): - for file_name in file_names: - if file_name in files: - return pathlib.Path(root).joinpath(file_name) + for file_name in file_names: + _user_file = pathlib.Path.cwd().joinpath(file_name) + if _user_file.exists(): + return _user_file # If the user is running on different mounted volume or outside # of their user space then the above will not return the file if check_user_space: for file_name in file_names: - if os.path.exists(_user_file := pathlib.Path.home().joinpath(file_name)): + _user_file = pathlib.Path.home().joinpath(file_name) + if _user_file.exists(): return _user_file return None @@ -357,7 +358,7 @@ def validate_timestamp(timestamp): Validate a user-provided timestamp """ try: - datetime.datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S.%f") + datetime.datetime.strptime(timestamp, DATETIME_FORMAT) except ValueError: return False @@ -395,3 +396,18 @@ def get_mimetype_for_file(file_path: pathlib.Path) -> str: """Return MIME type for the given file""" _guess, *_ = mimetypes.guess_type(file_path) return _guess or "application/octet-stream" + + +# Create a new Merge strategy for merging local file and staging attributes +staging_merger = Merger( + # pass in a list of tuple, with the + # strategies you are looking to apply + # to each type. + [(list, ["override"]), (dict, ["merge"]), (set, ["union"])], + # next, choose the fallback strategies, + # applied to all other types: + ["override"], + # finally, choose the strategies in + # the case where the types conflict: + ["override"], +) diff --git a/tests/functional/test_client.py b/tests/functional/test_client.py index 3b6c6f24..37c1a1ae 100644 --- a/tests/functional/test_client.py +++ b/tests/functional/test_client.py @@ -145,12 +145,21 @@ def test_get_artifacts_as_files( create_test_run[1]["run_id"], category=category, output_dir=tempd ) files = [os.path.basename(i) for i in glob.glob(os.path.join(tempd, "*"))] - if not category or category == "input": - assert create_test_run[1]["file_1"] in files - if not category or category == "output": - assert create_test_run[1]["file_2"] in files - if not category or category == "code": - assert create_test_run[1]["file_3"] in files + + if not category: + expected_files = ["file_1", "file_2", "file_3"] + elif category == "input": + expected_files = ["file_1"] + elif category == "output": + expected_files = ["file_2"] + elif category == "code": + expected_files = ["file_3"] + + for file in ["file_1", "file_2", "file_3"]: + if file in expected_files: + assert create_test_run[1][file] in files + else: + assert create_test_run[1][file] not in files @pytest.mark.dependency diff --git a/tests/functional/test_dispatch.py b/tests/functional/test_dispatch.py index de77ccb2..e2ab6362 100644 --- a/tests/functional/test_dispatch.py +++ b/tests/functional/test_dispatch.py @@ -67,9 +67,10 @@ def callback(___: list[typing.Any], _: str, args=check_dict, var=variable) -> No event.set() dispatcher.join() + time.sleep(0.1) for variable in variables: - assert check_dict[variable]["counter"] >= 2 if overload_buffer else 1, f"Check of counter for dispatcher '{variable}' failed with count = {check_dict[variable]['counter']}" + assert check_dict[variable]["counter"] >= (2 if overload_buffer else 1), f"Check of counter for dispatcher '{variable}' failed with count = {check_dict[variable]['counter']}" assert time.time() - start_time < time_threshold diff --git a/tests/functional/test_run_class.py b/tests/functional/test_run_class.py index 5de51a8a..b447659b 100644 --- a/tests/functional/test_run_class.py +++ b/tests/functional/test_run_class.py @@ -14,7 +14,7 @@ import pathlib import concurrent.futures import random - +import datetime import simvue from simvue.api.objects.alert.fetch import Alert from simvue.exception import SimvueRunError @@ -59,12 +59,14 @@ def test_run_with_emissions() -> None: @pytest.mark.run +@pytest.mark.parametrize("timestamp", (datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%f"), None), ids=("timestamp", "no_timestamp")) @pytest.mark.parametrize("overload_buffer", (True, False), ids=("overload", "normal")) @pytest.mark.parametrize( "visibility", ("bad_option", "tenant", "public", ["ciuser01"], None) ) def test_log_metrics( overload_buffer: bool, + timestamp: str | None, setup_logging: "CountingLogHandler", mocker, request: pytest.FixtureRequest, @@ -112,9 +114,9 @@ def test_log_metrics( if overload_buffer: for i in range(run._dispatcher._max_buffer_size * 3): - run.log_metrics({key: i for key in METRICS}) + run.log_metrics({key: i for key in METRICS}, timestamp=timestamp) else: - run.log_metrics(METRICS) + run.log_metrics(METRICS, timestamp=timestamp) time.sleep(2.0 if overload_buffer else 1.0) run.close() client = sv_cl.Client() @@ -220,9 +222,14 @@ def test_offline_tags(create_plain_run_offline: tuple[sv_run.Run, dict]) -> None @pytest.mark.run def test_update_metadata_running(create_test_run: tuple[sv_run.Run, dict]) -> None: - METADATA = {"a": 10, "b": 1.2, "c": "word"} + METADATA = {"a": 1, "b": 1.2, "c": "word", "d": "new"} run, _ = create_test_run - run.update_metadata(METADATA) + # Add an initial set of metadata + run.update_metadata({"a": 10, "b": 1.2, "c": "word"}) + # Try updating a second time, check original dict isnt overwritten + run.update_metadata({"d": "new"}) + # Try updating an already defined piece of metadata + run.update_metadata({"a": 1}) run.close() time.sleep(1.0) client = sv_cl.Client() @@ -234,9 +241,14 @@ def test_update_metadata_running(create_test_run: tuple[sv_run.Run, dict]) -> No @pytest.mark.run def test_update_metadata_created(create_pending_run: tuple[sv_run.Run, dict]) -> None: - METADATA = {"a": 10, "b": 1.2, "c": "word"} + METADATA = {"a": 1, "b": 1.2, "c": "word", "d": "new"} run, _ = create_pending_run - run.update_metadata(METADATA) + # Add an initial set of metadata + run.update_metadata({"a": 10, "b": 1.2, "c": "word"}) + # Try updating a second time, check original dict isnt overwritten + run.update_metadata({"d": "new"}) + # Try updating an already defined piece of metadata + run.update_metadata({"a": 1}) time.sleep(1.0) client = sv_cl.Client() run_info = client.get_run(run.id) @@ -250,13 +262,20 @@ def test_update_metadata_created(create_pending_run: tuple[sv_run.Run, dict]) -> def test_update_metadata_offline( create_plain_run_offline: tuple[sv_run.Run, dict], ) -> None: - METADATA = {"a": 10, "b": 1.2, "c": "word"} + METADATA = {"a": 1, "b": 1.2, "c": "word", "d": "new"} run, _ = create_plain_run_offline run_name = run._name - run.update_metadata(METADATA) + # Add an initial set of metadata + run.update_metadata({"a": 10, "b": 1.2, "c": "word"}) + # Try updating a second time, check original dict isnt overwritten + run.update_metadata({"d": "new"}) + # Try updating an already defined piece of metadata + run.update_metadata({"a": 1}) + sv_send.sender(os.environ["SIMVUE_OFFLINE_DIRECTORY"], 2, 10) run.close() time.sleep(1.0) + client = sv_cl.Client() run_info = client.get_run(client.get_run_id_from_name(run_name)) @@ -655,6 +674,29 @@ def test_update_tags_created( assert sorted(run_data.tags) == sorted(tags + ["additional"]) +@pytest.mark.offline +@pytest.mark.run +def test_update_tags_offline( + create_plain_run_offline: typing.Tuple[sv_run.Run, dict], +) -> None: + simvue_run, _ = create_plain_run_offline + run_name = simvue_run._name + + simvue_run.set_tags(["simvue_client_unit_tests",]) + + simvue_run.update_tags(["additional"]) + + sv_send.sender(os.environ["SIMVUE_OFFLINE_DIRECTORY"], 2, 10) + simvue_run.close() + time.sleep(1.0) + + client = sv_cl.Client() + run_data = client.get_run(client.get_run_id_from_name(run_name)) + + time.sleep(1) + run_data = client.get_run(simvue_run._id) + assert sorted(run_data.tags) == sorted(["simvue_client_unit_tests", "additional"]) + @pytest.mark.run @pytest.mark.parametrize("object_type", ("DataFrame", "ndarray")) def test_save_object( @@ -676,6 +718,94 @@ def test_save_object( save_obj = array([1, 2, 3, 4]) simvue_run.save_object(save_obj, "input", f"test_object_{object_type}") +@pytest.mark.run +def test_add_alerts() -> None: + _uuid = f"{uuid.uuid4()}".split("-")[0] + + run = sv_run.Run() + run.init( + name="test_add_alerts", + folder="/simvue_unit_tests", + retention_period="1 min", + tags=["test_add_alerts"], + visibility="tenant" + ) + + _expected_alerts = [] + + # Create alerts, have them attach to run automatically + _id = run.create_event_alert( + name=f"event_alert_{_uuid}", + pattern = "test", + ) + _expected_alerts.append(_id) + time.sleep(1) + # Retrieve run, check if alert has been added + _online_run = RunObject(identifier=run._id) + assert _id in _online_run.alerts + + # Create another alert and attach to run + _id = run.create_metric_range_alert( + name=f"metric_range_alert_{_uuid}", + metric="test", + range_low=10, + range_high=100, + rule="is inside range", + ) + _expected_alerts.append(_id) + time.sleep(1) + # Retrieve run, check both alerts have been added + _online_run.refresh() + assert sorted(_online_run.alerts) == sorted(_expected_alerts) + + # Create another alert, do not attach to run + _id = run.create_metric_threshold_alert( + name=f"metric_threshold_alert_{_uuid}", + metric="test", + threshold=10, + rule="is above", + attach_to_run=False + ) + time.sleep(1) + # Retrieve run, check alert has NOT been added + _online_run.refresh() + assert sorted(_online_run.alerts) == sorted(_expected_alerts) + + # Try adding all three alerts using add_alerts + _expected_alerts.append(_id) + run.add_alerts(names=[f"event_alert_{_uuid}", f"metric_range_alert_{_uuid}", f"metric_threshold_alert_{_uuid}"]) + time.sleep(1) + + # Check that there is no duplication + _online_run.refresh() + assert sorted(_online_run.alerts) == sorted(_expected_alerts) + + # Create another run without adding to run + _id = run.create_user_alert( + name=f"user_alert_{_uuid}", + attach_to_run=False + ) + time.sleep(1) + + # Check alert is not added + _online_run.refresh() + assert sorted(_online_run.alerts) == sorted(_expected_alerts) + + # Try adding alerts with IDs, check there is no duplication + _expected_alerts.append(_id) + run.add_alerts(ids=_expected_alerts) + time.sleep(1) + + _online_run.refresh() + assert sorted(_online_run.alerts) == sorted(_expected_alerts) + + run.close() + + client = sv_cl.Client() + client.delete_run(run._id) + for _id in _expected_alerts: + client.delete_alert(_id) + @pytest.mark.run def test_abort_on_alert_process(mocker: pytest_mock.MockerFixture) -> None: @@ -756,8 +886,8 @@ def testing_exit(status: int) -> None: run.add_process(identifier="forever_long", executable="bash", c="sleep 10") time.sleep(2) run.log_alert(alert_id, "critical") - _alert = Alert(identifier=alert_id) time.sleep(1) + _alert = Alert(identifier=alert_id) assert _alert.get_status(run.id) == "critical" counter = 0 while run._status != "terminated" and counter < 15: diff --git a/tests/unit/test_events.py b/tests/unit/test_events.py index 205643e6..fb5d0587 100644 --- a/tests/unit/test_events.py +++ b/tests/unit/test_events.py @@ -7,6 +7,7 @@ from simvue.api.objects import Events, Folder, Run from simvue.models import DATETIME_FORMAT +from simvue.sender import sender @pytest.mark.api @pytest.mark.online @@ -30,3 +31,39 @@ def test_events_creation_online() -> None: _run.delete() _folder.delete(recursive=True, delete_runs=True, runs_only=False) +@pytest.mark.api +@pytest.mark.offline +def test_events_creation_offline() -> None: + _uuid: str = f"{uuid.uuid4()}".split("-")[0] + _folder_name = f"/simvue_unit_testing/{_uuid}" + _folder = Folder.new(path=_folder_name, offline=True) + _run = Run.new(folder=_folder_name, offline=True) + _folder.commit() + _run.commit() + _timestamp = datetime.datetime.now().strftime(DATETIME_FORMAT) + _events = Events.new( + run=_run.id, + events=[ + {"message": "This is a test!", "timestamp": _timestamp} + ], + offline=True + ) + _events.commit() + with _events._local_staging_file.open() as in_f: + _local_data = json.load(in_f) + + assert _local_data.get("run") == _run.id + assert _local_data.get("events")[0].get("message") == "This is a test!" + assert _local_data.get("events")[0].get("timestamp") == _timestamp + + _id_mapping = sender(_events._local_staging_file.parents[1], 1, 10, ["folders", "runs", "events"]) + time.sleep(1) + + # Get online version of events + _online_events = Events(_id_mapping.get(_events.id)) + _event_content = next(_online_events.get(run_id=_id_mapping.get(_run.id))) + assert _event_content.message == "This is a test!" + assert _event_content.timestamp == _timestamp + + _run.delete() + _folder.delete(recursive=True, delete_runs=True, runs_only=False) \ No newline at end of file diff --git a/tests/unit/test_metrics.py b/tests/unit/test_metrics.py index d8432fc0..5d8b7575 100644 --- a/tests/unit/test_metrics.py +++ b/tests/unit/test_metrics.py @@ -6,6 +6,8 @@ import uuid from simvue.api.objects import Metrics, Folder, Run +from simvue.models import DATETIME_FORMAT +from simvue.sender import sender @pytest.mark.api @pytest.mark.online @@ -38,9 +40,60 @@ def test_metrics_creation_online() -> None: ) assert _metrics.to_dict() _metrics.commit() - assert _metrics.get(metrics=["x", "y", "z"], xaxis="step") + assert _metrics.get(metrics=["x", "y", "z"], xaxis="step", runs=[_run.id]) assert _metrics.span(run_ids=[_run.id]) assert _metrics.names(run_ids=[_run.id]) _run.delete() _folder.delete(recursive=True, delete_runs=True, runs_only=False) +@pytest.mark.api +@pytest.mark.offline +def test_metrics_creation_offline() -> None: + _uuid: str = f"{uuid.uuid4()}".split("-")[0] + _folder_name = f"/simvue_unit_testing/{_uuid}" + _folder = Folder.new(path=_folder_name, offline=True) + _run = Run.new(name="hello", folder=_folder_name, offline=True) + _folder.commit() + _run.commit() + + _values = { + "x": 1, + "y": 2.0, + "z": True + } + _time: int = 1 + _step: int = 1 + _timestamp = datetime.datetime.now().strftime(DATETIME_FORMAT) + _metrics = Metrics.new( + run=_run.id, + metrics=[ + { + "timestamp": _timestamp, + "time": _time, + "step": _step, + "values": _values, + } + ], + offline=True + ) + _metrics.commit() + with _metrics._local_staging_file.open() as in_f: + _local_data = json.load(in_f) + + assert _local_data.get("run") == _run.id + assert _local_data.get("metrics")[0].get("values") == _values + assert _local_data.get("metrics")[0].get("timestamp") == _timestamp + assert _local_data.get("metrics")[0].get("step") == _step + assert _local_data.get("metrics")[0].get("time") == _time + + _id_mapping = sender(_metrics._local_staging_file.parents[1], 1, 10, ["folders", "runs", "metrics"]) + time.sleep(1) + + # Get online version of metrics + _online_metrics = Metrics(_id_mapping.get(_metrics.id)) + _data = _online_metrics.get(metrics=["x", "y", "z"], runs=[_id_mapping.get(_run.id)], xaxis="step") + assert sorted(_online_metrics.names(run_ids=[_id_mapping.get(_run.id)])) == sorted(_values.keys()) + assert _data.get(_id_mapping.get(_run.id)).get('y')[0].get('value') == 2.0 + assert _data.get(_id_mapping.get(_run.id)).get('y')[0].get('step') == 1 + _run.delete() + _folder.delete(recursive=True, delete_runs=True, runs_only=False) \ No newline at end of file diff --git a/tests/unit/test_user_alert.py b/tests/unit/test_user_alert.py index 71d08336..a819df15 100644 --- a/tests/unit/test_user_alert.py +++ b/tests/unit/test_user_alert.py @@ -191,13 +191,28 @@ def test_user_alert_status_offline() -> None: _run.alerts = [_alert.id] _run.commit() - sender(_alert._local_staging_file.parents[1], 1, 10, ["folders", "runs", "alerts"]) + _id_mapping = sender(_alert._local_staging_file.parents[1], 1, 10, ["folders", "runs", "alerts"]) time.sleep(1) + + # Get online aler, check status is not set + _online_alert = UserAlert(_id_mapping.get(_alert.id)) + assert not _online_alert.get_status(run_id=_id_mapping.get(_run.id)) _alert.set_status(_run.id, "critical") _alert.commit() - import pdb; pdb.set_trace() time.sleep(1) + + # Check online status is still not set as change has not been sent + _online_alert.refresh() + assert not _online_alert.get_status(run_id=_id_mapping.get(_run.id)) + + sender(_alert._local_staging_file.parents[1], 1, 10, ["alerts"]) + time.sleep(1) + + # Check online status has been updated + _online_alert.refresh() + assert _online_alert.get_status(run_id=_id_mapping.get(_run.id)) == "critical" + _run.delete() _folder.delete(recursive=True, runs_only=False, delete_runs=True) _alert.delete()