diff --git a/.gitignore b/.gitignore index b66b92e..b2ac7f1 100644 --- a/.gitignore +++ b/.gitignore @@ -52,6 +52,7 @@ tests/sdr/ # Sphinx documentation docs/build/ +docs/_build/ # Jupyter Notebook .ipynb_checkpoints diff --git a/docs/_build/html/_sources/intro/getting_started.rst.txt b/docs/_build/html/_sources/intro/getting_started.rst.txt new file mode 100644 index 0000000..c2386ee --- /dev/null +++ b/docs/_build/html/_sources/intro/getting_started.rst.txt @@ -0,0 +1,1083 @@ +############### +Getting Started +############### + +This is a practical reference for the ``ria`` CLI from ``ria-toolkit-oss``. + +**Scope of this guide:** + +* Installation and setup +* End-to-end CLI workflows +* Full command reference for CLI features +* Brief scripting section + +**Official resources:** + +* `Project README `_ +* `Documentation `_ +* `PyPI package `_ +* `RIA Hub Conda package `_ + +.. contents:: Contents + :local: + :depth: 2 + :backlinks: none + + +1) Installation and Setup +========================== + +1.1 Installation with Conda +---------------------------- + +RIA Toolkit OSS is available as a Conda package on RIA Hub. This is typically the easiest +path when using SDR tooling that depends on native/system libraries. + +.. code-block:: bash + + conda update --force conda + conda config --add channels https://riahub.ai/api/packages/qoherent/conda + conda activate base + conda install ria-toolkit-oss + +Verify: + +.. code-block:: bash + + conda list | grep ria-toolkit-oss + + +1.2 Installation with pip +-------------------------- + +Use pip unless you specifically need to edit toolkit source. + +.. code-block:: bash + + python3 -m venv .venv + source .venv/bin/activate + pip install --upgrade pip + pip install ria-toolkit-oss + +Verify CLI entrypoint: + +.. code-block:: bash + + ria --help + +``pyproject.toml`` defines two script entry points: + +* ``ria`` +* ``ria-tools`` + +Both point to the same CLI module (``ria_toolkit_oss_cli.cli:cli``). + + +1.3 Optional install from source +---------------------------------- + +Use this for local development or testing unreleased changes. + +.. code-block:: bash + + git clone https://riahub.ai/qoherent/ria-toolkit-oss.git + cd ria-toolkit-oss + python3 -m venv .venv + source .venv/bin/activate + pip install -e . + + +1.4 SDR driver prerequisites +----------------------------- + +Toolkit package install does not install all system SDR drivers. Install vendor/runtime +dependencies for the hardware you use. + +Examples (depends on device and OS): + +* USRP: UHD drivers +* Pluto: libiio / IIO utilities +* BladeRF: libbladeRF +* HackRF: libhackrf +* RTL-SDR: librtlsdr + +See repo docs under ``docs/source/sdr_guides/*`` and your OS package instructions. + + +2) CLI Structure +================= + +Top-level CLI follows this model: + +.. code-block:: bash + + ria [GLOBAL_OPTS] [ARGS] [OPTIONS] + +**Global:** + +* ``-v, --verbose`` (defined on root click group) + +**Top-level commands:** + +* ``discover`` +* ``init`` +* ``capture`` +* ``view`` +* ``annotate`` (group) +* ``convert`` +* ``split`` +* ``combine`` +* ``generate`` (group) +* ``transform`` (group) +* ``transmit`` +* ``synth`` (alias of ``generate`` in command bindings) + + +3) Quick End-to-End Workflow +============================= + +3.1 Discover radios +-------------------- + +Run this first in any new environment to verify drivers and detect hardware before +attempting RX/TX commands. + +.. code-block:: bash + + ria discover + ria discover -v + ria discover --json-output + + +3.2 Initialize local metadata defaults +--------------------------------------- + +Set reusable metadata once so generated/captured files automatically include consistent +provenance fields. + +.. code-block:: bash + + ria init + # or non-interactive + ria init --author "Jane Doe" --project "rf-campaign-1" --location "Lab-A" + # show config + ria init --show + + +3.3 Capture IQ +--------------- + +Capture baseband data from a connected SDR into a reusable file format. + +.. code-block:: bash + + ria capture -d pluto -f 2.44G -s 2e6 -n 500000 -o capture.sigmf-data + + +3.4 Visualize and inspect +-------------------------- + +Render quick diagnostic plots to validate signal presence, quality, and rough structure. + +.. code-block:: bash + + ria view capture.sigmf-data --type simple + ria view capture.sigmf-data --type full --show --no-save + + +3.5 Auto-annotate and inspect annotations +------------------------------------------ + +Create initial labels automatically, then inspect annotation objects before downstream use. + +.. code-block:: bash + + ria annotate energy capture.sigmf-data --label signal --threshold 1.2 + ria annotate list capture.sigmf-data --verbose + + +3.6 Convert and split +---------------------- + +Normalize file format and split large captures into manageable chunks for processing or +training. + +.. code-block:: bash + + ria convert capture.sigmf-data capture.npy + ria split capture.sigmf-data --split-every 100000 --output-dir chunks + + +3.7 Apply transforms +--------------------- + +Augment or impair recordings to produce controlled variants. + +.. code-block:: bash + + ria transform augment channel_swap capture.npy + ria transform impair add_awgn_to_signal capture.npy --params snr=10 + + +3.8 Transmit (TX-capable radios only) +-------------------------------------- + +Replay recorded or synthesized IQ through a transmit-capable SDR. + +.. code-block:: bash + + ria transmit -d hackrf -f 2.44G -s 2e6 --input capture.sigmf-data + # or generated waveform + ria transmit -d hackrf --generate lfm --continuous + + +4) Command Reference +===================== + +4.1 ``discover`` +----------------- + +**Purpose:** + +* Probe available SDR drivers and enumerate attached hardware. +* Confirm whether runtime libraries/drivers are installed and discoverable before + capture/transmit. + +**Usage:** + +.. code-block:: bash + + ria discover [--verbose] [--json-output] + +**Options:** + +* ``-v, --verbose``: include per-driver probe details and import/init failures. +* ``--json-output``: emit JSON (useful for automation and inventory scripts). + +**Behavior notes:** + +* ``discover`` checks multiple backends (USB and network paths, depending on driver support). +* A device not appearing here usually means one of: missing system driver, permission issue, + USB/network connectivity issue. +* Use ``--verbose`` first when troubleshooting; it surfaces driver-level failures that are + hidden in default output. + + +4.2 ``init`` +------------- + +**Purpose:** + +* Create/manage user config file (defaults to ``~/.ria/config.yaml``, or + ``$XDG_CONFIG_HOME/ria/config.yaml``). + +**Usage:** + +.. code-block:: bash + + ria init [options] + +**Key options:** + +*Metadata defaults:* +``--author``, ``--organization``, ``--project``, ``--location``, ``--testbed``, +``--license``, ``--hw``, ``--dataset`` + +*Actions:* +``--show``, ``--reset`` + +*Control:* +``--config-path``, ``--interactive`` / ``--no-interactive``, ``-y`` / ``--yes`` + +**What each option category does:** + +* Metadata defaults (``--author``, ``--project``, etc.): stored once and reused for later + recordings so files have consistent provenance. +* SigMF-focused fields (``--license``, ``--hw``, ``--dataset``): populate metadata commonly + expected in shared datasets. +* ``--show``: read-only inspect of the current resolved config. +* ``--reset``: remove config and start clean. +* ``--config-path``: use a non-default config location (useful for isolated environments or + CI). +* ``--interactive`` / ``--no-interactive``: force prompts on or off regardless of terminal + auto-detection. +* ``--yes``: suppress confirmation prompts for scripted runs. + +.. note:: + Current command output includes a note that some config integration is still being + finalized. Config values are already consumed by multiple commands (capture, convert, + generate metadata, and YAML config loading paths). + + +4.3 ``capture`` +---------------- + +**Purpose:** + +* Record IQ samples from a supported SDR and save to ``sigmf``, ``npy``, ``wav``, or + ``blue``. + +**Usage:** + +.. code-block:: bash + + ria capture [options] + +Device selection (``--device``) is optional if only one device is detected. Exactly one of +``--num-samples`` or ``--duration`` is required. + +**Core options:** + +*Device/connection:* + +* ``-d, --device {pluto,hackrf,bladerf,usrp,rtlsdr,thinkrf}`` +* ``-i, --ident`` +* ``-c, --config `` + +*RF/capture:* + +* ``-s, --sample-rate`` +* ``-f, --center-frequency`` (supports values like ``915e6``, ``2.4G``) +* ``-g, --gain`` +* ``-b, --bandwidth`` +* ``-n, --num-samples`` +* ``-t, --duration`` + +*Output:* + +* ``-o, --output`` +* ``--output-dir`` +* ``--format {npy,sigmf,wav,blue}`` +* ``--save-image`` + +*Metadata/logging:* + +* ``-m, --metadata KEY=VALUE`` (repeatable) +* ``-v, --verbose``, ``-q, --quiet`` + +**How options work in practice:** + +* ``--device`` + ``--ident``: select both device class and target instance; ``--ident`` + takes serial/IP style selectors. +* ``--config``: load a YAML option set, then override specific fields on the CLI as needed. +* ``--num-samples`` vs ``--duration``: use exact sample count for deterministic datasets, + or time-based capture for quick acquisition. +* ``--format``: ``sigmf`` is best for metadata/annotation workflows. +* ``--save-image``: writes a quick visual summary alongside capture output. +* ``--metadata KEY=VALUE``: injects run-specific metadata (campaign ID, antenna, scenario + tag, etc.). + +**Output behavior:** + +* If ``--output`` is omitted, a timestamped filename is generated automatically. +* If ``--output-dir`` is omitted, captures default to ``recordings/``. +* Format is inferred from the ``--output`` extension when no explicit ``--format`` is given. + +**Examples:** + +.. code-block:: bash + + ria capture -d hackrf -s 2e6 -f 2.44G -n 1000000 -o rf.sigmf-data + ria capture -d pluto -f 915e6 -t 2 --format npy --output-dir recordings + ria capture -c capture_config.yaml + + +4.4 ``view`` +------------- + +**Purpose:** + +* Generate visualizations from IQ files. +* Quickly validate signal quality, occupancy, and annotation coverage without writing custom + plotting code. + +**Usage:** + +.. code-block:: bash + + ria view [options] + +```` accepts SigMF, NPY, WAV, and Blue files. + +**Mode** (``--type``): + +* ``simple``: fast-look plots for sanity checks and quick iteration. +* ``full``: multi-panel diagnostic figure (IQ, time, frequency, metadata views). +* ``annotations`` / ``annotation``: render annotation overlays. +* ``channels``: channelized/segmented visualization. +* ``annotate``: convenience path used in some annotation workflows. + +**Output/display options:** + +* ``--output``, ``--format {png,pdf,svg,jpg}`` +* ``--show``: open an interactive window (requires a GUI display environment). +* ``--no-save``: suppress file output; only meaningful with ``--show``. +* ``--overwrite`` + +**Style options:** + +* ``--dpi``, ``--figsize WxH``, ``--title`` +* ``--light``: switch to a light theme (useful for reports/slides). + +**Loading options:** + +* ``--legacy``: force legacy NPY loading path for older datasets. +* ``--config`` + +**Mode-specific options:** + +*simple:* ``--fast``, ``--compact``, ``--horizontal``, ``--constellation``, ``--labels``, +``--slice start:end[:step]`` + +*full:* ``--plot-length``, ``--no-spectrogram``, ``--no-iq``, ``--no-frequency``, +``--no-constellation``, ``--no-metadata``, ``--no-logo``, ``--spines`` + +*annotations/channels:* ``--channel`` + +**Examples:** + +.. code-block:: bash + + ria view capture.sigmf-data --type simple + ria view capture.npy --type full --title "Test Capture" --format pdf + ria view capture.npy --show --no-save + ria view old.npy --legacy --type simple + + +4.5 ``annotate`` group +----------------------- + +**Purpose:** + +* Manual annotation management and auto-detection/separation. +* Build or refine label metadata directly in recordings for downstream training, QA, and + filtering. + +**Command shape:** + +.. code-block:: bash + + ria annotate ... + +**Subcommands:** ``list``, ``add``, ``remove``, ``clear``, ``energy``, ``cusum``, +``threshold``, ``separate`` + +**General behavior:** + +* SigMF is the preferred format for durable annotation metadata. +* For non-SigMF input, many operations write a new output artifact unless overwrite behavior + is explicitly requested. +* ``--type {standalone,parallel,intersection}`` controls annotation relation semantics. + +``ria annotate list`` +~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: bash + + ria annotate list [--verbose] + +Prints all annotations for a recording in index order. ``--verbose`` includes additional +detail per record. + +``ria annotate add`` +~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: bash + + ria annotate add --start --count --label [options] + +Adds one explicit annotation with sample-domain boundaries. + +* ``--start``: first sample index of the annotated region. +* ``--count``: number of samples in the region. +* ``--freq-lower``, ``--freq-upper``: optional spectral bounds in Hz. +* ``--comment``, ``--type``, ``-o`` / ``--output``, ``--overwrite``, ``--quiet`` + +``ria annotate remove`` +~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: bash + + ria annotate remove [--output ...] [--overwrite] [--quiet] + +Removes exactly one annotation by list index. Run ``annotate list`` first to confirm the +index. + +``ria annotate clear`` +~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: bash + + ria annotate clear [--force] [--overwrite] [--quiet] + +Removes all annotations from the recording. ``--force`` bypasses the confirmation prompt. + +``ria annotate energy`` +~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: bash + + ria annotate energy [options] + +Detects energetic regions above the estimated noise floor and writes them as annotations. + +* ``--label`` +* ``--threshold``: noise-floor multiplier; higher values reduce false positives but can miss + weak signals. +* ``--segments``: number of segments used to estimate baseline noise. +* ``--window-size``: smoothing size; larger windows stabilize detections at the cost of + sharp transition precision. +* ``--min-distance``: minimum sample spacing between detections, preventing dense duplicate + regions. +* ``--freq-method {nbw,obw,full-detected,full-bandwidth}``: how frequency bounds are assigned + to annotations. +* ``--nfft``, ``--obw-power`` +* ``--type``, ``-o`` / ``--output``, ``--overwrite``, ``--quiet`` + +``ria annotate cusum`` +~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: bash + + ria annotate cusum [options] + +Uses change-point detection (CUSUM-style logic) to find regime changes and annotate +contiguous segments. + +* ``--label`` +* ``--min-duration`` (ms): prevents tiny over-segmented labels. +* ``--window-size`` +* ``--tolerance``: merges nearby boundaries when set above default. +* ``--type``, ``-o`` / ``--output``, ``--overwrite``, ``--quiet`` + +``ria annotate threshold`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: bash + + ria annotate threshold --threshold <0.0..1.0> [options] + +Uses normalized magnitude thresholding to derive annotation spans. Use where a fixed +amplitude threshold is sufficient. + +* ``--label``, ``--window-size``, ``--type``, ``-o`` / ``--output``, ``--overwrite``, + ``--quiet`` + +``ria annotate separate`` +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: bash + + ria annotate separate [options] + +Decomposes selected annotations into narrower spectral components and emits refined +annotations. + +* ``--indices "0,1,2"``: limit operation to specific annotations; omit to process all. +* ``--nfft``: larger FFT improves frequency resolution but increases compute time. +* ``--noise-threshold-db``: stabilizes detection across heterogeneous captures. +* ``--min-component-bw``: rejects narrow fragments likely to be noise artifacts. +* ``-o`` / ``--output``, ``--overwrite``, ``--quiet``, ``--verbose`` + +**Examples:** + +.. code-block:: bash + + ria annotate list capture.sigmf-data --verbose + ria annotate add capture.sigmf-data --start 10000 --count 5000 --label burst + ria annotate energy capture.sigmf-data --label signal --threshold 1.3 + ria annotate cusum capture.sigmf-data --min-duration 5 + ria annotate separate capture.sigmf-data --indices 0,1 --verbose + + +4.6 ``convert`` +---------------- + +**Purpose:** + +* Convert between ``sigmf``, ``npy``, ``wav``, and ``blue``. +* Normalize datasets into the format required by downstream tooling or collaboration targets. + +**Usage:** + +.. code-block:: bash + + ria convert [output] [options] + +If ``output`` is omitted, ``--format`` must be provided. If both are given, format is +inferred from the output file extension. + +**Options:** + +* ``--format {npy,sigmf,wav,blue}`` +* ``--output-dir`` +* ``--legacy``: use older NPY loader behavior for historical recordings. +* ``--wav-sample-rate``: target sample rate for WAV export. +* ``--wav-bits {16,32}``: output PCM depth; higher preserves more dynamic range. +* ``--blue-format {CI,CF,CD}``: Bluefile complex sample representation. +* ``--metadata KEY=VALUE`` (repeatable): add or override metadata during conversion; + especially useful when exporting to SigMF. +* ``--overwrite``, ``-v`` / ``--verbose``, ``-q`` / ``--quiet`` + +**Examples:** + +.. code-block:: bash + + ria convert recording.sigmf-data output.npy + ria convert recording.npy --format sigmf + ria convert highrate.npy audio.wav --wav-sample-rate 48000 + ria convert old.npy --format sigmf --legacy --overwrite + + +4.7 ``split`` +-------------- + +**Purpose:** + +* Split, trim, or extract recordings. +* Create manageable dataset shards or extract windows of interest without custom scripts. + +**Usage:** + +.. code-block:: bash + + ria split [operation] [options] + +Choose exactly one operation per invocation: + +* ``--split-at ``: binary split at a specific sample index. +* ``--split-every ``: fixed-size chunking for ML pipelines. +* ``--split-duration ``: time-based chunking. +* ``--trim`` (with ``--start`` + ``--length`` or ``--end``): extract one sub-window. +* ``--extract-annotations``: write each annotated region as a standalone file. + +**Trim controls:** ``--start``, ``--length``, ``--end`` + +**Annotation extraction filters:** ``--annotation-label``, ``--annotation-index`` + +**Output controls:** +``--output-dir``, ``--output-prefix``, ``--output-format {npy,sigmf,wav,blue}``, +``--overwrite``, ``--legacy``, ``-v`` / ``--verbose``, ``-q`` / ``--quiet`` + +**Examples:** + +.. code-block:: bash + + ria split recording.sigmf-data --split-at 500000 --output-dir out + ria split recording.sigmf-data --split-every 100000 --output-dir chunks + ria split recording.sigmf-data --split-duration 1.0 --output-dir chunks + ria split recording.npy --trim --start 1000 --length 5000 --output-dir trimmed + ria split annotated.sigmf-data --extract-annotations --annotation-label payload + + +4.8 ``combine`` +---------------- + +**Purpose:** + +* Merge multiple recordings by concatenation or sample-wise addition. +* Assemble multi-part captures or synthesize mixtures for testing and model training. + +**Usage:** + +.. code-block:: bash + + ria combine [input3 ...] [options] + +**Options:** + +* ``--mode {concat,add}`` +* ``--align-mode {error,truncate,pad,pad-start,pad-center,pad-end,repeat,repeat-spaced}`` +* ``--pad-start-sample``, ``--repeat-spacing`` +* ``--normalize``: rescale combined output to avoid clipping/saturation after addition. +* ``--output-format {sigmf,npy,wav,blue}`` +* ``--overwrite``, ``--metadata KEY=VALUE`` (repeatable) +* ``--legacy``, ``--verbose``, ``--quiet`` + +**Mode semantics:** + +* ``concat``: append inputs sequentially in time. +* ``add``: sample-wise summation — all inputs must be aligned to the same length. + +**Alignment options for** ``--mode add``: + +* ``error``: fail if lengths differ. +* ``truncate``: cut all to shortest length. +* ``pad``, ``pad-start``, ``pad-center``, ``pad-end``: zero-pad shorter streams. +* ``repeat``: tile shorter streams to match longest. +* ``repeat-spaced``: repeated placement with spacing via ``--repeat-spacing``. + +**Examples:** + +.. code-block:: bash + + ria combine a.npy b.npy c.npy merged.npy + ria combine signal.npy noise.npy noisy.npy --mode add + ria combine long.npy short.npy out.npy --mode add --align-mode pad-center + ria combine signal.npy pattern.npy out.npy --mode add --align-mode repeat-spaced --repeat-spacing 10000 + + +4.9 ``generate`` group (and ``synth`` alias) +--------------------------------------------- + +**Purpose:** + +* Generate synthetic IQ signals and save in ``npy``, ``sigmf``, ``wav``, or ``blue``. +* Create known-reference waveforms and synthetic datasets for validation, demos, and ML + data generation. + +``ria synth ...`` is an alias for ``ria generate ...``. + +**Shape:** + +.. code-block:: bash + + ria generate [subcommand options] [common options] + +**Available subcommands:** +``tone``, ``noise``, ``chirp``, ``square``, ``sawtooth``, ``qam``, ``apsk``, ``pam``, +``fsk``, ``ook``, ``oqpsk``, ``gmsk``, ``psk`` + +**Common options shared across all generators:** + +* ``-s, --sample-rate`` (required) +* ``-n, --num-samples`` or ``-t, --duration`` +* ``--frequency-shift``, ``-fc`` / ``--center-frequency`` +* ``--add-noise``, ``--noise-power``, ``--path-gain`` +* ``-o, --output`` (required), ``-F`` / ``--format {npy,sigmf,wav,blue}`` +* ``--multipath-paths``, ``--multipath-max-delay`` +* ``--iq-amp-imbalance``, ``--iq-phase-imbalance``, ``--iq-dc-offset`` +* ``--config `` +* ``-w`` / ``--overwrite``, ``-m`` / ``--metadata KEY=VALUE`` (repeatable) +* ``-v`` / ``--verbose``, ``-q`` / ``--quiet`` + +``--frequency-shift`` and ``--center-frequency`` let you separate the baseband shape from +RF metadata context. ``--add-noise`` and ``--noise-power`` apply post-generation noise. +Multipath and IQ imbalance flags apply impairment-style post-processing during generation. + +``tone`` +~~~~~~~~~ + +Options: ``--frequency``, ``--amplitude``, ``--phase`` + +Clean sinusoidal calibration/reference source. + +``noise`` +~~~~~~~~~~ + +Options: ``--noise-type {gaussian,uniform}``, ``--power`` + +Baseline noise floor data or controlled additive-noise synthesis. + +``chirp`` +~~~~~~~~~~ + +Options: ``--bandwidth`` (required), ``--period`` (required), ``--type {up,down,up_down}`` + +Sweep-based radar/sonar-style signals and bandwidth occupancy tests. + +``square`` +~~~~~~~~~~~ + +Options: ``--frequency``, ``--amplitude``, ``--duty-cycle``, ``--phase`` + +``sawtooth`` +~~~~~~~~~~~~~ + +Options: ``--frequency``, ``--amplitude``, ``--phase`` + +Digital modulation families: ``qam``, ``apsk``, ``pam``, ``psk`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* ``--symbols``, ``--order`` +* ``--symbol-rate`` +* ``--filter {rrc,rc,gaussian,none}``, ``--filter-span``, ``--filter-beta`` +* ``--message-source {random,file,string}``, ``--message-content`` + +Use ``--message-source random`` for synthetic datasets, ``file`` for deterministic replay, +or ``string`` for small human-readable payload testing. Pulse-shaping filter options +(``--filter``, ``--filter-span``, ``--filter-beta``) control spectral occupancy and ISI. + +``fsk`` +~~~~~~~~ + +Options: ``--symbols``, ``--order``, ``--symbol-rate``, ``--freq-spacing``, +``--modulation-index``, ``--message-source``, ``--message-content`` + +``--freq-spacing`` and ``--modulation-index`` drive tone separation and spectral profile. + +``ook``, ``oqpsk``, ``gmsk`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Options: ``--symbol-rate`` (required), ``--message-source {random,file,string}``, +``--message-content``; ``gmsk`` also accepts ``--bt`` + +``gmsk --bt`` sets the Gaussian filter bandwidth-time product (spectral compactness vs +symbol transition sharpness). + +**Examples:** + +.. code-block:: bash + + ria generate tone -s 2e6 -n 500000 --frequency 50e3 -o tone.sigmf-data + ria generate noise -s 2e6 -n 500000 --noise-type gaussian --power 0.05 -o noise.npy + ria generate chirp -s 5e6 -t 0.5 --bandwidth 2e6 --period 0.01 --type up -o chirp.sigmf-data + ria generate qam -s 2e6 -r 100e3 -M 16 -N 5000 --message-source random -o qam16.npy + ria synth psk -s 2e6 -r 100e3 -M 8 -N 8000 -o psk8.npy + + +4.10 ``transform`` group +------------------------- + +**Purpose:** + +* Apply algorithmic transforms to existing recordings. +* Run reusable augmentations/impairments for dataset diversity and robustness testing. + +**Shape:** + +.. code-block:: bash + + ria transform ... + +``augment`` +~~~~~~~~~~~~ + +.. code-block:: bash + + ria transform augment [augmentation] [input] [output] [options] + +Applies transforms from ``iq_augmentations`` (dataset-expansion style modifications). + +Options: ``--list``, ``--help-transform``, ``--params KEY=VALUE`` (repeatable), ``--view``, +``--overwrite``, ``-v`` / ``--verbose``, ``-q`` / ``--quiet`` + +``impair`` +~~~~~~~~~~~ + +.. code-block:: bash + + ria transform impair [impairment] [input] [output] [options] + +Applies transforms from ``iq_impairments`` (noise, distortion, and channel degradation +effects). Same options as ``augment``. + +``custom`` +~~~~~~~~~~~ + +.. code-block:: bash + + ria transform custom [transform_name] [input] [output] --transform-dir [options] + +Dynamically loads public functions from Python files in ``--transform-dir`` and exposes them +as callable transforms. + +Options: ``--transform-dir`` (required), ``--list``, ``--help-transform``, +``--params KEY=VALUE`` (repeatable), ``--view``, ``--overwrite``, ``-v`` / ``--verbose``, +``-q`` / ``--quiet`` + +``--params`` values must be ``KEY=VALUE``; types are inferred as int, float, or string. +Use ``--list`` to enumerate available transform names, and ``--help-transform `` to +inspect parameter hints. ``--view`` writes a PNG preview alongside transform output. + +**Examples:** + +.. code-block:: bash + + ria transform augment --list + ria transform augment channel_swap in.npy out.npy + ria transform augment drop_samples in.npy --params max_section_size=5 --view + + ria transform impair --list + ria transform impair add_awgn_to_signal in.npy out.npy --params snr=10 + + ria transform custom --transform-dir ./my_transforms --list + ria transform custom my_filter in.npy out.npy --transform-dir ./my_transforms --params cutoff=0.2 + + +4.11 ``transmit`` +------------------ + +**Purpose:** + +* Transmit IQ via a TX-capable SDR (``pluto``, ``hackrf``, ``bladerf``, ``usrp``). +* Support playback of captured/generated waveforms for over-the-air or wired-loop test + scenarios. + +**Usage:** + +.. code-block:: bash + + ria transmit [options] + +**Input source (choose one):** + +* ``--input ``: transmit an existing recording. +* ``--generate {lfm,chirp,sine,pulse}``: synthesize a transmit signal on the fly. +* If neither is specified, the command defaults to a generated LFM waveform. + +**Core options:** + +*Device/radio:* ``-d`` / ``--device {pluto,hackrf,bladerf,usrp}``, ``-i`` / ``--ident``, +``-c`` / ``--config`` + +*RF:* ``-s`` / ``--sample-rate``, ``-f`` / ``--center-frequency``, ``-g`` / ``--gain``, +``-b`` / ``--bandwidth`` + +*Input/gen:* ``--input``, ``--legacy``, ``--generate {lfm,chirp,sine,pulse}`` + +*TX control:* + +* ``-r, --repeat`` +* ``--continuous``: transmit until interrupted (``Ctrl+C``). +* ``--tx-delay``: pause between repeats when ``--repeat`` is used. +* ``-y, --yes``: skip confirmation prompts; use carefully in scripted environments. + +*Logging:* ``-v`` / ``--verbose``, ``-q`` / ``--quiet`` + +.. warning:: + ``--continuous`` transmits until manually interrupted. Validate gain settings, antenna + configuration, and regulatory compliance before use. + +**Examples:** + +.. code-block:: bash + + ria transmit -d pluto -f 915e6 -s 2e6 --input capture.sigmf-data + ria transmit -d hackrf --generate lfm -f 2.44G --continuous + ria transmit -d usrp --input msg.npy -r 3 --tx-delay 0.5 + + +5) YAML Config Patterns +======================== + +Several commands accept ``--config `` for parameter loading. CLI flags generally +override values loaded from ``--config``. + +Keep one stable baseline YAML per workflow (capture, generate, transmit), then override only +experiment-specific fields on the CLI. + +**Capture config example:** + +.. code-block:: yaml + + device: pluto + ident: 192.168.2.1 + sample_rate: 2000000 + center_frequency: 2.44G + gain: 20 + bandwidth: 2000000 + num_samples: 500000 + format: sigmf + output: run1.sigmf-data + metadata: + campaign: lab_eval + antenna: dipole + +.. code-block:: bash + + ria capture -c capture.yaml + +**Generate config example:** + +.. code-block:: yaml + + sample_rate: 2000000 + num_samples: 200000 + format: npy + output: synth.npy + noise_power: 0.02 + +.. code-block:: bash + + ria generate noise --config generate.yaml + + +6) Practical Tips and Safety +============================= + +* Use ``ria discover`` before capture/transmit sessions. +* Keep TX gain conservative first; validate with attenuators/dummy loads when needed. +* Prefer SigMF for interoperable metadata and annotations. +* For long workflows, keep outputs organized by campaign directories and consistent prefixes. +* Use ``--verbose`` when debugging device init or driver issues. + + +7) Version Notes +================= + +These notes are based on the current implementation and should be re-validated against future +releases. + +1. Some command docstrings and examples still mention ``utils`` or ``ria_toolkit_oss`` + command prefixes in text blocks. The operational command is ``ria ...``. +2. Command bindings currently import ``viewe`` instead of ``view`` in + ``src/ria_toolkit_oss_cli/ria_toolkit_oss/commands.py``. +3. Multiple non-CLI modules still import ``utils.*``, which can create runtime dependency + coupling when using only ``ria-toolkit-oss`` in isolation. + +If you observe unexpected import errors after install, check the package version and +changelog, then test ``ria --help`` in a clean virtual environment. + + +8) Brief Scripting (Python) Preview +===================================== + +For quick non-CLI use: + +.. code-block:: python + + from ria_toolkit_oss.datatypes import Recording + from ria_toolkit_oss.io import load_recording, to_sigmf + from ria_toolkit_oss.transforms import iq_augmentations, iq_impairments + + rec = load_recording("capture.sigmf-data") + aug = iq_augmentations.channel_swap(rec) + imp = iq_impairments.add_awgn_to_signal(aug, snr=10) + to_sigmf(imp, filename="capture_awgn", path=".") + +You can also call annotation algorithms and block-generator primitives from Python directly. + + +9) Cheat Sheet +=============== + +.. code-block:: bash + + # Install + pip install ria-toolkit-oss + + # Discover + ria discover -v + + # Init defaults + ria init --author "Jane" --project "rf1" --location "Lab-A" + + # Capture + ria capture -d pluto -f 2.44G -s 2e6 -n 1000000 -o cap.sigmf-data + + # View + ria view cap.sigmf-data --type simple + + # Annotate + ria annotate energy cap.sigmf-data --threshold 1.2 + ria annotate list cap.sigmf-data --verbose + + # Convert + ria convert cap.sigmf-data cap.npy + + # Split + ria split cap.sigmf-data --split-every 100000 --output-dir chunks + + # Combine + ria combine chunks/a.npy chunks/b.npy merged.npy + + # Generate + ria generate qam -s 2e6 -r 100e3 -M 16 -N 5000 -o qam16.npy + + # Transform + ria transform augment channel_swap cap.npy + ria transform impair add_awgn_to_signal cap.npy --params snr=10 + + # Transmit + ria transmit -d hackrf --input cap.sigmf-data -f 2.44G -s 2e6 diff --git a/docs/source/_static/custom.css b/docs/source/_static/custom.css new file mode 100644 index 0000000..40b2823 --- /dev/null +++ b/docs/source/_static/custom.css @@ -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; } diff --git a/docs/source/_static/custom.js b/docs/source/_static/custom.js new file mode 100644 index 0000000..f9caf03 --- /dev/null +++ b/docs/source/_static/custom.js @@ -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$2' + ); + }); +}); diff --git a/docs/source/conf.py b/docs/source/conf.py index 9db1d19..1a3b6d0 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -14,7 +14,7 @@ sys.path.insert(0, os.path.abspath(os.path.join('..', '..'))) project = 'ria-toolkit-oss' copyright = '2025, Qoherent Inc' author = 'Qoherent Inc.' -release = '0.1.4' +release = '0.1.5' # -- 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 html_theme = 'sphinx_rtd_theme' +html_static_path = ['_static'] +html_css_files = ['custom.css'] +html_js_files = ['custom.js'] diff --git a/docs/source/intro/getting_started.rst b/docs/source/intro/getting_started.rst index 928e753..c2386ee 100644 --- a/docs/source/intro/getting_started.rst +++ b/docs/source/intro/getting_started.rst @@ -1,7 +1,1083 @@ -Getting Started -=============== - -.. todo:: - - Getting started instructions are coming soon! In the meantime, feel free - to explore the project documentation. Many components include usage examples. +############### +Getting Started +############### + +This is a practical reference for the ``ria`` CLI from ``ria-toolkit-oss``. + +**Scope of this guide:** + +* Installation and setup +* End-to-end CLI workflows +* Full command reference for CLI features +* Brief scripting section + +**Official resources:** + +* `Project README `_ +* `Documentation `_ +* `PyPI package `_ +* `RIA Hub Conda package `_ + +.. contents:: Contents + :local: + :depth: 2 + :backlinks: none + + +1) Installation and Setup +========================== + +1.1 Installation with Conda +---------------------------- + +RIA Toolkit OSS is available as a Conda package on RIA Hub. This is typically the easiest +path when using SDR tooling that depends on native/system libraries. + +.. code-block:: bash + + conda update --force conda + conda config --add channels https://riahub.ai/api/packages/qoherent/conda + conda activate base + conda install ria-toolkit-oss + +Verify: + +.. code-block:: bash + + conda list | grep ria-toolkit-oss + + +1.2 Installation with pip +-------------------------- + +Use pip unless you specifically need to edit toolkit source. + +.. code-block:: bash + + python3 -m venv .venv + source .venv/bin/activate + pip install --upgrade pip + pip install ria-toolkit-oss + +Verify CLI entrypoint: + +.. code-block:: bash + + ria --help + +``pyproject.toml`` defines two script entry points: + +* ``ria`` +* ``ria-tools`` + +Both point to the same CLI module (``ria_toolkit_oss_cli.cli:cli``). + + +1.3 Optional install from source +---------------------------------- + +Use this for local development or testing unreleased changes. + +.. code-block:: bash + + git clone https://riahub.ai/qoherent/ria-toolkit-oss.git + cd ria-toolkit-oss + python3 -m venv .venv + source .venv/bin/activate + pip install -e . + + +1.4 SDR driver prerequisites +----------------------------- + +Toolkit package install does not install all system SDR drivers. Install vendor/runtime +dependencies for the hardware you use. + +Examples (depends on device and OS): + +* USRP: UHD drivers +* Pluto: libiio / IIO utilities +* BladeRF: libbladeRF +* HackRF: libhackrf +* RTL-SDR: librtlsdr + +See repo docs under ``docs/source/sdr_guides/*`` and your OS package instructions. + + +2) CLI Structure +================= + +Top-level CLI follows this model: + +.. code-block:: bash + + ria [GLOBAL_OPTS] [ARGS] [OPTIONS] + +**Global:** + +* ``-v, --verbose`` (defined on root click group) + +**Top-level commands:** + +* ``discover`` +* ``init`` +* ``capture`` +* ``view`` +* ``annotate`` (group) +* ``convert`` +* ``split`` +* ``combine`` +* ``generate`` (group) +* ``transform`` (group) +* ``transmit`` +* ``synth`` (alias of ``generate`` in command bindings) + + +3) Quick End-to-End Workflow +============================= + +3.1 Discover radios +-------------------- + +Run this first in any new environment to verify drivers and detect hardware before +attempting RX/TX commands. + +.. code-block:: bash + + ria discover + ria discover -v + ria discover --json-output + + +3.2 Initialize local metadata defaults +--------------------------------------- + +Set reusable metadata once so generated/captured files automatically include consistent +provenance fields. + +.. code-block:: bash + + ria init + # or non-interactive + ria init --author "Jane Doe" --project "rf-campaign-1" --location "Lab-A" + # show config + ria init --show + + +3.3 Capture IQ +--------------- + +Capture baseband data from a connected SDR into a reusable file format. + +.. code-block:: bash + + ria capture -d pluto -f 2.44G -s 2e6 -n 500000 -o capture.sigmf-data + + +3.4 Visualize and inspect +-------------------------- + +Render quick diagnostic plots to validate signal presence, quality, and rough structure. + +.. code-block:: bash + + ria view capture.sigmf-data --type simple + ria view capture.sigmf-data --type full --show --no-save + + +3.5 Auto-annotate and inspect annotations +------------------------------------------ + +Create initial labels automatically, then inspect annotation objects before downstream use. + +.. code-block:: bash + + ria annotate energy capture.sigmf-data --label signal --threshold 1.2 + ria annotate list capture.sigmf-data --verbose + + +3.6 Convert and split +---------------------- + +Normalize file format and split large captures into manageable chunks for processing or +training. + +.. code-block:: bash + + ria convert capture.sigmf-data capture.npy + ria split capture.sigmf-data --split-every 100000 --output-dir chunks + + +3.7 Apply transforms +--------------------- + +Augment or impair recordings to produce controlled variants. + +.. code-block:: bash + + ria transform augment channel_swap capture.npy + ria transform impair add_awgn_to_signal capture.npy --params snr=10 + + +3.8 Transmit (TX-capable radios only) +-------------------------------------- + +Replay recorded or synthesized IQ through a transmit-capable SDR. + +.. code-block:: bash + + ria transmit -d hackrf -f 2.44G -s 2e6 --input capture.sigmf-data + # or generated waveform + ria transmit -d hackrf --generate lfm --continuous + + +4) Command Reference +===================== + +4.1 ``discover`` +----------------- + +**Purpose:** + +* Probe available SDR drivers and enumerate attached hardware. +* Confirm whether runtime libraries/drivers are installed and discoverable before + capture/transmit. + +**Usage:** + +.. code-block:: bash + + ria discover [--verbose] [--json-output] + +**Options:** + +* ``-v, --verbose``: include per-driver probe details and import/init failures. +* ``--json-output``: emit JSON (useful for automation and inventory scripts). + +**Behavior notes:** + +* ``discover`` checks multiple backends (USB and network paths, depending on driver support). +* A device not appearing here usually means one of: missing system driver, permission issue, + USB/network connectivity issue. +* Use ``--verbose`` first when troubleshooting; it surfaces driver-level failures that are + hidden in default output. + + +4.2 ``init`` +------------- + +**Purpose:** + +* Create/manage user config file (defaults to ``~/.ria/config.yaml``, or + ``$XDG_CONFIG_HOME/ria/config.yaml``). + +**Usage:** + +.. code-block:: bash + + ria init [options] + +**Key options:** + +*Metadata defaults:* +``--author``, ``--organization``, ``--project``, ``--location``, ``--testbed``, +``--license``, ``--hw``, ``--dataset`` + +*Actions:* +``--show``, ``--reset`` + +*Control:* +``--config-path``, ``--interactive`` / ``--no-interactive``, ``-y`` / ``--yes`` + +**What each option category does:** + +* Metadata defaults (``--author``, ``--project``, etc.): stored once and reused for later + recordings so files have consistent provenance. +* SigMF-focused fields (``--license``, ``--hw``, ``--dataset``): populate metadata commonly + expected in shared datasets. +* ``--show``: read-only inspect of the current resolved config. +* ``--reset``: remove config and start clean. +* ``--config-path``: use a non-default config location (useful for isolated environments or + CI). +* ``--interactive`` / ``--no-interactive``: force prompts on or off regardless of terminal + auto-detection. +* ``--yes``: suppress confirmation prompts for scripted runs. + +.. note:: + Current command output includes a note that some config integration is still being + finalized. Config values are already consumed by multiple commands (capture, convert, + generate metadata, and YAML config loading paths). + + +4.3 ``capture`` +---------------- + +**Purpose:** + +* Record IQ samples from a supported SDR and save to ``sigmf``, ``npy``, ``wav``, or + ``blue``. + +**Usage:** + +.. code-block:: bash + + ria capture [options] + +Device selection (``--device``) is optional if only one device is detected. Exactly one of +``--num-samples`` or ``--duration`` is required. + +**Core options:** + +*Device/connection:* + +* ``-d, --device {pluto,hackrf,bladerf,usrp,rtlsdr,thinkrf}`` +* ``-i, --ident`` +* ``-c, --config `` + +*RF/capture:* + +* ``-s, --sample-rate`` +* ``-f, --center-frequency`` (supports values like ``915e6``, ``2.4G``) +* ``-g, --gain`` +* ``-b, --bandwidth`` +* ``-n, --num-samples`` +* ``-t, --duration`` + +*Output:* + +* ``-o, --output`` +* ``--output-dir`` +* ``--format {npy,sigmf,wav,blue}`` +* ``--save-image`` + +*Metadata/logging:* + +* ``-m, --metadata KEY=VALUE`` (repeatable) +* ``-v, --verbose``, ``-q, --quiet`` + +**How options work in practice:** + +* ``--device`` + ``--ident``: select both device class and target instance; ``--ident`` + takes serial/IP style selectors. +* ``--config``: load a YAML option set, then override specific fields on the CLI as needed. +* ``--num-samples`` vs ``--duration``: use exact sample count for deterministic datasets, + or time-based capture for quick acquisition. +* ``--format``: ``sigmf`` is best for metadata/annotation workflows. +* ``--save-image``: writes a quick visual summary alongside capture output. +* ``--metadata KEY=VALUE``: injects run-specific metadata (campaign ID, antenna, scenario + tag, etc.). + +**Output behavior:** + +* If ``--output`` is omitted, a timestamped filename is generated automatically. +* If ``--output-dir`` is omitted, captures default to ``recordings/``. +* Format is inferred from the ``--output`` extension when no explicit ``--format`` is given. + +**Examples:** + +.. code-block:: bash + + ria capture -d hackrf -s 2e6 -f 2.44G -n 1000000 -o rf.sigmf-data + ria capture -d pluto -f 915e6 -t 2 --format npy --output-dir recordings + ria capture -c capture_config.yaml + + +4.4 ``view`` +------------- + +**Purpose:** + +* Generate visualizations from IQ files. +* Quickly validate signal quality, occupancy, and annotation coverage without writing custom + plotting code. + +**Usage:** + +.. code-block:: bash + + ria view [options] + +```` accepts SigMF, NPY, WAV, and Blue files. + +**Mode** (``--type``): + +* ``simple``: fast-look plots for sanity checks and quick iteration. +* ``full``: multi-panel diagnostic figure (IQ, time, frequency, metadata views). +* ``annotations`` / ``annotation``: render annotation overlays. +* ``channels``: channelized/segmented visualization. +* ``annotate``: convenience path used in some annotation workflows. + +**Output/display options:** + +* ``--output``, ``--format {png,pdf,svg,jpg}`` +* ``--show``: open an interactive window (requires a GUI display environment). +* ``--no-save``: suppress file output; only meaningful with ``--show``. +* ``--overwrite`` + +**Style options:** + +* ``--dpi``, ``--figsize WxH``, ``--title`` +* ``--light``: switch to a light theme (useful for reports/slides). + +**Loading options:** + +* ``--legacy``: force legacy NPY loading path for older datasets. +* ``--config`` + +**Mode-specific options:** + +*simple:* ``--fast``, ``--compact``, ``--horizontal``, ``--constellation``, ``--labels``, +``--slice start:end[:step]`` + +*full:* ``--plot-length``, ``--no-spectrogram``, ``--no-iq``, ``--no-frequency``, +``--no-constellation``, ``--no-metadata``, ``--no-logo``, ``--spines`` + +*annotations/channels:* ``--channel`` + +**Examples:** + +.. code-block:: bash + + ria view capture.sigmf-data --type simple + ria view capture.npy --type full --title "Test Capture" --format pdf + ria view capture.npy --show --no-save + ria view old.npy --legacy --type simple + + +4.5 ``annotate`` group +----------------------- + +**Purpose:** + +* Manual annotation management and auto-detection/separation. +* Build or refine label metadata directly in recordings for downstream training, QA, and + filtering. + +**Command shape:** + +.. code-block:: bash + + ria annotate ... + +**Subcommands:** ``list``, ``add``, ``remove``, ``clear``, ``energy``, ``cusum``, +``threshold``, ``separate`` + +**General behavior:** + +* SigMF is the preferred format for durable annotation metadata. +* For non-SigMF input, many operations write a new output artifact unless overwrite behavior + is explicitly requested. +* ``--type {standalone,parallel,intersection}`` controls annotation relation semantics. + +``ria annotate list`` +~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: bash + + ria annotate list [--verbose] + +Prints all annotations for a recording in index order. ``--verbose`` includes additional +detail per record. + +``ria annotate add`` +~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: bash + + ria annotate add --start --count --label [options] + +Adds one explicit annotation with sample-domain boundaries. + +* ``--start``: first sample index of the annotated region. +* ``--count``: number of samples in the region. +* ``--freq-lower``, ``--freq-upper``: optional spectral bounds in Hz. +* ``--comment``, ``--type``, ``-o`` / ``--output``, ``--overwrite``, ``--quiet`` + +``ria annotate remove`` +~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: bash + + ria annotate remove [--output ...] [--overwrite] [--quiet] + +Removes exactly one annotation by list index. Run ``annotate list`` first to confirm the +index. + +``ria annotate clear`` +~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: bash + + ria annotate clear [--force] [--overwrite] [--quiet] + +Removes all annotations from the recording. ``--force`` bypasses the confirmation prompt. + +``ria annotate energy`` +~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: bash + + ria annotate energy [options] + +Detects energetic regions above the estimated noise floor and writes them as annotations. + +* ``--label`` +* ``--threshold``: noise-floor multiplier; higher values reduce false positives but can miss + weak signals. +* ``--segments``: number of segments used to estimate baseline noise. +* ``--window-size``: smoothing size; larger windows stabilize detections at the cost of + sharp transition precision. +* ``--min-distance``: minimum sample spacing between detections, preventing dense duplicate + regions. +* ``--freq-method {nbw,obw,full-detected,full-bandwidth}``: how frequency bounds are assigned + to annotations. +* ``--nfft``, ``--obw-power`` +* ``--type``, ``-o`` / ``--output``, ``--overwrite``, ``--quiet`` + +``ria annotate cusum`` +~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: bash + + ria annotate cusum [options] + +Uses change-point detection (CUSUM-style logic) to find regime changes and annotate +contiguous segments. + +* ``--label`` +* ``--min-duration`` (ms): prevents tiny over-segmented labels. +* ``--window-size`` +* ``--tolerance``: merges nearby boundaries when set above default. +* ``--type``, ``-o`` / ``--output``, ``--overwrite``, ``--quiet`` + +``ria annotate threshold`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: bash + + ria annotate threshold --threshold <0.0..1.0> [options] + +Uses normalized magnitude thresholding to derive annotation spans. Use where a fixed +amplitude threshold is sufficient. + +* ``--label``, ``--window-size``, ``--type``, ``-o`` / ``--output``, ``--overwrite``, + ``--quiet`` + +``ria annotate separate`` +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: bash + + ria annotate separate [options] + +Decomposes selected annotations into narrower spectral components and emits refined +annotations. + +* ``--indices "0,1,2"``: limit operation to specific annotations; omit to process all. +* ``--nfft``: larger FFT improves frequency resolution but increases compute time. +* ``--noise-threshold-db``: stabilizes detection across heterogeneous captures. +* ``--min-component-bw``: rejects narrow fragments likely to be noise artifacts. +* ``-o`` / ``--output``, ``--overwrite``, ``--quiet``, ``--verbose`` + +**Examples:** + +.. code-block:: bash + + ria annotate list capture.sigmf-data --verbose + ria annotate add capture.sigmf-data --start 10000 --count 5000 --label burst + ria annotate energy capture.sigmf-data --label signal --threshold 1.3 + ria annotate cusum capture.sigmf-data --min-duration 5 + ria annotate separate capture.sigmf-data --indices 0,1 --verbose + + +4.6 ``convert`` +---------------- + +**Purpose:** + +* Convert between ``sigmf``, ``npy``, ``wav``, and ``blue``. +* Normalize datasets into the format required by downstream tooling or collaboration targets. + +**Usage:** + +.. code-block:: bash + + ria convert [output] [options] + +If ``output`` is omitted, ``--format`` must be provided. If both are given, format is +inferred from the output file extension. + +**Options:** + +* ``--format {npy,sigmf,wav,blue}`` +* ``--output-dir`` +* ``--legacy``: use older NPY loader behavior for historical recordings. +* ``--wav-sample-rate``: target sample rate for WAV export. +* ``--wav-bits {16,32}``: output PCM depth; higher preserves more dynamic range. +* ``--blue-format {CI,CF,CD}``: Bluefile complex sample representation. +* ``--metadata KEY=VALUE`` (repeatable): add or override metadata during conversion; + especially useful when exporting to SigMF. +* ``--overwrite``, ``-v`` / ``--verbose``, ``-q`` / ``--quiet`` + +**Examples:** + +.. code-block:: bash + + ria convert recording.sigmf-data output.npy + ria convert recording.npy --format sigmf + ria convert highrate.npy audio.wav --wav-sample-rate 48000 + ria convert old.npy --format sigmf --legacy --overwrite + + +4.7 ``split`` +-------------- + +**Purpose:** + +* Split, trim, or extract recordings. +* Create manageable dataset shards or extract windows of interest without custom scripts. + +**Usage:** + +.. code-block:: bash + + ria split [operation] [options] + +Choose exactly one operation per invocation: + +* ``--split-at ``: binary split at a specific sample index. +* ``--split-every ``: fixed-size chunking for ML pipelines. +* ``--split-duration ``: time-based chunking. +* ``--trim`` (with ``--start`` + ``--length`` or ``--end``): extract one sub-window. +* ``--extract-annotations``: write each annotated region as a standalone file. + +**Trim controls:** ``--start``, ``--length``, ``--end`` + +**Annotation extraction filters:** ``--annotation-label``, ``--annotation-index`` + +**Output controls:** +``--output-dir``, ``--output-prefix``, ``--output-format {npy,sigmf,wav,blue}``, +``--overwrite``, ``--legacy``, ``-v`` / ``--verbose``, ``-q`` / ``--quiet`` + +**Examples:** + +.. code-block:: bash + + ria split recording.sigmf-data --split-at 500000 --output-dir out + ria split recording.sigmf-data --split-every 100000 --output-dir chunks + ria split recording.sigmf-data --split-duration 1.0 --output-dir chunks + ria split recording.npy --trim --start 1000 --length 5000 --output-dir trimmed + ria split annotated.sigmf-data --extract-annotations --annotation-label payload + + +4.8 ``combine`` +---------------- + +**Purpose:** + +* Merge multiple recordings by concatenation or sample-wise addition. +* Assemble multi-part captures or synthesize mixtures for testing and model training. + +**Usage:** + +.. code-block:: bash + + ria combine [input3 ...] [options] + +**Options:** + +* ``--mode {concat,add}`` +* ``--align-mode {error,truncate,pad,pad-start,pad-center,pad-end,repeat,repeat-spaced}`` +* ``--pad-start-sample``, ``--repeat-spacing`` +* ``--normalize``: rescale combined output to avoid clipping/saturation after addition. +* ``--output-format {sigmf,npy,wav,blue}`` +* ``--overwrite``, ``--metadata KEY=VALUE`` (repeatable) +* ``--legacy``, ``--verbose``, ``--quiet`` + +**Mode semantics:** + +* ``concat``: append inputs sequentially in time. +* ``add``: sample-wise summation — all inputs must be aligned to the same length. + +**Alignment options for** ``--mode add``: + +* ``error``: fail if lengths differ. +* ``truncate``: cut all to shortest length. +* ``pad``, ``pad-start``, ``pad-center``, ``pad-end``: zero-pad shorter streams. +* ``repeat``: tile shorter streams to match longest. +* ``repeat-spaced``: repeated placement with spacing via ``--repeat-spacing``. + +**Examples:** + +.. code-block:: bash + + ria combine a.npy b.npy c.npy merged.npy + ria combine signal.npy noise.npy noisy.npy --mode add + ria combine long.npy short.npy out.npy --mode add --align-mode pad-center + ria combine signal.npy pattern.npy out.npy --mode add --align-mode repeat-spaced --repeat-spacing 10000 + + +4.9 ``generate`` group (and ``synth`` alias) +--------------------------------------------- + +**Purpose:** + +* Generate synthetic IQ signals and save in ``npy``, ``sigmf``, ``wav``, or ``blue``. +* Create known-reference waveforms and synthetic datasets for validation, demos, and ML + data generation. + +``ria synth ...`` is an alias for ``ria generate ...``. + +**Shape:** + +.. code-block:: bash + + ria generate [subcommand options] [common options] + +**Available subcommands:** +``tone``, ``noise``, ``chirp``, ``square``, ``sawtooth``, ``qam``, ``apsk``, ``pam``, +``fsk``, ``ook``, ``oqpsk``, ``gmsk``, ``psk`` + +**Common options shared across all generators:** + +* ``-s, --sample-rate`` (required) +* ``-n, --num-samples`` or ``-t, --duration`` +* ``--frequency-shift``, ``-fc`` / ``--center-frequency`` +* ``--add-noise``, ``--noise-power``, ``--path-gain`` +* ``-o, --output`` (required), ``-F`` / ``--format {npy,sigmf,wav,blue}`` +* ``--multipath-paths``, ``--multipath-max-delay`` +* ``--iq-amp-imbalance``, ``--iq-phase-imbalance``, ``--iq-dc-offset`` +* ``--config `` +* ``-w`` / ``--overwrite``, ``-m`` / ``--metadata KEY=VALUE`` (repeatable) +* ``-v`` / ``--verbose``, ``-q`` / ``--quiet`` + +``--frequency-shift`` and ``--center-frequency`` let you separate the baseband shape from +RF metadata context. ``--add-noise`` and ``--noise-power`` apply post-generation noise. +Multipath and IQ imbalance flags apply impairment-style post-processing during generation. + +``tone`` +~~~~~~~~~ + +Options: ``--frequency``, ``--amplitude``, ``--phase`` + +Clean sinusoidal calibration/reference source. + +``noise`` +~~~~~~~~~~ + +Options: ``--noise-type {gaussian,uniform}``, ``--power`` + +Baseline noise floor data or controlled additive-noise synthesis. + +``chirp`` +~~~~~~~~~~ + +Options: ``--bandwidth`` (required), ``--period`` (required), ``--type {up,down,up_down}`` + +Sweep-based radar/sonar-style signals and bandwidth occupancy tests. + +``square`` +~~~~~~~~~~~ + +Options: ``--frequency``, ``--amplitude``, ``--duty-cycle``, ``--phase`` + +``sawtooth`` +~~~~~~~~~~~~~ + +Options: ``--frequency``, ``--amplitude``, ``--phase`` + +Digital modulation families: ``qam``, ``apsk``, ``pam``, ``psk`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* ``--symbols``, ``--order`` +* ``--symbol-rate`` +* ``--filter {rrc,rc,gaussian,none}``, ``--filter-span``, ``--filter-beta`` +* ``--message-source {random,file,string}``, ``--message-content`` + +Use ``--message-source random`` for synthetic datasets, ``file`` for deterministic replay, +or ``string`` for small human-readable payload testing. Pulse-shaping filter options +(``--filter``, ``--filter-span``, ``--filter-beta``) control spectral occupancy and ISI. + +``fsk`` +~~~~~~~~ + +Options: ``--symbols``, ``--order``, ``--symbol-rate``, ``--freq-spacing``, +``--modulation-index``, ``--message-source``, ``--message-content`` + +``--freq-spacing`` and ``--modulation-index`` drive tone separation and spectral profile. + +``ook``, ``oqpsk``, ``gmsk`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Options: ``--symbol-rate`` (required), ``--message-source {random,file,string}``, +``--message-content``; ``gmsk`` also accepts ``--bt`` + +``gmsk --bt`` sets the Gaussian filter bandwidth-time product (spectral compactness vs +symbol transition sharpness). + +**Examples:** + +.. code-block:: bash + + ria generate tone -s 2e6 -n 500000 --frequency 50e3 -o tone.sigmf-data + ria generate noise -s 2e6 -n 500000 --noise-type gaussian --power 0.05 -o noise.npy + ria generate chirp -s 5e6 -t 0.5 --bandwidth 2e6 --period 0.01 --type up -o chirp.sigmf-data + ria generate qam -s 2e6 -r 100e3 -M 16 -N 5000 --message-source random -o qam16.npy + ria synth psk -s 2e6 -r 100e3 -M 8 -N 8000 -o psk8.npy + + +4.10 ``transform`` group +------------------------- + +**Purpose:** + +* Apply algorithmic transforms to existing recordings. +* Run reusable augmentations/impairments for dataset diversity and robustness testing. + +**Shape:** + +.. code-block:: bash + + ria transform ... + +``augment`` +~~~~~~~~~~~~ + +.. code-block:: bash + + ria transform augment [augmentation] [input] [output] [options] + +Applies transforms from ``iq_augmentations`` (dataset-expansion style modifications). + +Options: ``--list``, ``--help-transform``, ``--params KEY=VALUE`` (repeatable), ``--view``, +``--overwrite``, ``-v`` / ``--verbose``, ``-q`` / ``--quiet`` + +``impair`` +~~~~~~~~~~~ + +.. code-block:: bash + + ria transform impair [impairment] [input] [output] [options] + +Applies transforms from ``iq_impairments`` (noise, distortion, and channel degradation +effects). Same options as ``augment``. + +``custom`` +~~~~~~~~~~~ + +.. code-block:: bash + + ria transform custom [transform_name] [input] [output] --transform-dir [options] + +Dynamically loads public functions from Python files in ``--transform-dir`` and exposes them +as callable transforms. + +Options: ``--transform-dir`` (required), ``--list``, ``--help-transform``, +``--params KEY=VALUE`` (repeatable), ``--view``, ``--overwrite``, ``-v`` / ``--verbose``, +``-q`` / ``--quiet`` + +``--params`` values must be ``KEY=VALUE``; types are inferred as int, float, or string. +Use ``--list`` to enumerate available transform names, and ``--help-transform `` to +inspect parameter hints. ``--view`` writes a PNG preview alongside transform output. + +**Examples:** + +.. code-block:: bash + + ria transform augment --list + ria transform augment channel_swap in.npy out.npy + ria transform augment drop_samples in.npy --params max_section_size=5 --view + + ria transform impair --list + ria transform impair add_awgn_to_signal in.npy out.npy --params snr=10 + + ria transform custom --transform-dir ./my_transforms --list + ria transform custom my_filter in.npy out.npy --transform-dir ./my_transforms --params cutoff=0.2 + + +4.11 ``transmit`` +------------------ + +**Purpose:** + +* Transmit IQ via a TX-capable SDR (``pluto``, ``hackrf``, ``bladerf``, ``usrp``). +* Support playback of captured/generated waveforms for over-the-air or wired-loop test + scenarios. + +**Usage:** + +.. code-block:: bash + + ria transmit [options] + +**Input source (choose one):** + +* ``--input ``: transmit an existing recording. +* ``--generate {lfm,chirp,sine,pulse}``: synthesize a transmit signal on the fly. +* If neither is specified, the command defaults to a generated LFM waveform. + +**Core options:** + +*Device/radio:* ``-d`` / ``--device {pluto,hackrf,bladerf,usrp}``, ``-i`` / ``--ident``, +``-c`` / ``--config`` + +*RF:* ``-s`` / ``--sample-rate``, ``-f`` / ``--center-frequency``, ``-g`` / ``--gain``, +``-b`` / ``--bandwidth`` + +*Input/gen:* ``--input``, ``--legacy``, ``--generate {lfm,chirp,sine,pulse}`` + +*TX control:* + +* ``-r, --repeat`` +* ``--continuous``: transmit until interrupted (``Ctrl+C``). +* ``--tx-delay``: pause between repeats when ``--repeat`` is used. +* ``-y, --yes``: skip confirmation prompts; use carefully in scripted environments. + +*Logging:* ``-v`` / ``--verbose``, ``-q`` / ``--quiet`` + +.. warning:: + ``--continuous`` transmits until manually interrupted. Validate gain settings, antenna + configuration, and regulatory compliance before use. + +**Examples:** + +.. code-block:: bash + + ria transmit -d pluto -f 915e6 -s 2e6 --input capture.sigmf-data + ria transmit -d hackrf --generate lfm -f 2.44G --continuous + ria transmit -d usrp --input msg.npy -r 3 --tx-delay 0.5 + + +5) YAML Config Patterns +======================== + +Several commands accept ``--config `` for parameter loading. CLI flags generally +override values loaded from ``--config``. + +Keep one stable baseline YAML per workflow (capture, generate, transmit), then override only +experiment-specific fields on the CLI. + +**Capture config example:** + +.. code-block:: yaml + + device: pluto + ident: 192.168.2.1 + sample_rate: 2000000 + center_frequency: 2.44G + gain: 20 + bandwidth: 2000000 + num_samples: 500000 + format: sigmf + output: run1.sigmf-data + metadata: + campaign: lab_eval + antenna: dipole + +.. code-block:: bash + + ria capture -c capture.yaml + +**Generate config example:** + +.. code-block:: yaml + + sample_rate: 2000000 + num_samples: 200000 + format: npy + output: synth.npy + noise_power: 0.02 + +.. code-block:: bash + + ria generate noise --config generate.yaml + + +6) Practical Tips and Safety +============================= + +* Use ``ria discover`` before capture/transmit sessions. +* Keep TX gain conservative first; validate with attenuators/dummy loads when needed. +* Prefer SigMF for interoperable metadata and annotations. +* For long workflows, keep outputs organized by campaign directories and consistent prefixes. +* Use ``--verbose`` when debugging device init or driver issues. + + +7) Version Notes +================= + +These notes are based on the current implementation and should be re-validated against future +releases. + +1. Some command docstrings and examples still mention ``utils`` or ``ria_toolkit_oss`` + command prefixes in text blocks. The operational command is ``ria ...``. +2. Command bindings currently import ``viewe`` instead of ``view`` in + ``src/ria_toolkit_oss_cli/ria_toolkit_oss/commands.py``. +3. Multiple non-CLI modules still import ``utils.*``, which can create runtime dependency + coupling when using only ``ria-toolkit-oss`` in isolation. + +If you observe unexpected import errors after install, check the package version and +changelog, then test ``ria --help`` in a clean virtual environment. + + +8) Brief Scripting (Python) Preview +===================================== + +For quick non-CLI use: + +.. code-block:: python + + from ria_toolkit_oss.datatypes import Recording + from ria_toolkit_oss.io import load_recording, to_sigmf + from ria_toolkit_oss.transforms import iq_augmentations, iq_impairments + + rec = load_recording("capture.sigmf-data") + aug = iq_augmentations.channel_swap(rec) + imp = iq_impairments.add_awgn_to_signal(aug, snr=10) + to_sigmf(imp, filename="capture_awgn", path=".") + +You can also call annotation algorithms and block-generator primitives from Python directly. + + +9) Cheat Sheet +=============== + +.. code-block:: bash + + # Install + pip install ria-toolkit-oss + + # Discover + ria discover -v + + # Init defaults + ria init --author "Jane" --project "rf1" --location "Lab-A" + + # Capture + ria capture -d pluto -f 2.44G -s 2e6 -n 1000000 -o cap.sigmf-data + + # View + ria view cap.sigmf-data --type simple + + # Annotate + ria annotate energy cap.sigmf-data --threshold 1.2 + ria annotate list cap.sigmf-data --verbose + + # Convert + ria convert cap.sigmf-data cap.npy + + # Split + ria split cap.sigmf-data --split-every 100000 --output-dir chunks + + # Combine + ria combine chunks/a.npy chunks/b.npy merged.npy + + # Generate + ria generate qam -s 2e6 -r 100e3 -M 16 -N 5000 -o qam16.npy + + # Transform + ria transform augment channel_swap cap.npy + ria transform impair add_awgn_to_signal cap.npy --params snr=10 + + # Transmit + ria transmit -d hackrf --input cap.sigmf-data -f 2.44G -s 2e6 diff --git a/poetry.lock b/poetry.lock index 6b691a5..a9c4476 100644 --- a/poetry.lock +++ b/poetry.lock @@ -98,6 +98,83 @@ files = [ [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\""] +[[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]] name = "black" version = "26.3.1" @@ -182,7 +259,7 @@ description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.9" groups = ["main"] -markers = "implementation_name == \"pypy\"" +markers = "implementation_name == \"pypy\" or platform_python_implementation != \"PyPy\"" 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_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, @@ -414,14 +491,14 @@ files = [ [[package]] name = "click" -version = "8.3.1" +version = "8.3.2" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.10" groups = ["main", "dev", "docs", "server", "test"] files = [ - {file = "click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"}, - {file = "click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a"}, + {file = "click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d"}, + {file = "click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5"}, ] [package.dependencies] @@ -611,6 +688,79 @@ mypy = ["bokeh", "contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.17.0)", " test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] 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]] name = "cycler" version = "0.12.1" @@ -627,6 +777,18 @@ files = [ docs = ["ipython", "matplotlib", "numpydoc", "sphinx"] tests = ["pytest", "pytest-cov", "pytest-xdist"] +[[package]] +name = "defusedxml" +version = "0.7.1" +description = "XML bomb protection for Python stdlib modules" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["main"] +files = [ + {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, + {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, +] + [[package]] name = "dill" version = "0.4.1" @@ -688,14 +850,14 @@ test = ["pytest (>=6)"] [[package]] name = "fastapi" -version = "0.135.3" +version = "0.136.0" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false python-versions = ">=3.10" groups = ["server", "test"] files = [ - {file = "fastapi-0.135.3-py3-none-any.whl", hash = "sha256:9b0f590c813acd13d0ab43dd8494138eb58e484bfac405db1f3187cfc5810d98"}, - {file = "fastapi-0.135.3.tar.gz", hash = "sha256:bd6d7caf1a2bdd8d676843cdcd2287729572a1ef524fc4d65c17ae002a1be654"}, + {file = "fastapi-0.136.0-py3-none-any.whl", hash = "sha256:8793d44ec7378e2be07f8a013cf7f7aa47d6327d0dfe9804862688ec4541a6b4"}, + {file = "fastapi-0.136.0.tar.gz", hash = "sha256:cf08e067cc66e106e102d9ba659463abfac245200752f8a5b7b1e813de4ff73e"}, ] [package.dependencies] @@ -707,19 +869,19 @@ typing-inspection = ">=0.4.2" [package.extras] all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "uvicorn[standard] (>=0.12.0)"] -standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] +standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "fastar (>=0.9.0)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[standard-no-fastapi-cloud-cli] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] [[package]] name = "filelock" -version = "3.25.2" +version = "3.29.0" description = "A platform independent file lock." optional = false python-versions = ">=3.10" groups = ["test"] files = [ - {file = "filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70"}, - {file = "filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694"}, + {file = "filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258"}, + {file = "filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90"}, ] [[package]] @@ -746,6 +908,7 @@ description = "The FlatBuffers serialization format for Python" optional = false python-versions = "*" groups = ["server", "test"] +markers = "python_version >= \"3.11\"" files = [ {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"}, ] +[[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]] name = "isort" version = "5.13.2" @@ -1443,6 +1618,7 @@ description = "Python library for arbitrary-precision floating-point arithmetic" optional = false python-versions = "*" groups = ["server", "test"] +markers = "python_version >= \"3.11\"" files = [ {file = "mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c"}, {file = "mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f"}, @@ -1468,14 +1644,14 @@ files = [ [[package]] name = "narwhals" -version = "2.18.1" +version = "2.20.0" description = "Extremely lightweight compatibility layer between dataframe libraries" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "narwhals-2.18.1-py3-none-any.whl", hash = "sha256:a0a8bb80205323851338888ba3a12b4f65d352362c8a94be591244faf36504ad"}, - {file = "narwhals-2.18.1.tar.gz", hash = "sha256:652a1fcc9d432bbf114846688884c215f17eb118aa640b7419295d2f910d2a8b"}, + {file = "narwhals-2.20.0-py3-none-any.whl", hash = "sha256:16e750ea5507d4ba6e8d03455b5f93a535e0405976561baea235bca5dc9f475d"}, + {file = "narwhals-2.20.0.tar.gz", hash = "sha256:c10994975fa7dc5a68c2cffcddbd5908fc8ebb2d463c5bab085309c0ee1f551e"}, ] [package.extras] @@ -1537,48 +1713,7 @@ files = [ {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, ] - -[[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 = "*" +markers = {server = "python_version >= \"3.11\"", test = "python_version >= \"3.11\""} [[package]] name = "onnxruntime" @@ -1624,15 +1759,16 @@ sympy = "*" [[package]] name = "packaging" -version = "26.0" +version = "26.1" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" groups = ["main", "dev", "docs", "server", "test"] files = [ - {file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"}, - {file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"}, + {file = "packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f"}, + {file = "packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de"}, ] +markers = {server = "python_version >= \"3.11\""} [[package]] name = "pandas" @@ -1701,9 +1837,9 @@ files = [ [package.dependencies] numpy = [ - {version = ">=1.22.4", markers = "python_version < \"3.11\""}, {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, {version = ">=1.23.2", markers = "python_version == \"3.11\""}, + {version = ">=1.22.4", markers = "python_version < \"3.11\""}, ] python-dateutil = ">=2.8.2" 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)"] 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]] name = "pathspec" version = "1.0.4" @@ -1863,26 +2020,26 @@ xmp = ["defusedxml"] [[package]] name = "platformdirs" -version = "4.9.4" +version = "4.9.6" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.10" groups = ["dev", "test"] files = [ - {file = "platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868"}, - {file = "platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934"}, + {file = "platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917"}, + {file = "platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a"}, ] [[package]] name = "plotly" -version = "6.6.0" +version = "6.7.0" description = "An open-source interactive data visualization library for Python" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "plotly-6.6.0-py3-none-any.whl", hash = "sha256:8d6daf0f87412e0c0bfe72e809d615217ab57cc715899a1e5145135a7800d1d0"}, - {file = "plotly-6.6.0.tar.gz", hash = "sha256:b897f15f3b02028d69f755f236be890ba950d0a42d7dfc619b44e2d8cea8748c"}, + {file = "plotly-6.7.0-py3-none-any.whl", hash = "sha256:ac8aca1c25c663a59b5b9140a549264a5badde2e057d79b8c772ae2920e32ff0"}, + {file = "plotly-6.7.0.tar.gz", hash = "sha256:45eea0ff27e2a23ccd62776f77eb43aa1ca03df4192b76036e380bb479b892c6"}, ] [package.dependencies] @@ -1890,11 +2047,11 @@ narwhals = ">=1.15.1" packaging = "*" [package.extras] -dev = ["plotly[dev-optional]"] -dev-build = ["build", "jupyter", "plotly[dev-core]"] +dev = ["anywidget", "build", "colorcet", "fiona (<=1.9.6) ; python_version <= \"3.8\"", "geopandas", "inflect", "jupyterlab", "kaleido (>=1.1.0)", "numpy (>=1.22)", "orjson", "pandas", "pdfrw", "pillow", "plotly-geo", "polars[timezone]", "pyarrow", "pyshp", "pytest", "pytz", "requests", "ruff (==0.11.12)", "scikit-image", "scipy", "shapely", "statsmodels", "vaex ; python_version <= \"3.9\"", "xarray"] +dev-build = ["build", "jupyterlab", "pytest", "requests", "ruff (==0.11.12)"] dev-core = ["pytest", "requests", "ruff (==0.11.12)"] -dev-optional = ["anywidget", "colorcet", "fiona (<=1.9.6) ; python_version <= \"3.8\"", "geopandas", "inflect", "numpy", "orjson", "pandas", "pdfrw", "pillow", "plotly-geo", "plotly[dev-build]", "plotly[kaleido]", "polars[timezone]", "pyarrow", "pyshp", "pytz", "scikit-image", "scipy", "shapely", "statsmodels", "vaex ; python_version <= \"3.9\"", "xarray"] -express = ["numpy"] +dev-optional = ["anywidget", "build", "colorcet", "fiona (<=1.9.6) ; python_version <= \"3.8\"", "geopandas", "inflect", "jupyterlab", "kaleido (>=1.1.0)", "numpy (>=1.22)", "orjson", "pandas", "pdfrw", "pillow", "plotly-geo", "polars[timezone]", "pyarrow", "pyshp", "pytest", "pytz", "requests", "ruff (==0.11.12)", "scikit-image", "scipy", "shapely", "statsmodels", "vaex ; python_version <= \"3.9\"", "xarray"] +express = ["numpy (>=1.22)"] kaleido = ["kaleido (>=1.1.0)"] [[package]] @@ -1920,6 +2077,7 @@ description = "" optional = false python-versions = ">=3.10" groups = ["server", "test"] +markers = "python_version >= \"3.11\"" files = [ {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"}, @@ -1950,7 +2108,7 @@ description = "C parser in Python" optional = false python-versions = ">=3.10" groups = ["main"] -markers = "implementation_name == \"pypy\"" +markers = "(platform_python_implementation != \"PyPy\" or implementation_name == \"pypy\") and implementation_name != \"PyPy\"" files = [ {file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"}, {file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"}, @@ -1958,19 +2116,19 @@ files = [ [[package]] name = "pydantic" -version = "2.12.5" +version = "2.13.3" description = "Data validation using Python type hints" optional = false python-versions = ">=3.9" groups = ["server", "test"] files = [ - {file = "pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d"}, - {file = "pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49"}, + {file = "pydantic-2.13.3-py3-none-any.whl", hash = "sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927"}, + {file = "pydantic-2.13.3.tar.gz", hash = "sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d"}, ] [package.dependencies] annotated-types = ">=0.6.0" -pydantic-core = "2.41.5" +pydantic-core = "2.46.3" typing-extensions = ">=4.14.1" typing-inspection = ">=0.4.2" @@ -1980,133 +2138,132 @@ timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows [[package]] name = "pydantic-core" -version = "2.41.5" +version = "2.46.3" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.9" groups = ["server", "test"] files = [ - {file = "pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146"}, - {file = "pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c"}, - {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2"}, - {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556"}, - {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49"}, - {file = "pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba"}, - {file = "pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9"}, - {file = "pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6"}, - {file = "pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b"}, - {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284"}, - {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594"}, - {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e"}, - {file = "pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b"}, - {file = "pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe"}, - {file = "pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f"}, - {file = "pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7"}, - {file = "pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5"}, - {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c"}, - {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294"}, - {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1"}, - {file = "pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d"}, - {file = "pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815"}, - {file = "pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3"}, - {file = "pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9"}, - {file = "pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d"}, - {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740"}, - {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e"}, - {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858"}, - {file = "pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36"}, - {file = "pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11"}, - {file = "pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd"}, - {file = "pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a"}, - {file = "pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553"}, - {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90"}, - {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07"}, - {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb"}, - {file = "pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23"}, - {file = "pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf"}, - {file = "pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008"}, - {file = "pydantic_core-2.41.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf"}, - {file = "pydantic_core-2.41.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425"}, - {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504"}, - {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5"}, - {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3"}, - {file = "pydantic_core-2.41.5-cp39-cp39-win32.whl", hash = "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460"}, - {file = "pydantic_core-2.41.5-cp39-cp39-win_amd64.whl", hash = "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b"}, - {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034"}, - {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c"}, - {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2"}, - {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad"}, - {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd"}, - {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc"}, - {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56"}, - {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51"}, - {file = "pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e"}, + {file = "pydantic_core-2.46.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:1da3786b8018e60349680720158cc19161cc3b4bdd815beb0a321cd5ce1ad5b1"}, + {file = "pydantic_core-2.46.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cc0988cb29d21bf4a9d5cf2ef970b5c0e38d8d8e107a493278c05dc6c1dda69f"}, + {file = "pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27f9067c3bfadd04c55484b89c0d267981b2f3512850f6f66e1e74204a4e4ce3"}, + {file = "pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a642ac886ecf6402d9882d10c405dcf4b902abeb2972cd5fb4a48c83cd59279a"}, + {file = "pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79f561438481f28681584b89e2effb22855e2179880314bcddbf5968e935e807"}, + {file = "pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57a973eae4665352a47cf1a99b4ee864620f2fe663a217d7a8da68a1f3a5bfda"}, + {file = "pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83d002b97072a53ea150d63e0a3adfae5670cef5aa8a6e490240e482d3b22e57"}, + {file = "pydantic_core-2.46.3-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b40ddd51e7c44b28cfaef746c9d3c506d658885e0a46f9eeef2ee815cbf8e045"}, + {file = "pydantic_core-2.46.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac5ec7fb9b87f04ee839af2d53bcadea57ded7d229719f56c0ed895bff987943"}, + {file = "pydantic_core-2.46.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a3b11c812f61b3129c4905781a2601dfdfdea5fe1e6c1cfb696b55d14e9c054f"}, + {file = "pydantic_core-2.46.3-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:1108da631e602e5b3c38d6d04fe5bb3bfa54349e6918e3ca6cf570b2e2b2f9d4"}, + {file = "pydantic_core-2.46.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:de885175515bcfa98ae618c1df7a072f13d179f81376c8007112af20567fd08a"}, + {file = "pydantic_core-2.46.3-cp310-cp310-win32.whl", hash = "sha256:d11058e3201527d41bc6b545c79187c9e4bf85e15a236a6007f0e991518882b7"}, + {file = "pydantic_core-2.46.3-cp310-cp310-win_amd64.whl", hash = "sha256:3612edf65c8ea67ac13616c4d23af12faef1ae435a8a93e5934c2a0cbbdd1fd6"}, + {file = "pydantic_core-2.46.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ab124d49d0459b2373ecf54118a45c28a1e6d4192a533fbc915e70f556feb8e5"}, + {file = "pydantic_core-2.46.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cca67d52a5c7a16aed2b3999e719c4bcf644074eac304a5d3d62dd70ae7d4b2c"}, + {file = "pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c024e08c0ba23e6fd68c771a521e9d6a792f2ebb0fa734296b36394dc30390e"}, + {file = "pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6645ce7eec4928e29a1e3b3d5c946621d105d3e79f0c9cddf07c2a9770949287"}, + {file = "pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a712c7118e6c5ea96562f7b488435172abb94a3c53c22c9efc1412264a45cbbe"}, + {file = "pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69a868ef3ff206343579021c40faf3b1edc64b1cc508ff243a28b0a514ccb050"}, + {file = "pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc7e8c32db809aa0f6ea1d6869ebc8518a65d5150fdfad8bcae6a49ae32a22e2"}, + {file = "pydantic_core-2.46.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:3481bd1341dc85779ee506bc8e1196a277ace359d89d28588a9468c3ecbe63fa"}, + {file = "pydantic_core-2.46.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8690eba565c6d68ffd3a8655525cbdd5246510b44a637ee2c6c03a7ebfe64d3c"}, + {file = "pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4de88889d7e88d50d40ee5b39d5dac0bcaef9ba91f7e536ac064e6b2834ecccf"}, + {file = "pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:e480080975c1ef7f780b8f99ed72337e7cc5efea2e518a20a692e8e7b278eb8b"}, + {file = "pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:de3a5c376f8cd94da9a1b8fd3dd1c16c7a7b216ed31dc8ce9fd7a22bf13b836e"}, + {file = "pydantic_core-2.46.3-cp311-cp311-win32.whl", hash = "sha256:fc331a5314ffddd5385b9ee9d0d2fee0b13c27e0e02dad71b1ae5d6561f51eeb"}, + {file = "pydantic_core-2.46.3-cp311-cp311-win_amd64.whl", hash = "sha256:b5b9c6cf08a8a5e502698f5e153056d12c34b8fb30317e0c5fd06f45162a6346"}, + {file = "pydantic_core-2.46.3-cp311-cp311-win_arm64.whl", hash = "sha256:5dfd51cf457482f04ec49491811a2b8fd5b843b64b11eecd2d7a1ee596ea78a6"}, + {file = "pydantic_core-2.46.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b11b59b3eee90a80a36701ddb4576d9ae31f93f05cb9e277ceaa09e6bf074a67"}, + {file = "pydantic_core-2.46.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:af8653713055ea18a3abc1537fe2ebc42f5b0bbb768d1eb79fd74eb47c0ac089"}, + {file = "pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75a519dab6d63c514f3a81053e5266c549679e4aa88f6ec57f2b7b854aceb1b0"}, + {file = "pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6cd87cb1575b1ad05ba98894c5b5c96411ef678fa2f6ed2576607095b8d9789"}, + {file = "pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f80a55484b8d843c8ada81ebf70a682f3f00a3d40e378c06cf17ecb44d280d7d"}, + {file = "pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3861f1731b90c50a3266316b9044f5c9b405eecb8e299b0a7120596334e4fe9c"}, + {file = "pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb528e295ed31570ac3dcc9bfdd6e0150bc11ce6168ac87a8082055cf1a67395"}, + {file = "pydantic_core-2.46.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:367508faa4973b992b271ba1494acaab36eb7e8739d1e47be5035fb1ea225396"}, + {file = "pydantic_core-2.46.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ad3c826fe523e4becf4fe39baa44286cff85ef137c729a2c5e269afbfd0905d"}, + {file = "pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ec638c5d194ef8af27db69f16c954a09797c0dc25015ad6123eb2c73a4d271ca"}, + {file = "pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:28ed528c45446062ee66edb1d33df5d88828ae167de76e773a3c7f64bd14e976"}, + {file = "pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aed19d0c783886d5bd86d80ae5030006b45e28464218747dcf83dabfdd092c7b"}, + {file = "pydantic_core-2.46.3-cp312-cp312-win32.whl", hash = "sha256:06d5d8820cbbdb4147578c1fe7ffcd5b83f34508cb9f9ab76e807be7db6ff0a4"}, + {file = "pydantic_core-2.46.3-cp312-cp312-win_amd64.whl", hash = "sha256:c3212fda0ee959c1dd04c60b601ec31097aaa893573a3a1abd0a47bcac2968c1"}, + {file = "pydantic_core-2.46.3-cp312-cp312-win_arm64.whl", hash = "sha256:f1f8338dd7a7f31761f1f1a3c47503a9a3b34eea3c8b01fa6ee96408affb5e72"}, + {file = "pydantic_core-2.46.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:12bc98de041458b80c86c56b24df1d23832f3e166cbaff011f25d187f5c62c37"}, + {file = "pydantic_core-2.46.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:85348b8f89d2c3508b65b16c3c33a4da22b8215138d8b996912bb1532868885f"}, + {file = "pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1105677a6df914b1fb71a81b96c8cce7726857e1717d86001f29be06a25ee6f8"}, + {file = "pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87082cd65669a33adeba5470769e9704c7cf026cc30afb9cc77fd865578ebaad"}, + {file = "pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e5f66e12c4f5212d08522963380eaaeac5ebd795826cfd19b2dfb0c7a52b9c"}, + {file = "pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6cdf19bf84128d5e7c37e8a73a0c5c10d51103a650ac585d42dd6ae233f2b7f"}, + {file = "pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031bb17f4885a43773c8c763089499f242aee2ea85cf17154168775dccdecf35"}, + {file = "pydantic_core-2.46.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:bcf2a8b2982a6673693eae7348ef3d8cf3979c1d63b54fca7c397a635cc68687"}, + {file = "pydantic_core-2.46.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28e8cf2f52d72ced402a137145923a762cbb5081e48b34312f7a0c8f55928ec3"}, + {file = "pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:17eaface65d9fc5abb940003020309c1bf7a211f5f608d7870297c367e6f9022"}, + {file = "pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:93fd339f23408a07e98950a89644f92c54d8729719a40b30c0a30bb9ebc55d23"}, + {file = "pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:23cbdb3aaa74dfe0837975dbf69b469753bbde8eacace524519ffdb6b6e89eb7"}, + {file = "pydantic_core-2.46.3-cp313-cp313-win32.whl", hash = "sha256:610eda2e3838f401105e6326ca304f5da1e15393ae25dacae5c5c63f2c275b13"}, + {file = "pydantic_core-2.46.3-cp313-cp313-win_amd64.whl", hash = "sha256:68cc7866ed863db34351294187f9b729964c371ba33e31c26f478471c52e1ed0"}, + {file = "pydantic_core-2.46.3-cp313-cp313-win_arm64.whl", hash = "sha256:f64b5537ac62b231572879cd08ec05600308636a5d63bcbdb15063a466977bec"}, + {file = "pydantic_core-2.46.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:afa3aa644f74e290cdede48a7b0bee37d1c35e71b05105f6b340d484af536d9b"}, + {file = "pydantic_core-2.46.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ced3310e51aa425f7f77da8bbbb5212616655bedbe82c70944320bc1dbe5e018"}, + {file = "pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e29908922ce9da1a30b4da490bd1d3d82c01dcfdf864d2a74aacee674d0bfa34"}, + {file = "pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0c9ff69140423eea8ed2d5477df3ba037f671f5e897d206d921bc9fdc39613e7"}, + {file = "pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b675ab0a0d5b1c8fdb81195dc5bcefea3f3c240871cdd7ff9a2de8aa50772eb2"}, + {file = "pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0087084960f209a9a4af50ecd1fb063d9ad3658c07bb81a7a53f452dacbfb2ba"}, + {file = "pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed42e6cc8e1b0e2b9b96e2276bad70ae625d10d6d524aed0c93de974ae029f9f"}, + {file = "pydantic_core-2.46.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:f1771ce258afb3e4201e67d154edbbae712a76a6081079fe247c2f53c6322c22"}, + {file = "pydantic_core-2.46.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a7610b6a5242a6c736d8ad47fd5fff87fcfe8f833b281b1c409c3d6835d9227f"}, + {file = "pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:ff5e7783bcc5476e1db448bf268f11cb257b1c276d3e89f00b5727be86dd0127"}, + {file = "pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:9d2e32edcc143bc01e95300671915d9ca052d4f745aa0a49c48d4803f8a85f2c"}, + {file = "pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6e42d83d1c6b87fa56b521479cff237e626a292f3b31b6345c15a99121b454c1"}, + {file = "pydantic_core-2.46.3-cp314-cp314-win32.whl", hash = "sha256:07bc6d2a28c3adb4f7c6ae46aa4f2d2929af127f587ed44057af50bf1ce0f505"}, + {file = "pydantic_core-2.46.3-cp314-cp314-win_amd64.whl", hash = "sha256:8940562319bc621da30714617e6a7eaa6b98c84e8c685bcdc02d7ed5e7c7c44e"}, + {file = "pydantic_core-2.46.3-cp314-cp314-win_arm64.whl", hash = "sha256:5dcbbcf4d22210ced8f837c96db941bdb078f419543472aca5d9a0bb7cddc7df"}, + {file = "pydantic_core-2.46.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d0fe3dce1e836e418f912c1ad91c73357d03e556a4d286f441bf34fed2dbeecf"}, + {file = "pydantic_core-2.46.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9ce92e58abc722dac1bf835a6798a60b294e48eb0e625ec9fd994b932ac5feee"}, + {file = "pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a03e6467f0f5ab796a486146d1b887b2dc5e5f9b3288898c1b1c3ad974e53e4a"}, + {file = "pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2798b6ba041b9d70acfb9071a2ea13c8456dd1e6a5555798e41ba7b0790e329c"}, + {file = "pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9be3e221bdc6d69abf294dcf7aff6af19c31a5cdcc8f0aa3b14be29df4bd03b1"}, + {file = "pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13936129ce841f2a5ddf6f126fea3c43cd128807b5a59588c37cf10178c2e64"}, + {file = "pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28b5f2ef03416facccb1c6ef744c69793175fd27e44ef15669201601cf423acb"}, + {file = "pydantic_core-2.46.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:830d1247d77ad23852314f069e9d7ddafeec5f684baf9d7e7065ed46a049c4e6"}, + {file = "pydantic_core-2.46.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0793c90c1a3c74966e7975eaef3ed30ebdff3260a0f815a62a22adc17e4c01c"}, + {file = "pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d2d0aead851b66f5245ec0c4fb2612ef457f8bbafefdf65a2bf9d6bac6140f47"}, + {file = "pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:2f40e4246676beb31c5ce77c38a55ca4e465c6b38d11ea1bd935420568e0b1ab"}, + {file = "pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:cf489cf8986c543939aeee17a09c04d6ffb43bfef8ca16fcbcc5cfdcbed24dba"}, + {file = "pydantic_core-2.46.3-cp314-cp314t-win32.whl", hash = "sha256:ffe0883b56cfc05798bf994164d2b2ff03efe2d22022a2bb080f3b626176dd56"}, + {file = "pydantic_core-2.46.3-cp314-cp314t-win_amd64.whl", hash = "sha256:706d9d0ce9cf4593d07270d8e9f53b161f90c57d315aeec4fb4fd7a8b10240d8"}, + {file = "pydantic_core-2.46.3-cp314-cp314t-win_arm64.whl", hash = "sha256:77706aeb41df6a76568434701e0917da10692da28cb69d5fb6919ce5fdb07374"}, + {file = "pydantic_core-2.46.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:fa3eb7c2995aa443687a825bc30395c8521b7c6ec201966e55debfd1128bcceb"}, + {file = "pydantic_core-2.46.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3d08782c4045f90724b44c95d35ebec0d67edb8a957a2ac81d5a8e4b8a200495"}, + {file = "pydantic_core-2.46.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:831eb19aa789a97356979e94c981e5667759301fb708d1c0d5adf1bc0098b873"}, + {file = "pydantic_core-2.46.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4335e87c7afa436a0dfa899e138d57a72f8aad542e2cf19c36fb428461caabd0"}, + {file = "pydantic_core-2.46.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99421e7684a60f7f3550a1d159ade5fdff1954baedb6bdd407cba6a307c9f27d"}, + {file = "pydantic_core-2.46.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd81f6907932ebac3abbe41378dac64b2380db1287e2aa64d8d88f78d170f51a"}, + {file = "pydantic_core-2.46.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f247596366f4221af52beddd65af1218797771d6989bc891a0b86ccaa019168"}, + {file = "pydantic_core-2.46.3-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:6dff8cc884679df229ebc6d8eb2321ea6f8e091bc7d4886d4dc2e0e71452843c"}, + {file = "pydantic_core-2.46.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68ef2f623dda6d5a9067ac014e406c020c780b2a358930a7e5c1b73702900720"}, + {file = "pydantic_core-2.46.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d56bdb4af1767cc15b0386b3c581fdfe659bb9ee4a4f776e92c1cd9d074000d6"}, + {file = "pydantic_core-2.46.3-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:91249bcb7c165c2fb2a2f852dbc5c91636e2e218e75d96dfdd517e4078e173dd"}, + {file = "pydantic_core-2.46.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4b068543bdb707f5d935dab765d99227aa2545ef2820935f2e5dd801795c7dbd"}, + {file = "pydantic_core-2.46.3-cp39-cp39-win32.whl", hash = "sha256:dcda6583921c05a40533f982321532f2d8db29326c7b95c4026941fa5074bd79"}, + {file = "pydantic_core-2.46.3-cp39-cp39-win_amd64.whl", hash = "sha256:a35cc284c8dd7edae8a31533713b4d2467dfe7c4f1b5587dd4031f28f90d1d13"}, + {file = "pydantic_core-2.46.3-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:9715525891ed524a0a1eb6d053c74d4d4ad5017677fb00af0b7c2644a31bae46"}, + {file = "pydantic_core-2.46.3-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:9d2f400712a99a013aff420ef1eb9be077f8189a36c1e3ef87660b4e1088a874"}, + {file = "pydantic_core-2.46.3-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd2aab0e2e9dc2daf36bd2686c982535d5e7b1d930a1344a7bb6e82baab42a76"}, + {file = "pydantic_core-2.46.3-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e9d76736da5f362fabfeea6a69b13b7f2be405c6d6966f06b2f6bfff7e64531"}, + {file = "pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:b12dd51f1187c2eb489af8e20f880362db98e954b54ab792fa5d92e8bcc6b803"}, + {file = "pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f00a0961b125f1a47af7bcc17f00782e12f4cd056f83416006b30111d941dfa3"}, + {file = "pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57697d7c056aca4bbb680200f96563e841a6386ac1129370a0102592f4dddff5"}, + {file = "pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd35aa21299def8db7ef4fe5c4ff862941a9a158ca7b63d61e66fe67d30416b4"}, + {file = "pydantic_core-2.46.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:13afdd885f3d71280cf286b13b310ee0f7ccfefd1dbbb661514a474b726e2f25"}, + {file = "pydantic_core-2.46.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f91c0aff3e3ee0928edd1232c57f643a7a003e6edf1860bc3afcdc749cb513f3"}, + {file = "pydantic_core-2.46.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6529d1d128321a58d30afcc97b49e98836542f68dd41b33c2e972bb9e5290536"}, + {file = "pydantic_core-2.46.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:975c267cff4f7e7272eacbe50f6cc03ca9a3da4c4fbd66fffd89c94c1e311aa1"}, + {file = "pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2b8e4f2bbdf71415c544b4b1138b8060db7b6611bc927e8064c769f64bed651c"}, + {file = "pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e61ea8e9fff9606d09178f577ff8ccdd7206ff73d6552bcec18e1033c4254b85"}, + {file = "pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b504bda01bafc69b6d3c7a0c7f039dcf60f47fab70e06fe23f57b5c75bdc82b8"}, + {file = "pydantic_core-2.46.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b00b76f7142fc60c762ce579bd29c8fa44aaa56592dd3c54fab3928d0d4ca6ff"}, + {file = "pydantic_core-2.46.3.tar.gz", hash = "sha256:41c178f65b8c29807239d47e6050262eb6bf84eb695e41101e62e38df4a5bc2c"}, ] [package.dependencies] @@ -2155,9 +2312,9 @@ files = [ astroid = ">=3.3.8,<=3.4.0.dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = [ - {version = ">=0.2", markers = "python_version < \"3.11\""}, {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, {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" mccabe = ">=0.6,<0.8" @@ -2169,6 +2326,48 @@ tomlkit = ">=0.10.1" spelling = ["pyenchant (>=3.2,<4.0)"] 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]] name = "pyparsing" version = "3.3.2" @@ -2245,14 +2444,14 @@ six = ">=1.5" [[package]] name = "python-discovery" -version = "1.2.1" +version = "1.2.2" description = "Python interpreter discovery" optional = false python-versions = ">=3.8" groups = ["test"] files = [ - {file = "python_discovery-1.2.1-py3-none-any.whl", hash = "sha256:b6a957b24c1cd79252484d3566d1b49527581d46e789aaf43181005e56201502"}, - {file = "python_discovery-1.2.1.tar.gz", hash = "sha256:180c4d114bff1c32462537eac5d6a332b768242b76b69c0259c7d14b1b680c9e"}, + {file = "python_discovery-1.2.2-py3-none-any.whl", hash = "sha256:e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a"}, + {file = "python_discovery-1.2.2.tar.gz", hash = "sha256:876e9c57139eb757cb5878cbdd9ae5379e5d96266c99ef731119e04fffe533bb"}, ] [package.dependencies] @@ -2775,17 +2974,18 @@ test = ["Cython", "array-api-strict (>=2.0,<2.1.1)", "asv", "gmpy2", "hypothesis [[package]] name = "sigmf" -version = "1.7.2" +version = "1.8.0" description = "Easily interact with Signal Metadata Format (SigMF) recordings." optional = false python-versions = ">=3.7" groups = ["main"] files = [ - {file = "sigmf-1.7.2-py3-none-any.whl", hash = "sha256:6599b95e8bd3ac2c568b8ec46c312a77b80868cbda79d729234f396d2194d3d8"}, - {file = "sigmf-1.7.2.tar.gz", hash = "sha256:5f80f7127539358c7528ccf26e0ac5b3c268ecaeb69a921542e8ff71d0c85346"}, + {file = "sigmf-1.8.0-py3-none-any.whl", hash = "sha256:f233ab04344fa3e42170926a646f7e53edd7edc65fcda42eb3d7efaf8a2e8263"}, + {file = "sigmf-1.8.0.tar.gz", hash = "sha256:91e10cb046499639e5f961d66a24c17a33ff76fc98df892eab0953cc9d659a50"}, ] [package.dependencies] +defusedxml = "*" jsonschema = "*" numpy = "*" @@ -3036,6 +3236,7 @@ description = "Computer algebra system (CAS) in Python" optional = false python-versions = ">=3.9" groups = ["server", "test"] +markers = "python_version >= \"3.11\"" files = [ {file = "sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5"}, {file = "sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517"}, @@ -3131,14 +3332,14 @@ files = [ [[package]] name = "tox" -version = "4.52.0" +version = "4.53.0" description = "tox is a generic virtualenv management and test command line tool" optional = false python-versions = ">=3.10" groups = ["test"] files = [ - {file = "tox-4.52.0-py3-none-any.whl", hash = "sha256:624d8ea4a8c6d5e8d168eedf0e318d736fb22e83ca83137d001ac65ffdec46fd"}, - {file = "tox-4.52.0.tar.gz", hash = "sha256:6054abf5c8b61d58776fbec991f9bf0d34bb883862beb93d2fe55601ef3977c9"}, + {file = "tox-4.53.0-py3-none-any.whl", hash = "sha256:cc4e716d18c4889aa179d785175c438fa60c35deef20ce689ec288d8fb656096"}, + {file = "tox-4.53.0.tar.gz", hash = "sha256:62c780e42f87d34ee60f2ea20342156253794fdcbd6885fd797d98ee05009f22"}, ] [package.dependencies] @@ -3149,7 +3350,7 @@ packaging = ">=26" platformdirs = ">=4.9.4" pluggy = ">=1.6" pyproject-api = ">=1.10" -python-discovery = ">=1.2.1" +python-discovery = ">=1.2.2" tomli = {version = ">=2.4", markers = "python_version < \"3.11\""} tomli-w = ">=1.2" typing-extensions = {version = ">=4.15", markers = "python_version < \"3.11\""} @@ -3169,7 +3370,7 @@ files = [ {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, {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]] name = "typing-inspection" @@ -3188,14 +3389,14 @@ typing-extensions = ">=4.12.0" [[package]] name = "tzdata" -version = "2025.3" +version = "2026.1" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" groups = ["main"] files = [ - {file = "tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1"}, - {file = "tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7"}, + {file = "tzdata-2026.1-py2.py3-none-any.whl", hash = "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9"}, + {file = "tzdata-2026.1.tar.gz", hash = "sha256:67658a1903c75917309e753fdc349ac0efd8c27db7a0cb406a25be4840f87f98"}, ] [[package]] @@ -3218,14 +3419,14 @@ zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] [[package]] name = "uvicorn" -version = "0.42.0" +version = "0.44.0" description = "The lightning-fast ASGI server." optional = false python-versions = ">=3.10" groups = ["docs", "server", "test"] files = [ - {file = "uvicorn-0.42.0-py3-none-any.whl", hash = "sha256:96c30f5c7abe6f74ae8900a70e92b85ad6613b745d4879eb9b16ccad15645359"}, - {file = "uvicorn-0.42.0.tar.gz", hash = "sha256:9b1f190ce15a2dd22e7758651d9b6d12df09a13d51ba5bf4fc33c383a48e1775"}, + {file = "uvicorn-0.44.0-py3-none-any.whl", hash = "sha256:ce937c99a2cc70279556967274414c087888e8cec9f9c94644dfca11bd3ced89"}, + {file = "uvicorn-0.44.0.tar.gz", hash = "sha256:6c942071b68f07e178264b9152f1f16dfac5da85880c4ce06366a96d70d4f31e"}, ] [package.dependencies] @@ -3310,21 +3511,21 @@ test = ["aiohttp (>=3.10.5)", "flake8 (>=6.1,<7.0)", "mypy (>=0.800)", "psutil", [[package]] name = "virtualenv" -version = "21.2.0" +version = "21.2.4" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" groups = ["test"] files = [ - {file = "virtualenv-21.2.0-py3-none-any.whl", hash = "sha256:1bd755b504931164a5a496d217c014d098426cddc79363ad66ac78125f9d908f"}, - {file = "virtualenv-21.2.0.tar.gz", hash = "sha256:1720dc3a62ef5b443092e3f499228599045d7fea4c79199770499df8becf9098"}, + {file = "virtualenv-21.2.4-py3-none-any.whl", hash = "sha256:29d21e941795206138d0f22f4e45ff7050e5da6c6472299fb7103318763861ac"}, + {file = "virtualenv-21.2.4.tar.gz", hash = "sha256:b294ef68192638004d72524ce7ef303e9d0cf5a44c95ce2e54a7500a6381cada"}, ] [package.dependencies] distlib = ">=0.3.7,<1" filelock = {version = ">=3.24.2,<4", markers = "python_version >= \"3.10\""} platformdirs = ">=3.9.1,<5" -python-discovery = ">=1" +python-discovery = ">=1.2.2" typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.11\""} [[package]] @@ -3523,4 +3724,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.10" -content-hash = "b1e5ddd7284aecf49624e51740b7a4c31bc8d0e703c255126ba5d9b2a4a0e519" +content-hash = "720436f5f3d6651c298cf9fdc2f15ba40dcda6a70b09ba6aefa39ae119ba8ee0" diff --git a/pyproject.toml b/pyproject.toml index 8db3469..095f647 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] 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" license = { text = "AGPL-3.0-only" } readme = "README.md" @@ -49,7 +49,8 @@ dependencies = [ "pyzmq (>=27.1.0,<28.0.0)", "pyyaml (>=6.0.3,<7.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 @@ -87,7 +88,7 @@ pytest = "^8.0.0" tox = "^4.19.0" fastapi = ">=0.111,<1.0" 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" [tool.poetry.group.docs.dependencies] @@ -121,7 +122,7 @@ ria-agent = "ria_toolkit_oss.agent:main" [tool.poetry.group.server.dependencies] fastapi = ">=0.111,<1.0" uvicorn = {version = ">=0.29,<1.0", extras = ["standard"]} -onnxruntime = ">=1.17,<2.0" +onnxruntime = {version = ">=1.17,<2.0", python = ">=3.11"} [tool.black] line-length = 119 diff --git a/src/ria_toolkit_oss/agent.py b/src/ria_toolkit_oss/agent.py index bd4b3fc..6e15eb1 100644 --- a/src/ria_toolkit_oss/agent.py +++ b/src/ria_toolkit_oss/agent.py @@ -14,19 +14,40 @@ Usage:: [--device plutosdr] \\ [--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: 1. Registers with RIA Hub and receives a ``node_id``. 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). - 4. Executes received campaigns via :class:`ria_toolkit_oss.orchestration.executor.CampaignExecutor`. - 5. Uploads recordings to the hub via chunked POST, keeping each request - under 50 MB so it passes through Cloudflare without needing the bypass - subdomain. - 6. Deregisters cleanly on SIGINT / SIGTERM. + 3. Long-polls ``GET /composer/nodes/{id}/commands`` (30 s timeout). + 4. Dispatches received commands: + - ``run_campaign``: executes via CampaignExecutor, uploads recordings. + - ``load_model``: loads an ONNX fingerprint or detector model. + - ``start_inference``: opens the SDR, runs the inference loop, posts + 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 +import json import logging import math 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 _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 +_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._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: import ria_toolkit_oss @@ -114,6 +161,7 @@ class NodeAgent: self._command_loop() finally: self._stop.set() + self._stop_inference() self._deregister() # ------------------------------------------------------------------ @@ -121,13 +169,16 @@ class NodeAgent: # ------------------------------------------------------------------ def _register(self) -> None: + capabilities = ["campaign"] + if self._ort_available: + capabilities.append("inference") resp = self._post( - "/orchestrator/nodes/register", + "/composer/nodes/register", json={ "name": self.name, "sdr_device": self.sdr_device, "ria_toolkit_version": self._ria_version, - "capabilities": ["inference", "campaign"], + "capabilities": capabilities, }, timeout=15, ) @@ -139,7 +190,7 @@ class NodeAgent: if not self.node_id: return 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) except Exception as exc: logger.debug("Deregister failed (ignored on shutdown): %s", exc) @@ -151,7 +202,7 @@ class NodeAgent: def _heartbeat_loop(self) -> None: while not self._stop.wait(_HEARTBEAT_INTERVAL): 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: logger.warning("Heartbeat got 404 — hub lost registration, re-registering") self._register() @@ -166,7 +217,7 @@ class NodeAgent: while not self._stop.is_set(): try: resp = self._get( - f"/orchestrator/nodes/{self.node_id}/commands", + f"/composer/nodes/{self.node_id}/commands", timeout=_POLL_CLIENT_TIMEOUT, ) if resp.status_code == 204: @@ -200,6 +251,24 @@ class NodeAgent: daemon=True, name=f"campaign-{campaign_id[:8]}", ).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: logger.warning("Unknown command %r — ignored", command) @@ -232,6 +301,270 @@ class NodeAgent: logger.error("Campaign %s failed: %s", campaign_id[:8], 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) # ------------------------------------------------------------------ @@ -244,7 +577,7 @@ class NodeAgent: repo_owner, repo_name = output_repo.split("/", 1) 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: output_path: str | None = getattr(step, "output_path", None) @@ -286,7 +619,7 @@ class NodeAgent: payload["error"] = error try: resp = self._post( - f"/orchestrator/nodes/{self.node_id}/campaign-status", + f"/composer/nodes/{self.node_id}/campaign-status", json=payload, timeout=15, ) @@ -304,7 +637,6 @@ class NodeAgent: headers = {"X-API-Key": self.api_key} verify = not self.insecure - # Small files: single POST (unchanged endpoint, no assembly needed server-side). if size <= _DIRECT_THRESHOLD: with open(file_path, "rb") as fh: resp = _requests.post( @@ -318,7 +650,6 @@ class NodeAgent: resp.raise_for_status() return resp.json() - # Large files: chunked upload — each request is ≤ 50 MB. total_chunks = math.ceil(size / _CHUNK_SIZE) upload_id = str(uuid.uuid4()) chunk_url = base_url + "/chunk" @@ -339,18 +670,13 @@ class NodeAgent: chunk_url, headers=headers, files={"file": (filename, chunk, "application/octet-stream")}, - data={ - **metadata, - "upload_id": upload_id, - "chunk_index": i, - "total_chunks": total_chunks, - }, + data={**metadata, "upload_id": upload_id, "chunk_index": i, "total_chunks": total_chunks}, timeout=120, verify=verify, ) if not resp.ok: 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() 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]: """Return paths to both SigMF files (.sigmf-data and .sigmf-meta) for a recording.""" 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)] +# --------------------------------------------------------------------------- +# 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 # --------------------------------------------------------------------------- @@ -420,67 +800,94 @@ def main() -> None: "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( "--hub", - required=True, + default=None, metavar="URL", help="RIA Hub base URL, e.g. https://riahub.company.com", ) parser.add_argument( "--key", - required=True, + default=None, metavar="API_KEY", help="Shared API key (must match [wac] API_KEY in the hub's app.ini)", ) parser.add_argument( "--name", - required=True, + default=None, metavar="NAME", help='Human-readable name shown in the Target Node dropdown, e.g. "lab-bench-1"', ) parser.add_argument( "--device", - default="unknown", + default=None, metavar="SDR", 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" ), ) parser.add_argument( "--insecure", action="store_true", + default=None, help="Disable TLS certificate verification (dev/self-signed certs only)", ) parser.add_argument( "--log-level", - default="INFO", + default=None, choices=["DEBUG", "INFO", "WARNING", "ERROR"], help="Logging verbosity (default: INFO)", ) 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( - level=getattr(logging, args.log_level), + level=getattr(logging, log_level), format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S", stream=sys.stderr, ) - # Warn loudly if --insecure is used outside of development. - if args.insecure: + if insecure: logger.warning( "--insecure disables TLS certificate verification. " "Only use this for local development with self-signed certs." ) agent = NodeAgent( - hub_url=args.hub, - api_key=args.key, - name=args.name, - sdr_device=args.device, - insecure=args.insecure, + hub_url=hub, + api_key=key, + name=name, + sdr_device=device, + insecure=insecure, ) agent.run() diff --git a/src/ria_toolkit_oss/orchestration/campaign.py b/src/ria_toolkit_oss/orchestration/campaign.py index 9d96c96..027c33f 100644 --- a/src/ria_toolkit_oss/orchestration/campaign.py +++ b/src/ria_toolkit_oss/orchestration/campaign.py @@ -223,13 +223,16 @@ class TransmitterConfig: id: str type: str # "wifi", "bluetooth", "sdr", "external" - control_method: str # "external_script" | "sdr" + control_method: str # "external_script" | "sdr" | "sdr_remote" schedule: list[CaptureStep] # For external_script control script: Optional[str] = None # path to control script 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 def from_dict(cls, d: dict) -> "TransmitterConfig": schedule = [CaptureStep.from_dict(s) for s in d.get("schedule", [])] @@ -240,6 +243,7 @@ class TransmitterConfig: schedule=schedule, script=d.get("script"), device=d.get("device"), + sdr_remote=d.get("sdr_remote"), ) diff --git a/src/ria_toolkit_oss/orchestration/executor.py b/src/ria_toolkit_oss/orchestration/executor.py index 629c0d8..1bdd4d8 100644 --- a/src/ria_toolkit_oss/orchestration/executor.py +++ b/src/ria_toolkit_oss/orchestration/executor.py @@ -196,6 +196,7 @@ class CampaignExecutor: self.config = config self.progress_cb = progress_cb self._sdr = None + self._remote_tx_controllers: dict = {} if verbose: logging.basicConfig(level=logging.DEBUG) @@ -222,6 +223,7 @@ class CampaignExecutor: ) self._init_sdr() + self._init_remote_tx_controllers() try: total = self.config.total_steps() step_index = 0 @@ -248,6 +250,7 @@ class CampaignExecutor: ) finally: self._close_sdr() + self._close_remote_tx_controllers() result.end_time = time.time() logger.info( @@ -287,6 +290,41 @@ class CampaignExecutor: logger.warning(f"SDR close error: {e}") 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: """Capture ``duration_s`` seconds of IQ samples.""" 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 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 not transmitter.script: @@ -384,6 +423,20 @@ class CampaignExecutor: elif transmitter.control_method == "sdr": 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: logger.warning(f"Unknown control method '{transmitter.control_method}' — skipping") @@ -391,6 +444,7 @@ class CampaignExecutor: """Signal the transmitter to stop. Calls ``