From dae9510981883b8d013ac7c8cd2150545d627ea6 Mon Sep 17 00:00:00 2001 From: ben Date: Mon, 20 Apr 2026 12:33:14 -0400 Subject: [PATCH 01/10] transmission code --- poetry.lock | 616 +++++++++--------- pyproject.toml | 5 + src/ria_toolkit_oss/agent.py | 124 +++- src/ria_toolkit_oss/orchestration/executor.py | 16 + src/ria_toolkit_oss/orchestration/labeler.py | 9 + .../orchestration/tx_executor.py | 253 +++++++ tests/orchestration/test_executor.py | 295 +++++++++ tests/orchestration/test_labeler.py | 36 + tests/orchestration/test_tx_executor.py | 155 +++++ tests/ria_toolkit_oss_cli/test_generate.py | 4 +- tests/test_agent.py | 249 +++++++ 11 files changed, 1448 insertions(+), 314 deletions(-) create mode 100644 src/ria_toolkit_oss/orchestration/tx_executor.py create mode 100644 tests/orchestration/test_executor.py create mode 100644 tests/orchestration/test_tx_executor.py create mode 100644 tests/test_agent.py diff --git a/poetry.lock b/poetry.lock index d2ddd55..09e83f8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -275,153 +275,153 @@ pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} [[package]] name = "charset-normalizer" -version = "3.4.6" +version = "3.4.7" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" groups = ["agent", "docs"] files = [ - {file = "charset_normalizer-3.4.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2e1d8ca8611099001949d1cdfaefc510cf0f212484fe7c565f735b68c78c3c95"}, - {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e25369dc110d58ddf29b949377a93e0716d72a24f62bad72b2b39f155949c1fd"}, - {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:259695e2ccc253feb2a016303543d691825e920917e31f894ca1a687982b1de4"}, - {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dda86aba335c902b6149a02a55b38e96287157e609200811837678214ba2b1db"}, - {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51fb3c322c81d20567019778cb5a4a6f2dc1c200b886bc0d636238e364848c89"}, - {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:4482481cb0572180b6fd976a4d5c72a30263e98564da68b86ec91f0fe35e8565"}, - {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39f5068d35621da2881271e5c3205125cc456f54e9030d3f723288c873a71bf9"}, - {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8bea55c4eef25b0b19a0337dc4e3f9a15b00d569c77211fa8cde38684f234fb7"}, - {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f0cdaecd4c953bfae0b6bb64910aaaca5a424ad9c72d85cb88417bb9814f7550"}, - {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:150b8ce8e830eb7ccb029ec9ca36022f756986aaaa7956aad6d9ec90089338c0"}, - {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e68c14b04827dd76dcbd1aeea9e604e3e4b78322d8faf2f8132c7138efa340a8"}, - {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:3778fd7d7cd04ae8f54651f4a7a0bd6e39a0cf20f801720a4c21d80e9b7ad6b0"}, - {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dad6e0f2e481fffdcf776d10ebee25e0ef89f16d691f1e5dee4b586375fdc64b"}, - {file = "charset_normalizer-3.4.6-cp310-cp310-win32.whl", hash = "sha256:74a2e659c7ecbc73562e2a15e05039f1e22c75b7c7618b4b574a3ea9118d1557"}, - {file = "charset_normalizer-3.4.6-cp310-cp310-win_amd64.whl", hash = "sha256:aa9cccf4a44b9b62d8ba8b4dd06c649ba683e4bf04eea606d2e94cfc2d6ff4d6"}, - {file = "charset_normalizer-3.4.6-cp310-cp310-win_arm64.whl", hash = "sha256:e985a16ff513596f217cee86c21371b8cd011c0f6f056d0920aa2d926c544058"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:82060f995ab5003a2d6e0f4ad29065b7672b6593c8c63559beefe5b443242c3e"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60c74963d8350241a79cb8feea80e54d518f72c26db618862a8f53e5023deaf9"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6e4333fb15c83f7d1482a76d45a0818897b3d33f00efd215528ff7c51b8e35d"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bc72863f4d9aba2e8fd9085e63548a324ba706d2ea2c83b260da08a59b9482de"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9cc4fc6c196d6a8b76629a70ddfcd4635a6898756e2d9cac5565cf0654605d73"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:0c173ce3a681f309f31b87125fecec7a5d1347261ea11ebbb856fa6006b23c8c"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c907cdc8109f6c619e6254212e794d6548373cc40e1ec75e6e3823d9135d29cc"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:404a1e552cf5b675a87f0651f8b79f5f1e6fd100ee88dc612f89aa16abd4486f"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e3c701e954abf6fc03a49f7c579cc80c2c6cc52525340ca3186c41d3f33482ef"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7a6967aaf043bceabab5412ed6bd6bd26603dae84d5cb75bf8d9a74a4959d398"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5feb91325bbceade6afab43eb3b508c63ee53579fe896c77137ded51c6b6958e"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f820f24b09e3e779fe84c3c456cb4108a7aa639b0d1f02c28046e11bfcd088ed"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b35b200d6a71b9839a46b9b7fff66b6638bb52fc9658aa58796b0326595d3021"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-win32.whl", hash = "sha256:9ca4c0b502ab399ef89248a2c84c54954f77a070f28e546a85e91da627d1301e"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:a9e68c9d88823b274cf1e72f28cb5dc89c990edf430b0bfd3e2fb0785bfeabf4"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:97d0235baafca5f2b09cf332cc275f021e694e8362c6bb9c96fc9a0eb74fc316"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-win32.whl", hash = "sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-win32.whl", hash = "sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-win32.whl", hash = "sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-win32.whl", hash = "sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c"}, - {file = "charset_normalizer-3.4.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:659a1e1b500fac8f2779dd9e1570464e012f43e580371470b45277a27baa7532"}, - {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f61aa92e4aad0be58eb6eb4e0c21acf32cf8065f4b2cae5665da756c4ceef982"}, - {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f50498891691e0864dc3da965f340fada0771f6142a378083dc4608f4ea513e2"}, - {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bf625105bb9eef28a56a943fec8c8a98aeb80e7d7db99bd3c388137e6eb2d237"}, - {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2bd9d128ef93637a5d7a6af25363cf5dec3fa21cf80e68055aad627f280e8afa"}, - {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux_2_31_armv7l.whl", hash = "sha256:d08ec48f0a1c48d75d0356cea971921848fb620fdeba805b28f937e90691209f"}, - {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ed80ff870ca6de33f4d953fda4d55654b9a2b340ff39ab32fa3adbcd718f264"}, - {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f98059e4fcd3e3e4e2d632b7cf81c2faae96c43c60b569e9c621468082f1d104"}, - {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:ab30e5e3e706e3063bc6de96b118688cb10396b70bb9864a430f67df98c61ecc"}, - {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:d5f5d1e9def3405f60e3ca8232d56f35c98fb7bf581efcc60051ebf53cb8b611"}, - {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:461598cd852bfa5a61b09cae2b1c02e2efcd166ee5516e243d540ac24bfa68a7"}, - {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:71be7e0e01753a89cf024abf7ecb6bca2c81738ead80d43004d9b5e3f1244e64"}, - {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:df01808ee470038c3f8dc4f48620df7225c49c2d6639e38f96e6d6ac6e6f7b0e"}, - {file = "charset_normalizer-3.4.6-cp38-cp38-win32.whl", hash = "sha256:69dd852c2f0ad631b8b60cfbe25a28c0058a894de5abb566619c205ce0550eae"}, - {file = "charset_normalizer-3.4.6-cp38-cp38-win_amd64.whl", hash = "sha256:517ad0e93394ac532745129ceabdf2696b609ec9f87863d337140317ebce1c14"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:31215157227939b4fb3d740cd23fe27be0439afef67b785a1eb78a3ae69cba9e"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecbbd45615a6885fe3240eb9db73b9e62518b611850fdf8ab08bd56de7ad2b17"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c45a03a4c69820a399f1dda9e1d8fbf3562eda46e7720458180302021b08f778"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e8aeb10fcbe92767f0fa69ad5a72deca50d0dca07fbde97848997d778a50c9fe"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54fae94be3d75f3e573c9a1b5402dc593de19377013c9a0e4285e3d402dd3a2a"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:2f7fdd9b6e6c529d6a2501a2d36b240109e78a8ceaef5687cfcfa2bbe671d297"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1d02209e06550bdaef34af58e041ad71b88e624f5d825519da3a3308e22687"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8bc5f0687d796c05b1e28ab0d38a50e6309906ee09375dd3aff6a9c09dd6e8f4"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ee4ec14bc1680d6b0afab9aea2ef27e26d2024f18b24a2d7155a52b60da7e833"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:d1a2ee9c1499fc8f86f4521f27a973c914b211ffa87322f4ee33bb35392da2c5"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:48696db7f18afb80a068821504296eb0787d9ce239b91ca15059d1d3eaacf13b"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4f41da960b196ea355357285ad1316a00099f22d0929fe168343b99b254729c9"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:802168e03fba8bbc5ce0d866d589e4b1ca751d06edee69f7f3a19c5a9fe6b597"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-win32.whl", hash = "sha256:8761ac29b6c81574724322a554605608a9960769ea83d2c73e396f3df896ad54"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-win_amd64.whl", hash = "sha256:1cf0a70018692f85172348fe06d3a4b63f94ecb055e13a00c644d368eb82e5b8"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-win_arm64.whl", hash = "sha256:3516bbb8d42169de9e61b8520cbeeeb716f12f4ecfe3fd30a9919aa16c806ca8"}, - {file = "charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69"}, - {file = "charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e5f4d355f0a2b1a31bc3edec6795b46324349c9cb25eed068049e4f472fb4259"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16d971e29578a5e97d7117866d15889a4a07befe0e87e703ed63cd90cb348c01"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dca4bbc466a95ba9c0234ef56d7dd9509f63da22274589ebd4ed7f1f4d4c54e3"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e80c8378d8f3d83cd3164da1ad2df9e37a666cdde7b1cb2298ed0b558064be30"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36836d6ff945a00b88ba1e4572d721e60b5b8c98c155d465f56ad19d68f23734"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux_2_31_armv7l.whl", hash = "sha256:bd9b23791fe793e4968dba0c447e12f78e425c59fc0e3b97f6450f4781f3ee60"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aef65cd602a6d0e0ff6f9930fcb1c8fec60dd2cfcb6facaf4bdb0e5873042db0"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:82b271f5137d07749f7bf32f70b17ab6eaabedd297e75dce75081a24f76eb545"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:1efde3cae86c8c273f1eb3b287be7d8499420cf2fe7585c41d370d3e790054a5"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:c593052c465475e64bbfe5dbd81680f64a67fdc752c56d7a0ae205dc8aeefe0f"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:af21eb4409a119e365397b2adbaca4c9ccab56543a65d5dbd9f920d6ac29f686"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:84c018e49c3bf790f9c2771c45e9313a08c2c2a6342b162cd650258b57817706"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dd915403e231e6b1809fe9b6d9fc55cf8fb5e02765ac625d9cd623342a7905d7"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-win32.whl", hash = "sha256:320ade88cfb846b8cd6b4ddf5ee9e80ee0c1f52401f2456b84ae1ae6a1a5f207"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-win_amd64.whl", hash = "sha256:1dc8b0ea451d6e69735094606991f32867807881400f808a106ee1d963c46a83"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:177a0ba5f0211d488e295aaf82707237e331c24788d8d76c96c5a41594723217"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e0d51f618228538a3e8f46bd246f87a6cd030565e015803691603f55e12afb5"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:14265bfe1f09498b9d8ec91e9ec9fa52775edf90fcbde092b25f4a33d444fea9"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:87fad7d9ba98c86bcb41b2dc8dbb326619be2562af1f8ff50776a39e55721c5a"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f22dec1690b584cea26fade98b2435c132c1b5f68e39f5a0b7627cd7ae31f1dc"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:d61f00a0869d77422d9b2aba989e2d24afa6ffd552af442e0e58de4f35ea6d00"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6370e8686f662e6a3941ee48ed4742317cafbe5707e36406e9df792cdb535776"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a6c5863edfbe888d9eff9c8b8087354e27618d9da76425c119293f11712a6319"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ed065083d0898c9d5b4bbec7b026fd755ff7454e6e8b73a67f8c744b13986e24"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2cd4a60d0e2fb04537162c62bbbb4182f53541fe0ede35cdf270a1c1e723cc42"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:813c0e0132266c08eb87469a642cb30aaff57c5f426255419572aaeceeaa7bf4"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:07d9e39b01743c3717745f4c530a6349eadbfa043c7577eef86c502c15df2c67"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c0f081d69a6e58272819b70288d3221a6ee64b98df852631c80f293514d3b274"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-win32.whl", hash = "sha256:8751d2787c9131302398b11e6c8068053dcb55d5a8964e114b6e196cf16cb366"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:12a6fff75f6bc66711b73a2f0addfc4c8c15a20e805146a02d147a318962c444"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:bb8cc7534f51d9a017b93e3e85b260924f909601c3df002bcdb58ddb4dc41a5c"}, + {file = "charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d"}, + {file = "charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5"}, ] [[package]] name = "click" -version = "8.3.1" +version = "8.3.2" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.10" groups = ["main", "dev", "docs", "server", "test"] files = [ - {file = "click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"}, - {file = "click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a"}, + {file = "click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d"}, + {file = "click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5"}, ] [package.dependencies] @@ -627,6 +627,18 @@ files = [ docs = ["ipython", "matplotlib", "numpydoc", "sphinx"] tests = ["pytest", "pytest-cov", "pytest-xdist"] +[[package]] +name = "defusedxml" +version = "0.7.1" +description = "XML bomb protection for Python stdlib modules" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["main"] +files = [ + {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, + {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, +] + [[package]] name = "dill" version = "0.4.1" @@ -688,14 +700,14 @@ test = ["pytest (>=6)"] [[package]] name = "fastapi" -version = "0.135.3" +version = "0.136.0" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false python-versions = ">=3.10" groups = ["server", "test"] files = [ - {file = "fastapi-0.135.3-py3-none-any.whl", hash = "sha256:9b0f590c813acd13d0ab43dd8494138eb58e484bfac405db1f3187cfc5810d98"}, - {file = "fastapi-0.135.3.tar.gz", hash = "sha256:bd6d7caf1a2bdd8d676843cdcd2287729572a1ef524fc4d65c17ae002a1be654"}, + {file = "fastapi-0.136.0-py3-none-any.whl", hash = "sha256:8793d44ec7378e2be07f8a013cf7f7aa47d6327d0dfe9804862688ec4541a6b4"}, + {file = "fastapi-0.136.0.tar.gz", hash = "sha256:cf08e067cc66e106e102d9ba659463abfac245200752f8a5b7b1e813de4ff73e"}, ] [package.dependencies] @@ -707,19 +719,19 @@ typing-inspection = ">=0.4.2" [package.extras] all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "uvicorn[standard] (>=0.12.0)"] -standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] +standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "fastar (>=0.9.0)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[standard-no-fastapi-cloud-cli] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] [[package]] name = "filelock" -version = "3.25.2" +version = "3.29.0" description = "A platform independent file lock." optional = false python-versions = ">=3.10" groups = ["test"] files = [ - {file = "filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70"}, - {file = "filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694"}, + {file = "filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258"}, + {file = "filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90"}, ] [[package]] @@ -1468,14 +1480,14 @@ files = [ [[package]] name = "narwhals" -version = "2.18.1" +version = "2.20.0" description = "Extremely lightweight compatibility layer between dataframe libraries" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "narwhals-2.18.1-py3-none-any.whl", hash = "sha256:a0a8bb80205323851338888ba3a12b4f65d352362c8a94be591244faf36504ad"}, - {file = "narwhals-2.18.1.tar.gz", hash = "sha256:652a1fcc9d432bbf114846688884c215f17eb118aa640b7419295d2f910d2a8b"}, + {file = "narwhals-2.20.0-py3-none-any.whl", hash = "sha256:16e750ea5507d4ba6e8d03455b5f93a535e0405976561baea235bca5dc9f475d"}, + {file = "narwhals-2.20.0.tar.gz", hash = "sha256:c10994975fa7dc5a68c2cffcddbd5908fc8ebb2d463c5bab085309c0ee1f551e"}, ] [package.extras] @@ -1624,14 +1636,14 @@ sympy = "*" [[package]] name = "packaging" -version = "26.0" +version = "26.1" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" groups = ["main", "dev", "docs", "server", "test"] files = [ - {file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"}, - {file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"}, + {file = "packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f"}, + {file = "packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de"}, ] [[package]] @@ -1863,26 +1875,26 @@ xmp = ["defusedxml"] [[package]] name = "platformdirs" -version = "4.9.4" +version = "4.9.6" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.10" groups = ["dev", "test"] files = [ - {file = "platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868"}, - {file = "platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934"}, + {file = "platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917"}, + {file = "platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a"}, ] [[package]] name = "plotly" -version = "6.6.0" +version = "6.7.0" description = "An open-source interactive data visualization library for Python" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "plotly-6.6.0-py3-none-any.whl", hash = "sha256:8d6daf0f87412e0c0bfe72e809d615217ab57cc715899a1e5145135a7800d1d0"}, - {file = "plotly-6.6.0.tar.gz", hash = "sha256:b897f15f3b02028d69f755f236be890ba950d0a42d7dfc619b44e2d8cea8748c"}, + {file = "plotly-6.7.0-py3-none-any.whl", hash = "sha256:ac8aca1c25c663a59b5b9140a549264a5badde2e057d79b8c772ae2920e32ff0"}, + {file = "plotly-6.7.0.tar.gz", hash = "sha256:45eea0ff27e2a23ccd62776f77eb43aa1ca03df4192b76036e380bb479b892c6"}, ] [package.dependencies] @@ -1890,11 +1902,11 @@ narwhals = ">=1.15.1" packaging = "*" [package.extras] -dev = ["plotly[dev-optional]"] -dev-build = ["build", "jupyter", "plotly[dev-core]"] +dev = ["anywidget", "build", "colorcet", "fiona (<=1.9.6) ; python_version <= \"3.8\"", "geopandas", "inflect", "jupyterlab", "kaleido (>=1.1.0)", "numpy (>=1.22)", "orjson", "pandas", "pdfrw", "pillow", "plotly-geo", "polars[timezone]", "pyarrow", "pyshp", "pytest", "pytz", "requests", "ruff (==0.11.12)", "scikit-image", "scipy", "shapely", "statsmodels", "vaex ; python_version <= \"3.9\"", "xarray"] +dev-build = ["build", "jupyterlab", "pytest", "requests", "ruff (==0.11.12)"] dev-core = ["pytest", "requests", "ruff (==0.11.12)"] -dev-optional = ["anywidget", "colorcet", "fiona (<=1.9.6) ; python_version <= \"3.8\"", "geopandas", "inflect", "numpy", "orjson", "pandas", "pdfrw", "pillow", "plotly-geo", "plotly[dev-build]", "plotly[kaleido]", "polars[timezone]", "pyarrow", "pyshp", "pytz", "scikit-image", "scipy", "shapely", "statsmodels", "vaex ; python_version <= \"3.9\"", "xarray"] -express = ["numpy"] +dev-optional = ["anywidget", "build", "colorcet", "fiona (<=1.9.6) ; python_version <= \"3.8\"", "geopandas", "inflect", "jupyterlab", "kaleido (>=1.1.0)", "numpy (>=1.22)", "orjson", "pandas", "pdfrw", "pillow", "plotly-geo", "polars[timezone]", "pyarrow", "pyshp", "pytest", "pytz", "requests", "ruff (==0.11.12)", "scikit-image", "scipy", "shapely", "statsmodels", "vaex ; python_version <= \"3.9\"", "xarray"] +express = ["numpy (>=1.22)"] kaleido = ["kaleido (>=1.1.0)"] [[package]] @@ -1958,19 +1970,19 @@ files = [ [[package]] name = "pydantic" -version = "2.12.5" +version = "2.13.3" description = "Data validation using Python type hints" optional = false python-versions = ">=3.9" groups = ["server", "test"] files = [ - {file = "pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d"}, - {file = "pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49"}, + {file = "pydantic-2.13.3-py3-none-any.whl", hash = "sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927"}, + {file = "pydantic-2.13.3.tar.gz", hash = "sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d"}, ] [package.dependencies] annotated-types = ">=0.6.0" -pydantic-core = "2.41.5" +pydantic-core = "2.46.3" typing-extensions = ">=4.14.1" typing-inspection = ">=0.4.2" @@ -1980,133 +1992,132 @@ timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows [[package]] name = "pydantic-core" -version = "2.41.5" +version = "2.46.3" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.9" groups = ["server", "test"] files = [ - {file = "pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146"}, - {file = "pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c"}, - {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2"}, - {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556"}, - {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49"}, - {file = "pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba"}, - {file = "pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9"}, - {file = "pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6"}, - {file = "pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b"}, - {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284"}, - {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594"}, - {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e"}, - {file = "pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b"}, - {file = "pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe"}, - {file = "pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f"}, - {file = "pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7"}, - {file = "pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5"}, - {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c"}, - {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294"}, - {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1"}, - {file = "pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d"}, - {file = "pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815"}, - {file = "pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3"}, - {file = "pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9"}, - {file = "pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d"}, - {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740"}, - {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e"}, - {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858"}, - {file = "pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36"}, - {file = "pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11"}, - {file = "pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd"}, - {file = "pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a"}, - {file = "pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553"}, - {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90"}, - {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07"}, - {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb"}, - {file = "pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23"}, - {file = "pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf"}, - {file = "pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008"}, - {file = "pydantic_core-2.41.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf"}, - {file = "pydantic_core-2.41.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425"}, - {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504"}, - {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5"}, - {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3"}, - {file = "pydantic_core-2.41.5-cp39-cp39-win32.whl", hash = "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460"}, - {file = "pydantic_core-2.41.5-cp39-cp39-win_amd64.whl", hash = "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b"}, - {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034"}, - {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c"}, - {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2"}, - {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad"}, - {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd"}, - {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc"}, - {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56"}, - {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51"}, - {file = "pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e"}, + {file = "pydantic_core-2.46.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:1da3786b8018e60349680720158cc19161cc3b4bdd815beb0a321cd5ce1ad5b1"}, + {file = "pydantic_core-2.46.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cc0988cb29d21bf4a9d5cf2ef970b5c0e38d8d8e107a493278c05dc6c1dda69f"}, + {file = "pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27f9067c3bfadd04c55484b89c0d267981b2f3512850f6f66e1e74204a4e4ce3"}, + {file = "pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a642ac886ecf6402d9882d10c405dcf4b902abeb2972cd5fb4a48c83cd59279a"}, + {file = "pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79f561438481f28681584b89e2effb22855e2179880314bcddbf5968e935e807"}, + {file = "pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57a973eae4665352a47cf1a99b4ee864620f2fe663a217d7a8da68a1f3a5bfda"}, + {file = "pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83d002b97072a53ea150d63e0a3adfae5670cef5aa8a6e490240e482d3b22e57"}, + {file = "pydantic_core-2.46.3-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b40ddd51e7c44b28cfaef746c9d3c506d658885e0a46f9eeef2ee815cbf8e045"}, + {file = "pydantic_core-2.46.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac5ec7fb9b87f04ee839af2d53bcadea57ded7d229719f56c0ed895bff987943"}, + {file = "pydantic_core-2.46.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a3b11c812f61b3129c4905781a2601dfdfdea5fe1e6c1cfb696b55d14e9c054f"}, + {file = "pydantic_core-2.46.3-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:1108da631e602e5b3c38d6d04fe5bb3bfa54349e6918e3ca6cf570b2e2b2f9d4"}, + {file = "pydantic_core-2.46.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:de885175515bcfa98ae618c1df7a072f13d179f81376c8007112af20567fd08a"}, + {file = "pydantic_core-2.46.3-cp310-cp310-win32.whl", hash = "sha256:d11058e3201527d41bc6b545c79187c9e4bf85e15a236a6007f0e991518882b7"}, + {file = "pydantic_core-2.46.3-cp310-cp310-win_amd64.whl", hash = "sha256:3612edf65c8ea67ac13616c4d23af12faef1ae435a8a93e5934c2a0cbbdd1fd6"}, + {file = "pydantic_core-2.46.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ab124d49d0459b2373ecf54118a45c28a1e6d4192a533fbc915e70f556feb8e5"}, + {file = "pydantic_core-2.46.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cca67d52a5c7a16aed2b3999e719c4bcf644074eac304a5d3d62dd70ae7d4b2c"}, + {file = "pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c024e08c0ba23e6fd68c771a521e9d6a792f2ebb0fa734296b36394dc30390e"}, + {file = "pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6645ce7eec4928e29a1e3b3d5c946621d105d3e79f0c9cddf07c2a9770949287"}, + {file = "pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a712c7118e6c5ea96562f7b488435172abb94a3c53c22c9efc1412264a45cbbe"}, + {file = "pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69a868ef3ff206343579021c40faf3b1edc64b1cc508ff243a28b0a514ccb050"}, + {file = "pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc7e8c32db809aa0f6ea1d6869ebc8518a65d5150fdfad8bcae6a49ae32a22e2"}, + {file = "pydantic_core-2.46.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:3481bd1341dc85779ee506bc8e1196a277ace359d89d28588a9468c3ecbe63fa"}, + {file = "pydantic_core-2.46.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8690eba565c6d68ffd3a8655525cbdd5246510b44a637ee2c6c03a7ebfe64d3c"}, + {file = "pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4de88889d7e88d50d40ee5b39d5dac0bcaef9ba91f7e536ac064e6b2834ecccf"}, + {file = "pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:e480080975c1ef7f780b8f99ed72337e7cc5efea2e518a20a692e8e7b278eb8b"}, + {file = "pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:de3a5c376f8cd94da9a1b8fd3dd1c16c7a7b216ed31dc8ce9fd7a22bf13b836e"}, + {file = "pydantic_core-2.46.3-cp311-cp311-win32.whl", hash = "sha256:fc331a5314ffddd5385b9ee9d0d2fee0b13c27e0e02dad71b1ae5d6561f51eeb"}, + {file = "pydantic_core-2.46.3-cp311-cp311-win_amd64.whl", hash = "sha256:b5b9c6cf08a8a5e502698f5e153056d12c34b8fb30317e0c5fd06f45162a6346"}, + {file = "pydantic_core-2.46.3-cp311-cp311-win_arm64.whl", hash = "sha256:5dfd51cf457482f04ec49491811a2b8fd5b843b64b11eecd2d7a1ee596ea78a6"}, + {file = "pydantic_core-2.46.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b11b59b3eee90a80a36701ddb4576d9ae31f93f05cb9e277ceaa09e6bf074a67"}, + {file = "pydantic_core-2.46.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:af8653713055ea18a3abc1537fe2ebc42f5b0bbb768d1eb79fd74eb47c0ac089"}, + {file = "pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75a519dab6d63c514f3a81053e5266c549679e4aa88f6ec57f2b7b854aceb1b0"}, + {file = "pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6cd87cb1575b1ad05ba98894c5b5c96411ef678fa2f6ed2576607095b8d9789"}, + {file = "pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f80a55484b8d843c8ada81ebf70a682f3f00a3d40e378c06cf17ecb44d280d7d"}, + {file = "pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3861f1731b90c50a3266316b9044f5c9b405eecb8e299b0a7120596334e4fe9c"}, + {file = "pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb528e295ed31570ac3dcc9bfdd6e0150bc11ce6168ac87a8082055cf1a67395"}, + {file = "pydantic_core-2.46.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:367508faa4973b992b271ba1494acaab36eb7e8739d1e47be5035fb1ea225396"}, + {file = "pydantic_core-2.46.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ad3c826fe523e4becf4fe39baa44286cff85ef137c729a2c5e269afbfd0905d"}, + {file = "pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ec638c5d194ef8af27db69f16c954a09797c0dc25015ad6123eb2c73a4d271ca"}, + {file = "pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:28ed528c45446062ee66edb1d33df5d88828ae167de76e773a3c7f64bd14e976"}, + {file = "pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aed19d0c783886d5bd86d80ae5030006b45e28464218747dcf83dabfdd092c7b"}, + {file = "pydantic_core-2.46.3-cp312-cp312-win32.whl", hash = "sha256:06d5d8820cbbdb4147578c1fe7ffcd5b83f34508cb9f9ab76e807be7db6ff0a4"}, + {file = "pydantic_core-2.46.3-cp312-cp312-win_amd64.whl", hash = "sha256:c3212fda0ee959c1dd04c60b601ec31097aaa893573a3a1abd0a47bcac2968c1"}, + {file = "pydantic_core-2.46.3-cp312-cp312-win_arm64.whl", hash = "sha256:f1f8338dd7a7f31761f1f1a3c47503a9a3b34eea3c8b01fa6ee96408affb5e72"}, + {file = "pydantic_core-2.46.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:12bc98de041458b80c86c56b24df1d23832f3e166cbaff011f25d187f5c62c37"}, + {file = "pydantic_core-2.46.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:85348b8f89d2c3508b65b16c3c33a4da22b8215138d8b996912bb1532868885f"}, + {file = "pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1105677a6df914b1fb71a81b96c8cce7726857e1717d86001f29be06a25ee6f8"}, + {file = "pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87082cd65669a33adeba5470769e9704c7cf026cc30afb9cc77fd865578ebaad"}, + {file = "pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e5f66e12c4f5212d08522963380eaaeac5ebd795826cfd19b2dfb0c7a52b9c"}, + {file = "pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6cdf19bf84128d5e7c37e8a73a0c5c10d51103a650ac585d42dd6ae233f2b7f"}, + {file = "pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031bb17f4885a43773c8c763089499f242aee2ea85cf17154168775dccdecf35"}, + {file = "pydantic_core-2.46.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:bcf2a8b2982a6673693eae7348ef3d8cf3979c1d63b54fca7c397a635cc68687"}, + {file = "pydantic_core-2.46.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28e8cf2f52d72ced402a137145923a762cbb5081e48b34312f7a0c8f55928ec3"}, + {file = "pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:17eaface65d9fc5abb940003020309c1bf7a211f5f608d7870297c367e6f9022"}, + {file = "pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:93fd339f23408a07e98950a89644f92c54d8729719a40b30c0a30bb9ebc55d23"}, + {file = "pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:23cbdb3aaa74dfe0837975dbf69b469753bbde8eacace524519ffdb6b6e89eb7"}, + {file = "pydantic_core-2.46.3-cp313-cp313-win32.whl", hash = "sha256:610eda2e3838f401105e6326ca304f5da1e15393ae25dacae5c5c63f2c275b13"}, + {file = "pydantic_core-2.46.3-cp313-cp313-win_amd64.whl", hash = "sha256:68cc7866ed863db34351294187f9b729964c371ba33e31c26f478471c52e1ed0"}, + {file = "pydantic_core-2.46.3-cp313-cp313-win_arm64.whl", hash = "sha256:f64b5537ac62b231572879cd08ec05600308636a5d63bcbdb15063a466977bec"}, + {file = "pydantic_core-2.46.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:afa3aa644f74e290cdede48a7b0bee37d1c35e71b05105f6b340d484af536d9b"}, + {file = "pydantic_core-2.46.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ced3310e51aa425f7f77da8bbbb5212616655bedbe82c70944320bc1dbe5e018"}, + {file = "pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e29908922ce9da1a30b4da490bd1d3d82c01dcfdf864d2a74aacee674d0bfa34"}, + {file = "pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0c9ff69140423eea8ed2d5477df3ba037f671f5e897d206d921bc9fdc39613e7"}, + {file = "pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b675ab0a0d5b1c8fdb81195dc5bcefea3f3c240871cdd7ff9a2de8aa50772eb2"}, + {file = "pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0087084960f209a9a4af50ecd1fb063d9ad3658c07bb81a7a53f452dacbfb2ba"}, + {file = "pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed42e6cc8e1b0e2b9b96e2276bad70ae625d10d6d524aed0c93de974ae029f9f"}, + {file = "pydantic_core-2.46.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:f1771ce258afb3e4201e67d154edbbae712a76a6081079fe247c2f53c6322c22"}, + {file = "pydantic_core-2.46.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a7610b6a5242a6c736d8ad47fd5fff87fcfe8f833b281b1c409c3d6835d9227f"}, + {file = "pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:ff5e7783bcc5476e1db448bf268f11cb257b1c276d3e89f00b5727be86dd0127"}, + {file = "pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:9d2e32edcc143bc01e95300671915d9ca052d4f745aa0a49c48d4803f8a85f2c"}, + {file = "pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6e42d83d1c6b87fa56b521479cff237e626a292f3b31b6345c15a99121b454c1"}, + {file = "pydantic_core-2.46.3-cp314-cp314-win32.whl", hash = "sha256:07bc6d2a28c3adb4f7c6ae46aa4f2d2929af127f587ed44057af50bf1ce0f505"}, + {file = "pydantic_core-2.46.3-cp314-cp314-win_amd64.whl", hash = "sha256:8940562319bc621da30714617e6a7eaa6b98c84e8c685bcdc02d7ed5e7c7c44e"}, + {file = "pydantic_core-2.46.3-cp314-cp314-win_arm64.whl", hash = "sha256:5dcbbcf4d22210ced8f837c96db941bdb078f419543472aca5d9a0bb7cddc7df"}, + {file = "pydantic_core-2.46.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d0fe3dce1e836e418f912c1ad91c73357d03e556a4d286f441bf34fed2dbeecf"}, + {file = "pydantic_core-2.46.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9ce92e58abc722dac1bf835a6798a60b294e48eb0e625ec9fd994b932ac5feee"}, + {file = "pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a03e6467f0f5ab796a486146d1b887b2dc5e5f9b3288898c1b1c3ad974e53e4a"}, + {file = "pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2798b6ba041b9d70acfb9071a2ea13c8456dd1e6a5555798e41ba7b0790e329c"}, + {file = "pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9be3e221bdc6d69abf294dcf7aff6af19c31a5cdcc8f0aa3b14be29df4bd03b1"}, + {file = "pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13936129ce841f2a5ddf6f126fea3c43cd128807b5a59588c37cf10178c2e64"}, + {file = "pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28b5f2ef03416facccb1c6ef744c69793175fd27e44ef15669201601cf423acb"}, + {file = "pydantic_core-2.46.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:830d1247d77ad23852314f069e9d7ddafeec5f684baf9d7e7065ed46a049c4e6"}, + {file = "pydantic_core-2.46.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0793c90c1a3c74966e7975eaef3ed30ebdff3260a0f815a62a22adc17e4c01c"}, + {file = "pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d2d0aead851b66f5245ec0c4fb2612ef457f8bbafefdf65a2bf9d6bac6140f47"}, + {file = "pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:2f40e4246676beb31c5ce77c38a55ca4e465c6b38d11ea1bd935420568e0b1ab"}, + {file = "pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:cf489cf8986c543939aeee17a09c04d6ffb43bfef8ca16fcbcc5cfdcbed24dba"}, + {file = "pydantic_core-2.46.3-cp314-cp314t-win32.whl", hash = "sha256:ffe0883b56cfc05798bf994164d2b2ff03efe2d22022a2bb080f3b626176dd56"}, + {file = "pydantic_core-2.46.3-cp314-cp314t-win_amd64.whl", hash = "sha256:706d9d0ce9cf4593d07270d8e9f53b161f90c57d315aeec4fb4fd7a8b10240d8"}, + {file = "pydantic_core-2.46.3-cp314-cp314t-win_arm64.whl", hash = "sha256:77706aeb41df6a76568434701e0917da10692da28cb69d5fb6919ce5fdb07374"}, + {file = "pydantic_core-2.46.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:fa3eb7c2995aa443687a825bc30395c8521b7c6ec201966e55debfd1128bcceb"}, + {file = "pydantic_core-2.46.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3d08782c4045f90724b44c95d35ebec0d67edb8a957a2ac81d5a8e4b8a200495"}, + {file = "pydantic_core-2.46.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:831eb19aa789a97356979e94c981e5667759301fb708d1c0d5adf1bc0098b873"}, + {file = "pydantic_core-2.46.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4335e87c7afa436a0dfa899e138d57a72f8aad542e2cf19c36fb428461caabd0"}, + {file = "pydantic_core-2.46.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99421e7684a60f7f3550a1d159ade5fdff1954baedb6bdd407cba6a307c9f27d"}, + {file = "pydantic_core-2.46.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd81f6907932ebac3abbe41378dac64b2380db1287e2aa64d8d88f78d170f51a"}, + {file = "pydantic_core-2.46.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f247596366f4221af52beddd65af1218797771d6989bc891a0b86ccaa019168"}, + {file = "pydantic_core-2.46.3-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:6dff8cc884679df229ebc6d8eb2321ea6f8e091bc7d4886d4dc2e0e71452843c"}, + {file = "pydantic_core-2.46.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68ef2f623dda6d5a9067ac014e406c020c780b2a358930a7e5c1b73702900720"}, + {file = "pydantic_core-2.46.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d56bdb4af1767cc15b0386b3c581fdfe659bb9ee4a4f776e92c1cd9d074000d6"}, + {file = "pydantic_core-2.46.3-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:91249bcb7c165c2fb2a2f852dbc5c91636e2e218e75d96dfdd517e4078e173dd"}, + {file = "pydantic_core-2.46.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4b068543bdb707f5d935dab765d99227aa2545ef2820935f2e5dd801795c7dbd"}, + {file = "pydantic_core-2.46.3-cp39-cp39-win32.whl", hash = "sha256:dcda6583921c05a40533f982321532f2d8db29326c7b95c4026941fa5074bd79"}, + {file = "pydantic_core-2.46.3-cp39-cp39-win_amd64.whl", hash = "sha256:a35cc284c8dd7edae8a31533713b4d2467dfe7c4f1b5587dd4031f28f90d1d13"}, + {file = "pydantic_core-2.46.3-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:9715525891ed524a0a1eb6d053c74d4d4ad5017677fb00af0b7c2644a31bae46"}, + {file = "pydantic_core-2.46.3-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:9d2f400712a99a013aff420ef1eb9be077f8189a36c1e3ef87660b4e1088a874"}, + {file = "pydantic_core-2.46.3-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd2aab0e2e9dc2daf36bd2686c982535d5e7b1d930a1344a7bb6e82baab42a76"}, + {file = "pydantic_core-2.46.3-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e9d76736da5f362fabfeea6a69b13b7f2be405c6d6966f06b2f6bfff7e64531"}, + {file = "pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:b12dd51f1187c2eb489af8e20f880362db98e954b54ab792fa5d92e8bcc6b803"}, + {file = "pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f00a0961b125f1a47af7bcc17f00782e12f4cd056f83416006b30111d941dfa3"}, + {file = "pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57697d7c056aca4bbb680200f96563e841a6386ac1129370a0102592f4dddff5"}, + {file = "pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd35aa21299def8db7ef4fe5c4ff862941a9a158ca7b63d61e66fe67d30416b4"}, + {file = "pydantic_core-2.46.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:13afdd885f3d71280cf286b13b310ee0f7ccfefd1dbbb661514a474b726e2f25"}, + {file = "pydantic_core-2.46.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f91c0aff3e3ee0928edd1232c57f643a7a003e6edf1860bc3afcdc749cb513f3"}, + {file = "pydantic_core-2.46.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6529d1d128321a58d30afcc97b49e98836542f68dd41b33c2e972bb9e5290536"}, + {file = "pydantic_core-2.46.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:975c267cff4f7e7272eacbe50f6cc03ca9a3da4c4fbd66fffd89c94c1e311aa1"}, + {file = "pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2b8e4f2bbdf71415c544b4b1138b8060db7b6611bc927e8064c769f64bed651c"}, + {file = "pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e61ea8e9fff9606d09178f577ff8ccdd7206ff73d6552bcec18e1033c4254b85"}, + {file = "pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b504bda01bafc69b6d3c7a0c7f039dcf60f47fab70e06fe23f57b5c75bdc82b8"}, + {file = "pydantic_core-2.46.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b00b76f7142fc60c762ce579bd29c8fa44aaa56592dd3c54fab3928d0d4ca6ff"}, + {file = "pydantic_core-2.46.3.tar.gz", hash = "sha256:41c178f65b8c29807239d47e6050262eb6bf84eb695e41101e62e38df4a5bc2c"}, ] [package.dependencies] @@ -2245,14 +2256,14 @@ six = ">=1.5" [[package]] name = "python-discovery" -version = "1.2.1" +version = "1.2.2" description = "Python interpreter discovery" optional = false python-versions = ">=3.8" groups = ["test"] files = [ - {file = "python_discovery-1.2.1-py3-none-any.whl", hash = "sha256:b6a957b24c1cd79252484d3566d1b49527581d46e789aaf43181005e56201502"}, - {file = "python_discovery-1.2.1.tar.gz", hash = "sha256:180c4d114bff1c32462537eac5d6a332b768242b76b69c0259c7d14b1b680c9e"}, + {file = "python_discovery-1.2.2-py3-none-any.whl", hash = "sha256:e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a"}, + {file = "python_discovery-1.2.2.tar.gz", hash = "sha256:876e9c57139eb757cb5878cbdd9ae5379e5d96266c99ef731119e04fffe533bb"}, ] [package.dependencies] @@ -2775,17 +2786,18 @@ test = ["Cython", "array-api-strict (>=2.0,<2.1.1)", "asv", "gmpy2", "hypothesis [[package]] name = "sigmf" -version = "1.7.2" +version = "1.8.0" description = "Easily interact with Signal Metadata Format (SigMF) recordings." optional = false python-versions = ">=3.7" groups = ["main"] files = [ - {file = "sigmf-1.7.2-py3-none-any.whl", hash = "sha256:6599b95e8bd3ac2c568b8ec46c312a77b80868cbda79d729234f396d2194d3d8"}, - {file = "sigmf-1.7.2.tar.gz", hash = "sha256:5f80f7127539358c7528ccf26e0ac5b3c268ecaeb69a921542e8ff71d0c85346"}, + {file = "sigmf-1.8.0-py3-none-any.whl", hash = "sha256:f233ab04344fa3e42170926a646f7e53edd7edc65fcda42eb3d7efaf8a2e8263"}, + {file = "sigmf-1.8.0.tar.gz", hash = "sha256:91e10cb046499639e5f961d66a24c17a33ff76fc98df892eab0953cc9d659a50"}, ] [package.dependencies] +defusedxml = "*" jsonschema = "*" numpy = "*" @@ -3131,14 +3143,14 @@ files = [ [[package]] name = "tox" -version = "4.52.0" +version = "4.53.0" description = "tox is a generic virtualenv management and test command line tool" optional = false python-versions = ">=3.10" groups = ["test"] files = [ - {file = "tox-4.52.0-py3-none-any.whl", hash = "sha256:624d8ea4a8c6d5e8d168eedf0e318d736fb22e83ca83137d001ac65ffdec46fd"}, - {file = "tox-4.52.0.tar.gz", hash = "sha256:6054abf5c8b61d58776fbec991f9bf0d34bb883862beb93d2fe55601ef3977c9"}, + {file = "tox-4.53.0-py3-none-any.whl", hash = "sha256:cc4e716d18c4889aa179d785175c438fa60c35deef20ce689ec288d8fb656096"}, + {file = "tox-4.53.0.tar.gz", hash = "sha256:62c780e42f87d34ee60f2ea20342156253794fdcbd6885fd797d98ee05009f22"}, ] [package.dependencies] @@ -3149,7 +3161,7 @@ packaging = ">=26" platformdirs = ">=4.9.4" pluggy = ">=1.6" pyproject-api = ">=1.10" -python-discovery = ">=1.2.1" +python-discovery = ">=1.2.2" tomli = {version = ">=2.4", markers = "python_version < \"3.11\""} tomli-w = ">=1.2" typing-extensions = {version = ">=4.15", markers = "python_version < \"3.11\""} @@ -3188,14 +3200,14 @@ typing-extensions = ">=4.12.0" [[package]] name = "tzdata" -version = "2025.3" +version = "2026.1" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" groups = ["main"] files = [ - {file = "tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1"}, - {file = "tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7"}, + {file = "tzdata-2026.1-py2.py3-none-any.whl", hash = "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9"}, + {file = "tzdata-2026.1.tar.gz", hash = "sha256:67658a1903c75917309e753fdc349ac0efd8c27db7a0cb406a25be4840f87f98"}, ] [[package]] @@ -3218,14 +3230,14 @@ zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] [[package]] name = "uvicorn" -version = "0.42.0" +version = "0.44.0" description = "The lightning-fast ASGI server." optional = false python-versions = ">=3.10" groups = ["docs", "server", "test"] files = [ - {file = "uvicorn-0.42.0-py3-none-any.whl", hash = "sha256:96c30f5c7abe6f74ae8900a70e92b85ad6613b745d4879eb9b16ccad15645359"}, - {file = "uvicorn-0.42.0.tar.gz", hash = "sha256:9b1f190ce15a2dd22e7758651d9b6d12df09a13d51ba5bf4fc33c383a48e1775"}, + {file = "uvicorn-0.44.0-py3-none-any.whl", hash = "sha256:ce937c99a2cc70279556967274414c087888e8cec9f9c94644dfca11bd3ced89"}, + {file = "uvicorn-0.44.0.tar.gz", hash = "sha256:6c942071b68f07e178264b9152f1f16dfac5da85880c4ce06366a96d70d4f31e"}, ] [package.dependencies] @@ -3310,21 +3322,21 @@ test = ["aiohttp (>=3.10.5)", "flake8 (>=6.1,<7.0)", "mypy (>=0.800)", "psutil", [[package]] name = "virtualenv" -version = "21.2.0" +version = "21.2.4" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" groups = ["test"] files = [ - {file = "virtualenv-21.2.0-py3-none-any.whl", hash = "sha256:1bd755b504931164a5a496d217c014d098426cddc79363ad66ac78125f9d908f"}, - {file = "virtualenv-21.2.0.tar.gz", hash = "sha256:1720dc3a62ef5b443092e3f499228599045d7fea4c79199770499df8becf9098"}, + {file = "virtualenv-21.2.4-py3-none-any.whl", hash = "sha256:29d21e941795206138d0f22f4e45ff7050e5da6c6472299fb7103318763861ac"}, + {file = "virtualenv-21.2.4.tar.gz", hash = "sha256:b294ef68192638004d72524ce7ef303e9d0cf5a44c95ce2e54a7500a6381cada"}, ] [package.dependencies] distlib = ">=0.3.7,<1" filelock = {version = ">=3.24.2,<4", markers = "python_version >= \"3.10\""} platformdirs = ">=3.9.1,<5" -python-discovery = ">=1" +python-discovery = ">=1.2.2" typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.11\""} [[package]] diff --git a/pyproject.toml b/pyproject.toml index 8db3469..982fcac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -146,6 +146,11 @@ exclude = ''' [tool.pytest.ini_options] pythonpath = ["src"] +filterwarnings = [ + # FastAPI emits this internally when handling 422 responses; the constant + # is not yet renamed in the installed starlette version, so we can't migrate. + "ignore:'HTTP_422_UNPROCESSABLE_ENTITY' is deprecated:DeprecationWarning", +] [tool.isort] profile = "black" diff --git a/src/ria_toolkit_oss/agent.py b/src/ria_toolkit_oss/agent.py index 6e15eb1..776238a 100644 --- a/src/ria_toolkit_oss/agent.py +++ b/src/ria_toolkit_oss/agent.py @@ -93,16 +93,24 @@ class NodeAgent: name: str, sdr_device: str = "unknown", insecure: bool = False, + role: str = "general", + session_code: str | None = None, ) -> None: self.hub_url = hub_url.rstrip("/") self.api_key = api_key self.name = name self.sdr_device = sdr_device self.insecure = insecure + self.role = role + self.session_code = session_code self.node_id: str | None = None self._stop = threading.Event() + # ── TX state ──────────────────────────────────────────────────────── + self._tx_stop = threading.Event() + self._tx_thread: threading.Thread | None = None + # ── Inference state ───────────────────────────────────────────────── # Protected by _inf_lock for cross-thread model swaps. self._inf_lock = threading.Lock() @@ -172,19 +180,27 @@ class NodeAgent: capabilities = ["campaign"] if self._ort_available: capabilities.append("inference") - resp = self._post( - "/composer/nodes/register", - json={ - "name": self.name, - "sdr_device": self.sdr_device, - "ria_toolkit_version": self._ria_version, - "capabilities": capabilities, - }, - timeout=15, - ) + if self.role == "tx": + capabilities.append("transmit") + payload: dict = { + "name": self.name, + "sdr_device": self.sdr_device, + "ria_toolkit_version": self._ria_version, + "capabilities": capabilities, + "role": self.role, + } + if self.session_code: + payload["session_code"] = self.session_code + resp = self._post("/composer/nodes/register", json=payload, timeout=15) resp.raise_for_status() self.node_id = resp.json()["node_id"] - logger.info("Registered as %r (node_id=%s)", self.name, self.node_id) + logger.info( + "Registered as %r (node_id=%s, role=%s%s)", + self.name, + self.node_id, + self.role, + f", session_code={self.session_code!r}" if self.session_code else "", + ) def _deregister(self) -> None: if not self.node_id: @@ -269,6 +285,17 @@ class NodeAgent: self._stop_inference() elif command == "configure_inference": self._queue_sdr_config(cmd) + elif command == "start_transmit": + threading.Thread( + target=self._start_transmit, + args=(cmd,), + daemon=True, + name="ria-start-tx", + ).start() + elif command == "stop_transmit": + self._stop_transmit() + elif command == "configure_transmit": + logger.info("configure_transmit received — will apply on next step boundary") else: logger.warning("Unknown command %r — ignored", command) @@ -301,6 +328,58 @@ class NodeAgent: logger.error("Campaign %s failed: %s", campaign_id[:8], exc) self._report_campaign_status(campaign_id, "failed", error=str(exc)) + # ------------------------------------------------------------------ + # TX execution + # ------------------------------------------------------------------ + + def _start_transmit(self, cmd: dict) -> None: + """Execute a synthetic transmit campaign using TxExecutor. + + The command payload mirrors a TransmitterConfig dict with an optional + ``schedule`` of steps. Each step synthesises a signal and transmits it + via the local SDR in TX mode. + """ + try: + from ria_toolkit_oss.orchestration.tx_executor import TxExecutor + except ImportError as exc: + logger.error("start_transmit: TxExecutor not available: %s", exc) + return + + if self._tx_thread and self._tx_thread.is_alive(): + logger.warning("start_transmit: TX already running — ignoring duplicate command") + return + + self._tx_stop.clear() + campaign_id: str = cmd.get("campaign_id") or str(uuid.uuid4()) + executor = TxExecutor( + config=cmd, + sdr_device=self.sdr_device, + stop_event=self._tx_stop, + ) + self._tx_thread = threading.Thread( + target=self._run_tx_campaign, + args=(executor, campaign_id), + daemon=True, + name=f"tx-campaign-{campaign_id[:8]}", + ) + self._tx_thread.start() + + def _run_tx_campaign(self, executor: Any, campaign_id: str) -> None: + try: + executor.run() + logger.info("TX campaign %s completed", campaign_id[:8]) + self._report_campaign_status(campaign_id, "completed") + except Exception as exc: + logger.error("TX campaign %s failed: %s", campaign_id[:8], exc) + self._report_campaign_status(campaign_id, "failed", error=str(exc)) + + def _stop_transmit(self) -> None: + """Signal the TX loop to stop gracefully.""" + self._tx_stop.set() + if self._tx_thread and self._tx_thread.is_alive(): + self._tx_thread.join(timeout=5.0) + logger.info("TX stopped") + # ------------------------------------------------------------------ # Inference — model loading # ------------------------------------------------------------------ @@ -848,6 +927,25 @@ def main() -> None: choices=["DEBUG", "INFO", "WARNING", "ERROR"], help="Logging verbosity (default: INFO)", ) + parser.add_argument( + "--role", + default=None, + choices=["general", "rx", "tx"], + help=( + "Node role reported to the hub. " + "'tx' enables synthetic transmission commands. " + "Default: general" + ), + ) + parser.add_argument( + "--session-code", + default=None, + metavar="CODE", + help=( + "3-word session code to pair this TX agent with a waiting campaign, " + "e.g. 'amber-peak-transmit'. Supplied by the campaign UI." + ), + ) args = parser.parse_args() @@ -861,6 +959,8 @@ def main() -> None: device = args.device or cfg.get("device", "unknown") insecure = args.insecure if args.insecure is not None else cfg.get("insecure", False) log_level = args.log_level or cfg.get("log_level", "INFO") + role = args.role or cfg.get("role", "general") + session_code = args.session_code or cfg.get("session_code") if not hub: parser.error("--hub is required (or set 'hub' in the config file)") @@ -888,6 +988,8 @@ def main() -> None: name=name, sdr_device=device, insecure=insecure, + role=role, + session_code=session_code, ) agent.run() diff --git a/src/ria_toolkit_oss/orchestration/executor.py b/src/ria_toolkit_oss/orchestration/executor.py index 1bdd4d8..3467995 100644 --- a/src/ria_toolkit_oss/orchestration/executor.py +++ b/src/ria_toolkit_oss/orchestration/executor.py @@ -169,6 +169,21 @@ def _run_script(script: str, *args: str, timeout: float = 15.0) -> str: # --------------------------------------------------------------------------- +def _extract_tx_params(transmitter: TransmitterConfig) -> dict | None: + """Build a tx_params dict from a transmitter's signal config for SigMF labeling. + + For sdr_agent transmitters, returns the synthetic generation parameters + (modulation, order, symbol_rate, etc.) so recordings capture what was + transmitted. Returns None for control methods without signal-level params. + """ + sdr_agent_cfg = getattr(transmitter, "sdr_agent", None) + if not sdr_agent_cfg: + return None + # Extract known signal-level fields; ignore infra fields + _INFRA_KEYS = {"node_id", "session_code"} + return {k: v for k, v in sdr_agent_cfg.items() if k not in _INFRA_KEYS and v is not None} + + class CampaignExecutor: """Executes a :class:`CampaignConfig` end-to-end. @@ -369,6 +384,7 @@ class CampaignExecutor: step=step, capture_timestamp=capture_timestamp, campaign_name=self.config.name, + tx_params=_extract_tx_params(transmitter), ) # QA diff --git a/src/ria_toolkit_oss/orchestration/labeler.py b/src/ria_toolkit_oss/orchestration/labeler.py index efcb97a..709be3c 100644 --- a/src/ria_toolkit_oss/orchestration/labeler.py +++ b/src/ria_toolkit_oss/orchestration/labeler.py @@ -15,6 +15,7 @@ def label_recording( step: CaptureStep, capture_timestamp: float, campaign_name: Optional[str] = None, + tx_params: Optional[dict] = None, ) -> Recording: """Apply device identity and capture configuration labels to a recording's metadata. @@ -27,6 +28,9 @@ def label_recording( step: The capture step that was active during this recording. capture_timestamp: Unix timestamp (float) of when capture started. campaign_name: Optional campaign name for cross-recording reference. + tx_params: Optional dict of transmitter signal parameters (e.g. modulation, + order, symbol_rate) written as ``ria:tx_`` fields so downstream + training pipelines know what was transmitted into the recording. Returns: The same recording with updated metadata. @@ -57,6 +61,11 @@ def label_recording( if step.power_dbm is not None: recording.update_metadata("tx_power_dbm", step.power_dbm) + # Transmitter signal parameters (e.g. from sdr_agent synthetic generation) + if tx_params: + for key, value in tx_params.items(): + recording.update_metadata(f"tx_{key}", value) + return recording diff --git a/src/ria_toolkit_oss/orchestration/tx_executor.py b/src/ria_toolkit_oss/orchestration/tx_executor.py new file mode 100644 index 0000000..e666a1a --- /dev/null +++ b/src/ria_toolkit_oss/orchestration/tx_executor.py @@ -0,0 +1,253 @@ +"""TX campaign executor — synthesises and transmits signals via a local SDR. + +The TxExecutor receives a transmitter config dict (matching the +``sdr_agent`` control method's schema) and a step schedule, then for each +step builds a signal chain with the block generator and transmits it via +the local SDR device. + +Supported modulations (``modulation`` field in config): + BPSK, QPSK, 8PSK, 16QAM, 64QAM, 256QAM, FSK, OOK, GMSK, OQPSK + +Example config dict (matches CampaignConfig transmitter with +``control_method: sdr_agent``):: + + { + "id": "synthetic-tx", + "type": "sdr", + "control_method": "sdr_agent", + "sdr_agent": { + "modulation": "QPSK", + "order": 4, + "symbol_rate": 1000000, + "center_frequency": 0.0, + "filter": "rrc", + "rolloff": 0.35 + }, + "schedule": [ + {"label": "step1", "duration": 10, "power_dbm": -10} + ] + } +""" + +from __future__ import annotations + +import logging +import threading +import time +from typing import Any + +logger = logging.getLogger(__name__) + +# Mapping from modulation name → (PSK/QAM order, generator_type) +# 'psk' uses PSKGenerator, 'qam' uses QAMGenerator +_MOD_TABLE: dict[str, tuple[int, str]] = { + "BPSK": (1, "psk"), + "QPSK": (2, "psk"), + "8PSK": (3, "psk"), + "16QAM": (4, "qam"), + "64QAM": (6, "qam"), + "256QAM": (8, "qam"), +} + +_SPECIAL_MODS = {"FSK", "OOK", "GMSK", "OQPSK"} + + +class TxExecutor: + """Synthesise and transmit a signal campaign via a local SDR. + + Args: + config: Transmitter config dict (must have ``sdr_agent`` sub-dict with + modulation params, and ``schedule`` list of step dicts). + sdr_device: SDR device name to open in TX mode (e.g. "pluto", "usrp"). + stop_event: External event that aborts the TX loop mid-step. + """ + + def __init__( + self, + config: dict, + sdr_device: str = "unknown", + stop_event: threading.Event | None = None, + ) -> None: + self.config = config + self.sdr_device = sdr_device + self.stop_event = stop_event or threading.Event() + self._sdr: Any = None + + def run(self) -> None: + """Execute all steps in the schedule, transmitting for each step duration.""" + agent_cfg: dict = self.config.get("sdr_agent") or {} + schedule: list[dict] = self.config.get("schedule") or [] + + if not schedule: + logger.warning("TxExecutor: no schedule steps — nothing to transmit") + return + + modulation: str = agent_cfg.get("modulation", "QPSK").upper() + symbol_rate: float = float(agent_cfg.get("symbol_rate", 1e6)) + center_freq: float = float(agent_cfg.get("center_frequency", 0.0)) + filter_type: str = agent_cfg.get("filter", "rrc").lower() + rolloff: float = float(agent_cfg.get("rolloff", 0.35)) + + # Upsampling factor: samples_per_symbol, fixed at 8 for SDR compatibility. + sps = 8 + sample_rate = symbol_rate * sps + + self._init_sdr(sample_rate, center_freq) + try: + for step in schedule: + if self.stop_event.is_set(): + break + self._execute_step(step, modulation, sps, symbol_rate, filter_type, rolloff) + finally: + self._close_sdr() + + def _execute_step( + self, + step: dict, + modulation: str, + sps: int, + symbol_rate: float, + filter_type: str, + rolloff: float, + ) -> None: + duration: float = float(step.get("duration", 10.0)) + label: str = step.get("label", "step") + gain: float = float(step.get("power_dbm") or 0.0) + sample_rate = symbol_rate * sps + + logger.info( + "TX step '%s': %.0f s, %s @ %.3f MHz (sps=%d, filter=%s)", + label, duration, modulation, symbol_rate / 1e6, sps, filter_type, + ) + + num_samples = int(duration * sample_rate) + signal = self._synthesise(modulation, sps, num_samples, filter_type, rolloff) + + if self._sdr is not None: + try: + # Apply gain update if SDR supports it + if hasattr(self._sdr, "set_tx_gain"): + self._sdr.set_tx_gain(gain) + self._sdr.tx_recording(signal, tx_time=duration) + except Exception as exc: + logger.error("TX step '%s' SDR error: %s", label, exc) + else: + # No SDR available — simulate by sleeping for the step duration. + logger.warning( + "TX step '%s': no SDR — simulating %.0f s delay", label, duration + ) + self.stop_event.wait(timeout=duration) + + def _synthesise( + self, + modulation: str, + sps: int, + num_samples: int, + filter_type: str, + rolloff: float, + ): + """Build a block-generator chain and return IQ samples as a numpy array.""" + try: + import numpy as np + from ria_toolkit_oss.signal.block_generator import ( + BinarySource, + GMSKModulator, + Mapper, + OOKModulator, + OQPSKModulator, + RaisedCosineFilter, + RootRaisedCosineFilter, + Upsampling, + ) + from ria_toolkit_oss.signal.block_generator.continuous_modulation.fsk_modulator import ( + FSKModulator, + ) + except ImportError as exc: + raise RuntimeError(f"ria_toolkit_oss block generator not available: {exc}") from exc + + # ── Special modulations with their own source-connected modulator ── + if modulation in ("OOK", "GMSK", "OQPSK"): + src = BinarySource() + if modulation == "OOK": + mod = OOKModulator(src, samples_per_symbol=sps) + elif modulation == "GMSK": + mod = GMSKModulator(src, samples_per_symbol=sps) + else: + mod = OQPSKModulator(src, samples_per_symbol=sps) + recording = mod.record(num_samples) + flat = np.asarray(recording.data).flatten().astype(np.complex64) + if len(flat) < num_samples: + flat = np.tile(flat, num_samples // len(flat) + 1) + return flat[:num_samples] + + if modulation == "FSK": + symbol_rate = num_samples / sps + bits_per_sym = 1 # 2-FSK + num_bits = max(num_samples // sps, 128) * bits_per_sym + bits = BinarySource()((1, num_bits)) + mod = FSKModulator( + num_bits_per_symbol=bits_per_sym, + frequency_spacing=symbol_rate * 0.5, + symbol_duration=1.0 / max(symbol_rate, 1.0), + sampling_frequency=symbol_rate * sps, + ) + flat = np.asarray(mod(bits)).flatten().astype(np.complex64) + if len(flat) < num_samples: + flat = np.tile(flat, num_samples // len(flat) + 1) + return flat[:num_samples] + + # ── PSK / QAM via Mapper → Upsampling → pulse filter ────────────── + if modulation not in _MOD_TABLE: + logger.warning("Unknown modulation %r — defaulting to QPSK", modulation) + modulation = "QPSK" + + bits_per_sym, gen_type = _MOD_TABLE[modulation] + mod_family = "QAM" if gen_type == "qam" else "PSK" + + source = BinarySource() + mapper = Mapper(constellation_type=mod_family, num_bits_per_symbol=bits_per_sym) + upsampler = Upsampling(factor=sps) + + mapper.connect_input([source]) + upsampler.connect_input([mapper]) + + if filter_type in ("rrc",): + pulse_filter = RootRaisedCosineFilter(span_in_symbols=6, upsampling_factor=sps, beta=rolloff) + pulse_filter.connect_input([upsampler]) + recording = pulse_filter.record(num_samples) + elif filter_type in ("rc",): + pulse_filter = RaisedCosineFilter(span_in_symbols=6, upsampling_factor=sps, beta=rolloff) + pulse_filter.connect_input([upsampler]) + recording = pulse_filter.record(num_samples) + else: + # "none", "rect", "gaussian" — use upsampler output directly + recording = upsampler.record(num_samples) + + flat = np.asarray(recording.data).flatten().astype(np.complex64) + if len(flat) < num_samples: + flat = np.tile(flat, num_samples // len(flat) + 1) + return flat[:num_samples] + + def _init_sdr(self, sample_rate: float, center_freq: float) -> None: + try: + from ria_toolkit_oss.sdr import get_sdr_device + self._sdr = get_sdr_device(self.sdr_device) + self._sdr.init_tx( + sample_rate=sample_rate, + center_frequency=center_freq, + gain=0, + channel=0, + gain_mode="manual", + ) + logger.info("TX SDR initialised: %s @ %.3f MHz, %.1f Msps", self.sdr_device, center_freq / 1e6, sample_rate / 1e6) + except Exception as exc: + logger.warning("TX SDR init failed (%s) — will simulate: %s", self.sdr_device, exc) + self._sdr = None + + def _close_sdr(self) -> None: + if self._sdr is not None: + try: + self._sdr.close() + except Exception as exc: + logger.debug("TX SDR close error: %s", exc) + self._sdr = None diff --git a/tests/orchestration/test_executor.py b/tests/orchestration/test_executor.py new file mode 100644 index 0000000..260c883 --- /dev/null +++ b/tests/orchestration/test_executor.py @@ -0,0 +1,295 @@ +"""Tests for orchestration executor — StepResult, CampaignResult, _run_script, _extract_tx_params.""" + +from __future__ import annotations + +import json +import stat +import threading +from types import SimpleNamespace + +import pytest + +from ria_toolkit_oss.orchestration.executor import ( + CampaignResult, + StepResult, + _extract_tx_params, + _run_script, +) +from ria_toolkit_oss.orchestration.qa import QAResult + + +def _ok_qa() -> QAResult: + return QAResult(passed=True, flagged=False, snr_db=20.0, duration_s=1.0) + + +def _flagged_qa() -> QAResult: + return QAResult(passed=True, flagged=True, snr_db=5.0, duration_s=1.0, issues=["low SNR"]) + + +def _failed_qa() -> QAResult: + return QAResult(passed=False, flagged=True, snr_db=0.0, duration_s=0.0, issues=["no signal"]) + + +# --------------------------------------------------------------------------- +# StepResult +# --------------------------------------------------------------------------- + + +class TestStepResult: + def test_ok_true_when_no_error_and_qa_passed(self): + r = StepResult( + transmitter_id="tx1", + step_label="step1", + output_path="/out/rec.sigmf-data", + qa=_ok_qa(), + capture_timestamp=0.0, + ) + assert r.ok is True + + def test_ok_false_when_error_set(self): + r = StepResult( + transmitter_id="tx1", + step_label="step1", + output_path=None, + qa=_ok_qa(), + capture_timestamp=0.0, + error="SDR failed", + ) + assert r.ok is False + + def test_ok_false_when_qa_not_passed(self): + r = StepResult( + transmitter_id="tx1", + step_label="step1", + output_path="/out", + qa=_failed_qa(), + capture_timestamp=0.0, + ) + assert r.ok is False + + def test_to_dict_contains_required_keys(self): + r = StepResult( + transmitter_id="tx1", + step_label="step1", + output_path="/out/rec.sigmf-data", + qa=_ok_qa(), + capture_timestamp=1234.5, + ) + d = r.to_dict() + assert d["transmitter_id"] == "tx1" + assert d["step_label"] == "step1" + assert d["output_path"] == "/out/rec.sigmf-data" + assert d["capture_timestamp"] == pytest.approx(1234.5) + assert d["error"] is None + assert d["qa"]["passed"] is True + + def test_to_dict_includes_error_when_set(self): + r = StepResult( + transmitter_id="tx1", + step_label="step1", + output_path=None, + qa=_failed_qa(), + capture_timestamp=0.0, + error="disk full", + ) + assert r.to_dict()["error"] == "disk full" + + +# --------------------------------------------------------------------------- +# CampaignResult +# --------------------------------------------------------------------------- + + +class TestCampaignResult: + def _make(self, steps: list) -> CampaignResult: + r = CampaignResult(campaign_name="test_campaign") + r.steps = steps + r.end_time = r.start_time + 5.0 + return r + + def test_total_steps(self): + r = self._make([ + StepResult("tx1", "s1", "/out", _ok_qa(), 0.0), + StepResult("tx1", "s2", "/out", _ok_qa(), 0.0), + ]) + assert r.total_steps == 2 + + def test_passed_count(self): + r = self._make([ + StepResult("tx1", "s1", "/out", _ok_qa(), 0.0), + StepResult("tx1", "s2", "/out", _failed_qa(), 0.0), + ]) + assert r.passed == 1 + + def test_failed_count(self): + r = self._make([ + StepResult("tx1", "s1", "/out", _ok_qa(), 0.0), + StepResult("tx1", "s2", "/out", _failed_qa(), 0.0), + ]) + assert r.failed == 1 + + def test_flagged_count(self): + r = self._make([ + StepResult("tx1", "s1", "/out", _ok_qa(), 0.0), + StepResult("tx1", "s2", "/out", _flagged_qa(), 0.0), + ]) + assert r.flagged == 1 + + def test_error_step_counts_as_failed_not_passed(self): + r = self._make([ + StepResult("tx1", "s1", None, _ok_qa(), 0.0, error="disk full"), + ]) + assert r.failed == 1 + assert r.passed == 0 + + def test_duration_s_from_end_time(self): + r = CampaignResult(campaign_name="c") + r.start_time = 100.0 + r.end_time = 115.0 + assert r.duration_s == pytest.approx(15.0) + + def test_to_dict_structure(self): + r = self._make([StepResult("tx1", "s1", "/out", _ok_qa(), 0.0)]) + d = r.to_dict() + assert d["campaign_name"] == "test_campaign" + assert d["total_steps"] == 1 + assert d["passed"] == 1 + assert len(d["steps"]) == 1 + + def test_write_report(self, tmp_path): + r = self._make([StepResult("tx1", "s1", "/out", _ok_qa(), 0.0)]) + out = tmp_path / "report.json" + r.write_report(str(out)) + assert out.exists() + data = json.loads(out.read_text()) + assert data["campaign_name"] == "test_campaign" + + def test_write_report_creates_nested_dirs(self, tmp_path): + r = self._make([]) + out = tmp_path / "nested" / "deep" / "report.json" + r.write_report(str(out)) + assert out.exists() + + +# --------------------------------------------------------------------------- +# _run_script +# --------------------------------------------------------------------------- + + +class TestRunScript: + def _script(self, tmp_path, body: str) -> str: + s = tmp_path / "script.sh" + s.write_text("#!/bin/sh\n" + body) + s.chmod(s.stat().st_mode | stat.S_IEXEC) + return str(s) + + def test_returns_stdout(self, tmp_path): + out = _run_script(self._script(tmp_path, 'echo "hello world"')) + assert out == "hello world" + + def test_passes_args_to_script(self, tmp_path): + out = _run_script(self._script(tmp_path, 'echo "$1 $2"'), "configure", "arg2") + assert "configure" in out + + def test_raises_on_nonzero_exit(self, tmp_path): + with pytest.raises(RuntimeError, match="exited 1"): + _run_script(self._script(tmp_path, "exit 1")) + + def test_raises_on_relative_path(self): + with pytest.raises(RuntimeError, match="absolute"): + _run_script("relative/script.sh") + + def test_raises_on_missing_file(self, tmp_path): + with pytest.raises(RuntimeError): + _run_script(str(tmp_path / "nonexistent.sh")) + + def test_raises_on_timeout(self, tmp_path): + with pytest.raises(RuntimeError, match="timed out"): + _run_script(self._script(tmp_path, "sleep 60"), timeout=0.1) + + def test_stderr_included_in_error_message(self, tmp_path): + with pytest.raises(RuntimeError) as exc_info: + _run_script(self._script(tmp_path, "echo 'bad thing' >&2; exit 1")) + assert "bad thing" in str(exc_info.value) + + +# --------------------------------------------------------------------------- +# _extract_tx_params +# --------------------------------------------------------------------------- + + +class TestExtractTxParams: + def test_returns_none_when_no_sdr_agent_attribute(self): + tx = SimpleNamespace() + assert _extract_tx_params(tx) is None + + def test_returns_none_when_sdr_agent_is_none(self): + tx = SimpleNamespace(sdr_agent=None) + assert _extract_tx_params(tx) is None + + def test_returns_none_when_sdr_agent_is_empty_dict(self): + tx = SimpleNamespace(sdr_agent={}) + assert _extract_tx_params(tx) is None + + def test_returns_signal_params(self): + tx = SimpleNamespace(sdr_agent={ + "modulation": "QPSK", + "symbol_rate": 1e6, + "center_frequency": 2.4e9, + }) + result = _extract_tx_params(tx) + assert result == {"modulation": "QPSK", "symbol_rate": 1e6, "center_frequency": 2.4e9} + + def test_strips_infra_key_node_id(self): + tx = SimpleNamespace(sdr_agent={ + "modulation": "BPSK", + "node_id": "node_abc123", + }) + result = _extract_tx_params(tx) + assert "node_id" not in result + assert result == {"modulation": "BPSK"} + + def test_strips_infra_key_session_code(self): + tx = SimpleNamespace(sdr_agent={ + "modulation": "FSK", + "session_code": "amber-peak-transmit", + }) + result = _extract_tx_params(tx) + assert "session_code" not in result + + def test_strips_none_values(self): + tx = SimpleNamespace(sdr_agent={ + "modulation": "QPSK", + "order": None, + "rolloff": 0.35, + }) + result = _extract_tx_params(tx) + assert "order" not in result + assert result == {"modulation": "QPSK", "rolloff": 0.35} + + def test_does_not_mutate_source_dict(self): + cfg = {"modulation": "QPSK", "node_id": "nid", "session_code": "code"} + tx = SimpleNamespace(sdr_agent=cfg) + _extract_tx_params(tx) + assert "node_id" in cfg + + def test_full_sdr_agent_config(self): + tx = SimpleNamespace(sdr_agent={ + "modulation": "16QAM", + "order": 4, + "symbol_rate": 5e6, + "center_frequency": 915e6, + "filter": "rrc", + "rolloff": 0.35, + "node_id": "node_xyz", + "session_code": "some-code", + }) + result = _extract_tx_params(tx) + assert result == { + "modulation": "16QAM", + "order": 4, + "symbol_rate": 5e6, + "center_frequency": 915e6, + "filter": "rrc", + "rolloff": 0.35, + } diff --git a/tests/orchestration/test_labeler.py b/tests/orchestration/test_labeler.py index 37b370d..670a9dc 100644 --- a/tests/orchestration/test_labeler.py +++ b/tests/orchestration/test_labeler.py @@ -109,6 +109,42 @@ class TestLabelRecording: result = label_recording(rec, "iphone13_001", _wifi_step(), time.time()) assert result is rec + def test_tx_params_none_by_default(self): + rec = label_recording(_simple_recording(), "iphone13_001", _wifi_step(), time.time()) + tx_keys = [k for k in rec.metadata if k.startswith("tx_")] + assert tx_keys == [] + + def test_tx_params_written_as_tx_prefix_keys(self): + params = {"modulation": "QPSK", "symbol_rate": 1e6} + rec = label_recording( + _simple_recording(), "dev", _wifi_step(), time.time(), tx_params=params + ) + assert rec.metadata["tx_modulation"] == "QPSK" + assert rec.metadata["tx_symbol_rate"] == pytest.approx(1e6) + + def test_tx_params_multiple_fields(self): + params = { + "modulation": "16QAM", + "order": 4, + "symbol_rate": 5e6, + "center_frequency": 915e6, + "filter": "rrc", + "rolloff": 0.35, + } + rec = label_recording( + _simple_recording(), "dev", _wifi_step(), time.time(), tx_params=params + ) + for k, v in params.items(): + assert f"tx_{k}" in rec.metadata + assert rec.metadata[f"tx_{k}"] == pytest.approx(v) if isinstance(v, float) else rec.metadata[f"tx_{k}"] == v + + def test_tx_params_empty_dict_writes_nothing(self): + rec = label_recording( + _simple_recording(), "dev", _wifi_step(), time.time(), tx_params={} + ) + tx_keys = [k for k in rec.metadata if k.startswith("tx_") and k != "tx_power_dbm"] + assert tx_keys == [] + # --------------------------------------------------------------------------- # build_output_filename diff --git a/tests/orchestration/test_tx_executor.py b/tests/orchestration/test_tx_executor.py new file mode 100644 index 0000000..9a03e6b --- /dev/null +++ b/tests/orchestration/test_tx_executor.py @@ -0,0 +1,155 @@ +"""Tests for TxExecutor — signal synthesis and step execution.""" + +from __future__ import annotations + +import threading +from unittest.mock import MagicMock, patch + +import numpy as np +import pytest + +from ria_toolkit_oss.orchestration.tx_executor import TxExecutor + + +def _cfg(modulation="QPSK", symbol_rate=100_000, steps=None): + return { + "id": "test-tx", + "type": "sdr", + "control_method": "sdr_agent", + "sdr_agent": { + "modulation": modulation, + "symbol_rate": symbol_rate, + "center_frequency": 0.0, + "filter": "rrc", + "rolloff": 0.35, + }, + "schedule": steps or [{"label": "step1", "duration": 0.001, "power_dbm": -10}], + } + + +# --------------------------------------------------------------------------- +# Initialisation +# --------------------------------------------------------------------------- + + +class TestTxExecutorInit: + def test_stores_sdr_device(self): + ex = TxExecutor(_cfg(), sdr_device="pluto") + assert ex.sdr_device == "pluto" + + def test_stop_event_created_when_not_supplied(self): + ex = TxExecutor(_cfg()) + assert isinstance(ex.stop_event, threading.Event) + assert not ex.stop_event.is_set() + + def test_accepts_external_stop_event(self): + ev = threading.Event() + ex = TxExecutor(_cfg(), stop_event=ev) + assert ex.stop_event is ev + + +# --------------------------------------------------------------------------- +# run() — schedule iteration +# --------------------------------------------------------------------------- + + +class TestTxExecutorRun: + def test_empty_schedule_returns_immediately(self): + cfg = _cfg(steps=[]) + ex = TxExecutor(cfg) + ex.run() # must not raise or block + + def test_pre_set_stop_event_skips_all_steps(self): + ev = threading.Event() + ev.set() + ex = TxExecutor(_cfg(), stop_event=ev) + # If stop was set, _execute_step should never be called. + # run() should return cleanly without attempting synthesis. + ex.run() + + def test_no_sdr_falls_back_to_simulation(self, monkeypatch): + """Without SDR hardware TxExecutor simulates by calling stop_event.wait.""" + cfg = _cfg(steps=[{"label": "s", "duration": 0.001, "power_dbm": 0}]) + waited = [] + real_ev = threading.Event() + + orig_wait = real_ev.wait + + def _fake_wait(timeout=None): + waited.append(timeout) + return False + + monkeypatch.setattr(real_ev, "wait", _fake_wait) + + # Patch SDR init to always fail (forces simulation path) + with patch.object(TxExecutor, "_init_sdr", lambda self, *a, **kw: setattr(self, "_sdr", None)): + ex = TxExecutor(cfg, sdr_device="nonexistent_xyz", stop_event=real_ev) + ex.run() + + assert len(waited) >= 1, "expected stop_event.wait to be called for simulation" + + +# --------------------------------------------------------------------------- +# _synthesise — all modulation types and filter types +# --------------------------------------------------------------------------- + + +class TestSynthesise: + @pytest.fixture(autouse=True) + def _ex(self): + self.ex = TxExecutor(_cfg()) + + def _synth(self, mod, num_samples=256): + return self.ex._synthesise(mod, sps=4, num_samples=num_samples, filter_type="rrc", rolloff=0.35) + + @pytest.mark.parametrize("mod", ["BPSK", "QPSK", "8PSK", "16QAM", "64QAM", "256QAM"]) + def test_psk_qam_returns_complex64_array(self, mod): + sig = self._synth(mod) + assert sig.dtype == np.complex64 + assert len(sig) == 256 + + def test_fsk_returns_correct_length(self): + sig = self._synth("FSK") + assert len(sig) == 256 + + def test_ook_returns_correct_length(self): + sig = self._synth("OOK") + assert len(sig) == 256 + + def test_gmsk_returns_correct_length(self): + sig = self._synth("GMSK") + assert len(sig) == 256 + + def test_oqpsk_returns_correct_length(self): + sig = self._synth("OQPSK") + assert len(sig) == 256 + + @pytest.mark.parametrize("mod", ["BPSK", "QPSK", "16QAM", "FSK", "OOK", "GMSK"]) + def test_samples_are_finite(self, mod): + sig = self._synth(mod) + assert np.all(np.isfinite(sig.real)), f"{mod}: non-finite real samples" + assert np.all(np.isfinite(sig.imag)), f"{mod}: non-finite imag samples" + + def test_unknown_modulation_defaults_to_qpsk(self): + sig = self._synth("UNKNOWN_MOD_XYZ") + assert len(sig) == 256 + assert sig.dtype == np.complex64 + + @pytest.mark.parametrize("filter_type", ["rrc", "rc", "gaussian", "rect", "none"]) + def test_all_filter_types(self, filter_type): + sig = self.ex._synthesise("QPSK", sps=4, num_samples=128, filter_type=filter_type, rolloff=0.35) + assert len(sig) == 128 + + @pytest.mark.parametrize("n", [64, 128, 512, 1024]) + def test_output_length_matches_requested_samples(self, n): + sig = self._synth("QPSK", num_samples=n) + assert len(sig) == n + + def test_bpsk_output_is_complex_not_real(self): + sig = self._synth("BPSK") + # complex64 always has imag part; just check dtype + assert sig.dtype == np.complex64 + + def test_256qam_correct_length(self): + sig = self._synth("256QAM") + assert len(sig) == 256 diff --git a/tests/ria_toolkit_oss_cli/test_generate.py b/tests/ria_toolkit_oss_cli/test_generate.py index 68d252c..8915037 100644 --- a/tests/ria_toolkit_oss_cli/test_generate.py +++ b/tests/ria_toolkit_oss_cli/test_generate.py @@ -189,6 +189,8 @@ class TestNoiseCommand: "10000", "--noise-type", "gaussian", + "--power", + "0.01", "--output", output, "-q", @@ -234,7 +236,7 @@ class TestNoiseCommand: "--num-samples", "10000", "--power", - "0.5", + "0.01", "--output", output, "-q", diff --git a/tests/test_agent.py b/tests/test_agent.py new file mode 100644 index 0000000..ce7ac9c --- /dev/null +++ b/tests/test_agent.py @@ -0,0 +1,249 @@ +"""Tests for NodeAgent — TX role, session code, and TX command dispatch.""" + +from __future__ import annotations + +import threading +import time +from unittest.mock import MagicMock, patch + +import pytest + +from ria_toolkit_oss.agent import NodeAgent + + +def _agent(role="general", session_code=None, **kwargs): + return NodeAgent( + hub_url="http://hub.test", + api_key="test-key", + name="test-node", + sdr_device="mock", + role=role, + session_code=session_code, + **kwargs, + ) + + +def _mock_register(agent, node_id="node_abc123"): + """Patch _post so _register() returns a fake node_id response.""" + resp = MagicMock() + resp.json.return_value = {"node_id": node_id} + resp.raise_for_status.return_value = None + agent._post = MagicMock(return_value=resp) + return agent._post + + +# --------------------------------------------------------------------------- +# Initialisation +# --------------------------------------------------------------------------- + + +class TestNodeAgentInit: + def test_stores_role_general(self): + assert _agent(role="general").role == "general" + + def test_stores_role_tx(self): + assert _agent(role="tx").role == "tx" + + def test_stores_role_rx(self): + assert _agent(role="rx").role == "rx" + + def test_session_code_stored(self): + assert _agent(session_code="amber-peak-transmit").session_code == "amber-peak-transmit" + + def test_session_code_none_by_default(self): + assert _agent().session_code is None + + def test_tx_stop_event_created(self): + a = _agent() + assert isinstance(a._tx_stop, threading.Event) + + def test_tx_thread_none_initially(self): + assert _agent()._tx_thread is None + + def test_hub_url_trailing_slash_stripped(self): + a = NodeAgent(hub_url="http://hub.test/", api_key="k", name="n") + assert a.hub_url == "http://hub.test" + + +# --------------------------------------------------------------------------- +# _register payload +# --------------------------------------------------------------------------- + + +class TestNodeAgentRegisterPayload: + def _payload(self, agent): + post = _mock_register(agent) + agent._register() + _, kwargs = post.call_args + return kwargs["json"] + + def test_general_role_in_payload(self): + payload = self._payload(_agent(role="general")) + assert payload["role"] == "general" + + def test_tx_role_in_payload(self): + payload = self._payload(_agent(role="tx")) + assert payload["role"] == "tx" + + def test_tx_role_adds_transmit_capability(self): + payload = self._payload(_agent(role="tx")) + assert "transmit" in payload["capabilities"] + + def test_general_role_omits_transmit_capability(self): + payload = self._payload(_agent(role="general")) + assert "transmit" not in payload.get("capabilities", []) + + def test_session_code_included_when_set(self): + payload = self._payload(_agent(role="tx", session_code="amber-peak-transmit")) + assert payload["session_code"] == "amber-peak-transmit" + + def test_session_code_omitted_when_none(self): + payload = self._payload(_agent()) + assert "session_code" not in payload + + def test_register_stores_returned_node_id(self): + a = _agent() + _mock_register(a, node_id="node_xyz999") + a._register() + assert a.node_id == "node_xyz999" + + def test_name_in_payload(self): + a = NodeAgent(hub_url="http://h", api_key="k", name="my-bench") + _mock_register(a) + a._register() + _, kwargs = a._post.call_args + assert kwargs["json"]["name"] == "my-bench" + + def test_sdr_device_in_payload(self): + a = _agent() + post = _mock_register(a) + a._register() + _, kwargs = post.call_args + assert kwargs["json"]["sdr_device"] == "mock" + + def test_campaign_capability_always_present(self): + for role in ("general", "rx", "tx"): + a = _agent(role=role) + payload = self._payload(a) + assert "campaign" in payload["capabilities"] + + +# --------------------------------------------------------------------------- +# _dispatch — TX commands +# --------------------------------------------------------------------------- + + +class TestNodeAgentDispatch: + def _make_agent(self): + a = _agent(role="tx") + a.node_id = "node_abc" + a._report_campaign_status = MagicMock() + return a + + def test_start_transmit_spawns_thread(self): + a = self._make_agent() + done = threading.Event() + + class _FakeExecutor: + def run(self_): + done.wait(timeout=2) + + with patch("ria_toolkit_oss.orchestration.tx_executor.TxExecutor", return_value=_FakeExecutor()): + a._dispatch({"command": "start_transmit", "sdr_agent": {}, "schedule": []}) + time.sleep(0.05) + assert a._tx_thread is not None + done.set() + + def test_start_transmit_clears_stop_event(self): + a = self._make_agent() + a._tx_stop.set() # pre-set + + done = threading.Event() + + class _FakeExecutor: + def run(self_): + done.wait(timeout=2) + + with patch("ria_toolkit_oss.orchestration.tx_executor.TxExecutor", return_value=_FakeExecutor()): + a._dispatch({"command": "start_transmit", "sdr_agent": {}, "schedule": []}) + time.sleep(0.05) + assert not a._tx_stop.is_set() + done.set() + + def test_stop_transmit_sets_stop_event(self): + a = self._make_agent() + a._dispatch({"command": "stop_transmit"}) + assert a._tx_stop.is_set() + + def test_configure_transmit_does_not_raise(self): + a = self._make_agent() + a._dispatch({"command": "configure_transmit", "modulation": "BPSK"}) + + def test_unknown_command_is_ignored(self): + a = self._make_agent() + a._dispatch({"command": "frobnicate_xyz"}) + + def test_duplicate_start_transmit_ignored_while_running(self): + a = self._make_agent() + done = threading.Event() + run_calls = [] + + class _FakeExecutor: + def run(self_): + run_calls.append(1) + done.wait(timeout=2) + + with patch("ria_toolkit_oss.orchestration.tx_executor.TxExecutor", return_value=_FakeExecutor()): + a._dispatch({"command": "start_transmit"}) + time.sleep(0.05) + a._dispatch({"command": "start_transmit"}) # second while first alive + done.set() + time.sleep(0.05) + + assert len(run_calls) == 1 + + def test_run_campaign_dispatched_in_thread(self): + a = self._make_agent() + done = threading.Event() + + with patch("ria_toolkit_oss.agent.NodeAgent._run_campaign") as mock_run: + mock_run.side_effect = lambda *_: done.set() + a._dispatch({"command": "run_campaign", "campaign_id": "c1", "payload": {}}) + done.wait(timeout=2) + assert mock_run.called + + +# --------------------------------------------------------------------------- +# _stop_transmit +# --------------------------------------------------------------------------- + + +class TestStopTransmit: + def test_no_thread_noop(self): + a = _agent() + a._stop_transmit() # must not raise + + def test_sets_stop_event(self): + a = _agent() + a._stop_transmit() + assert a._tx_stop.is_set() + + def test_joins_live_thread(self): + a = _agent() + finished = threading.Event() + unblock = threading.Event() + + def _task(): + unblock.wait(timeout=2) + finished.set() + + t = threading.Thread(target=_task, daemon=True) + t.start() + a._tx_thread = t + + # Signal stop and trigger thread exit + a._tx_stop.set() + unblock.set() + a._stop_transmit() + + assert not t.is_alive() From c27a5944c7ee852794bbe6823610f356e6ca4535 Mon Sep 17 00:00:00 2001 From: ben Date: Mon, 20 Apr 2026 16:49:52 -0400 Subject: [PATCH 02/10] formats --- src/ria_toolkit_oss/agent/legacy_executor.py | 6 +- .../orchestration/tx_executor.py | 28 +++-- tests/orchestration/test_executor.py | 115 ++++++++++-------- tests/orchestration/test_labeler.py | 16 +-- tests/orchestration/test_tx_executor.py | 4 +- tests/test_agent.py | 2 - 6 files changed, 92 insertions(+), 79 deletions(-) diff --git a/src/ria_toolkit_oss/agent/legacy_executor.py b/src/ria_toolkit_oss/agent/legacy_executor.py index 776238a..d8a56d6 100644 --- a/src/ria_toolkit_oss/agent/legacy_executor.py +++ b/src/ria_toolkit_oss/agent/legacy_executor.py @@ -931,11 +931,7 @@ def main() -> None: "--role", default=None, choices=["general", "rx", "tx"], - help=( - "Node role reported to the hub. " - "'tx' enables synthetic transmission commands. " - "Default: general" - ), + help=("Node role reported to the hub. " "'tx' enables synthetic transmission commands. " "Default: general"), ) parser.add_argument( "--session-code", diff --git a/src/ria_toolkit_oss/orchestration/tx_executor.py b/src/ria_toolkit_oss/orchestration/tx_executor.py index e666a1a..6ae32b1 100644 --- a/src/ria_toolkit_oss/orchestration/tx_executor.py +++ b/src/ria_toolkit_oss/orchestration/tx_executor.py @@ -33,7 +33,6 @@ from __future__ import annotations import logging import threading -import time from typing import Any logger = logging.getLogger(__name__) @@ -41,11 +40,11 @@ logger = logging.getLogger(__name__) # Mapping from modulation name → (PSK/QAM order, generator_type) # 'psk' uses PSKGenerator, 'qam' uses QAMGenerator _MOD_TABLE: dict[str, tuple[int, str]] = { - "BPSK": (1, "psk"), - "QPSK": (2, "psk"), - "8PSK": (3, "psk"), - "16QAM": (4, "qam"), - "64QAM": (6, "qam"), + "BPSK": (1, "psk"), + "QPSK": (2, "psk"), + "8PSK": (3, "psk"), + "16QAM": (4, "qam"), + "64QAM": (6, "qam"), "256QAM": (8, "qam"), } @@ -117,7 +116,12 @@ class TxExecutor: logger.info( "TX step '%s': %.0f s, %s @ %.3f MHz (sps=%d, filter=%s)", - label, duration, modulation, symbol_rate / 1e6, sps, filter_type, + label, + duration, + modulation, + symbol_rate / 1e6, + sps, + filter_type, ) num_samples = int(duration * sample_rate) @@ -133,9 +137,7 @@ class TxExecutor: logger.error("TX step '%s' SDR error: %s", label, exc) else: # No SDR available — simulate by sleeping for the step duration. - logger.warning( - "TX step '%s': no SDR — simulating %.0f s delay", label, duration - ) + logger.warning("TX step '%s': no SDR — simulating %.0f s delay", label, duration) self.stop_event.wait(timeout=duration) def _synthesise( @@ -149,6 +151,7 @@ class TxExecutor: """Build a block-generator chain and return IQ samples as a numpy array.""" try: import numpy as np + from ria_toolkit_oss.signal.block_generator import ( BinarySource, GMSKModulator, @@ -231,6 +234,7 @@ class TxExecutor: def _init_sdr(self, sample_rate: float, center_freq: float) -> None: try: from ria_toolkit_oss.sdr import get_sdr_device + self._sdr = get_sdr_device(self.sdr_device) self._sdr.init_tx( sample_rate=sample_rate, @@ -239,7 +243,9 @@ class TxExecutor: channel=0, gain_mode="manual", ) - logger.info("TX SDR initialised: %s @ %.3f MHz, %.1f Msps", self.sdr_device, center_freq / 1e6, sample_rate / 1e6) + logger.info( + "TX SDR initialised: %s @ %.3f MHz, %.1f Msps", self.sdr_device, center_freq / 1e6, sample_rate / 1e6 + ) except Exception as exc: logger.warning("TX SDR init failed (%s) — will simulate: %s", self.sdr_device, exc) self._sdr = None diff --git a/tests/orchestration/test_executor.py b/tests/orchestration/test_executor.py index 260c883..7aba499 100644 --- a/tests/orchestration/test_executor.py +++ b/tests/orchestration/test_executor.py @@ -4,7 +4,6 @@ from __future__ import annotations import json import stat -import threading from types import SimpleNamespace import pytest @@ -108,37 +107,47 @@ class TestCampaignResult: return r def test_total_steps(self): - r = self._make([ - StepResult("tx1", "s1", "/out", _ok_qa(), 0.0), - StepResult("tx1", "s2", "/out", _ok_qa(), 0.0), - ]) + r = self._make( + [ + StepResult("tx1", "s1", "/out", _ok_qa(), 0.0), + StepResult("tx1", "s2", "/out", _ok_qa(), 0.0), + ] + ) assert r.total_steps == 2 def test_passed_count(self): - r = self._make([ - StepResult("tx1", "s1", "/out", _ok_qa(), 0.0), - StepResult("tx1", "s2", "/out", _failed_qa(), 0.0), - ]) + r = self._make( + [ + StepResult("tx1", "s1", "/out", _ok_qa(), 0.0), + StepResult("tx1", "s2", "/out", _failed_qa(), 0.0), + ] + ) assert r.passed == 1 def test_failed_count(self): - r = self._make([ - StepResult("tx1", "s1", "/out", _ok_qa(), 0.0), - StepResult("tx1", "s2", "/out", _failed_qa(), 0.0), - ]) + r = self._make( + [ + StepResult("tx1", "s1", "/out", _ok_qa(), 0.0), + StepResult("tx1", "s2", "/out", _failed_qa(), 0.0), + ] + ) assert r.failed == 1 def test_flagged_count(self): - r = self._make([ - StepResult("tx1", "s1", "/out", _ok_qa(), 0.0), - StepResult("tx1", "s2", "/out", _flagged_qa(), 0.0), - ]) + r = self._make( + [ + StepResult("tx1", "s1", "/out", _ok_qa(), 0.0), + StepResult("tx1", "s2", "/out", _flagged_qa(), 0.0), + ] + ) assert r.flagged == 1 def test_error_step_counts_as_failed_not_passed(self): - r = self._make([ - StepResult("tx1", "s1", None, _ok_qa(), 0.0, error="disk full"), - ]) + r = self._make( + [ + StepResult("tx1", "s1", None, _ok_qa(), 0.0, error="disk full"), + ] + ) assert r.failed == 1 assert r.passed == 0 @@ -232,37 +241,45 @@ class TestExtractTxParams: assert _extract_tx_params(tx) is None def test_returns_signal_params(self): - tx = SimpleNamespace(sdr_agent={ - "modulation": "QPSK", - "symbol_rate": 1e6, - "center_frequency": 2.4e9, - }) + tx = SimpleNamespace( + sdr_agent={ + "modulation": "QPSK", + "symbol_rate": 1e6, + "center_frequency": 2.4e9, + } + ) result = _extract_tx_params(tx) assert result == {"modulation": "QPSK", "symbol_rate": 1e6, "center_frequency": 2.4e9} def test_strips_infra_key_node_id(self): - tx = SimpleNamespace(sdr_agent={ - "modulation": "BPSK", - "node_id": "node_abc123", - }) + tx = SimpleNamespace( + sdr_agent={ + "modulation": "BPSK", + "node_id": "node_abc123", + } + ) result = _extract_tx_params(tx) assert "node_id" not in result assert result == {"modulation": "BPSK"} def test_strips_infra_key_session_code(self): - tx = SimpleNamespace(sdr_agent={ - "modulation": "FSK", - "session_code": "amber-peak-transmit", - }) + tx = SimpleNamespace( + sdr_agent={ + "modulation": "FSK", + "session_code": "amber-peak-transmit", + } + ) result = _extract_tx_params(tx) assert "session_code" not in result def test_strips_none_values(self): - tx = SimpleNamespace(sdr_agent={ - "modulation": "QPSK", - "order": None, - "rolloff": 0.35, - }) + tx = SimpleNamespace( + sdr_agent={ + "modulation": "QPSK", + "order": None, + "rolloff": 0.35, + } + ) result = _extract_tx_params(tx) assert "order" not in result assert result == {"modulation": "QPSK", "rolloff": 0.35} @@ -274,16 +291,18 @@ class TestExtractTxParams: assert "node_id" in cfg def test_full_sdr_agent_config(self): - tx = SimpleNamespace(sdr_agent={ - "modulation": "16QAM", - "order": 4, - "symbol_rate": 5e6, - "center_frequency": 915e6, - "filter": "rrc", - "rolloff": 0.35, - "node_id": "node_xyz", - "session_code": "some-code", - }) + tx = SimpleNamespace( + sdr_agent={ + "modulation": "16QAM", + "order": 4, + "symbol_rate": 5e6, + "center_frequency": 915e6, + "filter": "rrc", + "rolloff": 0.35, + "node_id": "node_xyz", + "session_code": "some-code", + } + ) result = _extract_tx_params(tx) assert result == { "modulation": "16QAM", diff --git a/tests/orchestration/test_labeler.py b/tests/orchestration/test_labeler.py index 670a9dc..2e47739 100644 --- a/tests/orchestration/test_labeler.py +++ b/tests/orchestration/test_labeler.py @@ -116,9 +116,7 @@ class TestLabelRecording: def test_tx_params_written_as_tx_prefix_keys(self): params = {"modulation": "QPSK", "symbol_rate": 1e6} - rec = label_recording( - _simple_recording(), "dev", _wifi_step(), time.time(), tx_params=params - ) + rec = label_recording(_simple_recording(), "dev", _wifi_step(), time.time(), tx_params=params) assert rec.metadata["tx_modulation"] == "QPSK" assert rec.metadata["tx_symbol_rate"] == pytest.approx(1e6) @@ -131,17 +129,15 @@ class TestLabelRecording: "filter": "rrc", "rolloff": 0.35, } - rec = label_recording( - _simple_recording(), "dev", _wifi_step(), time.time(), tx_params=params - ) + rec = label_recording(_simple_recording(), "dev", _wifi_step(), time.time(), tx_params=params) for k, v in params.items(): assert f"tx_{k}" in rec.metadata - assert rec.metadata[f"tx_{k}"] == pytest.approx(v) if isinstance(v, float) else rec.metadata[f"tx_{k}"] == v + assert ( + rec.metadata[f"tx_{k}"] == pytest.approx(v) if isinstance(v, float) else rec.metadata[f"tx_{k}"] == v + ) def test_tx_params_empty_dict_writes_nothing(self): - rec = label_recording( - _simple_recording(), "dev", _wifi_step(), time.time(), tx_params={} - ) + rec = label_recording(_simple_recording(), "dev", _wifi_step(), time.time(), tx_params={}) tx_keys = [k for k in rec.metadata if k.startswith("tx_") and k != "tx_power_dbm"] assert tx_keys == [] diff --git a/tests/orchestration/test_tx_executor.py b/tests/orchestration/test_tx_executor.py index 9a03e6b..9d66850 100644 --- a/tests/orchestration/test_tx_executor.py +++ b/tests/orchestration/test_tx_executor.py @@ -3,7 +3,7 @@ from __future__ import annotations import threading -from unittest.mock import MagicMock, patch +from unittest.mock import patch import numpy as np import pytest @@ -73,8 +73,6 @@ class TestTxExecutorRun: waited = [] real_ev = threading.Event() - orig_wait = real_ev.wait - def _fake_wait(timeout=None): waited.append(timeout) return False diff --git a/tests/test_agent.py b/tests/test_agent.py index ce7ac9c..67991f9 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -6,8 +6,6 @@ import threading import time from unittest.mock import MagicMock, patch -import pytest - from ria_toolkit_oss.agent import NodeAgent From 4c2c9c028886712deaa0d37545634402ef8cdf58 Mon Sep 17 00:00:00 2001 From: ben Date: Tue, 21 Apr 2026 13:23:49 -0400 Subject: [PATCH 03/10] rx and tx test --- poetry.lock | 2 +- pyproject.toml | 2 +- src/ria_toolkit_oss/orchestration/campaign.py | 4 ++ src/ria_toolkit_oss/orchestration/executor.py | 38 +++++++++++++++++++ 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/poetry.lock b/poetry.lock index f235cb2..9d1e5fe 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3749,4 +3749,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.10" -content-hash = "ffde300b2fc93161d2279a6e2b899bc988d3b5eb3833135821830affc9a5fb62" +content-hash = "66c9adf647316db90f963da05e8a83574378bfa4db2c69ce751446b5ee7c408c" diff --git a/pyproject.toml b/pyproject.toml index 00784cc..48a9e1c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ dependencies = [ "pyyaml (>=6.0.3,<7.0.0)", "click (>=8.1.0,<9.0.0)", "matplotlib (>=3.8.0,<4.0.0)", - "paramiko (>=4.0.0)" + "paramiko (>=3.5.1)" ] # [project.optional-dependencies] Commented out to prevent Tox tests from failing diff --git a/src/ria_toolkit_oss/orchestration/campaign.py b/src/ria_toolkit_oss/orchestration/campaign.py index 027c33f..5fe1128 100644 --- a/src/ria_toolkit_oss/orchestration/campaign.py +++ b/src/ria_toolkit_oss/orchestration/campaign.py @@ -233,6 +233,9 @@ class TransmitterConfig: # For sdr_remote control — keys: host, ssh_user, ssh_key_path, device_type, device_id, zmq_port sdr_remote: Optional[dict] = None + # For sdr_agent control — keys: modulation, order, symbol_rate, center_frequency, filter, rolloff + sdr_agent: Optional[dict] = None + @classmethod def from_dict(cls, d: dict) -> "TransmitterConfig": schedule = [CaptureStep.from_dict(s) for s in d.get("schedule", [])] @@ -244,6 +247,7 @@ class TransmitterConfig: script=d.get("script"), device=d.get("device"), sdr_remote=d.get("sdr_remote"), + sdr_agent=d.get("sdr_agent"), ) diff --git a/src/ria_toolkit_oss/orchestration/executor.py b/src/ria_toolkit_oss/orchestration/executor.py index 3467995..445b16b 100644 --- a/src/ria_toolkit_oss/orchestration/executor.py +++ b/src/ria_toolkit_oss/orchestration/executor.py @@ -5,6 +5,7 @@ from __future__ import annotations import json import logging import subprocess +import threading import time from dataclasses import dataclass, field from pathlib import Path @@ -16,6 +17,7 @@ from ria_toolkit_oss.io.recording import to_sigmf from .campaign import CampaignConfig, CaptureStep, TransmitterConfig from .labeler import build_output_filename, label_recording from .qa import QAResult, check_recording +from .tx_executor import TxExecutor logger = logging.getLogger(__name__) @@ -212,6 +214,7 @@ class CampaignExecutor: self.progress_cb = progress_cb self._sdr = None self._remote_tx_controllers: dict = {} + self._tx_executors: dict[str, tuple] = {} # tx_id → (TxExecutor, stop_event, thread) if verbose: logging.basicConfig(level=logging.DEBUG) @@ -266,6 +269,7 @@ class CampaignExecutor: finally: self._close_sdr() self._close_remote_tx_controllers() + self._close_tx_executors() result.end_time = time.time() logger.info( @@ -340,6 +344,12 @@ class CampaignExecutor: logger.warning(f"Error closing remote Tx controller {tx_id}: {exc}") self._remote_tx_controllers.clear() + def _close_tx_executors(self) -> None: + for tx_id, (_, stop_event, t) in list(self._tx_executors.items()): + stop_event.set() + t.join(timeout=5.0) + self._tx_executors.clear() + def _record(self, duration_s: float) -> Recording: """Capture ``duration_s`` seconds of IQ samples.""" num_samples = int(duration_s * self.config.recorder.sample_rate) @@ -453,6 +463,27 @@ class CampaignExecutor: # Start transmission in background; _record() runs concurrently ctrl.transmit_async(step.duration + 1.0) + elif transmitter.control_method == "sdr_agent": + if not transmitter.sdr_agent: + logger.warning(f"Transmitter '{transmitter.id}' has no sdr_agent config — skipping") + return + step_dict: dict = {"label": step.label, "duration": step.duration + 1.0} + if step.power_dbm is not None: + step_dict["power_dbm"] = step.power_dbm + tx_config = { + "id": transmitter.id, + "sdr_agent": transmitter.sdr_agent, + "schedule": [step_dict], + } + rec = self.config.recorder + tx_device = transmitter.device or rec.device + sdr_device = _DEVICE_ALIASES.get(tx_device.lower(), tx_device.lower()) + stop_event = threading.Event() + executor = TxExecutor(tx_config, sdr_device=sdr_device, stop_event=stop_event) + t = threading.Thread(target=executor.run, daemon=True, name=f"tx-{transmitter.id}") + self._tx_executors[transmitter.id] = (executor, stop_event, t) + t.start() + else: logger.warning(f"Unknown control method '{transmitter.control_method}' — skipping") @@ -475,6 +506,13 @@ class CampaignExecutor: if ctrl is not None: ctrl.wait_transmit(timeout=step.duration + 10.0) + elif transmitter.control_method == "sdr_agent": + entry = self._tx_executors.pop(transmitter.id, None) + if entry is not None: + _, stop_event, t = entry + stop_event.set() + t.join(timeout=step.duration + 10.0) + @staticmethod def _step_params_json(transmitter: TransmitterConfig, step: CaptureStep) -> str: """Serialise step parameters to a JSON string for the control script.""" From 4aea2841bed3b0fc84e1dcb30107cb3f25d27fb5 Mon Sep 17 00:00:00 2001 From: ben Date: Tue, 21 Apr 2026 14:09:36 -0400 Subject: [PATCH 04/10] two-machine TX/RX --- src/ria_toolkit_oss/agent/legacy_executor.py | 9 +++++---- src/ria_toolkit_oss/orchestration/executor.py | 5 +++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/ria_toolkit_oss/agent/legacy_executor.py b/src/ria_toolkit_oss/agent/legacy_executor.py index d8a56d6..91221e1 100644 --- a/src/ria_toolkit_oss/agent/legacy_executor.py +++ b/src/ria_toolkit_oss/agent/legacy_executor.py @@ -261,9 +261,10 @@ class NodeAgent: if command == "run_campaign": campaign_id: str = cmd.get("campaign_id") or str(uuid.uuid4()) config_dict: dict = cmd.get("payload") or {} + skip_local_tx: bool = bool(cmd.get("skip_local_tx", False)) threading.Thread( target=self._run_campaign, - args=(campaign_id, config_dict), + args=(campaign_id, config_dict, skip_local_tx), daemon=True, name=f"campaign-{campaign_id[:8]}", ).start() @@ -303,7 +304,7 @@ class NodeAgent: # Campaign execution # ------------------------------------------------------------------ - def _run_campaign(self, campaign_id: str, config_dict: dict) -> None: + def _run_campaign(self, campaign_id: str, config_dict: dict, skip_local_tx: bool = False) -> None: try: from ria_toolkit_oss.orchestration.campaign import CampaignConfig from ria_toolkit_oss.orchestration.executor import CampaignExecutor @@ -315,10 +316,10 @@ class NodeAgent: ) return - logger.info("Campaign %s starting", campaign_id[:8]) + logger.info("Campaign %s starting (skip_local_tx=%s)", campaign_id[:8], skip_local_tx) try: config = CampaignConfig.from_dict(config_dict) - executor = CampaignExecutor(config) + executor = CampaignExecutor(config, skip_local_tx=skip_local_tx) result = executor.run() logger.info("Campaign %s completed — uploading recordings", campaign_id[:8]) self._upload_recordings(campaign_id, config, result) diff --git a/src/ria_toolkit_oss/orchestration/executor.py b/src/ria_toolkit_oss/orchestration/executor.py index 445b16b..66c5273 100644 --- a/src/ria_toolkit_oss/orchestration/executor.py +++ b/src/ria_toolkit_oss/orchestration/executor.py @@ -209,9 +209,11 @@ class CampaignExecutor: config: CampaignConfig, progress_cb: Optional[Callable[[int, int, StepResult], None]] = None, verbose: bool = False, + skip_local_tx: bool = False, ): self.config = config self.progress_cb = progress_cb + self.skip_local_tx = skip_local_tx self._sdr = None self._remote_tx_controllers: dict = {} self._tx_executors: dict[str, tuple] = {} # tx_id → (TxExecutor, stop_event, thread) @@ -464,6 +466,9 @@ class CampaignExecutor: ctrl.transmit_async(step.duration + 1.0) elif transmitter.control_method == "sdr_agent": + if self.skip_local_tx: + logger.debug(f"skip_local_tx — TX for '{transmitter.id}' delegated to TX agent node") + return if not transmitter.sdr_agent: logger.warning(f"Transmitter '{transmitter.id}' has no sdr_agent config — skipping") return From 4d3aaf6ec8e8abaf8fd1d7cf0fa813317f027f23 Mon Sep 17 00:00:00 2001 From: ben Date: Tue, 21 Apr 2026 14:34:48 -0400 Subject: [PATCH 05/10] json access issue --- .../orchestration/tx_executor.py | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/ria_toolkit_oss/orchestration/tx_executor.py b/src/ria_toolkit_oss/orchestration/tx_executor.py index 6ae32b1..84e200b 100644 --- a/src/ria_toolkit_oss/orchestration/tx_executor.py +++ b/src/ria_toolkit_oss/orchestration/tx_executor.py @@ -37,6 +37,26 @@ from typing import Any logger = logging.getLogger(__name__) + +def _parse_hz(val: object) -> float: + """Parse a frequency value that may be a float (Hz) or a string like '2.45GHz'.""" + if isinstance(val, (int, float)): + return float(val) + s = str(val).strip() + for suffix, mult in (("GHz", 1e9), ("MHz", 1e6), ("kHz", 1e3), ("Hz", 1.0)): + if s.endswith(suffix): + return float(s[: -len(suffix)]) * mult + return float(s) + + +def _parse_seconds(val: object) -> float: + """Parse a duration value that may be a float (seconds) or a string like '5s'.""" + if isinstance(val, (int, float)): + return float(val) + s = str(val).strip() + return float(s[:-1]) if s.endswith("s") else float(s) + + # Mapping from modulation name → (PSK/QAM order, generator_type) # 'psk' uses PSKGenerator, 'qam' uses QAMGenerator _MOD_TABLE: dict[str, tuple[int, str]] = { @@ -83,7 +103,7 @@ class TxExecutor: modulation: str = agent_cfg.get("modulation", "QPSK").upper() symbol_rate: float = float(agent_cfg.get("symbol_rate", 1e6)) - center_freq: float = float(agent_cfg.get("center_frequency", 0.0)) + center_freq: float = _parse_hz(agent_cfg.get("center_frequency", 0.0)) filter_type: str = agent_cfg.get("filter", "rrc").lower() rolloff: float = float(agent_cfg.get("rolloff", 0.35)) @@ -109,7 +129,7 @@ class TxExecutor: filter_type: str, rolloff: float, ) -> None: - duration: float = float(step.get("duration", 10.0)) + duration: float = _parse_seconds(step.get("duration", 10.0)) label: str = step.get("label", "step") gain: float = float(step.get("power_dbm") or 0.0) sample_rate = symbol_rate * sps From 39d5d74d6adb5470fd7f4f22349d9fa90661682d Mon Sep 17 00:00:00 2001 From: ben Date: Tue, 21 Apr 2026 15:03:57 -0400 Subject: [PATCH 06/10] large memory fix --- src/ria_toolkit_oss/orchestration/tx_executor.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/ria_toolkit_oss/orchestration/tx_executor.py b/src/ria_toolkit_oss/orchestration/tx_executor.py index 84e200b..2d9e1c1 100644 --- a/src/ria_toolkit_oss/orchestration/tx_executor.py +++ b/src/ria_toolkit_oss/orchestration/tx_executor.py @@ -70,6 +70,12 @@ _MOD_TABLE: dict[str, tuple[int, str]] = { _SPECIAL_MODS = {"FSK", "OOK", "GMSK", "OQPSK"} +# usrp-uhd-client's tx_recording() streams 2 000-sample chunks and loops the +# source buffer for the full tx_time, so only this many samples ever need to +# be in RAM regardless of step duration or sample rate. +# 50 000 complex64 samples ≈ 400 kB — enough spectral diversity for looping. +_SYNTH_BLOCK_SAMPLES = 50_000 + class TxExecutor: """Synthesise and transmit a signal campaign via a local SDR. @@ -145,7 +151,12 @@ class TxExecutor: ) num_samples = int(duration * sample_rate) - signal = self._synthesise(modulation, sps, num_samples, filter_type, rolloff) + + # Synthesise a short representative block. tx_recording() loops this + # buffer for the full tx_time using a 2 000-sample streaming callback, + # so peak memory is O(_SYNTH_BLOCK_SAMPLES) regardless of duration. + block_size = min(num_samples, _SYNTH_BLOCK_SAMPLES) + signal = self._synthesise(modulation, sps, block_size, filter_type, rolloff) if self._sdr is not None: try: From 34b67c0c17c189137b879f9a4930b6b24949e3f7 Mon Sep 17 00:00:00 2001 From: ben Date: Tue, 21 Apr 2026 15:56:04 -0400 Subject: [PATCH 07/10] campaign loop support --- src/ria_toolkit_oss/orchestration/campaign.py | 11 ++-- src/ria_toolkit_oss/orchestration/executor.py | 50 +++++++++++-------- .../orchestration/tx_executor.py | 13 ++++- 3 files changed, 47 insertions(+), 27 deletions(-) diff --git a/src/ria_toolkit_oss/orchestration/campaign.py b/src/ria_toolkit_oss/orchestration/campaign.py index 5fe1128..6f9be35 100644 --- a/src/ria_toolkit_oss/orchestration/campaign.py +++ b/src/ria_toolkit_oss/orchestration/campaign.py @@ -297,6 +297,7 @@ class CampaignConfig: qa: QAConfig = field(default_factory=QAConfig) output: OutputConfig = field(default_factory=OutputConfig) mode: str = "controlled_testbed" + loops: int = 1 # repeat full schedule this many times; labels get _run{N:02d} suffix # --------------------------------------------------------------------------- # Loaders @@ -324,6 +325,7 @@ class CampaignConfig: return cls( name=safe_name, mode=str(campaign_meta.get("mode", "controlled_testbed")), + loops=max(1, int(campaign_meta.get("loops", 1))), recorder=RecorderConfig.from_dict(raw["recorder"]), transmitters=transmitters, qa=QAConfig.from_dict(raw.get("qa", {})), @@ -388,6 +390,7 @@ class CampaignConfig: return cls( name=safe_name, mode=str(campaign_meta.get("mode", "controlled_testbed")), + loops=max(1, int(campaign_meta.get("loops", 1))), recorder=RecorderConfig.from_dict(raw["recorder"]), transmitters=transmitters, qa=QAConfig.from_dict(raw.get("qa", {})), @@ -490,9 +493,9 @@ class CampaignConfig: ) def total_capture_time_s(self) -> float: - """Sum of all step durations across all transmitters.""" - return sum(step.duration for tx in self.transmitters for step in tx.schedule) + """Sum of all step durations across all transmitters and loops.""" + return sum(step.duration for tx in self.transmitters for step in tx.schedule) * self.loops def total_steps(self) -> int: - """Total number of capture steps across all transmitters.""" - return sum(len(tx.schedule) for tx in self.transmitters) + """Total number of capture steps across all transmitters and loops.""" + return sum(len(tx.schedule) for tx in self.transmitters) * self.loops diff --git a/src/ria_toolkit_oss/orchestration/executor.py b/src/ria_toolkit_oss/orchestration/executor.py index 66c5273..222b348 100644 --- a/src/ria_toolkit_oss/orchestration/executor.py +++ b/src/ria_toolkit_oss/orchestration/executor.py @@ -7,7 +7,7 @@ import logging import subprocess import threading import time -from dataclasses import dataclass, field +from dataclasses import dataclass, field, replace from pathlib import Path from typing import Callable, Optional @@ -236,10 +236,12 @@ class CampaignExecutor: """ result = CampaignResult(campaign_name=self.config.name) + loops = self.config.loops logger.info( f"Starting campaign '{self.config.name}': " - f"{self.config.total_steps()} steps, " - f"~{self.config.total_capture_time_s():.0f}s capture time" + f"{self.config.total_steps()} steps" + + (f" ({self.config.total_steps() // loops} × {loops} loops)" if loops > 1 else "") + + f", ~{self.config.total_capture_time_s():.0f}s capture time" ) self._init_sdr() @@ -248,26 +250,32 @@ class CampaignExecutor: total = self.config.total_steps() step_index = 0 - for transmitter in self.config.transmitters: - logger.info(f"Transmitter: {transmitter.id} ({len(transmitter.schedule)} steps)") - for step in transmitter.schedule: - step_result = self._execute_step(transmitter, step) - result.steps.append(step_result) - step_index += 1 + for loop_idx in range(loops): + if loops > 1: + logger.info(f"Loop {loop_idx + 1}/{loops}") + for transmitter in self.config.transmitters: + logger.info(f"Transmitter: {transmitter.id} ({len(transmitter.schedule)} steps)") + for step in transmitter.schedule: + looped_step = replace(step, label=f"{step.label}_run{loop_idx + 1:02d}") if loops > 1 else step + step_result = self._execute_step(transmitter, looped_step) + result.steps.append(step_result) + step_index += 1 - if self.progress_cb: - self.progress_cb(step_index, total, step_result) + if self.progress_cb: + self.progress_cb(step_index, total, step_result) - if step_result.error: - logger.warning(f"Step '{step.label}' error: {step_result.error}") - elif step_result.qa.flagged: - logger.warning(f"Step '{step.label}' flagged for review: " + "; ".join(step_result.qa.issues)) - else: - logger.info( - f"Step '{step.label}' OK " - f"(SNR {step_result.qa.snr_db:.1f} dB, " - f"{step_result.qa.duration_s:.1f}s)" - ) + if step_result.error: + logger.warning(f"Step '{looped_step.label}' error: {step_result.error}") + elif step_result.qa.flagged: + logger.warning( + f"Step '{looped_step.label}' flagged for review: " + "; ".join(step_result.qa.issues) + ) + else: + logger.info( + f"Step '{looped_step.label}' OK " + f"(SNR {step_result.qa.snr_db:.1f} dB, " + f"{step_result.qa.duration_s:.1f}s)" + ) finally: self._close_sdr() self._close_remote_tx_controllers() diff --git a/src/ria_toolkit_oss/orchestration/tx_executor.py b/src/ria_toolkit_oss/orchestration/tx_executor.py index 2d9e1c1..a3c9bdc 100644 --- a/src/ria_toolkit_oss/orchestration/tx_executor.py +++ b/src/ria_toolkit_oss/orchestration/tx_executor.py @@ -112,6 +112,7 @@ class TxExecutor: center_freq: float = _parse_hz(agent_cfg.get("center_frequency", 0.0)) filter_type: str = agent_cfg.get("filter", "rrc").lower() rolloff: float = float(agent_cfg.get("rolloff", 0.35)) + loops: int = max(1, int(self.config.get("loops", 1))) # Upsampling factor: samples_per_symbol, fixed at 8 for SDR compatibility. sps = 8 @@ -119,10 +120,18 @@ class TxExecutor: self._init_sdr(sample_rate, center_freq) try: - for step in schedule: + for loop_idx in range(loops): if self.stop_event.is_set(): break - self._execute_step(step, modulation, sps, symbol_rate, filter_type, rolloff) + if loops > 1: + logger.info("TX loop %d/%d", loop_idx + 1, loops) + for step in schedule: + if self.stop_event.is_set(): + break + looped_step = ( + {**step, "label": f"{step.get('label', 'step')}_run{loop_idx + 1:02d}"} if loops > 1 else step + ) + self._execute_step(looped_step, modulation, sps, symbol_rate, filter_type, rolloff) finally: self._close_sdr() From 53e8e5adb630ecfa62cc99f915aa127a22459290 Mon Sep 17 00:00:00 2001 From: ben Date: Tue, 21 Apr 2026 16:40:49 -0400 Subject: [PATCH 08/10] chunk timeout error --- src/ria_toolkit_oss/agent/legacy_executor.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/ria_toolkit_oss/agent/legacy_executor.py b/src/ria_toolkit_oss/agent/legacy_executor.py index 91221e1..76b55c5 100644 --- a/src/ria_toolkit_oss/agent/legacy_executor.py +++ b/src/ria_toolkit_oss/agent/legacy_executor.py @@ -68,7 +68,7 @@ _HEARTBEAT_INTERVAL = 30 # seconds between heartbeats _POLL_TIMEOUT = 30 # server-side long-poll duration _POLL_CLIENT_TIMEOUT = 40 # client read timeout — slightly longer than server _RECONNECT_PAUSE = 5 # seconds to wait after a poll error before retrying -_CHUNK_SIZE = 50 * 1024 * 1024 # 50 MB — well below Cloudflare's 100 MB limit +_CHUNK_SIZE = 10 * 1024 * 1024 # 10 MB per chunk — fast enough for git-LFS to process within timeout _DIRECT_THRESHOLD = 90 * 1024 * 1024 # files above this use chunked upload _CAPTURE_SAMPLES = 4096 # IQ samples per inference window _IDLE_LABELS = frozenset({"noise", "idle", "no_signal", "unknown_protocol", "background"}) @@ -659,13 +659,16 @@ class NodeAgent: base_url = f"{self.hub_url}/datasets/upload" steps = (result.get("steps") if isinstance(result, dict) else getattr(result, "steps", None)) or [] + campaign_name: str = getattr(config, "name", None) or "" for step in steps: output_path: str | None = getattr(step, "output_path", None) if not output_path: continue device_id: str = getattr(step, "transmitter_id", "") or "" for fpath in _sigmf_files(output_path): - filename = os.path.basename(fpath) + basename = os.path.basename(fpath) + path_parts = [p for p in (campaign_name, device_id) if p] + filename = "/".join(path_parts + [basename]) metadata = { "filename": filename, "repo_owner": repo_owner, @@ -751,7 +754,7 @@ class NodeAgent: headers=headers, files={"file": (filename, chunk, "application/octet-stream")}, data={**metadata, "upload_id": upload_id, "chunk_index": i, "total_chunks": total_chunks}, - timeout=120, + timeout=300, verify=verify, ) if not resp.ok: From c9b19949adba23ef71371e599e4922a535c779f5 Mon Sep 17 00:00:00 2001 From: ben Date: Tue, 21 Apr 2026 17:11:16 -0400 Subject: [PATCH 09/10] timeout chunk improvements --- src/ria_toolkit_oss/agent/legacy_executor.py | 6 ++++-- src/ria_toolkit_oss/orchestration/campaign.py | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/ria_toolkit_oss/agent/legacy_executor.py b/src/ria_toolkit_oss/agent/legacy_executor.py index 76b55c5..d4a302a 100644 --- a/src/ria_toolkit_oss/agent/legacy_executor.py +++ b/src/ria_toolkit_oss/agent/legacy_executor.py @@ -659,7 +659,9 @@ class NodeAgent: base_url = f"{self.hub_url}/datasets/upload" steps = (result.get("steps") if isinstance(result, dict) else getattr(result, "steps", None)) or [] - campaign_name: str = getattr(config, "name", None) or "" + output_obj = getattr(config, "output", None) + folder = getattr(output_obj, "folder", None) + campaign_name: str = folder if folder is not None else (getattr(config, "name", None) or "") for step in steps: output_path: str | None = getattr(step, "output_path", None) if not output_path: @@ -754,7 +756,7 @@ class NodeAgent: headers=headers, files={"file": (filename, chunk, "application/octet-stream")}, data={**metadata, "upload_id": upload_id, "chunk_index": i, "total_chunks": total_chunks}, - timeout=300, + timeout=(30, None), # 30s connect, no read timeout — server may take minutes on final chunk verify=verify, ) if not resp.ok: diff --git a/src/ria_toolkit_oss/orchestration/campaign.py b/src/ria_toolkit_oss/orchestration/campaign.py index 6f9be35..105cc40 100644 --- a/src/ria_toolkit_oss/orchestration/campaign.py +++ b/src/ria_toolkit_oss/orchestration/campaign.py @@ -276,6 +276,7 @@ class OutputConfig: path: str = "recordings" device_id: Optional[str] = None # for device-profile campaigns repo: Optional[str] = None + folder: Optional[str] = None # repo subfolder: None = use campaign name, "" = no subfolder, str = custom @classmethod def from_dict(cls, d: dict) -> "OutputConfig": @@ -284,6 +285,7 @@ class OutputConfig: path=str(d.get("path", "recordings")), device_id=d.get("device_id"), repo=d.get("repo"), + folder=d.get("folder"), ) From 07c72294f5eb137b9a3848b57a09cacdf86da679 Mon Sep 17 00:00:00 2001 From: ben Date: Wed, 22 Apr 2026 10:10:25 -0400 Subject: [PATCH 10/10] removing orchestrator references --- src/ria_toolkit_oss/server/app.py | 8 ++-- src/ria_toolkit_oss/server/models.py | 2 +- .../routers/{orchestrator.py => conductor.py} | 2 +- .../ria_toolkit_oss/serve.py | 6 +-- tests/server/test_server.py | 40 +++++++++---------- 5 files changed, 29 insertions(+), 29 deletions(-) rename src/ria_toolkit_oss/server/routers/{orchestrator.py => conductor.py} (98%) diff --git a/src/ria_toolkit_oss/server/app.py b/src/ria_toolkit_oss/server/app.py index 5e4c58b..42799a6 100644 --- a/src/ria_toolkit_oss/server/app.py +++ b/src/ria_toolkit_oss/server/app.py @@ -3,7 +3,7 @@ from fastapi import Depends, FastAPI from .auth import require_api_key -from .routers import inference, orchestrator +from .routers import conductor, inference def create_app(api_key: str = "") -> FastAPI: @@ -28,9 +28,9 @@ def create_app(api_key: str = "") -> FastAPI: app.state.api_key = api_key app.include_router( - orchestrator.router, - prefix="/orchestrator", - tags=["Orchestrator"], + conductor.router, + prefix="/conductor", + tags=["Conductor"], dependencies=[Depends(require_api_key)], ) app.include_router( diff --git a/src/ria_toolkit_oss/server/models.py b/src/ria_toolkit_oss/server/models.py index e2ba450..9fd88d9 100644 --- a/src/ria_toolkit_oss/server/models.py +++ b/src/ria_toolkit_oss/server/models.py @@ -7,7 +7,7 @@ from pathlib import Path from pydantic import BaseModel, field_validator # --------------------------------------------------------------------------- -# Orchestrator +# Conductor # --------------------------------------------------------------------------- diff --git a/src/ria_toolkit_oss/server/routers/orchestrator.py b/src/ria_toolkit_oss/server/routers/conductor.py similarity index 98% rename from src/ria_toolkit_oss/server/routers/orchestrator.py rename to src/ria_toolkit_oss/server/routers/conductor.py index dfc01af..7ec7d9d 100644 --- a/src/ria_toolkit_oss/server/routers/orchestrator.py +++ b/src/ria_toolkit_oss/server/routers/conductor.py @@ -1,4 +1,4 @@ -"""Orchestrator routes: campaign deployment, status, and cancellation.""" +"""Conductor routes: campaign deployment, status, and cancellation.""" from __future__ import annotations diff --git a/src/ria_toolkit_oss_cli/ria_toolkit_oss/serve.py b/src/ria_toolkit_oss_cli/ria_toolkit_oss/serve.py index 21beb6e..5d541b4 100644 --- a/src/ria_toolkit_oss_cli/ria_toolkit_oss/serve.py +++ b/src/ria_toolkit_oss_cli/ria_toolkit_oss/serve.py @@ -23,9 +23,9 @@ def serve(host: str, port: int, api_key: str, log_level: str): \b Endpoints: - POST /orchestrator/deploy - GET /orchestrator/status/{campaign_id} - POST /orchestrator/cancel/{campaign_id} + POST /conductor/deploy + GET /conductor/status/{campaign_id} + POST /conductor/cancel/{campaign_id} POST /inference/load POST /inference/start POST /inference/stop diff --git a/tests/server/test_server.py b/tests/server/test_server.py index 3e9c8db..e3345ae 100644 --- a/tests/server/test_server.py +++ b/tests/server/test_server.py @@ -1,6 +1,6 @@ """Tests for the RT-OSS HTTP server. -Covers: auth, inference lifecycle (without SDR/ONNX hardware), orchestrator +Covers: auth, inference lifecycle (without SDR/ONNX hardware), conductor lifecycle (with mocked executor), and state helpers. ``start_inference`` and ``_inference_loop`` require real SDR hardware and an @@ -286,17 +286,17 @@ class TestInferenceStop: # --------------------------------------------------------------------------- -# POST /orchestrator/deploy +# POST /conductor/deploy # --------------------------------------------------------------------------- -class TestOrchestratorDeploy: +class TestConductorDeploy: def test_deploy_422_on_invalid_config(self, client): with patch( - "ria_toolkit_oss.server.routers.orchestrator.CampaignConfig.from_dict", + "ria_toolkit_oss.server.routers.conductor.CampaignConfig.from_dict", side_effect=ValueError("missing required field 'name'"), ): - resp = client.post("/orchestrator/deploy", json={"config": {}}) + resp = client.post("/conductor/deploy", json={"config": {}}) assert resp.status_code == 422 def test_deploy_returns_campaign_id(self, client): @@ -307,10 +307,10 @@ class TestOrchestratorDeploy: mock_executor.return_value.run.return_value = MagicMock(to_dict=lambda: {}) with ( - patch("ria_toolkit_oss.server.routers.orchestrator.CampaignConfig.from_dict", return_value=mock_cfg), - patch("ria_toolkit_oss.server.routers.orchestrator.CampaignExecutor", mock_executor), + patch("ria_toolkit_oss.server.routers.conductor.CampaignConfig.from_dict", return_value=mock_cfg), + patch("ria_toolkit_oss.server.routers.conductor.CampaignExecutor", mock_executor), ): - resp = client.post("/orchestrator/deploy", json={"config": {"name": "test_campaign"}}) + resp = client.post("/conductor/deploy", json={"config": {"name": "test_campaign"}}) assert resp.status_code == 200 body = resp.json() @@ -325,23 +325,23 @@ class TestOrchestratorDeploy: mock_executor.return_value.run.return_value = MagicMock(to_dict=lambda: {}) with ( - patch("ria_toolkit_oss.server.routers.orchestrator.CampaignConfig.from_dict", return_value=mock_cfg), - patch("ria_toolkit_oss.server.routers.orchestrator.CampaignExecutor", mock_executor), + patch("ria_toolkit_oss.server.routers.conductor.CampaignConfig.from_dict", return_value=mock_cfg), + patch("ria_toolkit_oss.server.routers.conductor.CampaignExecutor", mock_executor), ): - resp = client.post("/orchestrator/deploy", json={"config": {}}) + resp = client.post("/conductor/deploy", json={"config": {}}) campaign_id = resp.json()["campaign_id"] assert state_module._campaigns.get(campaign_id) is not None # --------------------------------------------------------------------------- -# GET /orchestrator/status/{campaign_id} +# GET /conductor/status/{campaign_id} # --------------------------------------------------------------------------- -class TestOrchestratorStatus: +class TestConductorStatus: def test_status_404_for_unknown_id(self, client): - resp = client.get("/orchestrator/status/nonexistent-id") + resp = client.get("/conductor/status/nonexistent-id") assert resp.status_code == 404 def test_status_returns_campaign_state(self, client): @@ -357,7 +357,7 @@ class TestOrchestratorStatus: ) state_module._campaigns["abc-123"] = state - resp = client.get("/orchestrator/status/abc-123") + resp = client.get("/conductor/status/abc-123") assert resp.status_code == 200 body = resp.json() assert body["campaign_id"] == "abc-123" @@ -367,13 +367,13 @@ class TestOrchestratorStatus: # --------------------------------------------------------------------------- -# POST /orchestrator/cancel/{campaign_id} +# POST /conductor/cancel/{campaign_id} # --------------------------------------------------------------------------- -class TestOrchestratorCancel: +class TestConductorCancel: def test_cancel_404_for_unknown_id(self, client): - resp = client.post("/orchestrator/cancel/no-such-id") + resp = client.post("/conductor/cancel/no-such-id") assert resp.status_code == 404 def test_cancel_sets_cancel_event(self, client): @@ -387,7 +387,7 @@ class TestOrchestratorCancel: ) state_module._campaigns["camp-to-cancel"] = state - resp = client.post("/orchestrator/cancel/camp-to-cancel") + resp = client.post("/conductor/cancel/camp-to-cancel") assert resp.status_code == 200 assert resp.json()["cancelled"] is True assert cancel_event.is_set() @@ -403,7 +403,7 @@ class TestOrchestratorCancel: ) state_module._campaigns["done"] = state - resp = client.post("/orchestrator/cancel/done") + resp = client.post("/conductor/cancel/done") assert resp.status_code == 200 assert resp.json()["cancelled"] is False assert not cancel_event.is_set()