diff --git a/CHANGELOG.md b/CHANGELOG.md index 546e3dad..58e4f919 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,16 @@ # Change Log -## [v2.5.1](https://github.com/simvue-io/python-api/releases/tag/v2.5.1) - 2026-05-06 +## [v2.5.2](https://github.com/simvue-io/python-api/releases/tag/v2.5.2) - 2026-05-15 +- Fixed issue whereby providing alternative server URL or token arguments was ignored. - Fixed legacy event logging support. +## ~~[v2.5.1](https://github.com/simvue-io/python-api/releases/tag/v2.5.1) - 2026-05-06~~ + +**Yanked from PyPi as issue not fixed.** + +- ~~Fixed legacy event logging support.~~ + ## [v2.5.0](https://github.com/simvue-io/python-api/releases/tag/v2.5.0) - 2026-05-05 - Added support for log level in events creation. diff --git a/poetry.lock b/poetry.lock index 7b236028..38caa529 100644 --- a/poetry.lock +++ b/poetry.lock @@ -642,63 +642,63 @@ files = [ [[package]] name = "fonttools" -version = "4.62.1" +version = "4.63.0" description = "Tools to manipulate font files" optional = true python-versions = ">=3.10" groups = ["main"] markers = "extra == \"plot\"" files = [ - {file = "fonttools-4.62.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ad5cca75776cd453b1b035b530e943334957ae152a36a88a320e779d61fc980c"}, - {file = "fonttools-4.62.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b3ae47e8636156a9accff64c02c0924cbebad62854c4a6dbdc110cd5b4b341a"}, - {file = "fonttools-4.62.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b9e288b4da2f64fd6180644221749de651703e8d0c16bd4b719533a3a7d6e3"}, - {file = "fonttools-4.62.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7bca7a1c1faf235ffe25d4f2e555246b4750220b38de8261d94ebc5ce8a23c23"}, - {file = "fonttools-4.62.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b4e0fcf265ad26e487c56cb12a42dffe7162de708762db951e1b3f755319507d"}, - {file = "fonttools-4.62.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2d850f66830a27b0d498ee05adb13a3781637b1826982cd7e2b3789ef0cc71ae"}, - {file = "fonttools-4.62.1-cp310-cp310-win32.whl", hash = "sha256:486f32c8047ccd05652aba17e4a8819a3a9d78570eb8a0e3b4503142947880ed"}, - {file = "fonttools-4.62.1-cp310-cp310-win_amd64.whl", hash = "sha256:5a648bde915fba9da05ae98856987ca91ba832949a9e2888b48c47ef8b96c5a9"}, - {file = "fonttools-4.62.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:40975849bac44fb0b9253d77420c6d8b523ac4dcdcefeff6e4d706838a5b80f7"}, - {file = "fonttools-4.62.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9dde91633f77fa576879a0c76b1d89de373cae751a98ddf0109d54e173b40f14"}, - {file = "fonttools-4.62.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6acb4109f8bee00fec985c8c7afb02299e35e9c94b57287f3ea542f28bd0b0a7"}, - {file = "fonttools-4.62.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1c5c25671ce8805e0d080e2ffdeca7f1e86778c5cbfbeae86d7f866d8830517b"}, - {file = "fonttools-4.62.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a5d8825e1140f04e6c99bb7d37a9e31c172f3bc208afbe02175339e699c710e1"}, - {file = "fonttools-4.62.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:268abb1cb221e66c014acc234e872b7870d8b5d4657a83a8f4205094c32d2416"}, - {file = "fonttools-4.62.1-cp311-cp311-win32.whl", hash = "sha256:942b03094d7edbb99bdf1ae7e9090898cad7bf9030b3d21f33d7072dbcb51a53"}, - {file = "fonttools-4.62.1-cp311-cp311-win_amd64.whl", hash = "sha256:e8514f4924375f77084e81467e63238b095abda5107620f49421c368a6017ed2"}, - {file = "fonttools-4.62.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:90365821debbd7db678809c7491ca4acd1e0779b9624cdc6ddaf1f31992bf974"}, - {file = "fonttools-4.62.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12859ff0b47dd20f110804c3e0d0970f7b832f561630cd879969011541a464a9"}, - {file = "fonttools-4.62.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c125ffa00c3d9003cdaaf7f2c79e6e535628093e14b5de1dccb08859b680936"}, - {file = "fonttools-4.62.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:149f7d84afca659d1a97e39a4778794a2f83bf344c5ee5134e09995086cc2392"}, - {file = "fonttools-4.62.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0aa72c43a601cfa9273bb1ae0518f1acadc01ee181a6fc60cd758d7fdadffc04"}, - {file = "fonttools-4.62.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:19177c8d96c7c36359266e571c5173bcee9157b59cfc8cb0153c5673dc5a3a7d"}, - {file = "fonttools-4.62.1-cp312-cp312-win32.whl", hash = "sha256:a24decd24d60744ee8b4679d38e88b8303d86772053afc29b19d23bb8207803c"}, - {file = "fonttools-4.62.1-cp312-cp312-win_amd64.whl", hash = "sha256:9e7863e10b3de72376280b515d35b14f5eeed639d1aa7824f4cf06779ec65e42"}, - {file = "fonttools-4.62.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c22b1014017111c401469e3acc5433e6acf6ebcc6aa9efb538a533c800971c79"}, - {file = "fonttools-4.62.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68959f5fc58ed4599b44aad161c2837477d7f35f5f79402d97439974faebfebe"}, - {file = "fonttools-4.62.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef46db46c9447103b8f3ff91e8ba009d5fe181b1920a83757a5762551e32bb68"}, - {file = "fonttools-4.62.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6706d1cb1d5e6251a97ad3c1b9347505c5615c112e66047abbef0f8545fa30d1"}, - {file = "fonttools-4.62.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e7abd2b1e11736f58c1de27819e1955a53267c21732e78243fa2fa2e5c1e069"}, - {file = "fonttools-4.62.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:403d28ce06ebfc547fbcb0cb8b7f7cc2f7a2d3e1a67ba9a34b14632df9e080f9"}, - {file = "fonttools-4.62.1-cp313-cp313-win32.whl", hash = "sha256:93c316e0f5301b2adbe6a5f658634307c096fd5aae60a5b3412e4f3e1728ab24"}, - {file = "fonttools-4.62.1-cp313-cp313-win_amd64.whl", hash = "sha256:7aa21ff53e28a9c2157acbc44e5b401149d3c9178107130e82d74ceb500e5056"}, - {file = "fonttools-4.62.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fa1d16210b6b10a826d71bed68dd9ec24a9e218d5a5e2797f37c573e7ec215ca"}, - {file = "fonttools-4.62.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:aa69d10ed420d8121118e628ad47d86e4caa79ba37f968597b958f6cceab7eca"}, - {file = "fonttools-4.62.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd13b7999d59c5eb1c2b442eb2d0c427cb517a0b7a1f5798fc5c9e003f5ff782"}, - {file = "fonttools-4.62.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8d337fdd49a79b0d51c4da87bc38169d21c3abbf0c1aa9367eff5c6656fb6dae"}, - {file = "fonttools-4.62.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d241cdc4a67b5431c6d7f115fdf63335222414995e3a1df1a41e1182acd4bcc7"}, - {file = "fonttools-4.62.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c05557a78f8fa514da0f869556eeda40887a8abc77c76ee3f74cf241778afd5a"}, - {file = "fonttools-4.62.1-cp314-cp314-win32.whl", hash = "sha256:49a445d2f544ce4a69338694cad575ba97b9a75fff02720da0882d1a73f12800"}, - {file = "fonttools-4.62.1-cp314-cp314-win_amd64.whl", hash = "sha256:1eecc128c86c552fb963fe846ca4e011b1be053728f798185a1687502f6d398e"}, - {file = "fonttools-4.62.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1596aeaddf7f78e21e68293c011316a25267b3effdaccaf4d59bc9159d681b82"}, - {file = "fonttools-4.62.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8f8fca95d3bb3208f59626a4b0ea6e526ee51f5a8ad5d91821c165903e8d9260"}, - {file = "fonttools-4.62.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee91628c08e76f77b533d65feb3fbe6d9dad699f95be51cf0d022db94089cdc4"}, - {file = "fonttools-4.62.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f37df1cac61d906e7b836abe356bc2f34c99d4477467755c216b72aa3dc748b"}, - {file = "fonttools-4.62.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:92bb00a947e666169c99b43753c4305fc95a890a60ef3aeb2a6963e07902cc87"}, - {file = "fonttools-4.62.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bdfe592802ef939a0e33106ea4a318eeb17822c7ee168c290273cbd5fabd746c"}, - {file = "fonttools-4.62.1-cp314-cp314t-win32.whl", hash = "sha256:b820fcb92d4655513d8402d5b219f94481c4443d825b4372c75a2072aa4b357a"}, - {file = "fonttools-4.62.1-cp314-cp314t-win_amd64.whl", hash = "sha256:59b372b4f0e113d3746b88985f1c796e7bf830dd54b28374cd85c2b8acd7583e"}, - {file = "fonttools-4.62.1-py3-none-any.whl", hash = "sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd"}, - {file = "fonttools-4.62.1.tar.gz", hash = "sha256:e54c75fd6041f1122476776880f7c3c3295ffa31962dc6ebe2543c00dca58b5d"}, + {file = "fonttools-4.63.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e3297a6a4059b4acc3a1e9a8b04741f240a80044eef08ebd32e8b5bcdddce75b"}, + {file = "fonttools-4.63.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b1cd75a03ad8cb5bc40c90bfde68c0c47de423aa19e5c0f362b43520645eea94"}, + {file = "fonttools-4.63.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0425b277a59cff3d80ca42162a8de360f318438a2ac83570842a678d826d579"}, + {file = "fonttools-4.63.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d7e5c9973aa04c95650c96e5f5ad865fbf42d62079163ecfab1e01cbc2504c22"}, + {file = "fonttools-4.63.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cb014d58140a38135f16064c74c652ed57aa0b75cbf8bb59cac821f7edb5334e"}, + {file = "fonttools-4.63.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:032038247a96c1690f9f31e377c389383c902531b085aa4e4dabd6f57f870e69"}, + {file = "fonttools-4.63.0-cp310-cp310-win32.whl", hash = "sha256:a8b33a82979e0a6a34ff435cc81317be1f95ec1ebb7a3a2d1c8a6a54f02ae44e"}, + {file = "fonttools-4.63.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c18358a155d75034911c5ee397a5b44cd19dd325dbb8b35fb60bf421d6a72ac"}, + {file = "fonttools-4.63.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2b8ae05d9eacf6081414d759c0a352769ac28ce31280d6bb8e77b03f9e3c449f"}, + {file = "fonttools-4.63.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79cdc9f567aec74a72918fd060283911406750cbc9fd28c1316023deb6ce31a9"}, + {file = "fonttools-4.63.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c14b4fd138c4bafcca294765c547914e1aa431ae1ca94ab99d8db08c958bd3b"}, + {file = "fonttools-4.63.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76ac49f929aecaf82d83250b8347e099d7aecba0f4726c1d9b6df3b8bb5fe18"}, + {file = "fonttools-4.63.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dcf076a4474fe0d7367e5bbf5b052c7284fa1feca729c04176ce513521afd8a0"}, + {file = "fonttools-4.63.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7dd683fef0663e9f0f45cf541d788d24caa3ec9db50796b588e1757d8b3bc007"}, + {file = "fonttools-4.63.0-cp311-cp311-win32.whl", hash = "sha256:afefc1ed0a59785a7fb06ea7e1678e849c193e1e387db783579bc7b3056fcfcb"}, + {file = "fonttools-4.63.0-cp311-cp311-win_amd64.whl", hash = "sha256:063e08bd17bd5a90127a14123de0d6a952dbc847695fd98b63c043d58057f90c"}, + {file = "fonttools-4.63.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:37dd23e621e3b0aef1baa70a303b80aaf38449632cfc8fd2a55fb285bbccfc02"}, + {file = "fonttools-4.63.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a9faff9e0c1f76f9fd55899d2ce785832efebab37eb8ae13995853aef178bef0"}, + {file = "fonttools-4.63.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef3048ef05dbb552b89817713d9cac912e00d0fde4a3105c00d29e52e10c89af"}, + {file = "fonttools-4.63.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58dc6bb86a78d782f00f9190ca02c119cf5bbe2807536e361e18d42019f877d8"}, + {file = "fonttools-4.63.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee08ebfa58f6e1aeff5697ab9582105bb620008c1caafb681e4c557e7483027b"}, + {file = "fonttools-4.63.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:27fdc65af8da6f88b9c6121c47a464cbe359fcfff7ff6fc2d37a1f395d755b78"}, + {file = "fonttools-4.63.0-cp312-cp312-win32.whl", hash = "sha256:af2fd1664d00a397d75f806985ddb36282091c2131a73a6485c23b4a34722263"}, + {file = "fonttools-4.63.0-cp312-cp312-win_amd64.whl", hash = "sha256:59ac449f8cca9b4ffa08d2e7bbadad87ce710d69d1eda5c3c1ce579baa987272"}, + {file = "fonttools-4.63.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cd7e9857e5e63738b9d9fd707bc1f59c8b09e5177726d23664db393c59bb08bd"}, + {file = "fonttools-4.63.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c2a2a42198b696a6f48fad91709afb55176e66a5e566131219dba372fb7f8c59"}, + {file = "fonttools-4.63.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e874792a8212b44583ea02189d9e693906b2f78b261f372f95d6c563210ac1d"}, + {file = "fonttools-4.63.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22135da48a348785c5e2d5d2d9d6bec5ed44adacbaeb9db12d9493bf6c6bfa68"}, + {file = "fonttools-4.63.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ccf41f2efdf56994d22d73bef4ced1052161958169428d06ba9724ea9e9a64be"}, + {file = "fonttools-4.63.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9ced0bd02ac751dd6319b0da88aaef24414e3b0dbc32bb4f24944821a3741a27"}, + {file = "fonttools-4.63.0-cp313-cp313-win32.whl", hash = "sha256:85be818f5506e8a7753153def2c9550178f0ecae6a47b5e0e8dbb23f7cc90380"}, + {file = "fonttools-4.63.0-cp313-cp313-win_amd64.whl", hash = "sha256:ba04cb5891d4c0c21b6da95eda8d7b090021508a294fff33464fc7d241e0856b"}, + {file = "fonttools-4.63.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fd1e3094f42d806d3d7c79162fc59e5910fcbe3a7360c385b8da969bc4493745"}, + {file = "fonttools-4.63.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6e528da43bc3791085f8cb6141b1d13e459226790240340fcbb4625649238b03"}, + {file = "fonttools-4.63.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b2248c5decb223562f7902ff6325077a073f608ee8e33e88ad88db734eb9f49"}, + {file = "fonttools-4.63.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:308f957cdeaf8abe4e5f2f124902ef405448af92c90f80e302a3b771c2e6116b"}, + {file = "fonttools-4.63.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bf00f21eb5fb721dbaf73d1e9da6d02a1af7768f2ebcf9798be98beab8ba90f6"}, + {file = "fonttools-4.63.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c1aaa4b9c75798400ac043ce04d74e7830376c85095a5a6ed7cba2f17a266bf4"}, + {file = "fonttools-4.63.0-cp314-cp314-win32.whl", hash = "sha256:22693918177bd9ceabec4736d338045f357769416fc6b0b2508eefef75b08616"}, + {file = "fonttools-4.63.0-cp314-cp314-win_amd64.whl", hash = "sha256:7d782fac32985914c351556f68ac0855391572bcd87de50e05970d3cd4c96fc5"}, + {file = "fonttools-4.63.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6db5140a60a5d731d21ec076745b40a310607731b0a565b50776393188649001"}, + {file = "fonttools-4.63.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d76edbff9014094dbf03bd2d074709dfa6ec7aba13d838c937a2b33d2d6a86e"}, + {file = "fonttools-4.63.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0eac00b9118c3c2f87d272e45341871c5b3066baa3c86897fa634a7c3fb59096"}, + {file = "fonttools-4.63.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51394295f1a51de8b5f30bdb1e1b9a4231536c7064ef5c6e211eec19fa36036f"}, + {file = "fonttools-4.63.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9e12f105d2b6342c559c298afb674006bb2893afc7102dcf8a1b55b0486b4e40"}, + {file = "fonttools-4.63.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:796f27556dbe094c4824f75ca85267e4df776c79036c8441469a4df37038c196"}, + {file = "fonttools-4.63.0-cp314-cp314t-win32.whl", hash = "sha256:948428a275741f0b64b113c955425a953314f4b9ab9997f73a72c83e68e569c8"}, + {file = "fonttools-4.63.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6d4741eb179121cab9eea4cb2393d24492373a260d7945006358c08cfbf45419"}, + {file = "fonttools-4.63.0-py3-none-any.whl", hash = "sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d"}, + {file = "fonttools-4.63.0.tar.gz", hash = "sha256:caeb583deeb5168e694b65cda8b4ee62abedfa66cf88488734466f2366b9c4e0"}, ] [package.extras] @@ -796,14 +796,14 @@ pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_ve [[package]] name = "idna" -version = "3.14" +version = "3.15" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "idna-3.14-py3-none-any.whl", hash = "sha256:e677eaf072e290f7b725f9acf0b3a2bd55f9fd6f7c70abe5f0e34823d0accf69"}, - {file = "idna-3.14.tar.gz", hash = "sha256:466d810d7a2cc1022bea9b037c39728d51ae7dad40d480fc9b7d7ecf98ba8ee3"}, + {file = "idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8"}, + {file = "idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc"}, ] [package.extras] @@ -1979,15 +1979,15 @@ diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pyreadline3" -version = "3.5.4" +version = "3.5.6" description = "A python implementation of GNU readline." optional = false python-versions = ">=3.8" groups = ["main"] markers = "sys_platform == \"win32\"" files = [ - {file = "pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6"}, - {file = "pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7"}, + {file = "pyreadline3-3.5.6-py3-none-any.whl", hash = "sha256:8449b734232e42a5dcd74048e39b60db2839a4c38cf3ae2bf7707d58b5389c0d"}, + {file = "pyreadline3-3.5.6.tar.gz", hash = "sha256:61e53218b99656091ddb077df9e71f25850e72e030b6183b39c9b7e6e4f4a9bf"}, ] [package.extras] @@ -2251,14 +2251,14 @@ decorator = "*" [[package]] name = "requests" -version = "2.34.0" +version = "2.34.2" description = "Python HTTP for Humans." optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "requests-2.34.0-py3-none-any.whl", hash = "sha256:917520a21b767485ce7c588f4ebb917c436b24a31231b44228715eaeb5a52c60"}, - {file = "requests-2.34.0.tar.gz", hash = "sha256:7d62fe92f50eb82c529b0916bb445afa1531a566fc8f35ffdc64446e771b856a"}, + {file = "requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0"}, + {file = "requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed"}, ] [package.dependencies] @@ -2273,30 +2273,30 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"] [[package]] name = "ruff" -version = "0.15.12" +version = "0.15.13" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "ruff-0.15.12-py3-none-linux_armv6l.whl", hash = "sha256:f86f176e188e94d6bdbc09f09bfd9dc729059ad93d0e7390b5a73efe19f8861c"}, - {file = "ruff-0.15.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e3bcd123364c3770b8e1b7baaf343cc99a35f197c5c6e8af79015c666c423a6c"}, - {file = "ruff-0.15.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fe87510d000220aa1ed530d4448a7c696a0cae1213e5ec30e5874287b66557b5"}, - {file = "ruff-0.15.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84a1630093121375a3e2a95b4a6dc7b59e2b4ee76216e32d81aae550a832d002"}, - {file = "ruff-0.15.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fb129f40f114f089ebe0ca56c0d251cf2061b17651d464bb6478dc01e69f11f5"}, - {file = "ruff-0.15.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0c862b172d695db7598426b8af465e7e9ac00a3ea2a3630ee67eb82e366aaa6"}, - {file = "ruff-0.15.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2849ea9f3484c3aca43a82f484210370319e7170df4dfe4843395ddf6c57bc33"}, - {file = "ruff-0.15.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e77c7e51c07fe396826d5969a5b846d9cd4c402535835fb6e21ce8b28fef847"}, - {file = "ruff-0.15.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b2f4f2f3b1026b5fb449b467d9264bf22067b600f7b6f41fc5958909f449d0"}, - {file = "ruff-0.15.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9ba3b8f1afd7e2e43d8943e55f249e13f9682fde09711644a6e7290eb4f3e339"}, - {file = "ruff-0.15.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e852ba9fdc890655e1d78f2df1499efbe0e54126bd405362154a75e2bde159c5"}, - {file = "ruff-0.15.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dd8aed930da53780d22fc70bdf84452c843cf64f8cb4eb38984319c24c5cd5fd"}, - {file = "ruff-0.15.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:01da3988d225628b709493d7dc67c3b9b12c0210016b08690ef9bd27970b262b"}, - {file = "ruff-0.15.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9cae0f92bd5700d1213188b31cd3bdd2b315361296d10b96b8e2337d3d11f53e"}, - {file = "ruff-0.15.12-py3-none-win32.whl", hash = "sha256:d0185894e038d7043ba8fd6aee7499ece6462dc0ea9f1e260c7451807c714c20"}, - {file = "ruff-0.15.12-py3-none-win_amd64.whl", hash = "sha256:c87a162d61ab3adca47c03f7f717c68672edec7d1b5499e652331780fe74950d"}, - {file = "ruff-0.15.12-py3-none-win_arm64.whl", hash = "sha256:a538f7a82d061cee7be55542aca1d86d1393d55d81d4fcc314370f4340930d4f"}, - {file = "ruff-0.15.12.tar.gz", hash = "sha256:ecea26adb26b4232c0c2ca19ccbc0083a68344180bba2a600605538ce51a40a6"}, + {file = "ruff-0.15.13-py3-none-linux_armv6l.whl", hash = "sha256:444b580fc72fd6887e650acd3e575e18cdc79dbcf42fb4030b491057921f61f8"}, + {file = "ruff-0.15.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6590d009e7cb7ebf36f83dbdd44a3fa48a0994ff6f1cdc1b08006abe58f98dc7"}, + {file = "ruff-0.15.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1c26d2f66163deeb6e08d8b39fbbe983ce3c71cea06a6d7591cfd1421793c629"}, + {file = "ruff-0.15.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dbd6f94b434f896308e4d57fb7bfde0d02b99f7a64b3bdab0fdfa6a864203a5"}, + {file = "ruff-0.15.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3259f3be4d181bda591da5db2571aed6853c6a048157756448020bc6c5cd22"}, + {file = "ruff-0.15.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae9c17e5eb4430c154e76abc25d79a318190f5a997f38fb6b114416c5319ffc9"}, + {file = "ruff-0.15.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e2e39bff6c341f4b577a21b801326fab0b11847f48fcaa83f00a113c9b3cb55"}, + {file = "ruff-0.15.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e8d9a8e08013542e94d3220bc5b62cc3e5ef87c5f74bff367d3fac14fab013e6"}, + {file = "ruff-0.15.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc411dfebe5eebe55ce041c6ae080eb7668955e866daa2fbb16692a784f1c4ca"}, + {file = "ruff-0.15.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:768494eb08b9cee54e2fd27969966f74db5a57f6eaa7a90fcb3306af34dfc4bd"}, + {file = "ruff-0.15.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:fb75f9a3a7e42ffe117d734494e6c5e5cb3565d66e12612cb63d0e572a41a5b6"}, + {file = "ruff-0.15.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8cb74dd33bb2f6613faf7fc03b660053b5ac4f80e706d5788c6335e2a8048d51"}, + {file = "ruff-0.15.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7ef823f817fcd191dc934e984be9cf4094f808effa16f2542ad8e821ba02bbf2"}, + {file = "ruff-0.15.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f345a13937bd7f09f6f5d19fa0721b0c103e00e7f62bc67089a8e5e037719e0b"}, + {file = "ruff-0.15.13-py3-none-win32.whl", hash = "sha256:4044f94208b3b05ba0fc4a4abd0558cf4d6459bd18325eead7fd8cc66f909b41"}, + {file = "ruff-0.15.13-py3-none-win_amd64.whl", hash = "sha256:7064884d442b7d477b4e7473d12da7f08851d2b1982763c5d3f388a19468a1a4"}, + {file = "ruff-0.15.13-py3-none-win_arm64.whl", hash = "sha256:2471da9bd1068c8c064b5fd9c0c4b6dddffd6369cb1cd68b29993b1709ff1b21"}, + {file = "ruff-0.15.13.tar.gz", hash = "sha256:f9d89f17f7ba7fb2ed42921f0df75da797a9a5d71bc39049e2c687cf2baf44b7"}, ] [[package]] @@ -2471,14 +2471,14 @@ files = [ [[package]] name = "types-requests" -version = "2.33.0.20260508" +version = "2.33.0.20260513" description = "Typing stubs for requests" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "types_requests-2.33.0.20260508-py3-none-any.whl", hash = "sha256:fa01459cca184229713df03709db46a905325906d27e042cd4fd7ea3d15d3400"}, - {file = "types_requests-2.33.0.20260508.tar.gz", hash = "sha256:81b2ae5f0d20967714a6aa5ef9284c05570d7cb06b7de8f2a77b918b63ddd411"}, + {file = "types_requests-2.33.0.20260513-py3-none-any.whl", hash = "sha256:d5a965f9d18b6e06b72039a69565de9027e58f36a7f709857da747fbe7521122"}, + {file = "types_requests-2.33.0.20260513.tar.gz", hash = "sha256:bd845450e954e751373d5d33526742592f298808a3ee3bda7e858e46b839b57f"}, ] [package.dependencies] @@ -2565,4 +2565,4 @@ plot = ["matplotlib", "plotly"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<3.15" -content-hash = "b0b2336ed601d2f07ddccfa9b38499e24074cceb679cb9ba1a72c13356272b6d" +content-hash = "fd9a7290f76bca76092e19472c887a9fd7ede519a9ac333520c6f99d69ef871d" diff --git a/pyproject.toml b/pyproject.toml index 0f40d2d8..0bd9bb6e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "simvue" -version = "2.5.1" +version = "2.5.2" description = "Simulation tracking and monitoring" authors = [{ name = "Simvue Development Team", email = "info@simvue.io" }] license = "Apache v2" @@ -85,6 +85,9 @@ pytest-timeout = "^2.3.1" requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" +[dependency-groups] +dev = [] + [tool.ruff] lint.extend-select = ["C901", "T201"] lint.mccabe.max-complexity = 11 diff --git a/simvue/api/objects/administrator/__init__.py b/simvue/api/objects/administrator/__init__.py index 315fc0c2..52fcdb6f 100644 --- a/simvue/api/objects/administrator/__init__.py +++ b/simvue/api/objects/administrator/__init__.py @@ -1,6 +1,4 @@ -""" -Simvue Admin Objects -==================== +"""Simvue Admin Objects. These are Simvue objects only accessible to an administrator of the server. diff --git a/simvue/api/objects/administrator/tenant.py b/simvue/api/objects/administrator/tenant.py index f3db4635..61c4b7fc 100644 --- a/simvue/api/objects/administrator/tenant.py +++ b/simvue/api/objects/administrator/tenant.py @@ -1,6 +1,4 @@ -""" -Simvue Tenants -============== +"""Simvue Tenants. Contains a class for remotely connecting to Simvue tenants, or defining a new tenant given relevant arguments. @@ -8,9 +6,10 @@ """ try: - from typing import Self + from typing import Self, override except ImportError: - from typing_extensions import Self + from typing_extensions import Self, override +from collections.abc import Generator import pydantic import datetime @@ -19,16 +18,21 @@ class Tenant(SimvueObject): - """ - Simvue Tenant - ============= + """Simvue Tenant. This class is used to connect to/create tenant objects on the Simvue server, any modification of instance attributes is mirrored on the remote object. """ - def __init__(self, identifier: str | None = None, **kwargs) -> None: + @override + def __init__( + self, + identifier: str | None = None, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, + **kwargs, + ) -> None: """Initialise a Tenant If an identifier is provided a connection will be made to the @@ -41,11 +45,18 @@ def __init__(self, identifier: str | None = None, **kwargs) -> None: ---------- identifier : str, optional the remote server unique id for the target folder + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None **kwargs : dict any additional arguments to be passed to the object initialiser """ - super().__init__(identifier, **kwargs) + super().__init__( + identifier, server_url=server_url, server_token=server_token, **kwargs + ) + @override @classmethod @pydantic.validate_call def new( @@ -57,6 +68,8 @@ def new( max_runs: int = 0, max_data_volume: int = 0, offline: bool = False, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, **_, ) -> Self: """Create a new tenant on the Simvue server. @@ -77,6 +90,10 @@ def new( the maximum volume of data allowed within this tenant, default is no limit. offline: bool, optional create in offline mode, default is False. + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None Returns ------- @@ -84,16 +101,58 @@ def new( a tenant instance with staged changes """ - return Tenant( + return cls( name=name, is_enabled=is_enabled, max_request_rate=max_request_rate, max_runs=max_runs, max_data_volume=max_data_volume, + server_url=server_url, + server_token=server_token, _read_only=False, _offline=offline, ) + @override + @classmethod + @pydantic.validate_call + def get( + cls, + *, + count: pydantic.PositiveInt | None = None, + offset: pydantic.NonNegativeInt | None = None, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, + **kwargs, + ) -> Generator[tuple[str, Self | None]]: + """Retrieve tenants from the server. + + Parameters + ---------- + count: int | None, optional + limit number of objects + offset : int | None, optional + set start index for objects list + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None + + Yields + ------ + tuple[str, Tenant | None] + object corresponding to an entry on the server. + + Returns + ------- + Generator[tuple[str, Tenant | None]] + """ + # Currently no tenant filters + _ = kwargs.pop("filters", None) + return super().get( + count=count, offset=offset, server_url=server_url, server_token=server_token + ) + @property def name(self) -> str: """Retrieve the name of the tenant""" diff --git a/simvue/api/objects/administrator/user.py b/simvue/api/objects/administrator/user.py index bab8696c..272cc7c5 100644 --- a/simvue/api/objects/administrator/user.py +++ b/simvue/api/objects/administrator/user.py @@ -1,6 +1,4 @@ -""" -Simvue Users -============ +"""Simvue Users. Contains a class for remotely connecting to Simvue users, or defining a new user given relevant arguments. @@ -13,23 +11,29 @@ from simvue.models import DATETIME_FORMAT try: - from typing import Self + from typing import Self, override except ImportError: - from typing_extensions import Self + from typing_extensions import Self, override from simvue.api.objects.base import SimvueObject, staging_check, write_only class User(SimvueObject): - """ - Simvue User - =========== + """Simvue User. This class is used to connect to/create user objects on the Simvue server, any modification of instance attributes is mirrored on the remote object. """ - def __init__(self, identifier: str | None = None, **kwargs) -> None: + @override + def __init__( + self, + identifier: str | None = None, + *, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, + **kwargs, + ) -> None: """Initialise a User If an identifier is provided a connection will be made to the @@ -42,11 +46,18 @@ def __init__(self, identifier: str | None = None, **kwargs) -> None: ---------- identifier : str, optional the remote server unique id for the target folder + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None **kwargs : dict any additional arguments to be passed to the object initialiser """ - super().__init__(identifier, **kwargs) + super().__init__( + identifier, server_url=server_url, server_token=server_token, **kwargs + ) + @override @classmethod @pydantic.validate_call def new( @@ -62,6 +73,8 @@ def new( tenant: str, enabled: bool = True, offline: bool = False, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, **_, ) -> Self: """Create a new user on the Simvue server. @@ -90,6 +103,10 @@ def new( whether to enable the user on creation, default is True offline: bool, optional create in offline mode, default is False. + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None Returns ------- @@ -107,19 +124,28 @@ def new( "is_admin": is_admin, "is_enabled": enabled, } - _user = User( + _user = cls( user=_user_info, tenant=tenant, offline=offline, _read_only=False, _offline=offline, + server_url=server_url, + server_token=server_token, ) _user._staging |= _user_info return _user + @override @classmethod def get( - cls, *, count: int | None = None, offset: int | None = None, **kwargs + cls, + *, + count: int | None = None, + offset: int | None = None, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, + **kwargs, ) -> dict[str, "User"]: """Retrieve users from the Simvue server. @@ -129,6 +155,10 @@ def get( limit the number of results, default is no limit. offset : int, optional start index for results, default is 0. + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None Yields ------ @@ -136,8 +166,14 @@ def get( user instance representing user on server """ # Currently no user filters - kwargs.pop("filters", None) - return super().get(count=count, offset=offset, **kwargs) + _ = kwargs.pop("filters", None) + return super().get( + count=count, + offset=offset, + server_url=server_url, + server_token=server_token, + **kwargs, + ) @property @staging_check diff --git a/simvue/api/objects/alert/__init__.py b/simvue/api/objects/alert/__init__.py index b9be7d66..71cabde8 100644 --- a/simvue/api/objects/alert/__init__.py +++ b/simvue/api/objects/alert/__init__.py @@ -1,6 +1,4 @@ -""" -Simvue Alerts -============= +"""Simvue Alert Retrieval. Creation and management of Alerts on the Simvue server, the alerts are split into sub-categories to ensure correct arguments diff --git a/simvue/api/objects/alert/base.py b/simvue/api/objects/alert/base.py index 9aaa2088..4eceb699 100644 --- a/simvue/api/objects/alert/base.py +++ b/simvue/api/objects/alert/base.py @@ -1,6 +1,4 @@ -""" -Alert Object Base -================= +"""Alert Object Base. Contains general definitions for Simvue Alert objects. @@ -16,9 +14,9 @@ from simvue.models import NAME_REGEX, DATETIME_FORMAT try: - from typing import override + from typing import Self, override except ImportError: - from typing_extensions import override # noqa: UP035 + from typing_extensions import Self, override # noqa: UP035 class AlertBase(SimvueObject): @@ -27,15 +25,29 @@ class AlertBase(SimvueObject): Contains properties common to all alert types. """ + _label: str = "alert" + + @override @classmethod - def new(cls, read_only: bool = False, **kwargs): - """Create a new alert""" - pass + def new(cls, *_, **__) -> Self: + raise NotImplementedError - def __init__(self, identifier: str | None = None, **kwargs) -> None: + @override + def __init__( + self, + identifier: str | None = None, + *, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, + **kwargs, + ) -> None: """Retrieve an alert from the Simvue server by identifier""" - self._label = "alert" - super().__init__(identifier=identifier, **kwargs) + super().__init__( + identifier=identifier, + server_url=server_url, + server_token=server_token, + **kwargs, + ) self._local_only_args += [ "frequency", "pattern", diff --git a/simvue/api/objects/alert/events.py b/simvue/api/objects/alert/events.py index 6963568f..2833b82c 100644 --- a/simvue/api/objects/alert/events.py +++ b/simvue/api/objects/alert/events.py @@ -1,6 +1,4 @@ -""" -Simvue Event Alerts -=================== +"""Simvue Event Alerts. Interface to event-based Simvue alerts. @@ -9,26 +7,35 @@ import typing import pydantic +from collections.abc import Generator + try: from typing import Self, override except ImportError: from typing_extensions import Self, override -from simvue.api.objects.base import write_only -from .base import AlertBase, staging_check + +from simvue.api.objects.base import write_only, staging_check +from .base import AlertBase from simvue.models import NAME_REGEX class EventsAlert(AlertBase): - """ - Simvue Events Alert - =================== + """Simvue Events Alert. This class is used to connect to/create event-based alert objects on the Simvue server, any modification of EventsAlert instance attributes is mirrored on the remote object. """ - def __init__(self, identifier: str | None = None, **kwargs) -> None: + @override + def __init__( + self, + identifier: str | None = None, + *, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, + **kwargs, + ) -> None: """Initialise an Events Alert If an identifier is provided a connection will be made to the @@ -39,16 +46,28 @@ def __init__(self, identifier: str | None = None, **kwargs) -> None: ---------- identifier : str, optional the remote server unique id for the target folder + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None **kwargs : dict any additional arguments to be passed to the object initialiser """ self.alert = EventAlertDefinition(self) - super().__init__(identifier, **kwargs) + super().__init__( + identifier, server_url=server_url, server_token=server_token, **kwargs + ) + @override @classmethod def get( - cls, count: int | None = None, offset: int | None = None - ) -> dict[str, typing.Any]: + cls, + *, + count: int | None = None, + offset: int | None = None, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, + ) -> Generator[dict[str, typing.Any]]: """Retrieve only alerts of the event alert type""" raise NotImplementedError("Retrieval of only event alerts is not yet supported") @@ -62,6 +81,8 @@ def new( notification: typing.Literal["none", "email"], pattern: str, frequency: pydantic.PositiveInt, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, enabled: bool = True, offline: bool = False, **_, @@ -86,6 +107,10 @@ def new( enable this alert upon creation, default is True offline : bool, optional create alert locally, default is False + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None Returns ------- @@ -95,13 +120,15 @@ def new( """ _alert_definition = {"pattern": pattern, "frequency": frequency} - _alert = EventsAlert( + _alert = cls( name=name, description=description, notification=notification, source="events", alert=_alert_definition, enabled=enabled, + server_url=server_url, + server_token=server_token, _read_only=False, _offline=offline, ) diff --git a/simvue/api/objects/alert/fetch.py b/simvue/api/objects/alert/fetch.py index 7bdf6fbe..2d6d503f 100644 --- a/simvue/api/objects/alert/fetch.py +++ b/simvue/api/objects/alert/fetch.py @@ -1,6 +1,4 @@ -""" -Simvue Alert Retrieval -====================== +"""Simvue Alert Retrieval. To simplify case whereby user does not know the alert type associated with an identifier, use a generic alert object. @@ -11,6 +9,14 @@ import pydantic +from simvue.api.url import URL +from simvue.config.user import SimvueConfiguration + +try: + from typing import override +except ImportError: + from typing_extensions import override + from collections.abc import Generator from simvue.api.objects.alert.user import UserAlert from simvue.api.objects.base import Sort @@ -33,28 +39,44 @@ def check_column(cls, column: str) -> str: class Alert: - """ - Simvue Alert - ============ + """Simvue Alert. Generic Simvue alert retrieval class. """ - def __init__(self, identifier: str | None = None, *args, **kwargs) -> None: - """Initialise an instance of generic alert retriever. + @override + @pydantic.validate_call() + def __new__( + cls, + identifier: str | None = None, + *, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, + **kwargs, + ) -> AlertType: + """Retrieve an object representing an alert on the server by id. Parameters ---------- identifier : str - identifier of alert object to retrieve + identifier of alert to retrieve + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None + + Returns + ------- + MetricsThresholdAlert | MetricRangeAlert | UserAlert | EventsAlert + object representing an alert """ - super().__init__(identifier=identifier, *args, **kwargs) - - @pydantic.validate_call() - def __new__(cls, identifier: str, **kwargs) -> AlertType: - """Retrieve an object representing an alert either locally or on the server by id""" - _alert_pre = AlertBase(identifier=identifier, **kwargs) + _alert_pre = AlertBase( + identifier=identifier, + server_url=server_url, + server_token=server_token, + **kwargs, + ) if ( identifier is not None and identifier.startswith("offline_") @@ -62,17 +84,37 @@ def __new__(cls, identifier: str, **kwargs) -> AlertType: ): raise RuntimeError( "Cannot determine Alert type - this is likely because you are attempting to reconnect " - "to an offline alert which has already been sent to the server. To fix this, use the " - "exact Alert type instead (eg MetricThresholdAlert, MetricRangeAlert etc)." + + "to an offline alert which has already been sent to the server. To fix this, use the " + + "exact Alert type instead (eg MetricThresholdAlert, MetricRangeAlert etc)." ) if _alert_pre.source == "events": - return EventsAlert(identifier=identifier, **kwargs) + return EventsAlert( + identifier=identifier, + server_url=server_url, + server_token=server_token, + **kwargs, + ) elif _alert_pre.source == "metrics" and _alert_pre.get_alert().get("threshold"): - return MetricsThresholdAlert(identifier=identifier, **kwargs) + return MetricsThresholdAlert( + identifier=identifier, + server_url=server_url, + server_token=server_token, + **kwargs, + ) elif _alert_pre.source == "metrics": - return MetricsRangeAlert(identifier=identifier, **kwargs) + return MetricsRangeAlert( + identifier=identifier, + server_url=server_url, + server_token=server_token, + **kwargs, + ) elif _alert_pre.source == "user": - return UserAlert(identifier=identifier, **kwargs) + return UserAlert( + identifier=identifier, + server_url=server_url, + server_token=server_token, + **kwargs, + ) raise RuntimeError(f"Unknown source type '{_alert_pre.source}'") @@ -80,10 +122,12 @@ def __new__(cls, identifier: str, **kwargs) -> AlertType: @pydantic.validate_call def get( cls, - offline: bool = False, + *, count: int | None = None, offset: int | None = None, sorting: list[AlertSort] | None = None, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, **kwargs, ) -> Generator[tuple[str, AlertType]]: """Fetch all alerts from the server for the current user. @@ -96,6 +140,10 @@ def get( start index for returned results, default of None starts at 0. sorting : list[dict] | None, optional list of sorting definitions in the form {'column': str, 'descending': bool} + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None Yields ------ @@ -103,27 +151,27 @@ def get( identifier for an alert the alert itself as a class instance """ - if offline: - return # Currently no alert filters - kwargs.pop("filters", None) + _ = kwargs.pop("filters", None) + + _config: SimvueConfiguration = SimvueConfiguration.fetch( + mode="online", server_url=server_url, server_token=server_token + ) - _class_instance = AlertBase(_local=True, _read_only=True) - _url = f"{_class_instance._base_url}" + _url = URL(f"{_config.server.url}") / AlertBase.endpoint() _params: dict[str, int | str] = {"start": offset, "count": count} if sorting: _params["sorting"] = json.dumps([sort.to_params() for sort in sorting]) _response = sv_get( - _url, - headers=_class_instance._headers, + f"{_url}", + headers=_config.headers, params=_params | kwargs, ) - _label: str = _class_instance.__class__.__name__.lower() - _label = _label.replace("base", "") + _label: str = cls.__name__.lower() _json_response = get_json_from_response( response=_response, expected_status=[http.HTTPStatus.OK], diff --git a/simvue/api/objects/alert/metrics.py b/simvue/api/objects/alert/metrics.py index 7eef0e7b..43ce4843 100644 --- a/simvue/api/objects/alert/metrics.py +++ b/simvue/api/objects/alert/metrics.py @@ -1,6 +1,4 @@ -""" -Simvue Metric Alerts -==================== +"""Simvue Metric Alerts. Classes for interacting with metric-based alerts either defined locally or on a Simvue server @@ -29,16 +27,21 @@ class MetricsThresholdAlert(AlertBase): - """ - Simvue Metrics Threshold Alert - ============================== + """Simvue Metrics Threshold Alert. This class is used to connect to/create metrics threshold alert objects on the Simvue server, any modification of MetricsThresholdAlert instance attributes is mirrored on the remote object. """ - def __init__(self, identifier: str | None = None, **kwargs) -> None: + def __init__( + self, + identifier: str | None = None, + *, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, + **kwargs, + ) -> None: """Initialise a Metrics Threshold Alert If an identifier is provided a connection will be made to the @@ -49,11 +52,17 @@ def __init__(self, identifier: str | None = None, **kwargs) -> None: ---------- identifier : str, optional the remote server unique id for the target folder + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None **kwargs : dict any additional arguments to be passed to the object initialiser """ self.alert = MetricThresholdAlertDefinition(self) - super().__init__(identifier, **kwargs) + super().__init__( + identifier, server_url=server_url, server_token=server_token, **kwargs + ) self._local_only_args += [ "rule", "window", @@ -61,13 +70,21 @@ def __init__(self, identifier: str | None = None, **kwargs) -> None: "threshold", ] + @override @classmethod def get( - cls, count: int | None = None, offset: int | None = None + cls, + *, + count: int | None = None, + offset: int | None = None, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, + **_, ) -> dict[str, typing.Any]: """Retrieve only MetricsThresholdAlerts""" raise NotImplementedError("Retrieve of only metric alerts is not yet supported") + @override @classmethod @pydantic.validate_call def new( @@ -84,6 +101,8 @@ def new( frequency: pydantic.PositiveInt, enabled: bool = True, offline: bool = False, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, **_, ) -> Self: """Create a new metric threshold alert either locally or on the server @@ -114,7 +133,15 @@ def new( whether this alert is enabled upon creation, default is True offline : bool, optional whether to create the alert locally, default is False - + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None + + Returns + ------- + MetricsThresholdAlert + object representing a metric threshold alert """ _alert_definition = { "rule": rule, @@ -124,13 +151,15 @@ def new( "aggregation": aggregation, "threshold": threshold, } - _alert = MetricsThresholdAlert( + _alert = cls( name=name, description=description, notification=notification, source="metrics", alert=_alert_definition, enabled=enabled, + server_url=server_url, + server_token=server_token, _read_only=False, _offline=offline, ) @@ -147,16 +176,21 @@ def _compare_objects(self, other: "AlertBase") -> bool: class MetricsRangeAlert(AlertBase): - """ - Simvue Metrics Range Alert - ========================== + """Simvue Metrics Range Alert. This class is used to connect to/create metrics range alert objects on the Simvue server, any modification of MetricsRangeAlert instance attributes is mirrored on the remote object. """ - def __init__(self, identifier: str | None = None, **kwargs) -> None: + def __init__( + self, + identifier: str | None = None, + *, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, + **kwargs, + ) -> None: """Initialise a Metrics Range Alert If an identifier is provided a connection will be made to the @@ -167,11 +201,17 @@ def __init__(self, identifier: str | None = None, **kwargs) -> None: ---------- identifier : str, optional the remote server unique id for the target folder + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None **kwargs : dict any additional arguments to be passed to the object initialiser """ self.alert = MetricRangeAlertDefinition(self) - super().__init__(identifier, **kwargs) + super().__init__( + identifier, server_url=server_url, server_token=server_token, **kwargs + ) self._local_only_args += [ "rule", "window", @@ -204,6 +244,8 @@ def new( frequency: pydantic.PositiveInt, enabled: bool = True, offline: bool = False, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, **_, ) -> Self: """Create a new metric range alert either locally or on the server @@ -236,6 +278,10 @@ def new( whether this alert is enabled upon creation, default is True offline : bool, optional whether to create the alert locally, default is False + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None """ if range_low >= range_high: @@ -250,13 +296,15 @@ def new( "range_low": range_low, "range_high": range_high, } - _alert = MetricsRangeAlert( + _alert = cls( name=name, description=description, notification=notification, source="metrics", enabled=enabled, alert=_alert_definition, + server_url=server_url, + server_token=server_token, _read_only=False, _offline=offline, ) diff --git a/simvue/api/objects/alert/user.py b/simvue/api/objects/alert/user.py index 94e2e003..2aef813b 100644 --- a/simvue/api/objects/alert/user.py +++ b/simvue/api/objects/alert/user.py @@ -1,6 +1,4 @@ -""" -Simvue User Alert -================= +"""Simvue User Alert. Class for connecting with a local/remote user defined alert. @@ -21,16 +19,21 @@ class UserAlert(AlertBase): - """ - Simvue User Alert - ================= + """Simvue User Alert. This class is used to connect to/create user alert objects on the Simvue server, any modification of UserAlert instance attributes is mirrored on the remote object. """ - def __init__(self, identifier: str | None = None, **kwargs) -> None: + @override + def __init__( + self, + identifier: str | None = None, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, + **kwargs, + ) -> None: """Initialise a User Alert If an identifier is provided a connection will be made to the @@ -41,12 +44,19 @@ def __init__(self, identifier: str | None = None, **kwargs) -> None: ---------- identifier : str, optional the remote server unique id for the target folder + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None **kwargs : dict any additional arguments to be passed to the object initialiser """ - super().__init__(identifier, **kwargs) + super().__init__( + identifier, server_url=server_url, server_token=server_token, **kwargs + ) self._local_status: dict[str, str | None] = kwargs.pop("status", {}) + @override @classmethod @pydantic.validate_call def new( @@ -57,6 +67,8 @@ def new( notification: typing.Literal["none", "email"], enabled: bool = True, offline: bool = False, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, **_, ) -> Self: """Create a new user-defined alert @@ -75,14 +87,20 @@ def new( whether this alert is enabled upon creation, default is True offline : bool, optional whether this alert should be created locally, default is False + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None """ - _alert = UserAlert( + _alert = cls( name=name, description=description, notification=notification, source="user", enabled=enabled, + server_url=server_url, + server_token=server_token, _read_only=False, _offline=offline, ) diff --git a/simvue/api/objects/artifact/__init__.py b/simvue/api/objects/artifact/__init__.py index 168fb3a8..52f3e438 100644 --- a/simvue/api/objects/artifact/__init__.py +++ b/simvue/api/objects/artifact/__init__.py @@ -1,3 +1,5 @@ +"""Simvue Artifact Retrieval.""" + from .fetch import Artifact as Artifact from .file import FileArtifact as FileArtifact from .object import ObjectArtifact as ObjectArtifact diff --git a/simvue/api/objects/artifact/base.py b/simvue/api/objects/artifact/base.py index cb056061..14991c15 100644 --- a/simvue/api/objects/artifact/base.py +++ b/simvue/api/objects/artifact/base.py @@ -1,6 +1,4 @@ -""" -Simvue Artifact -=============== +"""Simvue Artifact. Class for defining and interacting with artifact objects. @@ -13,9 +11,9 @@ import pydantic try: - from typing import Self + from typing import Self, override except ImportError: - from typing_extensions import Self # noqa: F401 + from typing_extensions import Self, override # noqa: F401, from simvue.api.url import URL from collections.abc import Generator @@ -41,20 +39,25 @@ class ArtifactBase(SimvueObject): """Connect to/create an artifact locally or on the server""" + _label: str = "artifact" + + @override def __init__( - self, identifier: str | None = None, _read_only: bool = True, **kwargs + self, + identifier: str | None = None, + *, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, + **kwargs, ) -> None: - """Initialise an artifact connection. - - Parameters - ---------- - identifier : str, optional - the identifier of this object on the server. - """ + """Retrieve an artifact instance from the Simvue server by identifier.""" - self._label = "artifact" - self._endpoint = f"{self._label}s" - super().__init__(identifier=identifier, _read_only=_read_only, **kwargs) + super().__init__( + identifier=identifier, + server_url=server_url, + server_token=server_token, + **kwargs, + ) self._local_only_args += ["storage", "file_path", "runs"] # If the artifact is an online instance, need a place to store the response @@ -317,7 +320,7 @@ def get_category(self, run_id: str) -> Category: ) if _response.status_code == http.HTTPStatus.NOT_FOUND: raise ObjectNotFoundError( - self._label, self._identifier, extra=f"for run '{run_id}'" + self.label(), self._identifier, extra=f"for run '{run_id}'" ) return _json_response["category"] @@ -356,7 +359,7 @@ def download_content(self) -> Generator[bytes]: response=_response, allow_parse_failure=True, expected_status=[http.HTTPStatus.OK], - scenario=f"Retrieval of file for {self._label} '{self._identifier}'", + scenario=f"Retrieval of file for {self.label()} '{self._identifier}'", ) _total_length: str | None = _response.headers.get("content-length") diff --git a/simvue/api/objects/artifact/fetch.py b/simvue/api/objects/artifact/fetch.py index 67aab36d..b15f7f77 100644 --- a/simvue/api/objects/artifact/fetch.py +++ b/simvue/api/objects/artifact/fetch.py @@ -1,10 +1,18 @@ +"""Simvue Artifact Retrieval. + +To simplify case whereby user does not know the artifact type associated +with an identifier, use a generic artifact object. +""" + import http import typing import pydantic import json + from simvue.api.objects.artifact.base import ArtifactBase from simvue.api.objects.base import Sort +from simvue.config.user import SimvueConfiguration from .file import FileArtifact from collections.abc import Generator from simvue.api.objects.artifact.object import ObjectArtifact @@ -28,38 +36,65 @@ def check_column(cls, column: str) -> str: class Artifact: - """ - Simvue Artifact - =============== + """Simvue Artifact. Generic Simvue artifact retrieval class. """ - def __init__(self, identifier: str | None = None, *args, **kwargs) -> None: - """Initialise an instance of generic artifact retriever. + def __new__( + cls, + identifier: str | None = None, + *, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, + **kwargs, + ) -> FileArtifact | ObjectArtifact: + """Retrieve an object representing an artifact on the server by id. Parameters ---------- identifier : str - identifier of artifact object to retrieve - """ - super().__init__(identifier=identifier, *args, **kwargs) + identifier of storage object to retrieve + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None - def __new__(cls, identifier: str | None = None, **kwargs): - """Retrieve an object representing an Artifact by id""" - _artifact_pre = ArtifactBase(identifier=identifier, **kwargs) + Returns + ------- + FileArtifact | ObjectArtifact + object representing storage + """ + _artifact_pre = ArtifactBase( + identifier=identifier, + server_url=server_url, + server_token=server_token, + **kwargs, + ) if _artifact_pre.original_path: - return FileArtifact(identifier=identifier, **kwargs) + return FileArtifact( + identifier=identifier, + server_url=server_url, + server_token=server_token, + **kwargs, + ) else: - return ObjectArtifact(identifier=identifier, **kwargs) + return ObjectArtifact( + identifier=identifier, + server_url=server_url, + server_token=server_token, + **kwargs, + ) @classmethod def from_run( cls, run_id: str, + *, category: typing.Literal["input", "output", "code"] | None = None, - **kwargs, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, ) -> Generator[tuple[str, FileArtifact | ObjectArtifact]]: """Return artifacts associated with a given run. @@ -72,6 +107,10 @@ def from_run( * input - this file is an input file. * output - this file is created by the run. * code - this file represents an executed script + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None Returns ------- @@ -89,10 +128,12 @@ def from_run( 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" + _config: SimvueConfiguration = SimvueConfiguration.fetch( + mode="online", server_url=server_url, server_token=server_token + ) + _url = URL(f"{_config.server.url}") / f"runs/{run_id}/artifacts" _response = sv_get( - url=f"{_url}", params={"category": category}, headers=_temp._headers + url=f"{_url}", params={"category": category}, headers=_config.headers ) _json_response = get_json_from_response( expected_type=list, @@ -103,7 +144,7 @@ def from_run( if _response.status_code == http.HTTPStatus.NOT_FOUND or not _json_response: raise ObjectNotFoundError( - _temp._label, category, extra=f"for run '{run_id}'" + ArtifactBase.label, category, extra=f"for run '{run_id}'" ) for _entry in _json_response: @@ -115,8 +156,14 @@ def from_run( @classmethod def from_name( - cls, run_id: str, name: str, force_overwrite: bool = False, **kwargs - ) -> typing.Union[FileArtifact | ObjectArtifact, None]: + cls, + run_id: str, + name: str, + *, + force_overwrite: bool = False, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, + ) -> FileArtifact | ObjectArtifact | None: """Retrieve an artifact by name. Parameters @@ -129,6 +176,10 @@ def from_name( if duplicates are detected force download the first match, default of False will raise an exception + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None Returns ------- @@ -140,9 +191,13 @@ def from_name( RuntimeError when duplicate artifacts are found within a single run """ - _temp = ArtifactBase(**kwargs) - _url = URL(_temp._user_config.server.url) / f"runs/{run_id}/artifacts" - _response = sv_get(url=f"{_url}", params={"name": name}, headers=_temp._headers) + _config: SimvueConfiguration = SimvueConfiguration.fetch( + mode="online", server_url=server_url, server_token=server_token + ) + _url = URL(f"{_config.server.url}") / f"runs/{run_id}/artifacts" + _response = sv_get( + url=f"{_url}", params={"name": name}, headers=_config.headers + ) _json_response = get_json_from_response( expected_type=list, response=_response, @@ -151,7 +206,9 @@ def from_name( ) if _response.status_code == http.HTTPStatus.NOT_FOUND or not _json_response: - raise ObjectNotFoundError(_temp._label, name, extra=f"for run '{run_id}'") + raise ObjectNotFoundError( + ArtifactBase.label(), name, extra=f"for run '{run_id}'" + ) if (_n_res := len(_json_response)) > 1 and not force_overwrite: raise RuntimeError( @@ -165,8 +222,9 @@ def from_name( return Artifact( identifier=_artifact_id, run=run_id, + server_url=server_url, + server_token=server_token, **_first_result, - _read_only=True, _local=True, ) @@ -174,9 +232,12 @@ def from_name( @pydantic.validate_call def get( cls, + *, count: int | None = None, offset: int | None = None, sorting: list[ArtifactSort] | None = None, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, **kwargs, ) -> Generator[tuple[str, FileArtifact | ObjectArtifact]]: """Returns artifacts associated with the current user. @@ -189,6 +250,10 @@ def get( start index for returned results, default of None starts at 0. sorting : list[dict] | None, optional list of sorting definitions in the form {'column': str, 'descending': bool} + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None Yields ------ @@ -197,8 +262,10 @@ def get( the artifact itself as a class instance """ - _class_instance = ArtifactBase(_local=True, _read_only=True) - _url = f"{_class_instance._base_url}" + _config: SimvueConfiguration = SimvueConfiguration.fetch( + mode="online", server_url=server_url, server_token=server_token + ) + _url = URL(f"{_config.server.url}") / ArtifactBase.endpoint() _params = {"start": offset, "count": count} if sorting: @@ -206,10 +273,10 @@ def get( _response = sv_get( _url, - headers=_class_instance._headers, + headers=_config.headers, params=_params | kwargs, ) - _label: str = _class_instance.__class__.__name__.lower() + _label: str = cls.__name__.lower() _label = _label.replace("base", "") _json_response = get_json_from_response( response=_response, diff --git a/simvue/api/objects/artifact/file.py b/simvue/api/objects/artifact/file.py index 8ad31ad7..bfc07e61 100644 --- a/simvue/api/objects/artifact/file.py +++ b/simvue/api/objects/artifact/file.py @@ -1,3 +1,10 @@ +"""Simvue File Artifacts. + +Classes for interacting with file based artifacts defined +locally or on a Simvue server + +""" + from .base import ArtifactBase import typing @@ -17,9 +24,7 @@ class FileArtifact(ArtifactBase): - """ - Simvue File Artifact - ==================== + """Simvue File Artifact. This class is used to connect to/create file artifact objects on the Simvue server, any modification of instance attributes is mirrored on the remote object. @@ -27,7 +32,11 @@ class FileArtifact(ArtifactBase): """ def __init__( - self, identifier: str | None = None, _read_only: bool = True, **kwargs + self, + identifier: str | None = None, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, + **kwargs, ) -> None: """Initialise a File Artifact @@ -39,10 +48,19 @@ def __init__( ---------- identifier : str, optional the remote server unique id for the target folder + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None **kwargs : dict any additional arguments to be passed to the object initialiser """ - super().__init__(identifier=identifier, _read_only=_read_only, **kwargs) + super().__init__( + identifier=identifier, + server_url=server_url, + server_token=server_token, + **kwargs, + ) @classmethod def new( @@ -56,6 +74,8 @@ def new( upload_timeout: int | None = None, offline: bool = False, snapshot: bool = False, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, **kwargs, ) -> Self: """Create a new artifact either locally or on the server @@ -80,6 +100,15 @@ def new( whether to define this artifact locally, default is False snapshot : bool, optional whether to create a snapshot of this file before uploading it, default is False + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None + + Returns + ------- + FileArtifact + an object representing the artifact """ _mime_type = mime_type or get_mimetype_for_file(file_path) @@ -94,7 +123,9 @@ def new( file_path = pathlib.Path(file_path) if snapshot: _user_config = SimvueConfiguration.fetch( - mode="offline" if offline else "online" + mode="offline" if offline else "online", + server_url=server_url, + server_token=server_token, ) _local_staging_dir: pathlib.Path = _user_config.offline.cache.joinpath( @@ -111,11 +142,13 @@ def new( _file_orig_path = file_path.expanduser().absolute() _file_checksum = calculate_sha256(f"{file_path}", is_file=True) - _artifact = FileArtifact( + _artifact = cls( name=name, storage=storage, original_path=os.path.expandvars(_file_orig_path), size=_file_size, + server_url=server_url, + server_token=server_token, mime_type=_mime_type, checksum=_file_checksum, _offline=offline, diff --git a/simvue/api/objects/artifact/object.py b/simvue/api/objects/artifact/object.py index 33ea6259..8faff12d 100644 --- a/simvue/api/objects/artifact/object.py +++ b/simvue/api/objects/artifact/object.py @@ -1,3 +1,10 @@ +"""Simvue Object Artifacts. + +Classes for interacting with object based artifacts defined +locally or on a Simvue server + +""" + from .base import ArtifactBase from simvue.models import NAME_REGEX from simvue.serialization import serialize_object @@ -9,23 +16,27 @@ import io try: - from typing import Self + from typing import Self, override except ImportError: - from typing_extensions import Self + from typing_extensions import Self, override class ObjectArtifact(ArtifactBase): - """ - Simvue Object Artifact - ====================== + """Simvue Object Artifact. This class is used to connect to/create file object artifact objects on the Simvue server, any modification of instance attributes is mirrored on the remote object. """ + @override def __init__( - self, identifier: str | None = None, _read_only: bool = True, **kwargs + self, + identifier: str | None = None, + *, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, + **kwargs, ) -> None: """Initialise a Object Artifact @@ -37,12 +48,17 @@ def __init__( ---------- identifier : str, optional the remote server unique id for the target folder + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None **kwargs : dict any additional arguments to be passed to the object initialiser """ kwargs.pop("original_path", None) - super().__init__(identifier, _read_only, original_path="", **kwargs) + super().__init__(identifier, original_path="", **kwargs) + @override @classmethod @pydantic.validate_call def new( @@ -55,6 +71,8 @@ def new( upload_timeout: int | None = None, allow_pickling: bool = True, offline: bool = False, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, **kwargs, ) -> Self: """Create a new artifact either locally or on the server @@ -78,6 +96,15 @@ def new( serialization found. Default is True offline : bool, optional whether to define this artifact locally, default is False + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None + + Returns + ------- + ObjectArtifact + an object representing the artifact """ # If the object has been saved as a bytes file, obj will be None @@ -104,12 +131,14 @@ def new( _checksum = calculate_sha256(_serialized, is_file=False) - _artifact = ObjectArtifact( + _artifact = cls( name=name, storage=storage, size=sys.getsizeof(_serialized), mime_type=_data_type, checksum=_checksum, + server_url=server_url, + server_token=server_token, metadata=metadata, _offline=offline, _read_only=False, diff --git a/simvue/api/objects/base.py b/simvue/api/objects/base.py index 30bf85e8..fb979103 100644 --- a/simvue/api/objects/base.py +++ b/simvue/api/objects/base.py @@ -1,6 +1,4 @@ -""" -Simvue RestAPI Objects -====================== +"""Simvue RestAPI Objects. Contains base class for interacting with objects on the Simvue server """ @@ -76,7 +74,7 @@ def _wrapper(self: "SimvueObject", *args, **kwargs) -> typing.Any: if _sv_obj._read_only: raise AssertionError( f"Cannot set property '{attribute_func.__name__}' " - f"on read-only object of type '{self._label}'" + f"on read-only object of type '{self.label()}'" ) return attribute_func(self, *args, **kwargs) @@ -183,7 +181,10 @@ class ObjectBatchArgs(pydantic.BaseModel): class SimvueObject(abc.ABC): def __init__( self, - identifier: str | None = None, + identifier: str | None, + *, + server_url: str | None, + server_token: pydantic.SecretStr | None, _read_only: bool = True, _local: bool = False, _user_agent: str | None = None, @@ -191,11 +192,9 @@ def __init__( **kwargs, ) -> None: self._logger = logging.getLogger(f"simvue.{self.__class__.__name__}") - self._label: str = getattr(self, "_label", self.__class__.__name__.lower()) self._local: bool = _local self._read_only: bool = _read_only self._is_set: bool = False - self._endpoint: str = getattr(self, "_endpoint", f"{self._label}s") # For simvue object initialisation, unlike the server there is no nested # arguments, however this means that there are extra keys during post which @@ -214,19 +213,17 @@ def __init__( identifier is not None and identifier.startswith("offline_") ) - _config_args = { - "server_url": kwargs.pop("server_url", None), - "server_token": kwargs.pop("server_token", None), - "mode": "offline" if self._offline else "online", - } - - self._user_config = SimvueConfiguration.fetch(**_config_args) + self._user_config = SimvueConfiguration.fetch( + mode="offline" if self._offline else "online", + server_token=server_token, + server_url=server_url, + ) # Use a single file for each object so we can have parallelism # e.g. multiple runs writing at the same time self._local_staging_file: pathlib.Path = ( self._user_config.offline.cache.joinpath( - self._endpoint, f"{self._identifier}.json" + self.endpoint(), f"{self._identifier}.json" ) ) @@ -234,7 +231,7 @@ def __init__( self._user_config.headers if not self._offline else {} ) - self._params: dict[str, str] = {} + self._params: dict[str, str | bool] = {} self._staging: dict[str, typing.Any] = {} @@ -329,22 +326,22 @@ def _get_attribute( return _attribute raise AttributeError( f"Could not retrieve attribute '{attribute}' " - f"for {self._label} '{self._identifier}' from cached data" + f"for {self.label()} '{self._identifier}' from cached data" ) from e try: self._logger.debug( - f"Retrieving attribute '{attribute}' from {self._label} '{self._identifier}'" + f"Retrieving attribute '{attribute}' from {self.label()} '{self._identifier}'" ) return self._get(url=url)[attribute] except KeyError as e: if self._offline: raise AttributeError( f"A value for attribute '{attribute}' has " - f"not yet been committed for offline {self._label} '{self._identifier}'" + f"not yet been committed for offline {self.label()} '{self._identifier}'" ) from e raise RuntimeError( - f"Expected key '{attribute}' for {self._label} '{self._identifier}'" + f"Expected key '{attribute}' for {self.label()} '{self._identifier}'" ) from e def _clear_staging(self) -> None: @@ -356,8 +353,8 @@ def _clear_staging(self) -> None: with self._local_staging_file.open() as in_f: _staged_data = json.load(in_f) - if _staged_data.get(self._label): - _staged_data[self._label].pop(self._identifier, None) + if _staged_data.get(self.label()): + _staged_data[self.label()].pop(self._identifier, None) with self._local_staging_file.open("w") as out_f: json.dump(_staged_data, out_f, indent=2) @@ -382,7 +379,13 @@ def batch_create( @classmethod def ids( - cls, count: int | None = None, offset: int | None = None, **kwargs + cls, + *, + count: int | None = None, + offset: int | None = None, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, + **kwargs, ) -> Generator[str, None, None]: """Retrieve a list of all object identifiers. @@ -392,18 +395,27 @@ def ids( limit number of objects offset : int | None, optional set start index for objects list + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None Yields ------- str identifiers for all objects of this type. """ - _class_instance = cls(_read_only=True, _local=True) _count: int = 0 - for response in cls._get_all_objects(offset, count=count, **kwargs): + for response in cls._get_all_objects( + offset, + count=count, + server_url=server_url, + server_token=server_token, + **kwargs, + ): if (_data := response.get("data")) is None: raise RuntimeError( - f"Expected key 'data' for retrieval of {_class_instance.__class__.__name__.lower()}s" + f"Expected key 'data' for retrieval of {cls.__name__.lower()}s" ) for entry in _data: yield entry["id"] @@ -415,10 +427,13 @@ def ids( @pydantic.validate_call def get( cls, + *_, count: pydantic.PositiveInt | None = None, offset: pydantic.NonNegativeInt | None = None, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, **kwargs, - ) -> Generator[tuple[str, T | None], None, None]: + ) -> Generator[tuple[str, T | None]]: """Retrieve items of this object type from the server. Parameters @@ -427,6 +442,10 @@ def get( limit number of objects offset : int | None, optional set start index for objects list + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None Yields ------ @@ -435,16 +454,22 @@ def get( Returns ------- - Generator[tuple[str, SimvueObject | None], None, None] + Generator[tuple[str, SimvueObject | None]] """ - _class_instance = cls(_read_only=True, _local=True) _count: int = 0 - for _response in cls._get_all_objects(offset, count=count, **kwargs): + + for _response in cls._get_all_objects( + offset, + count=count, + server_url=server_url, + server_token=server_token, + **kwargs, + ): if count and _count > count: return if (_data := _response.get("data")) is None: raise RuntimeError( - f"Expected key 'data' for retrieval of {_class_instance.__class__.__name__.lower()}s" + f"Expected key 'data' for retrieval of {cls.__name__.lower()}s" ) # If data is an empty list @@ -453,24 +478,52 @@ def get( for entry in _data: _id = entry["id"] - yield _id, cls(_read_only=True, identifier=_id, _local=True, **entry) + yield ( + _id, + cls( + _read_only=True, + identifier=_id, + server_url=server_url, + server_token=server_token, + _local=True, + **entry, + ), + ) _count += 1 @classmethod - def count(cls, **kwargs) -> int: + def count( + cls, + *, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, + **kwargs, + ) -> int: """Return the total number of entries for this object type from the server. + Parameters + ---------- + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None + Returns ------- int total from server database for current user. """ - _class_instance = cls(_read_only=True) _count_total: int = 0 - for _data in cls._get_all_objects(count=None, offset=None, **kwargs): + for _data in cls._get_all_objects( + count=None, + offset=None, + server_token=server_token, + server_url=server_url, + **kwargs, + ): if not (_count := _data.get("count")): raise RuntimeError( - f"Expected key 'count' for retrieval of {_class_instance.__class__.__name__.lower()}s" + f"Expected key 'count' for retrieval of {cls.__name__.lower()}s" ) _count_total += _count return _count_total @@ -480,26 +533,26 @@ def _get_all_objects( cls, offset: int | None, count: int | None, + server_url: str | None, + server_token: pydantic.SecretStr | None, endpoint: str | None = None, expected_type: type = dict, **kwargs, ) -> Generator[dict, None, None]: - _class_instance = cls(_read_only=True) + _config: SimvueConfiguration = SimvueConfiguration.fetch( + mode="online", server_url=server_url, server_token=server_token + ) # Allow the possibility of paginating a URL that is not the # main class endpoint - _url = ( - f"{_class_instance._user_config.server.url}/{endpoint}" - if endpoint - else f"{_class_instance._base_url}" - ) + _url = f"{_config.server.url}/{endpoint or cls.endpoint()}" - _label = _class_instance.__class__.__name__.lower() + _label = cls.label() if _label.endswith("s"): _label = _label[:-1] for response in get_paginated( - _url, headers=_class_instance._headers, offset=offset, count=count, **kwargs + _url, headers=_config.headers, offset=offset, count=count, **kwargs ): _generator = get_json_from_response( response=response, @@ -539,7 +592,7 @@ def commit(self) -> dict | list[dict] | None: if self._offline: self._logger.debug( - f"Writing updates to staging file for {self._label} '{self.id}': {self._staging}" + f"Writing updates to staging file for {self.label()} '{self.id}': {self._staging}" ) self._cache() return @@ -552,17 +605,17 @@ def commit(self) -> dict | list[dict] | None: # If batch upload send as list, else send as dictionary of params if _batch_commit := self._staging.get("batch"): self._logger.debug( - f"Posting batched data to server: {len(_batch_commit)} {self._label}s" + f"Posting batched data to server: {len(_batch_commit)} {self.label()}s" ) _response = self._post_batch(batch_data=_batch_commit) else: self._logger.debug( - f"Posting from staged data for {self._label} '{self.id}': {self._staging}" + f"Posting from staged data for {self.label()} '{self.id}': {self._staging}" ) _response = self._post_single(**self._staging) elif self._staging: self._logger.debug( - f"Pushing updates from staged data for {self._label} '{self.id}': {self._staging}" + f"Pushing updates from staged data for {self.label()} '{self.id}': {self._staging}" ) _response = self._put(**self._staging) @@ -583,7 +636,7 @@ def id(self) -> str | None: @property def _base_url(self) -> URL: - return URL(self._user_config.server.url) / self._endpoint + return URL(self._user_config.server.url) / self.endpoint() @property def url(self) -> URL | None: @@ -609,13 +662,13 @@ def _post_batch( if _response.status_code == http.HTTPStatus.FORBIDDEN: raise RuntimeError( - f"Forbidden: You do not have permission to create object of type '{self._label}'" + f"Forbidden: You do not have permission to create object of type '{self.label()}'" ) _json_response = get_json_from_response( response=_response, expected_status=[http.HTTPStatus.OK, http.HTTPStatus.CONFLICT], - scenario=f"Creation of multiple {self._label}s", + scenario=f"Creation of multiple {self.label()}s", expected_type=list, ) @@ -624,7 +677,7 @@ def _post_batch( f"Expected {len(batch_data)} to be created, but only {_n_created} found." ) - self._logger.debug(f"successfully created {_n_created} {self._label}s") + self._logger.debug(f"successfully created {_n_created} {self.label()}s") return _json_response @@ -648,13 +701,13 @@ def _post_single( if _response.status_code == http.HTTPStatus.FORBIDDEN: raise RuntimeError( - f"Forbidden: You do not have permission to create object of type '{self._label}'" + f"Forbidden: You do not have permission to create object of type '{self.label()}'" ) _json_response = get_json_from_response( response=_response, expected_status=[http.HTTPStatus.OK, http.HTTPStatus.CONFLICT], - scenario=f"Creation of {self._label}", + scenario=f"Creation of {self.label()}", ) if _id := _json_response.get("id"): @@ -667,14 +720,14 @@ def _post_single( _detail = "No information in JSON response." raise RuntimeError( - f"Expected new ID for {self._label} but none found: {_detail}." + f"Expected new ID for {self.label()} but none found: {_detail}." ) return _json_response def _put(self, **kwargs) -> dict[str, typing.Any]: if not self.url: - raise RuntimeError(f"Identifier for instance of {self._label} Unknown") + raise RuntimeError(f"Identifier for instance of {self.label()} Unknown") # Remove any extra keys for key in self._local_only_args: @@ -686,13 +739,13 @@ def _put(self, **kwargs) -> dict[str, typing.Any]: if _response.status_code == http.HTTPStatus.FORBIDDEN: raise RuntimeError( - f"Forbidden: You do not have permission to create object of type '{self._label}'" + f"Forbidden: You do not have permission to create object of type '{self.label()}'" ) return get_json_from_response( response=_response, expected_status=[http.HTTPStatus.OK, http.HTTPStatus.CONFLICT], - scenario=f"Creation of {self._label} '{self._identifier}", + scenario=f"Creation of {self.label()} '{self._identifier}", ) def delete(self, **kwargs) -> dict[str, typing.Any]: @@ -711,15 +764,15 @@ def delete(self, **kwargs) -> dict[str, typing.Any]: return {"id": self._identifier} if not self._identifier: - raise RuntimeError(f"Object of type '{self._label}' has no identifier.") + raise RuntimeError(f"Object of type '{self.label()}' has no identifier.") if not self.url: - raise RuntimeError(f"Identifier for instance of {self._label} Unknown") + raise RuntimeError(f"Identifier for instance of {self.label()} Unknown") _response = sv_delete(url=f"{self.url}", headers=self._headers, params=kwargs) _json_response = get_json_from_response( response=_response, expected_status=[http.HTTPStatus.OK, http.HTTPStatus.NO_CONTENT], - scenario=f"Deletion of {self._label} '{self._identifier}'", + scenario=f"Deletion of {self.label()} '{self._identifier}'", ) self._logger.debug("'%s' deleted successfully", self._identifier) @@ -732,7 +785,7 @@ def _get( return self._get_local_staged() if not self.url: - raise RuntimeError(f"Identifier for instance of {self._label} Unknown") + raise RuntimeError(f"Identifier for instance of {self.label()} Unknown") _response = sv_get( url=f"{url or self.url}", headers=self._headers, params=kwargs @@ -740,20 +793,20 @@ def _get( if _response.status_code == http.HTTPStatus.NOT_FOUND: raise ObjectNotFoundError( - obj_type=self._label, name=self._identifier or "Unknown" + obj_type=self.label(), name=self._identifier or "Unknown" ) _json_response = get_json_from_response( response=_response, expected_status=[http.HTTPStatus.OK], allow_parse_failure=allow_parse_failure, - scenario=f"Retrieval of {self._label} '{self._identifier}'", + scenario=f"Retrieval of {self.label()} '{self._identifier}'", ) self._logger.debug("'%s' retrieved successfully", self._identifier) if not isinstance(_json_response, dict): raise RuntimeError( - f"Expected dictionary from JSON response during {self._label} retrieval " + f"Expected dictionary from JSON response during {self.label()} retrieval " f"but got '{type(_json_response)}'" ) return _json_response @@ -793,7 +846,7 @@ def on_reconnect(self, id_mapping: dict[str, str]) -> None: In this case no action is taken. """ - pass + _ = id_mapping @property def staged(self) -> dict[str, typing.Any] | None: @@ -806,6 +859,16 @@ def staged(self) -> dict[str, typing.Any] | None: """ return self._staging or None + @classmethod + def label(cls) -> str: + """Return API label for this object type.""" + return getattr(cls, "_label", cls.__name__.lower()) + + @classmethod + def endpoint(cls) -> str: + """Return the API endpoint for this object type.""" + return getattr(cls, "_endpoint", f"{cls.label()}s") + def __str__(self) -> str: """String representation of Simvue object.""" return f"{self.__class__.__name__}({self.id=})" diff --git a/simvue/api/objects/events.py b/simvue/api/objects/events.py index ade8c80a..066c635e 100644 --- a/simvue/api/objects/events.py +++ b/simvue/api/objects/events.py @@ -29,24 +29,42 @@ class Events(SimvueObject): - """ - Simvue Events - ============= + """Simvue Events. This class is used to connect to/create events objects on the Simvue server, any modification of instance attributes is mirrored on the remote object. """ + _label: str = "event" + def __init__( self, - _read_only: bool = True, - _local: bool = False, + identifier: str | None = None, + *, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, **kwargs, ) -> None: - """Initialise an Events object instance for creation/retrieval.""" - self._label = "event" - super().__init__(_read_only=_read_only, _local=_local, **kwargs) + """Initialise an Events object instance for creation/retrieval. + + Parameters + ---------- + identifier : str | None, optional + unique identifier for event + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None + **kwargs : dict + any additional arguments to be passed to the object initialiser + """ + super().__init__( + identifier=identifier, + server_url=server_url, + server_token=server_token, + **kwargs, + ) self._run_id = self._staging.get("run") self._is_set = True @@ -58,12 +76,45 @@ def get( *, count: pydantic.PositiveInt | None = None, offset: pydantic.PositiveInt | None = None, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, **kwargs, ) -> Generator[EventSet]: + """Retrieve events from the server. + + Parameters + ---------- + run_id: str + unique identifier of target Simvue run + count: int | None, optional + limit number of objects + offset : int | None, optional + set start index for objects list + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None + + Yields + ------ + EventSet + object corresponding to a set of events + + Returns + ------- + Generator[EventSet] + """ _class_instance = cls(_read_only=True, _local=True) _count: int = 0 - for response in cls._get_all_objects(offset, count=count, run=run_id, **kwargs): + for response in cls._get_all_objects( + offset, + count=count, + run=run_id, + server_url=server_url, + server_token=server_token, + **kwargs, + ): if (_data := response.get("data")) is None: raise RuntimeError( f"Expected key 'data' for retrieval of {_class_instance.__class__.__name__.lower()}s" @@ -78,12 +129,40 @@ def get( @classmethod @pydantic.validate_call def new( - cls, *, run: str, offline: bool = False, events: list[EventSet], **kwargs + cls, + *, + run: str, + events: list[EventSet], + offline: bool = False, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, + **kwargs, ) -> Self: - """Create a new Events entry on the Simvue server""" - return Events( + """Create a new Events entry on the Simvue server. + + Parameters + ---------- + run : str + unique identifier of target run for these events + events : list[EventSet] + set of events to attach to the target run + offline : bool, optional + whether to create in offline mode, default False + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None + + Returns + ------- + Events + an object representing this event set + """ + return cls( run=run, events=[event.model_dump() for event in events], + server_url=server_url, + server_token=server_token, _read_only=False, _offline=offline, **kwargs, diff --git a/simvue/api/objects/folder.py b/simvue/api/objects/folder.py index 19a8cea6..03943336 100644 --- a/simvue/api/objects/folder.py +++ b/simvue/api/objects/folder.py @@ -49,16 +49,20 @@ def check_column(cls, column: str) -> str: class Folder(SimvueObject): - """ - Simvue Folder - ============= + """Simvue Folder. This class is used to connect to/create folder objects on the Simvue server, any modification of instance attributes is mirrored on the remote object. """ - def __init__(self, identifier: str | None = None, **kwargs) -> None: + def __init__( + self, + identifier: str | None = None, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, + **kwargs, + ) -> None: """Initialise a Folder If an identifier is provided a connection will be made to the @@ -67,12 +71,18 @@ def __init__(self, identifier: str | None = None, **kwargs) -> None: Parameters ---------- - identifier : str, optional + identifier : str | None, optional the remote server unique id for the target folder + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None **kwargs : dict any additional arguments to be passed to the object initialiser """ - super().__init__(identifier, **kwargs) + super().__init__( + identifier, server_token=server_token, server_url=server_url, **kwargs + ) self._properties.remove("tree") @classmethod @@ -82,18 +92,30 @@ def new( *, path: typing.Annotated[str, pydantic.Field(pattern=FOLDER_REGEX)], offline: bool = False, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, **kwargs, ) -> Self: """Create a new Folder on the Simvue server with the given path""" - return Folder(path=path, _read_only=False, _offline=offline, **kwargs) + return cls( + path=path, + _read_only=False, + _offline=offline, + server_url=server_url, + server_token=server_token, + **kwargs, + ) @classmethod @pydantic.validate_call def get( cls, + *, count: pydantic.PositiveInt | None = None, offset: pydantic.NonNegativeInt | None = None, sorting: list[FolderSort] | None = None, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, **kwargs, ) -> Generator[tuple[str, T | None]]: """Get folders from the server. @@ -106,6 +128,10 @@ def get( start index for results, default is 0. sorting : list[dict] | None, optional list of sorting definitions in the form {'column': str, 'descending': bool} + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None Yields ------ diff --git a/simvue/api/objects/grids.py b/simvue/api/objects/grids.py index 83d648ea..de0f156f 100644 --- a/simvue/api/objects/grids.py +++ b/simvue/api/objects/grids.py @@ -1,6 +1,4 @@ -""" -Simvue Server Grid -================== +"""Simvue Server Grid. Contains a class for remotely connecting to a Simvue grid, or defining a new grid given relevant arguments. @@ -28,9 +26,9 @@ ) try: - from typing import Self + from typing import Self, override except ImportError: - from typing_extensions import Self + from typing_extensions import Self, override __all__ = ["Grid"] @@ -51,16 +49,21 @@ def check_ordered_array( class Grid(SimvueObject): - """ - Simvue Grid - =========== + """Simvue Grid. This class is used to connect to/create grid objects on the Simvue server, any modification of instance attributes is mirrored on the remote object. """ - def __init__(self, identifier: str | None = None, **kwargs) -> None: + def __init__( + self, + identifier: str | None = None, + *, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, + **kwargs, + ) -> None: """Initialise a Grid If an identifier is provided a connection will be made to the @@ -71,10 +74,19 @@ def __init__(self, identifier: str | None = None, **kwargs) -> None: ---------- identifier : str, optional the remote server unique id for the target folder + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None **kwargs : dict any additional arguments to be passed to the object initialiser """ - super().__init__(identifier, **kwargs) + super().__init__( + identifier, + server_url=server_url, + server_token=server_token, + **kwargs, + ) @pydantic.validate_call @write_only @@ -143,6 +155,8 @@ def new( ], labels: list[str], offline: bool = False, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, **kwargs, ) -> Self: """Create a new Grid on the Simvue server. @@ -158,6 +172,10 @@ def new( label each of the axes defined. offline: bool, optional whether to create in offline mode, default is False. + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None Returns ------- @@ -171,12 +189,14 @@ def new( f"grid dimension {len(grid)}." ) - return Grid( + return cls( grid=grid, labels=labels, name=name, _read_only=False, _offline=offline, + server_url=server_url, + server_token=server_token, **kwargs, ) @@ -279,24 +299,34 @@ def get( class GridMetrics(SimvueObject): - """ - Simvue Grid Metrics - =================== + """Simvue Grid Metrics. This class is used to connect to/create grid metrics on the Simvue server, any modification of instance attributes is mirrored on the remote object. """ + _label: str = "grid_metric" + + @override def __init__( self, - _read_only: bool = True, - _local: bool = False, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, **kwargs, ) -> None: - """Initialise a GridMetrics object instance.""" - self._label = "grid_metric" - super().__init__(_read_only=_read_only, _local=_local, **kwargs) + """Initialise a GridMetrics object instance. + + Parameters + ---------- + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None + """ + super().__init__( + identifier=None, server_url=server_url, server_token=server_token, **kwargs + ) self._run_id = self._staging.get("run") self._is_set = True @@ -312,10 +342,18 @@ def _get_attribute(self, attribute: str, *default) -> typing.Any: url=f"{self._user_config.server.url}/{self.run_grids_endpoint(self._run_id)}", ) + @override @classmethod @pydantic.validate_call def new( - cls, *, run: str, data: list[GridMetricSet], offline: bool = False, **kwargs + cls, + *, + run: str, + data: list[GridMetricSet], + offline: bool = False, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, + **kwargs, ) -> Self: """Create a new GridMetrics object for n-dimensional metric submission. @@ -327,19 +365,26 @@ def new( set of tensor-based metrics to attach to run. offline: bool, optional whether to create in offline mode, default is False. + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None Returns ------- Metrics metrics object """ - return GridMetrics( + return cls( run=run, - data=[metric.model_dump() for metric in data], _read_only=False, + data=[metric.model_dump() for metric in data], + server_url=server_url, + server_token=server_token, _offline=offline, ) + @override @classmethod @pydantic.validate_call def get( @@ -348,7 +393,11 @@ def get( runs: list[str], metrics: list[str], step: pydantic.NonNegativeInt, + count: pydantic.PositiveInt | None = None, + offset: pydantic.NonNegativeInt | None = None, spans: bool = False, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, **kwargs, ) -> Generator[dict[str, dict[str, list[dict[str, float]]]]]: """Retrieve tensor-metrics from the server for a given set of runs. @@ -359,10 +408,18 @@ def get( list of runs to return metric values for. metrics : list[str] list of metrics to retrieve. + count : int, optional + limit the number of objects returned, default no limit. + offset : int, optional + start index for results, default is 0. step : int the timestep to retrieve grid metrics for spans : bool, optional return spans informations + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None Yields ------ @@ -374,8 +431,10 @@ def get( yield from cls._get_all_objects( endpoint=f"{cls.run_grids_endpoint(run)}/{metric}/values", step=step, - offset=None, - count=None, + offset=offset, + count=count, + server_url=server_url, + server_token=server_token, ) def commit(self) -> dict | None: diff --git a/simvue/api/objects/metrics.py b/simvue/api/objects/metrics.py index 3b737bc3..12b20f2c 100644 --- a/simvue/api/objects/metrics.py +++ b/simvue/api/objects/metrics.py @@ -1,6 +1,4 @@ -""" -Simvue Server Metrics -===================== +"""Simvue Server Metrics. Contains a class for remotely connecting to Simvue metrics, or defining a new set of metrics given relevant arguments. @@ -20,38 +18,60 @@ from simvue.api.request import get as sv_get, get_json_from_response try: - from typing import Self + from typing import Self, override except ImportError: - from typing_extensions import Self + from typing_extensions import Self, override __all__ = ["Metrics"] class Metrics(SimvueObject): - """ - Simvue Metrics - ============== + """Simvue Metrics. Class for retrieving metrics stored on the server. - """ + _label: str = "metric" + def __init__( self, - _read_only: bool = True, - _local: bool = False, + identifier: str | None = None, + *, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, **kwargs, ) -> None: - """Initialise a Metrics object instance for creation/retrieval.""" - self._label = "metric" - super().__init__(_read_only=_read_only, _local=_local, **kwargs) + """Initialise an Metrics object instance for creation/retrieval. + + Parameters + ---------- + identifier : str | None, optional + unique identifier for a metric set + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None + **kwargs : dict + any additional arguments to be passed to the object initialiser + """ + super().__init__( + identifier=None, server_url=server_url, server_token=server_token, **kwargs + ) self._run_id = self._staging.get("run") self._is_set = True + @override @classmethod @pydantic.validate_call def new( - cls, *, run: str, metrics: list[MetricSet], offline: bool = False, **kwargs + cls, + *, + run: str, + metrics: list[MetricSet], + offline: bool = False, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, + **kwargs, ) -> Self: """Create a new Metrics entry on the Simvue server. @@ -63,19 +83,26 @@ def new( set of metrics to attach to run. offline: bool, optional whether to create in offline mode, default is False. + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None Returns ------- Metrics metrics object """ - return Metrics( + return cls( run=run, metrics=[metric.model_dump() for metric in metrics], + server_url=server_url, + server_token=server_token, _read_only=False, _offline=offline, ) + @override @classmethod @pydantic.validate_call def get( @@ -86,6 +113,8 @@ def get( *, count: pydantic.PositiveInt | None = None, offset: pydantic.PositiveInt | None = None, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, **kwargs, ) -> Generator[dict[str, dict[str, list[dict[str, float]]]]]: """Retrieve metrics from the server for a given set of runs. @@ -105,6 +134,10 @@ def get( limit result count. offset : int | None, optional index offset for count. + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None Yields ------ @@ -115,6 +148,8 @@ def get( offset=offset, metrics=json.dumps(metrics), runs=json.dumps(runs), + server_url=server_url, + server_token=server_token.get_secret_value() if server_token else None, xaxis=xaxis, count=count, **kwargs, diff --git a/simvue/api/objects/run.py b/simvue/api/objects/run.py index 185f3d16..79edd31a 100644 --- a/simvue/api/objects/run.py +++ b/simvue/api/objects/run.py @@ -77,16 +77,21 @@ class RunBatchArgs(ObjectBatchArgs): class Run(SimvueObject): - """ - Simvue Run - ========== + """Simvue Run. This class is used to connect to/create run objects on the Simvue server, any modification of instance attributes is mirrored on the remote object. """ - def __init__(self, identifier: str | None = None, **kwargs) -> None: + def __init__( + self, + identifier: str | None = None, + *, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, + **kwargs, + ) -> None: """Initialise a Run. If an identifier is provided a connection will be made to the @@ -95,13 +100,19 @@ def __init__(self, identifier: str | None = None, **kwargs) -> None: Parameters ---------- - identifier : str, optional + identifier : str | None, optional the remote server unique id for the target run + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None **kwargs : dict any additional arguments to be passed to the object initialiser """ self.visibility = Visibility(self) - super().__init__(identifier, **kwargs) + super().__init__( + identifier, server_url=server_url, server_token=server_token, **kwargs + ) @classmethod def filter(cls) -> RunsFilter: @@ -127,6 +138,8 @@ def new( "terminated", "created", "failed", "completed", "lost", "running" ] = "created", offline: bool = False, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, **kwargs, ) -> Self: """Create a new Run on the Simvue server. @@ -135,8 +148,16 @@ def new( ---------- folder : str folder to contain this run + system : dict[str, Any] | None, optional + dict defining system information to attach, default None. + status: 'terminated' | 'created' | 'failed' | 'completed' | 'lost' | 'running' + set status of new run, default 'created'. offline : bool, optional create the run in offline mode, default False. + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None Returns ------- @@ -156,12 +177,14 @@ def new( run.commit() ``` """ - return Run( + return cls( folder=folder, system=system, status=status, - _read_only=False, + server_url=server_url, + server_token=server_token, _offline=offline, + _read_only=False, **kwargs, ) @@ -175,6 +198,9 @@ def batch_create( folder: typing.Annotated[str, pydantic.StringConstraints(pattern=FOLDER_REGEX)] | None = None, metadata: dict[str, str | int | float | bool] | None = None, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, + **kwargs, ) -> Generator[str]: """Create a batch of Runs as a single request. @@ -188,6 +214,10 @@ def batch_create( override folder specification for these runs to be a single folder, default None. metadata : dict[str, int | str | float | bool], optional override metadata specification for these runs, default None. + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None Yields ------ @@ -205,7 +235,16 @@ def batch_create( | {"metadata": (entry.metadata or {}) | (metadata or {})} for entry in entries ] - for entry in Run(batch=_data, _read_only=False).commit() or []: + for entry in ( + Run( + batch=_data, + _read_only=False, + server_url=server_url, + server_token=server_token, + **kwargs, + ).commit() + or [] + ): _id: str = entry["id"] yield _id @@ -403,13 +442,17 @@ def alerts(self) -> list[str]: return [alert["id"] for alert in self.get_alert_details()] + @override @classmethod @pydantic.validate_call def get( cls, + *, count: pydantic.PositiveInt | None = None, offset: pydantic.NonNegativeInt | None = None, sorting: list[RunSort] | None = None, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, **kwargs, ) -> Generator[tuple[str, T | None]]: """Get runs from the server. @@ -422,6 +465,10 @@ def get( start index for results, default is 0. sorting : list[dict] | None, optional list of sorting definitions in the form {'column': str, 'descending': bool} + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None Yields ------ @@ -434,7 +481,13 @@ def get( if sorting: _params["sorting"] = json.dumps([i.to_params() for i in sorting]) - return super().get(count=count, offset=offset, **_params) + return super().get( + count=count, + offset=offset, + server_url=server_url, + server_token=server_token, + **_params, + ) @alerts.setter @write_only diff --git a/simvue/api/objects/stats.py b/simvue/api/objects/stats.py index 7c8c68b9..a121d4db 100644 --- a/simvue/api/objects/stats.py +++ b/simvue/api/objects/stats.py @@ -1,6 +1,4 @@ -""" -Simvue Stats -============ +"""Simvue Stats. Statistics accessible to the current user. @@ -32,9 +30,7 @@ class UserStatistics(BaseModel): class Stats(SimvueObject): - """ - Simvue Stats - ============ + """Simvue Stats. Class for retrieving statistics stored on the server. @@ -42,12 +38,27 @@ class Stats(SimvueObject): _single: bool = True _tenant: str | None = None - - def __init__(self) -> None: - """Initialise a statistics query object.""" + _label: str = "stat" + + def __init__( + self, + *, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, + ) -> None: + """Initialise a statistics query object. + + Parameters + ---------- + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None + """ self.runs = RunStatistics(self) - self._label = "stat" - super().__init__() + super().__init__( + identifier=None, server_url=server_url, server_token=server_token + ) # Stats is a singular object (i.e. identifier is not applicable) # set it to empty string so not None diff --git a/simvue/api/objects/storage/__init__.py b/simvue/api/objects/storage/__init__.py index cfb43777..01034513 100644 --- a/simvue/api/objects/storage/__init__.py +++ b/simvue/api/objects/storage/__init__.py @@ -1,6 +1,4 @@ -""" -Simvue Storage -============== +"""Simvue Storage. Contains classes for interacting with Simvue storage objects, the storage types are split into classes to ensure correct @@ -8,6 +6,8 @@ """ -from .file import FileStorage as FileStorage -from .s3 import S3Storage as S3Storage -from .fetch import Storage as Storage +from .file import FileStorage +from .s3 import S3Storage +from .fetch import Storage + +__all__ = ["FileStorage", "S3Storage", "Storage"] diff --git a/simvue/api/objects/storage/base.py b/simvue/api/objects/storage/base.py index 7d7e288a..750451a5 100644 --- a/simvue/api/objects/storage/base.py +++ b/simvue/api/objects/storage/base.py @@ -13,6 +13,13 @@ from simvue.api.objects.base import SimvueObject, staging_check, write_only from simvue.models import NAME_REGEX, DATETIME_FORMAT +try: + from typing import Self, override +except ImportError: + from typing_extensions import Self, override + +__all__ = ["StorageBase"] + class StorageBase(SimvueObject): """Storage object base class from which all storage types inherit. @@ -21,19 +28,27 @@ class StorageBase(SimvueObject): """ + _label: str = "storage" + _endpoint: str = "storage" + + @override def __init__( self, - identifier: str | None = None, - _read_only: bool = False, + identifier: str | None, + *, + server_url: str | None, + server_token: pydantic.SecretStr | None, **kwargs, ) -> None: - """Retrieve a storage instance from the Simvue server by identifier""" - self._label = "storage" - self._endpoint = self._label - super().__init__(identifier, _read_only=_read_only, **kwargs) + """Retrieve a storage instance from the Simvue server by identifier.""" + super().__init__( + identifier, server_url=server_url, server_token=server_token, **kwargs + ) @classmethod - def new(cls, **_): + def new( + cls, *, server_url: str | None, server_token: pydantic.SecretStr | None, **_ + ) -> Self: """Create a new instance of a storage type""" pass diff --git a/simvue/api/objects/storage/fetch.py b/simvue/api/objects/storage/fetch.py index e6bc1c78..1468e9f3 100644 --- a/simvue/api/objects/storage/fetch.py +++ b/simvue/api/objects/storage/fetch.py @@ -1,6 +1,4 @@ -""" -Simvue Storage Retrieval -==============--======== +"""Simvue Storage Retrieval. To simplify case whereby user does not know the storage type associated with an identifier, use a generic storage object. @@ -19,26 +17,36 @@ class Storage: - """ - Simvue Storage - ============== + """Simvue Storage. Generic Simvue storage retrieval class. """ - def __init__(self, identifier: str | None = None, *args, **kwargs) -> None: - """Initialise an instance of generic storage retriever. + def __new__( + cls, + identifier: str | None = None, + *, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, + **kwargs, + ) -> S3Storage | FileStorage: + """Retrieve an object representing on the server by id. Parameters ---------- identifier : str identifier of storage object to retrieve + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None + + Returns + ------- + S3Storage | FileStorage + object representing storage """ - super().__init__(identifier=identifier, *args, **kwargs) - - def __new__(cls, identifier: str | None = None, **kwargs): - """Retrieve an object representing an storage either locally or on the server by id""" _storage_pre = StorageBase(identifier=identifier, **kwargs) if _storage_pre.backend == "S3": return S3Storage(identifier=identifier, **kwargs) @@ -50,7 +58,13 @@ def __new__(cls, identifier: str | None = None, **kwargs): @classmethod @pydantic.validate_call def get( - cls, count: int | None = None, offset: int | None = None, **kwargs + cls, + *, + count: int | None = None, + offset: int | None = None, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, + **kwargs, ) -> Generator[tuple[str, FileStorage | S3Storage]]: """Returns storage systems accessible to the current user. @@ -60,6 +74,10 @@ def get( limit the number of results, default of None returns all. offset : int, optional start index for returned results, default of None starts at 0. + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None Yields ------ @@ -69,9 +87,14 @@ def get( """ # Currently no storage filters - kwargs.pop("filters", None) + _ = kwargs.pop("filters", None) - _class_instance = StorageBase(_local=True, _read_only=True) + _class_instance = StorageBase( + identifier=None, + server_url=server_url, + server_token=server_token, + _local=True, + ) _url = f"{_class_instance._base_url}" _response = sv_get( _url, @@ -94,12 +117,24 @@ def get( if _entry["backend"] == "S3": yield ( _id, - S3Storage(_local=True, _read_only=True, identifier=_id, **_entry), + S3Storage( + _local=True, + identifier=_id, + server_url=server_url, + server_token=server_token, + **_entry, + ), ) elif _entry["backend"] == "File": yield ( _id, - FileStorage(_local=True, _read_only=True, identifier=_id, **_entry), + FileStorage( + _local=True, + identifier=_id, + server_url=server_url, + server_token=server_token, + **_entry, + ), ) else: raise RuntimeError( diff --git a/simvue/api/objects/storage/file.py b/simvue/api/objects/storage/file.py index 1cde5dbc..1e8396ba 100644 --- a/simvue/api/objects/storage/file.py +++ b/simvue/api/objects/storage/file.py @@ -1,6 +1,4 @@ -""" -Simvue File Storage -=================== +"""Simvue File Storage. Class for interacting with a file based storage on the server. @@ -9,9 +7,9 @@ import typing try: - from typing import Self + from typing import Self, override except ImportError: - from typing_extensions import Self + from typing_extensions import Self, override import pydantic from .base import StorageBase @@ -19,19 +17,20 @@ class FileStorage(StorageBase): - """ - Simvue File Storage - =================== + """Simvue File Storage. This class is used to connect to/create file storage objects on the Simvue server, any modification of instance attributes is mirrored on the remote object. """ + @override def __init__( self, identifier: str | None = None, - _read_only: bool = False, + *, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, **kwargs, ) -> None: """Initialise a File Storage @@ -44,11 +43,18 @@ def __init__( ---------- identifier : str, optional the remote server unique id for the target folder + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None **kwargs : dict any additional arguments to be passed to the object initialiser """ - super().__init__(identifier, **kwargs) + super().__init__( + identifier, server_url=server_url, server_token=server_token, **kwargs + ) + @override @classmethod @pydantic.validate_call def new( @@ -60,6 +66,8 @@ def new( is_enabled: bool, is_default: bool, offline: bool = False, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, **_, ) -> Self: """Create a new file storage object. @@ -78,19 +86,25 @@ def new( if this storage system should become the new default offline : bool, optional if this instance should be created in offline mode, default False + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None Returns ------- FileStorage instance of storage system with staged changes """ - return FileStorage( + return cls( name=name, backend="File", disable_check=disable_check, is_tenant_useable=is_tenant_useable, is_default=is_default, is_enabled=is_enabled, + server_url=server_url, + server_token=server_token, _read_only=False, _offline=offline, ) diff --git a/simvue/api/objects/storage/s3.py b/simvue/api/objects/storage/s3.py index 6e03305f..6c80870c 100644 --- a/simvue/api/objects/storage/s3.py +++ b/simvue/api/objects/storage/s3.py @@ -1,6 +1,4 @@ -""" -Simvue S3 Storage -================= +"""Simvue S3 Storage. Class for interacting with an S3 based storage on the server. @@ -9,28 +7,34 @@ import typing try: - from typing import Self + from typing import Self, override except ImportError: - from typing_extensions import Self + from typing_extensions import Self, override import pydantic -from simvue.api.objects.base import write_only +from simvue.api.objects.base import write_only, staging_check -from .base import StorageBase, staging_check +from .base import StorageBase from simvue.models import NAME_REGEX class S3Storage(StorageBase): - """ - Simvue S3 Storage - =================== + """Simvue S3 Storage. This class is used to connect to/create S3 storage objects on the Simvue server, any modification of instance attributes is mirrored on the remote object. """ - def __init__(self, identifier: str | None = None, **kwargs) -> None: + @override + def __init__( + self, + identifier: str | None = None, + *, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, + **kwargs, + ) -> None: """Initialise a S3 Storage If an identifier is provided a connection will be made to the @@ -41,11 +45,17 @@ def __init__(self, identifier: str | None = None, **kwargs) -> None: ---------- identifier : str, optional the remote server unique id for the target folder + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None **kwargs : dict any additional arguments to be passed to the object initialiser """ self.config = Config(self) - super().__init__(identifier, **kwargs) + super().__init__( + identifier, server_url=server_url, server_token=server_token, **kwargs + ) self._local_only_args += [ "endpoint_url", "region_name", @@ -54,6 +64,7 @@ def __init__(self, identifier: str | None = None, **kwargs) -> None: "bucket", ] + @override @classmethod @pydantic.validate_call def new( @@ -70,6 +81,8 @@ def new( is_default: bool, is_enabled: bool, offline: bool = False, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, **__, ) -> Self: """Create a new S3 storage object. @@ -98,6 +111,10 @@ def new( if this storage system should become the new is_default offline : bool, optional if this instance should be created in offline mode, is_default False + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None Returns ------- @@ -112,7 +129,7 @@ def new( "secret_access_key": secret_access_key.get_secret_value(), "bucket": bucket, } - _storage = S3Storage( + _storage = cls( name=name, backend="S3", config=_config, @@ -120,8 +137,10 @@ def new( is_tenant_useable=is_tenant_useable, is_default=is_default, is_enabled=is_enabled, - _read_only=False, + server_url=server_url, + server_token=server_token, _offline=offline, + _read_only=False, ) _storage._staging |= _config return _storage diff --git a/simvue/api/objects/tag.py b/simvue/api/objects/tag.py index 08c0ee69..a7fc15ba 100644 --- a/simvue/api/objects/tag.py +++ b/simvue/api/objects/tag.py @@ -1,6 +1,4 @@ -""" -Simvue Server Tag -================= +"""Simvue Server Tag. Contains a class for remotely connecting to a Simvue Tag, or defining a new tag given relevant arguments. @@ -18,9 +16,9 @@ from collections.abc import Generator try: - from typing import Self + from typing import Self, override except ImportError: - from typing_extensions import Self + from typing_extensions import Self, override __all__ = ["Tag"] @@ -35,16 +33,21 @@ def check_column(cls, column: str) -> str: class Tag(SimvueObject): - """ - Simvue Tag - ========== + """Simvue Tag. This class is used to connect to/create tag objects on the Simvue server, any modification of instance attributes is mirrored on the remote object. """ - def __init__(self, identifier: str | None = None, **kwargs) -> None: + @override + def __init__( + self, + identifier: str | None = None, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, + **kwargs, + ) -> None: """Initialise a Tag If an identifier is provided a connection will be made to the @@ -55,14 +58,29 @@ def __init__(self, identifier: str | None = None, **kwargs) -> None: ---------- identifier : str, optional the remote server unique id for the target folder + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None **kwargs : dict any additional arguments to be passed to the object initialiser """ - super().__init__(identifier, **kwargs) + super().__init__( + identifier, server_url=server_url, server_token=server_token, **kwargs + ) + @override @classmethod @pydantic.validate_call - def new(cls, *, name: str, offline: bool = False, **kwargs) -> Self: + def new( + cls, + *, + name: str, + offline: bool = False, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, + **kwargs, + ) -> Self: """Create a new Tag on the Simvue server. Parameters @@ -71,6 +89,10 @@ def new(cls, *, name: str, offline: bool = False, **kwargs) -> Self: name for the tag offline : bool, optional create this tag in offline mode, default False. + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None Returns ------- @@ -78,7 +100,14 @@ def new(cls, *, name: str, offline: bool = False, **kwargs) -> Self: tag object with staged attributes """ _data: dict[str, typing.Any] = {"name": name} - return Tag(name=name, _read_only=False, _offline=offline, **kwargs) + return cls( + name=name, + server_url=server_url, + server_token=server_token, + _offline=offline, + _read_only=False, + **kwargs, + ) @property @staging_check @@ -131,6 +160,7 @@ def created(self) -> datetime.datetime | None: else None ) + @override @classmethod @pydantic.validate_call def get( @@ -139,6 +169,8 @@ def get( count: int | None = None, offset: int | None = None, sorting: list[TagSort] | None = None, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, **kwargs, ) -> Generator[tuple[str, "SimvueObject"]]: """Get tags from the server. @@ -151,6 +183,10 @@ def get( start index for results, default is 0. sorting : list[dict] | None, optional list of sorting definitions in the form {'column': str, 'descending': bool} + server_url: str | None, optional + alternative server URL, default None + server_token : str | None, optional + token for alternative server, default None Yields ------ @@ -159,7 +195,7 @@ def get( Tag object representing object on server """ # There are currently no tag filters - kwargs.pop("filters", None) + _ = kwargs.pop("filters", None) _params: dict[str, str] = {} @@ -169,6 +205,8 @@ def get( return super().get( count=count, offset=offset, + server_url=server_url, + server_token=server_token, **_params, **kwargs, ) diff --git a/simvue/client.py b/simvue/client.py index 95f3a29b..ef5ceef4 100644 --- a/simvue/client.py +++ b/simvue/client.py @@ -1,6 +1,4 @@ -""" -Simvue Client -============= +"""Simvue Client. Contains a Simvue client class for interacting with existing objects on the server including deletion and retrieval. @@ -157,7 +155,12 @@ def get_run(self, run_id: str) -> Run | None: RuntimeError if retrieval of information from the server on this run failed """ - return Run(identifier=run_id, read_only=True) + return Run( + identifier=run_id, + server_url=self._user_config.server.url, + server_token=self._user_config.server.token, + read_only=True, + ) @prettify_pydantic @pydantic.validate_call @@ -174,7 +177,11 @@ def get_run_name_from_id(self, run_id: str) -> str: str the registered name for the run """ - return Run(identifier=run_id).name + return Run( + identifier=run_id, + server_url=self._user_config.server.url, + server_token=self._user_config.server.token, + ).name @prettify_pydantic @pydantic.validate_call @@ -269,6 +276,8 @@ def get_runs( return_metrics=metrics, return_alerts=alerts, return_metadata=metadata, + server_url=self._user_config.server.url, + server_token=self._user_config.server.token, sorting=[dict(zip(("column", "descending"), a)) for a in sort_by_columns] if sort_by_columns else None, @@ -305,7 +314,14 @@ def delete_run(self, run_id: str) -> dict | None: RuntimeError if the deletion failed due to server request error """ - return Run(identifier=run_id).delete() or None + return ( + Run( + identifier=run_id, + server_url=self._user_config.server.url, + server_token=self._user_config.server.token, + ).delete() + or None + ) def _get_folder_from_path(self, path: str) -> Folder | None: """Retrieve folder for the specified path if found @@ -320,7 +336,11 @@ def _get_folder_from_path(self, path: str) -> Folder | None: Folder | None if a match is found, return the folder """ - _folders = Folder.get(filters=json.dumps([f"path == {path}"])) + _folders = Folder.get( + filters=json.dumps([f"path == {path}"]), + server_url=self._user_config.server.url, + server_token=self._user_config.server.token, + ) _, _folder = next(_folders, (None, None)) @@ -339,7 +359,11 @@ def _get_folder_id_from_path(self, path: str) -> str | None: str | None if a match is found, return the identifier of the folder """ - _ids = Folder.ids(filters=json.dumps([f"path == {path}"])) + _ids = Folder.ids( + filters=json.dumps([f"path == {path}"]), + server_url=self._user_config.server.url, + server_token=self._user_config.server.token, + ) if not (_id := next(_ids, None)): return None @@ -424,8 +448,14 @@ def delete_folder( name=folder_path, obj_type="folder", ) - _response = Folder(identifier=folder_id).delete( - delete_runs=remove_runs, recursive=recursive, runs_only=False + _response = Folder( + identifier=folder_id, + server_url=self._user_config.server.url, + server_token=self._user_config.server.token, + ).delete( + delete_runs=remove_runs, + recursive=recursive, + runs_only=False, ) if folder_id not in _response.get("folders", []): @@ -443,7 +473,11 @@ def delete_alert(self, alert_id: str) -> None: alert_id : str the unique identifier for the alert """ - Alert(identifier=alert_id).delete() # type: ignore + Alert( + identifier=alert_id, + server_url=self._user_config.server.url, + server_token=self._user_config.server.token, + ).delete() # type: ignore @prettify_pydantic @pydantic.validate_call @@ -473,6 +507,8 @@ def list_artifacts( """ return Artifact.get( runs=json.dumps([run_id]), + server_url=self._user_config.server.url, + server_token=self._user_config.server.token, sorting=[dict(zip(("column", "descending"), a)) for a in sort_by_columns] if sort_by_columns else None, @@ -505,7 +541,11 @@ def abort_run(self, run_id: str, reason: str) -> dict | list: dict | list response from server """ - return Run(identifier=run_id).abort(reason=reason) + return Run( + identifier=run_id, + server_url=self._user_config.server.url, + server_token=self._user_config.server.token, + ).abort(reason=reason) @prettify_pydantic @pydantic.validate_call @@ -621,6 +661,8 @@ def get_artifacts_as_files( if there was a failure retrieving artifacts from the server """ _artifacts: Generator[tuple[str, Artifact]] = Artifact.from_run( + server_url=self._user_config.server.url, + server_token=self._user_config.server.token, run_id=run_id, category=category, ) @@ -715,6 +757,8 @@ def get_folders( filters=json.dumps(filters or []), count=count, offset=start_index, + server_url=self._user_config.server.url, + server_token=self._user_config.server.token, sorting=[dict(zip(("column", "descending"), a)) for a in sort_by_columns] if sort_by_columns else None, @@ -1084,7 +1128,12 @@ def get_alerts( ) _alerts = [ - Alert(identifier=alert.get("id"), **alert) + Alert( + identifier=alert.get("id"), + server_url=self._user_config.server.url, + server_token=self._user_config.server.token, + **alert, + ) for alert in Run(identifier=run_id).get_alert_details() ] @@ -1130,6 +1179,8 @@ def get_tags( return Tag.get( count=count_limit, offset=start_index, + server_url=self._user_config.server.url, + server_token=self._user_config.server.token, sorting=[dict(zip(("column", "descending"), a)) for a in sort_by_columns] if sort_by_columns else None, @@ -1151,7 +1202,11 @@ def delete_tag(self, tag_id: str) -> None: if the deletion failed due to a server request error """ with contextlib.suppress(ValueError): - Tag(identifier=tag_id).delete() + Tag( + identifier=tag_id, + server_url=self._user_config.server.url, + server_token=self._user_config.server.token, + ).delete() @prettify_pydantic @pydantic.validate_call @@ -1175,4 +1230,8 @@ def get_tag(self, tag_id: str) -> Tag: ObjectNotFoundError if tag does not exist """ - return Tag(identifier=tag_id) + return Tag( + identifier=tag_id, + server_url=self._user_config.server.url, + server_token=self._user_config.server.token, + ) diff --git a/simvue/executor.py b/simvue/executor.py index 99ac9148..7b7f1a9a 100644 --- a/simvue/executor.py +++ b/simvue/executor.py @@ -1,6 +1,4 @@ -""" -Simvue Client Executor -====================== +"""Simvue Job Executor. Adds functionality for executing commands from the command line as part of a Simvue run, the executor monitors the exit code of the command setting the status to failure if non-zero. @@ -417,7 +415,11 @@ def _update_alerts(self) -> None: # We don't want to override the user's setting for the alert status # This is so that if a process incorrectly reports its return code, # the user can manually set the correct status depending on logs etc. - _alert = UserAlert(identifier=self._alert_ids[proc_id]) + _alert = UserAlert( + identifier=self._alert_ids[proc_id], + server_url=self._runner._user_config.server.url, + server_token=self._runner._user_config.server.token, + ) _is_set = _alert.get_status(run_id=self._runner.id) if process.returncode != 0: diff --git a/simvue/run.py b/simvue/run.py index 0e39df47..6f73531f 100644 --- a/simvue/run.py +++ b/simvue/run.py @@ -1,6 +1,4 @@ -""" -Simvue Run -========== +"""Simvue Run. Main class for recording metrics and information to Simvue during code execution. This forms the central API for users. @@ -503,6 +501,8 @@ def _dispatch_callback( _events = Events.new( run=self.id, offline=self._user_config.run.mode == "offline", + server_url=self._user_config.server.url, + server_token=self._user_config.server.token, events=buffer, ) return _events.commit() @@ -510,6 +510,8 @@ def _dispatch_callback( _grid_metrics = GridMetrics.new( run=self.id, data=buffer, + server_url=self._user_config.server.url, + server_token=self._user_config.server.token, offline=self._user_config.run.mode == "offline", ) return _grid_metrics.commit() @@ -517,6 +519,8 @@ def _dispatch_callback( _metrics = Metrics.new( run=self.id, offline=self._user_config.run.mode == "offline", + server_url=self._user_config.server.url, + server_token=self._user_config.server.token, metrics=buffer, ) return _metrics.commit() @@ -716,15 +720,13 @@ def init( self._term_color = not no_color self._folder = Folder.new( - path=folder, offline=self._user_config.run.mode == "offline" + path=folder, + offline=self._user_config.run.mode == "offline", + server_url=self._user_config.server.url, + server_token=self._user_config.server.token, ) self._folder.commit() # type: ignore - if isinstance(visibility, str) and visibility not in ("public", "tenant"): - self._error( - "invalid visibility option, must be either None, 'public', 'tenant' or a list of users" - ) - if self._user_config.run.mode not in ("online", "offline"): self._error("invalid mode specified, must be online, offline or disabled") return False @@ -760,7 +762,10 @@ def init( self._timer = time.time() self._sv_obj = RunObject.new( - folder=folder, offline=self._user_config.run.mode == "offline" + folder=folder, + offline=self._user_config.run.mode == "offline", + server_url=self._user_config.server.url, + server_token=self._user_config.server.token, ) if description: @@ -1088,7 +1093,12 @@ def reconnect(self, run_id: str) -> bool: """ self._status = "running" - self._sv_obj = RunObject(identifier=run_id, _read_only=False) + self._sv_obj = RunObject( + identifier=run_id, + _read_only=False, + server_url=self._user_config.server.url, + server_token=self._user_config.server.token, + ) self._sv_obj.status = self._status self._sv_obj.system = get_system() @@ -1421,7 +1431,8 @@ def log_event( _log_level_server_version = semver.parse("1.2.16") if ( - self._user_config.run.mode != "offline" + log_level + and self._user_config.run.mode != "offline" and self._user_config.server_version < _log_level_server_version ): self._error("Log level is not supported on current server.") @@ -1617,6 +1628,8 @@ def assign_metric_to_grid( grid=axes_ticks, labels=axes_labels, offline=self._user_config.run.mode == "offline", + server_url=self._user_config.server.url, + server_token=self._user_config.server.token, ) _new_grid.commit() @@ -1638,6 +1651,8 @@ def assign_metric_to_grid( _grid_attach = Grid( identifier=self._grids[grid_name]["id"], offline=self._user_config.run.mode == "offline", + server_url=self._user_config.server.url, + server_token=self._user_config.server.token, ) _grid_attach.read_only(False) _grid_attach.attach_metric_for_run(self.id, metric_name) @@ -1791,11 +1806,9 @@ def save_object( self, obj: typing.Any, category: typing.Literal["input", "output", "code"], - name: typing.Optional[ - typing.Annotated[str, pydantic.Field(pattern=NAME_REGEX)] - ] = None, + name: typing.Annotated[str, pydantic.Field(pattern=NAME_REGEX)] | None = None, allow_pickle: bool = False, - metadata: dict[str, typing.Any] = None, + metadata: dict[str, typing.Any] | None = None, ) -> bool: """Save an object to the Simvue server @@ -1847,6 +1860,8 @@ def save_object( storage=self._storage_id, metadata=metadata, offline=self._user_config.run.mode == "offline", + server_url=self._user_config.server.url, + server_token=self._user_config.server.token, ) _artifact.attach_to_run(self.id, category) except (ValueError, RuntimeError) as e: @@ -1865,10 +1880,8 @@ def save_file( file_type: str | None = None, preserve_path: bool = False, snapshot: bool = False, - name: typing.Optional[ - typing.Annotated[str, pydantic.Field(pattern=NAME_REGEX)] - ] = None, - metadata: dict[str, typing.Any] = None, + name: typing.Annotated[str, pydantic.Field(pattern=NAME_REGEX)] | None = None, + metadata: dict[str, typing.Any] | None = None, ) -> bool: """Upload file to the server @@ -1922,6 +1935,8 @@ def save_file( mime_type=file_type, metadata=metadata, snapshot=snapshot, + server_url=self._user_config.server.url, + server_token=self._user_config.server.token, ) _artifact.attach_to_run(self.id, category) except (ValueError, RuntimeError) as e: @@ -2222,7 +2237,11 @@ def add_alerts( ) return False try: - if alerts := Alert.get(offline=self._user_config.run.mode == "offline"): + if alerts := Alert.get( + offline=self._user_config.run.mode == "offline", + server_url=self._user_config.server.url, + server_token=self._user_config.server.token, + ): ids += [id for id, alert in alerts if alert.name in names] else: self._error("No existing alerts") @@ -2245,7 +2264,9 @@ def _check_if_alert_exists(self, alert: "AlertBase") -> str | None: """Check if an existing alert matches definition.""" # If the alert already exists just add the existing one for _id, _existing_alert in Alert.get( - offline=self._user_config.run.mode == "offline" + offline=self._user_config.run.mode == "offline", + server_url=self._user_config.server.url, + server_token=self._user_config.server.token, ): if _existing_alert == alert: return _id @@ -2328,6 +2349,8 @@ def create_metric_range_alert( range_high=range_high, frequency=frequency or 60, offline=self._user_config.run.mode == "offline", + server_url=self._user_config.server.url, + server_token=self._user_config.server.token, ) # If the alert already exists just add the existing one @@ -2416,6 +2439,8 @@ def create_metric_threshold_alert( aggregation=aggregation, notification=notification, offline=self._user_config.run.mode == "offline", + server_url=self._user_config.server.url, + server_token=self._user_config.server.token, ) # If the alert already exists just add the existing one @@ -2477,6 +2502,8 @@ def create_event_alert( notification=notification, frequency=frequency, offline=self._user_config.run.mode == "offline", + server_url=self._user_config.server.url, + server_token=self._user_config.server.token, ) # If the alert already exists just add the existing one @@ -2535,6 +2562,8 @@ def create_user_alert( notification=notification, description=description, offline=self._user_config.run.mode == "offline", + server_url=self._user_config.server.url, + server_token=self._user_config.server.token, ) # If the alert already exists just add the existing one @@ -2606,7 +2635,11 @@ def log_alert( if not identifier: self._error(f"Alert with name '{name}' could not be found.") - _alert = UserAlert(identifier=identifier) + _alert = UserAlert( + identifier=identifier, + server_url=self._user_config.server.url, + server_token=self._user_config.server.token, + ) if not isinstance(_alert, UserAlert): self._error( f"Cannot update state for alert '{identifier}' "