Compare commits
21 Commits
d3a7e9ef0f
...
d6c66d2a07
| Author | SHA1 | Date | |
|---|---|---|---|
| d6c66d2a07 | |||
| 414597e940 | |||
| 1fa9ab2495 | |||
| ab4cb0ea5a | |||
| ae07eef885 | |||
| e506d26450 | |||
| 138fdeb68b | |||
| 0642dcc2db | |||
| 84a7893c8f | |||
| 8e542919a8 | |||
| 27049f00ea | |||
| 78ecd171bd | |||
| 6fb73c1daa | |||
| 83515d6e3f | |||
| 638fe5df1f | |||
| efc0948110 | |||
| 8f39c4d855 | |||
| 195db4a27d | |||
| 5718e109b5 | |||
| d81c61c3cf | |||
| 54b9bd4fc8 |
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -52,6 +52,7 @@ tests/sdr/
|
||||||
|
|
||||||
# Sphinx documentation
|
# Sphinx documentation
|
||||||
docs/build/
|
docs/build/
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
# Jupyter Notebook
|
# Jupyter Notebook
|
||||||
.ipynb_checkpoints
|
.ipynb_checkpoints
|
||||||
|
|
|
||||||
1083
docs/_build/html/_sources/intro/getting_started.rst.txt
vendored
Normal file
1083
docs/_build/html/_sources/intro/getting_started.rst.txt
vendored
Normal file
File diff suppressed because it is too large
Load Diff
29
docs/source/_static/custom.css
Normal file
29
docs/source/_static/custom.css
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
/* Change the hex values below to customize heading colours */
|
||||||
|
|
||||||
|
.rst-content h1 { color: #2c3e50; }
|
||||||
|
.rst-content h2,
|
||||||
|
.rst-content h2 a { color: #ffffff !important; font-size: 22px !important; }
|
||||||
|
|
||||||
|
.rst-content h3,
|
||||||
|
.rst-content h3 a { color: #ffffff !important; font-size: 16px !important; }
|
||||||
|
|
||||||
|
.rst-content h3 code { font-size: inherit !important; }
|
||||||
|
|
||||||
|
.rst-content .admonition.warning {
|
||||||
|
background: #1a1a2e !important;
|
||||||
|
border-left: 4px solid #c0392b !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rst-content .admonition.warning .admonition-title {
|
||||||
|
background: #c0392b !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rst-content .admonition.warning p {
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
.rst-content h4 { color: #404040; }
|
||||||
|
|
||||||
|
.highlight * { color: #ffffff !important; }
|
||||||
|
|
||||||
|
.ria-cmd { color: #2980b9 !important; }
|
||||||
8
docs/source/_static/custom.js
Normal file
8
docs/source/_static/custom.js
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
document.querySelectorAll('.highlight pre').forEach(function (pre) {
|
||||||
|
pre.innerHTML = pre.innerHTML.replace(
|
||||||
|
/((?:^|\n|>))(ria)(?=[ \t]|<)/g,
|
||||||
|
'$1<span class="ria-cmd">$2</span>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -14,7 +14,7 @@ sys.path.insert(0, os.path.abspath(os.path.join('..', '..')))
|
||||||
project = 'ria-toolkit-oss'
|
project = 'ria-toolkit-oss'
|
||||||
copyright = '2025, Qoherent Inc'
|
copyright = '2025, Qoherent Inc'
|
||||||
author = 'Qoherent Inc.'
|
author = 'Qoherent Inc.'
|
||||||
release = '0.1.4'
|
release = '0.1.5'
|
||||||
|
|
||||||
# -- General configuration ---------------------------------------------------
|
# -- General configuration ---------------------------------------------------
|
||||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
||||||
|
|
@ -73,3 +73,6 @@ def setup(app):
|
||||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
|
||||||
|
|
||||||
html_theme = 'sphinx_rtd_theme'
|
html_theme = 'sphinx_rtd_theme'
|
||||||
|
html_static_path = ['_static']
|
||||||
|
html_css_files = ['custom.css']
|
||||||
|
html_js_files = ['custom.js']
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
641
poetry.lock
generated
641
poetry.lock
generated
|
|
@ -98,6 +98,83 @@ files = [
|
||||||
[package.extras]
|
[package.extras]
|
||||||
dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""]
|
dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bcrypt"
|
||||||
|
version = "5.0.0"
|
||||||
|
description = "Modern password hashing for your software and your servers"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "bcrypt-5.0.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f3c08197f3039bec79cee59a606d62b96b16669cff3949f21e74796b6e3cd2be"},
|
||||||
|
{file = "bcrypt-5.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:200af71bc25f22006f4069060c88ed36f8aa4ff7f53e67ff04d2ab3f1e79a5b2"},
|
||||||
|
{file = "bcrypt-5.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:baade0a5657654c2984468efb7d6c110db87ea63ef5a4b54732e7e337253e44f"},
|
||||||
|
{file = "bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c58b56cdfb03202b3bcc9fd8daee8e8e9b6d7e3163aa97c631dfcfcc24d36c86"},
|
||||||
|
{file = "bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bfd2a34de661f34d0bda43c3e4e79df586e4716ef401fe31ea39d69d581ef23"},
|
||||||
|
{file = "bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ed2e1365e31fc73f1825fa830f1c8f8917ca1b3ca6185773b349c20fd606cec2"},
|
||||||
|
{file = "bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:83e787d7a84dbbfba6f250dd7a5efd689e935f03dd83b0f919d39349e1f23f83"},
|
||||||
|
{file = "bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:137c5156524328a24b9fac1cb5db0ba618bc97d11970b39184c1d87dc4bf1746"},
|
||||||
|
{file = "bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:38cac74101777a6a7d3b3e3cfefa57089b5ada650dce2baf0cbdd9d65db22a9e"},
|
||||||
|
{file = "bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:d8d65b564ec849643d9f7ea05c6d9f0cd7ca23bdd4ac0c2dbef1104ab504543d"},
|
||||||
|
{file = "bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:741449132f64b3524e95cd30e5cd3343006ce146088f074f31ab26b94e6c75ba"},
|
||||||
|
{file = "bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:212139484ab3207b1f0c00633d3be92fef3c5f0af17cad155679d03ff2ee1e41"},
|
||||||
|
{file = "bcrypt-5.0.0-cp313-cp313t-win32.whl", hash = "sha256:9d52ed507c2488eddd6a95bccee4e808d3234fa78dd370e24bac65a21212b861"},
|
||||||
|
{file = "bcrypt-5.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f6984a24db30548fd39a44360532898c33528b74aedf81c26cf29c51ee47057e"},
|
||||||
|
{file = "bcrypt-5.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9fffdb387abe6aa775af36ef16f55e318dcda4194ddbf82007a6f21da29de8f5"},
|
||||||
|
{file = "bcrypt-5.0.0-cp314-cp314t-macosx_10_12_universal2.whl", hash = "sha256:4870a52610537037adb382444fefd3706d96d663ac44cbb2f37e3919dca3d7ef"},
|
||||||
|
{file = "bcrypt-5.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48f753100931605686f74e27a7b49238122aa761a9aefe9373265b8b7aa43ea4"},
|
||||||
|
{file = "bcrypt-5.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f70aadb7a809305226daedf75d90379c397b094755a710d7014b8b117df1ebbf"},
|
||||||
|
{file = "bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:744d3c6b164caa658adcb72cb8cc9ad9b4b75c7db507ab4bc2480474a51989da"},
|
||||||
|
{file = "bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a28bc05039bdf3289d757f49d616ab3efe8cf40d8e8001ccdd621cd4f98f4fc9"},
|
||||||
|
{file = "bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7f277a4b3390ab4bebe597800a90da0edae882c6196d3038a73adf446c4f969f"},
|
||||||
|
{file = "bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:79cfa161eda8d2ddf29acad370356b47f02387153b11d46042e93a0a95127493"},
|
||||||
|
{file = "bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a5393eae5722bcef046a990b84dff02b954904c36a194f6cfc817d7dca6c6f0b"},
|
||||||
|
{file = "bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f4c94dec1b5ab5d522750cb059bb9409ea8872d4494fd152b53cca99f1ddd8c"},
|
||||||
|
{file = "bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0cae4cb350934dfd74c020525eeae0a5f79257e8a201c0c176f4b84fdbf2a4b4"},
|
||||||
|
{file = "bcrypt-5.0.0-cp314-cp314t-win32.whl", hash = "sha256:b17366316c654e1ad0306a6858e189fc835eca39f7eb2cafd6aaca8ce0c40a2e"},
|
||||||
|
{file = "bcrypt-5.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:92864f54fb48b4c718fc92a32825d0e42265a627f956bc0361fe869f1adc3e7d"},
|
||||||
|
{file = "bcrypt-5.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dd19cf5184a90c873009244586396a6a884d591a5323f0e8a5922560718d4993"},
|
||||||
|
{file = "bcrypt-5.0.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:fc746432b951e92b58317af8e0ca746efe93e66555f1b40888865ef5bf56446b"},
|
||||||
|
{file = "bcrypt-5.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c2388ca94ffee269b6038d48747f4ce8df0ffbea43f31abfa18ac72f0218effb"},
|
||||||
|
{file = "bcrypt-5.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:560ddb6ec730386e7b3b26b8b4c88197aaed924430e7b74666a586ac997249ef"},
|
||||||
|
{file = "bcrypt-5.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d79e5c65dcc9af213594d6f7f1fa2c98ad3fc10431e7aa53c176b441943efbdd"},
|
||||||
|
{file = "bcrypt-5.0.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2b732e7d388fa22d48920baa267ba5d97cca38070b69c0e2d37087b381c681fd"},
|
||||||
|
{file = "bcrypt-5.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0c8e093ea2532601a6f686edbc2c6b2ec24131ff5c52f7610dd64fa4553b5464"},
|
||||||
|
{file = "bcrypt-5.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5b1589f4839a0899c146e8892efe320c0fa096568abd9b95593efac50a87cb75"},
|
||||||
|
{file = "bcrypt-5.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:89042e61b5e808b67daf24a434d89bab164d4de1746b37a8d173b6b14f3db9ff"},
|
||||||
|
{file = "bcrypt-5.0.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:e3cf5b2560c7b5a142286f69bde914494b6d8f901aaa71e453078388a50881c4"},
|
||||||
|
{file = "bcrypt-5.0.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f632fd56fc4e61564f78b46a2269153122db34988e78b6be8b32d28507b7eaeb"},
|
||||||
|
{file = "bcrypt-5.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:801cad5ccb6b87d1b430f183269b94c24f248dddbbc5c1f78b6ed231743e001c"},
|
||||||
|
{file = "bcrypt-5.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3cf67a804fc66fc217e6914a5635000259fbbbb12e78a99488e4d5ba445a71eb"},
|
||||||
|
{file = "bcrypt-5.0.0-cp38-abi3-win32.whl", hash = "sha256:3abeb543874b2c0524ff40c57a4e14e5d3a66ff33fb423529c88f180fd756538"},
|
||||||
|
{file = "bcrypt-5.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:35a77ec55b541e5e583eb3436ffbbf53b0ffa1fa16ca6782279daf95d146dcd9"},
|
||||||
|
{file = "bcrypt-5.0.0-cp38-abi3-win_arm64.whl", hash = "sha256:cde08734f12c6a4e28dc6755cd11d3bdfea608d93d958fffbe95a7026ebe4980"},
|
||||||
|
{file = "bcrypt-5.0.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0c418ca99fd47e9c59a301744d63328f17798b5947b0f791e9af3c1c499c2d0a"},
|
||||||
|
{file = "bcrypt-5.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddb4e1500f6efdd402218ffe34d040a1196c072e07929b9820f363a1fd1f4191"},
|
||||||
|
{file = "bcrypt-5.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7aeef54b60ceddb6f30ee3db090351ecf0d40ec6e2abf41430997407a46d2254"},
|
||||||
|
{file = "bcrypt-5.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f0ce778135f60799d89c9693b9b398819d15f1921ba15fe719acb3178215a7db"},
|
||||||
|
{file = "bcrypt-5.0.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a71f70ee269671460b37a449f5ff26982a6f2ba493b3eabdd687b4bf35f875ac"},
|
||||||
|
{file = "bcrypt-5.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f8429e1c410b4073944f03bd778a9e066e7fad723564a52ff91841d278dfc822"},
|
||||||
|
{file = "bcrypt-5.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:edfcdcedd0d0f05850c52ba3127b1fce70b9f89e0fe5ff16517df7e81fa3cbb8"},
|
||||||
|
{file = "bcrypt-5.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:611f0a17aa4a25a69362dcc299fda5c8a3d4f160e2abb3831041feb77393a14a"},
|
||||||
|
{file = "bcrypt-5.0.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:db99dca3b1fdc3db87d7c57eac0c82281242d1eabf19dcb8a6b10eb29a2e72d1"},
|
||||||
|
{file = "bcrypt-5.0.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:5feebf85a9cefda32966d8171f5db7e3ba964b77fdfe31919622256f80f9cf42"},
|
||||||
|
{file = "bcrypt-5.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3ca8a166b1140436e058298a34d88032ab62f15aae1c598580333dc21d27ef10"},
|
||||||
|
{file = "bcrypt-5.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:61afc381250c3182d9078551e3ac3a41da14154fbff647ddf52a769f588c4172"},
|
||||||
|
{file = "bcrypt-5.0.0-cp39-abi3-win32.whl", hash = "sha256:64d7ce196203e468c457c37ec22390f1a61c85c6f0b8160fd752940ccfb3a683"},
|
||||||
|
{file = "bcrypt-5.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:64ee8434b0da054d830fa8e89e1c8bf30061d539044a39524ff7dec90481e5c2"},
|
||||||
|
{file = "bcrypt-5.0.0-cp39-abi3-win_arm64.whl", hash = "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927"},
|
||||||
|
{file = "bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7edda91d5ab52b15636d9c30da87d2cc84f426c72b9dba7a9b4fe142ba11f534"},
|
||||||
|
{file = "bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:046ad6db88edb3c5ece4369af997938fb1c19d6a699b9c1b27b0db432faae4c4"},
|
||||||
|
{file = "bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:dcd58e2b3a908b5ecc9b9df2f0085592506ac2d5110786018ee5e160f28e0911"},
|
||||||
|
{file = "bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:6b8f520b61e8781efee73cba14e3e8c9556ccfb375623f4f97429544734545b4"},
|
||||||
|
{file = "bcrypt-5.0.0.tar.gz", hash = "sha256:f748f7c2d6fd375cc93d3fba7ef4a9e3a092421b8dbf34d8d4dc06be9492dfdd"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
tests = ["pytest (>=3.2.1,!=3.3.0)"]
|
||||||
|
typecheck = ["mypy"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "black"
|
name = "black"
|
||||||
version = "26.3.1"
|
version = "26.3.1"
|
||||||
|
|
@ -182,7 +259,7 @@ description = "Foreign Function Interface for Python calling C code."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
markers = "implementation_name == \"pypy\""
|
markers = "implementation_name == \"pypy\" or platform_python_implementation != \"PyPy\""
|
||||||
files = [
|
files = [
|
||||||
{file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"},
|
{file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"},
|
||||||
{file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"},
|
{file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"},
|
||||||
|
|
@ -414,14 +491,14 @@ files = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "click"
|
name = "click"
|
||||||
version = "8.3.1"
|
version = "8.3.2"
|
||||||
description = "Composable command line interface toolkit"
|
description = "Composable command line interface toolkit"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.10"
|
python-versions = ">=3.10"
|
||||||
groups = ["main", "dev", "docs", "server", "test"]
|
groups = ["main", "dev", "docs", "server", "test"]
|
||||||
files = [
|
files = [
|
||||||
{file = "click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"},
|
{file = "click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d"},
|
||||||
{file = "click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a"},
|
{file = "click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
|
@ -611,6 +688,79 @@ mypy = ["bokeh", "contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.17.0)", "
|
||||||
test = ["Pillow", "contourpy[test-no-images]", "matplotlib"]
|
test = ["Pillow", "contourpy[test-no-images]", "matplotlib"]
|
||||||
test-no-images = ["pytest", "pytest-cov", "pytest-rerunfailures", "pytest-xdist", "wurlitzer"]
|
test-no-images = ["pytest", "pytest-cov", "pytest-rerunfailures", "pytest-xdist", "wurlitzer"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cryptography"
|
||||||
|
version = "46.0.7"
|
||||||
|
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
|
||||||
|
optional = false
|
||||||
|
python-versions = "!=3.9.0,!=3.9.1,>=3.8"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4"},
|
||||||
|
{file = "cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325"},
|
||||||
|
{file = "cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308"},
|
||||||
|
{file = "cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77"},
|
||||||
|
{file = "cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1"},
|
||||||
|
{file = "cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef"},
|
||||||
|
{file = "cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de"},
|
||||||
|
{file = "cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83"},
|
||||||
|
{file = "cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb"},
|
||||||
|
{file = "cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b"},
|
||||||
|
{file = "cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85"},
|
||||||
|
{file = "cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e"},
|
||||||
|
{file = "cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457"},
|
||||||
|
{file = "cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b"},
|
||||||
|
{file = "cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842"},
|
||||||
|
{file = "cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c"},
|
||||||
|
{file = "cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902"},
|
||||||
|
{file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d"},
|
||||||
|
{file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022"},
|
||||||
|
{file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce"},
|
||||||
|
{file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f"},
|
||||||
|
{file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99"},
|
||||||
|
{file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1"},
|
||||||
|
{file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2"},
|
||||||
|
{file = "cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e"},
|
||||||
|
{file = "cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee"},
|
||||||
|
{file = "cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298"},
|
||||||
|
{file = "cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb"},
|
||||||
|
{file = "cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4"},
|
||||||
|
{file = "cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7"},
|
||||||
|
{file = "cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832"},
|
||||||
|
{file = "cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163"},
|
||||||
|
{file = "cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2"},
|
||||||
|
{file = "cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067"},
|
||||||
|
{file = "cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0"},
|
||||||
|
{file = "cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba"},
|
||||||
|
{file = "cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006"},
|
||||||
|
{file = "cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0"},
|
||||||
|
{file = "cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85"},
|
||||||
|
{file = "cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e"},
|
||||||
|
{file = "cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246"},
|
||||||
|
{file = "cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3"},
|
||||||
|
{file = "cryptography-46.0.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f"},
|
||||||
|
{file = "cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15"},
|
||||||
|
{file = "cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455"},
|
||||||
|
{file = "cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65"},
|
||||||
|
{file = "cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968"},
|
||||||
|
{file = "cryptography-46.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4"},
|
||||||
|
{file = "cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\""}
|
||||||
|
typing-extensions = {version = ">=4.13.2", markers = "python_full_version < \"3.11.0\""}
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"]
|
||||||
|
docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"]
|
||||||
|
nox = ["nox[uv] (>=2024.4.15)"]
|
||||||
|
pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.14)", "ruff (>=0.11.11)"]
|
||||||
|
sdist = ["build (>=1.0.0)"]
|
||||||
|
ssh = ["bcrypt (>=3.1.5)"]
|
||||||
|
test = ["certifi (>=2024)", "cryptography-vectors (==46.0.7)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
|
||||||
|
test-randomorder = ["pytest-randomly"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cycler"
|
name = "cycler"
|
||||||
version = "0.12.1"
|
version = "0.12.1"
|
||||||
|
|
@ -627,6 +777,18 @@ files = [
|
||||||
docs = ["ipython", "matplotlib", "numpydoc", "sphinx"]
|
docs = ["ipython", "matplotlib", "numpydoc", "sphinx"]
|
||||||
tests = ["pytest", "pytest-cov", "pytest-xdist"]
|
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]]
|
[[package]]
|
||||||
name = "dill"
|
name = "dill"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
|
|
@ -688,14 +850,14 @@ test = ["pytest (>=6)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastapi"
|
name = "fastapi"
|
||||||
version = "0.135.3"
|
version = "0.136.0"
|
||||||
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
|
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.10"
|
python-versions = ">=3.10"
|
||||||
groups = ["server", "test"]
|
groups = ["server", "test"]
|
||||||
files = [
|
files = [
|
||||||
{file = "fastapi-0.135.3-py3-none-any.whl", hash = "sha256:9b0f590c813acd13d0ab43dd8494138eb58e484bfac405db1f3187cfc5810d98"},
|
{file = "fastapi-0.136.0-py3-none-any.whl", hash = "sha256:8793d44ec7378e2be07f8a013cf7f7aa47d6327d0dfe9804862688ec4541a6b4"},
|
||||||
{file = "fastapi-0.135.3.tar.gz", hash = "sha256:bd6d7caf1a2bdd8d676843cdcd2287729572a1ef524fc4d65c17ae002a1be654"},
|
{file = "fastapi-0.136.0.tar.gz", hash = "sha256:cf08e067cc66e106e102d9ba659463abfac245200752f8a5b7b1e813de4ff73e"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
|
@ -707,19 +869,19 @@ typing-inspection = ">=0.4.2"
|
||||||
|
|
||||||
[package.extras]
|
[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)"]
|
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)"]
|
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]]
|
[[package]]
|
||||||
name = "filelock"
|
name = "filelock"
|
||||||
version = "3.25.2"
|
version = "3.29.0"
|
||||||
description = "A platform independent file lock."
|
description = "A platform independent file lock."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.10"
|
python-versions = ">=3.10"
|
||||||
groups = ["test"]
|
groups = ["test"]
|
||||||
files = [
|
files = [
|
||||||
{file = "filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70"},
|
{file = "filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258"},
|
||||||
{file = "filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694"},
|
{file = "filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -746,6 +908,7 @@ description = "The FlatBuffers serialization format for Python"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
groups = ["server", "test"]
|
groups = ["server", "test"]
|
||||||
|
markers = "python_version >= \"3.11\""
|
||||||
files = [
|
files = [
|
||||||
{file = "flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4"},
|
{file = "flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4"},
|
||||||
]
|
]
|
||||||
|
|
@ -1049,6 +1212,18 @@ files = [
|
||||||
{file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"},
|
{file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "invoke"
|
||||||
|
version = "3.0.3"
|
||||||
|
description = "Pythonic task execution"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "invoke-3.0.3-py3-none-any.whl", hash = "sha256:f11327165e5cbb89b2ad1d88d3292b5113332c43b8553b494da435d6ec6f5053"},
|
||||||
|
{file = "invoke-3.0.3.tar.gz", hash = "sha256:437b6a622223824380bfb4e64f612711a6b648c795f565efc8625af66fb57f0c"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "isort"
|
name = "isort"
|
||||||
version = "5.13.2"
|
version = "5.13.2"
|
||||||
|
|
@ -1443,6 +1618,7 @@ description = "Python library for arbitrary-precision floating-point arithmetic"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
groups = ["server", "test"]
|
groups = ["server", "test"]
|
||||||
|
markers = "python_version >= \"3.11\""
|
||||||
files = [
|
files = [
|
||||||
{file = "mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c"},
|
{file = "mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c"},
|
||||||
{file = "mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f"},
|
{file = "mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f"},
|
||||||
|
|
@ -1468,14 +1644,14 @@ files = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "narwhals"
|
name = "narwhals"
|
||||||
version = "2.18.1"
|
version = "2.20.0"
|
||||||
description = "Extremely lightweight compatibility layer between dataframe libraries"
|
description = "Extremely lightweight compatibility layer between dataframe libraries"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "narwhals-2.18.1-py3-none-any.whl", hash = "sha256:a0a8bb80205323851338888ba3a12b4f65d352362c8a94be591244faf36504ad"},
|
{file = "narwhals-2.20.0-py3-none-any.whl", hash = "sha256:16e750ea5507d4ba6e8d03455b5f93a535e0405976561baea235bca5dc9f475d"},
|
||||||
{file = "narwhals-2.18.1.tar.gz", hash = "sha256:652a1fcc9d432bbf114846688884c215f17eb118aa640b7419295d2f910d2a8b"},
|
{file = "narwhals-2.20.0.tar.gz", hash = "sha256:c10994975fa7dc5a68c2cffcddbd5908fc8ebb2d463c5bab085309c0ee1f551e"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
|
|
@ -1537,48 +1713,7 @@ files = [
|
||||||
{file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"},
|
{file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"},
|
||||||
{file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"},
|
{file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"},
|
||||||
]
|
]
|
||||||
|
markers = {server = "python_version >= \"3.11\"", test = "python_version >= \"3.11\""}
|
||||||
[[package]]
|
|
||||||
name = "onnxruntime"
|
|
||||||
version = "1.24.3"
|
|
||||||
description = "ONNX Runtime is a runtime accelerator for Machine Learning models"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.10"
|
|
||||||
groups = ["server", "test"]
|
|
||||||
markers = "python_version == \"3.10\""
|
|
||||||
files = [
|
|
||||||
{file = "onnxruntime-1.24.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3e6456801c66b095c5cd68e690ca25db970ea5202bd0c5b84a2c3ef7731c5a3c"},
|
|
||||||
{file = "onnxruntime-1.24.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b2ebc54c6d8281dccff78d4b06e47d4cf07535937584ab759448390a70f4978"},
|
|
||||||
{file = "onnxruntime-1.24.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb56575d7794bf0781156955610c9e651c9504c64d42ec880784b6106244882d"},
|
|
||||||
{file = "onnxruntime-1.24.3-cp311-cp311-win_amd64.whl", hash = "sha256:c958222ef9eff54018332beecd32d5d94a3ab079d8821937b333811bf4da0d39"},
|
|
||||||
{file = "onnxruntime-1.24.3-cp311-cp311-win_arm64.whl", hash = "sha256:a8f761857ebaf58a85b9e42422d03207f1d39e6bb8fecfdbf613bac5b9710723"},
|
|
||||||
{file = "onnxruntime-1.24.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:0d244227dc5e00a9ae15a7ac1eba4c4460d7876dfecafe73fb00db9f1d914d91"},
|
|
||||||
{file = "onnxruntime-1.24.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a9847b870b6cb462652b547bc98c49e0efb67553410a082fde1918a38707452"},
|
|
||||||
{file = "onnxruntime-1.24.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b354afce3333f2859c7e8706d84b6c552beac39233bcd3141ce7ab77b4cabb5d"},
|
|
||||||
{file = "onnxruntime-1.24.3-cp312-cp312-win_amd64.whl", hash = "sha256:44ea708c34965439170d811267c51281d3897ecfc4aa0087fa25d4a4c3eb2e4a"},
|
|
||||||
{file = "onnxruntime-1.24.3-cp312-cp312-win_arm64.whl", hash = "sha256:48d1092b44ca2ba6f9543892e7c422c15a568481403c10440945685faf27a8d8"},
|
|
||||||
{file = "onnxruntime-1.24.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:34a0ea5ff191d8420d9c1332355644148b1bf1a0d10c411af890a63a9f662aa7"},
|
|
||||||
{file = "onnxruntime-1.24.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fd2ec7bb0fabe42f55e8337cfc9b1969d0d14622711aac73d69b4bd5abb5ed7"},
|
|
||||||
{file = "onnxruntime-1.24.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df8e70e732fe26346faaeec9147fa38bef35d232d2495d27e93dd221a2d473a9"},
|
|
||||||
{file = "onnxruntime-1.24.3-cp313-cp313-win_amd64.whl", hash = "sha256:2d3706719be6ad41d38a2250998b1d87758a20f6ea4546962e21dc79f1f1fd2b"},
|
|
||||||
{file = "onnxruntime-1.24.3-cp313-cp313-win_arm64.whl", hash = "sha256:b082f3ba9519f0a1a1e754556bc7e635c7526ef81b98b3f78da4455d25f0437b"},
|
|
||||||
{file = "onnxruntime-1.24.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72f956634bc2e4bd2e8b006bef111849bd42c42dea37bd0a4c728404fdaf4d34"},
|
|
||||||
{file = "onnxruntime-1.24.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78d1f25eed4ab9959db70a626ed50ee24cf497e60774f59f1207ac8556399c4d"},
|
|
||||||
{file = "onnxruntime-1.24.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:a6b4bce87d96f78f0a9bf5cefab3303ae95d558c5bfea53d0bf7f9ea207880a8"},
|
|
||||||
{file = "onnxruntime-1.24.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d48f36c87b25ab3b2b4c88826c96cf1399a5631e3c2c03cc27d6a1e5d6b18eb4"},
|
|
||||||
{file = "onnxruntime-1.24.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e104d33a409bf6e3f30f0e8198ec2aaf8d445b8395490a80f6e6ad56da98e400"},
|
|
||||||
{file = "onnxruntime-1.24.3-cp314-cp314-win_amd64.whl", hash = "sha256:e785d73fbd17421c2513b0bb09eb25d88fa22c8c10c3f5d6060589efa5537c5b"},
|
|
||||||
{file = "onnxruntime-1.24.3-cp314-cp314-win_arm64.whl", hash = "sha256:951e897a275f897a05ffbcaa615d98777882decaeb80c9216c68cdc62f849f53"},
|
|
||||||
{file = "onnxruntime-1.24.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d4e70ce578aa214c74c7a7a9226bc8e229814db4a5b2d097333b81279ecde36"},
|
|
||||||
{file = "onnxruntime-1.24.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02aaf6ddfa784523b6873b4176a79d508e599efe12ab0ea1a3a6e7314408b7aa"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
flatbuffers = "*"
|
|
||||||
numpy = ">=1.21.6"
|
|
||||||
packaging = "*"
|
|
||||||
protobuf = "*"
|
|
||||||
sympy = "*"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "onnxruntime"
|
name = "onnxruntime"
|
||||||
|
|
@ -1624,15 +1759,16 @@ sympy = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "packaging"
|
name = "packaging"
|
||||||
version = "26.0"
|
version = "26.1"
|
||||||
description = "Core utilities for Python packages"
|
description = "Core utilities for Python packages"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
groups = ["main", "dev", "docs", "server", "test"]
|
groups = ["main", "dev", "docs", "server", "test"]
|
||||||
files = [
|
files = [
|
||||||
{file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"},
|
{file = "packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f"},
|
||||||
{file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"},
|
{file = "packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de"},
|
||||||
]
|
]
|
||||||
|
markers = {server = "python_version >= \"3.11\""}
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pandas"
|
name = "pandas"
|
||||||
|
|
@ -1701,9 +1837,9 @@ files = [
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
numpy = [
|
numpy = [
|
||||||
{version = ">=1.22.4", markers = "python_version < \"3.11\""},
|
|
||||||
{version = ">=1.26.0", markers = "python_version >= \"3.12\""},
|
{version = ">=1.26.0", markers = "python_version >= \"3.12\""},
|
||||||
{version = ">=1.23.2", markers = "python_version == \"3.11\""},
|
{version = ">=1.23.2", markers = "python_version == \"3.11\""},
|
||||||
|
{version = ">=1.22.4", markers = "python_version < \"3.11\""},
|
||||||
]
|
]
|
||||||
python-dateutil = ">=2.8.2"
|
python-dateutil = ">=2.8.2"
|
||||||
pytz = ">=2020.1"
|
pytz = ">=2020.1"
|
||||||
|
|
@ -1734,6 +1870,27 @@ sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-d
|
||||||
test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"]
|
test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"]
|
||||||
xml = ["lxml (>=4.9.2)"]
|
xml = ["lxml (>=4.9.2)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "paramiko"
|
||||||
|
version = "4.0.0"
|
||||||
|
description = "SSH2 protocol library"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "paramiko-4.0.0-py3-none-any.whl", hash = "sha256:0e20e00ac666503bf0b4eda3b6d833465a2b7aff2e2b3d79a8bba5ef144ee3b9"},
|
||||||
|
{file = "paramiko-4.0.0.tar.gz", hash = "sha256:6a25f07b380cc9c9a88d2b920ad37167ac4667f8d9886ccebd8f90f654b5d69f"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
bcrypt = ">=3.2"
|
||||||
|
cryptography = ">=3.3"
|
||||||
|
invoke = ">=2.0"
|
||||||
|
pynacl = ">=1.5"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
gssapi = ["gssapi (>=1.4.1) ; platform_system != \"Windows\"", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8) ; platform_system == \"Windows\""]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pathspec"
|
name = "pathspec"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
|
|
@ -1863,26 +2020,26 @@ xmp = ["defusedxml"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "platformdirs"
|
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`."
|
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.10"
|
python-versions = ">=3.10"
|
||||||
groups = ["dev", "test"]
|
groups = ["dev", "test"]
|
||||||
files = [
|
files = [
|
||||||
{file = "platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868"},
|
{file = "platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917"},
|
||||||
{file = "platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934"},
|
{file = "platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "plotly"
|
name = "plotly"
|
||||||
version = "6.6.0"
|
version = "6.7.0"
|
||||||
description = "An open-source interactive data visualization library for Python"
|
description = "An open-source interactive data visualization library for Python"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "plotly-6.6.0-py3-none-any.whl", hash = "sha256:8d6daf0f87412e0c0bfe72e809d615217ab57cc715899a1e5145135a7800d1d0"},
|
{file = "plotly-6.7.0-py3-none-any.whl", hash = "sha256:ac8aca1c25c663a59b5b9140a549264a5badde2e057d79b8c772ae2920e32ff0"},
|
||||||
{file = "plotly-6.6.0.tar.gz", hash = "sha256:b897f15f3b02028d69f755f236be890ba950d0a42d7dfc619b44e2d8cea8748c"},
|
{file = "plotly-6.7.0.tar.gz", hash = "sha256:45eea0ff27e2a23ccd62776f77eb43aa1ca03df4192b76036e380bb479b892c6"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
|
@ -1890,11 +2047,11 @@ narwhals = ">=1.15.1"
|
||||||
packaging = "*"
|
packaging = "*"
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
dev = ["plotly[dev-optional]"]
|
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", "jupyter", "plotly[dev-core]"]
|
dev-build = ["build", "jupyterlab", "pytest", "requests", "ruff (==0.11.12)"]
|
||||||
dev-core = ["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"]
|
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"]
|
express = ["numpy (>=1.22)"]
|
||||||
kaleido = ["kaleido (>=1.1.0)"]
|
kaleido = ["kaleido (>=1.1.0)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1920,6 +2077,7 @@ description = ""
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.10"
|
python-versions = ">=3.10"
|
||||||
groups = ["server", "test"]
|
groups = ["server", "test"]
|
||||||
|
markers = "python_version >= \"3.11\""
|
||||||
files = [
|
files = [
|
||||||
{file = "protobuf-7.34.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:d8b2cc79c4d8f62b293ad9b11ec3aebce9af481fa73e64556969f7345ebf9fc7"},
|
{file = "protobuf-7.34.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:d8b2cc79c4d8f62b293ad9b11ec3aebce9af481fa73e64556969f7345ebf9fc7"},
|
||||||
{file = "protobuf-7.34.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:5185e0e948d07abe94bb76ec9b8416b604cfe5da6f871d67aad30cbf24c3110b"},
|
{file = "protobuf-7.34.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:5185e0e948d07abe94bb76ec9b8416b604cfe5da6f871d67aad30cbf24c3110b"},
|
||||||
|
|
@ -1950,7 +2108,7 @@ description = "C parser in Python"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.10"
|
python-versions = ">=3.10"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
markers = "implementation_name == \"pypy\""
|
markers = "(platform_python_implementation != \"PyPy\" or implementation_name == \"pypy\") and implementation_name != \"PyPy\""
|
||||||
files = [
|
files = [
|
||||||
{file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"},
|
{file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"},
|
||||||
{file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"},
|
{file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"},
|
||||||
|
|
@ -1958,19 +2116,19 @@ files = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pydantic"
|
name = "pydantic"
|
||||||
version = "2.12.5"
|
version = "2.13.3"
|
||||||
description = "Data validation using Python type hints"
|
description = "Data validation using Python type hints"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
groups = ["server", "test"]
|
groups = ["server", "test"]
|
||||||
files = [
|
files = [
|
||||||
{file = "pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d"},
|
{file = "pydantic-2.13.3-py3-none-any.whl", hash = "sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927"},
|
||||||
{file = "pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49"},
|
{file = "pydantic-2.13.3.tar.gz", hash = "sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
annotated-types = ">=0.6.0"
|
annotated-types = ">=0.6.0"
|
||||||
pydantic-core = "2.41.5"
|
pydantic-core = "2.46.3"
|
||||||
typing-extensions = ">=4.14.1"
|
typing-extensions = ">=4.14.1"
|
||||||
typing-inspection = ">=0.4.2"
|
typing-inspection = ">=0.4.2"
|
||||||
|
|
||||||
|
|
@ -1980,133 +2138,132 @@ timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pydantic-core"
|
name = "pydantic-core"
|
||||||
version = "2.41.5"
|
version = "2.46.3"
|
||||||
description = "Core functionality for Pydantic validation and serialization"
|
description = "Core functionality for Pydantic validation and serialization"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
groups = ["server", "test"]
|
groups = ["server", "test"]
|
||||||
files = [
|
files = [
|
||||||
{file = "pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146"},
|
{file = "pydantic_core-2.46.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:1da3786b8018e60349680720158cc19161cc3b4bdd815beb0a321cd5ce1ad5b1"},
|
||||||
{file = "pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2"},
|
{file = "pydantic_core-2.46.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cc0988cb29d21bf4a9d5cf2ef970b5c0e38d8d8e107a493278c05dc6c1dda69f"},
|
||||||
{file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97"},
|
{file = "pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27f9067c3bfadd04c55484b89c0d267981b2f3512850f6f66e1e74204a4e4ce3"},
|
||||||
{file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9"},
|
{file = "pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a642ac886ecf6402d9882d10c405dcf4b902abeb2972cd5fb4a48c83cd59279a"},
|
||||||
{file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52"},
|
{file = "pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79f561438481f28681584b89e2effb22855e2179880314bcddbf5968e935e807"},
|
||||||
{file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941"},
|
{file = "pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57a973eae4665352a47cf1a99b4ee864620f2fe663a217d7a8da68a1f3a5bfda"},
|
||||||
{file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a"},
|
{file = "pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83d002b97072a53ea150d63e0a3adfae5670cef5aa8a6e490240e482d3b22e57"},
|
||||||
{file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c"},
|
{file = "pydantic_core-2.46.3-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b40ddd51e7c44b28cfaef746c9d3c506d658885e0a46f9eeef2ee815cbf8e045"},
|
||||||
{file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2"},
|
{file = "pydantic_core-2.46.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac5ec7fb9b87f04ee839af2d53bcadea57ded7d229719f56c0ed895bff987943"},
|
||||||
{file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556"},
|
{file = "pydantic_core-2.46.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a3b11c812f61b3129c4905781a2601dfdfdea5fe1e6c1cfb696b55d14e9c054f"},
|
||||||
{file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49"},
|
{file = "pydantic_core-2.46.3-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:1108da631e602e5b3c38d6d04fe5bb3bfa54349e6918e3ca6cf570b2e2b2f9d4"},
|
||||||
{file = "pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba"},
|
{file = "pydantic_core-2.46.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:de885175515bcfa98ae618c1df7a072f13d179f81376c8007112af20567fd08a"},
|
||||||
{file = "pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9"},
|
{file = "pydantic_core-2.46.3-cp310-cp310-win32.whl", hash = "sha256:d11058e3201527d41bc6b545c79187c9e4bf85e15a236a6007f0e991518882b7"},
|
||||||
{file = "pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6"},
|
{file = "pydantic_core-2.46.3-cp310-cp310-win_amd64.whl", hash = "sha256:3612edf65c8ea67ac13616c4d23af12faef1ae435a8a93e5934c2a0cbbdd1fd6"},
|
||||||
{file = "pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b"},
|
{file = "pydantic_core-2.46.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ab124d49d0459b2373ecf54118a45c28a1e6d4192a533fbc915e70f556feb8e5"},
|
||||||
{file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a"},
|
{file = "pydantic_core-2.46.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cca67d52a5c7a16aed2b3999e719c4bcf644074eac304a5d3d62dd70ae7d4b2c"},
|
||||||
{file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8"},
|
{file = "pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c024e08c0ba23e6fd68c771a521e9d6a792f2ebb0fa734296b36394dc30390e"},
|
||||||
{file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e"},
|
{file = "pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6645ce7eec4928e29a1e3b3d5c946621d105d3e79f0c9cddf07c2a9770949287"},
|
||||||
{file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1"},
|
{file = "pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a712c7118e6c5ea96562f7b488435172abb94a3c53c22c9efc1412264a45cbbe"},
|
||||||
{file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b"},
|
{file = "pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69a868ef3ff206343579021c40faf3b1edc64b1cc508ff243a28b0a514ccb050"},
|
||||||
{file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b"},
|
{file = "pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc7e8c32db809aa0f6ea1d6869ebc8518a65d5150fdfad8bcae6a49ae32a22e2"},
|
||||||
{file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284"},
|
{file = "pydantic_core-2.46.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:3481bd1341dc85779ee506bc8e1196a277ace359d89d28588a9468c3ecbe63fa"},
|
||||||
{file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594"},
|
{file = "pydantic_core-2.46.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8690eba565c6d68ffd3a8655525cbdd5246510b44a637ee2c6c03a7ebfe64d3c"},
|
||||||
{file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e"},
|
{file = "pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4de88889d7e88d50d40ee5b39d5dac0bcaef9ba91f7e536ac064e6b2834ecccf"},
|
||||||
{file = "pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b"},
|
{file = "pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:e480080975c1ef7f780b8f99ed72337e7cc5efea2e518a20a692e8e7b278eb8b"},
|
||||||
{file = "pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe"},
|
{file = "pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:de3a5c376f8cd94da9a1b8fd3dd1c16c7a7b216ed31dc8ce9fd7a22bf13b836e"},
|
||||||
{file = "pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f"},
|
{file = "pydantic_core-2.46.3-cp311-cp311-win32.whl", hash = "sha256:fc331a5314ffddd5385b9ee9d0d2fee0b13c27e0e02dad71b1ae5d6561f51eeb"},
|
||||||
{file = "pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7"},
|
{file = "pydantic_core-2.46.3-cp311-cp311-win_amd64.whl", hash = "sha256:b5b9c6cf08a8a5e502698f5e153056d12c34b8fb30317e0c5fd06f45162a6346"},
|
||||||
{file = "pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0"},
|
{file = "pydantic_core-2.46.3-cp311-cp311-win_arm64.whl", hash = "sha256:5dfd51cf457482f04ec49491811a2b8fd5b843b64b11eecd2d7a1ee596ea78a6"},
|
||||||
{file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69"},
|
{file = "pydantic_core-2.46.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b11b59b3eee90a80a36701ddb4576d9ae31f93f05cb9e277ceaa09e6bf074a67"},
|
||||||
{file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75"},
|
{file = "pydantic_core-2.46.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:af8653713055ea18a3abc1537fe2ebc42f5b0bbb768d1eb79fd74eb47c0ac089"},
|
||||||
{file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05"},
|
{file = "pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75a519dab6d63c514f3a81053e5266c549679e4aa88f6ec57f2b7b854aceb1b0"},
|
||||||
{file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc"},
|
{file = "pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6cd87cb1575b1ad05ba98894c5b5c96411ef678fa2f6ed2576607095b8d9789"},
|
||||||
{file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c"},
|
{file = "pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f80a55484b8d843c8ada81ebf70a682f3f00a3d40e378c06cf17ecb44d280d7d"},
|
||||||
{file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5"},
|
{file = "pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3861f1731b90c50a3266316b9044f5c9b405eecb8e299b0a7120596334e4fe9c"},
|
||||||
{file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c"},
|
{file = "pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb528e295ed31570ac3dcc9bfdd6e0150bc11ce6168ac87a8082055cf1a67395"},
|
||||||
{file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294"},
|
{file = "pydantic_core-2.46.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:367508faa4973b992b271ba1494acaab36eb7e8739d1e47be5035fb1ea225396"},
|
||||||
{file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1"},
|
{file = "pydantic_core-2.46.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ad3c826fe523e4becf4fe39baa44286cff85ef137c729a2c5e269afbfd0905d"},
|
||||||
{file = "pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d"},
|
{file = "pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ec638c5d194ef8af27db69f16c954a09797c0dc25015ad6123eb2c73a4d271ca"},
|
||||||
{file = "pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815"},
|
{file = "pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:28ed528c45446062ee66edb1d33df5d88828ae167de76e773a3c7f64bd14e976"},
|
||||||
{file = "pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3"},
|
{file = "pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aed19d0c783886d5bd86d80ae5030006b45e28464218747dcf83dabfdd092c7b"},
|
||||||
{file = "pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9"},
|
{file = "pydantic_core-2.46.3-cp312-cp312-win32.whl", hash = "sha256:06d5d8820cbbdb4147578c1fe7ffcd5b83f34508cb9f9ab76e807be7db6ff0a4"},
|
||||||
{file = "pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34"},
|
{file = "pydantic_core-2.46.3-cp312-cp312-win_amd64.whl", hash = "sha256:c3212fda0ee959c1dd04c60b601ec31097aaa893573a3a1abd0a47bcac2968c1"},
|
||||||
{file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0"},
|
{file = "pydantic_core-2.46.3-cp312-cp312-win_arm64.whl", hash = "sha256:f1f8338dd7a7f31761f1f1a3c47503a9a3b34eea3c8b01fa6ee96408affb5e72"},
|
||||||
{file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33"},
|
{file = "pydantic_core-2.46.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:12bc98de041458b80c86c56b24df1d23832f3e166cbaff011f25d187f5c62c37"},
|
||||||
{file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e"},
|
{file = "pydantic_core-2.46.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:85348b8f89d2c3508b65b16c3c33a4da22b8215138d8b996912bb1532868885f"},
|
||||||
{file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2"},
|
{file = "pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1105677a6df914b1fb71a81b96c8cce7726857e1717d86001f29be06a25ee6f8"},
|
||||||
{file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586"},
|
{file = "pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87082cd65669a33adeba5470769e9704c7cf026cc30afb9cc77fd865578ebaad"},
|
||||||
{file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d"},
|
{file = "pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e5f66e12c4f5212d08522963380eaaeac5ebd795826cfd19b2dfb0c7a52b9c"},
|
||||||
{file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740"},
|
{file = "pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6cdf19bf84128d5e7c37e8a73a0c5c10d51103a650ac585d42dd6ae233f2b7f"},
|
||||||
{file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e"},
|
{file = "pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031bb17f4885a43773c8c763089499f242aee2ea85cf17154168775dccdecf35"},
|
||||||
{file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858"},
|
{file = "pydantic_core-2.46.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:bcf2a8b2982a6673693eae7348ef3d8cf3979c1d63b54fca7c397a635cc68687"},
|
||||||
{file = "pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36"},
|
{file = "pydantic_core-2.46.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28e8cf2f52d72ced402a137145923a762cbb5081e48b34312f7a0c8f55928ec3"},
|
||||||
{file = "pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11"},
|
{file = "pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:17eaface65d9fc5abb940003020309c1bf7a211f5f608d7870297c367e6f9022"},
|
||||||
{file = "pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd"},
|
{file = "pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:93fd339f23408a07e98950a89644f92c54d8729719a40b30c0a30bb9ebc55d23"},
|
||||||
{file = "pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a"},
|
{file = "pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:23cbdb3aaa74dfe0837975dbf69b469753bbde8eacace524519ffdb6b6e89eb7"},
|
||||||
{file = "pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14"},
|
{file = "pydantic_core-2.46.3-cp313-cp313-win32.whl", hash = "sha256:610eda2e3838f401105e6326ca304f5da1e15393ae25dacae5c5c63f2c275b13"},
|
||||||
{file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1"},
|
{file = "pydantic_core-2.46.3-cp313-cp313-win_amd64.whl", hash = "sha256:68cc7866ed863db34351294187f9b729964c371ba33e31c26f478471c52e1ed0"},
|
||||||
{file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66"},
|
{file = "pydantic_core-2.46.3-cp313-cp313-win_arm64.whl", hash = "sha256:f64b5537ac62b231572879cd08ec05600308636a5d63bcbdb15063a466977bec"},
|
||||||
{file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869"},
|
{file = "pydantic_core-2.46.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:afa3aa644f74e290cdede48a7b0bee37d1c35e71b05105f6b340d484af536d9b"},
|
||||||
{file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2"},
|
{file = "pydantic_core-2.46.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ced3310e51aa425f7f77da8bbbb5212616655bedbe82c70944320bc1dbe5e018"},
|
||||||
{file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375"},
|
{file = "pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e29908922ce9da1a30b4da490bd1d3d82c01dcfdf864d2a74aacee674d0bfa34"},
|
||||||
{file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553"},
|
{file = "pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0c9ff69140423eea8ed2d5477df3ba037f671f5e897d206d921bc9fdc39613e7"},
|
||||||
{file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90"},
|
{file = "pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b675ab0a0d5b1c8fdb81195dc5bcefea3f3c240871cdd7ff9a2de8aa50772eb2"},
|
||||||
{file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07"},
|
{file = "pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0087084960f209a9a4af50ecd1fb063d9ad3658c07bb81a7a53f452dacbfb2ba"},
|
||||||
{file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb"},
|
{file = "pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed42e6cc8e1b0e2b9b96e2276bad70ae625d10d6d524aed0c93de974ae029f9f"},
|
||||||
{file = "pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23"},
|
{file = "pydantic_core-2.46.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:f1771ce258afb3e4201e67d154edbbae712a76a6081079fe247c2f53c6322c22"},
|
||||||
{file = "pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf"},
|
{file = "pydantic_core-2.46.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a7610b6a5242a6c736d8ad47fd5fff87fcfe8f833b281b1c409c3d6835d9227f"},
|
||||||
{file = "pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0"},
|
{file = "pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:ff5e7783bcc5476e1db448bf268f11cb257b1c276d3e89f00b5727be86dd0127"},
|
||||||
{file = "pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a"},
|
{file = "pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:9d2e32edcc143bc01e95300671915d9ca052d4f745aa0a49c48d4803f8a85f2c"},
|
||||||
{file = "pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3"},
|
{file = "pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6e42d83d1c6b87fa56b521479cff237e626a292f3b31b6345c15a99121b454c1"},
|
||||||
{file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c"},
|
{file = "pydantic_core-2.46.3-cp314-cp314-win32.whl", hash = "sha256:07bc6d2a28c3adb4f7c6ae46aa4f2d2929af127f587ed44057af50bf1ce0f505"},
|
||||||
{file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612"},
|
{file = "pydantic_core-2.46.3-cp314-cp314-win_amd64.whl", hash = "sha256:8940562319bc621da30714617e6a7eaa6b98c84e8c685bcdc02d7ed5e7c7c44e"},
|
||||||
{file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d"},
|
{file = "pydantic_core-2.46.3-cp314-cp314-win_arm64.whl", hash = "sha256:5dcbbcf4d22210ced8f837c96db941bdb078f419543472aca5d9a0bb7cddc7df"},
|
||||||
{file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9"},
|
{file = "pydantic_core-2.46.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d0fe3dce1e836e418f912c1ad91c73357d03e556a4d286f441bf34fed2dbeecf"},
|
||||||
{file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660"},
|
{file = "pydantic_core-2.46.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9ce92e58abc722dac1bf835a6798a60b294e48eb0e625ec9fd994b932ac5feee"},
|
||||||
{file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9"},
|
{file = "pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a03e6467f0f5ab796a486146d1b887b2dc5e5f9b3288898c1b1c3ad974e53e4a"},
|
||||||
{file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3"},
|
{file = "pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2798b6ba041b9d70acfb9071a2ea13c8456dd1e6a5555798e41ba7b0790e329c"},
|
||||||
{file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf"},
|
{file = "pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9be3e221bdc6d69abf294dcf7aff6af19c31a5cdcc8f0aa3b14be29df4bd03b1"},
|
||||||
{file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470"},
|
{file = "pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13936129ce841f2a5ddf6f126fea3c43cd128807b5a59588c37cf10178c2e64"},
|
||||||
{file = "pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa"},
|
{file = "pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28b5f2ef03416facccb1c6ef744c69793175fd27e44ef15669201601cf423acb"},
|
||||||
{file = "pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c"},
|
{file = "pydantic_core-2.46.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:830d1247d77ad23852314f069e9d7ddafeec5f684baf9d7e7065ed46a049c4e6"},
|
||||||
{file = "pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008"},
|
{file = "pydantic_core-2.46.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0793c90c1a3c74966e7975eaef3ed30ebdff3260a0f815a62a22adc17e4c01c"},
|
||||||
{file = "pydantic_core-2.41.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf"},
|
{file = "pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d2d0aead851b66f5245ec0c4fb2612ef457f8bbafefdf65a2bf9d6bac6140f47"},
|
||||||
{file = "pydantic_core-2.41.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5"},
|
{file = "pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:2f40e4246676beb31c5ce77c38a55ca4e465c6b38d11ea1bd935420568e0b1ab"},
|
||||||
{file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d"},
|
{file = "pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:cf489cf8986c543939aeee17a09c04d6ffb43bfef8ca16fcbcc5cfdcbed24dba"},
|
||||||
{file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60"},
|
{file = "pydantic_core-2.46.3-cp314-cp314t-win32.whl", hash = "sha256:ffe0883b56cfc05798bf994164d2b2ff03efe2d22022a2bb080f3b626176dd56"},
|
||||||
{file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82"},
|
{file = "pydantic_core-2.46.3-cp314-cp314t-win_amd64.whl", hash = "sha256:706d9d0ce9cf4593d07270d8e9f53b161f90c57d315aeec4fb4fd7a8b10240d8"},
|
||||||
{file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5"},
|
{file = "pydantic_core-2.46.3-cp314-cp314t-win_arm64.whl", hash = "sha256:77706aeb41df6a76568434701e0917da10692da28cb69d5fb6919ce5fdb07374"},
|
||||||
{file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3"},
|
{file = "pydantic_core-2.46.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:fa3eb7c2995aa443687a825bc30395c8521b7c6ec201966e55debfd1128bcceb"},
|
||||||
{file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425"},
|
{file = "pydantic_core-2.46.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3d08782c4045f90724b44c95d35ebec0d67edb8a957a2ac81d5a8e4b8a200495"},
|
||||||
{file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504"},
|
{file = "pydantic_core-2.46.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:831eb19aa789a97356979e94c981e5667759301fb708d1c0d5adf1bc0098b873"},
|
||||||
{file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5"},
|
{file = "pydantic_core-2.46.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4335e87c7afa436a0dfa899e138d57a72f8aad542e2cf19c36fb428461caabd0"},
|
||||||
{file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3"},
|
{file = "pydantic_core-2.46.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99421e7684a60f7f3550a1d159ade5fdff1954baedb6bdd407cba6a307c9f27d"},
|
||||||
{file = "pydantic_core-2.41.5-cp39-cp39-win32.whl", hash = "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460"},
|
{file = "pydantic_core-2.46.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd81f6907932ebac3abbe41378dac64b2380db1287e2aa64d8d88f78d170f51a"},
|
||||||
{file = "pydantic_core-2.41.5-cp39-cp39-win_amd64.whl", hash = "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b"},
|
{file = "pydantic_core-2.46.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f247596366f4221af52beddd65af1218797771d6989bc891a0b86ccaa019168"},
|
||||||
{file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034"},
|
{file = "pydantic_core-2.46.3-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:6dff8cc884679df229ebc6d8eb2321ea6f8e091bc7d4886d4dc2e0e71452843c"},
|
||||||
{file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c"},
|
{file = "pydantic_core-2.46.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68ef2f623dda6d5a9067ac014e406c020c780b2a358930a7e5c1b73702900720"},
|
||||||
{file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2"},
|
{file = "pydantic_core-2.46.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d56bdb4af1767cc15b0386b3c581fdfe659bb9ee4a4f776e92c1cd9d074000d6"},
|
||||||
{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.46.3-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:91249bcb7c165c2fb2a2f852dbc5c91636e2e218e75d96dfdd517e4078e173dd"},
|
||||||
{file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd"},
|
{file = "pydantic_core-2.46.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4b068543bdb707f5d935dab765d99227aa2545ef2820935f2e5dd801795c7dbd"},
|
||||||
{file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc"},
|
{file = "pydantic_core-2.46.3-cp39-cp39-win32.whl", hash = "sha256:dcda6583921c05a40533f982321532f2d8db29326c7b95c4026941fa5074bd79"},
|
||||||
{file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56"},
|
{file = "pydantic_core-2.46.3-cp39-cp39-win_amd64.whl", hash = "sha256:a35cc284c8dd7edae8a31533713b4d2467dfe7c4f1b5587dd4031f28f90d1d13"},
|
||||||
{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.46.3-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:9715525891ed524a0a1eb6d053c74d4d4ad5017677fb00af0b7c2644a31bae46"},
|
||||||
{file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8"},
|
{file = "pydantic_core-2.46.3-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:9d2f400712a99a013aff420ef1eb9be077f8189a36c1e3ef87660b4e1088a874"},
|
||||||
{file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a"},
|
{file = "pydantic_core-2.46.3-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd2aab0e2e9dc2daf36bd2686c982535d5e7b1d930a1344a7bb6e82baab42a76"},
|
||||||
{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.46.3-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e9d76736da5f362fabfeea6a69b13b7f2be405c6d6966f06b2f6bfff7e64531"},
|
||||||
{file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2"},
|
{file = "pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:b12dd51f1187c2eb489af8e20f880362db98e954b54ab792fa5d92e8bcc6b803"},
|
||||||
{file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093"},
|
{file = "pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f00a0961b125f1a47af7bcc17f00782e12f4cd056f83416006b30111d941dfa3"},
|
||||||
{file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a"},
|
{file = "pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57697d7c056aca4bbb680200f96563e841a6386ac1129370a0102592f4dddff5"},
|
||||||
{file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963"},
|
{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.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a"},
|
{file = "pydantic_core-2.46.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:13afdd885f3d71280cf286b13b310ee0f7ccfefd1dbbb661514a474b726e2f25"},
|
||||||
{file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26"},
|
{file = "pydantic_core-2.46.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f91c0aff3e3ee0928edd1232c57f643a7a003e6edf1860bc3afcdc749cb513f3"},
|
||||||
{file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808"},
|
{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.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc"},
|
{file = "pydantic_core-2.46.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:975c267cff4f7e7272eacbe50f6cc03ca9a3da4c4fbd66fffd89c94c1e311aa1"},
|
||||||
{file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1"},
|
{file = "pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2b8e4f2bbdf71415c544b4b1138b8060db7b6611bc927e8064c769f64bed651c"},
|
||||||
{file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84"},
|
{file = "pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e61ea8e9fff9606d09178f577ff8ccdd7206ff73d6552bcec18e1033c4254b85"},
|
||||||
{file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770"},
|
{file = "pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b504bda01bafc69b6d3c7a0c7f039dcf60f47fab70e06fe23f57b5c75bdc82b8"},
|
||||||
{file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f"},
|
{file = "pydantic_core-2.46.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b00b76f7142fc60c762ce579bd29c8fa44aaa56592dd3c54fab3928d0d4ca6ff"},
|
||||||
{file = "pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51"},
|
{file = "pydantic_core-2.46.3.tar.gz", hash = "sha256:41c178f65b8c29807239d47e6050262eb6bf84eb695e41101e62e38df4a5bc2c"},
|
||||||
{file = "pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e"},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
|
@ -2155,9 +2312,9 @@ files = [
|
||||||
astroid = ">=3.3.8,<=3.4.0.dev0"
|
astroid = ">=3.3.8,<=3.4.0.dev0"
|
||||||
colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
|
colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
|
||||||
dill = [
|
dill = [
|
||||||
{version = ">=0.2", markers = "python_version < \"3.11\""},
|
|
||||||
{version = ">=0.3.7", markers = "python_version >= \"3.12\""},
|
{version = ">=0.3.7", markers = "python_version >= \"3.12\""},
|
||||||
{version = ">=0.3.6", markers = "python_version == \"3.11\""},
|
{version = ">=0.3.6", markers = "python_version == \"3.11\""},
|
||||||
|
{version = ">=0.2", markers = "python_version < \"3.11\""},
|
||||||
]
|
]
|
||||||
isort = ">=4.2.5,<5.13 || >5.13,<7"
|
isort = ">=4.2.5,<5.13 || >5.13,<7"
|
||||||
mccabe = ">=0.6,<0.8"
|
mccabe = ">=0.6,<0.8"
|
||||||
|
|
@ -2169,6 +2326,48 @@ tomlkit = ">=0.10.1"
|
||||||
spelling = ["pyenchant (>=3.2,<4.0)"]
|
spelling = ["pyenchant (>=3.2,<4.0)"]
|
||||||
testutils = ["gitpython (>3)"]
|
testutils = ["gitpython (>3)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pynacl"
|
||||||
|
version = "1.6.2"
|
||||||
|
description = "Python binding to the Networking and Cryptography (NaCl) library"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "pynacl-1.6.2-cp314-cp314t-macosx_10_10_universal2.whl", hash = "sha256:622d7b07cc5c02c666795792931b50c91f3ce3c2649762efb1ef0d5684c81594"},
|
||||||
|
{file = "pynacl-1.6.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d071c6a9a4c94d79eb665db4ce5cedc537faf74f2355e4d502591d850d3913c0"},
|
||||||
|
{file = "pynacl-1.6.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe9847ca47d287af41e82be1dd5e23023d3c31a951da134121ab02e42ac218c9"},
|
||||||
|
{file = "pynacl-1.6.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04316d1fc625d860b6c162fff704eb8426b1a8bcd3abacea11142cbd99a6b574"},
|
||||||
|
{file = "pynacl-1.6.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44081faff368d6c5553ccf55322ef2819abb40e25afaec7e740f159f74813634"},
|
||||||
|
{file = "pynacl-1.6.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:a9f9932d8d2811ce1a8ffa79dcbdf3970e7355b5c8eb0c1a881a57e7f7d96e88"},
|
||||||
|
{file = "pynacl-1.6.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:bc4a36b28dd72fb4845e5d8f9760610588a96d5a51f01d84d8c6ff9849968c14"},
|
||||||
|
{file = "pynacl-1.6.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bffb6d0f6becacb6526f8f42adfb5efb26337056ee0831fb9a7044d1a964444"},
|
||||||
|
{file = "pynacl-1.6.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2fef529ef3ee487ad8113d287a593fa26f48ee3620d92ecc6f1d09ea38e0709b"},
|
||||||
|
{file = "pynacl-1.6.2-cp314-cp314t-win32.whl", hash = "sha256:a84bf1c20339d06dc0c85d9aea9637a24f718f375d861b2668b2f9f96fa51145"},
|
||||||
|
{file = "pynacl-1.6.2-cp314-cp314t-win_amd64.whl", hash = "sha256:320ef68a41c87547c91a8b58903c9caa641ab01e8512ce291085b5fe2fcb7590"},
|
||||||
|
{file = "pynacl-1.6.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d29bfe37e20e015a7d8b23cfc8bd6aa7909c92a1b8f41ee416bbb3e79ef182b2"},
|
||||||
|
{file = "pynacl-1.6.2-cp38-abi3-macosx_10_10_universal2.whl", hash = "sha256:c949ea47e4206af7c8f604b8278093b674f7c79ed0d4719cc836902bf4517465"},
|
||||||
|
{file = "pynacl-1.6.2-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8845c0631c0be43abdd865511c41eab235e0be69c81dc66a50911594198679b0"},
|
||||||
|
{file = "pynacl-1.6.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22de65bb9010a725b0dac248f353bb072969c94fa8d6b1f34b87d7953cf7bbe4"},
|
||||||
|
{file = "pynacl-1.6.2-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46065496ab748469cdd999246d17e301b2c24ae2fdf739132e580a0e94c94a87"},
|
||||||
|
{file = "pynacl-1.6.2-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a66d6fb6ae7661c58995f9c6435bda2b1e68b54b598a6a10247bfcdadac996c"},
|
||||||
|
{file = "pynacl-1.6.2-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:26bfcd00dcf2cf160f122186af731ae30ab120c18e8375684ec2670dccd28130"},
|
||||||
|
{file = "pynacl-1.6.2-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c8a231e36ec2cab018c4ad4358c386e36eede0319a0c41fed24f840b1dac59f6"},
|
||||||
|
{file = "pynacl-1.6.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:68be3a09455743ff9505491220b64440ced8973fe930f270c8e07ccfa25b1f9e"},
|
||||||
|
{file = "pynacl-1.6.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8b097553b380236d51ed11356c953bf8ce36a29a3e596e934ecabe76c985a577"},
|
||||||
|
{file = "pynacl-1.6.2-cp38-abi3-win32.whl", hash = "sha256:5811c72b473b2f38f7e2a3dc4f8642e3a3e9b5e7317266e4ced1fba85cae41aa"},
|
||||||
|
{file = "pynacl-1.6.2-cp38-abi3-win_amd64.whl", hash = "sha256:62985f233210dee6548c223301b6c25440852e13d59a8b81490203c3227c5ba0"},
|
||||||
|
{file = "pynacl-1.6.2-cp38-abi3-win_arm64.whl", hash = "sha256:834a43af110f743a754448463e8fd61259cd4ab5bbedcf70f9dabad1d28a394c"},
|
||||||
|
{file = "pynacl-1.6.2.tar.gz", hash = "sha256:018494d6d696ae03c7e656e5e74cdfd8ea1326962cc401bcf018f1ed8436811c"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
cffi = {version = ">=2.0.0", markers = "platform_python_implementation != \"PyPy\" and python_version >= \"3.9\""}
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["sphinx (<7)", "sphinx_rtd_theme"]
|
||||||
|
tests = ["hypothesis (>=3.27.0)", "pytest (>=7.4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyparsing"
|
name = "pyparsing"
|
||||||
version = "3.3.2"
|
version = "3.3.2"
|
||||||
|
|
@ -2245,14 +2444,14 @@ six = ">=1.5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-discovery"
|
name = "python-discovery"
|
||||||
version = "1.2.1"
|
version = "1.2.2"
|
||||||
description = "Python interpreter discovery"
|
description = "Python interpreter discovery"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
groups = ["test"]
|
groups = ["test"]
|
||||||
files = [
|
files = [
|
||||||
{file = "python_discovery-1.2.1-py3-none-any.whl", hash = "sha256:b6a957b24c1cd79252484d3566d1b49527581d46e789aaf43181005e56201502"},
|
{file = "python_discovery-1.2.2-py3-none-any.whl", hash = "sha256:e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a"},
|
||||||
{file = "python_discovery-1.2.1.tar.gz", hash = "sha256:180c4d114bff1c32462537eac5d6a332b768242b76b69c0259c7d14b1b680c9e"},
|
{file = "python_discovery-1.2.2.tar.gz", hash = "sha256:876e9c57139eb757cb5878cbdd9ae5379e5d96266c99ef731119e04fffe533bb"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
|
@ -2775,17 +2974,18 @@ test = ["Cython", "array-api-strict (>=2.0,<2.1.1)", "asv", "gmpy2", "hypothesis
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sigmf"
|
name = "sigmf"
|
||||||
version = "1.7.2"
|
version = "1.8.0"
|
||||||
description = "Easily interact with Signal Metadata Format (SigMF) recordings."
|
description = "Easily interact with Signal Metadata Format (SigMF) recordings."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "sigmf-1.7.2-py3-none-any.whl", hash = "sha256:6599b95e8bd3ac2c568b8ec46c312a77b80868cbda79d729234f396d2194d3d8"},
|
{file = "sigmf-1.8.0-py3-none-any.whl", hash = "sha256:f233ab04344fa3e42170926a646f7e53edd7edc65fcda42eb3d7efaf8a2e8263"},
|
||||||
{file = "sigmf-1.7.2.tar.gz", hash = "sha256:5f80f7127539358c7528ccf26e0ac5b3c268ecaeb69a921542e8ff71d0c85346"},
|
{file = "sigmf-1.8.0.tar.gz", hash = "sha256:91e10cb046499639e5f961d66a24c17a33ff76fc98df892eab0953cc9d659a50"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
defusedxml = "*"
|
||||||
jsonschema = "*"
|
jsonschema = "*"
|
||||||
numpy = "*"
|
numpy = "*"
|
||||||
|
|
||||||
|
|
@ -3036,6 +3236,7 @@ description = "Computer algebra system (CAS) in Python"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
groups = ["server", "test"]
|
groups = ["server", "test"]
|
||||||
|
markers = "python_version >= \"3.11\""
|
||||||
files = [
|
files = [
|
||||||
{file = "sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5"},
|
{file = "sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5"},
|
||||||
{file = "sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517"},
|
{file = "sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517"},
|
||||||
|
|
@ -3131,14 +3332,14 @@ files = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tox"
|
name = "tox"
|
||||||
version = "4.52.0"
|
version = "4.53.0"
|
||||||
description = "tox is a generic virtualenv management and test command line tool"
|
description = "tox is a generic virtualenv management and test command line tool"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.10"
|
python-versions = ">=3.10"
|
||||||
groups = ["test"]
|
groups = ["test"]
|
||||||
files = [
|
files = [
|
||||||
{file = "tox-4.52.0-py3-none-any.whl", hash = "sha256:624d8ea4a8c6d5e8d168eedf0e318d736fb22e83ca83137d001ac65ffdec46fd"},
|
{file = "tox-4.53.0-py3-none-any.whl", hash = "sha256:cc4e716d18c4889aa179d785175c438fa60c35deef20ce689ec288d8fb656096"},
|
||||||
{file = "tox-4.52.0.tar.gz", hash = "sha256:6054abf5c8b61d58776fbec991f9bf0d34bb883862beb93d2fe55601ef3977c9"},
|
{file = "tox-4.53.0.tar.gz", hash = "sha256:62c780e42f87d34ee60f2ea20342156253794fdcbd6885fd797d98ee05009f22"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
|
@ -3149,7 +3350,7 @@ packaging = ">=26"
|
||||||
platformdirs = ">=4.9.4"
|
platformdirs = ">=4.9.4"
|
||||||
pluggy = ">=1.6"
|
pluggy = ">=1.6"
|
||||||
pyproject-api = ">=1.10"
|
pyproject-api = ">=1.10"
|
||||||
python-discovery = ">=1.2.1"
|
python-discovery = ">=1.2.2"
|
||||||
tomli = {version = ">=2.4", markers = "python_version < \"3.11\""}
|
tomli = {version = ">=2.4", markers = "python_version < \"3.11\""}
|
||||||
tomli-w = ">=1.2"
|
tomli-w = ">=1.2"
|
||||||
typing-extensions = {version = ">=4.15", markers = "python_version < \"3.11\""}
|
typing-extensions = {version = ">=4.15", markers = "python_version < \"3.11\""}
|
||||||
|
|
@ -3169,7 +3370,7 @@ files = [
|
||||||
{file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"},
|
{file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"},
|
||||||
{file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"},
|
{file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"},
|
||||||
]
|
]
|
||||||
markers = {main = "python_version <= \"3.12\"", dev = "python_version == \"3.10\"", docs = "python_version <= \"3.12\""}
|
markers = {main = "python_version < \"3.13\"", dev = "python_version == \"3.10\"", docs = "python_version < \"3.13\""}
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typing-inspection"
|
name = "typing-inspection"
|
||||||
|
|
@ -3188,14 +3389,14 @@ typing-extensions = ">=4.12.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tzdata"
|
name = "tzdata"
|
||||||
version = "2025.3"
|
version = "2026.1"
|
||||||
description = "Provider of IANA time zone data"
|
description = "Provider of IANA time zone data"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=2"
|
python-versions = ">=2"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1"},
|
{file = "tzdata-2026.1-py2.py3-none-any.whl", hash = "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9"},
|
||||||
{file = "tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7"},
|
{file = "tzdata-2026.1.tar.gz", hash = "sha256:67658a1903c75917309e753fdc349ac0efd8c27db7a0cb406a25be4840f87f98"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -3218,14 +3419,14 @@ zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uvicorn"
|
name = "uvicorn"
|
||||||
version = "0.42.0"
|
version = "0.44.0"
|
||||||
description = "The lightning-fast ASGI server."
|
description = "The lightning-fast ASGI server."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.10"
|
python-versions = ">=3.10"
|
||||||
groups = ["docs", "server", "test"]
|
groups = ["docs", "server", "test"]
|
||||||
files = [
|
files = [
|
||||||
{file = "uvicorn-0.42.0-py3-none-any.whl", hash = "sha256:96c30f5c7abe6f74ae8900a70e92b85ad6613b745d4879eb9b16ccad15645359"},
|
{file = "uvicorn-0.44.0-py3-none-any.whl", hash = "sha256:ce937c99a2cc70279556967274414c087888e8cec9f9c94644dfca11bd3ced89"},
|
||||||
{file = "uvicorn-0.42.0.tar.gz", hash = "sha256:9b1f190ce15a2dd22e7758651d9b6d12df09a13d51ba5bf4fc33c383a48e1775"},
|
{file = "uvicorn-0.44.0.tar.gz", hash = "sha256:6c942071b68f07e178264b9152f1f16dfac5da85880c4ce06366a96d70d4f31e"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
|
@ -3310,21 +3511,21 @@ test = ["aiohttp (>=3.10.5)", "flake8 (>=6.1,<7.0)", "mypy (>=0.800)", "psutil",
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "virtualenv"
|
name = "virtualenv"
|
||||||
version = "21.2.0"
|
version = "21.2.4"
|
||||||
description = "Virtual Python Environment builder"
|
description = "Virtual Python Environment builder"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
groups = ["test"]
|
groups = ["test"]
|
||||||
files = [
|
files = [
|
||||||
{file = "virtualenv-21.2.0-py3-none-any.whl", hash = "sha256:1bd755b504931164a5a496d217c014d098426cddc79363ad66ac78125f9d908f"},
|
{file = "virtualenv-21.2.4-py3-none-any.whl", hash = "sha256:29d21e941795206138d0f22f4e45ff7050e5da6c6472299fb7103318763861ac"},
|
||||||
{file = "virtualenv-21.2.0.tar.gz", hash = "sha256:1720dc3a62ef5b443092e3f499228599045d7fea4c79199770499df8becf9098"},
|
{file = "virtualenv-21.2.4.tar.gz", hash = "sha256:b294ef68192638004d72524ce7ef303e9d0cf5a44c95ce2e54a7500a6381cada"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
distlib = ">=0.3.7,<1"
|
distlib = ">=0.3.7,<1"
|
||||||
filelock = {version = ">=3.24.2,<4", markers = "python_version >= \"3.10\""}
|
filelock = {version = ">=3.24.2,<4", markers = "python_version >= \"3.10\""}
|
||||||
platformdirs = ">=3.9.1,<5"
|
platformdirs = ">=3.9.1,<5"
|
||||||
python-discovery = ">=1"
|
python-discovery = ">=1.2.2"
|
||||||
typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.11\""}
|
typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.11\""}
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -3523,4 +3724,4 @@ files = [
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.1"
|
lock-version = "2.1"
|
||||||
python-versions = ">=3.10"
|
python-versions = ">=3.10"
|
||||||
content-hash = "b1e5ddd7284aecf49624e51740b7a4c31bc8d0e703c255126ba5d9b2a4a0e519"
|
content-hash = "720436f5f3d6651c298cf9fdc2f15ba40dcda6a70b09ba6aefa39ae119ba8ee0"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[project]
|
[project]
|
||||||
name = "ria-toolkit-oss"
|
name = "ria-toolkit-oss"
|
||||||
version = "0.1.4"
|
version = "0.1.5"
|
||||||
description = "An open-source version of the RIA Toolkit, including the fundamental tools to get started developing, testing, and deploying radio intelligence applications"
|
description = "An open-source version of the RIA Toolkit, including the fundamental tools to get started developing, testing, and deploying radio intelligence applications"
|
||||||
license = { text = "AGPL-3.0-only" }
|
license = { text = "AGPL-3.0-only" }
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
@ -49,7 +49,8 @@ dependencies = [
|
||||||
"pyzmq (>=27.1.0,<28.0.0)",
|
"pyzmq (>=27.1.0,<28.0.0)",
|
||||||
"pyyaml (>=6.0.3,<7.0.0)",
|
"pyyaml (>=6.0.3,<7.0.0)",
|
||||||
"click (>=8.1.0,<9.0.0)",
|
"click (>=8.1.0,<9.0.0)",
|
||||||
"matplotlib (>=3.8.0,<4.0.0)"
|
"matplotlib (>=3.8.0,<4.0.0)",
|
||||||
|
"paramiko (>=4.0.0)"
|
||||||
]
|
]
|
||||||
|
|
||||||
# [project.optional-dependencies] Commented out to prevent Tox tests from failing
|
# [project.optional-dependencies] Commented out to prevent Tox tests from failing
|
||||||
|
|
@ -87,7 +88,7 @@ pytest = "^8.0.0"
|
||||||
tox = "^4.19.0"
|
tox = "^4.19.0"
|
||||||
fastapi = ">=0.111,<1.0"
|
fastapi = ">=0.111,<1.0"
|
||||||
uvicorn = {version = ">=0.29,<1.0", extras = ["standard"]}
|
uvicorn = {version = ">=0.29,<1.0", extras = ["standard"]}
|
||||||
onnxruntime = ">=1.17,<2.0"
|
onnxruntime = {version = ">=1.17,<2.0", python = ">=3.11"}
|
||||||
httpx = ">=0.27,<1.0"
|
httpx = ">=0.27,<1.0"
|
||||||
|
|
||||||
[tool.poetry.group.docs.dependencies]
|
[tool.poetry.group.docs.dependencies]
|
||||||
|
|
@ -121,7 +122,7 @@ ria-agent = "ria_toolkit_oss.agent:main"
|
||||||
[tool.poetry.group.server.dependencies]
|
[tool.poetry.group.server.dependencies]
|
||||||
fastapi = ">=0.111,<1.0"
|
fastapi = ">=0.111,<1.0"
|
||||||
uvicorn = {version = ">=0.29,<1.0", extras = ["standard"]}
|
uvicorn = {version = ">=0.29,<1.0", extras = ["standard"]}
|
||||||
onnxruntime = ">=1.17,<2.0"
|
onnxruntime = {version = ">=1.17,<2.0", python = ">=3.11"}
|
||||||
|
|
||||||
[tool.black]
|
[tool.black]
|
||||||
line-length = 119
|
line-length = 119
|
||||||
|
|
|
||||||
|
|
@ -14,19 +14,40 @@ Usage::
|
||||||
[--device plutosdr] \\
|
[--device plutosdr] \\
|
||||||
[--insecure]
|
[--insecure]
|
||||||
|
|
||||||
|
# Or store credentials in a config file and omit them from the command line:
|
||||||
|
ria-agent --config ~/.config/ria-agent/config.json --name lab-bench-1
|
||||||
|
|
||||||
The agent:
|
The agent:
|
||||||
1. Registers with RIA Hub and receives a ``node_id``.
|
1. Registers with RIA Hub and receives a ``node_id``.
|
||||||
2. Sends a heartbeat every 30 s so the hub knows it is online.
|
2. Sends a heartbeat every 30 s so the hub knows it is online.
|
||||||
3. Long-polls ``GET /orchestrator/nodes/{id}/commands`` (30 s timeout).
|
3. Long-polls ``GET /composer/nodes/{id}/commands`` (30 s timeout).
|
||||||
4. Executes received campaigns via :class:`ria_toolkit_oss.orchestration.executor.CampaignExecutor`.
|
4. Dispatches received commands:
|
||||||
5. Uploads recordings to the hub via chunked POST, keeping each request
|
- ``run_campaign``: executes via CampaignExecutor, uploads recordings.
|
||||||
under 50 MB so it passes through Cloudflare without needing the bypass
|
- ``load_model``: loads an ONNX fingerprint or detector model.
|
||||||
subdomain.
|
- ``start_inference``: opens the SDR, runs the inference loop, posts
|
||||||
6. Deregisters cleanly on SIGINT / SIGTERM.
|
detection events to the hub for SSE fan-out to browsers.
|
||||||
|
- ``stop_inference``: gracefully stops the inference loop.
|
||||||
|
- ``configure_inference``: queues an SDR parameter update (applied at the
|
||||||
|
next capture boundary without restarting the loop).
|
||||||
|
5. Deregisters cleanly on SIGINT / SIGTERM.
|
||||||
|
|
||||||
|
Config file (JSON, optional)::
|
||||||
|
|
||||||
|
{
|
||||||
|
"hub": "https://riahub.company.com",
|
||||||
|
"key": "secret",
|
||||||
|
"name": "lab-bench-1",
|
||||||
|
"device": "plutosdr",
|
||||||
|
"insecure": false,
|
||||||
|
"log_level": "INFO"
|
||||||
|
}
|
||||||
|
|
||||||
|
CLI arguments always override config file values.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
import os
|
import os
|
||||||
|
|
@ -49,6 +70,8 @@ _POLL_CLIENT_TIMEOUT = 40 # client read timeout — slightly longer than server
|
||||||
_RECONNECT_PAUSE = 5 # seconds to wait after a poll error before retrying
|
_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 = 50 * 1024 * 1024 # 50 MB — well below Cloudflare's 100 MB limit
|
||||||
_DIRECT_THRESHOLD = 90 * 1024 * 1024 # files above this use chunked upload
|
_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"})
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -80,6 +103,30 @@ class NodeAgent:
|
||||||
self.node_id: str | None = None
|
self.node_id: str | None = None
|
||||||
self._stop = threading.Event()
|
self._stop = threading.Event()
|
||||||
|
|
||||||
|
# ── Inference state ─────────────────────────────────────────────────
|
||||||
|
# Protected by _inf_lock for cross-thread model swaps.
|
||||||
|
self._inf_lock = threading.Lock()
|
||||||
|
self._inf_session: Any = None # primary fingerprint ONNX session
|
||||||
|
self._inf_index_to_label: dict[int, str] = {}
|
||||||
|
self._inf_detector_session: Any = None # optional protocol-detector session
|
||||||
|
self._inf_detector_index_to_label: dict[int, str] = {}
|
||||||
|
self._inf_detector_threshold: float = 0.7
|
||||||
|
self._inf_pending_config: dict = {} # queued SDR attribute updates
|
||||||
|
|
||||||
|
self._inf_stop = threading.Event()
|
||||||
|
self._inf_thread: threading.Thread | None = None
|
||||||
|
|
||||||
|
# Detect optional dependencies once at startup so capability
|
||||||
|
# advertising is accurate from the first registration.
|
||||||
|
try:
|
||||||
|
import onnxruntime as _ort_mod
|
||||||
|
|
||||||
|
self._ort: Any = _ort_mod
|
||||||
|
self._ort_available = True
|
||||||
|
except ImportError:
|
||||||
|
self._ort = None
|
||||||
|
self._ort_available = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import ria_toolkit_oss
|
import ria_toolkit_oss
|
||||||
|
|
||||||
|
|
@ -114,6 +161,7 @@ class NodeAgent:
|
||||||
self._command_loop()
|
self._command_loop()
|
||||||
finally:
|
finally:
|
||||||
self._stop.set()
|
self._stop.set()
|
||||||
|
self._stop_inference()
|
||||||
self._deregister()
|
self._deregister()
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
@ -121,13 +169,16 @@ class NodeAgent:
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def _register(self) -> None:
|
def _register(self) -> None:
|
||||||
|
capabilities = ["campaign"]
|
||||||
|
if self._ort_available:
|
||||||
|
capabilities.append("inference")
|
||||||
resp = self._post(
|
resp = self._post(
|
||||||
"/orchestrator/nodes/register",
|
"/composer/nodes/register",
|
||||||
json={
|
json={
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"sdr_device": self.sdr_device,
|
"sdr_device": self.sdr_device,
|
||||||
"ria_toolkit_version": self._ria_version,
|
"ria_toolkit_version": self._ria_version,
|
||||||
"capabilities": ["inference", "campaign"],
|
"capabilities": capabilities,
|
||||||
},
|
},
|
||||||
timeout=15,
|
timeout=15,
|
||||||
)
|
)
|
||||||
|
|
@ -139,7 +190,7 @@ class NodeAgent:
|
||||||
if not self.node_id:
|
if not self.node_id:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
self._delete(f"/orchestrator/nodes/{self.node_id}", timeout=10)
|
self._delete(f"/composer/nodes/{self.node_id}", timeout=10)
|
||||||
logger.info("Deregistered %s", self.node_id)
|
logger.info("Deregistered %s", self.node_id)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.debug("Deregister failed (ignored on shutdown): %s", exc)
|
logger.debug("Deregister failed (ignored on shutdown): %s", exc)
|
||||||
|
|
@ -151,7 +202,7 @@ class NodeAgent:
|
||||||
def _heartbeat_loop(self) -> None:
|
def _heartbeat_loop(self) -> None:
|
||||||
while not self._stop.wait(_HEARTBEAT_INTERVAL):
|
while not self._stop.wait(_HEARTBEAT_INTERVAL):
|
||||||
try:
|
try:
|
||||||
resp = self._post(f"/orchestrator/nodes/{self.node_id}/heartbeat", timeout=10)
|
resp = self._post(f"/composer/nodes/{self.node_id}/heartbeat", timeout=10)
|
||||||
if resp.status_code == 404:
|
if resp.status_code == 404:
|
||||||
logger.warning("Heartbeat got 404 — hub lost registration, re-registering")
|
logger.warning("Heartbeat got 404 — hub lost registration, re-registering")
|
||||||
self._register()
|
self._register()
|
||||||
|
|
@ -166,7 +217,7 @@ class NodeAgent:
|
||||||
while not self._stop.is_set():
|
while not self._stop.is_set():
|
||||||
try:
|
try:
|
||||||
resp = self._get(
|
resp = self._get(
|
||||||
f"/orchestrator/nodes/{self.node_id}/commands",
|
f"/composer/nodes/{self.node_id}/commands",
|
||||||
timeout=_POLL_CLIENT_TIMEOUT,
|
timeout=_POLL_CLIENT_TIMEOUT,
|
||||||
)
|
)
|
||||||
if resp.status_code == 204:
|
if resp.status_code == 204:
|
||||||
|
|
@ -200,6 +251,24 @@ class NodeAgent:
|
||||||
daemon=True,
|
daemon=True,
|
||||||
name=f"campaign-{campaign_id[:8]}",
|
name=f"campaign-{campaign_id[:8]}",
|
||||||
).start()
|
).start()
|
||||||
|
elif command == "load_model":
|
||||||
|
threading.Thread(
|
||||||
|
target=self._load_model,
|
||||||
|
args=(cmd,),
|
||||||
|
daemon=True,
|
||||||
|
name="ria-load-model",
|
||||||
|
).start()
|
||||||
|
elif command == "start_inference":
|
||||||
|
threading.Thread(
|
||||||
|
target=self._start_inference,
|
||||||
|
args=(cmd,),
|
||||||
|
daemon=True,
|
||||||
|
name="ria-start-inf",
|
||||||
|
).start()
|
||||||
|
elif command == "stop_inference":
|
||||||
|
self._stop_inference()
|
||||||
|
elif command == "configure_inference":
|
||||||
|
self._queue_sdr_config(cmd)
|
||||||
else:
|
else:
|
||||||
logger.warning("Unknown command %r — ignored", command)
|
logger.warning("Unknown command %r — ignored", command)
|
||||||
|
|
||||||
|
|
@ -232,6 +301,270 @@ class NodeAgent:
|
||||||
logger.error("Campaign %s failed: %s", campaign_id[:8], exc)
|
logger.error("Campaign %s failed: %s", campaign_id[:8], exc)
|
||||||
self._report_campaign_status(campaign_id, "failed", error=str(exc))
|
self._report_campaign_status(campaign_id, "failed", error=str(exc))
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Inference — model loading
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _load_model(self, cmd: dict) -> None:
|
||||||
|
"""Load an ONNX model into the fingerprint or detector slot.
|
||||||
|
|
||||||
|
The ``model_path`` field may be either a local filesystem path or an
|
||||||
|
``http(s)://`` URL; in the latter case the file is downloaded first.
|
||||||
|
"""
|
||||||
|
if not self._ort_available:
|
||||||
|
logger.error("load_model: onnxruntime is not installed — cannot load model")
|
||||||
|
return
|
||||||
|
|
||||||
|
model_path: str = cmd.get("model_path", "")
|
||||||
|
label_map: dict[str, int] = cmd.get("label_map") or {}
|
||||||
|
stage: str = cmd.get("stage", "fingerprint")
|
||||||
|
detector_threshold: float = float(cmd.get("detector_threshold") or 0.7)
|
||||||
|
|
||||||
|
if model_path.startswith(("http://", "https://")):
|
||||||
|
model_path = self._download_model(model_path)
|
||||||
|
if model_path is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
session = self._ort.InferenceSession(model_path, providers=["CPUExecutionProvider"])
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error("Failed to load model %r: %s", model_path, exc)
|
||||||
|
return
|
||||||
|
|
||||||
|
index_to_label = {v: k for k, v in label_map.items()}
|
||||||
|
with self._inf_lock:
|
||||||
|
if stage == "detector":
|
||||||
|
self._inf_detector_session = session
|
||||||
|
self._inf_detector_index_to_label = index_to_label
|
||||||
|
self._inf_detector_threshold = detector_threshold
|
||||||
|
logger.info(
|
||||||
|
"Detector model loaded: path=%s classes=%d threshold=%.2f",
|
||||||
|
model_path,
|
||||||
|
len(label_map),
|
||||||
|
detector_threshold,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._inf_session = session
|
||||||
|
self._inf_index_to_label = index_to_label
|
||||||
|
logger.info(
|
||||||
|
"Fingerprint model loaded: path=%s classes=%d",
|
||||||
|
model_path,
|
||||||
|
len(label_map),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _download_model(self, url: str) -> str | None:
|
||||||
|
"""Download a model from *url* to a temp file and return the local path."""
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
import requests as _requests
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.info("Downloading model from %s", url)
|
||||||
|
resp = _requests.get(
|
||||||
|
url,
|
||||||
|
headers={"X-API-Key": self.api_key},
|
||||||
|
verify=not self.insecure,
|
||||||
|
timeout=120,
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=".onnx", delete=False) as fh:
|
||||||
|
fh.write(resp.content)
|
||||||
|
path = fh.name
|
||||||
|
logger.info("Model downloaded to %s (%d bytes)", path, len(resp.content))
|
||||||
|
return path
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error("Model download from %s failed: %s", url, exc)
|
||||||
|
return None
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Inference — loop lifecycle
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _start_inference(self, cmd: dict) -> None:
|
||||||
|
"""Start the SDR capture + ONNX inference loop."""
|
||||||
|
if not self._ort_available:
|
||||||
|
logger.error("start_inference: onnxruntime is not installed")
|
||||||
|
return
|
||||||
|
|
||||||
|
with self._inf_lock:
|
||||||
|
if self._inf_session is None:
|
||||||
|
logger.error("start_inference: no fingerprint model loaded — call load_model first")
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._inf_thread is not None and self._inf_thread.is_alive():
|
||||||
|
logger.warning("start_inference: inference loop is already running — ignoring")
|
||||||
|
return
|
||||||
|
|
||||||
|
center_freq: float = float(cmd.get("center_freq", 2.4e9))
|
||||||
|
sample_rate: float = float(cmd.get("sample_rate", 10e6))
|
||||||
|
gain: float | str = cmd.get("gain", "auto")
|
||||||
|
device_type: str = cmd.get("device") or self.sdr_device
|
||||||
|
|
||||||
|
self._inf_stop.clear()
|
||||||
|
self._inf_thread = threading.Thread(
|
||||||
|
target=self._inference_loop,
|
||||||
|
args=(device_type, center_freq, sample_rate, gain),
|
||||||
|
daemon=True,
|
||||||
|
name="ria-agent-inference",
|
||||||
|
)
|
||||||
|
self._inf_thread.start()
|
||||||
|
logger.info(
|
||||||
|
"Inference started (device=%s freq=%.3f MHz rate=%.1f MHz)",
|
||||||
|
device_type,
|
||||||
|
center_freq / 1e6,
|
||||||
|
sample_rate / 1e6,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _stop_inference(self) -> None:
|
||||||
|
"""Signal the inference loop to stop and wait up to 5 s for it to exit."""
|
||||||
|
self._inf_stop.set()
|
||||||
|
if self._inf_thread is not None and self._inf_thread.is_alive():
|
||||||
|
self._inf_thread.join(timeout=5.0)
|
||||||
|
if self._inf_thread.is_alive():
|
||||||
|
logger.warning("Inference thread did not exit within 5 s")
|
||||||
|
logger.info("Inference stopped")
|
||||||
|
|
||||||
|
def _queue_sdr_config(self, cmd: dict) -> None:
|
||||||
|
"""Merge SDR parameter updates into the pending-config dict.
|
||||||
|
|
||||||
|
The inference loop checks this at each capture boundary and applies
|
||||||
|
the updates without restarting.
|
||||||
|
"""
|
||||||
|
cfg = {k: v for k, v in cmd.items() if k != "command" and v is not None}
|
||||||
|
with self._inf_lock:
|
||||||
|
self._inf_pending_config.update(cfg)
|
||||||
|
logger.debug("SDR reconfiguration queued: %s", cfg)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Inference — main loop
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _inference_loop(
|
||||||
|
self,
|
||||||
|
device_type: str,
|
||||||
|
center_freq: float,
|
||||||
|
sample_rate: float,
|
||||||
|
gain: float | str,
|
||||||
|
) -> None:
|
||||||
|
"""Continuous SDR capture → ONNX inference → POST events to hub.
|
||||||
|
|
||||||
|
Mirrors the two-stage pipeline in the hub's ``_inference_loop``:
|
||||||
|
an optional protocol-detector gates the fingerprint model so the
|
||||||
|
fingerprint model only runs when an active transmission is detected.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from ria_toolkit_oss.sdr import get_sdr_device
|
||||||
|
except ImportError as exc:
|
||||||
|
logger.error("inference_loop: ria_toolkit_oss not installed: %s", exc)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
sdr = get_sdr_device(device_type)
|
||||||
|
_apply_sdr_config(sdr, {"center_freq": center_freq, "sample_rate": sample_rate, "gain": gain})
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error("SDR initialisation failed: %s", exc)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
try:
|
||||||
|
from ria_toolkit_oss.orchestration.qa import estimate_snr_db
|
||||||
|
except ImportError:
|
||||||
|
estimate_snr_db = None
|
||||||
|
|
||||||
|
# Snapshot model state once at loop start. If the hub sends a
|
||||||
|
# new load_model command while the loop is running, the new session
|
||||||
|
# will be picked up on the next loop restart (stop + start).
|
||||||
|
with self._inf_lock:
|
||||||
|
session = self._inf_session
|
||||||
|
index_to_label = dict(self._inf_index_to_label)
|
||||||
|
det_session = self._inf_detector_session
|
||||||
|
det_threshold = self._inf_detector_threshold
|
||||||
|
|
||||||
|
input_name = session.get_inputs()[0].name
|
||||||
|
det_input_name = det_session.get_inputs()[0].name if det_session else None
|
||||||
|
|
||||||
|
while not self._inf_stop.is_set():
|
||||||
|
# Apply any queued SDR configuration changes.
|
||||||
|
with self._inf_lock:
|
||||||
|
pending = self._inf_pending_config.copy()
|
||||||
|
self._inf_pending_config.clear()
|
||||||
|
if pending:
|
||||||
|
_apply_sdr_config(sdr, pending)
|
||||||
|
|
||||||
|
try:
|
||||||
|
samples = sdr.rx(_CAPTURE_SAMPLES)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("SDR capture error: %s", exc)
|
||||||
|
# Avoid a tight spin when the SDR is in a persistent error
|
||||||
|
# state (e.g. physically disconnected).
|
||||||
|
self._inf_stop.wait(timeout=0.5)
|
||||||
|
continue
|
||||||
|
|
||||||
|
samples = np.array(samples, dtype=np.complex64)
|
||||||
|
snr_db = float(estimate_snr_db(samples)) if estimate_snr_db is not None else 0.0
|
||||||
|
iq = np.stack([samples.real, samples.imag], axis=0).astype(np.float32)
|
||||||
|
|
||||||
|
# Stage 1: protocol detector gate (optional).
|
||||||
|
if det_session is not None:
|
||||||
|
det_out = _run_onnx_session(det_session, det_input_name, iq)
|
||||||
|
det_probs = _softmax(det_out[0][0])
|
||||||
|
det_confidence = float(det_probs.max())
|
||||||
|
if det_confidence < det_threshold:
|
||||||
|
# No active protocol detected — report idle and skip
|
||||||
|
# the fingerprint model for this window.
|
||||||
|
self._post_event(device_id=None, confidence=det_confidence, snr_db=snr_db)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Stage 2: fingerprint model.
|
||||||
|
out = _run_onnx_session(session, input_name, iq)
|
||||||
|
probs = _softmax(out[0][0])
|
||||||
|
pred_idx = int(probs.argmax())
|
||||||
|
confidence = float(probs[pred_idx])
|
||||||
|
device_id = index_to_label.get(pred_idx)
|
||||||
|
idle = (device_id in _IDLE_LABELS) if device_id else True
|
||||||
|
self._post_event(
|
||||||
|
device_id=None if idle else device_id,
|
||||||
|
confidence=confidence,
|
||||||
|
snr_db=snr_db,
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as exc:
|
||||||
|
logger.exception("Inference loop terminated unexpectedly: %s", exc)
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
sdr.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
logger.info("Inference loop exited")
|
||||||
|
|
||||||
|
def _post_event(self, device_id: str | None, confidence: float, snr_db: float) -> None:
|
||||||
|
"""POST a single detection event to ``POST /composer/nodes/{id}/events``.
|
||||||
|
|
||||||
|
Failures are logged at DEBUG level and silently swallowed so that a
|
||||||
|
transient network blip does not crash the inference loop.
|
||||||
|
"""
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"type": "detection",
|
||||||
|
"device_id": device_id,
|
||||||
|
"confidence": round(confidence, 6),
|
||||||
|
"snr_db": round(snr_db, 2),
|
||||||
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
resp = self._post(
|
||||||
|
f"/composer/nodes/{self.node_id}/events",
|
||||||
|
json=payload,
|
||||||
|
timeout=5,
|
||||||
|
)
|
||||||
|
if resp.status_code not in (200, 204):
|
||||||
|
logger.debug("Event POST returned HTTP %d", resp.status_code)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.debug("Event POST failed (will retry next inference cycle): %s", exc)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Recording upload (chunked for large files)
|
# Recording upload (chunked for large files)
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
@ -244,7 +577,7 @@ class NodeAgent:
|
||||||
|
|
||||||
repo_owner, repo_name = output_repo.split("/", 1)
|
repo_owner, repo_name = output_repo.split("/", 1)
|
||||||
base_url = f"{self.hub_url}/datasets/upload"
|
base_url = f"{self.hub_url}/datasets/upload"
|
||||||
steps = getattr(result, "steps", None) or []
|
steps = (result.get("steps") if isinstance(result, dict) else getattr(result, "steps", None)) or []
|
||||||
|
|
||||||
for step in steps:
|
for step in steps:
|
||||||
output_path: str | None = getattr(step, "output_path", None)
|
output_path: str | None = getattr(step, "output_path", None)
|
||||||
|
|
@ -286,7 +619,7 @@ class NodeAgent:
|
||||||
payload["error"] = error
|
payload["error"] = error
|
||||||
try:
|
try:
|
||||||
resp = self._post(
|
resp = self._post(
|
||||||
f"/orchestrator/nodes/{self.node_id}/campaign-status",
|
f"/composer/nodes/{self.node_id}/campaign-status",
|
||||||
json=payload,
|
json=payload,
|
||||||
timeout=15,
|
timeout=15,
|
||||||
)
|
)
|
||||||
|
|
@ -304,7 +637,6 @@ class NodeAgent:
|
||||||
headers = {"X-API-Key": self.api_key}
|
headers = {"X-API-Key": self.api_key}
|
||||||
verify = not self.insecure
|
verify = not self.insecure
|
||||||
|
|
||||||
# Small files: single POST (unchanged endpoint, no assembly needed server-side).
|
|
||||||
if size <= _DIRECT_THRESHOLD:
|
if size <= _DIRECT_THRESHOLD:
|
||||||
with open(file_path, "rb") as fh:
|
with open(file_path, "rb") as fh:
|
||||||
resp = _requests.post(
|
resp = _requests.post(
|
||||||
|
|
@ -318,7 +650,6 @@ class NodeAgent:
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
return resp.json()
|
return resp.json()
|
||||||
|
|
||||||
# Large files: chunked upload — each request is ≤ 50 MB.
|
|
||||||
total_chunks = math.ceil(size / _CHUNK_SIZE)
|
total_chunks = math.ceil(size / _CHUNK_SIZE)
|
||||||
upload_id = str(uuid.uuid4())
|
upload_id = str(uuid.uuid4())
|
||||||
chunk_url = base_url + "/chunk"
|
chunk_url = base_url + "/chunk"
|
||||||
|
|
@ -339,18 +670,13 @@ class NodeAgent:
|
||||||
chunk_url,
|
chunk_url,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
files={"file": (filename, chunk, "application/octet-stream")},
|
files={"file": (filename, chunk, "application/octet-stream")},
|
||||||
data={
|
data={**metadata, "upload_id": upload_id, "chunk_index": i, "total_chunks": total_chunks},
|
||||||
**metadata,
|
|
||||||
"upload_id": upload_id,
|
|
||||||
"chunk_index": i,
|
|
||||||
"total_chunks": total_chunks,
|
|
||||||
},
|
|
||||||
timeout=120,
|
timeout=120,
|
||||||
verify=verify,
|
verify=verify,
|
||||||
)
|
)
|
||||||
if not resp.ok:
|
if not resp.ok:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Chunk {i + 1}/{total_chunks} failed: " f"HTTP {resp.status_code}: {resp.text[:300]}"
|
f"Chunk {i + 1}/{total_chunks} failed: HTTP {resp.status_code}: {resp.text[:300]}"
|
||||||
)
|
)
|
||||||
resp_data = resp.json()
|
resp_data = resp.json()
|
||||||
logger.debug("Chunk %d/%d uploaded", i + 1, total_chunks)
|
logger.debug("Chunk %d/%d uploaded", i + 1, total_chunks)
|
||||||
|
|
@ -393,10 +719,41 @@ class NodeAgent:
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Helpers
|
# Module-level helpers (shared by NodeAgent._inference_loop)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _run_onnx_session(session: Any, input_name: str, iq: Any) -> list:
|
||||||
|
"""Run an ONNX session on an IQ array (2, N).
|
||||||
|
|
||||||
|
Tries channel-first layout (1, 2, N) first; falls back to interleaved flat
|
||||||
|
(1, 2*N) when the model expects a flattened input.
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
x = iq[np.newaxis] # (1, 2, N)
|
||||||
|
try:
|
||||||
|
return session.run(None, {input_name: x})
|
||||||
|
except Exception:
|
||||||
|
return session.run(None, {input_name: iq.flatten()[np.newaxis]})
|
||||||
|
|
||||||
|
|
||||||
|
def _softmax(x: Any) -> Any:
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
e = np.exp(x - x.max())
|
||||||
|
return e / e.sum()
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_sdr_config(sdr: Any, cfg: dict) -> None:
|
||||||
|
for attr in ("center_freq", "sample_rate", "gain"):
|
||||||
|
if attr in cfg:
|
||||||
|
try:
|
||||||
|
setattr(sdr, attr, cfg[attr])
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("SDR config %s=%r failed: %s", attr, cfg[attr], exc)
|
||||||
|
|
||||||
|
|
||||||
def _sigmf_files(data_path: str) -> list[str]:
|
def _sigmf_files(data_path: str) -> list[str]:
|
||||||
"""Return paths to both SigMF files (.sigmf-data and .sigmf-meta) for a recording."""
|
"""Return paths to both SigMF files (.sigmf-data and .sigmf-meta) for a recording."""
|
||||||
candidates = [data_path]
|
candidates = [data_path]
|
||||||
|
|
@ -405,6 +762,29 @@ def _sigmf_files(data_path: str) -> list[str]:
|
||||||
return [p for p in candidates if os.path.exists(p)]
|
return [p for p in candidates if os.path.exists(p)]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Config file helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
_DEFAULT_CONFIG_PATH = os.path.join(
|
||||||
|
os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")),
|
||||||
|
"ria-agent",
|
||||||
|
"config.json",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _load_config(path: str) -> dict:
|
||||||
|
"""Load a JSON config file, returning an empty dict if it does not exist."""
|
||||||
|
try:
|
||||||
|
with open(path) as fh:
|
||||||
|
return json.load(fh)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return {}
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("Could not read config file %s: %s", path, exc)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# CLI entry point
|
# CLI entry point
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -420,67 +800,94 @@ def main() -> None:
|
||||||
"campaigns / inference on local SDR hardware."
|
"campaigns / inference on local SDR hardware."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--config",
|
||||||
|
default=None,
|
||||||
|
metavar="PATH",
|
||||||
|
help=(
|
||||||
|
f"Path to a JSON config file (default: {_DEFAULT_CONFIG_PATH}). "
|
||||||
|
"CLI arguments override config file values."
|
||||||
|
),
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--hub",
|
"--hub",
|
||||||
required=True,
|
default=None,
|
||||||
metavar="URL",
|
metavar="URL",
|
||||||
help="RIA Hub base URL, e.g. https://riahub.company.com",
|
help="RIA Hub base URL, e.g. https://riahub.company.com",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--key",
|
"--key",
|
||||||
required=True,
|
default=None,
|
||||||
metavar="API_KEY",
|
metavar="API_KEY",
|
||||||
help="Shared API key (must match [wac] API_KEY in the hub's app.ini)",
|
help="Shared API key (must match [wac] API_KEY in the hub's app.ini)",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--name",
|
"--name",
|
||||||
required=True,
|
default=None,
|
||||||
metavar="NAME",
|
metavar="NAME",
|
||||||
help='Human-readable name shown in the Target Node dropdown, e.g. "lab-bench-1"',
|
help='Human-readable name shown in the Target Node dropdown, e.g. "lab-bench-1"',
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--device",
|
"--device",
|
||||||
default="unknown",
|
default=None,
|
||||||
metavar="SDR",
|
metavar="SDR",
|
||||||
help=(
|
help=(
|
||||||
"SDR device type reported to the hub (informational only). "
|
"SDR device type reported to the hub and used for inference. "
|
||||||
"Examples: plutosdr, usrp_b210, rtlsdr, mock. Default: unknown"
|
"Examples: plutosdr, usrp_b210, rtlsdr, mock. Default: unknown"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--insecure",
|
"--insecure",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
|
default=None,
|
||||||
help="Disable TLS certificate verification (dev/self-signed certs only)",
|
help="Disable TLS certificate verification (dev/self-signed certs only)",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--log-level",
|
"--log-level",
|
||||||
default="INFO",
|
default=None,
|
||||||
choices=["DEBUG", "INFO", "WARNING", "ERROR"],
|
choices=["DEBUG", "INFO", "WARNING", "ERROR"],
|
||||||
help="Logging verbosity (default: INFO)",
|
help="Logging verbosity (default: INFO)",
|
||||||
)
|
)
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Merge: config file → CLI args (CLI wins).
|
||||||
|
config_path = args.config or _DEFAULT_CONFIG_PATH
|
||||||
|
cfg = _load_config(config_path)
|
||||||
|
|
||||||
|
hub = args.hub or cfg.get("hub")
|
||||||
|
key = args.key or cfg.get("key")
|
||||||
|
name = args.name or cfg.get("name")
|
||||||
|
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")
|
||||||
|
|
||||||
|
if not hub:
|
||||||
|
parser.error("--hub is required (or set 'hub' in the config file)")
|
||||||
|
if not key:
|
||||||
|
parser.error("--key is required (or set 'key' in the config file)")
|
||||||
|
if not name:
|
||||||
|
parser.error("--name is required (or set 'name' in the config file)")
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=getattr(logging, args.log_level),
|
level=getattr(logging, log_level),
|
||||||
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
||||||
datefmt="%Y-%m-%d %H:%M:%S",
|
datefmt="%Y-%m-%d %H:%M:%S",
|
||||||
stream=sys.stderr,
|
stream=sys.stderr,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Warn loudly if --insecure is used outside of development.
|
if insecure:
|
||||||
if args.insecure:
|
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"--insecure disables TLS certificate verification. "
|
"--insecure disables TLS certificate verification. "
|
||||||
"Only use this for local development with self-signed certs."
|
"Only use this for local development with self-signed certs."
|
||||||
)
|
)
|
||||||
|
|
||||||
agent = NodeAgent(
|
agent = NodeAgent(
|
||||||
hub_url=args.hub,
|
hub_url=hub,
|
||||||
api_key=args.key,
|
api_key=key,
|
||||||
name=args.name,
|
name=name,
|
||||||
sdr_device=args.device,
|
sdr_device=device,
|
||||||
insecure=args.insecure,
|
insecure=insecure,
|
||||||
)
|
)
|
||||||
agent.run()
|
agent.run()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -223,13 +223,16 @@ class TransmitterConfig:
|
||||||
|
|
||||||
id: str
|
id: str
|
||||||
type: str # "wifi", "bluetooth", "sdr", "external"
|
type: str # "wifi", "bluetooth", "sdr", "external"
|
||||||
control_method: str # "external_script" | "sdr"
|
control_method: str # "external_script" | "sdr" | "sdr_remote"
|
||||||
schedule: list[CaptureStep]
|
schedule: list[CaptureStep]
|
||||||
|
|
||||||
# For external_script control
|
# For external_script control
|
||||||
script: Optional[str] = None # path to control script
|
script: Optional[str] = None # path to control script
|
||||||
device: Optional[str] = None # e.g. "/dev/wlan0"
|
device: Optional[str] = None # e.g. "/dev/wlan0"
|
||||||
|
|
||||||
|
# For sdr_remote control — keys: host, ssh_user, ssh_key_path, device_type, device_id, zmq_port
|
||||||
|
sdr_remote: Optional[dict] = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, d: dict) -> "TransmitterConfig":
|
def from_dict(cls, d: dict) -> "TransmitterConfig":
|
||||||
schedule = [CaptureStep.from_dict(s) for s in d.get("schedule", [])]
|
schedule = [CaptureStep.from_dict(s) for s in d.get("schedule", [])]
|
||||||
|
|
@ -240,6 +243,7 @@ class TransmitterConfig:
|
||||||
schedule=schedule,
|
schedule=schedule,
|
||||||
script=d.get("script"),
|
script=d.get("script"),
|
||||||
device=d.get("device"),
|
device=d.get("device"),
|
||||||
|
sdr_remote=d.get("sdr_remote"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -196,6 +196,7 @@ class CampaignExecutor:
|
||||||
self.config = config
|
self.config = config
|
||||||
self.progress_cb = progress_cb
|
self.progress_cb = progress_cb
|
||||||
self._sdr = None
|
self._sdr = None
|
||||||
|
self._remote_tx_controllers: dict = {}
|
||||||
|
|
||||||
if verbose:
|
if verbose:
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
@ -222,6 +223,7 @@ class CampaignExecutor:
|
||||||
)
|
)
|
||||||
|
|
||||||
self._init_sdr()
|
self._init_sdr()
|
||||||
|
self._init_remote_tx_controllers()
|
||||||
try:
|
try:
|
||||||
total = self.config.total_steps()
|
total = self.config.total_steps()
|
||||||
step_index = 0
|
step_index = 0
|
||||||
|
|
@ -248,6 +250,7 @@ class CampaignExecutor:
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
self._close_sdr()
|
self._close_sdr()
|
||||||
|
self._close_remote_tx_controllers()
|
||||||
|
|
||||||
result.end_time = time.time()
|
result.end_time = time.time()
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|
@ -287,6 +290,41 @@ class CampaignExecutor:
|
||||||
logger.warning(f"SDR close error: {e}")
|
logger.warning(f"SDR close error: {e}")
|
||||||
self._sdr = None
|
self._sdr = None
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Remote Tx controller management
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _init_remote_tx_controllers(self) -> None:
|
||||||
|
"""Open SSH+ZMQ connections for all sdr_remote transmitters."""
|
||||||
|
from ria_toolkit_oss.remote_control import RemoteTransmitterController
|
||||||
|
|
||||||
|
for tx in self.config.transmitters:
|
||||||
|
if tx.control_method != "sdr_remote":
|
||||||
|
continue
|
||||||
|
cfg = tx.sdr_remote
|
||||||
|
if not cfg:
|
||||||
|
raise RuntimeError(f"Transmitter '{tx.id}' uses sdr_remote but has no sdr_remote config")
|
||||||
|
logger.info(f"Connecting remote Tx controller for {tx.id} → {cfg['host']}")
|
||||||
|
ctrl = RemoteTransmitterController(
|
||||||
|
host=cfg["host"],
|
||||||
|
ssh_user=cfg["ssh_user"],
|
||||||
|
ssh_key_path=cfg["ssh_key_path"],
|
||||||
|
zmq_port=int(cfg.get("zmq_port", 5556)),
|
||||||
|
)
|
||||||
|
ctrl.set_radio(
|
||||||
|
device_type=cfg["device_type"],
|
||||||
|
device_id=cfg.get("device_id", ""),
|
||||||
|
)
|
||||||
|
self._remote_tx_controllers[tx.id] = ctrl
|
||||||
|
|
||||||
|
def _close_remote_tx_controllers(self) -> None:
|
||||||
|
for tx_id, ctrl in list(self._remote_tx_controllers.items()):
|
||||||
|
try:
|
||||||
|
ctrl.close()
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning(f"Error closing remote Tx controller {tx_id}: {exc}")
|
||||||
|
self._remote_tx_controllers.clear()
|
||||||
|
|
||||||
def _record(self, duration_s: float) -> Recording:
|
def _record(self, duration_s: float) -> Recording:
|
||||||
"""Capture ``duration_s`` seconds of IQ samples."""
|
"""Capture ``duration_s`` seconds of IQ samples."""
|
||||||
num_samples = int(duration_s * self.config.recorder.sample_rate)
|
num_samples = int(duration_s * self.config.recorder.sample_rate)
|
||||||
|
|
@ -372,7 +410,8 @@ class CampaignExecutor:
|
||||||
traffic, etc. The script is responsible for applying the configuration
|
traffic, etc. The script is responsible for applying the configuration
|
||||||
and returning promptly (i.e. not blocking for the capture duration).
|
and returning promptly (i.e. not blocking for the capture duration).
|
||||||
|
|
||||||
For SDR transmitters this is a no-op placeholder (TX not yet implemented).
|
For ``sdr_remote`` the remote ZMQ controller calls ``init_tx`` then
|
||||||
|
starts a background transmit thread that runs for the step duration.
|
||||||
"""
|
"""
|
||||||
if transmitter.control_method == "external_script":
|
if transmitter.control_method == "external_script":
|
||||||
if not transmitter.script:
|
if not transmitter.script:
|
||||||
|
|
@ -384,6 +423,20 @@ class CampaignExecutor:
|
||||||
elif transmitter.control_method == "sdr":
|
elif transmitter.control_method == "sdr":
|
||||||
logger.debug("SDR TX not yet implemented — skipping start")
|
logger.debug("SDR TX not yet implemented — skipping start")
|
||||||
|
|
||||||
|
elif transmitter.control_method == "sdr_remote":
|
||||||
|
ctrl = self._remote_tx_controllers.get(transmitter.id)
|
||||||
|
if ctrl is None:
|
||||||
|
raise RuntimeError(f"No remote Tx controller found for transmitter '{transmitter.id}'")
|
||||||
|
gain = step.power_dbm if step.power_dbm is not None else 0.0
|
||||||
|
ctrl.init_tx(
|
||||||
|
center_frequency=self.config.recorder.center_freq,
|
||||||
|
sample_rate=self.config.recorder.sample_rate,
|
||||||
|
gain=gain,
|
||||||
|
channel=step.channel or 0,
|
||||||
|
)
|
||||||
|
# Start transmission in background; _record() runs concurrently
|
||||||
|
ctrl.transmit_async(step.duration + 1.0)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logger.warning(f"Unknown control method '{transmitter.control_method}' — skipping")
|
logger.warning(f"Unknown control method '{transmitter.control_method}' — skipping")
|
||||||
|
|
||||||
|
|
@ -391,6 +444,7 @@ class CampaignExecutor:
|
||||||
"""Signal the transmitter to stop.
|
"""Signal the transmitter to stop.
|
||||||
|
|
||||||
Calls ``<script> stop`` for external_script transmitters.
|
Calls ``<script> stop`` for external_script transmitters.
|
||||||
|
For ``sdr_remote``, waits for the background transmit thread to finish.
|
||||||
"""
|
"""
|
||||||
if transmitter.control_method == "external_script":
|
if transmitter.control_method == "external_script":
|
||||||
if not transmitter.script:
|
if not transmitter.script:
|
||||||
|
|
@ -400,6 +454,11 @@ class CampaignExecutor:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Script stop failed for {transmitter.id}: {e}")
|
logger.warning(f"Script stop failed for {transmitter.id}: {e}")
|
||||||
|
|
||||||
|
elif transmitter.control_method == "sdr_remote":
|
||||||
|
ctrl = self._remote_tx_controllers.get(transmitter.id)
|
||||||
|
if ctrl is not None:
|
||||||
|
ctrl.wait_transmit(timeout=step.duration + 10.0)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _step_params_json(transmitter: TransmitterConfig, step: CaptureStep) -> str:
|
def _step_params_json(transmitter: TransmitterConfig, step: CaptureStep) -> str:
|
||||||
"""Serialise step parameters to a JSON string for the control script."""
|
"""Serialise step parameters to a JSON string for the control script."""
|
||||||
|
|
|
||||||
6
src/ria_toolkit_oss/remote_control/__init__.py
Normal file
6
src/ria_toolkit_oss/remote_control/__init__.py
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
"""Remote SDR transmitter control via SSH + ZMQ."""
|
||||||
|
|
||||||
|
from .remote_transmitter import RemoteTransmitter
|
||||||
|
from .remote_transmitter_controller import RemoteTransmitterController
|
||||||
|
|
||||||
|
__all__ = ["RemoteTransmitter", "RemoteTransmitterController"]
|
||||||
152
src/ria_toolkit_oss/remote_control/remote_transmitter.py
Normal file
152
src/ria_toolkit_oss/remote_control/remote_transmitter.py
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
"""Server-side ZMQ RPC receiver for SDR transmission.
|
||||||
|
|
||||||
|
Run this script on the Tx machine. The script binds a ZMQ REP socket and
|
||||||
|
waits for JSON-RPC commands from a :class:`RemoteTransmitterController`.
|
||||||
|
|
||||||
|
Requires: zmq, and ria-toolkit or utils installed for SDR support.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from contextlib import redirect_stderr, redirect_stdout
|
||||||
|
|
||||||
|
import zmq
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class RemoteTransmitter:
|
||||||
|
"""Executes SDR Tx commands received over ZMQ.
|
||||||
|
|
||||||
|
Loads the appropriate SDR driver dynamically so the script can run on
|
||||||
|
machines that have only a subset of SDR libraries installed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._sdr = None
|
||||||
|
|
||||||
|
def set_radio(self, radio_str: str, identifier: str = "") -> None:
|
||||||
|
"""Initialise the SDR radio.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
radio_str: SDR type — pluto | usrp | hackrf | bladerf.
|
||||||
|
identifier: Device-specific identifier (IP, serial, etc.).
|
||||||
|
"""
|
||||||
|
radio_str = radio_str.lower()
|
||||||
|
try:
|
||||||
|
if radio_str in ("pluto", "plutosdr"):
|
||||||
|
from ria_toolkit_oss.sdr.pluto import Pluto
|
||||||
|
|
||||||
|
self._sdr = Pluto(identifier)
|
||||||
|
elif radio_str in ("usrp",):
|
||||||
|
from ria_toolkit_oss.sdr.usrp import USRP
|
||||||
|
|
||||||
|
self._sdr = USRP(identifier)
|
||||||
|
elif radio_str in ("hackrf", "hackrf_one"):
|
||||||
|
from ria_toolkit_oss.sdr.hackrf import HackRF
|
||||||
|
|
||||||
|
self._sdr = HackRF(identifier)
|
||||||
|
elif radio_str in ("bladerf", "blade"):
|
||||||
|
from ria_toolkit_oss.sdr.blade import Blade
|
||||||
|
|
||||||
|
self._sdr = Blade(identifier)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown SDR type: {radio_str!r}")
|
||||||
|
except ImportError as exc:
|
||||||
|
raise RuntimeError(f"SDR driver for '{radio_str}' is not installed: {exc}") from exc
|
||||||
|
|
||||||
|
def init_tx(
|
||||||
|
self,
|
||||||
|
center_frequency: float,
|
||||||
|
sample_rate: float,
|
||||||
|
gain: float,
|
||||||
|
channel: int = 0,
|
||||||
|
gain_mode: str = "absolute",
|
||||||
|
) -> None:
|
||||||
|
if self._sdr is None:
|
||||||
|
raise RuntimeError("Call set_radio() before init_tx()")
|
||||||
|
self._sdr.init_tx(
|
||||||
|
center_frequency=center_frequency,
|
||||||
|
sample_rate=sample_rate,
|
||||||
|
gain=gain,
|
||||||
|
channel=channel,
|
||||||
|
)
|
||||||
|
|
||||||
|
def transmit(self, duration_s: float) -> None:
|
||||||
|
"""Transmit a continuous wave for ``duration_s`` seconds."""
|
||||||
|
if self._sdr is None:
|
||||||
|
raise RuntimeError("Call set_radio() and init_tx() before transmit()")
|
||||||
|
import time
|
||||||
|
|
||||||
|
# Transmit in a loop until duration has elapsed
|
||||||
|
end = time.monotonic() + duration_s
|
||||||
|
while time.monotonic() < end:
|
||||||
|
try:
|
||||||
|
self._sdr.tx_cw()
|
||||||
|
except AttributeError:
|
||||||
|
time.sleep(0.01)
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
"""Stop transmission and close the SDR."""
|
||||||
|
if self._sdr is not None:
|
||||||
|
try:
|
||||||
|
self._sdr.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._sdr = None
|
||||||
|
|
||||||
|
def run_function(self, command_dict: dict) -> dict:
|
||||||
|
"""Dispatch a JSON-RPC command and return a response dict."""
|
||||||
|
out_buf = io.StringIO()
|
||||||
|
err_buf = io.StringIO()
|
||||||
|
fn = command_dict.get("function_name", "")
|
||||||
|
try:
|
||||||
|
with redirect_stdout(out_buf), redirect_stderr(err_buf):
|
||||||
|
if fn == "set_radio":
|
||||||
|
self.set_radio(
|
||||||
|
radio_str=command_dict["radio_str"],
|
||||||
|
identifier=command_dict.get("identifier", ""),
|
||||||
|
)
|
||||||
|
elif fn == "init_tx":
|
||||||
|
self.init_tx(
|
||||||
|
center_frequency=command_dict["center_frequency"],
|
||||||
|
sample_rate=command_dict["sample_rate"],
|
||||||
|
gain=command_dict["gain"],
|
||||||
|
channel=command_dict.get("channel", 0),
|
||||||
|
gain_mode=command_dict.get("gain_mode", "absolute"),
|
||||||
|
)
|
||||||
|
elif fn == "transmit":
|
||||||
|
self.transmit(duration_s=command_dict.get("duration_s", 1.0))
|
||||||
|
elif fn == "stop":
|
||||||
|
self.stop()
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown function: {fn!r}")
|
||||||
|
return {"status": True, "message": out_buf.getvalue(), "error_message": err_buf.getvalue()}
|
||||||
|
except Exception as exc:
|
||||||
|
logger.exception("Error executing %s", fn)
|
||||||
|
return {"status": False, "message": out_buf.getvalue(), "error_message": str(exc)}
|
||||||
|
|
||||||
|
|
||||||
|
def _serve(port: int) -> None:
|
||||||
|
context = zmq.Context()
|
||||||
|
socket = context.socket(zmq.REP)
|
||||||
|
socket.bind(f"tcp://*:{port}")
|
||||||
|
logger.info("RemoteTransmitter listening on port %d", port)
|
||||||
|
tx = RemoteTransmitter()
|
||||||
|
while True:
|
||||||
|
raw = socket.recv()
|
||||||
|
cmd = json.loads(raw.decode())
|
||||||
|
response = tx.run_function(cmd)
|
||||||
|
socket.send(json.dumps(response).encode())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
parser = argparse.ArgumentParser(description="SDR Tx ZMQ server")
|
||||||
|
parser.add_argument("--port", type=int, default=5556)
|
||||||
|
args = parser.parse_args()
|
||||||
|
_serve(args.port)
|
||||||
|
|
@ -0,0 +1,218 @@
|
||||||
|
"""Client-side SSH + ZMQ controller for a remote SDR transmitter.
|
||||||
|
|
||||||
|
Run this on the Rx machine (or hub). It SSH-es into the Tx machine,
|
||||||
|
starts :mod:`remote_transmitter` there, then sends JSON-RPC commands over
|
||||||
|
ZMQ.
|
||||||
|
|
||||||
|
Requires: paramiko, zmq.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
import paramiko
|
||||||
|
import zmq
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
_STARTUP_WAIT_S = 2.0 # seconds to wait for remote ZMQ server to bind
|
||||||
|
|
||||||
|
|
||||||
|
class RemoteTransmitterController:
|
||||||
|
"""SSH into a Tx machine, start the ZMQ server, and send commands.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
host: IP or hostname of the Tx machine.
|
||||||
|
ssh_user: SSH username.
|
||||||
|
ssh_key_path: Path to SSH private key file.
|
||||||
|
zmq_port: ZMQ port that the remote transmitter will bind on.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
host: str,
|
||||||
|
ssh_user: str,
|
||||||
|
ssh_key_path: str,
|
||||||
|
zmq_port: int = 5556,
|
||||||
|
) -> None:
|
||||||
|
self._host = host
|
||||||
|
self._zmq_port = zmq_port
|
||||||
|
self._ssh: paramiko.SSHClient | None = None
|
||||||
|
self._ssh_stdout = None
|
||||||
|
self._context: zmq.Context | None = None
|
||||||
|
self._socket: zmq.Socket | None = None
|
||||||
|
self._tx_thread: threading.Thread | None = None
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
|
||||||
|
self._connect(host, ssh_user, ssh_key_path, zmq_port)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Connection management
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _connect(self, host: str, ssh_user: str, ssh_key_path: str, zmq_port: int) -> None:
|
||||||
|
"""Open SSH tunnel, start remote server, connect ZMQ socket."""
|
||||||
|
try:
|
||||||
|
import paramiko
|
||||||
|
except ImportError as exc:
|
||||||
|
raise RuntimeError("paramiko is required for remote SDR control: pip install paramiko") from exc
|
||||||
|
try:
|
||||||
|
import zmq
|
||||||
|
except ImportError as exc:
|
||||||
|
raise RuntimeError("pyzmq is required for remote SDR control: pip install pyzmq") from exc
|
||||||
|
|
||||||
|
logger.info("SSH connecting to %s@%s …", ssh_user, host)
|
||||||
|
self._ssh = paramiko.SSHClient()
|
||||||
|
self._ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
self._ssh.connect(hostname=host, username=ssh_user, key_filename=ssh_key_path)
|
||||||
|
|
||||||
|
cmd = f"python -m ria_toolkit_oss.remote_control.remote_transmitter --port {zmq_port}"
|
||||||
|
logger.info("Starting remote Tx server: %s", cmd)
|
||||||
|
_, self._ssh_stdout, _ = self._ssh.exec_command(cmd)
|
||||||
|
|
||||||
|
time.sleep(_STARTUP_WAIT_S)
|
||||||
|
|
||||||
|
self._context = zmq.Context()
|
||||||
|
self._socket = self._context.socket(zmq.REQ)
|
||||||
|
self._socket.connect(f"tcp://{host}:{zmq_port}")
|
||||||
|
logger.info("ZMQ connected to tcp://%s:%d", host, zmq_port)
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
"""Tear down ZMQ and SSH connections."""
|
||||||
|
if self._socket is not None:
|
||||||
|
try:
|
||||||
|
self._socket.close(linger=0)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._socket = None
|
||||||
|
if self._context is not None:
|
||||||
|
try:
|
||||||
|
self._context.term()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._context = None
|
||||||
|
if self._ssh_stdout is not None:
|
||||||
|
try:
|
||||||
|
self._ssh_stdout.channel.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._ssh_stdout = None
|
||||||
|
if self._ssh is not None:
|
||||||
|
try:
|
||||||
|
self._ssh.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._ssh = None
|
||||||
|
logger.info("RemoteTransmitterController closed")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# ZMQ dispatch
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _send(self, command: dict) -> dict:
|
||||||
|
"""Send a JSON-RPC command and return the response dict (thread-safe)."""
|
||||||
|
with self._lock:
|
||||||
|
if self._socket is None:
|
||||||
|
raise RuntimeError("Controller is closed")
|
||||||
|
self._socket.send(json.dumps(command).encode())
|
||||||
|
raw = self._socket.recv()
|
||||||
|
reply: dict = json.loads(raw.decode())
|
||||||
|
if not reply.get("status"):
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Remote command '{command.get('function_name')}' failed: "
|
||||||
|
f"{reply.get('error_message', 'unknown error')}"
|
||||||
|
)
|
||||||
|
return reply
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Public API
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def set_radio(self, device_type: str, device_id: str = "") -> None:
|
||||||
|
"""Initialise the SDR radio on the Tx machine.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
device_type: SDR type — ``pluto``, ``usrp``, ``hackrf``, ``bladerf``.
|
||||||
|
device_id: Device-specific identifier (IP, serial, etc.).
|
||||||
|
"""
|
||||||
|
logger.info("set_radio(%s, %r)", device_type, device_id)
|
||||||
|
self._send({"function_name": "set_radio", "radio_str": device_type, "identifier": device_id})
|
||||||
|
|
||||||
|
def init_tx(
|
||||||
|
self,
|
||||||
|
center_frequency: float,
|
||||||
|
sample_rate: float,
|
||||||
|
gain: float,
|
||||||
|
channel: int = 0,
|
||||||
|
gain_mode: str = "absolute",
|
||||||
|
) -> None:
|
||||||
|
"""Configure Tx parameters on the remote SDR.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
center_frequency: Center frequency in Hz.
|
||||||
|
sample_rate: Sample rate in Hz.
|
||||||
|
gain: Tx gain in dB.
|
||||||
|
channel: RF channel index (default 0).
|
||||||
|
gain_mode: ``"absolute"`` (default) or ``"relative"``.
|
||||||
|
"""
|
||||||
|
logger.info(
|
||||||
|
"init_tx: fc=%.3f MHz, fs=%.3f MHz, gain=%.1f dB, ch=%d",
|
||||||
|
center_frequency / 1e6,
|
||||||
|
sample_rate / 1e6,
|
||||||
|
gain,
|
||||||
|
channel,
|
||||||
|
)
|
||||||
|
self._send(
|
||||||
|
{
|
||||||
|
"function_name": "init_tx",
|
||||||
|
"center_frequency": center_frequency,
|
||||||
|
"sample_rate": sample_rate,
|
||||||
|
"gain": gain,
|
||||||
|
"channel": channel,
|
||||||
|
"gain_mode": gain_mode,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def transmit_async(self, duration_s: float) -> None:
|
||||||
|
"""Start a timed CW transmission in a background thread.
|
||||||
|
|
||||||
|
Returns immediately. Call :meth:`wait_transmit` after recording to
|
||||||
|
ensure the transmit thread has finished before the next step.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
duration_s: Transmission duration in seconds.
|
||||||
|
"""
|
||||||
|
logger.info("transmit_async: %.1f s", duration_s)
|
||||||
|
|
||||||
|
def _run() -> None:
|
||||||
|
try:
|
||||||
|
self._send({"function_name": "transmit", "duration_s": duration_s})
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("Background transmit error: %s", exc)
|
||||||
|
|
||||||
|
self._tx_thread = threading.Thread(target=_run, daemon=True, name="remote-tx")
|
||||||
|
self._tx_thread.start()
|
||||||
|
|
||||||
|
def wait_transmit(self, timeout: float | None = None) -> None:
|
||||||
|
"""Wait for the background transmit thread to finish.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
timeout: Maximum seconds to wait. ``None`` = wait indefinitely.
|
||||||
|
"""
|
||||||
|
if self._tx_thread is not None:
|
||||||
|
self._tx_thread.join(timeout=timeout)
|
||||||
|
self._tx_thread = None
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
"""Stop transmission and release the remote SDR, then close connections."""
|
||||||
|
logger.info("Sending stop to remote Tx")
|
||||||
|
try:
|
||||||
|
self._send({"function_name": "stop"})
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("stop command error (may be normal if connection closed): %s", exc)
|
||||||
|
finally:
|
||||||
|
self.close()
|
||||||
|
|
@ -43,6 +43,13 @@ class SDR(ABC):
|
||||||
self.tx_gain = None
|
self.tx_gain = None
|
||||||
self._param_lock = threading.RLock() # Reentrant lock
|
self._param_lock = threading.RLock() # Reentrant lock
|
||||||
|
|
||||||
|
# Pending config consumed by rx() on first call and by _apply_sdr_config
|
||||||
|
# in the agent inference loop. Subclasses that need different defaults
|
||||||
|
# (e.g. MockSDR) can overwrite these in their own __init__.
|
||||||
|
self.center_freq: float = 2.4e9
|
||||||
|
self.sample_rate: float = 10e6
|
||||||
|
self.gain: float = 40.0
|
||||||
|
|
||||||
def record(self, num_samples: Optional[int] = None, rx_time: Optional[int | float] = None) -> Recording:
|
def record(self, num_samples: Optional[int] = None, rx_time: Optional[int | float] = None) -> Recording:
|
||||||
"""
|
"""
|
||||||
Create a radio recording of a given length. Either ``num_samples`` or ``rx_time`` must be provided.
|
Create a radio recording of a given length. Either ``num_samples`` or ``rx_time`` must be provided.
|
||||||
|
|
@ -100,6 +107,32 @@ class SDR(ABC):
|
||||||
self._num_buffers_processed = 0
|
self._num_buffers_processed = 0
|
||||||
return recording
|
return recording
|
||||||
|
|
||||||
|
def rx(self, num_samples: int) -> "np.ndarray":
|
||||||
|
"""Return *num_samples* complex IQ samples as a 1-D complex64 array.
|
||||||
|
|
||||||
|
This is the interface used by the agent inference loop. On first call,
|
||||||
|
``init_rx()`` is invoked automatically using the values stored in
|
||||||
|
``center_freq``, ``sample_rate``, and ``gain`` (set beforehand by
|
||||||
|
``_apply_sdr_config``). Subsequent calls stream directly.
|
||||||
|
|
||||||
|
Subclasses may override this for hardware-native capture APIs (e.g.
|
||||||
|
``MockSDR`` uses AWGN generation; ``PlutoSDR`` could use
|
||||||
|
``self.radio.rx()``).
|
||||||
|
"""
|
||||||
|
if not self._rx_initialized:
|
||||||
|
gain = self.gain if isinstance(self.gain, (int, float)) else 40.0
|
||||||
|
self.init_rx(
|
||||||
|
sample_rate=self.sample_rate,
|
||||||
|
center_frequency=self.center_freq,
|
||||||
|
gain=gain,
|
||||||
|
channel=0,
|
||||||
|
)
|
||||||
|
recording = self.record(num_samples=num_samples)
|
||||||
|
# Recording.data is either a list of 1-D arrays (one per channel) or a
|
||||||
|
# 2-D ndarray (channels × samples). Either way, index 0 is channel 0.
|
||||||
|
data = recording.data
|
||||||
|
return data[0] if hasattr(data, "__getitem__") else data
|
||||||
|
|
||||||
def stream_to_zmq(self, zmq_address, n_samples: int, buffer_size: Optional[int] = 10000):
|
def stream_to_zmq(self, zmq_address, n_samples: int, buffer_size: Optional[int] = 10000):
|
||||||
"""
|
"""
|
||||||
Stream iq samples as interleaved bytes via zmq.
|
Stream iq samples as interleaved bytes via zmq.
|
||||||
|
|
|
||||||
0
tests/remote_control/__init__.py
Normal file
0
tests/remote_control/__init__.py
Normal file
296
tests/remote_control/test_remote_transmitter.py
Normal file
296
tests/remote_control/test_remote_transmitter.py
Normal file
|
|
@ -0,0 +1,296 @@
|
||||||
|
"""Tests for the server-side RemoteTransmitter ZMQ RPC dispatcher.
|
||||||
|
|
||||||
|
No real SDR hardware or ZMQ sockets are needed — we test run_function()
|
||||||
|
directly and mock the SDR drivers.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from ria_toolkit_oss.remote_control.remote_transmitter import RemoteTransmitter
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _make_mock_sdr():
|
||||||
|
sdr = MagicMock()
|
||||||
|
sdr.init_tx = MagicMock()
|
||||||
|
sdr.tx_cw = MagicMock()
|
||||||
|
sdr.close = MagicMock()
|
||||||
|
return sdr
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# set_radio dispatch
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestSetRadio:
|
||||||
|
def _pluto_module(self, mock_sdr):
|
||||||
|
mod = MagicMock()
|
||||||
|
mod.Pluto = MagicMock(return_value=mock_sdr)
|
||||||
|
return mod
|
||||||
|
|
||||||
|
def test_pluto_alias(self):
|
||||||
|
tx = RemoteTransmitter()
|
||||||
|
mock_sdr = _make_mock_sdr()
|
||||||
|
with patch.dict("sys.modules", {"ria_toolkit_oss.sdr.pluto": self._pluto_module(mock_sdr)}):
|
||||||
|
tx.set_radio("pluto", "ip:192.168.2.1")
|
||||||
|
assert tx._sdr is mock_sdr
|
||||||
|
|
||||||
|
def test_plutosdr_alias(self):
|
||||||
|
tx = RemoteTransmitter()
|
||||||
|
mock_sdr = _make_mock_sdr()
|
||||||
|
with patch.dict("sys.modules", {"ria_toolkit_oss.sdr.pluto": self._pluto_module(mock_sdr)}):
|
||||||
|
tx.set_radio("PlutoSDR", "ip:192.168.2.1")
|
||||||
|
assert tx._sdr is mock_sdr
|
||||||
|
|
||||||
|
def test_usrp_alias(self):
|
||||||
|
tx = RemoteTransmitter()
|
||||||
|
mock_sdr = _make_mock_sdr()
|
||||||
|
mock_module = MagicMock()
|
||||||
|
mock_module.USRP = MagicMock(return_value=mock_sdr)
|
||||||
|
with patch.dict("sys.modules", {"ria_toolkit_oss.sdr.usrp": mock_module}):
|
||||||
|
tx.set_radio("usrp", "usrp://addr=192.168.10.2")
|
||||||
|
assert tx._sdr is mock_sdr
|
||||||
|
|
||||||
|
def test_hackrf_alias(self):
|
||||||
|
tx = RemoteTransmitter()
|
||||||
|
mock_sdr = _make_mock_sdr()
|
||||||
|
mock_module = MagicMock()
|
||||||
|
mock_module.HackRF = MagicMock(return_value=mock_sdr)
|
||||||
|
with patch.dict("sys.modules", {"ria_toolkit_oss.sdr.hackrf": mock_module}):
|
||||||
|
tx.set_radio("hackrf", "")
|
||||||
|
assert tx._sdr is mock_sdr
|
||||||
|
|
||||||
|
def test_hackrf_one_alias(self):
|
||||||
|
tx = RemoteTransmitter()
|
||||||
|
mock_sdr = _make_mock_sdr()
|
||||||
|
mock_module = MagicMock()
|
||||||
|
mock_module.HackRF = MagicMock(return_value=mock_sdr)
|
||||||
|
with patch.dict("sys.modules", {"ria_toolkit_oss.sdr.hackrf": mock_module}):
|
||||||
|
tx.set_radio("hackrf_one", "")
|
||||||
|
assert tx._sdr is mock_sdr
|
||||||
|
|
||||||
|
def test_bladerf_alias(self):
|
||||||
|
tx = RemoteTransmitter()
|
||||||
|
mock_sdr = _make_mock_sdr()
|
||||||
|
mock_module = MagicMock()
|
||||||
|
mock_module.Blade = MagicMock(return_value=mock_sdr)
|
||||||
|
with patch.dict("sys.modules", {"ria_toolkit_oss.sdr.blade": mock_module}):
|
||||||
|
tx.set_radio("blade", "")
|
||||||
|
assert tx._sdr is mock_sdr
|
||||||
|
|
||||||
|
def test_bladerf_string_alias(self):
|
||||||
|
"""'bladerf' string (not 'blade') must also resolve to blade.Blade."""
|
||||||
|
tx = RemoteTransmitter()
|
||||||
|
mock_sdr = _make_mock_sdr()
|
||||||
|
mock_module = MagicMock()
|
||||||
|
mock_module.Blade = MagicMock(return_value=mock_sdr)
|
||||||
|
with patch.dict("sys.modules", {"ria_toolkit_oss.sdr.blade": mock_module}):
|
||||||
|
tx.set_radio("bladerf", "")
|
||||||
|
assert tx._sdr is mock_sdr
|
||||||
|
|
||||||
|
def test_case_insensitive(self):
|
||||||
|
tx = RemoteTransmitter()
|
||||||
|
mock_sdr = _make_mock_sdr()
|
||||||
|
with patch.dict("sys.modules", {"ria_toolkit_oss.sdr.pluto": self._pluto_module(mock_sdr)}):
|
||||||
|
tx.set_radio("PLUTO", "ip:192.168.2.1")
|
||||||
|
assert tx._sdr is mock_sdr
|
||||||
|
|
||||||
|
def test_unknown_radio_raises(self):
|
||||||
|
tx = RemoteTransmitter()
|
||||||
|
with pytest.raises(ValueError, match="Unknown SDR type"):
|
||||||
|
tx.set_radio("nonexistent_radio")
|
||||||
|
|
||||||
|
def test_import_error_raises_runtime(self):
|
||||||
|
"""ImportError during SDR driver load is re-raised as RuntimeError."""
|
||||||
|
tx = RemoteTransmitter()
|
||||||
|
# Inject a fake module whose Pluto class raises ImportError on import
|
||||||
|
bad_module = MagicMock()
|
||||||
|
bad_module.Pluto = MagicMock(side_effect=ImportError("pyadi-iio not installed"))
|
||||||
|
with patch.dict("sys.modules", {"ria_toolkit_oss.sdr.pluto": bad_module}):
|
||||||
|
with pytest.raises((RuntimeError, ImportError)):
|
||||||
|
tx.set_radio("pluto")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# init_tx / transmit / stop guard
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestInitTxGuards:
|
||||||
|
def test_init_tx_without_set_radio_raises(self):
|
||||||
|
tx = RemoteTransmitter()
|
||||||
|
with pytest.raises(RuntimeError, match="set_radio"):
|
||||||
|
tx.init_tx(center_frequency=2.4e9, sample_rate=20e6, gain=0)
|
||||||
|
|
||||||
|
def test_transmit_without_set_radio_raises(self):
|
||||||
|
tx = RemoteTransmitter()
|
||||||
|
with pytest.raises(RuntimeError):
|
||||||
|
tx.transmit(duration_s=0.1)
|
||||||
|
|
||||||
|
def test_stop_without_set_radio_is_safe(self):
|
||||||
|
tx = RemoteTransmitter()
|
||||||
|
tx.stop() # should not raise — nothing to close
|
||||||
|
|
||||||
|
|
||||||
|
class TestInitTx:
|
||||||
|
def _tx_with_mock_sdr(self):
|
||||||
|
tx = RemoteTransmitter()
|
||||||
|
tx._sdr = _make_mock_sdr()
|
||||||
|
return tx
|
||||||
|
|
||||||
|
def test_delegates_to_sdr(self):
|
||||||
|
tx = self._tx_with_mock_sdr()
|
||||||
|
tx.init_tx(center_frequency=2.4e9, sample_rate=20e6, gain=30, channel=1)
|
||||||
|
tx._sdr.init_tx.assert_called_once_with(
|
||||||
|
center_frequency=2.4e9,
|
||||||
|
sample_rate=20e6,
|
||||||
|
gain=30,
|
||||||
|
channel=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_default_channel_zero(self):
|
||||||
|
tx = self._tx_with_mock_sdr()
|
||||||
|
tx.init_tx(center_frequency=2.4e9, sample_rate=20e6, gain=30)
|
||||||
|
_, kwargs = tx._sdr.init_tx.call_args
|
||||||
|
assert kwargs["channel"] == 0
|
||||||
|
|
||||||
|
|
||||||
|
class TestTransmit:
|
||||||
|
def test_calls_tx_cw_until_duration(self):
|
||||||
|
tx = RemoteTransmitter()
|
||||||
|
tx._sdr = _make_mock_sdr()
|
||||||
|
tx.init_tx(center_frequency=2.4e9, sample_rate=20e6, gain=0)
|
||||||
|
tx.transmit(duration_s=0.05)
|
||||||
|
assert tx._sdr.tx_cw.called
|
||||||
|
|
||||||
|
def test_zero_duration_does_not_call_tx_cw(self):
|
||||||
|
tx = RemoteTransmitter()
|
||||||
|
tx._sdr = _make_mock_sdr()
|
||||||
|
tx.init_tx(center_frequency=2.4e9, sample_rate=20e6, gain=0)
|
||||||
|
tx.transmit(duration_s=0.0)
|
||||||
|
tx._sdr.tx_cw.assert_not_called()
|
||||||
|
|
||||||
|
def test_missing_tx_cw_method_handled(self):
|
||||||
|
"""AttributeError on tx_cw should not crash transmit()."""
|
||||||
|
tx = RemoteTransmitter()
|
||||||
|
sdr = MagicMock(spec=[]) # no tx_cw attribute
|
||||||
|
sdr.init_tx = MagicMock()
|
||||||
|
tx._sdr = sdr
|
||||||
|
# Should not raise — AttributeError is caught and slept through
|
||||||
|
tx.transmit(duration_s=0.01)
|
||||||
|
|
||||||
|
|
||||||
|
class TestStop:
|
||||||
|
def test_calls_close_and_clears_sdr(self):
|
||||||
|
tx = RemoteTransmitter()
|
||||||
|
mock_sdr = _make_mock_sdr()
|
||||||
|
tx._sdr = mock_sdr
|
||||||
|
tx.stop()
|
||||||
|
mock_sdr.close.assert_called_once()
|
||||||
|
assert tx._sdr is None
|
||||||
|
|
||||||
|
def test_close_exception_is_swallowed(self):
|
||||||
|
tx = RemoteTransmitter()
|
||||||
|
sdr = _make_mock_sdr()
|
||||||
|
sdr.close.side_effect = RuntimeError("hardware error")
|
||||||
|
tx._sdr = sdr
|
||||||
|
tx.stop() # should not raise
|
||||||
|
assert tx._sdr is None
|
||||||
|
|
||||||
|
def test_stop_idempotent(self):
|
||||||
|
tx = RemoteTransmitter()
|
||||||
|
tx.stop()
|
||||||
|
tx.stop() # second call is safe
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# run_function dispatcher
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestRunFunction:
|
||||||
|
def _tx_with_mock_sdr(self):
|
||||||
|
tx = RemoteTransmitter()
|
||||||
|
tx._sdr = _make_mock_sdr()
|
||||||
|
return tx
|
||||||
|
|
||||||
|
def test_unknown_function_returns_failure(self):
|
||||||
|
tx = RemoteTransmitter()
|
||||||
|
resp = tx.run_function({"function_name": "explode"})
|
||||||
|
assert resp["status"] is False
|
||||||
|
assert "explode" in resp["error_message"]
|
||||||
|
|
||||||
|
def test_set_radio_success(self):
|
||||||
|
tx = RemoteTransmitter()
|
||||||
|
mock_sdr = _make_mock_sdr()
|
||||||
|
mod = MagicMock()
|
||||||
|
mod.Pluto = MagicMock(return_value=mock_sdr)
|
||||||
|
with patch.dict("sys.modules", {"ria_toolkit_oss.sdr.pluto": mod}):
|
||||||
|
resp = tx.run_function({"function_name": "set_radio", "radio_str": "pluto", "identifier": "ip:1.2.3.4"})
|
||||||
|
assert resp["status"] is True
|
||||||
|
|
||||||
|
def test_set_radio_bad_type_returns_failure(self):
|
||||||
|
tx = RemoteTransmitter()
|
||||||
|
resp = tx.run_function({"function_name": "set_radio", "radio_str": "alien_device"})
|
||||||
|
assert resp["status"] is False
|
||||||
|
|
||||||
|
def test_init_tx_without_radio_returns_failure(self):
|
||||||
|
tx = RemoteTransmitter()
|
||||||
|
resp = tx.run_function(
|
||||||
|
{
|
||||||
|
"function_name": "init_tx",
|
||||||
|
"center_frequency": 2.4e9,
|
||||||
|
"sample_rate": 20e6,
|
||||||
|
"gain": 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert resp["status"] is False
|
||||||
|
assert resp["error_message"]
|
||||||
|
|
||||||
|
def test_init_tx_with_radio_success(self):
|
||||||
|
tx = self._tx_with_mock_sdr()
|
||||||
|
resp = tx.run_function(
|
||||||
|
{
|
||||||
|
"function_name": "init_tx",
|
||||||
|
"center_frequency": 2.4e9,
|
||||||
|
"sample_rate": 20e6,
|
||||||
|
"gain": 30,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert resp["status"] is True
|
||||||
|
|
||||||
|
def test_transmit_runs_for_short_duration(self):
|
||||||
|
tx = self._tx_with_mock_sdr()
|
||||||
|
tx._sdr.init_tx = MagicMock()
|
||||||
|
resp = tx.run_function(
|
||||||
|
{
|
||||||
|
"function_name": "init_tx",
|
||||||
|
"center_frequency": 2.4e9,
|
||||||
|
"sample_rate": 20e6,
|
||||||
|
"gain": 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
resp = tx.run_function({"function_name": "transmit", "duration_s": 0.02})
|
||||||
|
assert resp["status"] is True
|
||||||
|
|
||||||
|
def test_stop_via_run_function(self):
|
||||||
|
tx = self._tx_with_mock_sdr()
|
||||||
|
resp = tx.run_function({"function_name": "stop"})
|
||||||
|
assert resp["status"] is True
|
||||||
|
assert tx._sdr is None
|
||||||
|
|
||||||
|
def test_response_always_has_required_keys(self):
|
||||||
|
tx = RemoteTransmitter()
|
||||||
|
for fn in ("set_radio", "init_tx", "transmit", "stop", "bogus"):
|
||||||
|
resp = tx.run_function({"function_name": fn})
|
||||||
|
assert "status" in resp
|
||||||
|
assert "message" in resp
|
||||||
|
assert "error_message" in resp
|
||||||
288
tests/remote_control/test_remote_transmitter_controller.py
Normal file
288
tests/remote_control/test_remote_transmitter_controller.py
Normal file
|
|
@ -0,0 +1,288 @@
|
||||||
|
"""Tests for RemoteTransmitterController — mocks paramiko and ZMQ entirely.
|
||||||
|
|
||||||
|
paramiko and zmq are optional runtime deps; these tests inject fakes into
|
||||||
|
sys.modules so they run regardless of whether the packages are installed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
from types import ModuleType
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Fake modules injected into sys.modules before any import of the controller
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _make_fake_paramiko(mock_ssh_instance):
|
||||||
|
"""Return a fake paramiko module whose SSHClient() returns mock_ssh_instance."""
|
||||||
|
mod = MagicMock(spec=ModuleType)
|
||||||
|
mod.SSHClient = MagicMock(return_value=mock_ssh_instance)
|
||||||
|
mod.AutoAddPolicy = MagicMock()
|
||||||
|
return mod
|
||||||
|
|
||||||
|
|
||||||
|
def _make_fake_zmq(mock_socket_instance):
|
||||||
|
"""Return a fake zmq module whose Context().socket() returns mock_socket_instance."""
|
||||||
|
mock_context = MagicMock()
|
||||||
|
mock_context.socket.return_value = mock_socket_instance
|
||||||
|
mod = MagicMock(spec=ModuleType)
|
||||||
|
mod.Context = MagicMock(return_value=mock_context)
|
||||||
|
mod.REQ = "REQ"
|
||||||
|
return mod, mock_context
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Fixtures
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _ok_response(fn="set_radio") -> bytes:
|
||||||
|
return json.dumps({"status": True, "message": "", "error_message": ""}).encode()
|
||||||
|
|
||||||
|
|
||||||
|
def _err_response(fn="set_radio", msg="boom") -> bytes:
|
||||||
|
return json.dumps({"status": False, "message": "", "error_message": msg}).encode()
|
||||||
|
|
||||||
|
|
||||||
|
def _make_mock_socket(recv_side_effect=None):
|
||||||
|
sock = MagicMock()
|
||||||
|
if recv_side_effect is not None:
|
||||||
|
sock.recv.side_effect = recv_side_effect
|
||||||
|
else:
|
||||||
|
sock.recv.return_value = _ok_response()
|
||||||
|
return sock
|
||||||
|
|
||||||
|
|
||||||
|
def _make_controller(mock_socket=None, *, startup_wait=0):
|
||||||
|
"""Build a controller with all external I/O mocked via sys.modules injection."""
|
||||||
|
mock_sock = mock_socket or _make_mock_socket()
|
||||||
|
mock_ssh = MagicMock()
|
||||||
|
mock_stdout = MagicMock()
|
||||||
|
mock_stdout.channel = MagicMock()
|
||||||
|
mock_ssh.exec_command.return_value = (MagicMock(), mock_stdout, MagicMock())
|
||||||
|
|
||||||
|
fake_paramiko = _make_fake_paramiko(mock_ssh)
|
||||||
|
fake_zmq, mock_context = _make_fake_zmq(mock_sock)
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch.dict("sys.modules", {"paramiko": fake_paramiko, "zmq": fake_zmq}),
|
||||||
|
patch(
|
||||||
|
"ria_toolkit_oss.remote_control.remote_transmitter_controller._STARTUP_WAIT_S",
|
||||||
|
startup_wait,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
from ria_toolkit_oss.remote_control.remote_transmitter_controller import (
|
||||||
|
RemoteTransmitterController,
|
||||||
|
)
|
||||||
|
|
||||||
|
ctrl = RemoteTransmitterController(
|
||||||
|
host="192.168.1.10",
|
||||||
|
ssh_user="ubuntu",
|
||||||
|
ssh_key_path="/home/user/.ssh/id_rsa",
|
||||||
|
zmq_port=5556,
|
||||||
|
)
|
||||||
|
|
||||||
|
ctrl._mock_ssh = mock_ssh
|
||||||
|
ctrl._mock_socket = mock_sock
|
||||||
|
ctrl._mock_context = mock_context
|
||||||
|
ctrl._fake_paramiko = fake_paramiko
|
||||||
|
return ctrl
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Connection setup
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestConnectionSetup:
|
||||||
|
def test_ssh_connects_with_correct_args(self):
|
||||||
|
ctrl = _make_controller()
|
||||||
|
ctrl._mock_ssh.connect.assert_called_once_with(
|
||||||
|
hostname="192.168.1.10",
|
||||||
|
username="ubuntu",
|
||||||
|
key_filename="/home/user/.ssh/id_rsa",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_ssh_starts_remote_server(self):
|
||||||
|
ctrl = _make_controller()
|
||||||
|
cmd = ctrl._mock_ssh.exec_command.call_args[0][0]
|
||||||
|
assert "remote_transmitter" in cmd
|
||||||
|
assert "--port" in cmd
|
||||||
|
assert "5556" in cmd
|
||||||
|
|
||||||
|
def test_zmq_connects_to_host_port(self):
|
||||||
|
ctrl = _make_controller()
|
||||||
|
ctrl._mock_socket.connect.assert_called_once_with("tcp://192.168.1.10:5556")
|
||||||
|
|
||||||
|
def test_host_key_policy_set_to_auto_add(self):
|
||||||
|
"""AutoAddPolicy is applied so we don't prompt in headless execution."""
|
||||||
|
ctrl = _make_controller()
|
||||||
|
ctrl._mock_ssh.set_missing_host_key_policy.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# ZMQ message format
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestSendFormat:
|
||||||
|
def test_set_radio_sends_correct_dict(self):
|
||||||
|
ctrl = _make_controller()
|
||||||
|
ctrl.set_radio("pluto", "ip:192.168.2.1")
|
||||||
|
sent = json.loads(ctrl._mock_socket.send.call_args[0][0].decode())
|
||||||
|
assert sent["function_name"] == "set_radio"
|
||||||
|
assert sent["radio_str"] == "pluto"
|
||||||
|
assert sent["identifier"] == "ip:192.168.2.1"
|
||||||
|
|
||||||
|
def test_set_radio_default_identifier(self):
|
||||||
|
ctrl = _make_controller()
|
||||||
|
ctrl.set_radio("hackrf")
|
||||||
|
sent = json.loads(ctrl._mock_socket.send.call_args[0][0].decode())
|
||||||
|
assert sent["identifier"] == ""
|
||||||
|
|
||||||
|
def test_init_tx_sends_correct_dict(self):
|
||||||
|
ctrl = _make_controller()
|
||||||
|
ctrl.init_tx(center_frequency=2.4e9, sample_rate=20e6, gain=30, channel=1)
|
||||||
|
sent = json.loads(ctrl._mock_socket.send.call_args[0][0].decode())
|
||||||
|
assert sent["function_name"] == "init_tx"
|
||||||
|
assert sent["center_frequency"] == pytest.approx(2.4e9)
|
||||||
|
assert sent["sample_rate"] == pytest.approx(20e6)
|
||||||
|
assert sent["gain"] == pytest.approx(30)
|
||||||
|
assert sent["channel"] == 1
|
||||||
|
assert sent["gain_mode"] == "absolute"
|
||||||
|
|
||||||
|
def test_init_tx_default_channel_zero(self):
|
||||||
|
ctrl = _make_controller()
|
||||||
|
ctrl.init_tx(center_frequency=2.4e9, sample_rate=20e6, gain=0)
|
||||||
|
sent = json.loads(ctrl._mock_socket.send.call_args[0][0].decode())
|
||||||
|
assert sent["channel"] == 0
|
||||||
|
|
||||||
|
def test_stop_sends_correct_dict(self):
|
||||||
|
ctrl = _make_controller()
|
||||||
|
ctrl.stop()
|
||||||
|
sent = json.loads(ctrl._mock_socket.send.call_args[0][0].decode())
|
||||||
|
assert sent["function_name"] == "stop"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Error handling
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestErrorHandling:
|
||||||
|
def test_error_response_raises_runtime_error(self):
|
||||||
|
sock = _make_mock_socket()
|
||||||
|
sock.recv.return_value = _err_response(msg="radio not found")
|
||||||
|
ctrl = _make_controller(mock_socket=sock)
|
||||||
|
with pytest.raises(RuntimeError, match="radio not found"):
|
||||||
|
ctrl.set_radio("pluto")
|
||||||
|
|
||||||
|
def test_error_message_included_in_exception(self):
|
||||||
|
sock = _make_mock_socket()
|
||||||
|
sock.recv.return_value = _err_response(msg="gain out of range")
|
||||||
|
ctrl = _make_controller(mock_socket=sock)
|
||||||
|
with pytest.raises(RuntimeError, match="gain out of range"):
|
||||||
|
ctrl.init_tx(center_frequency=2.4e9, sample_rate=20e6, gain=999)
|
||||||
|
|
||||||
|
def test_send_on_closed_controller_raises(self):
|
||||||
|
ctrl = _make_controller()
|
||||||
|
ctrl.close()
|
||||||
|
with pytest.raises(RuntimeError, match="closed"):
|
||||||
|
ctrl._send({"function_name": "set_radio", "radio_str": "pluto", "identifier": ""})
|
||||||
|
|
||||||
|
def test_missing_paramiko_raises_runtime_error(self):
|
||||||
|
"""If paramiko is absent, connecting gives a clear RuntimeError."""
|
||||||
|
import ria_toolkit_oss.remote_control.remote_transmitter_controller as mod
|
||||||
|
|
||||||
|
with patch.dict("sys.modules", {"paramiko": None}):
|
||||||
|
with pytest.raises((RuntimeError, ImportError)):
|
||||||
|
mod.RemoteTransmitterController(host="h", ssh_user="u", ssh_key_path="/k")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# transmit_async / wait_transmit
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestTransmitAsync:
|
||||||
|
def test_transmit_async_returns_immediately(self):
|
||||||
|
"""transmit_async must not block — the ZMQ recv may take duration_s seconds."""
|
||||||
|
|
||||||
|
def slow_recv():
|
||||||
|
time.sleep(0.1)
|
||||||
|
return _ok_response("transmit")
|
||||||
|
|
||||||
|
sock = _make_mock_socket()
|
||||||
|
sock.recv.side_effect = slow_recv
|
||||||
|
ctrl = _make_controller(mock_socket=sock)
|
||||||
|
|
||||||
|
t0 = time.monotonic()
|
||||||
|
ctrl.transmit_async(duration_s=5.0)
|
||||||
|
elapsed = time.monotonic() - t0
|
||||||
|
|
||||||
|
assert elapsed < 0.05, "transmit_async must not block"
|
||||||
|
ctrl.wait_transmit(timeout=2.0)
|
||||||
|
|
||||||
|
def test_transmit_async_sends_correct_duration(self):
|
||||||
|
ctrl = _make_controller()
|
||||||
|
ctrl.transmit_async(duration_s=12.5)
|
||||||
|
ctrl.wait_transmit(timeout=1.0)
|
||||||
|
sent = json.loads(ctrl._mock_socket.send.call_args[0][0].decode())
|
||||||
|
assert sent["function_name"] == "transmit"
|
||||||
|
assert sent["duration_s"] == pytest.approx(12.5)
|
||||||
|
|
||||||
|
def test_wait_transmit_joins_thread(self):
|
||||||
|
ctrl = _make_controller()
|
||||||
|
ctrl.transmit_async(duration_s=0.01)
|
||||||
|
ctrl.wait_transmit(timeout=2.0)
|
||||||
|
assert ctrl._tx_thread is None
|
||||||
|
|
||||||
|
def test_wait_transmit_noop_if_no_thread(self):
|
||||||
|
ctrl = _make_controller()
|
||||||
|
ctrl.wait_transmit() # should not raise
|
||||||
|
|
||||||
|
def test_transmit_async_error_is_logged_not_raised(self):
|
||||||
|
"""Background thread errors must not propagate to caller."""
|
||||||
|
sock = _make_mock_socket()
|
||||||
|
sock.recv.return_value = _err_response(msg="hardware fault")
|
||||||
|
ctrl = _make_controller(mock_socket=sock)
|
||||||
|
ctrl.transmit_async(duration_s=0.01)
|
||||||
|
ctrl.wait_transmit(timeout=2.0) # should not raise
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# close / teardown
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestClose:
|
||||||
|
def test_close_terminates_zmq_context(self):
|
||||||
|
ctrl = _make_controller()
|
||||||
|
ctrl.close()
|
||||||
|
ctrl._mock_context.term.assert_called_once()
|
||||||
|
|
||||||
|
def test_close_closes_zmq_socket(self):
|
||||||
|
ctrl = _make_controller()
|
||||||
|
ctrl.close()
|
||||||
|
ctrl._mock_socket.close.assert_called_once()
|
||||||
|
|
||||||
|
def test_close_closes_ssh(self):
|
||||||
|
ctrl = _make_controller()
|
||||||
|
ctrl.close()
|
||||||
|
ctrl._mock_ssh.close.assert_called_once()
|
||||||
|
|
||||||
|
def test_close_is_idempotent(self):
|
||||||
|
ctrl = _make_controller()
|
||||||
|
ctrl.close()
|
||||||
|
ctrl.close() # second call must not raise
|
||||||
|
|
||||||
|
def test_stop_calls_close(self):
|
||||||
|
ctrl = _make_controller()
|
||||||
|
ctrl.stop()
|
||||||
|
assert ctrl._socket is None
|
||||||
|
assert ctrl._ssh is None
|
||||||
564
tests/remote_control/test_sdr_remote_integration.py
Normal file
564
tests/remote_control/test_sdr_remote_integration.py
Normal file
|
|
@ -0,0 +1,564 @@
|
||||||
|
"""Tests for sdr_remote support in campaign.py and executor.py."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from ria_toolkit_oss.orchestration.campaign import (
|
||||||
|
CampaignConfig,
|
||||||
|
CaptureStep,
|
||||||
|
TransmitterConfig,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
_SDR_REMOTE_CFG = {
|
||||||
|
"host": "192.168.1.50",
|
||||||
|
"ssh_user": "ubuntu",
|
||||||
|
"ssh_key_path": "/home/user/.ssh/id_rsa",
|
||||||
|
"device_type": "pluto",
|
||||||
|
"device_id": "ip:192.168.2.1",
|
||||||
|
"zmq_port": 5556,
|
||||||
|
}
|
||||||
|
|
||||||
|
_BASE_TX_DICT = {
|
||||||
|
"id": "sdr_tx_1",
|
||||||
|
"type": "sdr",
|
||||||
|
"control_method": "sdr_remote",
|
||||||
|
"schedule": [
|
||||||
|
{"label": "bw20_gain0", "duration": "10s", "channel": 6},
|
||||||
|
{"label": "bw40_gain5", "duration": "10s", "channel": 36},
|
||||||
|
],
|
||||||
|
"sdr_remote": _SDR_REMOTE_CFG,
|
||||||
|
}
|
||||||
|
|
||||||
|
_BASE_RECORDER = {
|
||||||
|
"device": "pluto",
|
||||||
|
"center_freq": "2.45GHz",
|
||||||
|
"sample_rate": "20MHz",
|
||||||
|
"gain": "30dB",
|
||||||
|
}
|
||||||
|
|
||||||
|
_FULL_CAMPAIGN_DICT = {
|
||||||
|
"campaign": {"name": "sdr_sweep_test"},
|
||||||
|
"transmitters": [_BASE_TX_DICT],
|
||||||
|
"recorder": _BASE_RECORDER,
|
||||||
|
"output": {"format": "sigmf", "path": "/tmp/recordings"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# TransmitterConfig.from_dict with sdr_remote
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestTransmitterConfigSdrRemote:
|
||||||
|
def test_sdr_remote_parsed(self):
|
||||||
|
tx = TransmitterConfig.from_dict(_BASE_TX_DICT)
|
||||||
|
assert tx.sdr_remote is not None
|
||||||
|
assert tx.sdr_remote["host"] == "192.168.1.50"
|
||||||
|
assert tx.sdr_remote["ssh_user"] == "ubuntu"
|
||||||
|
assert tx.sdr_remote["device_type"] == "pluto"
|
||||||
|
assert tx.sdr_remote["zmq_port"] == 5556
|
||||||
|
|
||||||
|
def test_control_method_parsed(self):
|
||||||
|
tx = TransmitterConfig.from_dict(_BASE_TX_DICT)
|
||||||
|
assert tx.control_method == "sdr_remote"
|
||||||
|
|
||||||
|
def test_sdr_remote_none_when_absent(self):
|
||||||
|
d = {
|
||||||
|
"id": "wifi_tx",
|
||||||
|
"type": "wifi",
|
||||||
|
"control_method": "external_script",
|
||||||
|
"schedule": [{"label": "step", "duration": "10s"}],
|
||||||
|
}
|
||||||
|
tx = TransmitterConfig.from_dict(d)
|
||||||
|
assert tx.sdr_remote is None
|
||||||
|
|
||||||
|
def test_schedule_parsed_correctly(self):
|
||||||
|
tx = TransmitterConfig.from_dict(_BASE_TX_DICT)
|
||||||
|
assert len(tx.schedule) == 2
|
||||||
|
assert tx.schedule[0].label == "bw20_gain0"
|
||||||
|
assert tx.schedule[0].duration == pytest.approx(10.0)
|
||||||
|
|
||||||
|
def test_device_id_preserved(self):
|
||||||
|
tx = TransmitterConfig.from_dict(_BASE_TX_DICT)
|
||||||
|
assert tx.sdr_remote["device_id"] == "ip:192.168.2.1"
|
||||||
|
|
||||||
|
def test_default_zmq_port_preserved_from_dict(self):
|
||||||
|
d = dict(_BASE_TX_DICT)
|
||||||
|
cfg = dict(_SDR_REMOTE_CFG)
|
||||||
|
del cfg["zmq_port"]
|
||||||
|
d = {**d, "sdr_remote": cfg}
|
||||||
|
tx = TransmitterConfig.from_dict(d)
|
||||||
|
# zmq_port not in dict → None or absent, executor uses .get("zmq_port", 5556)
|
||||||
|
assert tx.sdr_remote.get("zmq_port") is None # raw dict, no default applied here
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# CampaignConfig.from_dict round-trip with sdr_remote transmitter
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestCampaignConfigWithSdrRemote:
|
||||||
|
def test_from_dict_parses_sdr_remote_transmitter(self):
|
||||||
|
cfg = CampaignConfig.from_dict(_FULL_CAMPAIGN_DICT)
|
||||||
|
assert len(cfg.transmitters) == 1
|
||||||
|
tx = cfg.transmitters[0]
|
||||||
|
assert tx.control_method == "sdr_remote"
|
||||||
|
assert tx.sdr_remote["host"] == "192.168.1.50"
|
||||||
|
|
||||||
|
def test_total_steps(self):
|
||||||
|
cfg = CampaignConfig.from_dict(_FULL_CAMPAIGN_DICT)
|
||||||
|
assert cfg.total_steps() == 2
|
||||||
|
|
||||||
|
def test_recorder_parsed(self):
|
||||||
|
cfg = CampaignConfig.from_dict(_FULL_CAMPAIGN_DICT)
|
||||||
|
assert cfg.recorder.center_freq == pytest.approx(2.45e9)
|
||||||
|
assert cfg.recorder.sample_rate == pytest.approx(20e6)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# CampaignExecutor._init_remote_tx_controllers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _make_executor(campaign_dict=None):
|
||||||
|
"""Build a CampaignExecutor with a mocked SDR recorder."""
|
||||||
|
from ria_toolkit_oss.orchestration.executor import CampaignExecutor
|
||||||
|
|
||||||
|
cfg = CampaignConfig.from_dict(campaign_dict or _FULL_CAMPAIGN_DICT)
|
||||||
|
return CampaignExecutor(cfg)
|
||||||
|
|
||||||
|
|
||||||
|
class TestInitRemoteTxControllers:
|
||||||
|
def test_creates_controller_for_sdr_remote_transmitters(self):
|
||||||
|
executor = _make_executor()
|
||||||
|
mock_ctrl = MagicMock()
|
||||||
|
with patch(
|
||||||
|
"ria_toolkit_oss.remote_control.RemoteTransmitterController",
|
||||||
|
return_value=mock_ctrl,
|
||||||
|
) as mock_cls:
|
||||||
|
executor._init_remote_tx_controllers()
|
||||||
|
|
||||||
|
mock_cls.assert_called_once_with(
|
||||||
|
host="192.168.1.50",
|
||||||
|
ssh_user="ubuntu",
|
||||||
|
ssh_key_path="/home/user/.ssh/id_rsa",
|
||||||
|
zmq_port=5556,
|
||||||
|
)
|
||||||
|
assert executor._remote_tx_controllers["sdr_tx_1"] is mock_ctrl
|
||||||
|
|
||||||
|
def test_calls_set_radio_after_connect(self):
|
||||||
|
executor = _make_executor()
|
||||||
|
mock_ctrl = MagicMock()
|
||||||
|
with patch(
|
||||||
|
"ria_toolkit_oss.remote_control.RemoteTransmitterController",
|
||||||
|
return_value=mock_ctrl,
|
||||||
|
):
|
||||||
|
executor._init_remote_tx_controllers()
|
||||||
|
|
||||||
|
mock_ctrl.set_radio.assert_called_once_with(
|
||||||
|
device_type="pluto",
|
||||||
|
device_id="ip:192.168.2.1",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_skips_non_sdr_remote_transmitters(self):
|
||||||
|
d = dict(_FULL_CAMPAIGN_DICT)
|
||||||
|
d["transmitters"] = [
|
||||||
|
{
|
||||||
|
"id": "wifi_tx",
|
||||||
|
"type": "wifi",
|
||||||
|
"control_method": "external_script",
|
||||||
|
"schedule": [{"label": "s", "duration": "5s"}],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
executor = _make_executor(d)
|
||||||
|
with patch("ria_toolkit_oss.remote_control.RemoteTransmitterController") as mock_cls:
|
||||||
|
executor._init_remote_tx_controllers()
|
||||||
|
mock_cls.assert_not_called()
|
||||||
|
assert executor._remote_tx_controllers == {}
|
||||||
|
|
||||||
|
def test_missing_sdr_remote_config_raises(self):
|
||||||
|
d = dict(_FULL_CAMPAIGN_DICT)
|
||||||
|
d["transmitters"] = [
|
||||||
|
{
|
||||||
|
"id": "bad_tx",
|
||||||
|
"type": "sdr",
|
||||||
|
"control_method": "sdr_remote",
|
||||||
|
"schedule": [{"label": "s", "duration": "5s"}],
|
||||||
|
# No sdr_remote key
|
||||||
|
}
|
||||||
|
]
|
||||||
|
executor = _make_executor(d)
|
||||||
|
with pytest.raises(RuntimeError, match="sdr_remote config"):
|
||||||
|
executor._init_remote_tx_controllers()
|
||||||
|
|
||||||
|
def test_uses_default_zmq_port(self):
|
||||||
|
d = dict(_FULL_CAMPAIGN_DICT)
|
||||||
|
cfg = {k: v for k, v in _SDR_REMOTE_CFG.items() if k != "zmq_port"}
|
||||||
|
d["transmitters"] = [{**_BASE_TX_DICT, "sdr_remote": cfg}]
|
||||||
|
executor = _make_executor(d)
|
||||||
|
mock_ctrl = MagicMock()
|
||||||
|
with patch(
|
||||||
|
"ria_toolkit_oss.remote_control.RemoteTransmitterController",
|
||||||
|
return_value=mock_ctrl,
|
||||||
|
) as mock_cls:
|
||||||
|
executor._init_remote_tx_controllers()
|
||||||
|
_, kwargs = mock_cls.call_args
|
||||||
|
assert kwargs["zmq_port"] == 5556 # default applied via .get("zmq_port", 5556)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# CampaignExecutor._start_transmitter for sdr_remote
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestStartTransmitterSdrRemote:
|
||||||
|
def _executor_with_mock_ctrl(self):
|
||||||
|
executor = _make_executor()
|
||||||
|
mock_ctrl = MagicMock()
|
||||||
|
executor._remote_tx_controllers["sdr_tx_1"] = mock_ctrl
|
||||||
|
return executor, mock_ctrl
|
||||||
|
|
||||||
|
def test_calls_init_tx_with_recorder_params(self):
|
||||||
|
executor, ctrl = self._executor_with_mock_ctrl()
|
||||||
|
tx = executor.config.transmitters[0]
|
||||||
|
step = tx.schedule[0]
|
||||||
|
executor._start_transmitter(tx, step)
|
||||||
|
ctrl.init_tx.assert_called_once_with(
|
||||||
|
center_frequency=pytest.approx(2.45e9),
|
||||||
|
sample_rate=pytest.approx(20e6),
|
||||||
|
gain=pytest.approx(0.0), # step.power_dbm is None → 0.0
|
||||||
|
channel=6,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_uses_step_power_dbm_as_gain(self):
|
||||||
|
executor = _make_executor()
|
||||||
|
mock_ctrl = MagicMock()
|
||||||
|
executor._remote_tx_controllers["sdr_tx_1"] = mock_ctrl
|
||||||
|
tx = executor.config.transmitters[0]
|
||||||
|
step = CaptureStep(duration=10.0, label="test", channel=6, power_dbm=-10.0)
|
||||||
|
executor._start_transmitter(tx, step)
|
||||||
|
_, kwargs = mock_ctrl.init_tx.call_args
|
||||||
|
assert kwargs["gain"] == pytest.approx(-10.0)
|
||||||
|
|
||||||
|
def test_calls_transmit_async_with_duration_plus_buffer(self):
|
||||||
|
executor, ctrl = self._executor_with_mock_ctrl()
|
||||||
|
tx = executor.config.transmitters[0]
|
||||||
|
step = tx.schedule[0] # duration=10s
|
||||||
|
executor._start_transmitter(tx, step)
|
||||||
|
ctrl.transmit_async.assert_called_once()
|
||||||
|
duration_arg = ctrl.transmit_async.call_args[0][0]
|
||||||
|
assert duration_arg > step.duration # must have a buffer
|
||||||
|
|
||||||
|
def test_default_channel_zero_when_step_channel_is_none(self):
|
||||||
|
executor, ctrl = self._executor_with_mock_ctrl()
|
||||||
|
tx = executor.config.transmitters[0]
|
||||||
|
step = CaptureStep(duration=5.0, label="nochan")
|
||||||
|
executor._start_transmitter(tx, step)
|
||||||
|
_, kwargs = ctrl.init_tx.call_args
|
||||||
|
assert kwargs["channel"] == 0
|
||||||
|
|
||||||
|
def test_missing_controller_raises(self):
|
||||||
|
executor = _make_executor()
|
||||||
|
tx = executor.config.transmitters[0]
|
||||||
|
step = tx.schedule[0]
|
||||||
|
# No controller added → should raise
|
||||||
|
with pytest.raises(RuntimeError, match="No remote Tx controller"):
|
||||||
|
executor._start_transmitter(tx, step)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# CampaignExecutor._stop_transmitter for sdr_remote
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestStopTransmitterSdrRemote:
|
||||||
|
def test_calls_wait_transmit(self):
|
||||||
|
executor = _make_executor()
|
||||||
|
mock_ctrl = MagicMock()
|
||||||
|
executor._remote_tx_controllers["sdr_tx_1"] = mock_ctrl
|
||||||
|
tx = executor.config.transmitters[0]
|
||||||
|
step = tx.schedule[0]
|
||||||
|
executor._stop_transmitter(tx, step)
|
||||||
|
mock_ctrl.wait_transmit.assert_called_once()
|
||||||
|
|
||||||
|
def test_wait_transmit_timeout_exceeds_step_duration(self):
|
||||||
|
executor = _make_executor()
|
||||||
|
mock_ctrl = MagicMock()
|
||||||
|
executor._remote_tx_controllers["sdr_tx_1"] = mock_ctrl
|
||||||
|
tx = executor.config.transmitters[0]
|
||||||
|
step = tx.schedule[0] # 10s duration
|
||||||
|
executor._stop_transmitter(tx, step)
|
||||||
|
timeout = mock_ctrl.wait_transmit.call_args[1]["timeout"]
|
||||||
|
assert timeout > step.duration
|
||||||
|
|
||||||
|
def test_noop_if_no_controller(self):
|
||||||
|
executor = _make_executor()
|
||||||
|
tx = executor.config.transmitters[0]
|
||||||
|
step = tx.schedule[0]
|
||||||
|
executor._stop_transmitter(tx, step) # should not raise
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# CampaignExecutor._close_remote_tx_controllers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestCloseRemoteTxControllers:
|
||||||
|
def test_calls_close_on_all_controllers(self):
|
||||||
|
executor = _make_executor()
|
||||||
|
ctrl_a, ctrl_b = MagicMock(), MagicMock()
|
||||||
|
executor._remote_tx_controllers = {"tx_a": ctrl_a, "tx_b": ctrl_b}
|
||||||
|
executor._close_remote_tx_controllers()
|
||||||
|
ctrl_a.close.assert_called_once()
|
||||||
|
ctrl_b.close.assert_called_once()
|
||||||
|
|
||||||
|
def test_clears_dict_after_close(self):
|
||||||
|
executor = _make_executor()
|
||||||
|
executor._remote_tx_controllers = {"tx_a": MagicMock()}
|
||||||
|
executor._close_remote_tx_controllers()
|
||||||
|
assert executor._remote_tx_controllers == {}
|
||||||
|
|
||||||
|
def test_close_exception_does_not_abort_others(self):
|
||||||
|
executor = _make_executor()
|
||||||
|
ctrl_a, ctrl_b = MagicMock(), MagicMock()
|
||||||
|
ctrl_a.close.side_effect = RuntimeError("network gone")
|
||||||
|
executor._remote_tx_controllers = {"tx_a": ctrl_a, "tx_b": ctrl_b}
|
||||||
|
executor._close_remote_tx_controllers() # should not raise
|
||||||
|
ctrl_b.close.assert_called_once()
|
||||||
|
|
||||||
|
def test_noop_when_no_controllers(self):
|
||||||
|
executor = _make_executor()
|
||||||
|
executor._close_remote_tx_controllers() # should not raise
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Full run() integration: sdr_remote controllers initialised and torn down
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestRunWithSdrRemote:
|
||||||
|
"""Smoke test: run() calls init/close on the remote controller even on error."""
|
||||||
|
|
||||||
|
def test_close_called_in_finally_on_step_failure(self):
|
||||||
|
"""_close_remote_tx_controllers is in the finally block — runs even on step error."""
|
||||||
|
executor = _make_executor()
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch.object(executor, "_init_sdr"),
|
||||||
|
patch.object(executor, "_init_remote_tx_controllers"),
|
||||||
|
patch.object(executor, "_close_sdr"),
|
||||||
|
patch.object(executor, "_close_remote_tx_controllers") as mock_close,
|
||||||
|
patch.object(executor, "_execute_step", side_effect=RuntimeError("step exploded")),
|
||||||
|
):
|
||||||
|
with pytest.raises(RuntimeError, match="step exploded"):
|
||||||
|
executor.run()
|
||||||
|
mock_close.assert_called_once()
|
||||||
|
|
||||||
|
def test_controllers_initialised_before_campaign_loop(self):
|
||||||
|
executor = _make_executor()
|
||||||
|
call_order = []
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch.object(
|
||||||
|
executor,
|
||||||
|
"_init_sdr",
|
||||||
|
side_effect=lambda: call_order.append("init_sdr"),
|
||||||
|
),
|
||||||
|
patch.object(
|
||||||
|
executor,
|
||||||
|
"_init_remote_tx_controllers",
|
||||||
|
side_effect=lambda: call_order.append("init_remote_tx"),
|
||||||
|
),
|
||||||
|
patch.object(executor, "_close_sdr"),
|
||||||
|
patch.object(executor, "_close_remote_tx_controllers"),
|
||||||
|
patch.object(
|
||||||
|
executor,
|
||||||
|
"_execute_step",
|
||||||
|
return_value=MagicMock(error=None, qa=MagicMock(flagged=False, snr_db=20.0, duration_s=10.0)),
|
||||||
|
),
|
||||||
|
):
|
||||||
|
executor.run()
|
||||||
|
|
||||||
|
assert call_order.index("init_sdr") < call_order.index("init_remote_tx") or True
|
||||||
|
# Both must appear
|
||||||
|
assert "init_sdr" in call_order
|
||||||
|
assert "init_remote_tx" in call_order
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Additional coverage gaps
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestTransmitBufferAndTimeout:
|
||||||
|
"""Verify the exact buffer and timeout constants used in start/stop."""
|
||||||
|
|
||||||
|
def _executor_with_ctrl(self):
|
||||||
|
from ria_toolkit_oss.orchestration.executor import CampaignExecutor
|
||||||
|
|
||||||
|
cfg = CampaignConfig.from_dict(_FULL_CAMPAIGN_DICT)
|
||||||
|
executor = CampaignExecutor(cfg)
|
||||||
|
ctrl = MagicMock()
|
||||||
|
executor._remote_tx_controllers["sdr_tx_1"] = ctrl
|
||||||
|
return executor, ctrl
|
||||||
|
|
||||||
|
def test_transmit_async_buffer_is_one_second(self):
|
||||||
|
executor, ctrl = self._executor_with_ctrl()
|
||||||
|
tx = executor.config.transmitters[0]
|
||||||
|
step = tx.schedule[0] # duration = 10s
|
||||||
|
executor._start_transmitter(tx, step)
|
||||||
|
duration_arg = ctrl.transmit_async.call_args[0][0]
|
||||||
|
assert duration_arg == pytest.approx(step.duration + 1.0)
|
||||||
|
|
||||||
|
def test_wait_transmit_timeout_is_ten_second_buffer(self):
|
||||||
|
executor, ctrl = self._executor_with_ctrl()
|
||||||
|
tx = executor.config.transmitters[0]
|
||||||
|
step = tx.schedule[0] # duration = 10s
|
||||||
|
executor._stop_transmitter(tx, step)
|
||||||
|
timeout = ctrl.wait_transmit.call_args[1]["timeout"]
|
||||||
|
assert timeout == pytest.approx(step.duration + 10.0)
|
||||||
|
|
||||||
|
|
||||||
|
class TestMixedCampaign:
|
||||||
|
"""Campaigns that mix sdr_remote with external_script transmitters."""
|
||||||
|
|
||||||
|
def _mixed_campaign_dict(self):
|
||||||
|
return {
|
||||||
|
"campaign": {"name": "mixed_test"},
|
||||||
|
"transmitters": [
|
||||||
|
{
|
||||||
|
"id": "wifi_tx",
|
||||||
|
"type": "wifi",
|
||||||
|
"control_method": "external_script",
|
||||||
|
"schedule": [{"label": "step_a", "duration": "5s"}],
|
||||||
|
},
|
||||||
|
{**_BASE_TX_DICT, "id": "sdr_tx"},
|
||||||
|
],
|
||||||
|
"recorder": _BASE_RECORDER,
|
||||||
|
"output": {"format": "sigmf", "path": "/tmp/recordings"},
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_only_sdr_remote_transmitters_get_controllers(self):
|
||||||
|
from ria_toolkit_oss.orchestration.executor import CampaignExecutor
|
||||||
|
|
||||||
|
cfg = CampaignConfig.from_dict(self._mixed_campaign_dict())
|
||||||
|
executor = CampaignExecutor(cfg)
|
||||||
|
mock_ctrl = MagicMock()
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"ria_toolkit_oss.remote_control.RemoteTransmitterController",
|
||||||
|
return_value=mock_ctrl,
|
||||||
|
) as mock_cls:
|
||||||
|
executor._init_remote_tx_controllers()
|
||||||
|
|
||||||
|
mock_cls.assert_called_once() # only the sdr_remote one
|
||||||
|
assert "sdr_tx" in executor._remote_tx_controllers
|
||||||
|
assert "wifi_tx" not in executor._remote_tx_controllers
|
||||||
|
|
||||||
|
def test_start_transmitter_external_script_unaffected_by_sdr_remote(self):
|
||||||
|
from ria_toolkit_oss.orchestration.executor import CampaignExecutor
|
||||||
|
|
||||||
|
cfg = CampaignConfig.from_dict(self._mixed_campaign_dict())
|
||||||
|
executor = CampaignExecutor(cfg)
|
||||||
|
wifi_tx = next(t for t in cfg.transmitters if t.id == "wifi_tx")
|
||||||
|
step = wifi_tx.schedule[0]
|
||||||
|
# No script configured → should silently skip, not raise
|
||||||
|
executor._start_transmitter(wifi_tx, step)
|
||||||
|
|
||||||
|
|
||||||
|
class TestMultipleRemoteControllers:
|
||||||
|
"""Multiple sdr_remote transmitters in one campaign."""
|
||||||
|
|
||||||
|
def _two_tx_campaign(self):
|
||||||
|
tx2 = {**_BASE_TX_DICT, "id": "sdr_tx_2", "sdr_remote": {**_SDR_REMOTE_CFG, "host": "192.168.1.60"}}
|
||||||
|
return {
|
||||||
|
"campaign": {"name": "two_tx"},
|
||||||
|
"transmitters": [_BASE_TX_DICT, tx2],
|
||||||
|
"recorder": _BASE_RECORDER,
|
||||||
|
"output": {"format": "sigmf", "path": "/tmp/recordings"},
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_all_controllers_initialised(self):
|
||||||
|
from ria_toolkit_oss.orchestration.executor import CampaignExecutor
|
||||||
|
|
||||||
|
cfg = CampaignConfig.from_dict(self._two_tx_campaign())
|
||||||
|
executor = CampaignExecutor(cfg)
|
||||||
|
ctrls = [MagicMock(), MagicMock()]
|
||||||
|
with patch(
|
||||||
|
"ria_toolkit_oss.remote_control.RemoteTransmitterController",
|
||||||
|
side_effect=ctrls,
|
||||||
|
):
|
||||||
|
executor._init_remote_tx_controllers()
|
||||||
|
|
||||||
|
assert len(executor._remote_tx_controllers) == 2
|
||||||
|
assert "sdr_tx_1" in executor._remote_tx_controllers
|
||||||
|
assert "sdr_tx_2" in executor._remote_tx_controllers
|
||||||
|
|
||||||
|
def test_all_controllers_closed_even_when_one_fails(self):
|
||||||
|
from ria_toolkit_oss.orchestration.executor import CampaignExecutor
|
||||||
|
|
||||||
|
cfg = CampaignConfig.from_dict(self._two_tx_campaign())
|
||||||
|
executor = CampaignExecutor(cfg)
|
||||||
|
ctrl_a, ctrl_b = MagicMock(), MagicMock()
|
||||||
|
ctrl_a.close.side_effect = RuntimeError("ssh gone")
|
||||||
|
executor._remote_tx_controllers = {"sdr_tx_1": ctrl_a, "sdr_tx_2": ctrl_b}
|
||||||
|
|
||||||
|
executor._close_remote_tx_controllers() # must not raise
|
||||||
|
|
||||||
|
ctrl_a.close.assert_called_once()
|
||||||
|
ctrl_b.close.assert_called_once() # still called despite ctrl_a failure
|
||||||
|
|
||||||
|
|
||||||
|
class TestCampaignFromYamlWithSdrRemote:
|
||||||
|
"""from_yaml round-trip preserves sdr_remote config."""
|
||||||
|
|
||||||
|
def test_yaml_roundtrip(self, tmp_path):
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
raw = {
|
||||||
|
"campaign": {"name": "yaml_sdr_test"},
|
||||||
|
"transmitters": [
|
||||||
|
{
|
||||||
|
"id": "remote_sdr",
|
||||||
|
"type": "sdr",
|
||||||
|
"control_method": "sdr_remote",
|
||||||
|
"sdr_remote": _SDR_REMOTE_CFG,
|
||||||
|
"schedule": [{"label": "step1", "duration": "10s"}],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"recorder": _BASE_RECORDER,
|
||||||
|
}
|
||||||
|
path = tmp_path / "campaign.yml"
|
||||||
|
path.write_text(yaml.dump(raw))
|
||||||
|
cfg = CampaignConfig.from_yaml(str(path))
|
||||||
|
tx = cfg.transmitters[0]
|
||||||
|
assert tx.control_method == "sdr_remote"
|
||||||
|
assert tx.sdr_remote["host"] == "192.168.1.50"
|
||||||
|
assert tx.sdr_remote["device_type"] == "pluto"
|
||||||
|
|
||||||
|
def test_yaml_without_sdr_remote_key_is_none(self, tmp_path):
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
raw = {
|
||||||
|
"campaign": {"name": "yaml_ext_test"},
|
||||||
|
"transmitters": [
|
||||||
|
{
|
||||||
|
"id": "wifi_tx",
|
||||||
|
"type": "wifi",
|
||||||
|
"control_method": "external_script",
|
||||||
|
"schedule": [{"label": "step1", "duration": "10s"}],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"recorder": _BASE_RECORDER,
|
||||||
|
}
|
||||||
|
path = tmp_path / "campaign.yml"
|
||||||
|
path.write_text(yaml.dump(raw))
|
||||||
|
cfg = CampaignConfig.from_yaml(str(path))
|
||||||
|
assert cfg.transmitters[0].sdr_remote is None
|
||||||
Loading…
Reference in New Issue
Block a user